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.
| Control | State |
|---|---|
Interface rows, ⊗ Clear Selection | Live — they drive the overlay |
SHOW GRIDLINES Off / On | Live — <ChartContainer grid> |
TIME 1h / 6h | Live — re-windows the chart and the table's numbers |
| Drag / wheel on the plot | Live — panZoom="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 track | lower / upper | the interface's min→max over the window |
| the filled inner bar | q1 / q3 | the middle half of the same window |
| the dark tick | value | the value at the instant under the pointer |
| the number beside it | format | that 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 theasrole- 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