@fasl-work/caos-app-shell 0.17.0 → 0.19.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/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, barExtent, clipExtent, allIntegers, bandsOf, windowRange, windowZoomed, unusedLegendTones, NOT_AVAILABLE, formatNumber, widestLabel, fitLabel, GROUP_MARGIN, axisTitleFits, wrapLines, formatTicks, niceTicks, textWidth, repeatedLabels, chartSummary, NBSP, CHART_ROLE, ChartSummary, ChartDataTable, ResetZoomButton, memberSlots, wheelFactor, pinchState, DRAG_THRESHOLD, panRange, brushWindow, keyAction, KEY_ZOOM, stepIndex, KEY_PAN, stepWindow, roundWindow, zoomRange, categoryAt, memberAt } from './chunk-3LONJM5B.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-3LONJM5B.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 = {
@@ -3210,23 +3142,87 @@ var TONE = {
3210
3142
  accent: "var(--color-accent)",
3211
3143
  neutral: "var(--color-fg-faint)"
3212
3144
  };
3145
+ var REST = {
3146
+ en: "Point at a bar, or use the arrow keys, to read its value",
3147
+ es: "Apunte a una barra, o use las flechas, para leer su valor"
3148
+ };
3213
3149
  function Lines({ x, y, lines, anchor, fill }) {
3214
3150
  return /* @__PURE__ */ jsx("text", { x, y, textAnchor: anchor, fontSize: PX, fill, style: { fontFamily: "var(--font-sans)" }, children: lines.map((line, i) => /* @__PURE__ */ jsx("tspan", { x, dy: i === 0 ? 0 : LINE, children: line }, i)) });
3215
3151
  }
3216
- function BarChart({ title, data, axis, orientation = "horizontal", values = true, height, onSelect }) {
3152
+ function BarChart({ title, data, axis, orientation = "horizontal", values = true, height, onSelect, zoomOff, legend }) {
3217
3153
  const lang = useShellLang();
3218
- const [ref, box] = useStageSize();
3154
+ const [measureRef, box] = useStageSize();
3155
+ const innerRef = useRef(null);
3156
+ const setInner = useCallback(
3157
+ (el) => {
3158
+ innerRef.current = el;
3159
+ measureRef(el);
3160
+ },
3161
+ [measureRef]
3162
+ );
3219
3163
  const fill = height === "fill";
3220
3164
  const name = pick(title, lang);
3221
- const axisTitle = axis.unit ? `${pick(axis.label, lang)} (${pick(axis.unit, lang)})` : pick(axis.label, lang);
3165
+ const unit = axis.unit ? pick(axis.unit, lang) : "";
3166
+ const axisTitle = unit ? `${pick(axis.label, lang)} (${unit})` : pick(axis.label, lang);
3222
3167
  const fmt = axis.format ?? { digits: 3 };
3168
+ const n = data.length;
3223
3169
  const finite = data.map((d) => d.value).filter((v) => v !== null && v !== void 0 && Number.isFinite(v));
3224
- const lo = Math.min(0, axis.range ? axis.range[0] : Math.min(0, ...finite));
3225
- const hi = Math.max(0, axis.range ? axis.range[1] : Math.max(0, ...finite));
3170
+ const spanEnds = data.flatMap((d) => {
3171
+ const e = barExtent(d.value, d.span);
3172
+ return e && e.given ? [e.a, e.b] : [];
3173
+ });
3174
+ const floating = data.some((d) => d.span !== void 0 && d.span !== null);
3175
+ const reach = [...finite, ...spanEnds];
3176
+ const lo = floating && axis.range ? Math.min(axis.range[0], axis.range[1]) : Math.min(0, axis.range ? axis.range[0] : Math.min(0, ...reach));
3177
+ const hi = floating && axis.range ? Math.max(axis.range[0], axis.range[1]) : Math.max(0, axis.range ? axis.range[1] : Math.max(0, ...reach));
3178
+ const extents = data.map((d) => clipExtent(barExtent(d.value, d.span), lo, hi, floating));
3179
+ const anyDot = extents.some((e) => e !== null && e.dot);
3180
+ const countAxis = fmt.decimals === void 0 && reach.length > 0 && allIntegers(reach) && allIntegers(axis.range ?? []);
3181
+ const resolution = fmt.decimals !== void 0 ? 10 ** -fmt.decimals / (fmt.percent ? 100 : 1) : countAxis ? 1 : 0;
3226
3182
  const anyHighlight = data.some((d) => d.highlight);
3227
3183
  const colour = (d) => d.tone ? TONE[d.tone] : !anyHighlight || d.highlight ? "var(--color-accent)" : "var(--color-fg-faint)";
3228
- const shown = (d) => d.value === null || d.value === void 0 || !Number.isFinite(d.value) ? NOT_AVAILABLE[lang] : formatNumber(d.value, lang, fmt);
3184
+ const outline = (d) => d.tone && d.highlight ? { stroke: "var(--color-fg)", strokeWidth: 2 } : {};
3185
+ const fmtOf = (d) => d.format ?? fmt;
3186
+ const shown = (d) => d.value === null || d.value === void 0 || !Number.isFinite(d.value) ? NOT_AVAILABLE[lang] : formatNumber(d.value, lang, fmtOf(d));
3229
3187
  const width = box.width;
3188
+ const ids = useId();
3189
+ const tableId = `${ids}-table`;
3190
+ const summaryId = `${ids}-summary`;
3191
+ const [win, setWin] = useState(null);
3192
+ const [cursor, setCursor] = useState(null);
3193
+ const [brush, setBrush] = useState(null);
3194
+ const bands = useMemo(() => bandsOf(data.map((d) => d.group?.id)), [data]);
3195
+ const nb = bands.length;
3196
+ const grouped = data.some((d) => d.group !== void 0 && d.group !== null);
3197
+ const most = Math.max(1, ...bands.map((b) => b.to - b.from));
3198
+ const bandOf = (i) => bands.findIndex((b) => i >= b.from && i < b.to);
3199
+ const bandLabel = (b) => pick(b.group !== void 0 ? data[b.from].group.label : data[b.from].label, lang);
3200
+ const [s0, e0] = windowRange(nb, win);
3201
+ const zoomed = windowZoomed(nb, win);
3202
+ const zoomIsOff = zoomOff !== void 0 && zoomOff !== null;
3203
+ const zoomable = !zoomIsOff && nb >= 3;
3204
+ const zoomWhy = zoomIsOff ? pick(zoomOff, lang) : "";
3205
+ const kbRef = useRef(false);
3206
+ const geoRef = useRef(null);
3207
+ const live = useRef({ win, n: nb, zoomable, cursor });
3208
+ live.current = { win, n: nb, zoomable, cursor };
3209
+ const idKey = data.map((d) => d.group ? `${d.id}${d.group.id}` : d.id).join("");
3210
+ useEffect(() => {
3211
+ setWin(null);
3212
+ setCursor(null);
3213
+ setBrush(null);
3214
+ }, [idKey]);
3215
+ const unusedTones = legend ? unusedLegendTones(data.map((d) => d.tone ?? (anyHighlight && !d.highlight ? "neutral" : "accent")), legend) : [];
3216
+ const unusedKey = unusedTones.join(",");
3217
+ useEffect(() => {
3218
+ let dev = false;
3219
+ try {
3220
+ dev = process.env.NODE_ENV !== "production";
3221
+ } catch {
3222
+ dev = false;
3223
+ }
3224
+ if (dev && unusedKey) console.warn(`BarChart "${name}": the legend names the tone(s) ${unusedKey}, which no bar uses`);
3225
+ }, [unusedKey, name]);
3230
3226
  const select = (id) => onSelect?.(id);
3231
3227
  const keyed = (id) => (e) => {
3232
3228
  if (e.key === "Enter" || e.key === " ") {
@@ -3235,53 +3231,103 @@ function BarChart({ title, data, axis, orientation = "horizontal", values = true
3235
3231
  }
3236
3232
  };
3237
3233
  const interactive = (d) => onSelect ? { role: "button", tabIndex: 0, onClick: () => select(d.id), onKeyDown: keyed(d.id), style: { cursor: "pointer" } } : {};
3234
+ const hitArea = Boolean(onSelect) && (floating || grouped);
3238
3235
  let drawing = null;
3239
3236
  let drawnHeight = 0;
3240
- if (width > 0 && data.length > 0) {
3241
- const valueLabels = data.map(shown);
3237
+ let tickLabels = [];
3238
+ const valueLabels = data.map(shown);
3239
+ const endsText = data.map((d) => {
3240
+ const e = barExtent(d.value, d.span);
3241
+ if (!e || !e.given && !floating) return "";
3242
+ const [a, b] = [formatNumber(e.a, lang, fmtOf(d)), formatNumber(e.b, lang, fmtOf(d))];
3243
+ return lang === "es" ? `de ${a} a ${b}` : `from ${a} to ${b}`;
3244
+ });
3245
+ const memberName = (d, room) => {
3246
+ if (!d.group) return "";
3247
+ const full = pick(d.label, lang);
3248
+ for (let k = full.length; k >= 1; k--) {
3249
+ const t2 = full.slice(0, k).trimEnd();
3250
+ if (t2 && textWidth(t2, PX) <= room) return t2;
3251
+ }
3252
+ return "";
3253
+ };
3254
+ const fullName = (d) => d.group ? `${pick(d.group.label, lang)}, ${pick(d.label, lang)}` : pick(d.label, lang);
3255
+ if (width > 0 && n > 0) {
3256
+ const count = e0 - s0;
3242
3257
  const valueCol = values ? widestLabel(valueLabels, PX) + 8 : 0;
3258
+ const vis = bands.slice(s0, e0);
3259
+ const items = vis.flatMap((b, kb) => Array.from({ length: b.to - b.from }, (_, j) => ({ b, kb, j, i: b.from + j, c: b.to - b.from })));
3260
+ const slots = (c) => memberSlots(c, most);
3243
3261
  if (orientation === "horizontal") {
3244
- const labelCol = Math.min(widestLabel(data.map((d) => pick(d.label, lang)), PX), Math.max(80, 0.38 * width));
3245
- const fitted = data.map((d) => fitLabel(pick(d.label, lang), labelCol, PX, 2));
3246
- const need = Math.max(22, Math.max(...fitted.map((f) => f.lines.length)) * LINE + 8);
3247
- const axisH = 6 + LINE + 4 + LINE + 4;
3248
- const x0 = labelCol + 10;
3249
- const x1 = Math.max(x0 + 40, width - valueCol - 6);
3262
+ const bandTexts = bands.map(bandLabel);
3263
+ const labelCol = Math.min(widestLabel(bandTexts, PX), Math.max(80, 0.38 * width));
3264
+ const fittedAll = bandTexts.map((t2) => fitLabel(t2, labelCol, PX, 2));
3265
+ const fitted = fittedAll.slice(s0, e0);
3266
+ const need = Math.max(22, Math.max(...fittedAll.map((f) => f.lines.length)) * LINE + 8, grouped ? most * 18 / (1 - 2 * GROUP_MARGIN) : 0);
3267
+ const memberRoom = grouped ? Math.min(widestLabel(data.map((d) => d.group ? pick(d.label, lang) : ""), PX), Math.max(24, 0.2 * width)) : 0;
3268
+ const memberCol = memberRoom > 0 ? memberRoom + 8 : 0;
3269
+ const p0 = labelCol + 10 + memberCol;
3270
+ const p1 = Math.max(p0 + 40, width - valueCol - 6);
3271
+ const cx = p0 + (p1 - p0) / 2;
3272
+ const titleFits = axisTitleFits(axisTitle, 2 * Math.min(cx, width - cx) - 8, PX);
3273
+ const titleLines = titleFits ? [axisTitle] : wrapLines(axisTitle, width - 8, PX);
3274
+ const axisH = 6 + LINE + 4 + titleLines.length * LINE + 4;
3275
+ const rowFull = fill && box.height > 0 ? Math.max(need, (box.height - axisH - 4) / nb) : need;
3276
+ const rowH = nb * rowFull / count;
3277
+ const barH = grouped ? Math.min(rowH * slots(most).size * 0.8, 40) : Math.min(rowH * 0.5, 40);
3278
+ const inset = anyDot ? barH / 2 : 0;
3279
+ const x0 = p0 + inset;
3280
+ const x1 = Math.max(x0 + 40, p1 - inset);
3250
3281
  const tickProbe = widestLabel(formatTicks([lo, hi], lang, fmt), PX);
3251
- const ticks = niceTicks(lo, hi, x1 - x0, tickProbe);
3282
+ const ticks = niceTicks(lo, hi, x1 - x0, tickProbe, 6, resolution);
3252
3283
  const t0 = ticks[0];
3253
3284
  const t1 = ticks[ticks.length - 1];
3254
- const tickLabels = formatTicks(ticks, lang, fmt);
3285
+ tickLabels = formatTicks(ticks, lang, fmt);
3255
3286
  const sx = (v) => x0 + (v - t0) / (t1 - t0 || 1) * (x1 - x0);
3256
- const rowH = fill && box.height > 0 ? Math.max(need, (box.height - axisH - 4) / data.length) : need;
3257
3287
  const top = 4;
3258
- const plotBottom = top + data.length * rowH;
3288
+ const plotBottom = top + nb * rowFull;
3259
3289
  drawnHeight = fill && box.height > 0 ? Math.max(box.height, plotBottom + axisH) : plotBottom + axisH;
3260
- const barH = Math.min(rowH * 0.5, 40);
3261
- drawing = /* @__PURE__ */ jsxs("svg", { width, height: drawnHeight, role: "img", "aria-label": name, "data-chart": "", "data-chart-kind": "bars", children: [
3290
+ const slotH = grouped ? rowH * slots(1).size : rowH;
3291
+ const centre = (it) => top + it.kb * rowH + (grouped ? rowH * (slots(it.c).start + (it.j + 0.5) * slots(it.c).size) : rowH / 2);
3292
+ geoRef.current = { horizontal: true, start: top, end: plotBottom };
3293
+ const cb = cursor !== null ? bandOf(cursor) : -1;
3294
+ drawing = /* @__PURE__ */ jsxs("svg", { width, height: drawnHeight, role: "presentation", "data-chart": "", "data-chart-kind": "bars", children: [
3262
3295
  /* @__PURE__ */ jsx("title", { children: name }),
3263
3296
  ticks.map((t2, i) => /* @__PURE__ */ jsxs("g", { children: [
3264
3297
  /* @__PURE__ */ jsx("line", { x1: sx(t2), x2: sx(t2), y1: top, y2: plotBottom, stroke: "var(--color-border)", strokeWidth: t2 === 0 ? 1.4 : 1 }),
3265
3298
  /* @__PURE__ */ jsx("text", { x: sx(t2), y: plotBottom + 6 + LINE - 2, textAnchor: i === 0 && sx(t2) - textWidth(tickLabels[i], PX) / 2 < 0 ? "start" : i === ticks.length - 1 && sx(t2) + textWidth(tickLabels[i], PX) / 2 > width ? "end" : "middle", fontSize: PX, fill: "var(--color-fg-subtle)", style: { fontFamily: "var(--font-sans)" }, children: tickLabels[i] })
3266
3299
  ] }, `t${i}`)),
3267
- /* @__PURE__ */ jsx("text", { x: x0 + (x1 - x0) / 2, y: plotBottom + 6 + LINE + 4 + LINE - 2, textAnchor: "middle", fontSize: PX, fill: "var(--color-fg-subtle)", style: { fontFamily: "var(--font-sans)" }, children: axisTitle }),
3268
- data.map((d, i) => {
3269
- const cy = top + i * rowH + rowH / 2;
3270
- const f = fitted[i];
3300
+ /* @__PURE__ */ jsx(Lines, { x: titleFits ? cx : width / 2, y: plotBottom + 6 + LINE + 4 + LINE - 2, lines: titleLines, anchor: "middle", fill: "var(--color-fg-subtle)" }),
3301
+ cursor !== null && cb >= s0 && cb < e0 && (grouped ? /* @__PURE__ */ jsx("rect", { className: "caos-bars-cursor", "data-bar-cursor": "", x: 0, y: top + (cb - s0) * rowH + rowH * (slots(bands[cb].to - bands[cb].from).start + (cursor - bands[cb].from) * slots(1).size), width, height: rowH * slots(1).size }) : /* @__PURE__ */ jsx("rect", { className: "caos-bars-cursor", "data-bar-cursor": "", x: 0, y: top + (cursor - s0) * rowH, width, height: rowH })),
3302
+ vis.map(
3303
+ (b, kb) => b.group !== void 0 ? /* @__PURE__ */ jsxs("g", { "data-group": b.group, children: [
3304
+ /* @__PURE__ */ jsx("title", { children: bandTexts[s0 + kb] }),
3305
+ /* @__PURE__ */ jsx(Lines, { x: labelCol + 2, y: top + kb * rowH + rowH / 2 - (fitted[kb].lines.length - 1) * LINE / 2 + 4, lines: fitted[kb].lines, anchor: "end", fill: "var(--color-fg)" })
3306
+ ] }, `grp${s0 + kb}`) : null
3307
+ ),
3308
+ items.map((it) => {
3309
+ const { i, kb } = it;
3310
+ const d = data[i];
3311
+ const cy = centre(it);
3312
+ const f = fitted[kb];
3271
3313
  const v = d.value;
3272
3314
  const ok = v !== null && v !== void 0 && Number.isFinite(v);
3273
- const xa = sx(0);
3274
- const xb = ok ? sx(v) : xa;
3315
+ const ex = extents[i];
3316
+ const xa = ex ? sx(ex.a) : sx(0);
3317
+ const xb = ex ? sx(ex.b) : xa;
3275
3318
  const labelText = valueLabels[i];
3276
- const negative = ok && v < 0;
3277
- return /* @__PURE__ */ jsxs("g", { "data-bar": d.id, ...interactive(d), children: [
3278
- /* @__PURE__ */ jsx("title", { children: [pick(d.label, lang), labelText, pick(d.hint, lang)].filter(Boolean).join(": ") }),
3279
- /* @__PURE__ */ jsx(Lines, { x: x0 - 8, y: cy - (f.lines.length - 1) * LINE / 2 + 4, lines: f.lines, anchor: "end", fill: d.highlight ? "var(--color-fg)" : "var(--color-fg-subtle)" }),
3280
- ok && /* @__PURE__ */ jsx("rect", { x: Math.min(xa, xb), y: cy - barH / 2, width: Math.max(1, Math.abs(xb - xa)), height: barH, rx: 2, fill: colour(d) }),
3319
+ const negative = ok && !ex?.given && v < 0;
3320
+ const far = Math.max(xa, xb);
3321
+ const member = memberName(d, memberRoom);
3322
+ return /* @__PURE__ */ jsxs("g", { "data-bar": d.id, "data-cursor": cursor === i ? "1" : void 0, ...interactive(d), children: [
3323
+ /* @__PURE__ */ jsx("title", { children: [fullName(d), labelText, pick(d.hint, lang), endsText[i]].filter(Boolean).join(": ") }),
3324
+ !d.group && /* @__PURE__ */ jsx(Lines, { x: labelCol + 2, y: cy - (f.lines.length - 1) * LINE / 2 + 4, lines: f.lines, anchor: "end", fill: d.highlight ? "var(--color-fg)" : "var(--color-fg-subtle)" }),
3325
+ ex && ex.dot && /* @__PURE__ */ jsx("circle", { cx: xa, cy, r: barH / 2, fill: colour(d), ...outline(d) }),
3326
+ ex && !ex.dot && /* @__PURE__ */ jsx("rect", { x: Math.min(xa, xb), y: cy - barH / 2, width: Math.max(1, Math.abs(xb - xa)), height: barH, rx: 2, fill: colour(d), ...outline(d) }),
3281
3327
  values && /* @__PURE__ */ jsx(
3282
3328
  "text",
3283
3329
  {
3284
- x: ok ? negative ? Math.max(x0 + textWidth(labelText, PX) + 2, xb - 4) : xb + 4 : xa + 4,
3330
+ x: ok ? negative ? Math.max(x0 + textWidth(labelText, PX) + 2, xb - 4) : (ex?.given ? far : xb) + 4 + (ex?.dot ? barH / 2 : 0) : xa + 4,
3285
3331
  y: cy + 4,
3286
3332
  textAnchor: negative ? "end" : "start",
3287
3333
  fontSize: PX,
@@ -3290,63 +3336,382 @@ function BarChart({ title, data, axis, orientation = "horizontal", values = true
3290
3336
  style: { fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums" },
3291
3337
  children: labelText
3292
3338
  }
3293
- )
3339
+ ),
3340
+ member && /* @__PURE__ */ jsx("text", { "data-member": "", x: p0 - 8, y: cy + 4, textAnchor: "end", fontSize: PX, fill: "var(--color-fg-subtle)", style: { fontFamily: "var(--font-sans)" }, children: member }),
3341
+ hitArea && /* @__PURE__ */ jsx("rect", { "data-bar-hit": "", x: 0, y: cy - slotH / 2, width, height: slotH, fill: "transparent" })
3294
3342
  ] }, d.id);
3295
- })
3343
+ }),
3344
+ brush && /* @__PURE__ */ jsx("rect", { className: "caos-bars-brush", "data-bar-brush": "", x: 0, y: Math.min(brush.a, brush.b), width, height: Math.abs(brush.b - brush.a) })
3296
3345
  ] });
3297
3346
  } else {
3298
3347
  const h = fill && box.height > 0 ? box.height : typeof height === "number" ? height : 240;
3299
- const titleRow = LINE + 6;
3348
+ const titleLines = wrapLines(axisTitle, width - 4, PX);
3349
+ const titleRow = titleLines.length * LINE + 6;
3300
3350
  const valueRow = values ? LINE + 4 : 4;
3301
3351
  const probe = widestLabel(formatTicks([lo, hi], lang, fmt), PX);
3302
3352
  const left = probe + 12;
3303
3353
  const right = 8;
3304
- const band = (width - left - right) / data.length;
3305
- const fitted = data.map((d) => fitLabel(pick(d.label, lang), Math.max(24, band - 6), PX, 2));
3306
- const bottom = Math.max(...fitted.map((f) => f.lines.length)) * LINE + 10;
3307
- const y0 = titleRow + valueRow;
3308
- const y1 = Math.max(y0 + 40, h - bottom);
3309
- const ticks = niceTicks(lo, hi, y1 - y0, LINE);
3354
+ const band = Math.max(0, (width - left - right) / count);
3355
+ const fitted = vis.map((b) => fitLabel(bandLabel(b), Math.max(24, band - 6), PX, 2));
3356
+ const slotW = band * slots(most).size;
3357
+ const barW = grouped ? Math.max(0, Math.min(slotW * 0.84, 56)) : Math.max(0, Math.min(band * 0.62, 56));
3358
+ const memberRow = grouped && data.some((d) => memberName(d, slotW - 2) !== "") ? LINE + 3 : 0;
3359
+ const bottom = Math.max(...fitted.map((f) => f.lines.length)) * LINE + 10 + memberRow;
3360
+ const inset = anyDot ? barW / 2 : 0;
3361
+ const y0 = titleRow + valueRow + inset;
3362
+ const y1 = Math.max(y0 + 40, h - bottom - inset);
3363
+ const yl = y1 + inset;
3364
+ const ticks = niceTicks(lo, hi, y1 - y0, LINE, 6, resolution);
3310
3365
  const t0 = ticks[0];
3311
3366
  const t1 = ticks[ticks.length - 1];
3312
- const tickLabels = formatTicks(ticks, lang, fmt);
3367
+ tickLabels = formatTicks(ticks, lang, fmt);
3313
3368
  const sy = (v) => y1 - (v - t0) / (t1 - t0 || 1) * (y1 - y0);
3314
- const barW = Math.min(band * 0.62, 56);
3369
+ const crowded = grouped && values && widestLabel(valueLabels, PX) > slotW - 2;
3370
+ const centre = (it) => left + band * it.kb + (grouped ? band * (slots(it.c).start + (it.j + 0.5) * slots(it.c).size) : band / 2);
3315
3371
  drawnHeight = h;
3316
- drawing = /* @__PURE__ */ jsxs("svg", { width, height: h, role: "img", "aria-label": name, "data-chart": "", "data-chart-kind": "bars", children: [
3372
+ geoRef.current = { horizontal: false, start: left, end: width - right };
3373
+ const cb = cursor !== null ? bandOf(cursor) : -1;
3374
+ drawing = /* @__PURE__ */ jsxs("svg", { width, height: h, role: "presentation", "data-chart": "", "data-chart-kind": "bars", children: [
3317
3375
  /* @__PURE__ */ jsx("title", { children: name }),
3318
- /* @__PURE__ */ jsx("text", { x: 0, y: LINE - 2, textAnchor: "start", fontSize: PX, fill: "var(--color-fg-subtle)", style: { fontFamily: "var(--font-sans)" }, children: axisTitle }),
3376
+ /* @__PURE__ */ jsx(Lines, { x: 0, y: LINE - 2, lines: titleLines, anchor: "start", fill: "var(--color-fg-subtle)" }),
3319
3377
  ticks.map((t2, i) => /* @__PURE__ */ jsxs("g", { children: [
3320
3378
  /* @__PURE__ */ jsx("line", { x1: left, x2: width - right, y1: sy(t2), y2: sy(t2), stroke: "var(--color-border)", strokeWidth: t2 === 0 ? 1.4 : 1 }),
3321
3379
  /* @__PURE__ */ jsx("text", { x: left - 6, y: sy(t2) + 4, textAnchor: "end", fontSize: PX, fill: "var(--color-fg-subtle)", style: { fontFamily: "var(--font-sans)" }, children: tickLabels[i] })
3322
3380
  ] }, `t${i}`)),
3323
- data.map((d, i) => {
3324
- const cx = left + band * i + band / 2;
3381
+ cursor !== null && cb >= s0 && cb < e0 && (grouped ? /* @__PURE__ */ jsx("rect", { className: "caos-bars-cursor", "data-bar-cursor": "", x: left + band * (cb - s0) + band * (slots(bands[cb].to - bands[cb].from).start + (cursor - bands[cb].from) * slots(1).size), y: y0, width: band * slots(1).size, height: h - y0 }) : /* @__PURE__ */ jsx("rect", { className: "caos-bars-cursor", "data-bar-cursor": "", x: left + band * (cursor - s0), y: y0, width: band, height: h - y0 })),
3382
+ vis.map(
3383
+ (b, kb) => b.group !== void 0 ? /* @__PURE__ */ jsxs("g", { "data-group": b.group, children: [
3384
+ /* @__PURE__ */ jsx("title", { children: bandLabel(b) }),
3385
+ /* @__PURE__ */ jsx(Lines, { x: left + band * kb + band / 2, y: yl + LINE + 2 + memberRow, lines: fitted[kb].lines, anchor: "middle", fill: "var(--color-fg)" })
3386
+ ] }, `grp${s0 + kb}`) : null
3387
+ ),
3388
+ items.map((it) => {
3389
+ const { i, kb } = it;
3390
+ const d = data[i];
3391
+ const cx = centre(it);
3325
3392
  const v = d.value;
3326
3393
  const ok = v !== null && v !== void 0 && Number.isFinite(v);
3327
- const ya = sy(0);
3328
- const yb = ok ? sy(v) : ya;
3329
- const f = fitted[i];
3330
- return /* @__PURE__ */ jsxs("g", { "data-bar": d.id, ...interactive(d), children: [
3331
- /* @__PURE__ */ jsx("title", { children: [pick(d.label, lang), valueLabels[i], pick(d.hint, lang)].filter(Boolean).join(": ") }),
3332
- ok && /* @__PURE__ */ jsx("rect", { x: cx - barW / 2, y: Math.min(ya, yb), width: barW, height: Math.max(1, Math.abs(yb - ya)), rx: 2, fill: colour(d) }),
3333
- values && /* @__PURE__ */ jsx("text", { x: cx, y: Math.min(ya, yb) - 5, textAnchor: "middle", fontSize: PX, fill: ok ? "var(--color-fg)" : "var(--color-fg-faint)", fontStyle: ok ? void 0 : "italic", style: { fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums" }, children: valueLabels[i] }),
3334
- /* @__PURE__ */ jsx(Lines, { x: cx, y: y1 + LINE + 2, lines: f.lines, anchor: "middle", fill: d.highlight ? "var(--color-fg)" : "var(--color-fg-subtle)" })
3394
+ const ex = extents[i];
3395
+ const ya = ex ? sy(ex.a) : sy(0);
3396
+ const yb = ex ? sy(ex.b) : ya;
3397
+ const f = fitted[kb];
3398
+ const member = memberName(d, slotW - 2);
3399
+ return /* @__PURE__ */ jsxs("g", { "data-bar": d.id, "data-cursor": cursor === i ? "1" : void 0, ...interactive(d), children: [
3400
+ /* @__PURE__ */ jsx("title", { children: [fullName(d), valueLabels[i], pick(d.hint, lang), endsText[i]].filter(Boolean).join(": ") }),
3401
+ ex && ex.dot && /* @__PURE__ */ jsx("circle", { cx, cy: ya, r: barW / 2, fill: colour(d), ...outline(d) }),
3402
+ ex && !ex.dot && /* @__PURE__ */ jsx("rect", { x: cx - barW / 2, y: Math.min(ya, yb), width: barW, height: Math.max(1, Math.abs(yb - ya)), rx: 2, fill: colour(d), ...outline(d) }),
3403
+ values && /* @__PURE__ */ jsx("text", { x: cx, y: Math.min(ya, yb) - 5 - (ex?.dot ? barW / 2 : 0) - (crowded && it.j % 2 === 1 ? LINE : 0), textAnchor: "middle", fontSize: PX, fill: ok ? "var(--color-fg)" : "var(--color-fg-faint)", fontStyle: ok ? void 0 : "italic", style: { fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums" }, children: valueLabels[i] }),
3404
+ !d.group && /* @__PURE__ */ jsx(Lines, { x: cx, y: yl + LINE + 2, lines: f.lines, anchor: "middle", fill: d.highlight ? "var(--color-fg)" : "var(--color-fg-subtle)" }),
3405
+ member && /* @__PURE__ */ jsx("text", { "data-member": "", x: cx, y: yl + LINE + 2, textAnchor: "middle", fontSize: PX, fill: "var(--color-fg-subtle)", style: { fontFamily: "var(--font-sans)" }, children: member }),
3406
+ hitArea && /* @__PURE__ */ jsx("rect", { "data-bar-hit": "", x: cx - (grouped ? slotW : band) / 2, y: 0, width: grouped ? slotW : band, height: h, fill: "transparent" })
3335
3407
  ] }, d.id);
3336
- })
3408
+ }),
3409
+ brush && /* @__PURE__ */ jsx("rect", { className: "caos-bars-brush", "data-bar-brush": "", x: Math.min(brush.a, brush.b), y: y0, width: Math.abs(brush.b - brush.a), height: h - y0 })
3337
3410
  ] });
3338
3411
  }
3339
3412
  }
3340
- return /* @__PURE__ */ jsx(
3413
+ const drawn = width > 0 && n > 0;
3414
+ const repeats = repeatedLabels(tickLabels);
3415
+ const a11y = useMemo(() => {
3416
+ if (!drawn) return null;
3417
+ const head = [
3418
+ ...grouped ? [lang === "es" ? "Grupo" : "Group"] : [],
3419
+ lang === "es" ? "Categor\xEDa" : "Category",
3420
+ unit ? `${pick(axis.label, lang)} (${unit})` : pick(axis.label, lang),
3421
+ ...floating ? lang === "es" ? ["Desde", "Hasta"] : ["From", "To"] : [],
3422
+ lang === "es" ? "Nota" : "Note"
3423
+ ];
3424
+ const rows = data.map((d, i) => {
3425
+ const e = barExtent(d.value, d.span);
3426
+ const end = (v) => e && v !== void 0 ? formatNumber(v, lang, fmtOf(d)) : "";
3427
+ return [...grouped ? [d.group ? pick(d.group.label, lang) : ""] : [], pick(d.label, lang), valueLabels[i], ...floating ? [end(e?.a), end(e?.b)] : [], pick(d.hint, lang)];
3428
+ });
3429
+ let low = -1;
3430
+ let high = -1;
3431
+ data.forEach((d, i) => {
3432
+ if (d.value === null || d.value === void 0 || !Number.isFinite(d.value)) return;
3433
+ if (low < 0 || d.value < data[low].value) low = i;
3434
+ if (high < 0 || d.value > data[high].value) high = i;
3435
+ });
3436
+ const withUnit = (i) => `${valueLabels[i]}${unit ? `${NBSP}${unit}` : ""}`;
3437
+ const drawnExtents = data.map((d) => barExtent(d.value, d.span));
3438
+ const firstAt = drawnExtents.findIndex((e) => e !== null);
3439
+ const lastAt = drawnExtents.length - 1 - [...drawnExtents].reverse().findIndex((e) => e !== null);
3440
+ const endOf = (i, v) => ({ text: `${formatNumber(v, lang, fmtOf(data[i]))}${unit ? `${NBSP}${unit}` : ""}`, at: fullName(data[i]) });
3441
+ const ends = floating && firstAt >= 0 ? { first: endOf(firstAt, drawnExtents[firstAt].a), last: endOf(lastAt, drawnExtents[lastAt].b) } : {};
3442
+ const summary = chartSummary(lang, {
3443
+ ...ends,
3444
+ kind: "bars",
3445
+ name,
3446
+ count: n,
3447
+ yLabel: axisTitle,
3448
+ low: low >= 0 ? { text: withUnit(low), at: fullName(data[low]) } : void 0,
3449
+ high: high >= 0 ? { text: withUnit(high), at: fullName(data[high]) } : void 0
3450
+ });
3451
+ return { head, rows, summary };
3452
+ }, [drawn, data, lang, unit, name, axisTitle, axis.label, floating, grouped]);
3453
+ const rest = lang === "es" ? REST.es : REST.en;
3454
+ const read = cursor !== null && cursor < n ? [`${fullName(data[cursor])}: ${valueLabels[cursor]}${data[cursor].value === null || data[cursor].value === void 0 || !unit ? "" : `${NBSP}${unit}`}`, pick(data[cursor].hint, lang), endsText[cursor]].filter(Boolean).join(" \xB7 ") : rest;
3455
+ const along = (e) => {
3456
+ const g = geoRef.current;
3457
+ const svg = innerRef.current?.querySelector("svg");
3458
+ if (!g || !svg) return null;
3459
+ const r = svg.getBoundingClientRect();
3460
+ return g.horizontal ? e.clientY - r.top : e.clientX - r.left;
3461
+ };
3462
+ const setWindow = (r) => {
3463
+ if (!r || r[0] <= 0 && r[1] >= nb) setWin(null);
3464
+ else setWin(roundWindow(r, nb));
3465
+ };
3466
+ const fine = useRef(null);
3467
+ const zoomAround = (factor, anchorBar, discrete = false) => {
3468
+ const cur = windowRange(nb, live.current.win);
3469
+ if (discrete) {
3470
+ fine.current = null;
3471
+ const next2 = stepWindow(cur, nb, anchorBar, factor);
3472
+ setWindow(next2);
3473
+ return next2[0] !== cur[0] || next2[1] !== cur[1];
3474
+ }
3475
+ const held = fine.current;
3476
+ const base = held && held[0] > cur[0] - 1 && held[1] < cur[1] + 1 && roundWindow(held, nb)[0] === cur[0] && roundWindow(held, nb)[1] === cur[1] ? held : cur;
3477
+ const next = zoomRange(base, [0, nb], anchorBar, factor, { minSpan: Math.min(2, nb) });
3478
+ fine.current = next;
3479
+ setWindow(next);
3480
+ return Math.abs(next[0] - base[0]) > 1e-9 || Math.abs(next[1] - base[1]) > 1e-9;
3481
+ };
3482
+ const barAt = (pos) => {
3483
+ const g = geoRef.current;
3484
+ if (pos === null || !g) return null;
3485
+ const win2 = windowRange(nb, live.current.win);
3486
+ const bi = categoryAt(pos, g.start, g.end, win2, nb);
3487
+ if (bi === null) return null;
3488
+ const b = bands[bi];
3489
+ if (b.to - b.from === 1) return b.from;
3490
+ const scaled = (pos - g.start) / (g.end - g.start || 1) * (win2[1] - win2[0]);
3491
+ return b.from + memberAt(scaled - Math.floor(scaled), b.to - b.from, most);
3492
+ };
3493
+ const barFloat = (pos) => {
3494
+ const g = geoRef.current;
3495
+ const [s, e] = windowRange(nb, live.current.win);
3496
+ return g ? s + (pos - g.start) / (g.end - g.start || 1) * (e - s) : s;
3497
+ };
3498
+ const wheelOn = drawn && zoomable;
3499
+ useEffect(() => {
3500
+ const el = innerRef.current;
3501
+ if (!el || !wheelOn) return;
3502
+ const onWheel = (e) => {
3503
+ if (!live.current.zoomable) return;
3504
+ if (!el.contains(document.activeElement)) return;
3505
+ const pos = along(e);
3506
+ if (pos === null) return;
3507
+ if (zoomAround(wheelFactor(e.deltaY, e.deltaMode), barFloat(pos))) e.preventDefault();
3508
+ };
3509
+ el.addEventListener("wheel", onWheel, { passive: false });
3510
+ return () => el.removeEventListener("wheel", onWheel);
3511
+ }, [wheelOn, nb]);
3512
+ const pointers = useRef(/* @__PURE__ */ new Map());
3513
+ const gesture = useRef({ kind: null, id: -1, start: 0, last: 0, moved: false, f: [0, 0], dist: 0 });
3514
+ const onPointerDown = (e) => {
3515
+ e.currentTarget.focus({ preventScroll: true });
3516
+ kbRef.current = false;
3517
+ if (!live.current.zoomable) return;
3518
+ pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
3519
+ const pos = along(e);
3520
+ const g = gesture.current;
3521
+ if (e.pointerType === "mouse") {
3522
+ if (e.button !== 0 || pos === null) return;
3523
+ Object.assign(g, { kind: e.shiftKey ? "pan" : "brush", id: e.pointerId, start: pos, last: pos, moved: false, f: windowRange(nb, live.current.win) });
3524
+ return;
3525
+ }
3526
+ if (pointers.current.size === 2) {
3527
+ const [a, b] = [...pointers.current.values()];
3528
+ Object.assign(g, { kind: "pinch", dist: pinchState(a, b).dist });
3529
+ } else if (pointers.current.size === 1 && windowZoomed(nb, live.current.win) && pos !== null) {
3530
+ Object.assign(g, { kind: "pan", id: e.pointerId, start: pos, last: pos, moved: false, f: windowRange(nb, live.current.win) });
3531
+ }
3532
+ };
3533
+ const onPointerMove = (e) => {
3534
+ const g = gesture.current;
3535
+ const pos = along(e);
3536
+ if (pointers.current.has(e.pointerId)) pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
3537
+ if (g.kind === "pinch" && pointers.current.size === 2) {
3538
+ const [a, b] = [...pointers.current.values()];
3539
+ const now = pinchState(a, b);
3540
+ const mid = along({ clientX: now.mx, clientY: now.my });
3541
+ if (g.dist > 0 && now.dist > 0 && mid !== null) {
3542
+ zoomAround(g.dist / now.dist, barFloat(mid));
3543
+ g.dist = now.dist;
3544
+ }
3545
+ return;
3546
+ }
3547
+ if (g.kind && g.id === e.pointerId && pos !== null) {
3548
+ const gap = geoRef.current ? geoRef.current.end - geoRef.current.start : 1;
3549
+ if (!g.moved && Math.abs(pos - g.start) < DRAG_THRESHOLD) return;
3550
+ if (!g.moved) {
3551
+ g.moved = true;
3552
+ try {
3553
+ e.currentTarget.setPointerCapture(e.pointerId);
3554
+ } catch {
3555
+ }
3556
+ }
3557
+ if (g.kind === "brush") setBrush({ a: g.start, b: pos });
3558
+ else {
3559
+ const [s, en] = windowRange(nb, live.current.win);
3560
+ const bars = (g.last - pos) / (gap || 1) * (en - s);
3561
+ g.last = pos;
3562
+ g.f = panRange(g.f, [0, nb], bars / (g.f[1] - g.f[0] || 1));
3563
+ setWindow(g.f);
3564
+ }
3565
+ return;
3566
+ }
3567
+ if (e.pointerType === "mouse" || e.pointerType === "pen") {
3568
+ const bar = barAt(pos);
3569
+ kbRef.current = false;
3570
+ setCursor(bar);
3571
+ }
3572
+ };
3573
+ const endPointer = (e) => {
3574
+ const g = gesture.current;
3575
+ pointers.current.delete(e.pointerId);
3576
+ if (e.pointerType === "touch" && e.type === "pointerup" && !g.moved && g.kind !== "pinch") {
3577
+ const bar = barAt(along(e));
3578
+ if (bar !== null) {
3579
+ kbRef.current = false;
3580
+ setCursor(bar);
3581
+ }
3582
+ }
3583
+ if (g.kind === "brush" && g.id === e.pointerId && g.moved) {
3584
+ const geo = geoRef.current;
3585
+ const pos = along(e);
3586
+ if (geo && pos !== null) {
3587
+ const w = brushWindow(g.start, pos, geo.start, geo.end, windowRange(nb, live.current.win), nb);
3588
+ if (w) setWindow(w);
3589
+ }
3590
+ }
3591
+ if (e.currentTarget.hasPointerCapture?.(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
3592
+ setBrush(null);
3593
+ if (g.kind !== "pinch" || pointers.current.size < 2) gesture.current = { ...g, kind: null, moved: false };
3594
+ };
3595
+ const onPointerLeave = (e) => {
3596
+ if (e.pointerType !== "touch" && !kbRef.current) setCursor(null);
3597
+ };
3598
+ const onKeyDown = (e) => {
3599
+ const onBar = e.target !== e.currentTarget && e.target instanceof Element && e.target.closest("[data-bar]") !== null;
3600
+ if (e.target !== e.currentTarget && !onBar || nb === 0) return;
3601
+ const act = keyAction(e.key, { shift: e.shiftKey, ctrl: e.ctrlKey, meta: e.metaKey, alt: e.altKey });
3602
+ if (!act || onBar && act === "activate") return;
3603
+ const [s, en] = windowRange(nb, win);
3604
+ const stand = cursor;
3605
+ switch (act) {
3606
+ case "prev":
3607
+ case "next":
3608
+ case "first":
3609
+ case "last": {
3610
+ const standBand = stand !== null ? bandOf(stand) : -1;
3611
+ const from = stand !== null && standBand >= s && standBand < en ? stand : null;
3612
+ let idx;
3613
+ if (from === null && (act === "next" || act === "prev") && windowZoomed(nb, win)) idx = act === "next" ? bands[s].from : bands[en - 1].to - 1;
3614
+ else idx = stepIndex(from, act, n);
3615
+ if (idx === null) return;
3616
+ kbRef.current = true;
3617
+ setCursor(idx);
3618
+ const into = bandOf(idx);
3619
+ if (into < s) setWindow([into, into + (en - s)]);
3620
+ else if (into >= en) setWindow([into + 1 - (en - s), into + 1]);
3621
+ break;
3622
+ }
3623
+ case "zoomIn":
3624
+ case "zoomOut":
3625
+ if (!zoomable) return;
3626
+ zoomAround(act === "zoomIn" ? KEY_ZOOM : 1 / KEY_ZOOM, stand !== null ? bandOf(stand) + 0.5 : (s + en) / 2, true);
3627
+ break;
3628
+ case "panPrev":
3629
+ case "panNext": {
3630
+ if (!zoomable || !windowZoomed(nb, win)) return;
3631
+ const bars = Math.max(1, Math.round((en - s) * KEY_PAN));
3632
+ setWindow(panRange([s, en], [0, nb], (act === "panNext" ? 1 : -1) * bars / (en - s)));
3633
+ break;
3634
+ }
3635
+ case "reset":
3636
+ if (e.key === "Escape" && !windowZoomed(nb, win)) return;
3637
+ setWin(null);
3638
+ e.stopPropagation();
3639
+ break;
3640
+ case "activate":
3641
+ if (stand === null || !onSelect) return;
3642
+ select(data[stand].id);
3643
+ break;
3644
+ }
3645
+ e.preventDefault();
3646
+ };
3647
+ const onBlur = (e) => {
3648
+ if (e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)) return;
3649
+ if (kbRef.current) {
3650
+ kbRef.current = false;
3651
+ setCursor(null);
3652
+ }
3653
+ };
3654
+ const resetZoom = () => {
3655
+ setWin(null);
3656
+ innerRef.current?.focus({ preventScroll: true });
3657
+ };
3658
+ return /* @__PURE__ */ jsxs(
3341
3659
  "div",
3342
3660
  {
3343
- ref,
3344
- className: fill ? "caos-bars fill" : "caos-bars",
3345
- style: !fill && orientation === "vertical" ? { height: typeof height === "number" ? height : 240 } : void 0,
3346
- "data-drawn": width > 0 ? "1" : "0",
3347
- "data-width": width,
3348
- "data-height": drawnHeight,
3349
- children: drawing
3661
+ className: fill ? "caos-bars-host fill" : "caos-bars-host",
3662
+ "data-chart-host": drawn ? "bars" : void 0,
3663
+ "data-points": drawn ? String(n) : void 0,
3664
+ "data-groups": drawn && grouped ? String(nb) : void 0,
3665
+ "data-legend": drawn && legend && legend.length > 0 ? String(legend.length) : void 0,
3666
+ "data-spans": drawn && floating ? String(data.filter((d) => d.span !== void 0 && d.span !== null).length) : void 0,
3667
+ "data-zoomable": drawn ? zoomable ? "1" : "0" : void 0,
3668
+ "data-zoomed": drawn && zoomed ? "1" : void 0,
3669
+ "data-zoom-off": drawn && zoomIsOff ? zoomWhy : void 0,
3670
+ "data-window": drawn ? `${s0},${e0}` : void 0,
3671
+ "data-orientation": drawn ? orientation : void 0,
3672
+ children: [
3673
+ /* @__PURE__ */ jsx(
3674
+ "div",
3675
+ {
3676
+ ref: setInner,
3677
+ className: fill ? "caos-bars fill" : "caos-bars",
3678
+ style: !fill && orientation === "vertical" ? { height: typeof height === "number" ? height : 240 } : void 0,
3679
+ "data-drawn": width > 0 ? "1" : "0",
3680
+ "data-width": width,
3681
+ "data-height": drawnHeight,
3682
+ "data-ticks-repeat": drawn ? String(repeats) : void 0,
3683
+ ...drawn ? {
3684
+ tabIndex: 0,
3685
+ role: "group",
3686
+ "aria-roledescription": CHART_ROLE[lang],
3687
+ "aria-label": name,
3688
+ "aria-describedby": summaryId,
3689
+ "aria-details": tableId,
3690
+ "data-chart-focus": "",
3691
+ "data-zoomed": zoomed ? "1" : void 0,
3692
+ onKeyDown,
3693
+ onBlur,
3694
+ onPointerDown,
3695
+ onPointerMove,
3696
+ onPointerUp: endPointer,
3697
+ onPointerCancel: endPointer,
3698
+ onPointerLeave,
3699
+ onDoubleClick: () => setWin(null)
3700
+ } : {},
3701
+ children: drawing
3702
+ }
3703
+ ),
3704
+ drawn && legend && legend.length > 0 && /* @__PURE__ */ jsx("ul", { className: "caos-bars-legend", "aria-label": lang === "es" ? "Leyenda" : "Legend", children: legend.map((l, k) => /* @__PURE__ */ jsxs("li", { className: "caos-bars-key", "data-tone": l.tone, children: [
3705
+ /* @__PURE__ */ jsx("span", { className: "caos-bars-swatch", style: { "--swatch": TONE[l.tone] }, "aria-hidden": "true" }),
3706
+ pick(l.label, lang)
3707
+ ] }, k)) }),
3708
+ drawn && a11y && /* @__PURE__ */ jsx(ChartSummary, { id: summaryId, text: a11y.summary }),
3709
+ drawn && a11y && /* @__PURE__ */ jsx(ChartDataTable, { id: tableId, caption: name, head: a11y.head, rows: a11y.rows }),
3710
+ drawn && /* @__PURE__ */ jsxs("div", { className: "caos-bars-foot", children: [
3711
+ /* @__PURE__ */ jsx("p", { className: "caos-bars-readout", "aria-live": "polite", title: read, children: read }),
3712
+ zoomed && zoomable && /* @__PURE__ */ jsx(ResetZoomButton, { lang, onReset: resetZoom })
3713
+ ] })
3714
+ ]
3350
3715
  }
3351
3716
  );
3352
3717
  }
@@ -3489,7 +3854,7 @@ function panel(id, lane, provenance, content) {
3489
3854
  return /* @__PURE__ */ jsx("div", { className: "caos-cw-panel", "data-group": id, "data-lane": lane, "data-provenance": provenance, children: content });
3490
3855
  }
3491
3856
  function CaseWorkbench(props) {
3492
- const { caseId, cases, source, controls, replayOnly, variants, rail, railLabel, groups, compare, context: context2, group, onGroupChange, deepLinkView = true, ariaLabel, focus } = props;
3857
+ const { caseId, cases, source, controls, replayOnly, variants, rail, railLabel, groups, compare, context, group, onGroupChange, deepLinkView = true, ariaLabel, focus } = props;
3493
3858
  const lang = useShellLang();
3494
3859
  const focusCfg = focus === true ? {} : focus || null;
3495
3860
  const { show: showFocus, setFocus, enterRef, memory } = useFocusMode(focusCfg);
@@ -3509,8 +3874,8 @@ function CaseWorkbench(props) {
3509
3874
  }
3510
3875
  tabs.push({
3511
3876
  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 })
3877
+ label: pick(context.label ?? { en: "Context", es: "Contexto" }, lang),
3878
+ content: /* @__PURE__ */ jsx("div", { className: "prose caos-cw-context", children: context.content })
3514
3879
  });
3515
3880
  if (tabs.length > MAX_WORKBENCH_GROUPS) {
3516
3881
  throw new Error(
@@ -3778,4 +4143,4 @@ function ViewsRow({ children, shares }) {
3778
4143
  }) });
3779
4144
  }
3780
4145
 
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 };
4146
+ 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 };