Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 17 additions & 35 deletions apps/cadecon/src/components/charts/AsymptoteTrends.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ import {
type KernelSnapshot,
} from '../../lib/iteration-store.ts';
import { viewedIteration } from '../../lib/viz-store.ts';
import { wheelZoomPlugin, AXIS_TEXT, AXIS_GRID, AXIS_TICK, METRIC_COLORS } from '@calab/ui/chart';
import {
wheelZoomPlugin,
chartAxis,
labeledAxis,
integerTickValues,
syncCursor,
safeRange,
METRIC_COLORS,
} from '@calab/ui/chart';
import { convergenceMarkerPlugin } from '../../lib/chart/convergence-marker-plugin.ts';
import { viewedIterationPlugin } from '../../lib/chart/viewed-iteration-plugin.ts';

Expand Down Expand Up @@ -73,23 +81,11 @@ const PANELS: PanelDef[] = [
},
];

/** Y range that never degenerates: uPlot throws in drawAxesGrid on a zero span. */
function yRange(_u: uPlot, dataMin: number, dataMax: number): [number, number] {
if (dataMin == null || dataMax == null || !isFinite(dataMin) || !isFinite(dataMax)) return [0, 1];
if (dataMin === dataMax) {
const pad = Math.abs(dataMin) * 0.05 || 0.5;
return [dataMin - pad, dataMax + pad];
}
const pad = (dataMax - dataMin) * 0.1;
return [dataMin - pad, dataMax + pad];
}

/** X range = exact iteration extent; pad only the single-point case (also avoids a zero span). */
function xRange(_u: uPlot, dataMin: number, dataMax: number): [number, number] {
if (dataMin == null || dataMax == null || !isFinite(dataMin) || !isFinite(dataMax)) return [0, 1];
if (dataMin === dataMax) return [dataMin - 0.5, dataMax + 0.5];
return [dataMin, dataMax];
}
// Degenerate-span guards (uPlot throws in drawAxesGrid on a zero span). Y pads
// 10%; X uses the exact iteration extent (the createEffect below drives the
// real x-scale — these are the fallbacks).
const yRange = safeRange(0.1);
const xRange = safeRange(0);

/** One compact trend chart; reads convergence history reactively. */
function MiniTrend(props: { panel: PanelDef }): JSX.Element {
Expand Down Expand Up @@ -158,22 +154,8 @@ function MiniTrend(props: { panel: PanelDef }): JSX.Element {
];

const axes: uPlot.Axis[] = [
{
stroke: AXIS_TEXT,
grid: { stroke: AXIS_GRID },
ticks: { stroke: AXIS_TICK },
size: 24,
values: (_u, splits) => (splits ?? []).map((v) => (Number.isInteger(v) ? String(v) : '')),
},
{
stroke: AXIS_TEXT,
grid: { stroke: AXIS_GRID },
ticks: { stroke: AXIS_TICK },
label: props.panel.unit,
labelSize: 10,
labelFont: '10px sans-serif',
size: 38,
},
chartAxis({ size: 24, values: integerTickValues }),
labeledAxis(props.panel.unit, { size: 38 }),
];

const plugins = [
Expand All @@ -182,7 +164,7 @@ function MiniTrend(props: { panel: PanelDef }): JSX.Element {
wheelZoomPlugin(),
];

const cursor: uPlot.Cursor = { sync: { key: 'cadecon-asymptote', setSeries: true } };
const cursor = syncCursor('cadecon-asymptote');

return (
<div class="asymptote-cell">
Expand Down
30 changes: 6 additions & 24 deletions apps/cadecon/src/components/charts/KernelConvergence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import {
import { tauToShape } from '@calab/compute';
import {
wheelZoomPlugin,
AXIS_TEXT,
AXIS_GRID,
AXIS_TICK,
labeledAxis,
integerTickValues,
syncCursor,
METRIC_COLORS,
withOpacity,
} from '@calab/ui/chart';
Expand Down Expand Up @@ -287,24 +287,8 @@ export function KernelConvergence(): JSX.Element {
};

const axes: uPlot.Axis[] = [
{
stroke: AXIS_TEXT,
grid: { stroke: AXIS_GRID },
ticks: { stroke: AXIS_TICK },
label: 'Iteration',
labelSize: 10,
labelFont: '10px sans-serif',
values: (_u: uPlot, splits: number[]) =>
splits.map((v) => (Number.isInteger(v) ? String(v) : '')),
},
{
stroke: AXIS_TEXT,
grid: { stroke: AXIS_GRID },
ticks: { stroke: AXIS_TICK },
label: 'ms',
labelSize: 10,
labelFont: '10px sans-serif',
},
labeledAxis('Iteration', { values: integerTickValues }),
labeledAxis('ms'),
];

const plugins = [
Expand All @@ -315,9 +299,7 @@ export function KernelConvergence(): JSX.Element {
wheelZoomPlugin(),
];

const cursor: uPlot.Cursor = {
sync: { key: 'cadecon-convergence', setSeries: true },
};
const cursor = syncCursor('cadecon-convergence');

return (
<Show
Expand Down
18 changes: 4 additions & 14 deletions apps/cadecon/src/components/distributions/HistogramCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SolidUplot } from '@dschz/solid-uplot';
import type uPlot from 'uplot';
import 'uplot/dist/uPlot.min.css';
import '@calab/ui/chart/chart-theme.css';
import { withOpacity, AXIS_TEXT, AXIS_GRID, AXIS_TICK, OKABE_ITO } from '@calab/ui/chart';
import { withOpacity, chartAxis, staticCursor, OKABE_ITO } from '@calab/ui/chart';
import { median, iqr } from '../../lib/math-utils.ts';

export interface HistogramCardProps {
Expand Down Expand Up @@ -96,22 +96,12 @@ export function HistogramCard(props: HistogramCardProps): JSX.Element {
});

const axes: uPlot.Axis[] = [
{
stroke: AXIS_TEXT,
grid: { show: false },
ticks: { stroke: AXIS_TICK },
size: 24,
},
{
stroke: AXIS_TEXT,
grid: { stroke: AXIS_GRID },
ticks: { stroke: AXIS_TICK },
size: 30,
},
chartAxis({ grid: { show: false }, size: 24 }),
chartAxis({ size: 30 }),
];

const scales: uPlot.Scales = { x: { time: false } };
const cursor: uPlot.Cursor = { drag: { x: false, y: false } };
const cursor = staticCursor;

const medVal = createMemo(() => median(props.values()));
const iqrVal = createMemo(() => iqr(props.values()));
Expand Down
24 changes: 5 additions & 19 deletions apps/cadecon/src/components/kernel/KernelDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ import {
GROUND_TRUTH_COLORS,
withOpacity,
wheelZoomPlugin,
AXIS_TEXT,
AXIS_GRID,
AXIS_TICK,
chartAxis,
labeledAxis,
syncCursor,
kernelAnnotationsPlugin,
type KernelAnnotations,
} from '@calab/ui/chart';
Expand Down Expand Up @@ -230,28 +230,14 @@ export function KernelDisplay(): JSX.Element {
return s;
});

const axes: uPlot.Axis[] = [
{
stroke: AXIS_TEXT,
grid: { stroke: AXIS_GRID },
ticks: { stroke: AXIS_TICK },
label: 'Time (ms)',
labelSize: 10,
labelFont: '10px sans-serif',
},
{
stroke: AXIS_TEXT,
grid: { stroke: AXIS_GRID },
ticks: { stroke: AXIS_TICK },
},
];
const axes: uPlot.Axis[] = [labeledAxis('Time (ms)'), chartAxis()];

const scales: uPlot.Scales = { x: { time: false } };
const plugins = createMemo(() => [
wheelZoomPlugin(),
kernelAnnotationsPlugin(() => annotations()),
]);
const cursor: uPlot.Cursor = { sync: { key: 'cadecon-kernel', setSeries: true } };
const cursor = syncCursor('cadecon-kernel');

const tauRMs = () => formatTauMs(currentTauRise());
const tauDMs = () => formatTauMs(currentTauDecay());
Expand Down
22 changes: 6 additions & 16 deletions packages/ui/src/chart/TracePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { SolidUplot } from '@dschz/solid-uplot';
import type uPlot from 'uplot';
import 'uplot/dist/uPlot.min.css';
import './chart-theme.css';
import { wheelZoomPlugin, AXIS_TEXT, AXIS_GRID, AXIS_TICK } from './index.ts';
import { wheelZoomPlugin } from './index.ts';
import { chartAxis, hiddenTickValues } from './axis-helpers.ts';

export interface TracePanelProps {
/** uPlot AlignedData format: [x, y1, y2, ...] -- signal accessor for reactivity */
Expand Down Expand Up @@ -64,28 +65,17 @@ export function TracePanel(props: TracePanelProps) {
// recreating the chart on every data update. `props.xLabel` is read at
// mount; callers never swap it mid-chart-life.
/* eslint-disable solid/reactivity */
const xAxis: uPlot.Axis = {
stroke: AXIS_TEXT,
grid: { stroke: AXIS_GRID },
ticks: { stroke: AXIS_TICK },
const xAxis: uPlot.Axis = chartAxis({
values: formatTimeValues,
...(props.xLabel
? { label: props.xLabel, labelSize: 10, labelGap: 0, labelFont: '10px sans-serif', size: 30 }
: {}),
};
});
/* eslint-enable solid/reactivity */

const yAxisBase: uPlot.Axis = {
stroke: AXIS_TEXT,
grid: { stroke: AXIS_GRID },
ticks: { stroke: AXIS_TICK },
};
const yAxisBase: uPlot.Axis = chartAxis();

const yAxisHidden: uPlot.Axis = {
...yAxisBase,
values: (_u: uPlot, vals: number[]) => vals.map(() => ''),
size: 20,
};
const yAxisHidden: uPlot.Axis = chartAxis({ values: hiddenTickValues, size: 20 });

const yAxis = () => (props.hideYValues ? yAxisHidden : yAxisBase);

Expand Down
66 changes: 66 additions & 0 deletions packages/ui/src/chart/axis-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Shared uPlot axis / cursor / scale-range helpers. The theme axis chrome
// (stroke + grid + ticks) and the degenerate-span guard were previously
// copy-pasted into every chart component across the apps; these collapse that
// duplication into one place.

import type uPlot from 'uplot';
import { AXIS_TEXT, AXIS_GRID, AXIS_TICK } from './theme-colors.ts';

const LABEL_FONT = '10px sans-serif';

/** Base axis chrome (theme stroke / grid / ticks), merged with `overrides`. */
export function chartAxis(overrides: uPlot.Axis = {}): uPlot.Axis {
return {
stroke: AXIS_TEXT,
grid: { stroke: AXIS_GRID },
ticks: { stroke: AXIS_TICK },
...overrides,
};
}

/** Axis chrome plus a consistently-styled axis label. */
export function labeledAxis(label: string, overrides: uPlot.Axis = {}): uPlot.Axis {
return chartAxis({ label, labelSize: 10, labelFont: LABEL_FONT, ...overrides });
}

/** Axis `values` formatter that shows only integer splits (e.g. an iteration axis). */
export function integerTickValues(_u: uPlot, splits: number[]): string[] {
return (splits ?? []).map((v) => (Number.isInteger(v) ? String(v) : ''));
}

/** Axis `values` formatter that hides all tick labels (keeps gridlines). */
export function hiddenTickValues(_u: uPlot, splits: number[]): string[] {
return (splits ?? []).map(() => '');
}

/** Cursor that syncs across charts sharing `key`; pass `drag: false` for static charts. */
export function syncCursor(key: string, opts: { drag?: boolean } = {}): uPlot.Cursor {
const cursor: uPlot.Cursor = { sync: { key, setSeries: true } };
if (opts.drag === false) cursor.drag = { x: false, y: false };
return cursor;
}

/** Cursor for a static (non-synced) chart with drag-zoom disabled. */
export const staticCursor: uPlot.Cursor = { drag: { x: false, y: false } };

/**
* uPlot scale-range fn that never returns a zero span — a degenerate [v, v]
* range crashes uPlot's drawAxesGrid. Non-finite/absent bounds fall back to
* [0, 1]; an equal min/max is padded; otherwise the span is padded by
* `padFrac` (use 0 for an exact-extent axis).
*/
export function safeRange(
padFrac = 0.1,
): (u: uPlot, dataMin: number, dataMax: number) => [number, number] {
return (_u, dataMin, dataMax) => {
if (dataMin == null || dataMax == null || !isFinite(dataMin) || !isFinite(dataMax)) {
return [0, 1];
}
if (dataMin === dataMax) {
const pad = Math.abs(dataMin) * 0.05 || 0.5;
return [dataMin - pad, dataMax + pad];
}
const pad = (dataMax - dataMin) * padFrac;
return [dataMin - pad, dataMax + pad];
};
}
9 changes: 9 additions & 0 deletions packages/ui/src/chart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ export { transientZonePlugin } from './transient-zone-plugin.ts';
export { AXIS_TEXT, AXIS_GRID, AXIS_TICK, getThemeColors } from './theme-colors.ts';
export { VIRIDIS_LUT, viridisRGB, viridisCss } from './colormap.ts';
export { niceTicks } from './chart-math.ts';
export {
chartAxis,
labeledAxis,
integerTickValues,
hiddenTickValues,
syncCursor,
staticCursor,
safeRange,
} from './axis-helpers.ts';
export {
OKABE_ITO,
OKABE_ITO_CYCLE,
Expand Down
Loading