Illustration de l'article

Dynamic diagrams with ROQ

In SVG, the force resides.

22 07 2026 6 min de lecture
🌐 Languages:

Since the ROQ v2.1.5 release, if you choose SVG as the rendering format, the plugin diagram injects the generated SVG directly into the page (when it used to do it through datauri).
That’s very good new because SVG isn’t just an image that zooms well.

No, Scalable Vector Graphics is a data format based on XML, which has the nice properties of being able to be styled via CSS, AND manipulated directly from the DOM.

Below you can see, for example, a view of the ROQ processing pipeline for building your static sites.
You will notice that it is clickable; indeed, when you select on a box, all its children and their links will be highlighted.

Here you are \o/ !

Roq FrontMatter processing pipeline (roq-frontmatter)Roq FrontMatter processing pipeline (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

How the hell does this work ?

It’s a 2 step process.

On page load, the following script is executed :

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 Classes used by plantuml for activity diagrams.
2 Link classes.
3 Internal representation.

Then when for later use :

 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 If a diagram box is clicked
2 Else every element is reset to it’s normal design
3 CSS classes a reassigned to each element to highlight or dim them

And we have 8 lines of CSS to affect display

/* 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;
}

References

The all javascript file to be placed in 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