Améliorer les diagrammes de mermaid avec Pan/Zoom et Export
Introduction
Package npm disponible: Cette mise en œuvre est maintenant disponible sous @mostlylucid/mermaidM SK2améliorations - une production - un paquet ready npmM SK2 Voir Publier les améliorations Mermaid en tant que paquet npm pour de plus amples renseignements sur l’utilisation dans vos projets.
Mermaid est un outil fantastique pour créer des diagrammes à partir du texte, mais le traçage par défaut peut être limité pour les diagrammes complexes . Les utilisateurs ne peuvent pas facilement zoomer en vue de voir les détails, tourner autour des grands diagrammesMSC4 ou les exporter pour la documentationM SK5 Dans cet articleMST6 JeMst7je vous montrerai comment j’ai amélioré les diagrammas Mermaid sur ce site avec un pan interactifM st8contrôles de zoomMstr9 visualisation d’une boîte lumineuse à écran completMSt10 et fonctionnalité d’exportation Mtr11formats PNG et SVG à la foisMrt12
Cette mise en oeuvre est de production-ready, manipule le mode noir gracieusementM SK2 et est résistant à l’interférence du chargeur de roquette CloudflareMSC3
À quoi cela ressemble
Par conséquent, ce que nous voulons c’est ce qui suit : une page agréable sur ( et un pop-out sur la mermaid de ). améliorer. signifie que les diagrammes ne prennent pas en compte les SCREENS, mais qu’ils sont toujours faciles à lire.

Le problème
Out of the box, Les diagrammes de mermaid ont plusieurs limites:
- Taille fixe - Les diagrammes de grande envergure sont soit coupés, soit réduits pour tenir compte
- Aucune interopérabilité - PeutM SK1 ne pas zoomer pour voir les détails ou faire tourner autour
- Aucune exportation - Les utilisateurs peuvent't sauvegarder les diagrammes pour usage externe
- Faible expérience mobile - Les petits écrans rendent les diagrammes complexes inutilisables
- Questions liées au changement de thème - Les diagrammes ne donnent pas toujours la correction de l’interruption entre mode lumineux et mode foncé
La solution
I' a mis en place un système d’amélioration complet qui ajoute
- Pan interactif/zoom utilisant la bibliothèque svg-pan-zoom
- boutons de contrôle flottants pour zoom in/outM SK1 reset, pan toggle , et exportation
- Case lumineuse à écran complet mode pour une meilleure visualisation
- Exportation de PNG et SVG fonctions
- Auto-adaptation à la charge pour que le diagramme entier soit visible par défaut Voir
- Display complet- largeur supprimer les contraintes de taille artificielle
Aperçu de l'architecture
La solution se compose de trois composants principaux:
graph TB
A[mermaid_theme_switch.js] -->|Initializes| B[Mermaid Diagrams]
B -->|Renders SVG| C[mermaid_enhancements.js]
C -->|Adds| D[Control Buttons]
C -->|Initializes| E[svg-pan-zoom]
D -->|Triggers| F[Pan/Zoom Actions]
D -->|Triggers| G[Export Functions]
D -->|Triggers| H[Fullscreen Lightbox]
Mise en œuvre
Installation des dépendances
Premièrement, installez les paquets npm requis:
npm install svg-pan-zoom html-to-image
Ces bibliothèques fournissent:
svg-pan-zoom- Functionnalité interactive de pan et de zoom pour les éléments SVGhtml-to-image- Functionnalité de l'exportation SVG/PNG
Module d'amélioration de la base
Le module principal d'amélioration (mermaid_enhancements.js) gère toutes les fonctions interactives.
Création de boutons de contrôle
Chaque diagramme reçoit un panneau de commande flottant avec des boutons pour toutes les actions:
function createControlButtons(container, diagramId) {
// Check if controls already exist
if (container.querySelector('.mermaid-controls')) {
return;
}
const controlsDiv = document.createElement('div');
controlsDiv.className = 'mermaid-controls';
const buttons = [
{ icon: 'bx-fullscreen', title: 'Fullscreen', action: 'fullscreen' },
{ icon: 'bx-zoom-in', title: 'Zoom In', action: 'zoomIn' },
{ icon: 'bx-zoom-out', title: 'Zoom Out', action: 'zoomOut' },
{ icon: 'bx-reset', title: 'Reset View', action: 'reset' },
{ icon: 'bx-move', title: 'Pan', action: 'pan' },
{ icon: 'bx-image', title: 'Export as PNG', action: 'exportPng' },
{ icon: 'bx-code-alt', title: 'Export as SVG', action: 'exportSvg' }
];
buttons.forEach(btn => {
const button = document.createElement('button');
button.className = `mermaid-control-btn bx ${btn.icon}`;
button.setAttribute('title', btn.title);
button.setAttribute('aria-label', btn.title);
button.setAttribute('data-action', btn.action);
button.setAttribute('data-diagram-id', diagramId);
controlsDiv.appendChild(button);
});
container.appendChild(controlsDiv);
}
Initialisation de Pan/Zoom
La bibliothèque svg-panM SK1zoom fournit une interaction efficace ,smooth
function initPanZoom(svgElement, diagramId) {
// Clean up existing instance if present
if (panZoomInstances.has(diagramId)) {
try {
panZoomInstances.get(diagramId).destroy();
} catch (e) {
console.warn('Failed to destroy existing pan-zoom instance:', e);
}
panZoomInstances.delete(diagramId);
}
try {
const panZoomInstance = svgPanZoom(svgElement, {
zoomEnabled: true,
controlIconsEnabled: false, // We use custom controls
fit: true,
center: true,
minZoom: 0.1,
maxZoom: 10,
zoomScaleSensitivity: 0.3,
dblClickZoomEnabled: true,
mouseWheelZoomEnabled: true,
preventMouseEventsDefault: true,
contain: false
});
panZoomInstances.set(diagramId, panZoomInstance);
return panZoomInstance;
} catch (error) {
console.error('Failed to initialize pan-zoom:', error);
return null;
}
}
Fonctionnalité d'exportation
Le système d'exportation préserve la qualité du diagramme et gère les formats PNG et SVG:
async function exportDiagram(container, format, diagramId) {
try {
const svgElement = container.querySelector('svg');
if (!svgElement) {
window.showToast && window.showToast('No diagram found to export', 3000, 'error');
return;
}
// Clone the SVG to avoid modifying the original
const clonedSvg = svgElement.cloneNode(true);
// Get the viewBox or calculate from bounding box
let viewBox = clonedSvg.getAttribute('viewBox');
if (!viewBox) {
const bbox = svgElement.getBBox();
viewBox = `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`;
clonedSvg.setAttribute('viewBox', viewBox);
}
// Parse viewBox to get dimensions
const [, , vbWidth, vbHeight] = viewBox.split(' ').map(Number);
// Set explicit dimensions based on viewBox for proper export
clonedSvg.setAttribute('width', vbWidth);
clonedSvg.setAttribute('height', vbHeight);
// Remove inline styles but keep viewBox
clonedSvg.removeAttribute('style');
clonedSvg.style.backgroundColor = 'transparent';
clonedSvg.style.maxWidth = 'none';
// Create temporary container
const tempDiv = document.createElement('div');
tempDiv.style.position = 'absolute';
tempDiv.style.left = '-9999px';
tempDiv.appendChild(clonedSvg);
document.body.appendChild(tempDiv);
let dataUrl;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `mermaid-diagram-${timestamp}`;
if (format === 'png') {
dataUrl = await toPng(clonedSvg, {
backgroundColor: 'white',
pixelRatio: 2 // Higher quality
});
downloadFile(dataUrl, `${filename}.png`);
} else {
dataUrl = await toSvg(clonedSvg, {
backgroundColor: 'transparent'
});
downloadFile(dataUrl, `${filename}.svg`);
}
// Clean up
document.body.removeChild(tempDiv);
window.showToast && window.showToast(`Diagram exported as ${format.toUpperCase()}`, 3000, 'success');
} catch (error) {
console.error('Failed to export diagram:', error);
window.showToast && window.showToast('Failed to export diagram', 3000, 'error');
}
}
Principales considérations à l’exportation:
- Préserver la boîte de vue - C'est essentiel pour capter l'ensemble du diagramme , pas seulement la partie visible
- Calculer les dimensions - Établir explicitement la largeurM SK1 la hauteur de la boîte de vue pour l'exportation cohérente
- Remplacer les transformations - Panne à bandesM SK1 transforme le zoom de sorte que l'exportation montre le diagramme complet
- Traitement du contexte - Fonds blanc pour PNG, transparent pour SVG
- Résolution élevée - Usage
pixelRatio: 2pour les exportations de PNG crisp
Case lumineuse à écran complet
La boîte lumineuse offre une expérience immersive de visualisation:
function openFullscreenLightbox(container, diagramId) {
const svgElement = container.querySelector('svg');
if (!svgElement) return;
// Create lightbox overlay
const lightbox = document.createElement('div');
lightbox.className = 'mermaid-lightbox';
lightbox.innerHTML = `
<div class="mermaid-lightbox-content">
<button class="mermaid-lightbox-close bx bx-x" aria-label="Close"></button>
<div class="mermaid-lightbox-diagram-wrapper">
<div class="mermaid-lightbox-diagram"></div>
</div>
</div>
`;
// Clone and prepare SVG
const clonedSvg = svgElement.cloneNode(true);
clonedSvg.removeAttribute('width');
clonedSvg.removeAttribute('height');
clonedSvg.style.width = '100%';
clonedSvg.style.height = '100%';
const diagramContainer = lightbox.querySelector('.mermaid-lightbox-diagram');
diagramContainer.appendChild(clonedSvg);
// Add controls to lightbox
const wrapper = lightbox.querySelector('.mermaid-lightbox-diagram-wrapper');
const lightboxDiagramId = `${diagramId}-lightbox`;
createControlButtons(wrapper, lightboxDiagramId);
document.body.appendChild(lightbox);
// Initialize pan-zoom after layout completes
setTimeout(() => {
const panZoom = initPanZoom(clonedSvg, lightboxDiagramId);
if (panZoom) {
panZoom.resize();
panZoom.fit();
panZoom.center();
}
}, 100);
// Close handlers
const closeLightbox = () => {
if (panZoomInstances.has(lightboxDiagramId)) {
try {
panZoomInstances.get(lightboxDiagramId).destroy();
} catch (e) {
console.warn('Failed to destroy lightbox pan-zoom:', e);
}
panZoomInstances.delete(lightboxDiagramId);
}
lightbox.remove();
};
lightbox.querySelector('.mermaid-lightbox-close').addEventListener('click', closeLightbox);
lightbox.addEventListener('click', (e) => {
if (e.target === lightbox) closeLightbox();
});
// ESC key to close
const escHandler = (e) => {
if (e.key === 'Escape') {
closeLightbox();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
}
Fonction principale d'amélioration
Cela relie tout ensemble et est appelé après Mermaid renders:
export function enhanceMermaidDiagrams() {
const diagrams = document.querySelectorAll('.mermaid[data-processed="true"]');
diagrams.forEach(diagram => {
const svgElement = diagram.querySelector('svg');
if (!svgElement) return;
// CRITICAL: Remove inline max-width constraint that Mermaid adds
svgElement.style.maxWidth = 'none';
// Wrap diagram with controls
const diagramId = wrapDiagramWithControls(diagram);
// Initialize pan/zoom and auto-fit
const panZoom = initPanZoom(svgElement, diagramId);
if (panZoom) {
// Fit diagram to container by default
setTimeout(() => {
panZoom.resize();
panZoom.fit();
panZoom.center();
}, 100);
}
});
// Set up event delegation for control buttons (only once)
if (!document.body.hasAttribute('data-mermaid-controls-initialized')) {
document.body.addEventListener('click', handleControlClick);
document.body.setAttribute('data-mermaid-controls-initialized', 'true');
}
}
Révision critique: Mermaid applique une ligne en ligne style="max-width: 1020px" à des éléments SVG, qui empêche l'affichage complet-width displayM SK2 La suppression de ce dernier est essentielle pour un comportement réceptif appropriéMSC3
Intégration du thème
Le commutateur thématique assure que les diagrammes re-render correctement lors de la transition entre modes lumineux et foncés:
import { enhanceMermaidDiagrams } from './mermaid_enhancements';
const loadMermaid = async (theme) => {
if (!window.mermaid) return;
try {
window.mermaid.initialize({
startOnLoad: false,
theme,
themeVariables: {
background: 'transparent'
}
});
await window.mermaid.run({
querySelector: elementSelector,
});
// Enhance diagrams after rendering completes
// Use requestAnimationFrame for better timing
await new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
enhanceMermaidDiagrams();
resolve();
});
});
});
} catch (err) {
console.error('Mermaid render error:', err);
}
};
Utilisation requestAnimationFrame deux fois s’assurer que le navigateur a terminé la peinture du SVG avant de nous essayer d’en améliorer.
Compatibilité du chargeur de roquettes Cloudflare
Cloudflare's Rocket Loader peut retarder l'exécution de JavaScriptM SK1break initialization. HereMSC3s the bulletproof solutionMNK4
// Wait for all dependencies to load with exponential backoff
function waitForDependencies(maxAttempts = 50) {
return new Promise((resolve) => {
let attempts = 0;
const checkDependencies = () => {
attempts++;
const depsReady =
typeof window.hljs !== 'undefined' &&
typeof window.mermaid !== 'undefined' &&
typeof window.Alpine !== 'undefined' &&
typeof window.htmx !== 'undefined';
if (depsReady) {
console.log('All dependencies loaded after', attempts, 'attempts');
// Start Alpine.js now that it's loaded
if (window.Alpine && !window.Alpine.version) {
try {
window.Alpine.start();
console.log('Alpine.js started');
} catch (err) {
console.error('Failed to start Alpine:', err);
}
}
resolve();
} else if (attempts >= maxAttempts) {
console.warn('Timeout waiting for dependencies');
resolve(); // Continue anyway
} else {
// Retry with exponential backoff
const delay = Math.min(50 * Math.pow(1.2, attempts), 500);
setTimeout(checkDependencies, delay);
}
};
checkDependencies();
});
}
// Robust initialization
async function safeInitialize() {
try {
await waitForDependencies();
if (document.readyState === 'loading') {
await new Promise(resolve => {
document.addEventListener('DOMContentLoaded', resolve, { once: true });
});
}
await initializePage();
} catch (err) {
console.error('Failed to initialize page:', err);
// Retry once after delay
setTimeout(() => {
initializePage().catch(e => console.error('Retry failed:', e));
}, 1000);
}
}
safeInitialize();
Veuillez également vous assurer que votre script principal data-cfasync="false" attribut pour l'exclure de Rocket Loader:
<script src="~/js/dist/main.js" type="module" asp-append-version="true" data-cfasync="false"></script>
Styling avec Tailwind/DaisyUI
Le CSS utilise les classes d'utilité Tailwind et le style personnalisé pour la polish:
/* Mermaid diagram wrapper */
.mermaid-wrapper {
@apply relative rounded-lg overflow-hidden w-full;
margin: 1rem 0;
}
.mermaid-wrapper .mermaid {
@apply m-0 w-full;
min-height: 500px;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.mermaid-wrapper .mermaid svg {
width: 100% !important;
height: auto !important;
min-height: 450px;
}
/* Control buttons */
.mermaid-controls {
@apply absolute top-2 right-2 flex gap-1 z-10;
background: rgba(255, 255, 255, 0.9);
border-radius: 0.5rem;
padding: 0.25rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.dark .mermaid-controls {
background: rgba(31, 41, 55, 0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.mermaid-control-btn {
@apply p-2 rounded cursor-pointer transition-all duration-200;
background: transparent;
border: none;
color: #4b5563;
font-size: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
}
.mermaid-control-btn:hover {
background: rgba(37, 99, 235, 0.1);
color: #2563eb;
transform: scale(1.1);
}
.dark .mermaid-control-btn {
color: #9ca3af;
}
.dark .mermaid-control-btn:hover {
background: rgba(55, 65, 81, 0.8);
color: #60a5fa;
}
/* Lightbox */
.mermaid-lightbox {
@apply fixed inset-0 z-50 flex items-center justify-center;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out;
}
.dark .mermaid-lightbox {
background: rgba(0, 0, 0, 0.95);
}
.mermaid-lightbox-content {
@apply relative w-11/12 h-5/6 bg-white rounded-lg shadow-2xl;
max-width: 1400px;
}
.dark .mermaid-lightbox-content {
@apply bg-gray-800;
}
.mermaid-lightbox-close {
@apply absolute top-4 right-4 z-10 p-2 rounded-full cursor-pointer transition-all;
background: rgba(0, 0, 0, 0.5);
border: none;
color: white;
font-size: 2rem;
width: 3rem;
height: 3rem;
display: flex;
align-items: center;
justify-content: center;
}
.mermaid-lightbox-close:hover {
background: rgba(220, 38, 38, 0.8);
transform: scale(1.1);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
Évaluation et débugging
Voici' comment vérifier que tout fonctionne:
Résultats de la console
Avec l’initialisation appropriée, vous devriez voir
All dependencies loaded after 1 attempts
Alpine.js started
Highlight.js copy plugin registered
Highlight.js initialized on page load
Mermaid initialized on page load
Document is ready - all initializations complete
HTMX event listener registered successfully
Contenu dynamique HTMX
Après les échanges HTMX, vous devriez voir:
HTMX afterSettle triggered for: contentcontainer
Highlight.js applied after HTMX swap
Mermaid initialized
Mermaid applied after HTMX swap
HTMX afterSettle complete for: contentcontainer
Examen de la liste de vérification
- Rendre les diagrammes lors de la charge initiale des pages
- PanM SK1contrôle du travail de la couche
- La boîte lumineuse à écran complet s'ouvre et se ferme ( Le bouton X M SK2 Cliquez à l'extérieur
- L'exportation de GNP saisit le diagramme complet ( pas seulement l'angle
- L'exportation de SVG préserve les vecteurs
- fonctionne après échange de contenu HTMX
- Changement de thème re-rend les diagrammes correctement
- Réceptif mobile ( Les contrôles demeurent visiblesM SK2 Échelle des diagrammes)
- Le style en mode foncé s'applique correctement
- Accessibilité de la claviere (tab aux commandesM SK2 entrer pour activer)
Considérations relatives au rendement
- Initialisation lente - Améliorer seulement les diagrammes qui existent sur la page
- Nettoyage de l'instance - Détruire les instances de pan-zoom lorsque les diagrammes sont supprimés
- Délégation des événements - L'écouteur à simple clic manipule tous les boutons de commande
- demandeFrame d'animation - Amélioration du temps par rapport aux valeurs d'arrêt arbitraire
- Exportations débitrices - Prévenir les clics rapides d'exportation de feuM SK1
Compatibilité du navigateur
Testé et travaillé sur:
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Les navigateurs mobiles (iOS SafariM SK1 Chrome Mobile)
IE11 n'est pas supporté en raison des fonctionnalités modernes de JavaScript (constM SK1 fonctions d'armoiries , async/awaitMSC4 demandeAnimationFrameMST5
Conclusion
Cette amélioration complète transforme les diagrammes statiques Mermaid en des visualisations interactives exportables, . La mise en œuvre est de productionM SK2 prête, résistante aux cases à bordMSC4 et offre une excellente expérience pour l’utilisateurMNK5
Key takeaways:
- Supprimer Mermaid's en ligne
max-widthrestriction pour les diagrammes complets-diagrammes de largeur - Préservez la boîte de vue lorsque vous exportez pour capturer tout le diagramme
- Utiliser la demandeAnimationFrame pour le timing au lieu de retards arbitraires
- Gérer Cloudflare Rocket Loader avec vérification de la dépendance et logique de réessai
- Fournir plusieurs méthodes de fermeture pour la boîte lumineuse ( bouton , cliquer à l'extérieur
- Utiliser la délégation des événements pour le rendement avec de nombreux diagrammes
Utilisation du paquet npm
Plutôt que de copier le code, vous pouvez maintenant installer cette fonctionnalité comme un paquet npm:
npm install @mostlylucid/mermaid-enhancements
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';
await init();
Voir Publier les améliorations Mermaid en tant que paquet npm pour la documentation intégrale, exemples d'intégration de cadres , et options de configuration avancées.
Le code source complet est également disponible dans ce blog's repository à Mostlylucid/src/js/mermaid_enhancements.js et comme un paquet open-source npm à Web plus lumineux/plus lumineux.
Diagramme d'exemple
Voici un exemple complexe de l’architecture de ce blog:
graph TB
subgraph Client["Client Browser"]
A[User Request] -->|HTMX| B[Blog Controller]
B -->|Cache Miss| C[Blog Service]
C -->|File Mode| D[Markdown Service]
C -->|DB Mode| E[EF Core Context]
D -->|Parse| F[Markdig Pipeline]
F -->|Render| G[HTML + Mermaid]
E -->|Query| H[PostgreSQL]
H -->|Full-Text Search| I[GIN Index]
G -->|Enhance| J[mermaid_enhancements.js]
J -->|Initialize| K[svg-pan-zoom]
J -->|Add| L[Control Buttons]
L -->|Export| M[html-to-image]
end
subgraph Background["Background Services"]
N[File Watcher] -->|Change Detected| O[Saves to DB]
O -->|Trigger| P[Translation Service]
P -->|Batch| Q[EasyNMT API]
Q -->|12 Languages| R[Translated Files]
end
style A stroke:#22c55e,stroke-width:3px,color:#4ade80
style G stroke:#3b82f6,stroke-width:3px,color:#60a5fa
style J stroke:#f59e0b,stroke-width:3px,color:#f59e0b
style K stroke:#ec4899,stroke-width:3px,color:#ec4899
style M stroke:#8b5cf6,stroke-width:3px,color:#8b5cf6
Essayez de cliquer sur les commandes du diagramme ci-dessus!