Horizon chart
many series, little space
Many series in little space. Takes a moment to learn, then very dense.
- Format.d3
- Length91 lines
- Includesnone
The source
91 lines of D3 JavaScript, and uses only the bundled D3 build. Copy it, or open the template inside Gnomon and render it as it is.
// Horizon chart — the answer to "I have 12 time series and one screen".
// The series is folded into N bands and each band overplotted at increasing
// saturation, so each row needs a fraction of the height a line chart would.
// Reading exact values is harder; spotting *when* things went wrong is much
// easier.
const BANDS = 3;
const rng = d3.randomLcg(5);
const names = ['orders', 'catalogue', 'payments', 'identity', 'search', 'pricing'];
// Every series is drawn BANDS times over, twice (above and below), through
// `curveBasis` — so the emitted path data grows as points × series × bands × 2.
// 6 × 80 keeps the SVG in the tens of KB; pushing both up an order of
// magnitude is what turns a horizon chart into a megabyte of <path>.
const points = 80;
const series = names.map((name, i) => ({
name,
values: d3.range(points).map(t =>
30 * Math.sin((t + i * 14) / 11) + 18 * Math.sin(t / 4.5) + rng() * 14 - 7),
}));
const margin = { top: 34, right: 20, bottom: 20, left: 90 };
const rowHeight = 30;
const boxHeight = margin.top + series.length * (rowHeight + 3) + margin.bottom;
const x = d3.scaleLinear().domain([0, points - 1])
.range([margin.left, width - margin.right]);
const maxAbs = d3.max(series, s => d3.max(s.values, Math.abs));
const y = d3.scaleLinear().domain([0, maxAbs / BANDS]).range([rowHeight, 0]);
const area = d3.area()
.x((_, i) => x(i))
.y0(rowHeight)
.y1(d => y(d))
.curve(d3.curveBasis);
const positive = d3.scaleSequential([-1, BANDS], d3.interpolateBlues);
const negative = d3.scaleSequential([-1, BANDS], d3.interpolateReds);
const svg = d3.select(container).append('svg')
.attr('viewBox', [0, 0, width, boxHeight])
.attr('width', width)
.attr('height', boxHeight)
.attr('font-family', 'system-ui, sans-serif')
.attr('font-size', 11);
const row = svg.selectAll('g.row')
.data(series)
.join('g')
.attr('class', 'row')
.attr('transform', (_, i) => `translate(0,${margin.top + i * (rowHeight + 3)})`);
// One clip per row, or band 2 of a tall spike would bleed into the row above.
row.append('clipPath')
.attr('id', (_, i) => `horizon-clip-${i}`)
.append('rect')
.attr('x', margin.left)
.attr('y', 0)
.attr('width', width - margin.left - margin.right)
.attr('height', rowHeight);
row.each(function (s, rowIndex) {
const g = d3.select(this).append('g')
.attr('clip-path', `url(#horizon-clip-${rowIndex})`);
for (let band = 0; band < BANDS; band++) {
// Shift the series down by one band and redraw: what pokes above the
// baseline is the part that exceeded that band.
const shift = band * (maxAbs / BANDS);
g.append('path')
.attr('fill', positive(band))
.attr('d', area(s.values.map(v => Math.max(0, v - shift))));
g.append('path')
.attr('fill', negative(band))
.attr('d', area(s.values.map(v => Math.max(0, -v - shift))));
}
});
row.append('text')
.attr('x', margin.left - 12)
.attr('y', rowHeight / 2)
.attr('dy', '0.35em')
.attr('text-anchor', 'end')
.attr('fill', theme.foreground)
.text(d => d.name);
svg.append('text')
.attr('x', margin.left)
.attr('y', 20)
.attr('fill', theme.muted)
.text(`error rate vs baseline · ${BANDS} bands · blue above, red below`);Render this offline
This template ships in Gnomon and renders on your machine, with no account and nothing sent to a server. The browser editor is free and needs no install.
Others in D3 visualisations
- Sankey — request flowFlow with volume. The best chart here for "where does it all go".
- Chord — service interactionWho talks to whom, when the traffic is bidirectional.
- Arc diagram — dependenciesNodes on one axis, arcs above. Readable where a force graph is not, provided the ordering means something.
- Adjacency matrix — couplingAdjacency matrix. Unfashionable, and better than a force graph for dense dependencies.
- Edge bundling — module importsHierarchical edge bundling. For import graphs big enough that straight edges become a hairball.
- Sunburst — nested spendNested hierarchy, radially. Prettier than an icicle, harder to compare.
- Icicle — nested spend (linear)A sunburst unrolled flat. Harder to love, much easier to compare siblings.
- Circle packing — nested sizeCircle packing. Nested size when the nesting matters more than reading exact areas.
- Treemap — portfolio costNested size. Good for cost, storage, lines of code.
- Tidy tree — structureA tidy tree. The default for anything with one parent per node.
- Radial tree — structure (radial)The same tree bent into a circle. Fits more depth on a slide, costs you easy comparison.
- Dendrogram — clusteringClustering, with join height carrying the distance. Not merely a tree with curves.
- Indented tree — file/spec outlineA file or spec outline. The chart that looks like the thing it describes.
- Force graph — service mapService map. Use for clusters, not for reading individual edges.
- Force graph — disjoint clustersForce graph that keeps unconnected clusters apart rather than flinging them off screen.
- Force graph — radial tiersForce layout pinned to rings, so tier is a position instead of a colour legend.
- Directed graph — call directionDirected edges with arrowheads. For when direction is the question, not just adjacency.
- Force tree — blast radiusBlast radius from one node: what breaks if this goes.
- Calendar heatmap — daily activityDaily activity over a year: deploys, incidents, commits.
- Streamgraph — shifting mixShifting composition over time. Good for the mix, poor for reading any single value.
- Gantt — delivery roadmapDelivery roadmap, rendered from data.
- Radar — capability scoringCapability scoring across axes. Fine for one subject, misleading with four overlaid.
- Bullet chart — target vs actualTarget against actual, in one line. The chart a gauge wishes it were.
- Beeswarm — distribution by groupEvery point, grouped, without the overplotting a strip plot suffers.
- Slope chart — before and afterBefore and after, two points, one line each. Devastatingly clear.
- Parallel coordinates — multi-criteriaMulti-criteria comparison: for option analysis.
- Box plot — latency distributionLatency distribution. The chart that shows the tail a mean hides.
- Grouped bar — category comparisonCategory comparison. Unglamorous, and usually the right answer.
- Multi-line — metrics over timeMetrics over time. Keep it under about five series, or switch to horizon.