Skip to main content

Site traffic dashboard

Every other page in this Gallery shows a chart. This one shows a product — a working replica of the operations dashboard the CERN border-router capture actually comes out of, rebuilt on pond: the header, the control strip, the mirrored total-traffic chart, and the interface table that drives it.

It is here to answer the question a chart library gets asked last and judged on first: can it disappear into somebody else's design?

The dashboard

Click an interface. Click it again — or ⊗ Clear Selection — to go back. Drag the plot to pan, wheel over it to zoom, and run the pointer across it: every number in the table is the value at the instant you are pointing at.

The interaction: a share, not a swap

This is the part worth copying, and it is one design decision.

The obvious way to link a table to a chart is to swap the series: no selection ⇒ draw the total, a row picked ⇒ draw that row. That's what the network traffic page's linked table does, and for "show me this interface" it is exactly right.

It is the wrong answer to the question this dashboard asks. An operator picking 111-lag-3-522 doesn't want to know what that interface did in isolation; they want to know how much of the site's traffic it was. Swap the series and the y axis rescales under them, the shape they were just looking at vanishes, and the comparison they came for is gone.

So nothing swaps. The site total is always the drawn shape. What the selection moves is which part of it is saturated, under one rule:

The saturated colour marks what you are looking at.

Nothing picked, and the total is what you are looking at — so it takes the strong blue and orange. Pick a row and the total steps back to a pale wash while that interface's contribution is drawn over it, from the same zero line, in the saturated shade — two more <AreaChart>s appended to the same <Layers>, and one swapped as on the pair already there:

<Layers>
{/* always the whole site — subject when nothing is picked, wash when
something is */}
<AreaChart
series={total}
column="in"
as={picked ? 'washIn' : 'strongIn'}
baseline={0}
/>
<AreaChart
series={totalDown}
column="out"
as={picked ? 'washOut' : 'strongOut'}
baseline={0}
/>

{/* …and, when a row is picked, its contribution within it */}
{picked && (
<>
<AreaChart series={picked} column="in" as="strongIn" baseline={0} />
<AreaChart series={pickedDown} column="out" as="strongOut" baseline={0} />
</>
)}
</Layers>

Note what the roles are named for. strongIn is not "the picked interface" and washIn is not "the total" — they are named for the emphasis, because that is the thing the selection actually assigns. The same layer takes either one.

Three things make the picked state read as contribution rather than as two charts on top of each other:

  • The same baseline. Both pairs fill from baseline={0}, so the dark region starts where the wash starts and its height is directly comparable.
  • The same axis. The domain is pinned symmetric around zero and does not move when the selection changes, so the wash is a fixed reference.
  • Flat fills. Every role sets flatFill, so neither area grades to transparent at the baseline. A graded wash would fade out exactly where the overlay needs to sit inside it.

The selection itself is one useState and a click handler. Nothing in @pond-ts/charts mediates it — a chart is a view of whatever series you hand it, so "the table drives the chart" is React state, as it should be.

The palette is the product's, not pond's

The colours here are not the pond theme, deliberately. A replica that renders in someone else's brand is the honest test of the theming layer.

They arrive the way every colour in this library is supposed to: as a ChartTheme. Not one hex literal reaches the JSX.

const replicaTheme: ChartTheme = {
...defaultTheme,
background: '#ffffff',
area: {
...defaultTheme.area,
washIn: flat('#c3dbf0'), // context
washOut: flat('#fbdcbe'),
strongIn: flat('#4a8fcf'), // the subject
strongOut: flat('#e8883c'),
},
box: {
...defaultTheme.box,
toSite: bullet('#bfd7f2', '#c9ddf2', '#5b9bd5'), // band, body, tick
fromSite: bullet('#fadbbc', '#f9d8b0', '#ed8c2b'),
},
axis: {
...defaultTheme.axis,
label: '#8a99a8',
grid: '#e8ecf0',
gridDash: [],
},
cursor: '#98a6b4',
chip: { background: '#ffffff' },
annotation: { color: '#2fa8a0', fillOpacity: 0.1, depth: [1, 0.7, 0.4] },
};

as="washIn" and as="strongIn" are just role names — the theme is free to invent them, and the layer resolves area[as] ?? area.default. Swapping theme={replicaTheme} for the site's useSiteChartTheme() turns this back into a pond chart with no other edit, which is the property that makes the one-channel rule worth keeping.

The table under the chart takes the same theme object, one register down: its rows resolve box[as], and the teal edge on the picked row is annotation.color — the register user-authored marks draw in, which a selection is. Two registers, one prop.

Two consequences worth naming. The panel around the chart is styled with the same fixed palette rather than the site's --pond-* tokens, so it stays light when the docs site goes dark — a replica of a light product is a light surface. And this is the only page in the Gallery that does any of this: everything else uses the pond theme, which is what you want unless you are reproducing a specific product.

What's live and what's chrome

The layout is reproduced completely; the behaviour is not, and the page would rather say so than let you find out by clicking.

ControlState
Interface rows, ⊗ Clear SelectionLive — they drive the overlay
SHOW GRIDLINES Off / OnLive<ChartContainer grid>
TIME 1h / 6hLive — re-windows the chart and the table's numbers
Drag / wheel on the plotLivepanZoom="panZoom", clamped to the capture

The product's own strip carries day, week, month and custom next to 1h and 6h, a TRAFFIC [All ▾] filter, and an Interfaces / Flow tab pair above them. A six-hour fixture of seven interfaces can honour none of it, and a control that silently clamps to the data it has is a control that lies — so rather than render dead buttons, a dead dropdown and a dead tab, this replica doesn't render them at all. Dead chrome in a docs example is noise; the reader can't tell "not wired up here" from "doesn't work".

1h is not a crop of 6h, either. It lands in the busy afternoon, and everything derived re-derives against that window: the axis bound and its gridline step come from the window's own peak (which happens to round to the same 300G here — the afternoon is where the peaks are), and every number in the table — each interface's min, quartiles and max, its tracked value, and the list's shared scale — is recomputed over the visible samples.

The presets and the gesture write the same piece of state: range is a useState here, the buttons set it outright, and onTimeRangeChange routes each pan and zoom back into it. That is also what keeps the buttons honest — aria-pressed compares the live window against each preset instead of remembering which was clicked, so panning away from 6h lights neither. bounds={TRAFFIC_RANGE} is the fixture's full extent, so the window stops at the first and last sample rather than drifting into empty time.

The table is the library too

The interface list is not hand-rolled markup next to a chart. It is <BoxList> — and this is the row cell that component was built for, because the product being replicated here is the one it was drawn from.

Look at what the product's bullet actually encodes and the mapping is one-to-one:

The product draws<BoxList>Fed here with
the pale extent tracklower / upperthe interface's min→max over the window
the filled inner barq1 / q3the middle half of the same window
the dark tickvaluethe value at the instant under the pointer
the number beside itformatthat same value, printed

Two of those lines per row — one columns entry for each direction — and the rest of the cell is props:

<BoxList
rows={listRows}
columns={[
{
lower: 'in_min',
q1: 'in_q1',
q3: 'in_q3',
upper: 'in_max',
value: 'in_now',
format: rate,
as: 'toSite',
},
{
lower: 'out_min',
q1: 'out_q1',
q3: 'out_q3',
upper: 'out_max',
value: 'out_now',
format: rate,
as: 'fromSite',
},
]}
theme={replicaTheme}
before={[{ key: 'category', render: (r) => <Tag row={r} /> }]}
selected={selected}
onRowClick={(r) => setSelected((s) => (r.key === s ? null : r.key))}
/>

selected and onRowClick are the same useState the overlay reads, so "clicking a row repaints the chart" is one piece of state with two subscribers, not a wire between two components.

Three things are worth pulling out.

It is a table, not a plot. <BoxList> renders a real <table> in DOM — no <ChartContainer>, no canvas, no time axis. That is the point: what this cell needs around its glyph is table semantics (a link label, a category cell, a clickable row, columns that align down the list), and one row per interface is nowhere near enough data to earn an axis. Its sister <BarList> is the same table with a plain value bar where the box goes; that's the one to reach for when a row has one number rather than a distribution.

It computes no statistics. lower/q1/q3/upper are just names of entries in each row's values, and the page fills them by sorting the visible samples itself. The list draws quantiles; deciding which quantiles — min/max here, a p5/p95 somewhere else — stays with the caller, the same contract the canvas <BoxPlot> keeps.

All rows share one scale. Every box of every row maps through one domain, auto-fitted across the list to the busiest interface's peak in the visible window, so a band is readable against the others — the only reason to put them in a column. Pass domain to pin it instead.

A readout of the hovered moment

The band is the window; the tick is now. <ChartContainer onTrackerChanged> fires with the pointer's time (and null on leave); the dashboard keeps that in state, rounds it to a sample index, and rebuilds each row's two *_now entries from it. Off-chart it falls back to the window's last sample.

The tick and the printed number are the same entry by construction — value positions the mark and format prints it, from one name — because a marker and a label that can disagree about what "current" means is the failure mode this readout has. The band either side of it does not move as you sweep: what changes is where now sits inside where the interface runs, which is the question an operator is actually asking.

Note what the tracker is used for and what it isn't. onTrackerChanged also hands you every drawn series' value at that time, but only two series are drawn here — the totals. The per-interface numbers come from indexing the fixture at the tracked time, which is the general shape: the tracker supplies the instant; your own data answers for it.

Where the component stops

Two things this replica reaches past the component for, both in the example's stylesheet and both worth knowing before you copy it:

  • The value label lives inside the glyph area, 8px right of its tick. Park the pointer on the busiest interface's own peak and the tick reaches the top of the scale, so the number would run off the panel — there is no gutter prop, so the page pads the glyph cell instead.
  • Bar ends are rounded to a pill; the product's are square, and there is no radius knob.

And one the component simply doesn't have: no header row. The product labels its columns (INTERFACE, CATEGORY, IN / OUT); <BoxList> renders rows only, so those labels are gone rather than faked. Every other piece of the cell — the palette, the selection edge, the hover tint, the row dividers — comes from the ChartTheme.

Note what the callback is used for and what it isn't. onTrackerChanged also hands you every drawn series' value at that time, but only two series are drawn here — the totals. The per-interface numbers come from indexing the fixture at the tracked time, which is the general shape: the tracker supplies the instant; your own data answers for it.

The data

The cern773-cr6 border-router capture described in full on the network traffic page: real measured telemetry, six hours, both directions per SAP (service access point).

One reduction matters for reading this page. The published fixture keeps the top 7 SAPs of 24 — 99.87% of all bytes; the other 17 are near-idle and would be invisible rows. The sample rate is the capture's own 30 seconds, not downsampled, so no peak is clipped and the chart carries the same bursts the product does. The real dashboard shows all 24 interfaces. Values are Gbps; the axis labels them the way the product does, as magnitudes with a G suffix and a bare 0.0 on the shared zero line.

See also

  • Network traffic — the same mirrored chart on the pond theme, with the swap-on-select table
  • Traffic by interface — the composition question, answered by stacking instead of overlaying
  • <AreaChart>baseline, flatFill, and the as role
  • Lists<BoxList> and <BarList>, the table-shaped half of the library
  • Styling and theming — the one-channel rule this page bends without breaking
  • Storybook — the systematic knob walk