Skip to main content

Guide

Mermaid vs D3.js vs Chart.js: Visualization 2026

Compare Mermaid, D3.js, and Chart.js for creating diagrams and data visualizations in JavaScript. Flowcharts, interactive charts, and custom visualizations.

·PkgPulse Team·
0

TL;DR

Mermaid generates diagrams from text — flowcharts, sequence diagrams, Gantt charts, ER diagrams, all from Markdown-like syntax, no coding needed, used in GitHub, Notion, and docs. D3.js is the low-level data visualization library — binds data to DOM/SVG, full control over every pixel, powers the NYT and Observable, steep learning curve. Chart.js is the simple charting library — responsive charts with minimal code, canvas-based, 8 chart types built-in, great for dashboards. In 2026: Mermaid for documentation diagrams, D3.js for custom/complex visualizations, Chart.js for standard dashboard charts.

Key Takeaways

  • Mermaid: ~3M weekly downloads — text-to-diagram, Markdown, no JS coding needed
  • D3.js: ~5M weekly downloads — full-control SVG/DOM, custom visualizations
  • Chart.js: ~5M weekly downloads — simple API, canvas, 8 chart types, responsive
  • Completely different tools: diagrams (Mermaid), custom viz (D3), charts (Chart.js)
  • Mermaid requires no JavaScript — just text definitions
  • D3 has the steepest learning curve but maximum flexibility

Mermaid

Mermaid — text-to-diagram:

Flowchart

```mermaid
flowchart TD
    A[Start] --> B{Is it working?}
    B -- Yes --> C[Great!]
    B -- No --> D[Debug]
    D --> E[Fix the bug]
    E --> B
    C --> F[Deploy]

### Sequence diagram

```markdown
```mermaid
sequenceDiagram
    participant Client
    participant API
    participant Database

    Client->>API: POST /api/packages
    API->>Database: INSERT package
    Database-->>API: OK
    API-->>Client: 201 Created

    Client->>API: GET /api/packages
    API->>Database: SELECT * FROM packages
    Database-->>API: [packages]
    API-->>Client: 200 [packages]

### ER diagram

```markdown
```mermaid
erDiagram
    USER ||--o{ PACKAGE : creates
    PACKAGE ||--|{ VERSION : has
    PACKAGE ||--o{ DOWNLOAD : tracks
    USER {
        string id PK
        string name
        string email
    }
    PACKAGE {
        string id PK
        string name
        string description
        string userId FK
    }
    VERSION {
        string id PK
        string version
        date publishedAt
        string packageId FK
    }

### JavaScript API

```typescript
import mermaid from "mermaid"

// Initialize:
mermaid.initialize({
  startOnLoad: true,
  theme: "dark",
  securityLevel: "loose",
})

// Render programmatically:
const { svg } = await mermaid.render("diagram-1", `
  flowchart LR
    A[npm install] --> B[Build]
    B --> C[Test]
    C --> D[Deploy]
`)

document.getElementById("output").innerHTML = svg

D3.js

D3.js — data-driven documents:

Bar chart

import * as d3 from "d3"

const data = [
  { name: "react", downloads: 25000000 },
  { name: "vue", downloads: 5000000 },
  { name: "svelte", downloads: 2000000 },
  { name: "solid", downloads: 500000 },
]

const width = 600, height = 400, margin = { top: 20, right: 20, bottom: 40, left: 80 }

const svg = d3.select("#chart")
  .append("svg")
  .attr("width", width)
  .attr("height", height)

const x = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.downloads)!])
  .range([margin.left, width - margin.right])

const y = d3.scaleBand()
  .domain(data.map(d => d.name))
  .range([margin.top, height - margin.bottom])
  .padding(0.2)

svg.selectAll("rect")
  .data(data)
  .join("rect")
  .attr("x", margin.left)
  .attr("y", d => y(d.name)!)
  .attr("width", d => x(d.downloads) - margin.left)
  .attr("height", y.bandwidth())
  .attr("fill", "#3b82f6")

// Add axes:
svg.append("g")
  .attr("transform", `translate(0,${height - margin.bottom})`)
  .call(d3.axisBottom(x).tickFormat(d => `${d / 1e6}M`))

svg.append("g")
  .attr("transform", `translate(${margin.left},0)`)
  .call(d3.axisLeft(y))

Interactive line chart

import * as d3 from "d3"

const data = [
  { date: new Date("2026-01"), value: 100 },
  { date: new Date("2026-02"), value: 150 },
  { date: new Date("2026-03"), value: 130 },
  // ...
]

const x = d3.scaleTime()
  .domain(d3.extent(data, d => d.date) as [Date, Date])
  .range([margin.left, width - margin.right])

const y = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.value)!])
  .range([height - margin.bottom, margin.top])

const line = d3.line<typeof data[0]>()
  .x(d => x(d.date))
  .y(d => y(d.value))
  .curve(d3.curveMonotoneX)

svg.append("path")
  .datum(data)
  .attr("d", line)
  .attr("fill", "none")
  .attr("stroke", "#3b82f6")
  .attr("stroke-width", 2)

// Add tooltip on hover:
svg.selectAll("circle")
  .data(data)
  .join("circle")
  .attr("cx", d => x(d.date))
  .attr("cy", d => y(d.value))
  .attr("r", 4)
  .attr("fill", "#3b82f6")
  .on("mouseover", (event, d) => {
    tooltip.style("opacity", 1)
      .html(`${d.date.toLocaleDateString()}: ${d.value}`)
      .style("left", `${event.pageX + 10}px`)
      .style("top", `${event.pageY - 10}px`)
  })

Treemap

import * as d3 from "d3"

const data = {
  name: "npm",
  children: [
    { name: "react", value: 25000000 },
    { name: "next", value: 8000000 },
    { name: "vue", value: 5000000 },
    { name: "express", value: 30000000 },
    { name: "axios", value: 50000000 },
  ],
}

const root = d3.treemap<typeof data>()
  .size([width, height])
  .padding(2)(
    d3.hierarchy(data).sum(d => d.value)
  )

svg.selectAll("rect")
  .data(root.leaves())
  .join("rect")
  .attr("x", d => d.x0)
  .attr("y", d => d.y0)
  .attr("width", d => d.x1 - d.x0)
  .attr("height", d => d.y1 - d.y0)
  .attr("fill", (_, i) => d3.schemeTableau10[i])

Chart.js

Chart.js — simple charting:

Bar chart

import { Chart } from "chart.js/auto"

new Chart(document.getElementById("chart") as HTMLCanvasElement, {
  type: "bar",
  data: {
    labels: ["react", "vue", "svelte", "solid"],
    datasets: [{
      label: "Weekly Downloads (M)",
      data: [25, 5, 2, 0.5],
      backgroundColor: ["#61dafb", "#42b883", "#ff3e00", "#446b9e"],
      borderRadius: 6,
    }],
  },
  options: {
    responsive: true,
    plugins: {
      title: { display: true, text: "Framework Downloads" },
      legend: { display: false },
    },
    scales: {
      y: { beginAtZero: true, title: { display: true, text: "Downloads (M)" } },
    },
  },
})

Line chart with multiple datasets

import { Chart } from "chart.js/auto"

new Chart(canvas, {
  type: "line",
  data: {
    labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
    datasets: [
      {
        label: "react",
        data: [22, 23, 24, 24, 25, 25],
        borderColor: "#61dafb",
        tension: 0.3,
      },
      {
        label: "vue",
        data: [4.5, 4.6, 4.8, 4.9, 5.0, 5.1],
        borderColor: "#42b883",
        tension: 0.3,
      },
    ],
  },
  options: {
    responsive: true,
    interaction: { mode: "index", intersect: false },
    plugins: {
      title: { display: true, text: "Download Trends (2026)" },
    },
  },
})

Doughnut / Pie chart

import { Chart } from "chart.js/auto"

new Chart(canvas, {
  type: "doughnut",
  data: {
    labels: ["npm", "pnpm", "yarn", "bun"],
    datasets: [{
      data: [60, 25, 10, 5],
      backgroundColor: ["#cb3837", "#f69220", "#2c8ebb", "#fbf0df"],
      borderWidth: 0,
    }],
  },
  options: {
    responsive: true,
    plugins: {
      title: { display: true, text: "Package Manager Market Share" },
    },
  },
})

React wrapper

import { Bar, Line, Doughnut } from "react-chartjs-2"
import { Chart, registerables } from "chart.js"

Chart.register(...registerables)

function DownloadsChart() {
  return (
    <Bar
      data={{
        labels: ["react", "vue", "svelte"],
        datasets: [{
          label: "Downloads (M)",
          data: [25, 5, 2],
          backgroundColor: ["#61dafb", "#42b883", "#ff3e00"],
        }],
      }}
      options={{
        responsive: true,
        plugins: { legend: { display: false } },
      }}
    />
  )
}

Feature Comparison

FeatureMermaidD3.jsChart.js
PurposeText-to-diagramCustom data vizStandard charts
RenderingSVGSVG/DOMCanvas
InputText/MarkdownData + codeData + config
Coding required❌ (text only)✅ (complex)✅ (simple)
Chart typesDiagrams onlyUnlimited8 built-in
Flowcharts✅ (manual)
Sequence diagrams
ER diagrams
InteractiveBasic✅ (full control)✅ (tooltips, zoom)
Animations
ResponsiveManual
Learning curveLowHighLow
React bindingsVia dangerouslySetInnerHTMLManualreact-chartjs-2
Used byGitHub, Notion, docsNYT, ObservableDashboards
Weekly downloads~3M~5M~5M

When to Use Each

Use Mermaid if:

  • Creating diagrams for documentation (flowcharts, sequence, ER)
  • Want to define diagrams in Markdown (GitHub renders natively)
  • No JavaScript coding needed
  • Building docs with VitePress, Docusaurus, or similar

Use D3.js if:

  • Need fully custom visualizations (no template constraints)
  • Building interactive data explorers or dashboards
  • Need maps, treemaps, force graphs, or novel chart types
  • Want complete control over every SVG element

Use Chart.js if:

  • Need standard charts quickly (bar, line, pie, doughnut, radar)
  • Building admin dashboards or analytics pages
  • Want responsive charts with minimal code
  • Using React (react-chartjs-2)

Migration Guide

From Chart.js to D3.js for custom visualizations

When Chart.js's built-in chart types are insufficient for custom visualizations, migrating to D3.js requires a mental model shift from configuration to SVG construction:

// Chart.js (old) — configuration-based
import { Chart } from "chart.js/auto"
new Chart(canvas, {
  type: "bar",
  data: { labels: ["react", "vue"], datasets: [{ data: [25, 5] }] },
  options: { responsive: true }
})

// D3.js (new) — SVG construction
import * as d3 from "d3"
const data = [{ name: "react", value: 25 }, { name: "vue", value: 5 }]
const x = d3.scaleBand().domain(data.map(d => d.name)).range([0, width]).padding(0.1)
const y = d3.scaleLinear().domain([0, 25]).range([height, 0])
svg.selectAll("rect").data(data).join("rect")
  .attr("x", d => x(d.name)!).attr("y", d => y(d.value))
  .attr("width", x.bandwidth()).attr("height", d => height - y(d.value))

The D3 migration is significant in scope. Plan for it only when Chart.js genuinely cannot express the visualization you need (custom projections, force simulations, hierarchical layouts).

Community Adoption in 2026

D3.js and Chart.js each reach approximately 5 million weekly downloads, representing very different audiences. D3 is used by data journalists at the New York Times and Washington Post, by Observable notebook authors, and by teams building bespoke data explorers. Chart.js serves a much broader audience of application developers who need standard charts embedded in dashboards, admin panels, and analytics pages.

Mermaid has approximately 3 million weekly downloads, but its actual usage is far larger than this number suggests. GitHub renders Mermaid diagrams natively in Markdown files and pull requests, so engineers writing documentation use Mermaid without ever installing it. The npm download count represents programmatic usage (documentation site generators, wikis) rather than the full audience. Notion, Confluence, and dozens of documentation platforms support Mermaid rendering natively.

The three libraries occupy non-overlapping niches. Teams comparing them are typically asking: "Should I create diagrams (Mermaid), display data in standard charts (Chart.js), or build a fully custom visualization (D3)?" The answer depends on the output type, not a preference between competing implementations of the same feature.

Accessibility and Export Options

Data visualization accessibility and export capabilities are increasingly important as organizations face accessibility compliance requirements and need to embed charts in reports and presentations.

Screen reader compatibility requires both semantic HTML structure and ARIA attributes. Chart.js 4 includes ARIA labels on <canvas> elements and supports the plugins.accessibility configuration for custom descriptions. D3.js requires manual ARIA implementation — the library produces SVG that needs role="img", aria-label, and aria-describedby attributes added manually. Mermaid diagrams rendered as SVG can be made accessible by adding %% accessible to the diagram source, which triggers ARIA role annotations on diagram elements. For applications that must meet WCAG 2.1 AA, evaluating the accessibility story of each library before committing is essential.

Export to image or PDF is a common requirement for dashboards and reports. Chart.js's canvas.toDataURL('image/png') exports the current chart state as a PNG — straightforward for Canvas-based rendering. D3.js exports to SVG, which can be serialized with new XMLSerializer().serializeToString(svgElement) and saved, or rasterized using Canvas in Node.js. Mermaid's CLI (mmdc) generates PNG, SVG, and PDF output from diagram source files, making it suitable for documentation pipelines where diagrams are generated at build time.

Server-side rendering for SEO or PDF generation works differently for each library. Chart.js supports server-side rendering via chartjs-node-canvas, which uses the Node.js Canvas implementation. D3.js works server-side natively since SVG is just text — d3.select(virtualDom).append('svg') works with jsdom or happy-dom. Mermaid has first-class server-side rendering via its CLI and Node.js API. For Next.js applications that need charts in server-rendered pages (for social sharing previews or PDF export), the server-side rendering story differs significantly between libraries.

For complex interactive dashboards in 2026, consider whether the visualization library needs to handle both development-time diagram generation (Mermaid) and runtime data visualization (Chart.js, D3). Many documentation sites use Mermaid for architecture diagrams embedded in MDX while using Chart.js for interactive dashboard widgets — different tools serving different purposes in the same codebase.

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on Mermaid v11.x, D3.js v7.x, and Chart.js v4.x.

Compare visualization libraries and frontend tooling on PkgPulse →

The Mermaid live editor (mermaid.live) deserves mention as a productivity tool that changes how teams create and iterate on diagrams. Rather than writing Mermaid syntax locally and checking the rendered output in a documentation site build, the live editor provides instant rendering with side-by-side preview, syntax highlighting, and a shareable link for the diagram. For architectural decision records, onboarding documentation, and API sequence documentation, the ability to share a live Mermaid editor link (rather than an image) means collaborators can view, copy, and modify the diagram directly. GitHub's native Mermaid rendering in Markdown files and pull request descriptions extends this further — a sequence diagram in a PR description renders inline without any tool installation, making it the fastest path from "diagram idea" to "diagram visible to teammates."

A practical distinction that rarely appears in library comparison articles: Chart.js renders to a <canvas> element, while D3.js renders to SVG (and occasionally HTML). The canvas vs SVG choice has real downstream consequences. Canvas renders faster for large datasets (thousands of data points) because it draws pixels rather than maintaining a DOM tree, but it produces a bitmap that cannot be selected, styled with CSS, or introspected with JavaScript after rendering. SVG elements are full DOM nodes — you can attach event listeners to individual bars or data points, apply CSS animations, and inspect the rendered structure with browser developer tools. For interactive data exploration where users click on specific data points or need to tooltip individual elements, D3's SVG approach is more natural. For dense time-series charts with hundreds of data points per pixel, Chart.js's canvas rendering handles the volume without DOM overhead.

See also: AVA vs Jest and Cytoscape.js vs vis-network vs Sigma.js, Fabric.js vs Konva vs PixiJS: Canvas & 2D Graphics 2026.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.