Engenharia · 9 diagramas

Mermaid com a cara do site

Diagrama como texto versionado é a parte fácil. O trabalho é fazer o Mermaid parar de parecer o Mermaid e passar a parecer a página.

Diagrama em imagem envelhece pior que código. O PNG exportado há dois anos continua no README mostrando um serviço que fala com um banco que já foi desligado, e ninguém corrige — corrigir exige achar o arquivo original, abrir a ferramenta, exportar de novo. Diagrama em texto envelhece junto com o texto: mora no mesmo arquivo, entra no mesmo commit, aparece no mesmo diff.

É por isso que este site desenha com Mermaid. O preço é que o Mermaid chega com identidade visual própria, e um site cuja regra é uma cor de acento e só uma não convive com isso. Este texto é o catálogo do que ele desenha aqui e o registro do que custou para ele desaparecer na página.

As nove cores

Toda cor de diagrama sai de um custom property do css/base.css, lido em tempo de execução. São nove, e são hex sólidos de propósito: o Mermaid faz conta com eles para derivar tons — borda de nó, fundo de nota, texto sobre fundo — e derivar a partir de rgba() com alfa dá resultado errado.

Troque o tema no canto superior direito e as nove trocam de valor. O que não troca sozinho é o desenho: o SVG que o Mermaid produz carrega a cor embutida em cada forma e não herda custom property. Trocar o tema aqui significa redesenhar — é a única parte deste site em que o JavaScript precisa saber que cor é a cor.

O catálogo

O Mermaid 11.15 desenha vinte e dois tipos de diagrama. Vinte e um funcionam por CDN sem plugin nenhum; os oito abaixo são os que este site usa, e cada um está desenhando algo verdadeiro sobre o próprio site — nenhum é exemplo de manual.

flowchart

largura 643px vem de conteúdo tema por themeVariables

O tipo mais usado, e o único em que vale gastar configuração: curve, nodeSpacing, rankSpacing e wrappingWidth mudam bastante o resultado. TD (de cima para baixo) costuma caber na coluna; LR costuma não.

mermaid · flowchart
flowchart TD
    F["fonte no .html"] --> W["espera document.fonts.ready"]
    subgraph tokens["css/base.css"]
      direction LR
      T1["--mm-node"] --- T2["--mm-line"]
    end
    tokens --> C["themeVariables"]
    W --> I["mermaid.initialize"]
    C --> I
    I --> R["mermaid.run"]
    R --> A{{"cabe na coluna?"}}
    A -- "sim" --> N(["fica no tamanho natural"])
    A -- "não" --> Z(["reduz, e o rótulo encolhe junto"])
O nó hexagonal usa {{"texto"}} — e é por causa dele que o .nojekyll na raiz do repositório não pode ser apagado: sem ele o GitHub Pages roda o Jekyll, que interpreta as chaves duplas como Liquid e quebra a página inteira.

sequenceDiagram

largura 690px vem de conteúdo tema por themeVariables

Para ordem no tempo, e o tipo que mais se beneficia de autonumber. A ativação (a barra sobre a linha de vida) usa --mm-note, o mesmo token da nota: é o único destaque de cor que um diagrama recebe aqui.

mermaid · sequenceDiagram
sequenceDiagram
    autonumber
    participant N as navegador
    participant F as document.fonts
    participant M as mermaid
    N->>F: espera fonts.ready
    activate F
    F-->>N: a JetBrains Mono chegou
    deactivate F
    N->>M: run() com os fontes do grafo
    activate M
    Note over M: mede o texto renderizado
    M-->>N: svg com a cor embutida
    deactivate M
O passo 1 é o que evita o defeito descrito mais abaixo: desenhar antes de a fonte chegar faz o Mermaid medir com a fonte de fallback e dimensionar o desenho errado.

stateDiagram-v2

largura 580px vem de conteúdo tema por themeVariables + themeCSS

Máquina de estados. É o tipo que mais deu trabalho para tematizar: o rótulo da transição não é <text> do SVG, mora dentro de um foreignObject e escapa dos seletores normais — precisa de uma regra própria no themeCSS.

mermaid · stateDiagram-v2
stateDiagram-v2
    [*] --> Escondido
    Escondido --> Desenhado: run() terminou
    Desenhado --> Ajustado: ajustarEscala
    Ajustado --> Escondido: tema mudou
    Escondido --> Fonte: mermaid fora do ar
    Fonte --> [*]
    note right of Fonte
        o leitor vê o texto do
        grafo, não um buraco
    end note
O ciclo de vida real de um bloco desta página. Em bloco note a quebra de linha no fonte funciona — é a exceção à regra do <br/>.

classDiagram

largura 247px vem de conteúdo tema por themeVariables

Estrutura e relação entre tipos. Serve bem para desenhar a superfície de uma API — abaixo, a que o site.js expõe e a publicação consome.

mermaid · classDiagram
classDiagram
    class Site {
        +tema() string
        +token(nome) string
        +alternar()
    }
    class Publicacao {
        -fontes
        +desenhar()
        +ajustarEscala()
    }
    Publicacao ..> Site : lê os tokens
O rótulo da relação não pode conter dois-pontos: escrever : ouve tema:mudou derruba o parser e o Mermaid desenha a bomba de erro no lugar do diagrama. Custou-me uma medição para descobrir que o defeito era meu, não dele.

erDiagram

largura 596px vem de conteúdo tema por themeVariables

Entidade e relacionamento. Aqui ele desenha o modelo do build.mjs — que não tem banco nenhum: a publicação é a fonte de verdade e o resto é derivado dela.

mermaid · erDiagram
erDiagram
    PUBLICACAO }o--|| TOPICO : "tem"
    PUBLICACAO ||--o{ DIAGRAMA : "contém"
    PUBLICACAO ||--|| CARTAO : "gera"
    PUBLICACAO {
        string titulo
        date publicado
        int palavras
    }
A cardinalidade é o valor deste tipo: }o--|| diz que toda publicação tem exatamente um tópico, e que um tópico pode não ter nenhuma publicação — que é justamente o estado de três dos quatro tópicos deste site.

gitGraph

largura 368px vem de conteúdo tema por git0..git7

Ramos e merges. Foi o primeiro tipo a mostrar que themeVariables não é suficiente: ele pinta por uma paleta numerada própria e ignorava primaryColor — os ramos saíam caqui.

mermaid · gitGraph
gitGraph
    commit id: "site no ar"
    branch fix/ancoras
    commit id: "checks na ci"
    checkout main
    merge fix/ancoras tag: "publica"
    commit id: "este texto"
O fluxo real deste repositório: ramo curto, PR, e o merge na main publicando — não há homologação, então o PR é a revisão. A main fica na cor de linha; o ramo, no acento.

mindmap

largura 665px vem de conteúdo tema por cScale0..cScale11

Hierarquia solta, sem seta. Bom para mapa de assunto e ruim para processo — não há ordem nem condição. Abaixo, o mapa da própria camada de customização que este texto descreve.

mermaid · mindmap
mindmap
  root((mermaid))
    cor
      themeVariables
      themeCSS
      redesenhar no tema
    medida
      fonts.ready
      useMaxWidth
      largura natural
    queda
      sem CDN
      grafo inválido
Terceiro tipo com paleta numerada própria (cScale0…), e a terceira vez que a mesma correção foi necessária. É o padrão que dá o título da seção seguinte.

xychart-beta

largura 700px vem de configuração tema por themeVariables.xyChart

Gráfico de barras ou linha. Não é diagrama, é gráfico — e por isso tem bloco de tema próprio, largura fixa e nenhuma relação com o conteúdo. Abaixo, as larguras medidas dos oito espécimes desta página, desenhadas pelo próprio Mermaid.

mermaid · xychart-beta
xychart-beta
    title "largura natural de cada espécime, em px"
    x-axis [flow, seq, state, class, er, git, mind, xy]
    y-axis "px" 0 --> 800
    bar [643, 690, 580, 247, 596, 368, 665, 700]
A linha do xychart é ele mesmo: 700px fixos, que não mudam com o número de barras. Sem o bloco xyChart no tema, essas barras seriam bege.

A camada de customização

Nada do que está acima é o Mermaid de fábrica. São nove ajustes, e cada um existe por um defeito visível — não por preferência estética:

O que incomoda Padrão do Mermaid O que este site faz Onde mora
Paleta Lilás e pastel próprios theme: 'base' e cada cor lida dos tokens em tempo de execução themeVariables
Fonte do rótulo Sans genérica; themeVariables.fontSize não chega ao rótulo do nó JetBrains Mono a 15px, com !important, por seletor de SVG themeCSS
Rótulo como HTML foreignObject com HTML dentro do SVG htmlLabels: false — o rótulo vira <text>/<tspan> e o SVG fica autocontido config do flowchart
Quebra de linha <br/>, que não funciona Rótulo corrido e wrappingWidth: 170 config do flowchart
Largura Tamanho natural, com barra de rolagem useMaxWidth: true no desktop; tamanho natural e arraste abaixo de 900px ajustarEscala
Medida do texto Desenha já, medindo com a fonte que estiver disponível Espera document.fonts.ready antes do primeiro desenho js/publicacao.js
Troca de tema Nada: a cor está embutida no SVG Redesenha ao ouvir tema:mudou js/site.js emite, a publicação ouve
Moldura Sem opinião Sem borda, sem fundo, sem barra; legenda numerada por contador CSS css/publicacao.css
Queda Buraco na página, ou a bomba de erro Mostra o fonte do grafo, com barra à esquerda .diagram--sem-js

As cores não são escritas na configuração: são lidas do CSS. É o que garante que existe um só lugar onde a paleta do site vive, e que o diagrama nunca discorda dele.

javascript · js/publicacao.js
function cor(nome) {
  return window.Site.token(nome);        // getComputedStyle no :root
}

themeVariables: {
  darkMode:           window.Site.tema() === 'dark',
  fontFamily:         MONO,
  primaryColor:       cor('--mm-node'),
  primaryBorderColor: cor('--mm-node-border'),
  primaryTextColor:   cor('--mm-text'),
  lineColor:          cor('--mm-line'),
  clusterBkg:         cor('--mm-cluster'),
  noteBkgColor:       cor('--mm-note'),
  noteBorderColor:    cor('--mm-note-border')
  // …e mais trinta, para ator, linha de vida, ativação e estado
}
Trinta e nove variáveis para nove cores. A conta fecha porque o Mermaid nomeia por papel no diagrama, e vários papéis recebem o mesmo token do site.

O ajuste menos óbvio e o mais importante é a espera pela fonte. O Mermaid calcula o tamanho do desenho medindo o texto renderizado; se ele roda antes de a JetBrains Mono chegar, mede com a fonte de fallback, que é mais larga. O mesmo diagrama de sequência sai com 922px em vez de 777px — número que está registrado no próprio código, de quando o defeito apareceu — e passa a ser reduzido para caber na coluna sem nenhum motivo, e sem erro nenhum no console.

javascript · js/publicacao.js
var fontesProntas = (document.fonts && document.fonts.ready)
  ? document.fonts.ready
  : Promise.resolve();

fontesProntas.then(desenhar).then(ajustarEscala);
Quatro linhas. Sem elas, todo diagrama da página nasce com o tamanho errado nas visitas em que a fonte demora — o que inclui, principalmente, a primeira visita de cada leitor.

As três camadas de obediência

Medindo tipo por tipo apareceu um padrão que eu não esperava, e que é a informação mais útil deste texto: os tipos do Mermaid não obedecem todos ao mesmo mecanismo de tema. São três grupos.

Primeiro: os que obedecem a themeVariables. Flowchart, sequência, estado, classe, ER, requisito, bloco, arquitetura. Configura uma vez, valem todos. É o grupo onde vive quase todo diagrama de engenharia.

Segundo: os que têm paleta numerada própria. O gitGraph pinta por git0..git7, o mindmap por cScale0..cScale11, o xychart por um bloco xyChart inteiro, e nenhum deles olha para primaryColor. Cada um exige tratamento separado — e o sintoma é sempre o mesmo: uma cor que não existe no seu site aparecendo no desenho. Caqui, no caso de dois deles.

Terceiro: os que não sobrevivem a uma cor de acento só. Aqui não é questão de configurar: gráfico de pizza e jornada de usuário precisam de cor categórica — cor que distingue, não que decora. Numa paleta de quase-branco, quase-preto e um vermelho, três fatias viram três tons quase idênticos:

pie title peso de uma publicação, em KB
    "mermaid pela CDN" : 3300
    "highlight.js" : 120
    "html, css e js do site" : 46
Contraexemplo deliberado, e o único desenho fora da paleta editorial desta página. A terceira fatia existe (46 KB, 1,3%) e é invisível: sem cor categórica, pizza não funciona. A conclusão honesta é não usar o tipo, não insistir na configuração — a mesma informação caberia numa frase.

A medição

A regra que eu seguia — "diagrama não pode passar de uns 840px" — estava escrita no CLAUDE.md do repositório como estimativa. Medindo, ela virou um número exato e ganhou explicação: 840px é a largura da coluna do artigo numa tela de 1440px. Numa tela de 1520px ou mais a coluna é de 920px. Acima disso, o desenho é reduzido para caber e o rótulo encolhe na mesma proporção.

E a largura vem de dois lugares diferentes, o que decide como consertar. Nos tipos que têm espécime nesta página, o número da tabela é o deste desenho — largura de tipo cujo tamanho vem do conteúdo só significa algo junto com o grafo que a produziu:

Tipo Largura natural De onde vem Cabe em 840px?
packet-beta1026pxfixa no tiponão — sempre reduzido
treemap-beta996pxfixa no tiponão — sempre reduzido
gantt920pxfixa no tiponão — sempre reduzido
journey900pxconteúdonão
timeline895pxconteúdonão
pie710pxconteúdosim
xychart-beta700pxfixa no tiposim
radar-beta700pxfixa no tiposim
sequenceDiagram690pxconteúdosim
mindmap665pxconteúdosim
flowchart643pxconteúdosim
sankey-beta600pxfixa no tiposim
erDiagram596pxconteúdosim
stateDiagram-v2580pxconteúdosim
quadrantChart500pxfixa no tiposim
kanban425pxconteúdosim
architecture-beta403pxconteúdosim
gitGraph368pxconteúdosim
requirementDiagram252pxconteúdosim
classDiagram247pxconteúdosim
block-beta239pxconteúdosim
zenumlnão desenha: não vem no pacote

A diferença é prática. Nos tipos cuja largura vem do conteúdo, um diagrama largo se resolve encurtando rótulo ou virando o eixo — mexendo no grafo. Nos de largura fixa, mexer no grafo não muda nada: dobrei o conteúdo do quadrantChart, do sankey, do packet e do gantt e os quatro devolveram exatamente a mesma largura. Lá o único caminho é configuração — ou não usar o tipo.

O que não funciona

Seis coisas que custaram tempo, na ordem em que doeriam de novo:

  1. <br/> em rótulo de nó. A tag é removida e o espaço em volta é engolido: "cabe na<br/>janela?" vira cabe najanela?. Não muda com htmlLabels ligado nem desligado.1
  2. Dois-pontos no rótulo de relação do classDiagram. Derruba o parser inteiro e desenha a bomba de erro. Vale para o rótulo depois de : em qualquer relação de classe.
  3. themeVariables.fontSize não chega ao rótulo do nó. Quem manda ali é o themeCSS, e só com !important.
  4. Um grafo inválido derruba os outros. Defeito deste site, não do Mermaid: o catch do mermaid.run é global, então um erro de sintaxe num diagrama marca todos os da página como indisponíveis. Foi assim que eu descobri o item 2 — e é o que a medição um-tipo-por-página contornou.3
  5. zenuml não vem no pacote. Está na documentação, mas o mermaid.min.js do cdnjs não tem uma única ocorrência da palavra: exige o pacote @mermaid-js/mermaid-zenuml à parte. Dos 22 tipos, é o único que não desenha aqui.
  6. Ligadura em bloco de código. A JetBrains Mono desenha != como um sinal de diferente. Em prosa é bonito; em código é mentira, porque mostra um caractere que não está lá e que não vem no copiar. Desligada em pre e code.

O que eu levo disso

Diagrama como texto vale a pena — o desenho envelhece junto com o texto, e isso resolve o problema real. Mas tematizar uma biblioteca de desenho não é escolher cores: é descobrir, um tipo por vez, onde cada cor foi parar. Três mecanismos diferentes, uma paleta numerada escondida por tipo, e um cálculo de layout que depende de a fonte já ter chegado.

E a regra que eu recomendaria a quem for fazer o mesmo é a mais chata: meça. As duas coisas que este texto corrigiu no meu próprio entendimento — de onde vem o 840px e quais tipos ignoram o tema — não apareceram lendo a documentação nem o código. Apareceram renderizando e lendo o número.


notas

  1. Em bloco note de diagrama de sequência ou de estado, a quebra de linha no próprio fonte funciona — é o jeito de ter nota em várias linhas, e está no espécime de stateDiagram-v2 acima. O caso apareceu primeiro em Um blog sem framework.
  2. O driver de medição são 90 linhas de Node sem dependência alguma — o WebSocket e o fetch já vêm na plataforma desde o Node 22, então falar CDP com o Chrome não exige instalar nada. Ele não está no repositório do site: é ferramenta de apuração, não parte da publicação.
  3. O conserto seria desenhar um bloco por vez, com catch por diagrama em vez de um catch em volta do run() inteiro. Está anotado como defeito conhecido e não corrigido na data desta publicação — quando for, esta nota vira uma correção datada, que é como este site trata texto que envelheceu.

Anderson Woss

Engenheiro de software. Escrevo aqui sobre sistemas distribuídos que precisam continuar de pé e modelos que precisam parar de inventar.