Des diagrammes dynamiques avec ROQ
Des diagrammes dynamiques avec ROQ
Dans le svg, la force réside.
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.
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
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();
}
});
});
-
Image d’illustration: https://fr.wikipedia.org/wiki/Scalable_Vector_Graphics
Jérôme Tama
Techlead/Architecte/Compagnon du devoir