slidemind 0.3.19 → 0.4.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/engine-slides.mjs +1 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// A chart's own labelling drawn as separate elements — a legend of swatch and
|
|
2
|
+
// name rows, value labels set beside the bars, category names under the axis —
|
|
3
|
+
// belongs to the chart, and the chart draws it natively. x111 QBR 3 drew its
|
|
4
|
+
// doughnut's legend as four texts ("DE–PL lane 21%"); x108 cloud 5 as swatch,
|
|
5
|
+
// figure and name in three boxes per row; x110 QBR 2 set every category and
|
|
6
|
+
// value of its bridge beside it as text. Built so, they are boxes that drift
|
|
7
|
+
// from the data and that nobody can edit with it.
|
|
8
|
+
//
|
|
9
|
+
// Folded here, before any layout, on declaration and evidence, never on
|
|
10
|
+
// geometry: the elements the reader declared the chart's own (`chart.owns`),
|
|
11
|
+
// and texts whose words ARE the chart's data — a category name (alone, or with
|
|
12
|
+
// that category's value), a series name of a chart with more than one series,
|
|
13
|
+
// or a figure the reader listed among the chart's labels that equals one of
|
|
14
|
+
// its values. A legend row's key (the wordless parts of a declared stack row
|
|
15
|
+
// whose text folded) goes with it. The chart is told what it now draws itself
|
|
16
|
+
// (`chart.folded`) and chart-jsx.mjs switches its legend, category labels and
|
|
17
|
+
// value labels on. What a doughnut paints inside its hole is the chart's own
|
|
18
|
+
// content, not its labelling: claimed first (chart-hole.mjs), never folded.
|
|
19
|
+
import { claimHoles, holeMembers } from "./chart-hole.mjs";
|
|
20
|
+
import { nativeChartData } from "./concept-geometry.mjs";
|
|
21
|
+
|
|
22
|
+
const TEXT = new Set(["text", "number"]);
|
|
23
|
+
// Marks a legend draws beside its names: a swatch, a dot, a tick.
|
|
24
|
+
const KEY = new Set(["shape", "disc", "badge", "rule", "bar", "icon"]);
|
|
25
|
+
// What a short label may carry beside its figure: a sign, a unit.
|
|
26
|
+
const UNIT = /^[\s%‰$€£¥+\-−–(),.:]*(?:pp|pts?|bn|mn|[kmb]|x|×)?[\s%‰$€£¥+\-−–(),.:]*$/i;
|
|
27
|
+
|
|
28
|
+
/** A label's words as compared: no list glyph, one space, no trailing colon, any case. */
|
|
29
|
+
export function labelKey(text) {
|
|
30
|
+
return String(text ?? "")
|
|
31
|
+
.replace(/^[\s•·▪◦‣■□●○◆◇▶►✓✔✗-]+/u, "")
|
|
32
|
+
.replace(/\s+/g, " ")
|
|
33
|
+
.trim()
|
|
34
|
+
.replace(/:$/, "")
|
|
35
|
+
.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The one figure a short label carries, as a number: "21%" 21, "$610k" 610,
|
|
39
|
+
// "−1.1 pp" -1.1, "1,200" 1200, "95,4" 95.4. Null for none or several.
|
|
40
|
+
export function figureOf(text) {
|
|
41
|
+
const found = String(text ?? "").match(/[-+−]?\d[\d.,\s]*\d|[-+−]?\d/g);
|
|
42
|
+
if (!found || found.length !== 1) return null;
|
|
43
|
+
let digits = found[0].replace(/\s/g, "").replace("−", "-").replace("+", "");
|
|
44
|
+
if (/^-?\d{1,3}(,\d{3})+(\.\d+)?$/.test(digits)) digits = digits.replace(/,/g, "");
|
|
45
|
+
else if (/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(digits)) digits = digits.replace(/\./g, "").replace(",", ".");
|
|
46
|
+
else digits = digits.replace(",", ".");
|
|
47
|
+
const value = Number(digits);
|
|
48
|
+
return Number.isFinite(value) ? value : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sameValue = (a, v) => a != null && Number.isFinite(v) && Math.abs(Math.abs(a) - Math.abs(v)) <= 0.051 + Math.abs(v) * 0.005;
|
|
52
|
+
|
|
53
|
+
// A label that says nothing but its figure (and a sign or unit).
|
|
54
|
+
export const onlyFigure = (text) => figureOf(text) != null && UNIT.test(String(text).replace(/[-+−]?\d[\d.,\s]*\d|[-+−]?\d/, ""));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* What one text is to one chart, from its words alone, or null: `as` "legend"
|
|
58
|
+
* (a slice's or a series' name), "category" (a category name of a plotted
|
|
59
|
+
* chart) or "value" (a figure among the chart's labels that equals one of its
|
|
60
|
+
* values); `value` true when a name carries its own value beside it.
|
|
61
|
+
*/
|
|
62
|
+
export function chartLabelOf(text, chart, data, { declared = false } = {}) {
|
|
63
|
+
const key = labelKey(text);
|
|
64
|
+
if (!key) return null;
|
|
65
|
+
const sliced = ["pie", "doughnut"].includes(chart.type);
|
|
66
|
+
const named = sliced ? "legend" : "category";
|
|
67
|
+
const valuesAt = (i) => data.series.map((s) => s.values[i]);
|
|
68
|
+
for (const [i, category] of data.categories.entries()) {
|
|
69
|
+
const name = labelKey(category);
|
|
70
|
+
if (!name) continue;
|
|
71
|
+
if (key === name) return { as: named, value: false };
|
|
72
|
+
for (const rest of [key.startsWith(`${name} `) ? key.slice(name.length) : null, key.endsWith(` ${name}`) ? key.slice(0, -name.length) : null]) {
|
|
73
|
+
if (rest != null && onlyFigure(rest) && valuesAt(i).some((v) => sameValue(figureOf(rest), v))) return { as: named, value: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (data.series.length > 1 && data.series.some((s) => labelKey(s.name) === key)) return { as: "legend", value: false };
|
|
77
|
+
// A series named at one of its own points, with that point's value: a
|
|
78
|
+
// target line's "Target 96.5%" (x113 QBR 3). The chart labels the series'
|
|
79
|
+
// last point with its name and value.
|
|
80
|
+
for (const [s, series] of data.series.entries()) {
|
|
81
|
+
const name = labelKey(series.name);
|
|
82
|
+
const rest = name && key.startsWith(`${name} `) ? key.slice(name.length) : null;
|
|
83
|
+
if (rest != null && onlyFigure(rest) && series.values.some((v) => sameValue(figureOf(rest), v))) return { as: "series", value: true, series: s };
|
|
84
|
+
}
|
|
85
|
+
const listed = declared || (chart.labels ?? []).some((l) => labelKey(l) === key);
|
|
86
|
+
if (listed && onlyFigure(text) && data.series.some((s) => s.values.some((v) => sameValue(figureOf(text), v)))) return { as: "value", value: true };
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A figure's format: its words and signs around the digits ("21%" and "52%"
|
|
91
|
+
// are one format, "9" another).
|
|
92
|
+
const formatOf = (text) => String(text).replace(/[-+−]?\d[\d.,\s]*\d|[-+−]?\d/, "#").replace(/\s+/g, "").toLowerCase();
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Which of `texts` are one chart's own labelling, each as `chartLabelOf` says
|
|
96
|
+
* or null — judged as sets, because a legend, an axis and a row of value
|
|
97
|
+
* labels are sets: names count only when they cover at least two and at least
|
|
98
|
+
* half of the chart's categories (or series) — one name alone is a heading or a
|
|
99
|
+
* callout (x107 QBR 3: "On-time delivery" over a two-series chart) — and
|
|
100
|
+
* figures only as one format labelling every point of a series (x111 GTM 1:
|
|
101
|
+
* "9%", "27%", "12%" — not the unrelated "9" beside them). `declared`
|
|
102
|
+
* (indices) are the reader's: they count alone.
|
|
103
|
+
*/
|
|
104
|
+
export function chartLabelling(texts, chart, data, { declared = new Set() } = {}) {
|
|
105
|
+
const out = texts.map((t, i) => chartLabelOf(t, chart, data, { declared: declared.has(i) }));
|
|
106
|
+
const nameOf = (t) => {
|
|
107
|
+
const key = labelKey(t);
|
|
108
|
+
const i = data.categories.findIndex((c) => { const n = labelKey(c); return n && (key === n || key.startsWith(`${n} `) || key.endsWith(` ${n}`)); });
|
|
109
|
+
return i >= 0 ? `c${i}` : `s${data.series.findIndex((s) => labelKey(s.name) === key)}`;
|
|
110
|
+
};
|
|
111
|
+
// A typed legend or axis covers most of what it names: at least two of the
|
|
112
|
+
// chart's categories (series) and at least half of them. Two of a bridge's
|
|
113
|
+
// six category names under two KPI figures are captions (x112 QBR 2:
|
|
114
|
+
// "actual", "plan" beside "18.6%" and "19.5%").
|
|
115
|
+
out.forEach((l, i) => { if (l?.as === "series" && !declared.has(i)) out[i] = null; });
|
|
116
|
+
const named = new Set(out.map((l, i) => (l && l.as !== "value" && !declared.has(i) ? nameOf(texts[i]) : null)).filter(Boolean));
|
|
117
|
+
const covers = (prefix, total) => { const n = [...named].filter((k) => k.startsWith(prefix)).length; return n >= 2 && n >= Math.ceil(total / 2); };
|
|
118
|
+
const categoriesSet = covers("c", data.categories.filter((c) => labelKey(c)).length);
|
|
119
|
+
const seriesSet = covers("s", data.series.length);
|
|
120
|
+
out.forEach((l, i) => {
|
|
121
|
+
if (!l || l.as === "value" || declared.has(i)) return;
|
|
122
|
+
if (!(nameOf(texts[i]).startsWith("c") ? categoriesSet : seriesSet)) out[i] = null;
|
|
123
|
+
});
|
|
124
|
+
const groups = new Map();
|
|
125
|
+
out.forEach((l, i) => { if (l?.as === "value" && !declared.has(i)) groups.set(formatOf(texts[i]), [...(groups.get(formatOf(texts[i])) ?? []), i]); });
|
|
126
|
+
for (const members of groups.values()) {
|
|
127
|
+
const set = new Set((seriesLabelSet(members.map((i) => texts[i]), data) ?? []).map((k) => members[k]));
|
|
128
|
+
for (const i of members) if (!set.has(i)) out[i] = null;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Fold every native chart's own labelling into the chart (see the header).
|
|
135
|
+
* Mutates `spec`: drops the folded elements, prunes the structures that named
|
|
136
|
+
* them (stacks, grids, repeats, connectors), records `chart.folded` and says
|
|
137
|
+
* what it did in `notes` (default `spec.notes`). Idempotent.
|
|
138
|
+
*/
|
|
139
|
+
export function foldChartLabels(spec, notes = null) {
|
|
140
|
+
const elements = spec.elements ?? [];
|
|
141
|
+
const charts = elements.filter((e) => e.kind === "chart" && e.w > 0 && e.h > 0 && nativeChartData(e.chart));
|
|
142
|
+
if (!charts.length) return spec;
|
|
143
|
+
const said = notes ?? (spec.notes ??= []);
|
|
144
|
+
const say = (note) => { if (!said.includes(note)) said.push(note); };
|
|
145
|
+
for (const note of claimHoles(spec)) say(note);
|
|
146
|
+
const inHole = holeMembers(elements);
|
|
147
|
+
const byId = new Map(elements.map((e) => [e.id, e]));
|
|
148
|
+
const parents = new Set(elements.map((e) => e.parent).filter(Boolean));
|
|
149
|
+
const ancestors = (e) => { const out = new Set(); for (let p = byId.get(e.parent); p && !out.has(p.id); p = byId.get(p.parent)) out.add(p.id); return out; };
|
|
150
|
+
const rows = (spec.stacks ?? []).flatMap((s) => s.rows ?? []);
|
|
151
|
+
const gone = new Set();
|
|
152
|
+
// Which chart the reader declared each element the own labelling of: a text
|
|
153
|
+
// another chart owns is never this chart's, however its words read (x113 QBR
|
|
154
|
+
// 1: eight mini charts with the same "Q2"/"Q3" names — the first took every
|
|
155
|
+
// card's category labels and the other seven drew none).
|
|
156
|
+
const ownerOf = new Map();
|
|
157
|
+
for (const c of charts) for (const id of Array.isArray(c.chart.owns) ? c.chart.owns : []) if (!ownerOf.has(id)) ownerOf.set(id, c.id);
|
|
158
|
+
for (const c of charts) {
|
|
159
|
+
const data = nativeChartData(c.chart);
|
|
160
|
+
const own = ancestors(c);
|
|
161
|
+
const foldable = (e) => e && !gone.has(e.id) && e !== c && e.kind !== "chart" && !own.has(e.id) && !parents.has(e.id) && !inHole.has(e.id)
|
|
162
|
+
&& (ownerOf.get(e.id) ?? c.id) === c.id
|
|
163
|
+
// Undeclared, a text is this chart's only where it shares the chart's
|
|
164
|
+
// container — beside several charts, words alone do not say whose.
|
|
165
|
+
&& (ownerOf.has(e.id) || charts.length === 1 || !c.parent || ancestors(e).has(c.parent));
|
|
166
|
+
const before = c.chart.folded ?? {};
|
|
167
|
+
const folded = { legend: !!before.legend, categories: !!before.categories, values: !!before.values, side: before.side ?? null, texts: [...(before.texts ?? [])], ids: [...(before.ids ?? [])], ...(before.axis ? { axis: before.axis } : {}), ...(before.seriesLabels ? { seriesLabels: [...before.seriesLabels] } : {}), ...(before.frame ? { frame: before.frame } : {}) };
|
|
168
|
+
const take = (e, label) => {
|
|
169
|
+
gone.add(e.id);
|
|
170
|
+
folded.ids.push(e.id);
|
|
171
|
+
if (e.text != null) folded.texts.push(String(e.text));
|
|
172
|
+
if (label?.as === "legend") folded.legend = true;
|
|
173
|
+
if (label?.as === "category") folded.categories = true;
|
|
174
|
+
if (label?.as === "series") folded.seriesLabels = [...new Set([...(folded.seriesLabels ?? []), label.series])];
|
|
175
|
+
if (label?.value) folded.values = folded.values || label.as !== "series";
|
|
176
|
+
};
|
|
177
|
+
const declared = new Set(Array.isArray(c.chart.owns) ? c.chart.owns : []);
|
|
178
|
+
// The texts: those whose words are this chart's own data — the reader's
|
|
179
|
+
// declaration names them first, the evidence finds the rest. A declared
|
|
180
|
+
// text whose words are not the chart's data stays, and is said.
|
|
181
|
+
const candidates = elements.filter((e) => foldable(e) && TEXT.has(e.kind) && e.text != null);
|
|
182
|
+
const labels = chartLabelling(candidates.map((e) => String(e.text)), c.chart, data, {
|
|
183
|
+
declared: new Set(candidates.map((e, i) => (declared.has(e.id) ? i : -1)).filter((i) => i >= 0)),
|
|
184
|
+
});
|
|
185
|
+
// A declared run of figures that are not the chart's data but step evenly
|
|
186
|
+
// through one format is its value axis, painted with its ticks (x113 QBR 3:
|
|
187
|
+
// 94.5% … 97.0% beside the line): the chart draws that axis natively.
|
|
188
|
+
const dataValues = data.series.flatMap((s) => s.values).filter(Number.isFinite);
|
|
189
|
+
const ticks = axisTicks(candidates.filter((e) => declared.has(e.id)), dataValues);
|
|
190
|
+
if (ticks) {
|
|
191
|
+
for (const e of ticks.elements) take(e, null);
|
|
192
|
+
folded.axis = ticks.axis;
|
|
193
|
+
}
|
|
194
|
+
candidates.forEach((e, i) => {
|
|
195
|
+
if (ticks?.elements.includes(e)) return;
|
|
196
|
+
if (labels[i]) take(e, labels[i]);
|
|
197
|
+
else if (declared.has(e.id)) say(`chart ${c.id}: ${e.id} was declared the chart's own, but "${String(e.text).slice(0, 40)}" is not its data (a category, a series or one of its values) — kept as text`);
|
|
198
|
+
});
|
|
199
|
+
// A legend's keys go with its names: the wordless marks the reader declared
|
|
200
|
+
// the chart's, and those in a declared stack row whose text folded.
|
|
201
|
+
if (folded.legend) {
|
|
202
|
+
const inRow = new Set(rows.filter((row) => row.some((id) => folded.ids.includes(id))).flat());
|
|
203
|
+
for (const e of elements) {
|
|
204
|
+
if (foldable(e) && KEY.has(e.kind) && e.text == null && (declared.has(e.id) || inRow.has(e.id))) take(e, null);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Where the image drew the legend rows: the side of the chart they stand on.
|
|
208
|
+
const legendTexts = folded.ids.map((id) => byId.get(id)).filter((e) => e && e.text != null && [e.x, e.y, e.w, e.h].every(Number.isFinite));
|
|
209
|
+
if (folded.legend && legendTexts.length && !folded.side) {
|
|
210
|
+
const cx = legendTexts.reduce((a, e) => a + e.x + e.w / 2, 0) / legendTexts.length;
|
|
211
|
+
const cy = legendTexts.reduce((a, e) => a + e.y + e.h / 2, 0) / legendTexts.length;
|
|
212
|
+
folded.side = cy >= c.y + c.h ? "bottom" : cy <= c.y ? "top" : cx <= c.x ? "left" : "right";
|
|
213
|
+
}
|
|
214
|
+
// The chart's frame holds its own labelling: the boxes the folded legend
|
|
215
|
+
// rows, axis and value labels were drawn in join the chart's (x113 QBR 3:
|
|
216
|
+
// a 190px doughnut whose legend stood beside it shrank to a dot once the
|
|
217
|
+
// legend was drawn inside its frame).
|
|
218
|
+
const newly = folded.ids.slice((before.ids ?? []).length).map((id) => byId.get(id)).filter((e) => e && [e.x, e.y, e.w, e.h].every(Number.isFinite));
|
|
219
|
+
if (newly.length) {
|
|
220
|
+
const x0 = Math.min(c.x, ...newly.map((e) => e.x));
|
|
221
|
+
const y0 = Math.min(c.y, ...newly.map((e) => e.y));
|
|
222
|
+
const x1 = Math.max(c.x + c.w, ...newly.map((e) => e.x + e.w));
|
|
223
|
+
const y1 = Math.max(c.y + c.h, ...newly.map((e) => e.y + e.h));
|
|
224
|
+
if (x0 < c.x || y0 < c.y || x1 > c.x + c.w || y1 > c.y + c.h) {
|
|
225
|
+
folded.frame = { from: { x: c.x, y: c.y, w: c.w, h: c.h } };
|
|
226
|
+
Object.assign(c, { x: x0, y: y0, w: x1 - x0, h: y1 - y0 });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (folded.ids.length > (before.ids ?? []).length) {
|
|
230
|
+
const drawn = [folded.legend && "legend", folded.categories && "category labels", folded.values && "value labels"].filter(Boolean);
|
|
231
|
+
say(`chart ${c.id}: ${folded.ids.join(", ")} drew the chart's own ${drawn.join(" and ") || "labelling"} beside it — folded into the chart, which shows ${drawn.length ? "them" : "it"} natively`);
|
|
232
|
+
}
|
|
233
|
+
c.chart.folded = folded;
|
|
234
|
+
}
|
|
235
|
+
if (!gone.size) return spec;
|
|
236
|
+
spec.elements = elements.filter((e) => !gone.has(e.id));
|
|
237
|
+
pruneStructures(spec, gone);
|
|
238
|
+
return spec;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* A value axis painted with its tick labels, among a chart's declared figures:
|
|
243
|
+
* the longest run of at least three in one format that step evenly (94.5% 95.0%
|
|
244
|
+
* … 97.0% beside the value labels 95.4% 95.9% 96.2%), of which at least two are
|
|
245
|
+
* not the chart's data — evenly spaced value labels (10 20 30) are labels, not
|
|
246
|
+
* an axis. Returns the run's elements and the axis (`min`, `max`, `step`, the
|
|
247
|
+
* ticks' `format` as an Excel number format), or null.
|
|
248
|
+
*/
|
|
249
|
+
export function axisTicks(elements, dataValues = []) {
|
|
250
|
+
const figures = elements.filter((e) => onlyFigure(e.text)).map((e) => ({ e, v: figureOf(e.text), f: formatOf(e.text) }));
|
|
251
|
+
let best = null;
|
|
252
|
+
for (const f of new Set(figures.map((x) => x.f))) {
|
|
253
|
+
const same = figures.filter((x) => x.f === f);
|
|
254
|
+
const values = [...new Set(same.map((x) => x.v))].sort((a, b) => a - b);
|
|
255
|
+
for (let i = 0; i < values.length; i++) {
|
|
256
|
+
for (let j = i + 1; j < values.length; j++) {
|
|
257
|
+
const step = values[j] - values[i];
|
|
258
|
+
const at = (v) => same.find((x) => Math.abs(x.v - v) <= step * 1e-3);
|
|
259
|
+
const run = [];
|
|
260
|
+
for (let k = 0; at(values[i] + k * step); k++) run.push(at(values[i] + k * step));
|
|
261
|
+
// A tick IS a data value only at the tick's own precision (95.0% is not
|
|
262
|
+
// the value 95.4).
|
|
263
|
+
const isData = (x) => { const dec = (/[.,](\d+)\D*$/.exec(String(x.e.text))?.[1] ?? "").length; return dataValues.some((d) => Math.abs(x.v - d) <= 0.5 * 10 ** -dec + 1e-9); };
|
|
264
|
+
if (run.length < 3 || run.filter((x) => !isData(x)).length < 2) continue;
|
|
265
|
+
if (!best || run.length > best.run.length) best = { run, step };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!best) return null;
|
|
270
|
+
const { run, step } = best;
|
|
271
|
+
const sample = String(run[0].e.text).trim();
|
|
272
|
+
const decimals = (/[.,](\d+)/.exec(sample.replace(/^[^\d]*/, ""))?.[1] ?? "").length;
|
|
273
|
+
const [pre, post] = sample.split(/[-+−]?\d[\d.,\s]*\d|[-+−]?\d/).map((s) => s.trim().replace(/"/g, ""));
|
|
274
|
+
const body = `0${decimals ? `.${"0".repeat(decimals)}` : ""}`;
|
|
275
|
+
const format = pre || post || decimals ? `${pre ? `"${pre}"` : ""}${body}${post ? `"${post}"` : ""}` : null;
|
|
276
|
+
const round = (v) => Number(v.toFixed(decimals + 2));
|
|
277
|
+
return { elements: run.map((x) => x.e), axis: { min: round(run[0].v), max: round(run[run.length - 1].v), step: round(step), ...(format ? { format } : {}) } };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The indices of `texts` that label every value of one series, one text per
|
|
282
|
+
* value (the first series they cover), or null. Spare copies of a value (a
|
|
283
|
+
* doughnut's centre "58%" beside its legend's "58%") are not part of the set.
|
|
284
|
+
*/
|
|
285
|
+
export function seriesLabelSet(texts, data) {
|
|
286
|
+
for (const series of data.series) {
|
|
287
|
+
if (!series.values.length) continue;
|
|
288
|
+
const free = texts.map((t, i) => i);
|
|
289
|
+
const picked = [];
|
|
290
|
+
const complete = series.values.every((v) => {
|
|
291
|
+
const k = free.findIndex((i) => sameValue(figureOf(texts[i]), v));
|
|
292
|
+
if (k < 0) return false;
|
|
293
|
+
picked.push(free[k]);
|
|
294
|
+
free.splice(k, 1);
|
|
295
|
+
return true;
|
|
296
|
+
});
|
|
297
|
+
if (complete) return picked;
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Whether `texts` carry every value of at least one series, one text each. */
|
|
303
|
+
export const labelsEverySeriesPoint = (texts, data) => seriesLabelSet(texts, data) != null;
|
|
304
|
+
|
|
305
|
+
// The structures that named a folded element follow it: a stack row loses it
|
|
306
|
+
// (an empty row goes, a stack of fewer than two rows goes, and a grid whose
|
|
307
|
+
// column lost its stack), a repeat member loses it, a connector to it goes.
|
|
308
|
+
function pruneStructures(spec, gone) {
|
|
309
|
+
if (Array.isArray(spec.stacks)) {
|
|
310
|
+
spec.stacks = spec.stacks
|
|
311
|
+
.map((s) => ({ ...s, rows: (s.rows ?? []).map((r) => r.filter((id) => !gone.has(id))).filter((r) => r.length) }))
|
|
312
|
+
.filter((s) => s.rows.length >= 2);
|
|
313
|
+
}
|
|
314
|
+
if (Array.isArray(spec.grids)) {
|
|
315
|
+
const stackOf = (col) => (spec.stacks ?? []).find((s) => s.parent === col);
|
|
316
|
+
spec.grids = spec.grids.filter((g) => (g.columns ?? []).every((col) => stackOf(col)?.rows.length === g.rows));
|
|
317
|
+
}
|
|
318
|
+
if (Array.isArray(spec.repeats)) {
|
|
319
|
+
spec.repeats = spec.repeats
|
|
320
|
+
.map((g) => ({ ...g, members: (g.members ?? []).map((m) => (Array.isArray(m) ? m.filter((id) => !gone.has(id)) : gone.has(m) ? null : m)).filter((m) => m != null && (!Array.isArray(m) || m.length)) }))
|
|
321
|
+
.filter((g) => g.members.length >= 2);
|
|
322
|
+
}
|
|
323
|
+
if (Array.isArray(spec.connectors)) {
|
|
324
|
+
spec.connectors = spec.connectors.filter((x) => !gone.has(x.from) && !gone.has(x.to));
|
|
325
|
+
}
|
|
326
|
+
}
|