Migrate from FiveThirtyEight-Style Data Dashboards to Observable Framework: 2025 Guide

Why FiveThirtyEight's Shutdown Is a Wake-Up Call for Data Journalists and Developers

FiveThirtyEight followed a textbook corporate acquisition death spiral. Nate Silver launched it as an independent blog in 2008, sold it to ESPN in 2013, watched it get absorbed into ABC News after Disney acquired ESPN's parent, and then saw Disney quietly kill the editorial operation in 2023 — wiping years of interactive charts, election models, and methodology documentation from the public web. The domain now redirects. The GitHub repositories went dark or became unmaintained. The bespoke D3.js chart components that defined a visual journalism aesthetic are gone.

The risk of building on corporate-owned data platforms

This isn't a FiveThirtyEight-specific problem. Any data team that builds on a platform they don't control faces the same risk:

  • Observable notebooks hosted at observablehq.com can be paywalled or deprecated if the company pivots
  • Datawrapper is excellent but proprietary; export options are limited and pricing can change
  • Tableau Public galleries have been deleted without notice when users violate updated terms
  • Flourish was acquired by Canva in 2022 — another single-vendor dependency
  • Google Data Studio (now Looker Studio) has already been rebranded once and had connectors deprecated

Every one of these platforms has a venture-backed company or a corporate parent that can reprice, pivot, or shut down the product.

What developers actually lost when FiveThirtyEight went dark

Beyond the editorial content, the technical loss was real:

  • Hundreds of public CSV datasets on elections, sports, and economics that were linked from academic papers
  • Documented D3.js patterns for annotations, confidence intervals, and styled tooltips
  • A live reference for how to interleave prose and charts at scale in a CMS

The case for owning your own data visualization stack

A replacement platform must clear this bar:

  • ✅ Self-hostable on infrastructure you control
  • ✅ Open-source license (MIT or Apache 2.0 preferred)
  • ✅ Prose + chart interleaving, not just dashboards
  • ✅ Static output deployable to any CDN
  • ✅ Native support for D3, CSV/Parquet data, and modern JS tooling
  • ✅ Active community with commits in the last 90 days

The four tools below all pass this checklist to varying degrees.


Alternative 1: Observable Framework (formerly Observable Notebooks)

Observable Framework is an open-source static site generator purpose-built for data apps — think Next.js but where every page is a Markdown file that can run JavaScript, query data loaders, and render D3 or Observable Plot charts natively.

What Observable Framework is and how it differs from Observable notebooks

The hosted Observable notebook product (observablehq.com) uses a reactive cell model inside a browser-based editor. Observable Framework, released in 2024 under the ISC license, is a local CLI tool. You write .md files with fenced JavaScript blocks, run a dev server, and npm run build produces a self-contained static site. There is no runtime dependency on observablehq.com.

Pros

  • True self-hostable static output — deploy to S3, GitHub Pages, Cloudflare Pages, or your own Nginx
  • Native D3 and Observable Plot — no config needed, both are bundled automatically
  • Data loaders.js, .py, or .sh files in src/data/ run at build time, not in the browser, keeping secrets server-side
  • File-based routingsrc/elections/results.md becomes /elections/results
  • ISC license — permissive, no CLA, company-backed but open-source

Cons

  • The transition from notebook cells to the file-based model requires a mental shift if your team lives in Observable notebooks
  • Smaller ecosystem than React; third-party component libraries are sparse
  • Server-side data loaders require Node.js at build time, which complicates pure static hosting pipelines
  • No built-in authentication for private dashboards

Quick setup: from zero to running dashboard in 10 minutes

npm create @observablehq@latest my-dashboard
cd my-dashboard
npm install
npm run dev

After scaffolding, your project looks like this:

my-dashboard/
├── src/
│   ├── index.md          # Homepage — Markdown + JS cells
│   ├── data/
│   │   └── elections.csv.js  # Data loader: runs at build, outputs CSV
│   └── components/
│       └── timeline.js   # Reusable JS/Plot component
├── observablehq.config.js  # Site title, theme, sidebar nav
└── package.json

Dev server output:

  Observable Framework 1.x
  ↳ http://localhost:3000/

  src/index.md
  src/data/elections.csv.js  (data loader)

Inside src/index.md, a chart cell looks like:

const data = await FileAttachment("data/elections.csv").csv({typed: true});
Plot.plot({
  marks: [
    Plot.barY(data, {x: "state", y: "margin", fill: "party"})
  ]
})

That's it. Observable Framework handles the bundling, the module resolution, and the reactive re-render on save.


Alternative 2: Evidence.dev — SQL-First Data Apps

Evidence.dev turns SQL queries and Markdown into interactive, version-controlled reports. Write .md files, embed SQL blocks, and Evidence renders charts using its own component library backed by Apache ECharts.

How Evidence turns SQL queries and Markdown into interactive reports

Evidence's model is deliberately opinionated: SQL is the primary data manipulation layer, and JavaScript lives at the edges. This makes it ideal for analyst teams that know SQL fluently but don't want to manage a React build pipeline.

Connecting Evidence to Postgres, DuckDB, BigQuery, or CSV files

Evidence supports Postgres, MySQL, SQLite, DuckDB, BigQuery, Snowflake, and flat CSV files via DuckDB. Connection config lives in evidence.sources.yaml.

Pros

  • SQL-native — analysts own the entire data pipeline without writing JavaScript
  • Version-controlled reports.md files live in Git alongside SQL, enabling PR-based review
  • DuckDB integration — query local CSVs, Parquet files, or S3 objects without a database server
  • Zero-config charting<BarChart>, <LineChart>, <ScatterPlot> components work out of the box

Cons

  • Custom interactivity is limited; you can't drop in arbitrary D3 code without workarounds
  • Chart components are proprietary Evidence wrappers — you're tied to their component API
  • Scrollytelling or narrative journalism layouts aren't a first-class use case
  • The commercial Evidence Cloud hosting is not open-source; the core is MIT

Deploying an Evidence app to Vercel

Here's a complete Evidence page that queries a local elections.csv via DuckDB and renders a grouped bar chart:

---
title: 2024 Election Results by State
---

# 2024 Election Results

Margins by party across swing states.

```sql swing_states
SELECT
  state,
  party,
  SUM(votes) AS total_votes,
  ROUND(SUM(votes) * 100.0 / SUM(SUM(votes)) OVER (PARTITION BY state), 2) AS vote_share
FROM read_csv_auto('sources/elections/elections.csv')
WHERE state IN ('Pennsylvania','Michigan','Wisconsin','Arizona','Nevada')
GROUP BY state, party
ORDER BY state, party
` ` `

<BarChart
  data={swing_states}
  x=state
  y=vote_share
  series=party
  title="Vote Share by State"
  subtitle="2024 General Election — Swing States"
  colorPalette={['#0052cc', '#e03131']}
/>

Deploy to Vercel with a single command:

npm install -g @evidence-dev/evidence
evidence build
# then push to GitHub and connect to Vercel — build command: npm run build

Alternative 3: Grafana with a Static Data Source

Grafana OSS is the battle-tested open-source observability platform that most engineers associate with metrics dashboards — but its JSON API data source and CSV plugin make it viable for public-facing data journalism dashboards too.

Using Grafana beyond time-series: JSON and CSV data sources

Grafana's plugin ecosystem includes marcusolsson-json-datasource (JSON API) and marcusolsson-csv-datasource, both maintained on GitHub. Pair these with Grafana's public dashboard sharing feature (enabled since Grafana 9) and you can expose a read-only dashboard URL without requiring viewer accounts.

Pros

  • Battle-tested at scale — used by thousands of organizations, actively maintained by Grafana Labs
  • Rich plugin ecosystem — 150+ community data source plugins, panel plugins for maps, Sankey diagrams, and more
  • Built-in alerting — if your data story needs monitoring, Grafana handles it natively
  • Public dashboard URLs — shareable without login, embeddable via iframe

Cons

  • Not designed for prose+chart narrative journalism; text panels are rudimentary
  • Dashboard JSON is hard to version-control meaningfully (large, auto-generated JSON blobs)
  • Embedding requires either a running Grafana server or Grafana Cloud — no true static output
  • Learning curve for non-engineers; the UI is optimized for ops teams

Self-hosting Grafana on Docker

# docker-compose.yml
version: '3.8'

services:
  grafana:
    image: grafana/grafana-oss:10.4.0
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=changeme
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
      - GF_FEATURE_TOGGLES_ENABLE=publicDashboards
      - GF_INSTALL_PLUGINS=marcusolsson-json-datasource,marcusolsson-csv-datasource
    volumes:
      - grafana-storage:/var/lib/grafana
      - ./provisioning:/etc/grafana/provisioning
    restart: unless-stopped

volumes:
  grafana-storage:

Place your data source and dashboard provisioning YAML files under ./provisioning/datasources/ and ./provisioning/dashboards/. Grafana will load them on startup, making the entire setup reproducible from Git.


Alternative 4: Svelte + LayerCake for Custom Data Stories

Svelte + LayerCake is the maximum-control option: you build every chart as a composable Svelte component, LayerCake handles the scale/dimension scaffolding, and you deploy a static site that ships almost no JavaScript compared to React-based alternatives.

Why Svelte is uniquely suited for data visualization components

Svelte compiles components to vanilla JS at build time. There's no virtual DOM diffing at runtime, which means your D3 scale calculations and DOM mutations happen in tight, predictable code. Reactive declarations ($:) map naturally to the derived-state pattern that data visualizations require.

LayerCake's composable chart abstraction explained

LayerCake provides a <LayerCake> wrapper that computes x/y scales, dimensions, and padding from your data, then exposes them via Svelte's context API to child components ("layers"). A scatter plot becomes: <LayerCake><Svg><ScatterLayer /><AxisX /><AxisY /><Tooltip />. Each layer is independently swappable.

Pros

  • Maximum flexibility — you can implement any FiveThirtyEight visual pattern: dot plots, confidence ribbons, annotated line charts
  • Tiny bundles — a full scrollytelling page often comes in under 80KB gzipped
  • Great for narrative journalism — Svelte's reactivity integrates cleanly with Intersection Observer scrollytelling libraries
  • Static deploy anywherenpm run build produces plain HTML/CSS/JS

Cons

  • Highest engineering effort of the four options; no out-of-the-box chart components
  • No built-in data fetching or SQL layer; you handle CSV loading manually
  • Smaller talent pool than React if you're hiring

LayerCake scatter plot component

<!-- ScatterPlot.svelte -->
<script>
  import { LayerCake, Svg } from 'layercake';
  import { scaleLinear } from 'd3-scale';
  import ScatterLayer from './layers/ScatterLayer.svelte';
  import AxisX from './layers/AxisX.svelte';
  import AxisY from './layers/AxisY.svelte';
  import Tooltip from './layers/Tooltip.svelte';

  export let data = [];       // Array of { x, y, label } objects
  export let xKey = 'x';
  export let yKey = 'y';
</script>

<div class="chart-container" style="height:400px">
  <LayerCake
    padding={{ top: 20, right: 20, bottom: 40, left: 50 }}
    {data}
    x={xKey}
    y={yKey}
    xScale={scaleLinear()}
    yScale={scaleLinear()}
  >
    <Svg>
      <AxisX />
      <AxisY />
      <ScatterLayer />
    </Svg>
    <!-- Tooltip renders in HTML layer for crisp text -->
    <Tooltip />
  </LayerCake>
</div>

<style>
  .chart-container {
    width: 100%;
    position: relative;
  }
</style>

Load your CSV data in the parent page component using D3's csv() and pass it as the data prop:

// +page.js (SvelteKit)
import { csv } from 'd3-fetch';
import { autoType } from 'd3-dsv';

export async function load() {
  const data = await csv('/data/elections.csv', autoType);
  return { data };
}

Comparison Table: FiveThirtyEight-Style Dashboard Alternatives at a Glance

Feature matrix

| Tool | Self-hostable | SQL-native | Custom JS | Prose+Chart Interleaving | Deploy Complexity | License | |---|---|---|---|---|---|---| | Observable Framework | ✅ Yes | ⚠️ Via data loaders | ✅ Full | ✅ Excellent | Low (static) | ISC | | Evidence.dev | ✅ Yes | ✅ First-class | ⚠️ Limited | ✅ Good | Low (static) | MIT | | Grafana OSS | ✅ Yes | ✅ Via plugins | ⚠️ Panel plugins only | ❌ Poor | Medium (server) | AGPL 3.0 | | Svelte + LayerCake | ✅ Yes | ❌ DIY | ✅ Full | ✅ Excellent | High (custom build) | MIT |

Effort vs. control trade-off

Evidence.dev sits at the low-effort, moderate-control quadrant — perfect for analyst-led teams that want publishable reports without a front-end engineer. Observable Framework is the sweet spot for developers who want D3-native flexibility with reasonable defaults. Svelte+LayerCake sits at maximum control, maximum effort. Grafana is an outlier: low effort for ops dashboards, high effort for narrative storytelling.

Licensing and long-term sustainability notes

Grafana's AGPL 3.0 license means any modifications you distribute must be open-sourced — relevant if you're building a commercial product on top of it. Observable Framework (ISC) and Evidence.dev (MIT) are permissive. Both have active GitHub repositories with commits in the last 30 days as of early 2025. Svelte and LayerCake are MIT and maintained by well-funded teams (Vercel sponsors Svelte development).

The FiveThirtyEight lesson here is directional: prefer tools with permissive open-source licenses, active governance outside a single corporate owner, and static output that doesn't require the vendor's infrastructure to stay online.


Migration Snippet: Porting a D3.js FiveThirtyEight-Style Chart to Observable Framework

Translating inline D3 scripts to Observable's reactive cell model

The core translation is: swap imperative D3 DOM manipulation for Observable Plot's declarative API, and replace d3.csv() fetch calls with FileAttachment. Here's a direct before/after:

Before — vanilla D3 v7 bar chart (standalone HTML script)

// d3-bar-chart.js — vanilla D3 v7, inline script in HTML page
import * as d3 from 'd3';

const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const width = 640 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

d3.csv('elections.csv', d3.autoType).then(data => {
  const svg = d3.select('#chart')
    .append('svg')
    .attr('width', width + margin.left + margin.right)
    .attr('height', height + margin.top + margin.bottom)
    .append('g')
    .attr('transform', `translate(${margin.left},${margin.top})`);

  const x = d3.scaleBand()
    .domain(data.map(d => d.state))
    .range([0, width])
    .padding(0.2);

  const y = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.margin)])
    .range([height, 0]);

  svg.selectAll('.bar')
    .data(data)
    .join('rect')
    .attr('class', 'bar')
    .attr('x', d => x(d.state))
    .attr('y', d => y(d.margin))
    .attr('width', x.bandwidth())
    .attr('height', d => height - y(d.margin))
    .attr('fill', d => d.party === 'Democrat' ? '#0052cc' : '#e03131');

  svg.append('g').attr('transform', `translate(0,${height})`).call(d3.axisBottom(x));
  svg.append('g').call(d3.axisLeft(y));
});

After — Observable Framework Markdown cell using Observable Plot

// Inside src/index.md — fenced ```js block
const elections = await FileAttachment("data/elections.csv").csv({typed: true});

Plot.plot({
  marginLeft: 50,
  marginBottom: 40,
  color: {
    domain: ["Democrat", "Republican"],
    range: ["#0052cc", "#e03131"]
  },
  marks: [
    Plot.barY(elections, {
      x: "state",
      y: "margin",
      fill: "party",
      tip: true          // built-in tooltip, no extra code
    }),
    Plot.ruleY([0])
  ]
})

Loading archived CSV data from GitHub or a self-hosted S3 bucket

If your original data lives on a GitHub repository (like FiveThirtyEight's archived CSVs), you can load it via a data loader that fetches at build time rather than in the browser:

// src/data/elections.csv.js — runs at build time only
const res = await fetch(
  'https://raw.githubusercontent.com/fivethirtyeight/data/master/elections-performance-index/epi2022.csv'
);
process.stdout.write(await res.text());

Migration considerations

The Observable Plot tip: true option replaces an entire custom tooltip component that would take 30-50 lines of D3 in the original codebase. The FileAttachment API enforces that data files live inside your project's src/ directory, which is actually a feature — it prevents the accidental external dependencies that made FiveThirtyEight's data links brittle. For annotations (the hallmark of journalism-style charts), use Plot.text() marks with frameAnchor positioning instead of D3's manual SVG text positioning.


When to Choose Each Alternative: Decision Framework for 2025

Here's the honest decision tree, without hedging:

You are a solo data journalist or analyst who publishes weekly data stories, knows SQL, and wants Git-based version control → Start with Evidence.dev. You'll be publishing in an afternoon. If you hit its limits on custom interactivity within a month, migrate to Observable Framework.

You have a D3.js background and are migrating existing Observable notebooks or standalone D3 scripts → Observable Framework is the direct upgrade path. The FileAttachment API, built-in Observable Plot, and data loaders map almost 1:1 to patterns you already know.

You are an engineering team with existing Kubernetes or Docker infrastructure who wants dashboards alongside your existing Prometheus/Grafana stack → Grafana. You probably already have it running. Add the JSON datasource plugin and CSV datasource, don't reinvent the wheel.

You need pixel-perfect narrative storytelling — the kind with scroll-triggered animations, custom map projections, annotated timelines, or coordinated multi-view interactions → Svelte + LayerCake. Accept that you're building a custom application, not configuring a tool.

You need the fastest time-to-publish — a working, shareable data story in under two hours → Evidence.dev, deploy to Vercel with vercel --prod.

The meta-principle

FiveThirtyEight's shutdown demonstrated that the most expensive part of a data journalism platform isn't the charting library — it's the institutional knowledge of how the data pipeline works. Own your data. Keep raw CSVs in Git. Write data loaders that are plain Node.js or Python scripts, not proprietary connectors. Deploy to infrastructure you can migrate off in a weekend.

All four tools above share one property: a build command that produces static files you can copy to any CDN. That's the 2025 baseline for a durable data visualization stack.