4b82df514f
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
112 lines
4.1 KiB
JavaScript
112 lines
4.1 KiB
JavaScript
// Pure helpers for the cross-subject metrics plot interactions. No React, no I/O.
|
|
|
|
// Sort a Recharts tooltip payload by numeric value descending, dropping entries
|
|
// whose value is null/undefined. Returns a NEW array (does not mutate input).
|
|
export function sortPayloadByValueDesc(payload) {
|
|
if (!Array.isArray(payload)) return [];
|
|
return payload
|
|
.filter((p) => p && p.value != null)
|
|
.sort((a, b) => b.value - a.value);
|
|
}
|
|
|
|
// Toggle an id's membership in a Set, returning a NEW Set.
|
|
export function toggleInSet(set, id) {
|
|
const next = new Set(set);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
}
|
|
|
|
// Add (hidden=true) or remove (hidden=false) a list of ids from a hidden-set.
|
|
// Returns a NEW Set.
|
|
export function setIdsHidden(set, ids, hidden) {
|
|
const next = new Set(set);
|
|
for (const id of ids) {
|
|
if (hidden) next.add(id);
|
|
else next.delete(id);
|
|
}
|
|
return next;
|
|
}
|
|
|
|
// Tri-state for a group checkbox given which subject ids are hidden.
|
|
// 'all' = all visible, 'none' = all hidden, 'some' = mixed.
|
|
export function groupVisibilityState(subjectIds, hiddenSet) {
|
|
if (subjectIds.length === 0) return 'none';
|
|
const hiddenCount = subjectIds.filter((id) => hiddenSet.has(id)).length;
|
|
if (hiddenCount === 0) return 'all';
|
|
if (hiddenCount === subjectIds.length) return 'none';
|
|
return 'some';
|
|
}
|
|
|
|
// Group chart "lines" by their .group field, preserving first-seen group order
|
|
// and line order within each group. Returns [{ group, subjects: [line...] }].
|
|
export function groupLines(lines) {
|
|
const order = [];
|
|
const byGroup = new Map();
|
|
for (const line of lines) {
|
|
const g = line.group ?? '—';
|
|
if (!byGroup.has(g)) { byGroup.set(g, []); order.push(g); }
|
|
byGroup.get(g).push(line);
|
|
}
|
|
return order.map((group) => ({ group, subjects: byGroup.get(group) }));
|
|
}
|
|
|
|
// ── Plot config (de)serialization ───────────────────────────────────────────────
|
|
// Translate between ExperimentAnalysisCharts React state and the stored JSONB blob.
|
|
// readPlotConfig is defensive: it never throws and fills safe defaults for missing
|
|
// or malformed input (unknown keys are ignored).
|
|
|
|
// Symmetric with readChart so a serialize→read round-trip is lossless.
|
|
function snapshotChart(s = {}) {
|
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
return {
|
|
height: Number.isFinite(s.height) ? s.height : undefined,
|
|
yMin: str(s.yMin),
|
|
yMax: str(s.yMax),
|
|
xZoomMin: str(s.xZoomMin),
|
|
xZoomMax: str(s.xZoomMax),
|
|
xFieldOverride: typeof s.xFieldOverride === 'string' ? s.xFieldOverride : null,
|
|
};
|
|
}
|
|
|
|
export function serializePlotConfig(state = {}) {
|
|
const { xField, groupBy, tickStep, metric, hiddenSubjects, sidebarOpen, counts, rate } = state;
|
|
return {
|
|
xField,
|
|
groupBy,
|
|
tickStep,
|
|
metric,
|
|
hiddenSubjects: [...(hiddenSubjects ?? [])].sort(),
|
|
sidebarOpen: !!sidebarOpen,
|
|
counts: snapshotChart(counts),
|
|
rate: snapshotChart(rate),
|
|
};
|
|
}
|
|
|
|
function readChart(c) {
|
|
const o = c && typeof c === 'object' && !Array.isArray(c) ? c : {};
|
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
return {
|
|
height: Number.isFinite(o.height) ? o.height : undefined,
|
|
yMin: str(o.yMin),
|
|
yMax: str(o.yMax),
|
|
xZoomMin: str(o.xZoomMin),
|
|
xZoomMax: str(o.xZoomMax),
|
|
xFieldOverride: typeof o.xFieldOverride === 'string' ? o.xFieldOverride : null,
|
|
};
|
|
}
|
|
|
|
export function readPlotConfig(raw) {
|
|
const c = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
return {
|
|
xField: typeof c.xField === 'string' ? c.xField : undefined,
|
|
groupBy: typeof c.groupBy === 'string' ? c.groupBy : undefined,
|
|
tickStep: Number.isFinite(c.tickStep) ? c.tickStep : undefined,
|
|
metric: typeof c.metric === 'string' ? c.metric : undefined,
|
|
hiddenSubjects: Array.isArray(c.hiddenSubjects) ? c.hiddenSubjects.filter((x) => typeof x === 'string') : [],
|
|
sidebarOpen: c.sidebarOpen === true,
|
|
counts: readChart(c.counts),
|
|
rate: readChart(c.rate),
|
|
};
|
|
}
|