Back to "Покращення діаграм Merganow з Pan/Zoom і експортування"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

DaisyUI Javascript Mermaid SVG Tailwind

Покращення діаграм Merganow з Pan/Zoom і експортування

Friday, 07 November 2025

Вступ

Доступний пакунок npm: Ця реалізація тепер доступна як @ mostulicid/mer покоївка- enthensments - пакунок для виробництва npm. Див. Оприлюднення програмного забезпечення як пакунка npm детально про те, як користуватися ним у своїх проектах.

Merowage - це фантастичний інструмент для створення діаграм з тексту, але типове зображення може бути обмеженим для складних діаграм. Користувачам не можна легко збільшити масштаб, об' єм великих діаграм або експортувати їх для документації. У цій статті ми покажемо вам, яким чином можна вдосконалити діаграми Meranow на цьому сайті за допомогою інтерактивних інструментів керування pan/ zoom, повноекранного перегляду і експорту функціональних можливостей (як у форматах PNG, так і SVG).

Ця реалізація є продуктивною, працює з темним режимом, перемикаючи його граціозно, і є стійкою до використання навантажувача Farflare.

Як це виглядає?

Ось що ми робимо: гарна на сторінці (і розпушкова) покоївка.js виставка, яка нагадує GitHub's but кращеЦе означає, що діаграми не беруть до уваги SCREENS, але все ще читаються еасуу.

mermaid_pan_zoom.png

Проблема

З коробки діаграми " Мернір" мають декілька обмежень:

  1. Фіксований розмір - Великі діаграми або відрізають, або зменшують, щоб їх вмістити.
  2. Без взаємодії - Не можу збільшити, щоб побачити деталі чи панчохи навколо
  3. Без експорту - Користувачі не можуть зберігати діаграми для зовнішнього використання
  4. Поганий мобільний досвід - Малі екрани роблять складні діаграми недосяжними
  5. Проблеми з перемиканням тем - Діаграми не завжди повторюються належним чином під час перемикання між світлом і темним режимом

Розв'язання

Я запровадив комплексну систему покращення, яка додає:

  • Інтерактивна панно/зоома використання бібліотеки svg- pan- zoom
  • Плаваючі кнопки керування для збільшення/ зменшення, відновлення, перемикання та експортування
  • Повноекранна лампочка режим для кращого перегляду
  • Експорт до PNG і SVG функціональність
  • Автоматично вкладати під час завантаження так що всю діаграму типово буде показано як видиму
  • Показ повної ширини вилучення штучних обмежень розміру

Огляд архітектури

Вирішення складається з трьох основних компонентів:

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]

Впровадження

Встановлення залежностей

Спочатку встановіть потрібні пакунки npm:

npm install svg-pan-zoom html-to-image

У цих бібліотеках містяться:

  • svg-pan-zoom - Інтерактивна панель і масштабування елементів SVG
  • html-to-image - Експорт функціональних можливостей SVG/ PNG

Основний модуль розширення

Головний модуль покращення (mermaid_enhancements.js) керує всіма інтерактивними функціями.

Створення кнопок керування

Кожна з діаграм отримує плаваючу панель керування з кнопками для всіх дій:

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);
}

Ініціалізація Pan/ Zoom

У бібліотеці svg- pan- zoom передбачено гладку і функціональну взаємодію:

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;
    }
}

Експортувати функціональність

Система експорту зберігає якість діаграм і керує форматами PNG і 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');
    }
}

Розгляд експорту ключів:

  1. Зберігати ViewBox - Критична для захоплення всієї діаграми, а не лише видимої частини
  2. Обчислення вимірів - Явно встановити ширину/ висоту з viewBox для послідовного експорту
  3. Вилучити перетворення - Стрічка-сума перетворення, отже експорт показує повну діаграму
  4. Робота з тлом - Біле тло для PNG, прозоре для SVG
  5. Висока роздільна здатність - Використання pixelRatio: 2 для експорту для Free PNG

Повноекранна скринька освітлень

Скринька світла є захопливим видовищем:

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);
}

Головна функція покращення

Це все з'єднує і називається після того, як "Мер покоївка" перекладає:

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');
    }
}

Критична фіксація: Mer покоївка застосовує вбудований рядок style="max-width: 1020px" до елементів SVG, які заважають показу повної ширини. Вилучення цього елемента є необхідним для належної реакції.

Інтеграція теми

Інструмент перемикання тем забезпечить повторне відтворення діаграм під час перемикання між світлими і темними режимами:

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);
    }
};

Користування requestAnimationFrame двічі гарантує, що браузер закінчив малювати SVG перед тим, як ми спробуємо збільшити його.

Сумісність з завантажувачем хмар

Завантажувач Rocket, Hamflare, може затримувати виконання JavaScript, ламаючи ініціалізація. Ось рішення куленепробивної системи:

// 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();

Також переконайтеся, що ваш головний скрипт має data-cfasync="false" атрибут виключення його з завантажувача Rocket:

<script src="~/js/dist/main.js" type="module" asp-append-version="true" data-cfasync="false"></script>

Стояння з хвостовим вітром/ Даісюі

CSS використовує допоміжні класи та нетипові стилі для полірування:

/* 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; }
}

Перевірка і зневадження

Ось як перевірити все працює:

Вивід консолі

З правильною ініціалізацією, ви повинні побачити:

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

Динамічний вміст HTMX

Після обміну з HTMX ви побачите:

HTMX afterSettle triggered for: contentcontainer
Highlight.js applied after HTMX swap
Mermaid initialized
Mermaid applied after HTMX swap
HTMX afterSettle complete for: contentcontainer

Перевірка списку перевірки

  • Схеми, показані під час початкового завантаження сторінки
  • Робота над керуванням pan/zoom
  • Fullscreen lightbox відкриває і закривається (кнопка X, клацніть зовні, клавіша ESC)
  • Експорт PNG захоплює повну діаграму (не тільки у куті)
  • Експорт SVG зберігає вектори
  • працює після свопінгу вмісту HTMX
  • Перемикання тем відтворює діаграми правильно
  • Мобільний реагування (кермування залишається видимим, шкала діаграм)
  • Заняття темних режимів застосовуються належним чином
  • Доступність клавіатури (встановлено для керування, введення для активації)

Обмірковування швидкодії

  1. Ініціалізація " Лази" Image/ info menu item (should be translated) - Только улучшие схемы, что существуют на странице.
  2. Очищення примірників - Знищувати сковороди під час вилучення діаграм
  3. Делегація подій - Програма для клацання одним клацанням лівою кнопкою керує усіма контрольними кнопками
  4. requestAnimationFrame - Краще відлік часу, ніж довільні значення setTimeout
  5. Делегований експорт - Запобігати швидкому експорту

Сумісність навігатора

Перевірено і працює над:

  • Chrome/Edge 90+
  • Firefox 88+
  • 14+ Safari
  • Мобільні навігатори (iOS Safari, Chrome Mobile)

IE11 не підтримується Через сучасні можливості JavaScript (const, функції зі стрілочками, синхронізацію/await, запитAnimationFrame).

Висновки

За допомогою цього комплексного покращення статичні діаграми MeRowles можна перетворити на інтерактивні, експортовані візуалізації. Реалізація - це виробничі, стійкі до випадків, пов' язаних з ребрами, і надає вам чудовий досвід користувача.

Захоплення ключів:

  • Вилучити вбудовану лінію Mer покоївки max-width Обмеження для діаграм повної ширини
  • Зберігати рамку перегляду під час експорту для захоплення всієї діаграми
  • Використовувати queryAnimationFrame замість довільної затримки
  • Працювати з навантажувачем Farflare за допомогою перевірки залежностей і повторних спроб
  • Надайте декілька способів закриття віконця (кнопка, клацання назовні, ESC)
  • Використовувати делегацію подій для швидкодії з багатьма діаграмами

Користування пакунком npm

Замість копіювання коду, ви можете встановити цю функціональну можливість як пакунок npm:

npm install @mostlylucid/mermaid-enhancements
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';

await init();

Див. Оприлюднення програмного забезпечення як пакунка npm для повної документації, прикладів інтеграції з базовими можливостями та додаткових параметрів налаштування.

Крім того, у сховищі цього блогу можна знайти повний вихідний код. Mostlylucid/src/js/mermaid_enhancements.js і як пакунок з відкритим кодом npm у three locidweb/ method-mer покоївка.

Приклад діаграми

Ось складний приклад архітектури системи вмісту блогу:

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

Спробуйте навести вказівник миші на діаграму, розташовану вище!

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.