Fountain (Jet d'Eau)
Experimental Comparison
Experimental - not yet stable
Unlike the other 16 charts (which are stable), the Fountain chart is experimental: its API, visuals, and ChartContext shape may change in future releases. It is a storytelling / communication mark, not a precision analysis tool - see When it earns its place. Pin a version if you depend on it.
Geneva pumps 500 litres a second into the sky. You photograph the jet. You never photograph the tonnes of water falling back unseen - the spray the column is actually made of. Most numbers are shaped like that: a bright visible peak, standing on a hidden mass nobody credits. The Fountain chart draws both at once - the headline you report, and the thing quietly eroding it (or holding it up).
- The spike apex is the number - read it off the y-axis, precisely. It's the strongest channel a chart has.
- The spray is a flag, not a ruler - "this one is bleeding / this one is shaky." The exact second figure lives on the tooltip and in
getContext()(spreadRatio), never measured off the plume's width.
So it is an honest storytelling and attribution chart: revenue booked vs revenue leaking, sales secured vs shrink, the stars you see vs the maintainers you don't. It is not a precision analysis tool - for that, reach for Fan (uncertainty bands), Vertical Stack Bar (sortable secured + at-risk), or a waterfall. See When it earns its place.
The default style: "jet" is the faithful Jet d'Eau: a tall, narrow column, dense at the base, fraying into a soft crown that drifts downwind. A more symmetric style: "plume" (an upright column with a feathery bloom and a mist skirt) is also available - see Two silhouettes.
Anatomy: how to read a fountain
Every visible part of the glyph has one stated meaning. Nothing else carries data.
- Apex - THE number. It sits on a real, labelled y-axis and it is the only thing you measure.
- Stem - the body of the number. Decorative; its width never encodes anything.
- Crown / froth - the flag: "this one is shaky / this one is bleeding." Wide and frothy means look closer; the exact second figure lives on the tooltip and in
getContext().jets[].spreadRatio, never in the width you see. - Symmetric vs leaning crown - symmetry is a signal. An upright crown says the spread is balanced (it could swing either way). A leaning crown says the hidden mass hangs on one side - a late tail, downside-heavy risk. Read only the direction; the skew figure is on the tooltip (
jets[].lean). - The wind - a jet that encodes no
leanstill drifts gently to one side. That shared drift is the Jet d'Eau signature (wind over the lake), purely decorative: every such jet drifts the same way, andleanisnullin the context. - Droplets and mist (plume style) - decoration; the droplet count scales with the optional
densityfield.
One chart, two modes - decided by the x-axis type. Set
xAxisDataType: "band"for Snapshot mode: one jet per category, comparing magnitudes side by side (fountains, cities, products). Use a temporal or numeric x ("date_annual","date_monthly","number") for Trend mode: a jet per period, the rising apexes trace the trend while each plume shows that period's volatility, and a forecast jet renders dashed with a wider, frothier crown.
Heavy data on WebGPU Experimental
FountainChart has an opt-in renderer="webgpu" that paints each jet's column and frayed plume 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. The shared sideways drift you see across the jets is the decorative wind (none of these items encode a lean), not data.
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:
const ref = useRef<FountainChartHandle>(null);
<FountainChart
ref={ref}
{...props}
progressiveDraw={{ durationMs: 2000 }}
/>;
// ref.current?.replay() re-runs the reveal on demand<FountainChart :options="{ ...props, progressiveDraw: { durationMs: 2000 } }" /><div use:fountainChart={{ ...props, progressiveDraw: { durationMs: 2000 } }}></div>applyFountainChartProps(this.c.nativeElement, {
...props,
progressiveDraw: { durationMs: 2000 },
});<michi-vz-fountain-chart id="c"></michi-vz-fountain-chart>
<script>
const el = document.getElementById("c");
el.progressiveDraw = { durationMs: 2000 };
// el.replay() re-runs the reveal
</script>durationMsandeasing("linear", "easeOutQuad", "easeInOutCubic", or a custom(t) => tfunction) shape the sweep.autoplay: falserenders the chart fully drawn; callreplay()(React ref handle, web-component method, or the core instance) to run the reveal on demand.replayOnUpdate: truere-runs it on every data change.- Respects
prefers-reduced-motion: the chart renders fully drawn instantly.
Play through the years
The data already spans years, so there is nothing to tag. Flip on timeline and the chart's own play button and scrubber step through those years: at each step the jets draw only up to the active year, and playing forward smoothly extends the trend further as the sweep advances. Scrub backward and the trend retracts to match. Hover only ever inspects what has actually been drawn. The timeline applies to Trend mode (temporal or numeric x); Snapshot mode is categorical with no year axis, so jets carry no date and the control simply does not render. 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<FountainChartHandle>(null);
<FountainChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / stepForward()<FountainChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" /><div use:fountainChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>applyFountainChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });<michi-vz-fountain-chart id="c"></michi-vz-fountain-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.- The headless controller is always available:
chart.timeline()exposesplay() / pause() / toggle() / seek(period) / stepForward() / stepBack(), plusonStepandformatPeriodin the config for custom UI. - Values glide between years by default (
interpolate); setinterpolate: falsefor hard jump-cuts. Reduced motion always jump-cuts. timelinewins overprogressiveDrawwhen both are set on the same chart.
Usage
import { FountainChart } from "@michi-vz/react";
export default () => <FountainChart {...props} />; // props = the chart options<script setup>
import { FountainChart } from "@michi-vz/vue";
</script>
<template>
<FountainChart :options="props" />
</template><script>
import { fountainChart } from "@michi-vz/svelte";
</script>
<div use:fountainChart={props}></div>// main.ts - register the elements once
import "@michi-vz/angular";
import { applyFountainChartProps } from "@michi-vz/angular";
// component (uses CUSTOM_ELEMENTS_SCHEMA)
// template: <michi-vz-fountain-chart #c></michi-vz-fountain-chart>
applyFountainChartProps(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-fountain-chart id="c"></michi-vz-fountain-chart>
<script>
Object.assign(document.getElementById("c"), props); // dataSet, …
</script>import { mountFountainChart } from "@michi-vz/core";
const chart = mountFountainChart(el, props);
chart.update(next);
chart.getContext(); // renderer-agnostic, LLM-ready
chart.destroy();Snapshot mode (categorical x)
Pass xAxisDataType: "band" (or omit it; "band" is the default). Each dataSet item becomes one jet, placed in its own x-band. This is the comparison mode: heights answer "which is bigger?" and plume widths answer "which is most uncertain?"
const props = {
xAxisDataType: "band",
dataSet: [
{ label: "Jet d'Eau", value: 140, spread: 20 },
{ label: "King Fahd", value: 312, spread: 35 },
{ label: "World Cup", value: 185, spread: 15 },
{ label: "Bellagio", value: 84, spread: 8 },
],
};Trend mode (temporal or numeric x)
Provide a temporal or numeric xAxisDataType and supply a date on each item. The jets are laid out along the time axis; a trend line threads their apexes. A predicted: true item renders dashed with a visibly frothier plume - the forecast look.
const props = {
xAxisDataType: "date_annual",
dataSet: [
{ label: "2020", date: 2020, value: 42, spread: 5 },
{ label: "2021", date: 2021, value: 51, spread: 6 },
{ label: "2022", date: 2022, value: 63, spread: 8 },
{ label: "2023", date: 2023, value: 70, spread: 10 },
{ label: "2024", date: 2024, value: 78, spread: 14, predicted: true },
{ label: "2025", date: 2025, value: 85, spread: 20, predicted: true },
],
};Best for 5-12 periods in trend mode
With many data points the jets compress and the chart reads like a decorated line chart - the plume detail is lost. For dense time series (20+ periods), prefer the Fan chart which encodes uncertainty as smooth confidence bands. The Fountain shines at human scale: a handful of periods where each plume can breathe.
Field guide: ways to read a fountain
The fountain is new, so here is the full repertoire - each demo is a real, live chart with an honest caption. The first four are the flagship reads (the headline and what is quietly eroding or carrying it); the rest generalize the same grammar to certainty, stability, risk, AI and audiences.
Sales secured vs shrink
Three stores post near-identical sales, so a bar chart would call them equal. The fat, dense plume on Store C flags where theft and spoilage are eroding margin, and where to send loss-prevention first. The shrink percentage lives on the tooltip; over 2% of sales is the alert threshold.
Delivered vs never billed (trend)
Trend mode: volume delivered keeps climbing, but the widening plume warns that a growing share never gets billed - leaks and unmetered use outpacing growth. Under 10% non-revenue water is good; over 20% means act.
A forecast that is high but shaky
The trend rises, but the forecast jets fray into dashed froth: growth is projected, and the confidence behind it is thinning fast. For precise bands, the Fan chart is the right tool; this is the memorable version of the same warning.
The stars you see, the maintainers you don't
The hook the chart was built for: the spike is what everyone sees and stars; the spray is the invisible contributors the project actually rests on. Similar fame, very different foundations. Storytelling, not measurement.
Same number, three certainties
Three teams estimate the same 72 days to launch. Identical apexes; only the plume separates the team that measured from the team that guessed. The flag says the 72 is soft - the exact range belongs on the tooltip, and real confidence intervals belong to the Fan chart.
Stable or shaky
Two services average 120 ms and two average 60 ms - a bar chart shows two pairs of twins. The plume splits each pair: the tight crown is the one you can put an SLO on. Lower apex is better here; say so in the caption when you use it. The same read works for profit vs volatility.
Expected loss vs the worst case
The apex is the expected loss; the crown reaches toward the stress-case (value + spread, the upperBound in getContext()). Two positions expect the same loss; one hides a far heavier tail. Read the worst-case number off the tooltip, never off the width.
AI answers: confident or guessing
The apex is the answer score; the plume is the model's own uncertainty, normalised into score units so both share the y-axis. Tight crown: safe to automate. Fraying crown: hand it to a human. The insights layer reads the same spreadRatio out of getContext() to narrate which answers to trust.
Same average, divided audience
Two articles average the same 5.5 minutes of engagement. One holds everyone for about that long; the other splits its readers between skimmers and devourers. The average hides the division; the plume flags it, and the flag is your cue to segment before concluding anything.
Which side does the risk hang on
Symmetry as a signal: three routes share the same median and the same spread, but one crown leans - its surprises are one-sided, a late tail (lean: 0.8). Upright (lean: 0) means balanced; leaning means the hidden mass hangs on that side. Read only the direction, never the angle.
Typhoons over the Philippines
Sometimes the lean is literal. Each jet is a typhoon: the apex its peak sustained winds, the spray reaching toward the gusts (same km/h), the froth thickness its wind-field size, and the crown leaning the way the storm travelled - Pacific typhoons cross the Philippines east to west, so the whole line leans left, and the one that recurved toward Japan leans the other way. One glyph, four honest channels, zero new chart types.
Two silhouettes
Set style to pick the shape; both encode the same data (apex = value, spread channel = spread).
style: "jet"(default) - the faithful Jet d'Eau: a tall, narrow column, dense and opaque at the base, fraying into a soft, translucent crown at the top (built from graduated-opacity layers; the crown width grows withspread, the layer count with the optionaldensity).lean(in [-1, 1]) makes the crown drift downwind. Iconic; best as a headline/KPI or a comparison.style: "plume"- a symmetric column blooming into a feathery crown:frothLayersgraduated-opacity slices at the apex, a softshowMistskirt, andshowDropletsballistic arcs.stemFractionandbloomExponenttune the column-to-crown profile. Cleaner for a single KPI where the spread reads as a confidence halo.
const props = { style: "plume", dataSet: [{ label: "Q4", value: 78, spread: 20 }] };Rule of thumb: plume for few jets, jet for the hero and for heavy data. At human scale (1 to 12 jets) the plume's symmetric layered crown is the easiest shape to read the symptom from - tight halo vs wide froth. At hundreds of jets the plume's bloom gets clamped to a sliver of the slot and it degrades into a plain bar, while the stem-dominant jet degrades gracefully into a tall strip (see the heavy-data demo above); it also carries the brand. For genuinely dense series, stop decorating and reach for the Fan chart.
Symmetry carries meaning. An upright crown (the plume style, or a jet with lean: 0) says the spread is balanced. A leaning crown (lean in [-1, 1], sign only) says the spread hangs on one side. A jet with no lean keeps a gentle decorative drift - the Geneva wind - and reports lean: null in the context, so consumers can tell flag from flourish.
Both styles share stemFraction (column base half-width as a fraction of the slot), the density field, and lean. Colours follow your data/colorsMapping; the froth/spray only modulate opacity of your hue, so the chart adapts to light and dark themes.
When the Fountain earns its place
The literature was checked before this chart shipped. The Jet d'Eau metaphor is novel in dataviz (no prior fountain/jet chart exists), and the underlying idea is a sound re-orientation of the raincloud / violin / density-strip family. But its honest job is communication, not measurement - so use it where a memorable headline-plus-its-hidden-half matters, and reach for a precision chart when you need to compare the second number exactly.
Strong fits
- Headline vs hidden erosion. Revenue booked vs leaking (the gross-to-net retention gap), sales secured vs shrink, capacity vs losses. One mark says "this is the number, and this is what's bleeding out from under it." This is its flagship use.
- High-but-shaky / pushed-high. A bar shows the level; the spray adds "and here's how fragile it is."
- "What you see vs what it took" storytelling - the visible win and the invisible work behind it. It wins on recognition and recall (the one thing the embellishment research backs).
Use it honestly
- The apex is the only thing readers measure. Put the headline number there, on a real labelled y-axis. Width and area are low-accuracy channels (people underestimate them), so never ask anyone to compare spray widths.
- The spray is a flag; the figure is text. Surface the exact second number on the tooltip / legend /
getContext().jets[].spreadRatio, and ground it in a stated threshold (shrink > 2%, NRR < 100%, non-revenue water > 20%, P10-P90). - Lead with snapshot mode; cap trend mode at a handful of periods. For dense or precise uncertainty work, prefer Fan (bands), Vertical Stack Bar (sortable secured + at-risk), or a waterfall.
- Keep it to 5-12 glyphs and sort snapshots by
spreadRatioso the frothiest item is easy to find.
API
Props are typed as FountainChartProps 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. Full reference: Fountain API.
