diagrams-js
diagrams-js/diagram-diff
coreVisual diff for diagrams. Compare two versions of a diagram to see added, removed, and modified elements with color-coded side-by-side rendering. Perfect for code reviews, documentation, and architecture evolution tracking.
This skill builds on diagrams-js/getting-started and diagrams-js/json-serialization. Read them first for foundational concepts.
diagrams-js β Diagram Diff
Compare two versions of a diagram visually. The diff feature highlights added, removed, and modified elements with color-coded overlays in a side-by-side view.
Use Cases
- Code reviews: See exactly what infrastructure changed in a PR
- Documentation: Show evolution of system architecture
- Auditing: Track what was added or removed between deployments
Core Patterns
Compute and Render a Diff
import { computeDiff, renderDiff } from "diagrams-js";
import { writeFileSync } from "fs";
const before = JSON.parse(await fs.readFile("arch-v1.json", "utf8"));
const after = JSON.parse(await fs.readFile("arch-v2.json", "utf8"));
// Compute the diff
const diff = computeDiff(before, after);
console.log(diff.summary); // { added: 2, removed: 1, modified: 3, unchanged: 5 }
// Render as self-contained HTML
const html = await renderDiff(diff, before, after, { format: "html" });
await fs.writeFile("diff.html", html);Use Convenience Static Methods
import { Diagram } from "diagrams-js";
// Just compute
const diff = Diagram.diff(before, after);
// Compute and render in one call
const html = await Diagram.renderDiff(before, after, { format: "html" });Understanding the Diff Result
const diff = computeDiff(before, after);
// Summary counts
console.log(diff.summary);
// { added: 2, removed: 1, modified: 3, unchanged: 5 }
// Check specific node
const nodeDiff = diff.nodes.get("web-server");
if (nodeDiff?.kind === "modified") {
console.log("Changes:", nodeDiff.changes);
// ["label: \"Web\" β \"Web v2\"", "type: \"EC2\" β \"Lambda\""]
}Render Options
const html = await renderDiff(diff, before, after, {
format: "html", // "html" or "svg"
theme: "light", // "light" or "dark"
layout: "side-by-side", // "side-by-side" or "stacked"
showUnchanged: "show", // "show" (default), "dim", or "hide"
showLegend: true, // Show color legend
showSummary: true, // Show change counts
hoverDetails: true, // Show tooltips on hover
});Change Types
| Kind | Color | Description |
|---|---|---|
| added | π’ Green | New element in after version |
| removed | π΄ Red | Element deleted in after version |
| modified | π Amber | Properties changed (including label changes) |
| unchanged | βͺ Gray | No changes (shown by default) |
Node Matching Algorithm
Nodes are matched using a three-phase approach:
Phase 1: Fingerprint Matching
Nodes with identical (label, provider, type, resource) are matched directly as unchanged or modified.
// Before: { label: "Web Server", provider: "aws", type: "compute", resource: "EC2" }
// After: { label: "Web Server", provider: "aws", type: "compute", resource: "EC2" }
// Result: unchanged
// Before: { label: "Web Server", provider: "aws", type: "compute", resource: "EC2" }
// After: { label: "Web Server v2", provider: "aws", type: "compute", resource: "EC2" }
// Result: modified (label change)Phase 2: Label Fingerprint + Edge Connectivity
Unmatched nodes with the same (provider, type, resource) are matched using edge connectivity:
- If edge connectivity patterns match β paired and marked as modified (label change)
- If edge connectivity differs β treated as separate nodes (removed + added)
// Example: worker4 β worker1 with same connectivity
// Before: { label: "worker4", provider: "aws", type: "EC2", connectsTo: ["lb"] }
// After: { label: "worker1", provider: "aws", type: "EC2", connectsTo: ["lb"] }
// Result: modified (label: "worker4" β "worker1")
// Example: Different connectivity = different nodes
// Before: { label: "worker4", provider: "aws", type: "EC2", connectsTo: ["lb"] }
// After: { label: "worker1", provider: "aws", type: "EC2", connectsTo: ["db"] }
// Result: removed (worker4) + added (worker1)Phase 3: Simple Label Fingerprint
Remaining unmatched nodes are matched 1:1 by (provider, type, resource):
- Same label β unchanged
- Different labels β modified
// Before: { label: "worker1", provider: "aws", type: "EC2" }
// After: { label: "worker2", provider: "aws", type: "EC2" }
// Result: modified (label: "worker1" β "worker2")Diff Options
const diff = computeDiff(before, after, {
// Ignore layout/position changes (default: true)
ignore: { position: true },
// Ignore all metadata
ignore: { metadata: true },
// Ignore specific metadata keys
ignore: { metadata: ["cpu", "memory"] },
// Ignore specific Graphviz attributes
ignore: { attrs: ["color", "fillcolor"] },
// Custom matching function
matchNodes: (a, b) => a.metadata?.resourceId === b.metadata?.resourceId,
});Metadata Changes
Diagram-level changes (name, theme, direction, etc.) are tracked separately:
const diff = computeDiff(before, after);
if (diff.meta.name) {
console.log(`Renamed: "${diff.meta.name.before}" β "${diff.meta.name.after}"`);
}
if (diff.meta.theme) {
console.log(`Theme changed: ${diff.meta.theme.before} β ${diff.meta.theme.after}`);
}These appear in the HTML output under "Diagram Options Changed".
CLI Tool
Use @diagrams-js/cli for git workflows:
# Install globally
npm install -g @diagrams-js/cli
# Compare with HEAD (default: diagram-diff.html)
diagrams diff show HEAD diagram.json
# Compare with explicit output
diagrams diff show HEAD diagram.json -o diff.html
# Output to stdout
diagrams diff show HEAD diagram.json --stdout
# Compare branches
diagrams diff show main...feature diagram.json -f html -o diff.html
# List changed files
diagrams diff list HEAD
# Batch diff all changed files
diagrams diff batch main...feature -o ./diffsCommon Mistakes
CRITICAL Forgetting to await renderDiff
Wrong:
const html = renderDiff(diff, before, after); // Missing await
fs.writeFileSync("diff.html", html); // html is Promise, not stringCorrect:
const html = await renderDiff(diff, before, after);
fs.writeFileSync("diff.html", html);renderDiff is async because it renders both diagrams to SVG.
HIGH Expecting modified nodes without matching criteria
Wrong:
// Before: { label: "worker4", provider: "aws", type: "EC2" }
// After: { label: "worker1", provider: "aws", type: "EC2" }
// Different labels, no edge connectivity match β detected as removed + addedCorrect:
// Before: { label: "worker1", provider: "aws", type: "EC2" }
// After: { label: "worker1-prod", provider: "aws", type: "EC2" }
// Same provider/type with matching connectivity β detected as modifiedKeep the same (provider, type, resource) and similar edge connectivity for modification detection.
MEDIUM Confusing modified with removed+added
- modified: Same (provider, type, resource) with matching edge connectivity, but different label
- removed + added: Different labels without matching connectivity, or different (provider, type, resource)
if (nodeDiff.kind === "modified") {
// Label changed but same entity
console.log("Renamed:", nodeDiff.changes);
}
if (nodeDiff.kind === "removed") {
// Node was deleted
console.log("Removed:", nodeDiff.before?.label);
}
if (nodeDiff.kind === "added") {
// Node is new
console.log("Added:", nodeDiff.after?.label);
}Best Practices
Use Stable Node Identifiers
Node matching relies on (label, provider, type, resource), not IDs:
// Good: Consistent identifiers across versions
const web = diagram.add(EC2("Web Server"));
// Good: Meaningful labels that persist
const api = diagram.add(Lambda("API Handler"));Export JSON for Versioning
Save diagram.toJSON() alongside your code:
// In your build/CI
await diagram.save("architecture.json", { format: "json" });Focus on Meaningful Changes
Ignore cosmetic changes:
computeDiff(before, after, {
ignore: {
position: true, // Layout changes
attrs: ["color"], // Visual styling
metadata: ["cost"], // Non-structural metadata
},
});Complete Example
import { Diagram, computeDiff, renderDiff } from "diagrams-js";
import { EC2, Lambda } from "diagrams-js/aws/compute";
import { RDS } from "diagrams-js/aws/database";
import { S3 } from "diagrams-js/aws/storage";
// Version 1: Traditional architecture
const v1 = Diagram("Architecture v1", { direction: "TB" });
const web1 = v1.add(EC2("Web Server"));
const db1 = v1.add(RDS("Database"));
web1.to(db1);
// Version 2: Serverless migration
const v2 = Diagram("Architecture v2", { direction: "TB" });
const web2 = v2.add(Lambda("API Handler")); // Changed: EC2 β Lambda
const db2 = v2.add(RDS("Database")); // Unchanged
const storage = v2.add(S3("Assets")); // Added
web2.to(db2);
web2.to(storage);
// Compute diff
const diff = computeDiff(v1.toJSON(), v2.toJSON());
console.log(diff.summary);
// { added: 1, removed: 0, modified: 1, unchanged: 1 }
// Render visual diff
const html = await renderDiff(diff, v1.toJSON(), v2.toJSON(), {
format: "html",
theme: "light",
showUnchanged: "show", // or "dim" to dim unchanged, "hide" to hide them
});
await fs.writeFile("architecture-diff.html", html);The HTML shows:
- π’ Green: New S3 "Assets" node
- π Amber: Changed EC2 β Lambda node
- βͺ Unchanged: RDS node (shown normally, use showUnchanged: "dim" to dim)
See Also
- diagrams-js/json-serialization β Export diagrams to JSON
- diagrams-js/rendering-export β General rendering options
- diagrams-js/diagram-configuration β Diagram options (name, theme, direction)