Five chart types on ECharts (modular imports, not the full bundle):
TimeSeriesChart (multi-series, legend toggle), BarChart (incl.
stacked), SingleStat (big number + sparkline + trend), Heatmap, TopN.
Shared interactions: tooltips, dataZoom feeding the global time-range
picker, click-to-drill-into-query (drilldown.ts strips a panel's query
to its pre-stats filter and appends the clicked series/x-value as a
new filter term -- no backend change needed).
pivot.ts reshapes the query language's existing {columns, rows} tabular
output into per-series chart data client-side -- `stats count by
service, timestamp` already returns "long" rows, so multi-series
support needed zero query-language changes. theme.ts reads real
computed CSS custom properties so charts render in the active theme's
actual colors, with an SSR_FALLBACK for adapter-static's prerender pass
where `document` doesn't exist.
heatmap is the one narrow, justified backend change: a new VizType
needed to feed a new visualization, not a new query capability. Three
places had to change together, not two -- api/dashboards/types.go's
validator, web/src/lib/api.ts's union (previous commit), and the
dashboard_panels table's viz_type CHECK constraint
(migrations/0035_add_heatmap_viz_type.sql), which mirrors the Go
validator and doesn't update itself.
/dev/charts (unlisted, dev-only) is a synthetic fixture/perf-test route:
confirmed 50ms first-two-frames render time on a production build
against a 30,006-row/6-series stress case, and a 211,975-byte gzipped
chart chunk -- both real measurements behind the ECharts-over-
Observable-Plot-or-D3 choice, not estimates.
122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
// ECharts renders to canvas, not the DOM -- it needs concrete color
|
|
// values, not CSS var() references. This reads the real resolved value
|
|
// of each token this module cares about directly off <html> so charts
|
|
// always match whatever theme/density is currently active instead of
|
|
// carrying a second, hand-maintained copy of the palette that can drift
|
|
// from tokens.css.
|
|
|
|
export type ChartTokens = {
|
|
text: string;
|
|
textMuted: string;
|
|
border: string;
|
|
surface: string;
|
|
surfaceRaised: string;
|
|
accent: string;
|
|
sevQuiet: string;
|
|
sevInfo: string;
|
|
sevWarn: string;
|
|
sevError: string;
|
|
sevCritical: string;
|
|
fontUI: string;
|
|
fontMono: string;
|
|
};
|
|
|
|
function cssVar(name: string): string {
|
|
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
}
|
|
|
|
// Dark-mode literal fallback for prerendering/SSR, where `document`
|
|
// doesn't exist -- adapter-static prerenders every route (including
|
|
// dev/charts) at build time, and each chart's `option` is a $derived
|
|
// that runs during that pass same as it does in the browser. The real
|
|
// values always take over immediately once the client mounts; this
|
|
// only has to look reasonable for the static HTML shell, not be
|
|
// theme-accurate (prerendering can't know the visitor's theme choice
|
|
// anyway).
|
|
const SSR_FALLBACK: ChartTokens = {
|
|
text: '#f0f0f1',
|
|
textMuted: '#85888d',
|
|
border: '#2a2c2f',
|
|
surface: '#17181a',
|
|
surfaceRaised: '#1e2023',
|
|
accent: '#3fb6ff',
|
|
sevQuiet: '#85888d',
|
|
sevInfo: '#4c8dff',
|
|
sevWarn: '#f5c242',
|
|
sevError: '#ff6a39',
|
|
sevCritical: '#ff2d78',
|
|
fontUI: 'Overpass, sans-serif',
|
|
fontMono: 'Overpass Mono, monospace'
|
|
};
|
|
|
|
export function readChartTokens(): ChartTokens {
|
|
if (typeof document === 'undefined') return SSR_FALLBACK;
|
|
return {
|
|
text: cssVar('--color-text'),
|
|
textMuted: cssVar('--color-text-muted'),
|
|
border: cssVar('--color-border'),
|
|
surface: cssVar('--color-surface'),
|
|
surfaceRaised: cssVar('--color-surface-raised'),
|
|
accent: cssVar('--color-accent'),
|
|
sevQuiet: cssVar('--color-sev-quiet'),
|
|
sevInfo: cssVar('--color-sev-info'),
|
|
sevWarn: cssVar('--color-sev-warn'),
|
|
sevError: cssVar('--color-sev-error'),
|
|
sevCritical: cssVar('--color-sev-critical'),
|
|
fontUI: cssVar('--font-ui'),
|
|
fontMono: cssVar('--font-mono')
|
|
};
|
|
}
|
|
|
|
// A fixed categorical palette for multi-series charts (hosts, services,
|
|
// etc. -- data that isn't severity-shaped, so severity's blue/amber/
|
|
// orange/magenta ramp doesn't apply). Chosen to stay distinguishable
|
|
// from the four severity colors above (no orange/magenta/amber-gold
|
|
// here) so a legend never makes a non-severity series look like it's
|
|
// signaling a severity. Colorblind-conscious: alternates hue and
|
|
// lightness, not just hue.
|
|
export const SERIES_PALETTE = [
|
|
'#3fb6ff', // accent blue
|
|
'#6fd6b0', // teal-green
|
|
'#b48cff', // violet
|
|
'#5c8dff', // periwinkle
|
|
'#4dd0e1', // cyan
|
|
'#8bc34a', // olive-green
|
|
'#7986cb', // indigo
|
|
'#4db6ac' // seafoam
|
|
];
|
|
|
|
// Shared base option every chart type extends -- background transparent
|
|
// (the card behind it supplies --color-surface), grid inset for axis
|
|
// labels, tooltip/legend/axis text all pulled from tokens so nothing is
|
|
// hardcoded per chart type.
|
|
export function baseOption(t: ChartTokens) {
|
|
return {
|
|
backgroundColor: 'transparent',
|
|
textStyle: { fontFamily: t.fontUI, color: t.text },
|
|
grid: { left: 48, right: 16, top: 28, bottom: 28, containLabel: true },
|
|
tooltip: {
|
|
backgroundColor: t.surfaceRaised,
|
|
borderColor: t.border,
|
|
borderWidth: 1,
|
|
textStyle: { color: t.text, fontFamily: t.fontMono, fontSize: 12 },
|
|
extraCssText: 'box-shadow: var(--shadow-md); border-radius: 6px;'
|
|
},
|
|
legend: {
|
|
textStyle: { color: t.textMuted, fontFamily: t.fontUI, fontSize: 12 },
|
|
inactiveColor: t.border,
|
|
top: 0
|
|
},
|
|
xAxis: {
|
|
axisLine: { lineStyle: { color: t.border } },
|
|
axisLabel: { color: t.textMuted, fontFamily: t.fontMono, fontSize: 11 },
|
|
splitLine: { show: false }
|
|
},
|
|
yAxis: {
|
|
axisLine: { show: false },
|
|
axisLabel: { color: t.textMuted, fontFamily: t.fontMono, fontSize: 11 },
|
|
splitLine: { lineStyle: { color: t.border, type: 'dashed' as const } }
|
|
}
|
|
};
|
|
}
|