Skip to main content

Shaping data to chart

You will learn
  • Turning raw point readings into time buckets a BarChart can draw
  • Smoothing a noisy line into a centreline with a variance envelope
  • Splitting one series into one line per host
  • Bucketing by value instead of time, for a distribution

Raw data is rarely chart-shaped. This chapter is the "make the most of pond" chapter — four transforms, four charts, all starting from the same raw cpu/latency readings chapter 3 introduced.

aggregate → bars

A BarChart needs a span to fill, not a point (chapter 3's temporal-keys note). .aggregate buckets point-keyed readings into interval-keyed time buckets — exactly the shape a bar needs:

src/examples/learn-04-aggregate-bars.tsx
import {
BarChart,
ChartContainer,
ChartRow,
Layers,
YAxis,
} from '@pond-ts/charts';
import { Sequence } from 'pond-ts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { singleHostSeries } from './lib/server-metrics';

export default function AggregateBars() {
const theme = useSiteChartTheme();
const buckets = singleHostSeries().aggregate(Sequence.every('10m'), {
cpu: 'avg',
});

return (
<ChartContainer range={buckets.timeRange()} width={560} theme={theme}>
<ChartRow height={200}>
<YAxis id="pct" side="right" format=".0%" min={0} />
<Layers>
<BarChart series={buckets} column="cpu" axis="pct" gap={3} />
</Layers>
</ChartRow>
</ChartContainer>
);
}
import { Sequence } from 'pond-ts';

const buckets = series.aggregate(Sequence.every('10m'), { cpu: 'avg' });
// buckets.schema[0].kind === 'interval'
<BarChart series={buckets} column="cpu" axis="pct" gap={3} />

{ cpu: 'avg' } is shorthand for { cpu: { from: 'cpu', using: 'avg' } }.aggregate takes a wide vocabulary of reducers (sum, min, max, count, median, p95, and more). Full list: Aggregation.

rolling + baseline → a smoothed line and its envelope

A noisy raw signal is rarely what you want to show as-is. .baseline computes a rolling mean and standard deviation in one pass, appending avg/sd/upper/lower columns — feed avg to a LineChart and lower/upper to a BandChart and you get a centreline with its own variance band:

src/examples/learn-04-rolling-band.tsx
import {
BandChart,
ChartContainer,
ChartRow,
Layers,
LineChart,
YAxis,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { singleHostSeries } from './lib/server-metrics';

export default function RollingBand() {
const theme = useSiteChartTheme();
const banded = singleHostSeries().baseline('cpu', {
window: '10m',
sigma: 1.5,
});

return (
<ChartContainer range={banded.timeRange()} width={560} theme={theme}>
<ChartRow height={220}>
<YAxis id="pct" side="right" format=".0%" />
<Layers>
<BandChart
series={banded}
lower="lower"
upper="upper"
axis="pct"
as="outer"
/>
<LineChart series={banded} column="avg" axis="pct" as="primary" />
</Layers>
</ChartRow>
</ChartContainer>
);
}
const banded = series.baseline('cpu', { window: '10m', sigma: 1.5 });
// banded columns: time, cpu, latency, avg, sd, upper, lower
<BandChart series={banded} lower="lower" upper="upper" axis="pct" as="outer" />
<LineChart series={banded} column="avg" axis="pct" as="primary" />

.baseline is sugar over a .rolling call with avg/stdev reducers — reach for .rolling directly when you want the window without the band-shaped output. Both: Rolling windows; .baseline/.outliers: Anomaly detection.

partitionBy → one line per host

The running example has been one host so far. With all three:

src/examples/learn-04-partition-multi.tsx
import {
ChartContainer,
ChartRow,
Layers,
LineChart,
YAxis,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { allHostsSeries, HOSTS } from './lib/server-metrics';

const ROLES = ['primary', 'secondary', 'slow'] as const;

export default function PartitionMulti() {
const theme = useSiteChartTheme();
const series = allHostsSeries();
const byHost = series.partitionBy('host').toMap();

return (
<ChartContainer range={series.timeRange()} width={560} theme={theme}>
<ChartRow height={220}>
<YAxis id="pct" side="right" format=".0%" />
<Layers>
{HOSTS.map((host, i) => {
const hostSeries = byHost.get(host);
if (!hostSeries) return null;
return (
<LineChart
key={host}
series={hostSeries}
column="cpu"
axis="pct"
as={ROLES[i]}
/>
);
})}
</Layers>
</ChartRow>
</ChartContainer>
);
}
const byHost = series.partitionBy('host').toMap(); // Map<string, TimeSeries<S>>
<Layers>
{HOSTS.map((host) => (
<LineChart
key={host}
series={byHost.get(host)!}
column="cpu"
as={/* … */}
/>
))}
</Layers>

.partitionBy returns a view, not a Map directly — .toMap() is the terminal call that gives you one series per partition key, ready to map over into one draw layer each. (If you instead want to run a transform per partition and rejoin into one series — a per-host rolling average, say — .collect() is the terminal call for that shape instead.) Full model: Reshape.

byColumn → a distribution

Every transform above buckets by time. .byColumn buckets by value instead — the shape a distribution or histogram needs, with no time axis at all:

src/examples/learn-04-histogram.tsx
import {
BarChart,
ChartContainer,
ChartRow,
Layers,
YAxis,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { allHostsSeries } from './lib/server-metrics';

export default function Histogram() {
const theme = useSiteChartTheme();
const series = allHostsSeries();
const count = new Float64Array(series.length).fill(1);
const bins = series
.withColumn('count', count)
.byColumn(
'latency',
{ width: 5 },
{ count: { from: 'count', using: 'sum' } },
);

return (
<ChartContainer range={[20, 90]} width={560} theme={theme}>
<ChartRow height={200}>
<YAxis id="count" label="minutes" min={0} width={44} />
<Layers>
<BarChart bins={bins} column="count" gap={2} />
</Layers>
</ChartRow>
</ChartContainer>
);
}
const count = new Float64Array(series.length).fill(1);
const bins = series
.withColumn('count', count)
.byColumn(
'latency',
{ width: 5 },
{ count: { from: 'count', using: 'sum' } },
);
// bins: Array<{ start, end, count }> — a plain array, not a series
<BarChart bins={bins} column="count" gap={2} />

.byColumn returns a plain array of bin records, not a series — value bins aren't time-indexed, so there's nothing to key. <BarChart bins={…}> reads that shape directly.

Recap

Four transforms, four chart-ready shapes: aggregate for time buckets, baseline for a smoothed centreline plus its envelope, partitionBy for one series per group, byColumn for a value distribution. Every one of them takes the exact same raw readings chapter 3 built and reshapes them without touching how the chart itself is drawn — the shaping happens before the chart ever sees the data.

Next: Styling and theming — the last core-vocabulary chapter: how a series column becomes a colour on screen.