Saya fork vscode-markdown-mermaid karena satu masalah sederhana: diagram Mermaid yang besar sulit untuk dilihat dan dinavigasi.
Masalah: Static Diagram Tidak User-Friendly
Problem Statement
Extension original hanya render SVG static. Untuk diagram complex seperti:
graph TD
A[Start] --> B[Process 1]
B --> C[Decision]
C -->|Yes| D[Process 2]
C -->|No| E[Process 3]
D --> F[Subprocess 1]
E --> G[Subprocess 2]
F --> H[End State 1]
G --> I[End State 2]
// ... 50+ more nodes
User tidak bisa:
Analisis: Kenapa Perlu Interactive?
Use Case Reality Check
Technical Gap
Original extension:
// Hanya render SVG, tanpa interactivity
const renderResult = await mermaid.render(diagramId, source);
writeOut(mermaidContainer, renderResult.svg);
Missing: Event handling untuk user interactions.
Solusi: Add Interactive Layer
Approach: Wrapper + Transform Pattern
Instead of modifying SVG directly, saya buat wrapper yang handle transforms:
class ContainerZoomPan {
private readonly wrapper: HTMLDivElement;
private scale = 1;
private offset: Point = { x: 0, y: 0 };
private createWrapper(): HTMLDivElement {
const wrapper = document.createElement('div');
wrapper.className = 'zoom-pan-wrapper';
Object.assign(wrapper.style, {
width: '100%',
height: '100%',
transformOrigin: '0 0',
});
wrapper.appendChild(this.svg); // SVG tetap unchanged
return wrapper;
}
}
Why this works: SVG keeps original dimensions, transforms apply pada container level.
Implementation: Event-Driven Interactions
Zoom dengan Mouse Wheel
private onWheel = (evt: WheelEvent): void => {
if (!evt.ctrlKey && !evt.metaKey) return; // Only with modifier keys
evt.preventDefault();
const rect = this.container.getBoundingClientRect();
const point: Point = {
x: evt.clientX - rect.left, // Mouse position relative to container
y: evt.clientY - rect.top,
};
const direction = evt.deltaY > 0 ? -1 : 1;
const factor = 1 + direction * this.config.zoomStep;
this.zoom(factor, point);
};
Key insight: Modifier keys prevent conflict dengan normal document scrolling.
Pan dengan Drag
private onMouseDown = (evt: MouseEvent): void => {
if (evt.button !== 0 || (!evt.ctrlKey && !evt.metaKey)) return;
this.isPanning = true;
this.startPoint = { x: evt.clientX, y: evt.clientY };
this.container.style.cursor = 'grabbing';
};
private onMouseMove = (evt: MouseEvent): void => {
if (!this.isPanning) return;
const dx = evt.clientX - this.startPoint.x;
const dy = evt.clientY - this.startPoint.y;
this.offset = { x: this.offset.x + dx, y: this.offset.y + dy };
this.startPoint = { x: evt.clientX, y: evt.clientY };
this.updateTransform();
};
Delta calculation: Track mouse movement untuk smooth panning experience.
Mathematical Solution: Zoom with Center Preservation
Challenge: Saat zoom, user expect cursor position tetap di tempat yang sama.
private zoom(factor: number, centre: Point): void {
const newScale = clamp(this.scale * factor, this.config.minZoom, this.config.maxZoom);
if (newScale === this.scale) return;
// Calculate new offset untuk maintain center point
const ratio = newScale / this.scale;
this.offset = {
x: centre.x - (centre.x - this.offset.x) * ratio,
y: centre.y - (centre.y - this.offset.y) * ratio,
};
this.scale = newScale;
this.updateTransform();
}
Math explanation: Formula centre.x - (centre.x - this.offset.x) * ratio keeps zoom centered pada cursor position.
Memory Management Solution
Problem: VS Code extension environment memiliki hot reload, bisa cause memory leaks.
Solution: WeakMap untuk automatic cleanup.
const INSTANCES = new WeakMap();
export function initializeContainerZoomPan(
svg: SVGElement,
container: HTMLElement,
config: ContainerZoomPanConfig = {}
): void {
destroyContainerZoomPan(container); // Cleanup existing
INSTANCES.set(container, new ContainerZoomPan(svg, container, config));
}
export function destroyContainerZoomPan(container: HTMLElement): void {
INSTANCES.get(container)?.destroy();
INSTANCES.delete(container);
}
WeakMap benefit: Automatic garbage collection saat container elements removed.
Integration Solution
Modify original rendering flow di index.ts:
export async function renderMermaidBlocksInElement(
root: HTMLElement,
writeOut: (mermaidContainer: HTMLElement, content: string) => void
): Promise {
// NEW: Cleanup existing zoom instances
for (const container of root.querySelectorAll('.mermaid')) {
destroyContainerZoomPan(container);
}
// Get configuration
const config = getZoomPanConfig();
// Process each diagram
for (const mermaidContainer of root.querySelectorAll('.mermaid')) {
const renderPromise = renderMermaidElement(mermaidContainer, writeOut).p;
renderPromises.push(renderPromise.then(() => {
const svg = mermaidContainer.querySelector('svg');
if (svg && config) {
// NEW: Initialize zoom/pan after rendering
setTimeout(() => {
initializeContainerZoomPan(svg, mermaidContainer, config);
}, 50);
}
}));
}
}
Integration points:
Bug Fix: Safe Configuration Parsing
Problem discovered: Invalid float values crash transform operations.
// BEFORE: Unsafe parsing
minZoom: parseFloat(host?.dataset.zoomMinZoom || `${DEFAULT_CFG.minZoom}`),
// AFTER: Safe parsing with fallback
const safeParseFloat = (value: string | undefined, defaultValue: number): number => {
const parsed = parseFloat(value || '');
return isNaN(parsed) ? defaultValue : parsed;
};
return {
minZoom: safeParseFloat(host?.dataset.zoomMinZoom, DEFAULT_CFG.minZoom),
maxZoom: safeParseFloat(host?.dataset.zoomMaxZoom, DEFAULT_CFG.maxZoom),
zoomStep: safeParseFloat(host?.dataset.zoomStep, DEFAULT_CFG.zoomStep),
};
Defensive programming: Handle edge cases dengan graceful fallback.
Results: Before vs After
Before (Original)
- Static SVG display
- No navigation for large diagrams
- User frustration dengan complex flowcharts
After (With Zoom/Pan)
- Ctrl+Scroll: Zoom in/out pada cursor position
- Ctrl+Drag: Pan untuk navigate
- Ctrl+Right-click: Reset ke original view
- Smooth 60fps interactions
- Memory-safe multiple diagram support
Performance Impact
Measurements
Optimization Techniques Used
User Experience Validation
Keyboard Shortcuts Rationale
Non-Intrusive Design
Implementation Lessons
What Worked Well
Challenges Overcome
Conclusion
Fork ini menyelesaikan exactly satu problem: make large Mermaid diagrams navigable. Implementation focused pada:
Total addition: 285 lines dalam containerZoomPan.ts + integration code. Result: Dramatically improved user experience untuk complex diagrams.
Next steps: Consider touch gestures untuk tablet support, atau minimap untuk very large diagrams.