Skip to content
rodolfo.gg
Go back

Inclusión de gráficos hecha sencilla: Mermaid.

CC BY-NC-ND 4.0
Rodolfo González González

Inclusión de gráficos hecha sencilla: Mermaid.

Introducción

Mermaid es un lenguaje de marcado que permite crear gráficos de forma declarativa, usando una librería de código abierto de JavaScript para generar las gráficas.

Mermaid fue creado por Knut Sveidqvist, quien se inspiró en sus hijos al ver la película La Sirenita (The Little Mermaid). Inicialmente fue desarrollado como una herramienta para generar diagramas a partir de texto con una sintaxis similar a Markdown, y ha evolucionado hacia todo un ecosistema ampliamente usado en la comunidad: actualmente, el proyecto cuenta con una base de usuarios masiva, superando las 87.500 estrellas en GitHub.

Mermaid se integra nativamente en sistemas como GitHub, GitLab, Visual Studio Code o Notion, entre muchos otros. De manera similar, hay muchas integraciones para prácticamente todas las librerías y frameworks de desarrollo de frontend.

Al usar un lenguaje sencillo como el de Mermaid, se pueden definir declarativamente los gráficos sin tener que aprender a utilizar algún programa para generarlos visualmente, que de por sí es más lento. Asimismo, resulta más fácil mantener código que mantener una serie de archivos fuente de Gimp o El Pato.

Yo lo descubrí (algo tarde) al momento de elaborar la guía de estudio de NLP.

A continuación presento algunos ejemplos de integración y de gráficos hechos con Mermaid, y posteriormente una guía para instalar lo necesario para usarlo y personalizarlo en Astro.


Tabla de contenido

Tabla de contenido

Integraciones populares de Mermaid

Dependiendo del stack, se puede usar la librería oficial mermaid o wrappers de la comunidad:


Ejemplos

Graph TD

Código fuente:

```mermaid
graph TD
A[Escribe bloque mermaid en MDX] --> B[Astro + astro-mermaid lo procesa]
B --> C[Se genera SVG inline]
C --> D[Se renderiza el gráfico en el post]
```

Gráfico renderizado:

graph TD
    A[Escribe bloque mermaid en MDX] --> B[Astro lo procesa]
    B --> C[Se genera SVG inline]
    C --> D[Se renderiza el gráfico en el post]

Flowchart LR

Código fuente:

```mermaid
flowchart LR
Inicio([Inicio]) --> Paso1[Definir objetivo]
Paso1 --> Paso2[Escribir bloque Mermaid]
Paso2 --> Paso3[Renderizar en Astro]
Paso3 --> Fin([Fin])
```

Gráfico renderizado:

flowchart LR
    Inicio([Inicio]) --> Paso1[Definir objetivo]
    Paso1 --> Paso2[Escribir bloque Mermaid]
    Paso2 --> Paso3[Renderizar en Astro]
    Paso3 --> Fin([Fin])

Sequence Diagram

Código fuente:

```mermaid
sequenceDiagram
participant U as Usuario
participant M as MDX
participant A as Astro
participant R as Render
U->>M: Escribe bloque mermaid
M->>A: Compila contenido
A->>R: Genera SVG inline
R-->>U: Muestra gráfico
```

Gráfico renderizado:

sequenceDiagram
    participant U as Usuario
    participant M as MDX
    participant A as Astro
    participant R as Render
    U->>M: Escribe bloque mermaid
    M->>A: Compila contenido
    A->>R: Genera SVG inline
    R-->>U: Muestra gráfico

Mindmap

Código fuente:

```mermaid
mindmap
root((Fundamentos))
Lengua (langue)
Habla (parole)
Competencia lingüística
Actuación lingüística (performance)
Signo lingüístico
Sincronía y diacronía
```

Gráfico renderizado:

mindmap
  root((Fundamentos))
    Lengua (langue)
    Habla (parole)
    Competencia lingüística
    Actuación lingüística (performance)
    Signo lingüístico
    Sincronía y diacronía

Class Diagram

Código fuente:

```mermaid
classDiagram
class Animal {
+String nombre
+comer()
}
class Perro {
+ladrar()
}
Animal <|-- Perro
```

Gráfico renderizado:

classDiagram
    class Animal {
      +String nombre
      +comer()
    }
    class Perro {
      +ladrar()
    }
    Animal <|-- Perro

State Diagram

Código fuente:

```mermaid
stateDiagram-v2
[*] --> Inactivo
Inactivo --> Activo: iniciar()
Activo --> Pausado: pausar()
Pausado --> Activo: reanudar()
Activo --> [*]: detener()
```

Gráfico renderizado:

stateDiagram-v2
    [*] --> Inactivo
    Inactivo --> Activo: iniciar()
    Activo --> Pausado: pausar()
    Pausado --> Activo: reanudar()
    Activo --> [*]: detener()

Entity Relationship (ER)

Código fuente:

```mermaid
erDiagram
USUARIO ||--o{ PEDIDO : realiza
PEDIDO ||--|{ LINEA_PEDIDO : contiene
PRODUCTO ||--o{ LINEA_PEDIDO : aparece_en
```

Gráfico renderizado:

erDiagram
    USUARIO ||--o{ PEDIDO : realiza
    PEDIDO ||--|{ LINEA_PEDIDO : contiene
    PRODUCTO ||--o{ LINEA_PEDIDO : aparece_en

Gantt

Código fuente:

```mermaid
gantt
title Plan de publicación
dateFormat YYYY-MM-DD
section Contenido
Redacción :a1, 2026-04-22, 2d
Revisión :a2, after a1, 1d
section Publicación
Maquetación :a3, after a2, 1d
Publicar :milestone, m1, after a3, 0d
```

Gráfico renderizado:

gantt
    title Plan de publicación
    dateFormat  YYYY-MM-DD
    section Contenido
    Redacción           :a1, 2026-04-22, 2d
    Revisión            :a2, after a1, 1d
    section Publicación
    Maquetación         :a3, after a2, 1d
    Publicar            :milestone, m1, after a3, 0d

Pie Chart

Código fuente:

```mermaid
pie title Distribución de tareas
"Documentación" : 35
"Desarrollo" : 45
"Pruebas" : 20
```

Gráfico renderizado:

pie title Distribución de tareas
    "Documentación" : 35
    "Desarrollo" : 45
    "Pruebas" : 20

Git Graph

Código fuente:

```mermaid
gitGraph
commit id: "init"
branch feature
checkout feature
commit id: "nuevo-diagrama"
checkout main
merge feature
commit id: "release"
```

Gráfico renderizado:

gitGraph
    commit id: "init"
    branch feature
    checkout feature
    commit id: "nuevo-diagrama"
    checkout main
    merge feature
    commit id: "release"

Aquí puedes ver más ejemplos.


Procedimiento de instalación en Astro

1. Instalar mermaid

Terminal window
bun add mermaid

2. Plugin remark en astro.config.ts

El problema con soluciones de renderizado en tiempo de compilación (como astro-mermaid o rehype-mermaid) es que crean conflictos con astro-expressive-code, la librería que se encarga del syntax highlighting, rompiendo el resaltado de código en todos los bloques ```javascript, ```bash etc.

La solución es renderizar Mermaid en el cliente, pero hay que evitar que expressiveCode intente procesar los bloques ```mermaid. Para esto se usa un plugin remark que convierte esos bloques a HTML raw <pre class="mermaid"> antes de que expressiveCode los vea:

astro.config.ts
import { visit } from 'unist-util-visit';
function remarkMermaidBypass() {
return (tree: any) => {
visit(tree, 'code', (node: any, index: number | undefined, parent: any) => {
if (node.lang === 'mermaid' && parent && typeof index === 'number') {
parent.children[index] = {
type: 'html',
value: `<pre class="mermaid">\n${node.value}\n</pre>`,
};
}
});
};
}

Se agrega al arreglo markdown.remarkPlugins (el orden importa: debe ir primero):

astro.config.ts
markdown: {
remarkPlugins: [
remarkMermaidBypass, // primero
remarkToc,
remarkMath,
remarkCollapse,
],
// ...
},

También es importante que la integración MDX herede la configuración de markdown en lugar de definir sus propios plugins:

astro.config.ts
mdx({
extendMarkdownConfig: true, // hereda remarkPlugins y rehypePlugins
}),

Si se pasan rehypePlugins o remarkPlugins directamente a mdx(), estos reemplazan (no se fusionan con) los de markdown.*, lo que causa que rehypeExpressiveCode desaparezca silenciosamente del pipeline de MDX.

3. Script de renderizado en el layout del post

En el layout que renderiza los posts (en Astro Paper: PostDetails.astro), se agrega un <script> que importa mermaid y lo renderiza en el cliente. El script usa el evento astro:page-load para ser compatible con View Transitions, y un MutationObserver para re-renderizar cuando el usuario cambia el tema (claro/oscuro):

PostDetails.astro
import mermaid from "mermaid";
let themeObserver: MutationObserver | null = null;
function getTheme() {
return document.documentElement.dataset.theme === "dark" ? "dark" : "forest";
}
async function renderMermaid() {
// Restaurar diagramas ya renderizados a pre.mermaid para re-renderizar
// (necesario al cambiar de tema)
document.querySelectorAll<HTMLElement>("[data-mermaid]").forEach(el => {
const pre = document.createElement("pre");
pre.className = "mermaid";
pre.textContent = el.dataset.mermaid!;
el.replaceWith(pre);
});
const blocks = Array.from(document.querySelectorAll<HTMLPreElement>("pre.mermaid"));
if (!blocks.length) return;
mermaid.initialize({ startOnLoad: false, theme: getTheme() });
await Promise.all(blocks.map(async pre => {
const source = (pre.textContent ?? "").trim();
const id = `mermaid-${Math.random().toString(36).slice(2, 9)}`;
try {
const { svg } = await mermaid.render(id, source);
const wrapper = document.createElement("div");
wrapper.className = "my-6 flex justify-center overflow-x-auto";
wrapper.dataset.mermaid = source; // guarda source para re-render en cambio de tema
wrapper.innerHTML = svg;
pre.replaceWith(wrapper);
} catch (err) {
console.error("[mermaid]", err);
}
}));
}
document.addEventListener("astro:page-load", () => {
renderMermaid();
themeObserver?.disconnect();
themeObserver = new MutationObserver(renderMermaid);
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
});
// mal
const codeBlocks = Array.from(document.querySelectorAll("pre"));
// bien
const codeBlocks = Array.from(document.querySelectorAll("pre:not(.mermaid)"));

4. Detección del tema

El sitio usa el atributo data-theme en el <html> para indicar el tema activo ("light" o "dark"). La función getTheme() lee ese atributo y devuelve el nombre del tema Mermaid correspondiente. En este blog se usan "forest" para claro y "dark" para oscuro.

Personalización de colores con CSS

Mermaid renderiza SVG inline, y sus estilos internos usan selectores de alta especificidad (#id .clase). Para sobreescribirlos con los colores del tema del sitio, se necesita !important en el CSS global.

Existen algunas peculiaridades en Mermaid v11 respecto a versiones anteriores:

El CSS completo usado en este blog, en global.css:

/* Mermaid: mindmap — secciones no-root usan color de texto del tema */
svg.mindmapDiagram [class*="section-"]:not(.section-root) span {
color: var(--foreground) !important;
}
/* Mermaid: flowchart — nodos (graph TD, flowchart LR, etc.) */
[id^="mermaid-"] .node rect,
[id^="mermaid-"] .node circle,
[id^="mermaid-"] .node ellipse,
[id^="mermaid-"] .node polygon,
[id^="mermaid-"] .node path {
fill: var(--muted) !important;
stroke: var(--border) !important;
}
/* Mermaid: flowchart — aristas */
[id^="mermaid-"] .edgePath .path,
[id^="mermaid-"] .flowchart-link {
stroke: var(--accent) !important;
}
/* Mermaid: flowchart — puntas de flecha (v11: .arrowheadPath) */
[id^="mermaid-"] .arrowheadPath {
fill: var(--accent) !important;
stroke: var(--accent) !important;
}
/* Mermaid: sequence — actores (v11: .actor directo en rect) */
[id^="mermaid-"] .actor {
fill: var(--muted) !important;
stroke: var(--border) !important;
}
/* Mermaid: sequence — líneas de mensajes */
[id^="mermaid-"] .messageLine0,
[id^="mermaid-"] .messageLine1 {
stroke: var(--accent) !important;
}
/* Mermaid: sequence — puntas de flecha */
[id$="-arrowhead"] path {
fill: var(--accent) !important;
stroke: var(--accent) !important;
}
/* Mermaid: sequence — lifelines verticales */
[id^="mermaid-"] .actor-line {
stroke: var(--border) !important;
}

Share this post on:

Previous Post
Guía para administración de servicios con systemd.
Next Post
Glosario de lingüística para Procesamiento de Lenguaje Natural.