Dual Horizontal Bar (Tornado)
Comparison
Which side wins, and by how much? Anchor two opposing values to a shared centre line and the imbalance reads at a glance - left vs right, men vs women, before vs after. The classic population pyramid and tornado chart, where the longest bar is the story.
The chart above is the same engine in every framework - only the integration code below differs.
When to reach for it
- When asymmetry is the story. Population pyramids, imports vs exports, promoters vs detractors: two opposing magnitudes on one centre line, and the lopsided side speaks first.
- Executive one-pagers. The longest bar and the heavier side communicate before a single number is read - ideal when the audience has ten seconds.
- If the two values do not oppose each other (this year vs last year, target vs actual), keep both on the same side of zero with a Comparable bar.
Heavy data on WebGPU Experimental
DualHorizontalBarChart has an opt-in renderer="webgpu" that paints value1/value2 bars on the GPU 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.
Play through the years
Give every row a date and flip on timeline: the tornado chart becomes a year-by-year story with its own play button and scrubber, snapshotting one period's left/right balance 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.
const ref = useRef<DualHorizontalBarChartHandle>(null);
<DualHorizontalBarChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / stepForward()<DualHorizontalBarChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" /><div use:dualHorizontalBarChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>applyDualHorizontalBarChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });<michi-vz-dual-horizontal-bar-chart id="c"></michi-vz-dual-horizontal-bar-chart>
<script>
const el = document.getElementById("c");
el.timeline = { speedMs: 1000, loop: true };
// el.getTimeline() -> play() / pause() / seek(year)
</script>speedMssets the pace,loopwraps around,autoplay: truestarts on mount,showControl: falsehides the built-in bar.- Values glide between periods by default (
interpolate); tune the motion withtweenMsandeasing, or setinterpolate: falsefor hard cuts. Reduced motion always gets the hard cut. - The headless controller is always available:
chart.timeline()exposesplay() / pause() / toggle() / seek(period) / stepForward() / stepBack(), plusonStepandformatPeriodin 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
datestay visible in every period.
Usage
import { DualHorizontalBarChart } from "@michi-vz/react";
export default () => <DualHorizontalBarChart {...props} />; // props = the chart options<script setup>
import { DualHorizontalBarChart } from "@michi-vz/vue";
</script>
<template>
<DualHorizontalBarChart :options="props" />
</template><script>
import { dualHorizontalBarChart } from "@michi-vz/svelte";
</script>
<div use:dualHorizontalBarChart={props}></div>// main.ts - register the elements once
import "@michi-vz/angular";
import { applyDualHorizontalBarChartProps } from "@michi-vz/angular";
// component (uses CUSTOM_ELEMENTS_SCHEMA)
// template: <michi-vz-dual-horizontal-bar-chart #c></michi-vz-dual-horizontal-bar-chart>
applyDualHorizontalBarChartProps(this.c.nativeElement, props);<script type="module" src="https://cdn.jsdelivr.net/npm/@michi-vz/wc/dist/michi-vz-wc.bundle.js"></script>
<michi-vz-dual-horizontal-bar-chart id="c"></michi-vz-dual-horizontal-bar-chart>
<script>
Object.assign(document.getElementById("c"), props); // dataSet/series, title, …
</script>import { mountDualHorizontalBarChart } from "@michi-vz/core";
const chart = mountDualHorizontalBarChart(el, props);
chart.update(next);
chart.getContext(); // renderer-agnostic, LLM-ready
chart.destroy();API
Props are typed as DualHorizontalBarChartProps 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.
