Illustration de l'article

Des diagrammes dynamiques avec ROQ

Dans le svg, la force réside.

22 07 2026 6 min de lecture
🌐 Languages:

Depuis la release v2.1.5 de ROQ, si vous choisissez le SVG comment format de rendu, le plugin diagram injecte directement le SVG produit dans la page.
Et le SVG, ce n’est pas juste une image qui zoom bien.

Non le Scalable Vector Graphics est un format de donnée fondé sur XML, qui a le bon goût de pouvoir être stylisé via CSS, ET manipulé directement depuis le DOM.

Ci-dessous on peut par exemple voir une vue du pipeline de traitement de construction de vos sites statics pas ROQ.
Vous remarquerez qu’il est clickable, en effet, au clique sur une boite, toutes ses filles et leurs liens seront mises en avant.

Ce traitement repose sur du styling CSS et un bout de code JS pour détecter le click et assignée les classes CSS idoines.

Et woilà \o/ !

Pipeline de Build de Roq FrontMatter (roq-frontmatter)Pipeline de Build de Roq FrontMatter (roq-frontmatter)Sources & ConfigurationStep 0: Setup(RoqFrontMatterStep0SetupProcessor)Step 1: Scan(RoqFrontMatterStep1ScanProcessor)Step 2: Assemble(RoqFrontMatterStep2AssembleProcessor)Step 3: Data(RoqFrontMatterStep3DataProcessor)Step 4: Publish(RoqFrontMatterStep4PublishProcessor)Step 5: Record(RoqFrontMatterStep5RecordProcessor)Step 6: Bind(RoqFrontMatterStep6BindProcessor)Downstream Integration (Runtime / Export)Fichiers de Contenu(.md, .html, .adoc)Gabarits de Layouts(templates/layouts/)Fichiers de Données(.yaml, .json)Fichiers Statiques(public/, static/)RoqSiteConfig& QuteConfigInitialisation des règlesScan et classificationdes fichiersApplication des modifierset génération intermédiaireRésolution des URLs, dateset héritage des layoutsPagination et traitementdes collectionsEnregistrement des CDI Beanset initialisation des routes Vert.xÉcriture des templates Quteet validation de typesMoteur de Template QuteRouteur Vert.x HTTPGénérateur Roq (SSG)RoqFrontMatterDataModificationBuildItemRoqFrontMatterHeaderParserBuildItemRoqFrontMatterScannedContentBuildItemRoqFrontMatterScannedLayoutBuildItemRoqFrontMatterRawPageBuildItemRoqFrontMatterRawLayoutBuildItemRoqFrontMatterPageTemplateBuildItemRoqFrontMatterLayoutTemplateBuildItemRoqFrontMatterDocumentBuildItemRoqFrontMatterPaginatePageBuildItemRoqFrontMatterStaticFileBuildItemRoqFrontMatterPublishDocumentPageBuildItemRoqFrontMatterPublishNormalPageBuildItemRoqFrontMatterOutputBuildItemSyntheticBeanBuildItem (Site, Sources)Parser YAMLModifications (Drafts, Escape)Pages Statiques & NormalesInjection CDI de 'site' et 'sources'Routes HTTP du siteFichiers de gabarits & validation de typesSélection de chemins & ressources statiques

Mais comment ça marche ??

Il s’agit d’un processus en 2 étape.

Au chargement de la page :

const entities = document.querySelectorAll('.entity,.cluster');(1)
const links = document.querySelectorAll('.link');(2)

// Build entity IDs set for quick existence check
const entityIds = new Set(Array.from(entities).map(e => e.id));

// Build the directed adjacency graph list (only outgoing: from data-entity-1 to data-entity-2)
const adj = {};
links.forEach(link => {(3)
    const ent1 = link.getAttribute('data-entity-1');
    const ent2 = link.getAttribute('data-entity-2');
    if (ent1 && ent2 && entityIds.has(ent1) && entityIds.has(ent2)) {
        if (!adj[ent1]) adj[ent1] = [];
        // Only follow outgoing links
        adj[ent1].push({ neighborId: ent2, linkElement: link });
    }
});
1 Les classes assignées par plantuml dans un diagramme d’activité.
2 Les classes de liens.
3 Constitution de la représentation interne.

Dans le cour de la lecture :

 svg.addEventListener('click', (event) => {
        let entityEl = event.target.closest('.entity');
        if(!entityEl) {
            entityEl = event.target.closest('.cluster');
        }

        if (entityEl) { (1)
            // We clicked on an entity
            event.stopPropagation(); // prevent document-level click handler
            const clickedId = entityEl.id;

            if (activeRootId === clickedId) { (2)
                // Clicking the already active root deselects it
                clearSelection();
            } else {
                // Otherwise highlight its network
                highlightNetwork(clickedId);
            }
        } else {
            // Clicking on the SVG background (or other non-entity element) clears selection
            clearSelection(); (3)
        }
    });
1 Si le click a eu lieu sur une boîte du diagramme.
2 On assigne les classes CSS à tous les éléments nécessaires.
3 Sinon on reaffiche tout normalement.

Et 8 lignes de CSS:

/* When an entity is selected, dim everything else */
svg.has-selection .entity:not(.highlighted),
svg.has-selection .cluster:not(.highlighted),
svg.has-selection .link:not(.highlighted) {
    opacity: 0.15;
}

/* Keep highlighted elements fully visible */
svg.has-selection .entity.highlighted,
svg.has-selection .cluster.highlighted,
svg.has-selection .link.highlighted {
    opacity: 1;
}

Références

Fichier à placer dans le dossier scr/main/resources/web/app
diagram.js
document.addEventListener('DOMContentLoaded', () => {
    const svg = document.querySelector('svg');
    if (!svg) return;

    const entities = document.querySelectorAll('.entity,.cluster');
    const links = document.querySelectorAll('.link');

    // Build entity IDs set for quick existence check
    const entityIds = new Set(Array.from(entities).map(e => e.id));

    // Build the directed adjacency graph list (only outgoing: from data-entity-1 to data-entity-2)
    const adj = {};
    links.forEach(link => {
        const ent1 = link.getAttribute('data-entity-1');
        const ent2 = link.getAttribute('data-entity-2');
        if (ent1 && ent2 && entityIds.has(ent1) && entityIds.has(ent2)) {
            if (!adj[ent1]) adj[ent1] = [];
            // Only follow outgoing links
            adj[ent1].push({ neighborId: ent2, linkElement: link });
        }
    });

    let activeRootId = null;

    // Helper to clear all selection states
    function clearSelection() {
        svg.classList.remove('has-selection');
        entities.forEach(ent => {
            ent.classList.remove('highlighted', 'active-root');
        });
        links.forEach(lnk => {
            lnk.classList.remove('highlighted');
        });
        activeRootId = null;
    }

    // Helper to perform BFS and highlight connected elements
    function highlightNetwork(startNodeId) {
        // Clear previous state
        entities.forEach(ent => ent.classList.remove('highlighted', 'active-root'));
        links.forEach(lnk => lnk.classList.remove('highlighted'));

        const visitedNodes = new Set();
        const visitedLinks = new Set();
        const queue = [startNodeId];
        visitedNodes.add(startNodeId);

        while (queue.length > 0) {
            const current = queue.shift();
            const connections = adj[current] || [];
            for (const conn of connections) {
                if (!visitedLinks.has(conn.linkElement)) {
                    visitedLinks.add(conn.linkElement);
                }
                if (!visitedNodes.has(conn.neighborId)) {
                    visitedNodes.add(conn.neighborId);
                    queue.push(conn.neighborId);
                }
            }
        }

        // Apply highlighted class
        visitedNodes.forEach(nodeId => {
            const nodeEl = document.getElementById(nodeId);
            if (nodeEl) {
                nodeEl.classList.add('highlighted');
            }
        });

        visitedLinks.forEach(linkEl => {
            linkEl.classList.add('highlighted');
        });

        // Mark the active root
        const rootEl = document.getElementById(startNodeId);
        if (rootEl) {
            rootEl.classList.add('active-root');
        }

        svg.classList.add('has-selection');
        activeRootId = startNodeId;
    }

    // Add click listeners
    svg.addEventListener('click', (event) => {
        let entityEl = event.target.closest('.entity');
        if(!entityEl) {
            entityEl = event.target.closest('.cluster');
        }

        if (entityEl) {
            // We clicked on an entity
            event.stopPropagation(); // prevent document-level click handler
            const clickedId = entityEl.id;

            if (activeRootId === clickedId) {
                // Clicking the already active root deselects it
                clearSelection();
            } else {
                // Otherwise highlight its network
                highlightNetwork(clickedId);
            }
        } else {
            // Clicking on the SVG background (or other non-entity element) clears selection
            clearSelection();
        }
    });

    // Also support clicking outside the SVG to clear selection
    document.addEventListener('click', (event) => {
        if (!svg.contains(event.target)) {
            clearSelection();
        }
    });
});
author-jtama

Jérôme Tama

Techlead/Architecte/Compagnon du devoir