Feature: Interactive graph [as html page]#333
Feature: Interactive graph [as html page]#333dzianis-dashkevich wants to merge 7 commits intopahen:masterfrom
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR adds an interactive HTML graph feature to madge, allowing users to generate dependency graphs as interactive web pages instead of static images. The feature provides visual controls for selecting and highlighting nodes and edges in the dependency graph.
- Adds new interactive HTML output format with click and keyboard controls
- Implements CSS styling and JavaScript functionality for graph interaction
- Integrates the interactive feature into the existing API and CLI
Reviewed Changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/interactive/templates/styles.css | CSS styles for interactive graph visualization with selection states |
| lib/interactive/templates/scripts.js | JavaScript functionality for node/edge selection and interaction |
| lib/interactive/index.js | Core module for generating interactive HTML from SVG graphs |
| lib/graph.js | Adds interactive function and refactors svg function for reuse |
| lib/api.js | Adds interactive method to the main Madge API class |
| README.md | Documentation for the new interactive feature and controls |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| svgEl.insertAdjacentHTML('afterbegin', `<linearGradient id="dualDirection"> | ||
| <stop offset="0%" stop-color="var(--color-from)" /> | ||
| <stop offset="100%" stop-color="var(--color-to)" /> | ||
| </linearGradient>`); | ||
|
|
There was a problem hiding this comment.
The linear gradient is inserted as raw HTML string. Consider creating the gradient elements using DOM methods for better maintainability and to avoid potential XSS issues.
| svgEl.insertAdjacentHTML('afterbegin', `<linearGradient id="dualDirection"> | |
| <stop offset="0%" stop-color="var(--color-from)" /> | |
| <stop offset="100%" stop-color="var(--color-to)" /> | |
| </linearGradient>`); | |
| const SVG_NS = "http://www.w3.org/2000/svg"; | |
| const linearGradient = document.createElementNS(SVG_NS, "linearGradient"); | |
| linearGradient.setAttribute("id", "dualDirection"); | |
| const stop1 = document.createElementNS(SVG_NS, "stop"); | |
| stop1.setAttribute("offset", "0%"); | |
| stop1.setAttribute("stop-color", "var(--color-from)"); | |
| linearGradient.appendChild(stop1); | |
| const stop2 = document.createElementNS(SVG_NS, "stop"); | |
| stop2.setAttribute("offset", "100%"); | |
| stop2.setAttribute("stop-color", "var(--color-to)"); | |
| linearGradient.appendChild(stop2); | |
| // Insert as the first child of the SVG (after <defs> if present, or create <defs>) | |
| let defs = svgEl.querySelector('defs'); | |
| if (!defs) { | |
| defs = document.createElementNS(SVG_NS, "defs"); | |
| svgEl.insertBefore(defs, svgEl.firstChild); | |
| } | |
| defs.appendChild(linearGradient); |
| let nodeList = [nodeTitleToNodeMap.get(from), nodeTitleToNodeMap.get(to)]; | ||
| edgesMap.set(title, nodeList); | ||
|
|
||
| nodeList = edgesMap.get(from) || []; | ||
| nodeList.push(edge); | ||
| edgesMap.set(from, nodeList); | ||
|
|
||
| nodeList = edgesMap.get(to) || []; | ||
| nodeList.push(edge); | ||
| edgesMap.set(to, nodeList); |
There was a problem hiding this comment.
The variable nodeList is being reused for different purposes which makes the code confusing. Use different variable names like connectedNodes, fromEdges, and toEdges to clarify intent.
| let nodeList = [nodeTitleToNodeMap.get(from), nodeTitleToNodeMap.get(to)]; | |
| edgesMap.set(title, nodeList); | |
| nodeList = edgesMap.get(from) || []; | |
| nodeList.push(edge); | |
| edgesMap.set(from, nodeList); | |
| nodeList = edgesMap.get(to) || []; | |
| nodeList.push(edge); | |
| edgesMap.set(to, nodeList); | |
| const connectedNodes = [nodeTitleToNodeMap.get(from), nodeTitleToNodeMap.get(to)]; | |
| edgesMap.set(title, connectedNodes); | |
| const fromEdges = edgesMap.get(from) || []; | |
| fromEdges.push(edge); | |
| edgesMap.set(from, fromEdges); | |
| const toEdges = edgesMap.get(to) || []; | |
| toEdges.push(edge); | |
| edgesMap.set(to, toEdges); |
| function toHtml(styles, content, scripts) { | ||
| return `<!DOCTYPE html> | ||
| <html lang="en" dir="ltr"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <title>Interactive Graph</title> | ||
| <style> | ||
| ${styles} | ||
| </style> | ||
| </head> | ||
| <body> | ||
| ${content} | ||
| <script> | ||
| ${scripts} | ||
| </script> | ||
| </body> | ||
| </html> | ||
| `; | ||
| } |
There was a problem hiding this comment.
Direct template interpolation of styles, content, and scripts parameters could lead to XSS vulnerabilities if the input contains malicious content. Consider sanitizing or validating these inputs before insertion.
Dear contributors,
Thank you for madge, this is a nice and easy-to-use library.
Recently, I forked madge and added an interactive feature for my personal needs.
I decided that this might be helpful for others as well. So here I am.
This feature is highly inspired by: https://github.com/sverweij/dependency-cruiser/blob/develop/src/cli/tools/wrap-stream-in-html.js
Please, check this deployed example based on the video.js open-source project (https://github.com/videojs/video.js):
https://dzianis-dashkevich.github.io/madge-interactive-examples/video-js-graph.html
Controls features (I added it to the README):
This example was generated by running the following command at the root of the project:
npx madge --interactive interactive-graph.html src/js/video.jsCurrently, it is pretty opinionated (eg: colors, controls, etc..), we can discuss potential changes, if you are interested.
Please, just close this PR, If you are not interested in this feature.
Regards,
Dzianis