Skip to content

Gap Chart

Comparison

How far apart are the two numbers that matter? Plot before and after, target and actual, men and women, and the bar between them is the story - the wider the gap, the louder it reads.

Example
canvas · responsive
Go deeper: Insights guide·DevTools guide

The chart above is the same engine in every framework - only the integration code below differs.

When to reach for it

  • Target vs actual, before vs after, forecast vs outturn. Two values per row where the distance between them is the headline - the gap bar IS the finding.
  • Ranking by gap. Sort the rows and the biggest wins (or worst misses) surface instantly - built for the Monday-morning review of who closed their gap.
  • If the absolute sizes matter more than the difference, side-by-side sub-bars on a Comparable bar keep both magnitudes readable.

Heavy data on WebGPU Experimental

GapChart has an opt-in renderer="webgpu" that paints the value1/value2 markers and connecting bars as GPU-instanced shapes while axes, labels and tooltips stay on the SVG layer. It is capability-gated: on a browser without WebGPU it downgrades to canvas automatically, and getContext().renderer reports whichever actually painted.

⚗️ Experimental - not yet stable. WebGPU rendering is an opt-in preview. It needs a WebGPU-capable browser (Chrome / Edge, or Safari 26+); everywhere else it falls back to canvas automatically. Axes, labels and tooltips stay on the SVG layer - only the data marks are painted on the GPU.
Heavy-data demo · ~195 countries… detecting

Play through the years

Give every row a date and flip on timeline: the chart becomes a year-by-year story with its own play button and scrubber, snapshotting one period at a time. Off by default - nothing changes until a chart opts in.

Press the play button under the chart: it steps through the years, one snapshot at a time. Drag the scrubber to jump to any year.

tsx
const ref = useRef<GapChartHandle>(null);

<GapChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / stepForward()
vue
<GapChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" />
svelte
<div use:gapChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>
ts
applyGapChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });
html
<michi-vz-gap-chart id="c"></michi-vz-gap-chart>
<script>
  const el = document.getElementById("c");
  el.timeline = { speedMs: 1000, loop: true };
  // el.getTimeline() -> play() / pause() / seek(year)
</script>
  • speedMs sets the pace, loop wraps around, autoplay: true starts on mount, showControl: false hides the built-in bar.
  • Values glide between periods by default (interpolate); tune the motion with tweenMs and easing, or set interpolate: false for hard cuts. Reduced motion always gets the hard cut.
  • The headless controller is always available: chart.timeline() exposes play() / pause() / toggle() / seek(period) / stepForward() / stepBack(), plus onStep and formatPeriod in the config for custom UI.
  • A filter (top-N, sorting) still applies inside each period, so a "top 5 per year" race works out of the box.
  • Rows without a date stay visible in every period.

Usage

tsx
import { GapChart } from "@michi-vz/react";

export default () => <GapChart {...props} />; // props = the chart options
vue
<script setup>
import { GapChart } from "@michi-vz/vue";
</script>

<template>
  <GapChart :options="props" />
</template>
svelte
<script>
  import { gapChart } from "@michi-vz/svelte";
</script>

<div use:gapChart={props}></div>
ts
// main.ts - register the elements once
import "@michi-vz/angular";
import { applyGapChartProps } from "@michi-vz/angular";

// component (uses CUSTOM_ELEMENTS_SCHEMA)
// template: <michi-vz-gap-chart #c></michi-vz-gap-chart>
applyGapChartProps(this.c.nativeElement, props);
html
<script type="module" src="https://cdn.jsdelivr.net/npm/@michi-vz/wc/dist/michi-vz-wc.bundle.js"></script>

<michi-vz-gap-chart id="c"></michi-vz-gap-chart>
<script>
  Object.assign(document.getElementById("c"), props); // dataSet/series, title, …
</script>
ts
import { mountGapChart } from "@michi-vz/core";

const chart = mountGapChart(el, props);
chart.update(next);
chart.getContext(); // renderer-agnostic, LLM-ready
chart.destroy();

API

Props are typed as GapChartProps in @michi-vz/core. Shared across all charts: width, height, margin, colors / colorsMapping, renderer ("svg", "canvas", or experimental "webgpu"), highlightItems, disabledItems, and the on* callbacks. onChartDataProcessed / getContext() return the renderer-agnostic ChartContext.