Skip to content

Bar-Bell

Composition

How does a running total stack up, piece by piece? Each row lays its parts end to end with an end-cap at every step, so the cumulative reach and each segment's share both read at a glance.

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

  • Funnels and cumulative build-ups. How the parts stack to a total, row by row, with an end-cap marking every step - pipeline stages, cost build-ups, mileage accumulations.
  • Two audiences, one row. The analyst reads each segment's contribution off the caps; the exec reads the final reach off the row's end. Nobody needs a second chart.
  • If comparing the same segment across rows matters more than each row's running total, the Vertical Stack Bar lines the segments up for you.

Heavy data on WebGPU Experimental

BarBellChart has an opt-in renderer="webgpu" that paints the segment bars and end-cap circles as GPU-instanced marks 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 · ~120 rows… detecting

Play through the years

BarBellChart already uses date for the row's category (the band on the y-axis), so the timeline tag is named period instead. Tag every row with a period and flip on timeline: a year's snapshot is the rows sharing that period, and each segment's length tweens between years. Off by default - nothing changes until a chart opts in. This is interactive year-by-year stepping, not the one-shot entrance further down.

ts
{ period: "2021", date: "Kenya", exports: 40, domestic: 25 }

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<BarBellChartHandle>(null);

<BarBellChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / stepForward()
vue
<BarBellChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" />
svelte
<div use:barBellChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>
ts
applyBarBellChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });
html
<michi-vz-bar-bell-chart id="c"></michi-vz-bar-bell-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.
  • Rows without a period stay visible in every period.
  • timeline wins over progressiveDraw when both are set - the reveal animation further down stays off while the timeline is in control.

Reveal animation

The chart wipes in from left to right on mount, revealing its marks in sequence before settling into place. Off by default - a chart opts in with the progressiveDraw prop.

The marks wipe in from left to right; axes and titles stay put. With reduced motion enabled, the chart renders fully drawn instantly.

progressiveDraw: true enables the defaults (1200 ms, easeInOutCubic). A config object tunes it:

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

<BarBellChart
  ref={ref}
  {...props}
  progressiveDraw={{ durationMs: 2000 }}
/>;
// ref.current?.replay() re-runs the reveal on demand
vue
<BarBellChart :options="{ ...props, progressiveDraw: { durationMs: 2000 } }" />
svelte
<div use:barBellChart={{ ...props, progressiveDraw: { durationMs: 2000 } }}></div>
ts
applyBarBellChartProps(this.c.nativeElement, {
  ...props,
  progressiveDraw: { durationMs: 2000 },
});
html
<michi-vz-bar-bell-chart id="c"></michi-vz-bar-bell-chart>
<script>
  const el = document.getElementById("c");
  el.progressiveDraw = { durationMs: 2000 };
  // el.replay() re-runs the reveal
</script>
  • durationMs and easing ("linear", "easeOutQuad", "easeInOutCubic", or a custom (t) => t function) shape the sweep.
  • autoplay: false renders the chart fully drawn; call replay() (React ref handle, web-component method, or the core instance) to run the reveal on demand. replayOnUpdate: true re-runs it on every data change.
  • Respects prefers-reduced-motion: the chart renders fully drawn instantly.
  • Reveal animation is a one-shot entrance; play through the years above steps through data year by year instead.

Usage

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

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

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

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

// component (uses CUSTOM_ELEMENTS_SCHEMA)
// template: <michi-vz-bar-bell-chart #c></michi-vz-bar-bell-chart>
applyBarBellChartProps(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-bar-bell-chart id="c"></michi-vz-bar-bell-chart>
<script>
  Object.assign(document.getElementById("c"), props); // dataSet/series, title, …
</script>
ts
import { mountBarBellChart } from "@michi-vz/core";

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

API

Props are typed as BarBellChartProps 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.