102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
import type { Bucket } from "@/lib/types";
|
|
|
|
export interface Rect {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface BarLayout {
|
|
bucket: Bucket;
|
|
/** queries - blocked - cached, clamped at 0. */
|
|
other: number;
|
|
slot: Rect;
|
|
segments: {
|
|
blocked: Rect;
|
|
cached: Rect;
|
|
other: Rect;
|
|
};
|
|
}
|
|
|
|
export interface ChartLayout {
|
|
width: number;
|
|
height: number;
|
|
plot: Rect;
|
|
scaleMax: number;
|
|
bars: BarLayout[];
|
|
yTicks: { value: number; y: number }[];
|
|
xTicks: { ts: number; x: number }[];
|
|
}
|
|
|
|
export const MARGIN = { top: 8, right: 8, bottom: 22, left: 44 } as const;
|
|
const BAR_GAP = 2;
|
|
const MIN_X_LABEL_PX = 90;
|
|
|
|
/** Tick values from 0 upward in a 1/2/5 step, extended until the last tick covers `max`. */
|
|
export function niceTicks(max: number, targetCount = 4): number[] {
|
|
if (max <= 0) return [0];
|
|
const rawStep = max / targetCount;
|
|
const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
|
|
const normalized = rawStep / magnitude;
|
|
const step = (normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10) * magnitude;
|
|
const ticks: number[] = [];
|
|
for (let value = 0; ; value += step) {
|
|
ticks.push(value);
|
|
if (value >= max) break;
|
|
}
|
|
return ticks;
|
|
}
|
|
|
|
export function isEmptyTimeseries(buckets: Bucket[]): boolean {
|
|
return buckets.every((bucket) => bucket.queries === 0);
|
|
}
|
|
|
|
export function layoutTimeseries(buckets: Bucket[], width: number, height: number): ChartLayout {
|
|
const plot: Rect = {
|
|
x: MARGIN.left,
|
|
y: MARGIN.top,
|
|
width: Math.max(0, width - MARGIN.left - MARGIN.right),
|
|
height: Math.max(0, height - MARGIN.top - MARGIN.bottom),
|
|
};
|
|
const maxQueries = buckets.reduce((max, bucket) => Math.max(max, bucket.queries), 0);
|
|
const tickValues = niceTicks(maxQueries);
|
|
const scaleMax = Math.max(tickValues[tickValues.length - 1], 1);
|
|
const baseline = plot.y + plot.height;
|
|
const toHeight = (value: number) => (value / scaleMax) * plot.height;
|
|
|
|
const slotWidth = buckets.length > 0 ? plot.width / buckets.length : 0;
|
|
const barWidth = Math.max(1, slotWidth - BAR_GAP);
|
|
|
|
const bars: BarLayout[] = buckets.map((bucket, i) => {
|
|
const slotX = plot.x + i * slotWidth;
|
|
const barX = slotX + (slotWidth - barWidth) / 2;
|
|
const other = Math.max(0, bucket.queries - bucket.blocked - bucket.cached);
|
|
const blockedH = toHeight(bucket.blocked);
|
|
const cachedH = toHeight(bucket.cached);
|
|
const otherH = toHeight(other);
|
|
return {
|
|
bucket,
|
|
other,
|
|
slot: { x: slotX, y: plot.y, width: slotWidth, height: plot.height },
|
|
segments: {
|
|
blocked: { x: barX, y: baseline - blockedH, width: barWidth, height: blockedH },
|
|
cached: { x: barX, y: baseline - blockedH - cachedH, width: barWidth, height: cachedH },
|
|
other: { x: barX, y: baseline - blockedH - cachedH - otherH, width: barWidth, height: otherH },
|
|
},
|
|
};
|
|
});
|
|
|
|
const yTicks = tickValues.map((value) => ({ value, y: baseline - toHeight(value) }));
|
|
|
|
const labelStep =
|
|
buckets.length > 0 && plot.width > 0
|
|
? Math.max(1, Math.ceil((buckets.length * MIN_X_LABEL_PX) / plot.width))
|
|
: 1;
|
|
const xTicks = bars
|
|
.filter((_, i) => i % labelStep === 0)
|
|
.map((bar) => ({ ts: bar.bucket.ts, x: bar.slot.x + bar.slot.width / 2 }));
|
|
|
|
return { width, height, plot, scaleMax, bars, yTicks, xTicks };
|
|
}
|