@fasl-work/caos-app-shell 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/chart.css CHANGED
@@ -22,3 +22,8 @@
22
22
  .caos-chart.fill[data-parity] > .caos-chart-plot { display: flex; align-items: center; justify-content: center; }
23
23
  .caos-chart[data-pick] .u-over { cursor: pointer; }
24
24
  .caos-chart .u-cursor-x, .caos-chart .u-cursor-y { border-color: var(--color-fg-faint) !important; }
25
+ /* The key that solos a series (`solo`, 0.18.0): each entry is a button. At rest it reads as the plain key; the soloed one is
26
+ set off by a border and bold text, in the text colour, never by colour alone. */
27
+ .caos-chart-key-button { display: inline-flex; align-items: center; gap: 0.3rem; min-width: 0; margin: 0; padding: 0.15rem 0.45rem; border: 1px solid transparent; border-radius: 0.4rem; background: none; color: inherit; font: inherit; line-height: inherit; text-align: start; cursor: pointer; }
28
+ .caos-chart-key-button:hover { border-color: var(--color-border); }
29
+ .caos-chart-key-button[aria-pressed="true"] { border-color: var(--color-fg-subtle); color: var(--color-fg); font-weight: 600; }
package/dist/chart.d.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import * as react from 'react';
2
2
  import uPlot from 'uplot';
3
- import { F as FormatOptions, S as ShellColorToken } from './tokens-BTOzar-U.js';
4
- import { B as BiText } from './text-BlY47nIt.js';
3
+ import { F as FormatOptions, S as ShellColorToken } from './text-DyQ1A8oo.js';
4
+ export { r as referenceLineName } from './text-DyQ1A8oo.js';
5
+ import { B as BiText, L as Lang } from './text-BlY47nIt.js';
5
6
  import 'zustand';
6
7
 
7
8
  interface ChartSeries {
9
+ /** What identifies the series to the solo key (0.18.0): the solo follows the series with this id when the list of series
10
+ * changes (one dropped, one added, a label changed), not its position. Without an id the English label is the key. Unique
11
+ * within the chart; with two series of one key the first is taken. */
12
+ id?: string;
8
13
  label: BiText;
9
14
  /** One value per x; `null` leaves a gap. */
10
15
  values: (number | null)[];
@@ -20,6 +25,14 @@ interface ChartSeries {
20
25
  * value. It is drawn over the plot on its own x, outside the curve's data, so it takes no part in the cursor, the
21
26
  * pick or the readout, and the axes take its extent in. */
22
27
  x?: number[];
28
+ /** A dot at each value of a `line` series (CAOS_Fragmenta drew them under 40 values: `dots={x.length < 40}`). Off by
29
+ * default; no dot is drawn at a `null`. */
30
+ dots?: boolean;
31
+ }
32
+ /** A named x tick: the x value it sits at and the text the axis reads there (and the readout, for an x on it). */
33
+ interface ChartTick {
34
+ value: number;
35
+ label: BiText;
23
36
  }
24
37
  /** What a reference line may be dragged over: the range it stays inside, and the step it snaps to (a hundredth of the
25
38
  * range when not given; on a log axis the step is a factor). */
@@ -46,6 +59,17 @@ interface UPlotChartProps {
46
59
  values: number[];
47
60
  time?: boolean;
48
61
  log?: boolean;
62
+ /** Named ticks (0.18.0): the axis ticks exactly at these values, no other, each labelled with its text in the page
63
+ * language (a categorical axis: protocols, blast ids). The readout names an x that sits on one by its label. A tick
64
+ * outside the range is not drawn; an empty list leaves uPlot its own ticks. A value listed twice keeps its first label.
65
+ * Labels must be different from each other, adjacent or not (the gate fails any repeated label, G6). An x is on a tick
66
+ * when it is within 1e-9 of the x span of it. Ignored on a time axis. */
67
+ ticks?: ChartTick[];
68
+ /** The x scale spans at least this range (0.18.0), as `y.range` fixes the y scale: widen it past the data so an end tick's
69
+ * label is not cut by the plot edge. The data and the overlays still widen it further. On a log axis a range that reaches 0
70
+ * or below is ignored (the overlays still widen the scale); on a parity chart it is ignored, the chart's own shared range
71
+ * rules. */
72
+ range?: [number, number];
49
73
  };
50
74
  y: ChartAxis & {
51
75
  log?: boolean;
@@ -58,10 +82,10 @@ interface UPlotChartProps {
58
82
  label: BiText;
59
83
  }[];
60
84
  /** Horizontal reference lines at y values, labelled at their right end (a null model's constant prediction, a
61
- * target, a limit). */
85
+ * target, a limit). A line without a `label` is drawn with no label box and nothing reserved for one (a zero line). */
62
86
  yMarks?: {
63
87
  y: number;
64
- label: BiText;
88
+ label?: BiText;
65
89
  tone?: YMarkTone;
66
90
  drag?: YMarkDrag;
67
91
  }[];
@@ -88,7 +112,40 @@ interface UPlotChartProps {
88
112
  readout?: (index: number) => string;
89
113
  /** The readout at rest; by default "Hover the chart to read the values". */
90
114
  hint?: BiText;
115
+ /** The key solos a series (0.18.0): each entry is a button (`aria-pressed`, Tab, click, Enter or Space) that dims every
116
+ * other series (alpha 0.12, 1 px) and leaves the chosen one as drawn; pressing it again releases it, pressing another
117
+ * moves the solo. The chart is redrawn, not rebuilt. A chart of one series shows the key too. Off by default. */
118
+ solo?: boolean;
91
119
  }
120
+ /** The alpha a series takes when another one is soloed (0.18.0; what CAOS_Fragmenta's key did). */
121
+ declare const SOLO_DIM_ALPHA = 0.12;
122
+ /** How a series is drawn while `solo` (the index in `series` of the soloed one, or null) is held: as given, or dimmed to
123
+ * `SOLO_DIM_ALPHA` and 1 px when another is soloed. Dimmed, never hidden, so the reader keeps the context. */
124
+ declare function soloStyle(solo: number | null, i: number, width: number): {
125
+ alpha: number;
126
+ width: number;
127
+ };
128
+ /** The values a named x axis ticks at inside [min, max]: ascending, each once; on a log axis only the ones above 0. A value
129
+ * that is not a number is dropped. */
130
+ declare function tickSplits(ticks: readonly ChartTick[], min: number, max: number, log?: boolean): number[];
131
+ /** The label of the named tick at an x, in the page language; null when no tick sits there. An x is on a tick when it is
132
+ * within 1e-9 of `span` (the x span, as `tickSplits` takes the range) of its value, so 0.1 + 0.2 is on a tick at 0.3; without
133
+ * a span only an equal x is. A value listed twice keeps its first label. */
134
+ declare function tickLabelAt(ticks: readonly ChartTick[] | undefined, x: number, lang: Lang, span?: number): string | null;
135
+ /** How many of the labels of named ticks repeat another's, adjacent or not (the gate's check of numeric ticks counts only
136
+ * neighbours, and on a categorical axis order means nothing), and which ones; an empty label is no label. */
137
+ declare function namedRepeats(labels: readonly string[]): {
138
+ n: number;
139
+ names: string[];
140
+ };
141
+ /** What a series is known by to the solo key: its id, else its English label. */
142
+ declare function soloKey(s: ChartSeries): string;
143
+ /** The x scale of a chart whose x range is fixed, or whose overlays reach past the curve: the data's extent widened by the
144
+ * given range and by the overlays' x; null when none is given (uPlot's own range), and on a log axis when the data would reach 0.
145
+ * One x alone gets a unit either side, a decade on a log axis. `xs` is sorted ascending. */
146
+ declare function xScaleRange(xs: readonly number[], ext: {
147
+ x: [number, number];
148
+ } | null, range: readonly [number, number] | undefined, log: boolean): [number, number] | null;
92
149
  /** The colour and dash of a series: its own, or the rotation's; when the rotation comes round (a seventh series
93
150
  * repeats the first colour) the repeat is dashed, so two series never look the same (CAOS_Fragmenta drew this for
94
151
  * itself until 0.9.0). */
@@ -169,12 +226,12 @@ declare function drawMarks(u: uPlot, marks: {
169
226
  /**
170
227
  * The horizontal reference lines of a chart and their labels: a dashed line across the plot, its label at the right
171
228
  * end above the line (below it when the line runs along the top), inside the plot, haloed; a label that would overlap
172
- * the one placed before it moves left of it. A line outside the y range is not drawn. A mark's `color` (the tone's) draws
229
+ * the one placed before it moves left of it. A line outside the y range is not drawn; a line without a label is drawn alone. A mark's `color` (the tone's) draws
173
230
  * its line and its label; without one `style.color` does.
174
231
  */
175
232
  declare function drawYMarks(u: uPlot, marks: {
176
233
  y: number;
177
- label: string;
234
+ label?: string;
178
235
  color?: string;
179
236
  }[], style: {
180
237
  color: string;
@@ -195,8 +252,10 @@ declare function nearestPoint(xs: readonly number[], ys: readonly (readonly (num
195
252
  * uPlot's legend, which a sized host clips (known shell defect 3). A chart of two or more series keys every series
196
253
  * under the plot, always (its colour, a dashed or a dotted swatch, its label): at rest the readout names none of them
197
254
  * (known shell defect 17). An axis with fixed decimals ticks only where its labels differ (`tickSteps`). The host
198
- * declares `data-series`, `data-axis-titles`, `data-ticks-cut`, `data-ticks-repeat` and `data-drawn` for the gate.
255
+ * declares `data-series`, `data-axis-titles`, `data-ticks-cut`, `data-ticks-repeat` and `data-drawn` for the gate. Since
256
+ * 0.18.0: named x ticks (`x.ticks`, declared in `data-x-tick-labels`), an x range (`x.range`), a key that solos a series
257
+ * (`solo`, declared in `data-solo`), dots on a line (`ChartSeries.dots`) and reference lines without a label.
199
258
  */
200
- declare function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height, onCursor, onPick, parity, readout, hint }: UPlotChartProps): react.JSX.Element;
259
+ declare function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height, onCursor, onPick, parity, readout, hint, solo }: UPlotChartProps): react.JSX.Element;
201
260
 
202
- export { type ChartAxis, type ChartSeries, LOG_TABLE_FLOOR, UPlotChart, type UPlotChartProps, YMARK_TONE_TOKENS, type YMarkDrag, type YMarkTone, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
261
+ export { type ChartAxis, type ChartSeries, type ChartTick, LOG_TABLE_FLOOR, SOLO_DIM_ALPHA, UPlotChart, type UPlotChartProps, YMARK_TONE_TOKENS, type YMarkDrag, type YMarkTone, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, namedRepeats, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, soloKey, soloStyle, sortOrder, tickLabelAt, tickSplits, tickSteps, xScaleRange };
package/dist/chart.js CHANGED
@@ -1,8 +1,9 @@
1
- import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, NBSP, formatNumber, formatTicks } from './chunk-YECZXGVG.js';
1
+ import { pick, useShellLang, useThemeStore, useStageSize, resolveToken, NBSP, formatNumber, formatTicks, referenceLineName } from './chunk-DBCMAL2G.js';
2
+ export { referenceLineName } from './chunk-DBCMAL2G.js';
2
3
  import './chunk-33WTUQ7Q.js';
3
- import { useRef, useCallback, useState, useMemo, useEffect } from 'react';
4
+ import { useRef, useCallback, useState, useEffect, useMemo } from 'react';
4
5
  import uPlot from 'uplot';
5
- import { jsxs, jsx } from 'react/jsx-runtime';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
7
 
7
8
  var YMARK_TONE_TOKENS = {
8
9
  warn: "--color-warn",
@@ -11,6 +12,52 @@ var YMARK_TONE_TOKENS = {
11
12
  neutral: "--color-fg-subtle"
12
13
  };
13
14
  var ROTATION = ["--color-accent", "--color-magenta", "--color-accent-2", "--color-good", "--color-warn", "--color-bad"];
15
+ var SOLO_DIM_ALPHA = 0.12;
16
+ function soloStyle(solo, i, width) {
17
+ return solo !== null && solo !== i ? { alpha: SOLO_DIM_ALPHA, width: 1 } : { alpha: 1, width };
18
+ }
19
+ function tickSplits(ticks, min, max, log = false) {
20
+ const eps = Math.abs(max - min) * 1e-9;
21
+ const values = ticks.map((t) => t.value).filter((v) => Number.isFinite(v) && (!log || v > 0) && v >= min - eps && v <= max + eps);
22
+ return [...new Set(values)].sort((a, b) => a - b);
23
+ }
24
+ function tickLabelAt(ticks, x, lang, span = 0) {
25
+ const eps = Math.abs(span) * 1e-9;
26
+ const t = ticks?.find((one) => Math.abs(one.value - x) <= eps);
27
+ return t ? pick(t.label, lang) : null;
28
+ }
29
+ function namedRepeats(labels) {
30
+ const seen = /* @__PURE__ */ new Set();
31
+ const names = [];
32
+ let n = 0;
33
+ for (const l of labels) {
34
+ if (!l) continue;
35
+ if (seen.has(l)) {
36
+ n++;
37
+ if (!names.includes(l)) names.push(l);
38
+ } else seen.add(l);
39
+ }
40
+ return { n, names };
41
+ }
42
+ function soloKey(s) {
43
+ return s.id ?? pick(s.label, "en");
44
+ }
45
+ function xScaleRange(xs, ext, range, log) {
46
+ const given = range && !(log && Math.min(range[0], range[1]) <= 0) ? range : void 0;
47
+ if (!ext && !given || xs.length === 0) return null;
48
+ let lo = xs[0];
49
+ let hi = xs[xs.length - 1];
50
+ if (ext) {
51
+ lo = Math.min(lo, ext.x[0]);
52
+ hi = Math.max(hi, ext.x[1]);
53
+ }
54
+ if (given) {
55
+ lo = Math.min(lo, given[0], given[1]);
56
+ hi = Math.max(hi, given[0], given[1]);
57
+ }
58
+ if (log && !(lo > 0)) return null;
59
+ return hi > lo ? [lo, hi] : log ? [lo / 10, hi * 10] : [lo - 1, hi + 1];
60
+ }
14
61
  function seriesStyle(s, i) {
15
62
  const color = s.color ?? ROTATION[i % ROTATION.length];
16
63
  const dash = s.dash ?? (!s.color && i >= ROTATION.length ? [6, 4] : void 0);
@@ -221,6 +268,7 @@ function drawYMarks(u, marks, style) {
221
268
  ctx.lineTo(right, py);
222
269
  ctx.stroke();
223
270
  ctx.setLineDash([]);
271
+ if (!m.label) continue;
224
272
  const w = ctx.measureText(m.label).width;
225
273
  const base = py - gap - textH < top ? py + gap + textH : py - gap;
226
274
  let x1 = right - gap;
@@ -255,7 +303,7 @@ function nearestPoint(xs, ys, left, top) {
255
303
  }
256
304
  return best;
257
305
  }
258
- function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint }) {
306
+ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280, onCursor, onPick, parity, readout, hint, solo }) {
259
307
  const lang = useShellLang();
260
308
  const theme = useThemeStore((s) => s.theme);
261
309
  const fill = height === "fill";
@@ -273,6 +321,14 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
273
321
  const uRef = useRef(null);
274
322
  const hostRef = useRef(null);
275
323
  const [cursor, setCursor] = useState(null);
324
+ const [soloPick, setSoloPick] = useState(null);
325
+ const soloFound = solo && soloPick !== null ? series.findIndex((s) => soloKey(s) === soloPick) : -1;
326
+ const soloIdx = soloFound >= 0 ? soloFound : null;
327
+ const soloRef = useRef(soloIdx);
328
+ soloRef.current = soloIdx;
329
+ useEffect(() => {
330
+ if (soloPick !== null && soloIdx === null) setSoloPick(null);
331
+ }, [soloPick, soloIdx]);
276
332
  const onCursorRef = useRef(onCursor);
277
333
  onCursorRef.current = onCursor;
278
334
  const onPickRef = useRef(onPick);
@@ -290,19 +346,15 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
290
346
  const levels = useMemo(() => (yMarks ?? []).flatMap((m) => m.drag ? [m.drag.min, m.drag.max] : [m.y]), [yMarks]);
291
347
  const range = useMemo(() => parity ? parityRange(xs, ...ys, levels, ext ? [...ext.x, ...ext.y] : []) : null, [parity, xs, ys, levels, ext]);
292
348
  const yLevels = !parity && !y.range && (levels.length > 0 || ext) ? [...levels, ...ext ? ext.y : []] : null;
293
- const xExt = useMemo(() => {
294
- if (!ext || range || xs.length === 0) return null;
295
- const lo = Math.min(xs[0], ext.x[0]);
296
- const hi = Math.max(xs[xs.length - 1], ext.x[1]);
297
- if (x.log && !(lo > 0)) return null;
298
- return hi > lo ? [lo, hi] : x.log ? [lo / 10, hi * 10] : [lo - 1, hi + 1];
299
- }, [ext, range, xs, x.log]);
349
+ const xExt = useMemo(() => range ? null : xScaleRange(xs, ext, x.range, Boolean(x.log)), [ext, range, xs, x.log, x.range]);
350
+ const xSpan = xs.length > 0 ? Math.max(Math.abs(xs[xs.length - 1] - xs[0]), x.range ? Math.abs(x.range[1] - x.range[0]) : 0) : 0;
351
+ const ticks = !x.time && x.ticks && x.ticks.length > 0 ? x.ticks : void 0;
300
352
  const tiny = useMemo(
301
353
  () => ({
302
- x: Boolean(x.log) && smallestPositive(ext ? [xs, ext.x] : [xs], range) < LOG_TABLE_FLOOR,
354
+ x: !ticks && Boolean(x.log) && smallestPositive(ext ? [xs, ext.x] : [xs], range) < LOG_TABLE_FLOOR,
303
355
  y: Boolean(y.log) && smallestPositive(yLevels ? [...ys, yLevels] : ys, range ?? y.range) < LOG_TABLE_FLOOR
304
356
  }),
305
- [x.log, y.log, xs, ys, range, y.range, yLevels, ext]
357
+ [x.log, y.log, xs, ys, range, y.range, yLevels, ext, ticks]
306
358
  );
307
359
  const xLabel = pick(x.label, lang);
308
360
  const yLabel = pick(y.label, lang);
@@ -318,14 +370,14 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
318
370
  const xUnit = x.unit ? pick(x.unit, lang) : "";
319
371
  const yUnit = y.unit ? pick(y.unit, lang) : "";
320
372
  const structure = JSON.stringify({
321
- s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9, isOverlay(s)]),
322
- x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x, xExt],
373
+ s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line", s.size ?? 9, isOverlay(s), Boolean(s.dots)]),
374
+ x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x, xExt, ticks ? ticks.map((t) => [t.value, pick(t.label, lang)]) : null],
323
375
  oe: ext,
324
376
  p: range,
325
377
  pick: Boolean(onPick),
326
378
  y: [yLabel, yUnit, Boolean(y.log), y.range ?? null, y.format ?? null, tiny.y],
327
379
  m: (marks ?? []).map((m) => [m.x, pick(m.label, lang)]),
328
- ym: (yMarks ?? []).map((m) => m.drag ? ["drag", m.drag] : [m.y, pick(m.label, lang), m.tone ?? "warn"]),
380
+ ym: (yMarks ?? []).map((m) => m.drag ? ["drag", m.drag, Boolean(m.label)] : [m.y, m.label ? pick(m.label, lang) : null, m.tone ?? "warn"]),
329
381
  theme,
330
382
  lang,
331
383
  h,
@@ -342,12 +394,24 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
342
394
  const font = `11px ${family}`;
343
395
  const surface = resolveToken("--color-surface", "#fff");
344
396
  const repeats = { x: 0, y: 0 };
397
+ const declareTicks = (axis, labels, repeated = repeatedLabels(labels)) => {
398
+ repeats[axis] = repeated;
399
+ hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
400
+ hostRef.current?.setAttribute(`data-ticks-${axis}`, String(labels.filter((l) => l).length));
401
+ };
345
402
  const tick = (fmt, axis, pre) => (_u, vals) => {
346
403
  const labels = formatTicks(pre ? pre(vals) : vals, lang, fmt ?? { digits: 4 });
347
404
  if (axis === "x" && pre) hostRef.current?.setAttribute("data-log-labels", labels.filter((l) => l).join("|"));
348
- repeats[axis] = repeatedLabels(labels);
349
- hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
350
- hostRef.current?.setAttribute(`data-ticks-${axis}`, String(labels.filter((l) => l).length));
405
+ declareTicks(axis, labels);
406
+ return labels;
407
+ };
408
+ const namedTicks = (_u, vals) => {
409
+ const labels = vals.map((v) => v === null || v === void 0 ? "" : tickLabelAt(ticks, v, lang) ?? "");
410
+ hostRef.current?.setAttribute("data-x-tick-labels", labels.filter((l) => l).join("|"));
411
+ const dup = namedRepeats(labels);
412
+ if (dup.n > 0) hostRef.current?.setAttribute("data-ticks-repeated", dup.names.join("|"));
413
+ else hostRef.current?.removeAttribute("data-ticks-repeated");
414
+ declareTicks("x", labels, dup.n);
351
415
  return labels;
352
416
  };
353
417
  const near = { left: NaN, top: NaN, idx: null };
@@ -401,10 +465,15 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
401
465
  label: xUnit ? `${xLabel} (${xUnit})` : xLabel,
402
466
  labelFont: font,
403
467
  // a log x axis labels the decades only; the ticks between stay drawn, unlabelled
404
- values: x.time ? void 0 : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
405
- ...xSteps ? { incrs: xSteps } : {},
406
- ...range ? { space: PARITY_TICK_SPACE } : {},
407
- ...tiny.x ? DECADE_AXIS : {}
468
+ values: x.time ? void 0 : ticks ? namedTicks : tick(x.format, "x", x.log && !tiny.x ? decadeOnly : void 0),
469
+ ...ticks ? (
470
+ // exactly the named values, no other tick
471
+ { splits: (_u, _i, min, max) => tickSplits(ticks, min, max, Boolean(x.log)), filter: (_u, splits) => splits }
472
+ ) : {
473
+ ...xSteps ? { incrs: xSteps } : {},
474
+ ...range ? { space: PARITY_TICK_SPACE } : {},
475
+ ...tiny.x ? DECADE_AXIS : {}
476
+ }
408
477
  },
409
478
  {
410
479
  stroke: fg,
@@ -434,7 +503,8 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
434
503
  ...baseEntries.map(({ s, i }) => {
435
504
  const style = seriesStyle(s, i);
436
505
  const stroke = resolveToken(style.color, "#4a8");
437
- return s.mode === "points" ? { label: pick(s.label, lang), stroke, width: 1.5, paths: () => null, points: { show: true, space: 0, size: s.size ?? 9, fill: stroke } } : { label: pick(s.label, lang), stroke, width: s.width ?? 2, dash: style.dash, spanGaps: false, points: { show: false } };
506
+ const dim = soloStyle(soloRef.current, i, s.mode === "points" ? 1.5 : s.width ?? 2);
507
+ return s.mode === "points" ? { label: pick(s.label, lang), stroke, width: dim.width, alpha: dim.alpha, paths: () => null, points: { show: true, space: 0, size: s.size ?? 9, fill: stroke } } : { label: pick(s.label, lang), stroke, width: dim.width, alpha: dim.alpha, dash: style.dash, spanGaps: false, points: s.dots ? { show: true, size: 5, fill: stroke } : { show: false } };
438
508
  })
439
509
  ],
440
510
  hooks: {
@@ -529,6 +599,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
529
599
  ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
530
600
  ctx.clip();
531
601
  for (const { s, i } of dots) {
602
+ ctx.globalAlpha = soloStyle(soloRef.current, i, 1).alpha;
532
603
  ctx.fillStyle = resolveToken(seriesStyle(s, i).color, "#4a8");
533
604
  ctx.strokeStyle = surface;
534
605
  ctx.lineWidth = Math.max(1, pr);
@@ -552,7 +623,7 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
552
623
  }
553
624
  const markStyle = { color: resolveToken("--color-warn", "#c80"), halo: surface, family };
554
625
  const lines = yMarksRef.current ?? [];
555
- if (lines.length) drawYMarks(u, lines.map((m, i) => ({ y: liveRef.current[i] ?? m.y, label: pick(m.label, lang), color: resolveToken(YMARK_TONE_TOKENS[m.tone ?? "warn"], "#c80") })), markStyle);
626
+ if (lines.length) drawYMarks(u, lines.map((m, i) => ({ y: liveRef.current[i] ?? m.y, label: m.label ? pick(m.label, lang) : void 0, color: resolveToken(YMARK_TONE_TOKENS[m.tone ?? "warn"], "#c80") })), markStyle);
556
627
  u.over.querySelectorAll("[data-y-handle]").forEach((el2) => {
557
628
  const i = Number(el2.getAttribute("data-y-handle"));
558
629
  const m = lines[i];
@@ -561,16 +632,22 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
561
632
  const top = u.valToPos(v, "y");
562
633
  el2.style.display = Number.isFinite(top) && top >= 0 && top <= u.bbox.height / (uPlot.pxRatio || 1) ? "" : "none";
563
634
  el2.style.top = `${top - HANDLE_HEIGHT / 2}px`;
564
- const label = pick(m.label, lang);
565
- el2.setAttribute("aria-label", label);
635
+ const label = m.label ? pick(m.label, lang) : "";
636
+ const shown = `${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`;
637
+ const unnamed = lines.flatMap((one, k) => one.drag && !one.label ? [k] : []);
638
+ el2.setAttribute("aria-label", label || referenceLineName(lang, unnamed.indexOf(i) + 1, unnamed.length));
566
639
  el2.setAttribute("aria-valuenow", String(Number(v.toPrecision(12))));
567
- el2.setAttribute("aria-valuetext", `${label}: ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`);
640
+ el2.setAttribute("aria-valuetext", label ? `${label}: ${shown}` : shown);
568
641
  });
569
642
  if (marks?.length) drawMarks(u, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), markStyle);
570
643
  }
571
644
  ]
572
645
  }
573
646
  };
647
+ if (!ticks) {
648
+ hostRef.current?.removeAttribute("data-x-tick-labels");
649
+ hostRef.current?.removeAttribute("data-ticks-repeated");
650
+ }
574
651
  uRef.current?.destroy();
575
652
  uRef.current = new uPlot(opts, data, el);
576
653
  return () => {
@@ -582,12 +659,25 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
582
659
  uRef.current?.setData(data);
583
660
  }, [data]);
584
661
  const overlaysKey = JSON.stringify(overlays.map(({ s, i }) => [i, s.x, s.values, s.size ?? 9, seriesStyle(s, i).color]));
585
- const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, pick(m.label, lang), m.tone ?? "warn"]));
662
+ const linesKey = JSON.stringify((yMarks ?? []).map((m) => [m.y, m.label ? pick(m.label, lang) : null, m.tone ?? "warn"]));
586
663
  useEffect(() => {
587
664
  uRef.current?.redraw(false);
588
665
  }, [linesKey, overlaysKey]);
666
+ useEffect(() => {
667
+ const u = uRef.current;
668
+ if (!u) return;
669
+ baseEntries.forEach(({ s, i }, k) => {
670
+ const dim = soloStyle(soloIdx, i, s.mode === "points" ? 1.5 : s.width ?? 2);
671
+ const one = u.series[k + 1];
672
+ if (!one) return;
673
+ one.alpha = dim.alpha;
674
+ one.width = dim.width;
675
+ });
676
+ u.redraw(false);
677
+ }, [soloIdx, baseEntries]);
589
678
  const read = cursor !== null && readout ? readout(order ? order[cursor] : cursor) : cursor !== null ? [
590
- `${xLabel} ${formatNumber(xs[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
679
+ // an x that sits on a named tick is named by it, not by its number
680
+ tickLabelAt(ticks, xs[cursor], lang, xSpan) !== null ? `${xLabel} ${tickLabelAt(ticks, xs[cursor], lang, xSpan)}` : `${xLabel} ${formatNumber(xs[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
591
681
  ...baseEntries.map(({ s }, k) => ({ s, v: ys[k][cursor] ?? null })).filter(({ v }) => !scatter || v !== null).map(({ s, v }) => `${pick(s.label, lang)} ${formatNumber(v, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`)
592
682
  ].join(" \xB7 ") : hint ? pick(hint, lang) : lang === "es" ? "Pase el cursor sobre el gr\xE1fico para leer los valores" : "Hover the chart to read the values";
593
683
  return /* @__PURE__ */ jsxs(
@@ -606,16 +696,20 @@ function UPlotChart({ x, y, series, marks, yMarks, onYMarkChange, height = 280,
606
696
  "data-y-marks": yMarks?.length ? String(yMarks.length) : void 0,
607
697
  "data-y-drag": dragCount ? String(dragCount) : void 0,
608
698
  "data-overlay": overlays.length ? String(overlays.length) : void 0,
699
+ "data-solo": soloIdx !== null ? String(soloIdx) : void 0,
609
700
  children: [
610
701
  /* @__PURE__ */ jsx("div", { ref: setPlot, className: "caos-chart-plot" }),
611
- series.length > 1 && /* @__PURE__ */ jsx("ul", { className: "caos-chart-legend", "aria-label": lang === "es" ? "Series del gr\xE1fico" : "Series of the chart", children: series.map((s, i) => /* @__PURE__ */ jsxs("li", { className: "caos-chart-key", "data-mode": s.mode ?? "line", "data-dash": seriesStyle(s, i).dash ? "1" : void 0, children: [
702
+ (series.length > 1 || solo) && /* @__PURE__ */ jsx("ul", { className: "caos-chart-legend", "aria-label": lang === "es" ? "Series del gr\xE1fico" : "Series of the chart", children: series.map((s, i) => /* @__PURE__ */ jsx("li", { className: "caos-chart-key", "data-mode": s.mode ?? "line", "data-dash": seriesStyle(s, i).dash ? "1" : void 0, children: solo ? /* @__PURE__ */ jsxs("button", { type: "button", className: "caos-chart-key-button", "aria-pressed": soloIdx === i, onClick: () => setSoloPick(soloIdx === i ? null : soloKey(s)), children: [
703
+ /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
704
+ pick(s.label, lang)
705
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
612
706
  /* @__PURE__ */ jsx("span", { className: "caos-chart-swatch", style: { "--swatch": `var(${seriesStyle(s, i).color})` }, "aria-hidden": "true" }),
613
707
  pick(s.label, lang)
614
- ] }, i)) }),
708
+ ] }) }, i)) }),
615
709
  /* @__PURE__ */ jsx("p", { className: "caos-chart-readout", "aria-live": "polite", title: read, children: read })
616
710
  ]
617
711
  }
618
712
  );
619
713
  }
620
714
 
621
- export { LOG_TABLE_FLOOR, UPlotChart, YMARK_TONE_TOKENS, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
715
+ export { LOG_TABLE_FLOOR, SOLO_DIM_ALPHA, UPlotChart, YMARK_TONE_TOKENS, clampMark, decadeOnly, drawMarks, drawYMarks, logDecades, namedRepeats, nearestPoint, nudgeMark, overlayExtent, parityBox, parityRange, rangeWithLevels, repeatedLabels, seriesStyle, smallestPositive, soloKey, soloStyle, sortOrder, tickLabelAt, tickSplits, tickSteps, xScaleRange };
@@ -214,4 +214,76 @@ function Stage({ label, children, className }) {
214
214
  );
215
215
  }
216
216
 
217
- export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens };
217
+ // src/chart/text.ts
218
+ function referenceLineName(lang, n, of) {
219
+ const base = lang === "es" ? "L\xEDnea de referencia" : "Reference line";
220
+ return of > 1 ? `${base} ${n}` : base;
221
+ }
222
+ var context;
223
+ function fontFamily() {
224
+ if (typeof document === "undefined") return "sans-serif";
225
+ return getComputedStyle(document.documentElement).getPropertyValue("--font-sans").trim() || "sans-serif";
226
+ }
227
+ function textWidth(text, px, weight = 400) {
228
+ if (context === void 0) {
229
+ try {
230
+ context = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
231
+ } catch {
232
+ context = null;
233
+ }
234
+ }
235
+ if (!context) return text.length * px * 0.62;
236
+ context.font = `${weight} ${px}px ${fontFamily()}`;
237
+ return context.measureText(text).width;
238
+ }
239
+ function widestLabel(labels, px, weight = 400) {
240
+ return Math.max(0, ...labels.map((label) => textWidth(label, px, weight)));
241
+ }
242
+ function shorten(line, width, px) {
243
+ if (textWidth(line, px) <= width) return line;
244
+ let cut = line;
245
+ while (cut.length > 1 && textWidth(`${cut}\u2026`, px) > width) cut = cut.slice(0, -1);
246
+ return `${cut.trimEnd()}\u2026`;
247
+ }
248
+ function fitLabel(label, width, px, maxLines = 2) {
249
+ if (textWidth(label, px) <= width) return { lines: [label], shortened: false };
250
+ const words = label.split(" ");
251
+ if (maxLines === 2 && words.length > 1) {
252
+ let best = null;
253
+ let bestWidth = Infinity;
254
+ for (let k = 1; k < words.length; k += 1) {
255
+ const first = words.slice(0, k).join(" ");
256
+ const second = words.slice(k).join(" ");
257
+ const widest = Math.max(textWidth(first, px), textWidth(second, px));
258
+ if (widest < bestWidth) {
259
+ bestWidth = widest;
260
+ best = [first, second];
261
+ }
262
+ }
263
+ if (best && bestWidth <= width) return { lines: best, shortened: false };
264
+ if (best) return { lines: [shorten(best[0], width, px), shorten(best[1], width, px)], shortened: true };
265
+ }
266
+ return { lines: [shorten(label, width, px)], shortened: true };
267
+ }
268
+ function niceStep(x) {
269
+ if (!(x > 0) || !Number.isFinite(x)) return 1;
270
+ const magnitude = 10 ** Math.floor(Math.log10(x));
271
+ return ([1, 2, 2.5, 5, 10].find((m) => m * magnitude >= x * (1 - 1e-12)) ?? 10) * magnitude;
272
+ }
273
+ function niceTicks(lo, hi, room, labelSize, most = 6) {
274
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return [0];
275
+ if (hi < lo) [lo, hi] = [hi, lo];
276
+ if (hi === lo) hi = lo === 0 ? 1 : lo + Math.abs(lo);
277
+ const count = Math.max(2, Math.min(most, Math.floor(room / (labelSize + 16)) + 1));
278
+ const step = niceStep((hi - lo) / (count - 1));
279
+ const first = Math.floor(lo / step + 1e-9) * step;
280
+ const ticks = [];
281
+ for (let k = 0; first + k * step <= hi + step * 1e-9 || ticks.length < 2; k += 1) {
282
+ ticks.push(Number((first + k * step).toPrecision(12)));
283
+ if (ticks.length > 50) break;
284
+ }
285
+ if (ticks[ticks.length - 1] < hi - step * 1e-9) ticks.push(Number((ticks[ticks.length - 1] + step).toPrecision(12)));
286
+ return ticks;
287
+ }
288
+
289
+ export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, fitLabel, fontFamily, formatNumber, formatTicks, niceStep, niceTicks, pick, readTheme, referenceLineName, resolveToken, textWidth, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens, widestLabel };
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ export { A as APP_ROUTE, a as AppShell, b as ArchTab, c as ArchitectureConfig, d
2
2
  import * as react from 'react';
3
3
  import { ReactNode, Component, ErrorInfo } from 'react';
4
4
  import * as zustand from 'zustand';
5
- import { F as FormatOptions } from './tokens-BTOzar-U.js';
6
- export { N as NBSP, a as NOT_AVAILABLE, b as SCIENTIFIC_BELOW, c as SHELL_TOKENS, S as ShellColorToken, d as ShellToken, f as formatNumber, e as formatTicks, r as resolveToken, u as useFormat, g as useThemeTokens } from './tokens-BTOzar-U.js';
5
+ import { F as FormatOptions } from './text-DyQ1A8oo.js';
6
+ export { a as FittedLabel, N as NBSP, b as NOT_AVAILABLE, c as SCIENTIFIC_BELOW, d as SHELL_TOKENS, S as ShellColorToken, e as ShellToken, f as fitLabel, g as fontFamily, h as formatNumber, i as formatTicks, n as niceStep, j as niceTicks, k as resolveToken, t as textWidth, u as useFormat, l as useThemeTokens, w as widestLabel } from './text-DyQ1A8oo.js';
7
7
  import { L as Lang, B as BiText } from './text-BlY47nIt.js';
8
8
  export { p as pick, u as useLangStore, a as useShellLang } from './text-BlY47nIt.js';
9
9
 
@@ -1118,40 +1118,6 @@ interface BarChartProps {
1118
1118
  */
1119
1119
  declare function BarChart({ title, data, axis, orientation, values, height, onSelect }: BarChartProps): react.JSX.Element;
1120
1120
 
1121
- /**
1122
- * Text that fits the drawing it labels, measured in the font the page renders.
1123
- *
1124
- * Products drew their axes and labels with fixed margins and a fixed width per character. The Linux fonts (DejaVu
1125
- * Sans) are wider than the Windows ones and Spanish labels are longer than English ones, so a column sized for one
1126
- * ran its labels out of the drawing in the other (CAOS_Fragmenta 0.05.000: "Tamaño medio clásico, limitado al bloque
1127
- * in situ" read "dio clásico, limitado al bloque in situ"; its deploy gate failed on Linux). Here a label is measured
1128
- * with the canvas text metrics of the page's own font (`--font-sans`, which the gate's wide-font pass replaces),
1129
- * broken over two lines at a word when it does not fit, and only then shortened with an ellipsis, so the drawing can
1130
- * give the whole label as its title. Ported from CAOS_Fragmenta `viz/text.ts` (0.07.000).
1131
- */
1132
- /** The page's sans-serif stack as the shell token resolves it. */
1133
- declare function fontFamily(): string;
1134
- /** The width of one line of text at a font size, in CSS pixels; outside a browser, an estimate on the wide side. */
1135
- declare function textWidth(text: string, px: number, weight?: number): number;
1136
- /** The widest a set of labels needs, each on one line. */
1137
- declare function widestLabel(labels: readonly string[], px: number, weight?: number): number;
1138
- interface FittedLabel {
1139
- /** One or two lines; the last is shortened with an ellipsis only when the lines allowed are not enough. */
1140
- lines: string[];
1141
- /** Whether the label was shortened, so the drawing gives the whole label as its title. */
1142
- shortened: boolean;
1143
- }
1144
- /** A label in at most `maxLines` lines of `width` pixels: whole when it fits, broken at a word, shortened last. */
1145
- declare function fitLabel(label: string, width: number, px: number, maxLines?: 1 | 2): FittedLabel;
1146
- /** The 1, 2, 2.5 or 5 times a power of ten at or above `x`. */
1147
- declare function niceStep(x: number): number;
1148
- /**
1149
- * Round ticks covering [lo, hi], as many as leave room for labels about `labelSize` pixels along an axis of `room`
1150
- * pixels (at least two, at most `most`). The first tick is at or below `lo` and the last at or above `hi`, so a bar
1151
- * or a point never runs past the last labelled value.
1152
- */
1153
- declare function niceTicks(lo: number, hi: number, room: number, labelSize: number, most?: number): number[];
1154
-
1155
1121
  /** One section of the control rail. A rail whose content does not fit is SPLIT into sections, one shown at a
1156
1122
  * time, never scrolled (ADR-0071 rule 6: a panel must show its own controls). */
1157
1123
  interface RailSection {
@@ -1380,4 +1346,4 @@ interface ViewsRowProps {
1380
1346
  */
1381
1347
  declare function ViewsRow({ children, shares }: ViewsRowProps): react.JSX.Element;
1382
1348
 
1383
- export { BREAKPOINTS, BarChart, type BarChartProps, type BarDatum, BiText, Callout, CardDeck, type CardDeckProps, type CaseDef, type CaseFocusConfig, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, CaseWorkbench, type CaseWorkbenchProps, ChipGroup, type ChipGroupProps, type ChipOption, type Citation, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, type DeckCard, DocPage, type DocPageProps, DocReferences, DocRefsScope, DocSection, type DocSectionProps, EXECUTIVE_SCHEMA_VERSION, Equation, type ExecutiveData, type ExecutiveProblem, ExecutiveView, type ExecutiveViewProps, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, type FittedLabel, FlipCard, type FlipCardProps, type FocusMode, type FocusModeConfig, type FocusModeOptions, FocusShell, type FocusShellProps, FormatOptions, Gauge, type GaugeProps, type GaugeZone, Glossary, type GlossaryEntry, InlineMath, Knob, type KnobProps, Landing, type Lane, LaneBadge, Lang, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, type PausedVizController, PlotCard, type PlotCardProps, type Provenance, Question, type QuestionProps, type RailSection, Readout, type ReadoutItem, type ReadoutProps, ReferenceList, Refs, Stage, type StageSize, Stat, type StatProps, type SubTabDef, SubTabs, type SubTabsProps, type SurfaceFocusConfig, SurfacePage, type SurfacePageProps, type TabDef, type TabGroupDef, TabGroups, type TabGroupsProps, Tabs, type TabsProps, type Theme, ThemeToggle, type Tone, type UsePausedVizOptions, VariantBar, type VariantBarProps, type VariantDef, Verdict, type VerdictProps, type ViewFocusConfig, ViewsRow, type ViewsRowProps, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, type WorkbenchGroup, WorkbenchLayout, type WorkbenchLayoutProps, type WorkbenchRoute, WorkbenchShell, type WorkbenchShellProps, type WorkbenchState, WorkbenchStateContext, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, deckPosition, deckWindow, firstAppearance, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, perPageFor, readCaseParam, readTheme, removeQueryParam, replaceQueryParam, sourcesPresent, textWidth, useFocusMode, usePausedViz, useStageSize, useThemeStore, useUrlView, useWorkbenchState, validateExecutive, viewParamOf, widestLabel, withCaseParam };
1349
+ export { BREAKPOINTS, BarChart, type BarChartProps, type BarDatum, BiText, Callout, CardDeck, type CardDeckProps, type CaseDef, type CaseFocusConfig, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, CaseWorkbench, type CaseWorkbenchProps, ChipGroup, type ChipGroupProps, type ChipOption, type Citation, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, type DeckCard, DocPage, type DocPageProps, DocReferences, DocRefsScope, DocSection, type DocSectionProps, EXECUTIVE_SCHEMA_VERSION, Equation, type ExecutiveData, type ExecutiveProblem, ExecutiveView, type ExecutiveViewProps, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, type FlipCardProps, type FocusMode, type FocusModeConfig, type FocusModeOptions, FocusShell, type FocusShellProps, FormatOptions, Gauge, type GaugeProps, type GaugeZone, Glossary, type GlossaryEntry, InlineMath, Knob, type KnobProps, Landing, type Lane, LaneBadge, Lang, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, type PausedVizController, PlotCard, type PlotCardProps, type Provenance, Question, type QuestionProps, type RailSection, Readout, type ReadoutItem, type ReadoutProps, ReferenceList, Refs, Stage, type StageSize, Stat, type StatProps, type SubTabDef, SubTabs, type SubTabsProps, type SurfaceFocusConfig, SurfacePage, type SurfacePageProps, type TabDef, type TabGroupDef, TabGroups, type TabGroupsProps, Tabs, type TabsProps, type Theme, ThemeToggle, type Tone, type UsePausedVizOptions, VariantBar, type VariantBarProps, type VariantDef, Verdict, type VerdictProps, type ViewFocusConfig, ViewsRow, type ViewsRowProps, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, type WorkbenchGroup, WorkbenchLayout, type WorkbenchLayoutProps, type WorkbenchRoute, WorkbenchShell, type WorkbenchShellProps, type WorkbenchState, WorkbenchStateContext, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, deckPosition, deckWindow, firstAppearance, groupByCategory, isStale, makeStateKey, perPageFor, readCaseParam, readTheme, removeQueryParam, replaceQueryParam, sourcesPresent, useFocusMode, usePausedViz, useStageSize, useThemeStore, useUrlView, useWorkbenchState, validateExecutive, viewParamOf, withCaseParam };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, useStageSize, NOT_AVAILABLE, formatTicks, formatNumber, NBSP } from './chunk-YECZXGVG.js';
2
- export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-YECZXGVG.js';
1
+ import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, useStageSize, NOT_AVAILABLE, widestLabel, fitLabel, formatTicks, niceTicks, textWidth, formatNumber, NBSP } from './chunk-DBCMAL2G.js';
2
+ export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, fitLabel, fontFamily, formatNumber, formatTicks, niceStep, niceTicks, pick, readTheme, resolveToken, textWidth, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens, widestLabel } from './chunk-DBCMAL2G.js';
3
3
  import { APP_ROUTE, LANDING_ROUTE } from './chunk-33WTUQ7Q.js';
4
4
  export { APP_ROUTE, LANDING_ROUTE, LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from './chunk-33WTUQ7Q.js';
5
5
  import { createContext, Component, useState, useRef, useId, useEffect, useLayoutEffect, useCallback, useContext, useMemo, Children, isValidElement } from 'react';
@@ -2092,7 +2092,7 @@ function executiveTextFields(d) {
2092
2092
  add("title", d.title, 1);
2093
2093
  add("key_statement", d.key_statement, 2);
2094
2094
  d.headline_figures.forEach((f, i) => figure(`headline_figures[${i}]`, f));
2095
- const { pain, context: context2, situation, trends, proposal } = d;
2095
+ const { pain, context, situation, trends, proposal } = d;
2096
2096
  add("pain.key_message", pain.key_message, 1);
2097
2097
  pain.paragraphs.forEach((p, i) => add(`pain.paragraphs[${i}]`, p, void 0, EXEC_LIMITS.minParagraphWords));
2098
2098
  pain.bearers.forEach((b, i) => {
@@ -2102,11 +2102,11 @@ function executiveTextFields(d) {
2102
2102
  });
2103
2103
  pain.figures.forEach((f, i) => figure(`pain.figures[${i}]`, f));
2104
2104
  pain.causes.forEach((c, i) => add(`pain.causes[${i}].cause`, c.cause));
2105
- add("context.key_message", context2.key_message, 1);
2106
- context2.paragraphs.forEach((p, i) => add(`context.paragraphs[${i}]`, p, void 0, EXEC_LIMITS.minParagraphWords));
2107
- add("context.setting", context2.setting);
2108
- context2.forces.forEach((f, i) => add(`context.forces[${i}].force`, f.force));
2109
- context2.evidence.forEach((e, i) => add(`context.evidence[${i}].statement`, e.statement, 1));
2105
+ add("context.key_message", context.key_message, 1);
2106
+ context.paragraphs.forEach((p, i) => add(`context.paragraphs[${i}]`, p, void 0, EXEC_LIMITS.minParagraphWords));
2107
+ add("context.setting", context.setting);
2108
+ context.forces.forEach((f, i) => add(`context.forces[${i}].force`, f.force));
2109
+ context.evidence.forEach((e, i) => add(`context.evidence[${i}].statement`, e.statement, 1));
2110
2110
  add("situation.key_message", situation.key_message, 1);
2111
2111
  situation.roles.forEach((r, i) => {
2112
2112
  add(`situation.roles[${i}].role`, r.role);
@@ -3133,74 +3133,6 @@ var BREAKPOINTS = {
3133
3133
  /** The gate's large screen: the viewport rules of ADR-0071 are measured from here up. */
3134
3134
  large: 1280
3135
3135
  };
3136
-
3137
- // src/chart/text.ts
3138
- var context;
3139
- function fontFamily() {
3140
- if (typeof document === "undefined") return "sans-serif";
3141
- return getComputedStyle(document.documentElement).getPropertyValue("--font-sans").trim() || "sans-serif";
3142
- }
3143
- function textWidth(text, px, weight = 400) {
3144
- if (context === void 0) {
3145
- try {
3146
- context = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
3147
- } catch {
3148
- context = null;
3149
- }
3150
- }
3151
- if (!context) return text.length * px * 0.62;
3152
- context.font = `${weight} ${px}px ${fontFamily()}`;
3153
- return context.measureText(text).width;
3154
- }
3155
- function widestLabel(labels, px, weight = 400) {
3156
- return Math.max(0, ...labels.map((label) => textWidth(label, px, weight)));
3157
- }
3158
- function shorten(line, width, px) {
3159
- if (textWidth(line, px) <= width) return line;
3160
- let cut = line;
3161
- while (cut.length > 1 && textWidth(`${cut}\u2026`, px) > width) cut = cut.slice(0, -1);
3162
- return `${cut.trimEnd()}\u2026`;
3163
- }
3164
- function fitLabel(label, width, px, maxLines = 2) {
3165
- if (textWidth(label, px) <= width) return { lines: [label], shortened: false };
3166
- const words = label.split(" ");
3167
- if (maxLines === 2 && words.length > 1) {
3168
- let best = null;
3169
- let bestWidth = Infinity;
3170
- for (let k = 1; k < words.length; k += 1) {
3171
- const first = words.slice(0, k).join(" ");
3172
- const second = words.slice(k).join(" ");
3173
- const widest = Math.max(textWidth(first, px), textWidth(second, px));
3174
- if (widest < bestWidth) {
3175
- bestWidth = widest;
3176
- best = [first, second];
3177
- }
3178
- }
3179
- if (best && bestWidth <= width) return { lines: best, shortened: false };
3180
- if (best) return { lines: [shorten(best[0], width, px), shorten(best[1], width, px)], shortened: true };
3181
- }
3182
- return { lines: [shorten(label, width, px)], shortened: true };
3183
- }
3184
- function niceStep(x) {
3185
- if (!(x > 0) || !Number.isFinite(x)) return 1;
3186
- const magnitude = 10 ** Math.floor(Math.log10(x));
3187
- return ([1, 2, 2.5, 5, 10].find((m) => m * magnitude >= x * (1 - 1e-12)) ?? 10) * magnitude;
3188
- }
3189
- function niceTicks(lo, hi, room, labelSize, most = 6) {
3190
- if (!Number.isFinite(lo) || !Number.isFinite(hi)) return [0];
3191
- if (hi < lo) [lo, hi] = [hi, lo];
3192
- if (hi === lo) hi = lo === 0 ? 1 : lo + Math.abs(lo);
3193
- const count = Math.max(2, Math.min(most, Math.floor(room / (labelSize + 16)) + 1));
3194
- const step = niceStep((hi - lo) / (count - 1));
3195
- const first = Math.floor(lo / step + 1e-9) * step;
3196
- const ticks = [];
3197
- for (let k = 0; first + k * step <= hi + step * 1e-9 || ticks.length < 2; k += 1) {
3198
- ticks.push(Number((first + k * step).toPrecision(12)));
3199
- if (ticks.length > 50) break;
3200
- }
3201
- if (ticks[ticks.length - 1] < hi - step * 1e-9) ticks.push(Number((ticks[ticks.length - 1] + step).toPrecision(12)));
3202
- return ticks;
3203
- }
3204
3136
  var PX = 11;
3205
3137
  var LINE = 13;
3206
3138
  var TONE = {
@@ -3489,7 +3421,7 @@ function panel(id, lane, provenance, content) {
3489
3421
  return /* @__PURE__ */ jsx("div", { className: "caos-cw-panel", "data-group": id, "data-lane": lane, "data-provenance": provenance, children: content });
3490
3422
  }
3491
3423
  function CaseWorkbench(props) {
3492
- const { caseId, cases, source, controls, replayOnly, variants, rail, railLabel, groups, compare, context: context2, group, onGroupChange, deepLinkView = true, ariaLabel, focus } = props;
3424
+ const { caseId, cases, source, controls, replayOnly, variants, rail, railLabel, groups, compare, context, group, onGroupChange, deepLinkView = true, ariaLabel, focus } = props;
3493
3425
  const lang = useShellLang();
3494
3426
  const focusCfg = focus === true ? {} : focus || null;
3495
3427
  const { show: showFocus, setFocus, enterRef, memory } = useFocusMode(focusCfg);
@@ -3509,8 +3441,8 @@ function CaseWorkbench(props) {
3509
3441
  }
3510
3442
  tabs.push({
3511
3443
  id: "context",
3512
- label: pick(context2.label ?? { en: "Context", es: "Contexto" }, lang),
3513
- content: /* @__PURE__ */ jsx("div", { className: "prose caos-cw-context", children: context2.content })
3444
+ label: pick(context.label ?? { en: "Context", es: "Contexto" }, lang),
3445
+ content: /* @__PURE__ */ jsx("div", { className: "prose caos-cw-context", children: context.content })
3514
3446
  });
3515
3447
  if (tabs.length > MAX_WORKBENCH_GROUPS) {
3516
3448
  throw new Error(
@@ -3778,4 +3710,4 @@ function ViewsRow({ children, shares }) {
3778
3710
  }) });
3779
3711
  }
3780
3712
 
3781
- export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CardDeck, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, DocPage, DocReferences, DocRefsScope, DocSection, EXECUTIVE_SCHEMA_VERSION, Equation, ExecutiveView, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, FocusShell, Gauge, Glossary, InlineMath, Knob, Landing, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Question, Readout, ReferenceList, Refs, Stat, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, deckPosition, deckWindow, firstAppearance, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, perPageFor, readCaseParam, removeQueryParam, replaceQueryParam, sourcesPresent, textWidth, useFocusMode, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, validateExecutive, viewParamOf, widestLabel, withCaseParam };
3713
+ export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CardDeck, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DECK_TEXT, DOC_AREAS, DocPage, DocReferences, DocRefsScope, DocSection, EXECUTIVE_SCHEMA_VERSION, Equation, ExecutiveView, FLIP_LIMITS, FLIP_TEXT, FOCUS_PARAM, FOCUS_TEXT, FOUR_CS, Figure, FlipCard, FocusShell, Gauge, Glossary, InlineMath, Knob, Landing, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Question, Readout, ReferenceList, Refs, Stat, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, deckPosition, deckWindow, firstAppearance, groupByCategory, isStale, makeStateKey, perPageFor, readCaseParam, removeQueryParam, replaceQueryParam, sourcesPresent, useFocusMode, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, validateExecutive, viewParamOf, withCaseParam };
@@ -58,4 +58,42 @@ declare function resolveToken(token: string, fallback?: string): string;
58
58
  */
59
59
  declare function useThemeTokens(): Record<ShellToken, string>;
60
60
 
61
- export { type FormatOptions as F, NBSP as N, type ShellColorToken as S, NOT_AVAILABLE as a, SCIENTIFIC_BELOW as b, SHELL_TOKENS as c, type ShellToken as d, formatTicks as e, formatNumber as f, useThemeTokens as g, resolveToken as r, useFormat as u };
61
+ /**
62
+ * Text that fits the drawing it labels, measured in the font the page renders.
63
+ *
64
+ * Products drew their axes and labels with fixed margins and a fixed width per character. The Linux fonts (DejaVu
65
+ * Sans) are wider than the Windows ones and Spanish labels are longer than English ones, so a column sized for one
66
+ * ran its labels out of the drawing in the other (CAOS_Fragmenta 0.05.000: "Tamaño medio clásico, limitado al bloque
67
+ * in situ" read "dio clásico, limitado al bloque in situ"; its deploy gate failed on Linux). Here a label is measured
68
+ * with the canvas text metrics of the page's own font (`--font-sans`, which the gate's wide-font pass replaces),
69
+ * broken over two lines at a word when it does not fit, and only then shortened with an ellipsis, so the drawing can
70
+ * give the whole label as its title. Ported from CAOS_Fragmenta `viz/text.ts` (0.07.000).
71
+ */
72
+
73
+ /** The name of a reference line that has no label, for the handle a reader drags (a screen reader says it): numbered when
74
+ * several unlabelled lines of one chart can be dragged, so each handle has a name of its own. */
75
+ declare function referenceLineName(lang: Lang, n: number, of: number): string;
76
+ /** The page's sans-serif stack as the shell token resolves it. */
77
+ declare function fontFamily(): string;
78
+ /** The width of one line of text at a font size, in CSS pixels; outside a browser, an estimate on the wide side. */
79
+ declare function textWidth(text: string, px: number, weight?: number): number;
80
+ /** The widest a set of labels needs, each on one line. */
81
+ declare function widestLabel(labels: readonly string[], px: number, weight?: number): number;
82
+ interface FittedLabel {
83
+ /** One or two lines; the last is shortened with an ellipsis only when the lines allowed are not enough. */
84
+ lines: string[];
85
+ /** Whether the label was shortened, so the drawing gives the whole label as its title. */
86
+ shortened: boolean;
87
+ }
88
+ /** A label in at most `maxLines` lines of `width` pixels: whole when it fits, broken at a word, shortened last. */
89
+ declare function fitLabel(label: string, width: number, px: number, maxLines?: 1 | 2): FittedLabel;
90
+ /** The 1, 2, 2.5 or 5 times a power of ten at or above `x`. */
91
+ declare function niceStep(x: number): number;
92
+ /**
93
+ * Round ticks covering [lo, hi], as many as leave room for labels about `labelSize` pixels along an axis of `room`
94
+ * pixels (at least two, at most `most`). The first tick is at or below `lo` and the last at or above `hi`, so a bar
95
+ * or a point never runs past the last labelled value.
96
+ */
97
+ declare function niceTicks(lo: number, hi: number, room: number, labelSize: number, most?: number): number[];
98
+
99
+ export { type FormatOptions as F, NBSP as N, type ShellColorToken as S, type FittedLabel as a, NOT_AVAILABLE as b, SCIENTIFIC_BELOW as c, SHELL_TOKENS as d, type ShellToken as e, fitLabel as f, fontFamily as g, formatNumber as h, formatTicks as i, niceTicks as j, resolveToken as k, useThemeTokens as l, niceStep as n, referenceLineName as r, textWidth as t, useFormat as u, widestLabel as w };
package/gate/inpage.mjs CHANGED
@@ -498,7 +498,7 @@ export function installLib() {
498
498
  // charts whose axis gives a tick the label of the tick before it (UPlotChart declares data-ticks-repeat)
499
499
  repeatedTicks: [...document.querySelectorAll('[data-ticks-repeat]')]
500
500
  .filter((el) => visible(el) && Number(el.getAttribute('data-ticks-repeat')) > 0)
501
- .map((el) => ({ label: describe(el.closest('[data-plot]') || el), n: Number(el.getAttribute('data-ticks-repeat')) })),
501
+ .map((el) => ({ label: describe(el.closest('[data-plot]') || el), n: Number(el.getAttribute('data-ticks-repeat')), names: el.getAttribute('data-ticks-repeated') || '' })),
502
502
  // the drawing a filling card keeps under its note (G6 stage floor): the body of every visible `PlotCard fill`
503
503
  fillStages: [...document.querySelectorAll('.caos-plot.fill')]
504
504
  .filter(visible)
package/gate/run.mjs CHANGED
@@ -481,7 +481,9 @@ export async function runGate(o) {
481
481
  if (d) s.walkState.doc.push(d);
482
482
  }
483
483
  for (const r of f.repeatedTicks ?? []) {
484
- fail('G6', where, `${r.label}: ${r.n} axis tick label(s) repeat the label before them (give the axis fixed decimals)`);
484
+ // named x ticks (UPlotChart declares the repeated names): a name used twice, wherever the two ticks sit on the axis
485
+ if (r.names) fail('G6', where, `${r.label}: ${r.n} named x tick label(s) repeat another tick's label (${r.names.split('|').map((n) => `"${n}"`).join(', ')}); give every named tick a label of its own`);
486
+ else fail('G6', where, `${r.label}: ${r.n} axis tick label(s) repeat the label before them (give the axis fixed decimals)`);
485
487
  }
486
488
  // a filling card whose note leaves its drawing under the floor (CAOS_APP_SHELL#95); below 1280 px the document scrolls
487
489
  if (big) for (const c of f.fillStages ?? []) if (c.h < opt.stageFloor) fail('G6', where, `the drawing of ${c.label} keeps ${Math.round(c.h)} px under its note (floor ${opt.stageFloor} px)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Shared web-app shell, workbench primitives, content primitives, design system and measured gate for the CAOS / Faena public apps (ADR-0016, ADR-0017, ADR-0071).",
5
5
  "license": "MIT",
6
6
  "author": "Felipe Santibáñez-Leal",
@@ -25,6 +25,7 @@
25
25
  "caos-card",
26
26
  "caos-chart",
27
27
  "caos-chart-key",
28
+ "caos-chart-key-button",
28
29
  "caos-chart-legend",
29
30
  "caos-chart-plot",
30
31
  "caos-chart-readout",
@@ -338,6 +339,7 @@
338
339
  "caos-card",
339
340
  "caos-chart",
340
341
  "caos-chart-key",
342
+ "caos-chart-key-button",
341
343
  "caos-chart-legend",
342
344
  "caos-chart-plot",
343
345
  "caos-chart-readout",