1import type { FigureState, AxesState } from "numbl/graphics";
2import { TRACE_CATEGORIES, type Tree } from "./tree";
4/**
5 * Per-element view state layered on top of the immutable parsed figure. The
6 * rendered figure is *derived* from (figure + ViewState) via `applyViewState`,
7 * so new kinds of per-element view controls (highlight, pin, style overrides,
8 * …) can be added as additional fields + transform steps without touching the
9 * parse/tree code.
10 *
11 * Today it carries visibility: which axes/traces are hidden. "Show only" is
12 * expressed as "hide everything else", so showing a set, hiding one, and
13 * isolating one all reduce to manipulating `hidden`.
14 */
15export interface ViewState {
16 /** Ids of axes/trace elements explicitly hidden from the figure. */
17 hidden: Set<string>;
18 // Future: highlighted: Set<string>; pinned: Set<string>; overrides: Map<…>;
19}
21export const emptyViewState = (): ViewState => ({ hidden: new Set() });
23export function toggleHidden(vs: ViewState, id: string): ViewState {
24 const hidden = new Set(vs.hidden);
25 if (hidden.has(id)) hidden.delete(id);
26 else hidden.add(id);
27 return { ...vs, hidden };
28}
30export function showAll(vs: ViewState): ViewState {
31 return { ...vs, hidden: new Set() };
32}
34/** The element itself was hidden. */
35export function isHidden(vs: ViewState, id: string): boolean {
36 return vs.hidden.has(id);
37}
39/** Hidden itself, or its containing axes is hidden (so it won't render). */
40export function isEffectivelyHidden(vs: ViewState, id: string): boolean {
41 if (vs.hidden.has(id)) return true;
42 const m = id.match(/^(axes\/\d+)\/trace\//);
43 return m ? vs.hidden.has(m[1]) : false;
44}
46/** Show only `id`: hide every other axes/trace. A dataset isolates its parent
47 * trace; the figure root shows everything. */
48export function isolate(vs: ViewState, tree: Tree, id: string): ViewState {
49 if (id === "figure") return showAll(vs);
50 const node = tree.byId.get(id);
51 if (!node) return vs;
53 const keep = new Set<string>();
54 if (node.icon === "axes") {
55 keep.add(id);
56 for (const c of node.children) if (c.icon === "trace") keep.add(c.id);
57 } else {
58 // trace or dataset → resolve to the trace and its axes
59 const traceId = node.icon === "dataset" ? id.replace(/\/[^/]+$/, "") : id;
60 keep.add(traceId);
61 keep.add(traceId.replace(/\/trace\/\d+$/, ""));
62 }
64 const hidden = new Set<string>();
65 for (const [nid, n] of tree.byId)
66 if ((n.icon === "axes" || n.icon === "trace") && !keep.has(nid))
67 hidden.add(nid);
68 return { ...vs, hidden };
69}
71/** Whether a figure has anything to draw (any trace, or a uihtml component). */
72export function figureHasContent(figure: FigureState): boolean {
73 if (figure.uihtml) return true;
74 for (const ax of Object.values(figure.axes)) {
75 for (const [field, , single] of TRACE_CATEGORIES) {
76 const cur = (ax as Record<string, unknown>)[field as string];
77 if (single ? !!cur : Array.isArray(cur) && cur.length > 0) return true;
78 }
79 }
80 return false;
81}
83/** Derive the figure to render from the parsed figure + view state. */
84export function applyViewState(figure: FigureState, vs: ViewState): FigureState {
85 if (figure.uihtml || vs.hidden.size === 0) return figure;
87 const outAxes: FigureState["axes"] = {};
88 for (const [idxStr, ax] of Object.entries(figure.axes)) {
89 const idx = Number(idxStr);
90 const axId = `axes/${idx}`;
91 if (vs.hidden.has(axId)) continue; // whole axes hidden
93 const next = { ...(ax as Record<string, unknown>) };
94 let k = 0; // flattened trace index, matching the object tree
95 for (const [field, , single] of TRACE_CATEGORIES) {
96 const cur = (ax as Record<string, unknown>)[field as string];
97 if (single) {
98 if (cur) {
99 const tid = `${axId}/trace/${k++}`;
100 if (vs.hidden.has(tid)) next[field as string] = undefined;
101 }
102 } else if (Array.isArray(cur)) {
103 const kept: unknown[] = [];
104 for (const t of cur) {
105 const tid = `${axId}/trace/${k++}`;
106 if (!vs.hidden.has(tid)) kept.push(t);
107 }
108 next[field as string] = kept;
109 }
110 }
111 outAxes[idx] = next as unknown as AxesState;
112 }
114 // If isolating collapsed a subplot grid to a single axes, let it fill.
115 let subplotGrid = figure.subplotGrid;
116 if (subplotGrid && Object.keys(outAxes).length <= 1) subplotGrid = undefined;
117 return { ...figure, axes: outAxes, subplotGrid };
118}