mixdog 1.0.3 → 1.0.5

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.
Files changed (160) hide show
  1. package/README.md +10 -5
  2. package/package.json +18 -17
  3. package/scripts/gate-local.mjs +93 -0
  4. package/scripts/lib/gate-scheduler.mjs +24 -0
  5. package/scripts/lib/run-node-tests.mjs +5 -1
  6. package/scripts/release-paths.mjs +22 -1
  7. package/scripts/test-direct.mjs +2 -1
  8. package/scripts/test.mjs +3 -1
  9. package/src/defaults/agents.json +0 -12
  10. package/src/defaults/mixdog-config.template.json +1 -2
  11. package/src/defaults/skills/docx/SKILL.md +7 -4
  12. package/src/defaults/skills/docx/references/html.md +46 -0
  13. package/src/defaults/skills/docx/references/native-authoring.md +18 -4
  14. package/src/defaults/skills/goal-management/SKILL.md +2 -2
  15. package/src/defaults/skills/pdf/SKILL.md +10 -2
  16. package/src/defaults/skills/pdf/references/html.md +64 -0
  17. package/src/defaults/skills/pptx/references/html.md +1 -1
  18. package/src/defaults/skills/xlsx/SKILL.md +7 -3
  19. package/src/defaults/skills/xlsx/references/html.md +40 -0
  20. package/src/defaults/skills/xlsx/references/report-design.md +21 -4
  21. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -1
  22. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  23. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +6 -5
  25. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +6 -3
  27. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -2
  28. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +50 -9
  29. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +28 -1
  30. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +4 -1
  31. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +34 -8
  32. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +10 -2
  33. package/src/runtime/agent/orchestrator/session/transcript-restore/agent-envelope.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
  35. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -1
  36. package/src/runtime/attachments/pdf-extract.mjs +7 -3
  37. package/src/runtime/computer-bridge/client.mjs +1 -1
  38. package/src/runtime/memory/index.mjs +4 -15
  39. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  40. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +0 -2
  41. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +3 -11
  42. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +1 -3
  43. package/src/runtime/memory/lib/cycle-scheduler/scheduled-enqueue.mjs +1 -11
  44. package/src/runtime/memory/lib/cycle-scheduler/tick-loop.mjs +0 -2
  45. package/src/runtime/memory/lib/cycle-scheduler.mjs +4 -13
  46. package/src/runtime/memory/lib/cycle-signatures.mjs +0 -6
  47. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +1 -1
  48. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +2 -2
  49. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -1
  50. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  51. package/src/runtime/memory/lib/memory-action-handlers/cycle-actions.mjs +7 -52
  52. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +6 -22
  53. package/src/runtime/memory/lib/memory-action-handlers/cycle-rebuild-action.mjs +10 -17
  54. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -3
  55. package/src/runtime/memory/lib/memory-action-handlers/manage-actions.mjs +2 -4
  56. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -4
  57. package/src/runtime/memory/lib/memory-cycle-requests.mjs +1 -1
  58. package/src/runtime/memory/lib/{memory-cycle2-shared.mjs → memory-cycle-shared.mjs} +14 -16
  59. package/src/runtime/memory/lib/memory-cycle.mjs +3 -2
  60. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  61. package/src/runtime/memory/lib/memory-embed.mjs +14 -1
  62. package/src/runtime/memory/lib/memory-ops-policy.mjs +8 -11
  63. package/src/runtime/memory/lib/memory-schema/entries.mjs +0 -1
  64. package/src/runtime/memory/lib/memory.mjs +2 -6
  65. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  66. package/src/runtime/memory/lib/query-handlers.mjs +1 -1
  67. package/src/runtime/memory/lib/query-hybrid-search.mjs +1 -1
  68. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -6
  69. package/src/runtime/memory/lib/recall-format.mjs +1 -1
  70. package/src/runtime/office/authoring/docx-html-build.mjs +375 -0
  71. package/src/runtime/office/authoring/docx-html-measure.mjs +561 -0
  72. package/src/runtime/office/authoring/docx-html-runner.mjs +102 -0
  73. package/src/runtime/office/authoring/html-browser.mjs +61 -0
  74. package/src/runtime/office/authoring/html-document-author.mjs +181 -0
  75. package/src/runtime/office/authoring/html-source-drift.mjs +36 -0
  76. package/src/runtime/office/authoring/pdf-author-action.mjs +266 -0
  77. package/src/runtime/office/authoring/pdf-html-charts.mjs +379 -0
  78. package/src/runtime/office/authoring/pdf-html-runner.mjs +317 -0
  79. package/src/runtime/office/authoring/pptx-author-action.mjs +44 -22
  80. package/src/runtime/office/authoring/pptx-author-session.mjs +12 -6
  81. package/src/runtime/office/authoring/pptx-html-build.mjs +23 -4
  82. package/src/runtime/office/authoring/pptx-html-measure.mjs +38 -44
  83. package/src/runtime/office/authoring/pptx-html-runner.mjs +18 -3
  84. package/src/runtime/office/authoring/pptx-receipt.mjs +39 -31
  85. package/src/runtime/office/authoring/pptx-review-artifacts.mjs +9 -1
  86. package/src/runtime/office/authoring/pptx-script-runner.mjs +6 -2
  87. package/src/runtime/office/authoring/xlsx-html-build.mjs +365 -0
  88. package/src/runtime/office/authoring/xlsx-html-measure.mjs +257 -0
  89. package/src/runtime/office/authoring/xlsx-html-runner.mjs +139 -0
  90. package/src/runtime/office/capabilities-catalog.mjs +8 -0
  91. package/src/runtime/office/capabilities-signatures.mjs +18 -6
  92. package/src/runtime/office/capabilities.mjs +26 -1
  93. package/src/runtime/office/com/office-com-host.ps1 +133 -17
  94. package/src/runtime/office/core/office-actions-batch.mjs +16 -1
  95. package/src/runtime/office/core/office-actions-inspect.mjs +3 -2
  96. package/src/runtime/office/core/office-actions-lifecycle.mjs +6 -2
  97. package/src/runtime/office/core/office-actions-render.mjs +4 -1
  98. package/src/runtime/office/core/office-finalize/review-stage.mjs +15 -2
  99. package/src/runtime/office/core/office-qa/design-review-stage.mjs +9 -0
  100. package/src/runtime/office/core/office-session-dispatch.mjs +3 -0
  101. package/src/runtime/office/core/office-sessionless-actions.mjs +7 -2
  102. package/src/runtime/office/core/office-sessions.mjs +19 -2
  103. package/src/runtime/office/pdf/pdf-writer.mjs +1 -1
  104. package/src/runtime/office/portable/docx-formatting.mjs +21 -0
  105. package/src/runtime/office/portable/ooxml-validator.mjs +22 -1
  106. package/src/runtime/office/portable/portable-chart.mjs +10 -2
  107. package/src/runtime/office/portable/portable-docx-edits.mjs +12 -6
  108. package/src/runtime/office/portable/portable-docx-operations.mjs +75 -1
  109. package/src/runtime/office/portable/portable-docx-xml.mjs +49 -6
  110. package/src/runtime/office/portable/portable-docx.mjs +2 -0
  111. package/src/runtime/office/portable/portable-snapshot-docx.mjs +2 -0
  112. package/src/runtime/office/portable/portable-validation.mjs +6 -3
  113. package/src/runtime/office/portable/portable-xlsx-cell-values.mjs +87 -0
  114. package/src/runtime/office/portable/portable-xlsx-charts.mjs +55 -13
  115. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +1 -1
  116. package/src/runtime/office/portable/xlsx-contract.mjs +82 -3
  117. package/src/runtime/office/quality/assurance-checklist.mjs +2 -2
  118. package/src/runtime/office/quality/assurance-rendered.mjs +27 -1
  119. package/src/runtime/office/quality/assurance-structure-docx.mjs +44 -0
  120. package/src/runtime/office/quality/assurance-structure-pptx.mjs +24 -6
  121. package/src/runtime/office/quality/assurance-structure-xlsx.mjs +50 -1
  122. package/src/runtime/office/quality/design-review-critique.mjs +3 -2
  123. package/src/runtime/office/quality/document-brief.mjs +44 -0
  124. package/src/runtime/office/quality/inline-audit.mjs +7 -2
  125. package/src/runtime/office/quality/quality-pipeline.mjs +9 -1
  126. package/src/runtime/office/tool-defs.mjs +2 -2
  127. package/src/runtime/shared/llm/quota-value-estimate.mjs +317 -0
  128. package/src/runtime/shared/llm/usage-ledger-quota.mjs +266 -63
  129. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +35 -0
  130. package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
  131. package/src/runtime/shared/llm/usage-ledger.mjs +10 -1
  132. package/src/runtime/shared/provider-accounts.mjs +11 -1
  133. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -4
  134. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  135. package/src/runtime/shared/wait-until.test-support.mjs +33 -0
  136. package/src/session-runtime/boot/apis.mjs +1 -1
  137. package/src/session-runtime/services/channel-admin.mjs +4 -3
  138. package/src/session-runtime/services/usage-stats-model.mjs +22 -0
  139. package/src/session-runtime/setup-tool/settings-contract.mjs +6 -1
  140. package/src/session-runtime/turn/session-ops.mjs +15 -9
  141. package/src/session-runtime/usage-stats-api.mjs +34 -2
  142. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  143. package/src/standalone/local-session-runtime.mjs +488 -0
  144. package/src/standalone/session-protocol.mjs +1 -0
  145. package/src/standalone/session-runtime-inline-host.mjs +1 -1
  146. package/src/tui/hooks/useSession.mjs +1 -1
  147. package/src/tui/session/session-api/commands.mjs +1 -1
  148. package/src/tui/session/session-api/integrations.mjs +3 -0
  149. package/src/tui/session/session-flow/auto-clear.mjs +6 -7
  150. package/src/tui/session/tool-card-results.mjs +1 -1
  151. package/src/tui/session/turn.mjs +5 -0
  152. package/src/tui/session-local.mjs +3 -488
  153. package/src/tui/session.mjs +1 -1
  154. package/src/ui/statusline-agents.mjs +0 -1
  155. package/src/rules/agent/41-cycle2-agent.md +0 -24
  156. package/src/runtime/memory/lib/cycle-scheduler/cycle2-runs.mjs +0 -120
  157. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +0 -110
  158. package/src/runtime/memory/lib/memory-cycle2-quarantine.mjs +0 -99
  159. package/src/runtime/memory/lib/memory-cycle2-review.mjs +0 -240
  160. package/src/runtime/memory/lib/memory-cycle2.mjs +0 -191
@@ -0,0 +1,379 @@
1
+ // data-chart on a printed page. The deck route turns `data-chart='{…}'` into a native PowerPoint chart
2
+ // (pptx-html-build.mjs addChart); the browser that prints a PDF draws nothing there. This draws the same
3
+ // spec — type, labels, values or series, colors, min/max, format, labels, legend, grid, value axis,
4
+ // gap, stacked, hole, plot — as an inline SVG inside the element before the page prints, so one HTML
5
+ // chart reads the same in a deck and on paper.
6
+
7
+ /**
8
+ * Runs inside the page (page.evaluate): self-contained. Fills every [data-chart] element with its SVG.
9
+ * @returns {{ drawn: number, warnings: object[] }}
10
+ */
11
+ export function drawDataCharts() {
12
+ const PALETTE = ['4472C4', 'ED7D31', 'A5A5A5', 'FFC000', '5B9BD5', '70AD47'];
13
+ const NS = 'http://www.w3.org/2000/svg';
14
+ const warnings = [];
15
+ let drawn = 0;
16
+ const hex = (value, fallback) => `#${String(value || fallback).replace(/^#/, '')}`;
17
+
18
+ // Excel number codes as the deck's charts read them: quoted and escaped literals, a number part of
19
+ // 0 # , . (decimals from the zeros after the point, thousands from a comma), and % (value × 100).
20
+ const formatter = (code) => {
21
+ if (!code) return (value) => String(Number(value));
22
+ let before = '';
23
+ let after = '';
24
+ let pattern = '';
25
+ let percent = false;
26
+ const text = String(code).split(';')[0];
27
+ for (let index = 0; index < text.length; index += 1) {
28
+ const char = text[index];
29
+ const literal = (value) => {
30
+ if (pattern) after += value;
31
+ else before += value;
32
+ };
33
+ if (char === '"') {
34
+ const end = text.indexOf('"', index + 1);
35
+ literal(text.slice(index + 1, end < 0 ? text.length : end));
36
+ index = end < 0 ? text.length : end;
37
+ } else if (char === '\\') {
38
+ literal(text[index + 1] || '');
39
+ index += 1;
40
+ } else if ('#0,.'.includes(char) && !after) pattern += char;
41
+ else {
42
+ if (char === '%') percent = true;
43
+ literal(char);
44
+ }
45
+ }
46
+ const point = pattern.indexOf('.');
47
+ const decimals = point < 0 ? 0 : (pattern.slice(point + 1).match(/0/g) || []).length;
48
+ const optional = point < 0 ? 0 : (pattern.slice(point + 1).match(/#/g) || []).length;
49
+ const grouped = (point < 0 ? pattern : pattern.slice(0, point)).includes(',');
50
+ return (value) => {
51
+ const number = Number(value) * (percent ? 100 : 1);
52
+ let body = number.toLocaleString('en-US', {
53
+ minimumFractionDigits: decimals,
54
+ maximumFractionDigits: decimals + optional,
55
+ useGrouping: grouped,
56
+ });
57
+ if (number < 0) body = `\u2212${body.replace('-', '')}`;
58
+ return `${before}${body}${after}`;
59
+ };
60
+ };
61
+
62
+ // A scale PowerPoint would choose: zero-based, topped at a round step above the largest value.
63
+ const niceStep = (span) => {
64
+ const raw = span / 5;
65
+ const power = 10 ** Math.floor(Math.log10(raw || 1));
66
+ const unit = raw / power;
67
+ return (unit <= 1 ? 1 : unit <= 2 ? 2 : unit <= 2.5 ? 2.5 : unit <= 5 ? 5 : 10) * power;
68
+ };
69
+
70
+ for (const element of document.querySelectorAll('[data-chart]')) {
71
+ let spec;
72
+ try {
73
+ spec = JSON.parse(element.dataset.chart);
74
+ } catch (error) {
75
+ warnings.push({
76
+ code: 'chart_invalid',
77
+ severity: 'warning',
78
+ message: `data-chart is not valid JSON (${error.message}); nothing was drawn there.`,
79
+ });
80
+ continue;
81
+ }
82
+ const box = element.getBoundingClientRect();
83
+ const style = getComputedStyle(element);
84
+ const W = box.width - parseFloat(style.borderLeftWidth) - parseFloat(style.borderRightWidth);
85
+ const H = box.height - parseFloat(style.borderTopWidth) - parseFloat(style.borderBottomWidth);
86
+ if (!(W > 0 && H > 0)) {
87
+ warnings.push({
88
+ code: 'chart_unsized',
89
+ severity: 'warning',
90
+ message: 'a data-chart element has no size; give it a width and a height.',
91
+ });
92
+ continue;
93
+ }
94
+ const type = String(spec.type || 'bar').toLowerCase();
95
+ const round = type === 'pie' || type === 'doughnut';
96
+ const horizontal = type === 'bar';
97
+ const labels = Array.isArray(spec.labels) ? spec.labels.map(String) : [];
98
+ const series = Array.isArray(spec.series)
99
+ ? spec.series.map((entry) => ({ name: String(entry.name ?? ''), values: (entry.values || []).map(Number) }))
100
+ : [{ name: String(spec.name || spec.unit || ''), values: (spec.values || []).map(Number) }];
101
+ const single = series.length === 1;
102
+ const colors = Array.isArray(spec.colors) && spec.colors.length ? spec.colors : PALETTE;
103
+ const colorOf = (seriesIndex, category) =>
104
+ hex(
105
+ single && !['line', 'area'].includes(type)
106
+ ? colors[category % colors.length]
107
+ : colors[seriesIndex % colors.length]
108
+ );
109
+ const ink = hex(spec.labelColor, '333333');
110
+ const font = spec.font || style.fontFamily;
111
+ const size = (Number(spec.size) || 12) * (96 / 72); // pt → CSS px
112
+ const format = formatter(spec.format);
113
+ const showValues = spec.showValues !== false;
114
+ const canvas = document.createElement('canvas').getContext('2d');
115
+ const textWidth = (text, bold) => {
116
+ canvas.font = `${bold ? 700 : 400} ${size}px ${font}`;
117
+ return canvas.measureText(text).width;
118
+ };
119
+ const svg = document.createElementNS(NS, 'svg');
120
+ svg.setAttribute('width', W);
121
+ svg.setAttribute('height', H);
122
+ svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
123
+ svg.setAttribute('role', 'img');
124
+ svg.setAttribute('aria-label', element.dataset.alt || element.getAttribute('aria-label') || 'chart');
125
+ svg.style.display = 'block';
126
+ svg.style.overflow = 'visible';
127
+ const add = (tag, attributes, text) => {
128
+ const node = document.createElementNS(NS, tag);
129
+ for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, value);
130
+ if (text != null) node.textContent = text;
131
+ svg.appendChild(node);
132
+ return node;
133
+ };
134
+ const label = (x, y, text, { anchor = 'middle', bold = false, color = ink, baseline = 'middle' } = {}) =>
135
+ add(
136
+ 'text',
137
+ {
138
+ x,
139
+ y,
140
+ 'text-anchor': anchor,
141
+ 'dominant-baseline': baseline,
142
+ 'font-family': font,
143
+ 'font-size': size,
144
+ 'font-weight': bold ? 700 : 400,
145
+ fill: color,
146
+ },
147
+ text
148
+ );
149
+
150
+ // The legend sits under the plot, one swatch and name per entry, centred.
151
+ const legendEntries = spec.legend
152
+ ? round || single
153
+ ? labels.map((name, index) => ({ name, color: colorOf(0, index) }))
154
+ : series.map((entry, index) => ({ name: entry.name, color: colorOf(index, 0) }))
155
+ : [];
156
+ const legendHeight = legendEntries.length ? size * 2 : 0;
157
+ if (legendEntries.length) {
158
+ const swatch = size * 0.75;
159
+ const widths = legendEntries.map((entry) => swatch + size * 0.4 + textWidth(entry.name) + size);
160
+ let x = (W - widths.reduce((sum, value) => sum + value, 0) + size) / 2;
161
+ const y = H - legendHeight / 2;
162
+ legendEntries.forEach((entry, index) => {
163
+ add('rect', { x, y: y - swatch / 2, width: swatch, height: swatch, fill: entry.color });
164
+ label(x + swatch + size * 0.4, y, entry.name, { anchor: 'start' });
165
+ x += widths[index];
166
+ });
167
+ }
168
+
169
+ if (round) {
170
+ const values = series[0].values;
171
+ const total = values.reduce((sum, value) => sum + Math.max(0, value), 0) || 1;
172
+ const cx = W / 2;
173
+ const cy = (H - legendHeight) / 2;
174
+ const r = Math.min(W, H - legendHeight) / 2 - size * 0.5;
175
+ const inner = type === 'doughnut' ? (r * (Number(spec.hole) || 60)) / 100 : 0;
176
+ let angle = -Math.PI / 2;
177
+ values.forEach((value, index) => {
178
+ const sweep = (Math.max(0, value) / total) * Math.PI * 2;
179
+ if (!sweep) return;
180
+ const end = angle + sweep;
181
+ const large = sweep > Math.PI ? 1 : 0;
182
+ const point = (radius, at) => `${cx + radius * Math.cos(at)} ${cy + radius * Math.sin(at)}`;
183
+ const d =
184
+ sweep >= Math.PI * 2 - 1e-6
185
+ ? inner
186
+ ? `M ${point(r, 0)} A ${r} ${r} 0 1 1 ${point(r, Math.PI)} A ${r} ${r} 0 1 1 ${point(r, 0)} M ${point(inner, 0)} A ${inner} ${inner} 0 1 0 ${point(inner, Math.PI)} A ${inner} ${inner} 0 1 0 ${point(inner, 0)} Z`
187
+ : `M ${point(r, 0)} A ${r} ${r} 0 1 1 ${point(r, Math.PI)} A ${r} ${r} 0 1 1 ${point(r, 0)} Z`
188
+ : inner
189
+ ? `M ${point(r, angle)} A ${r} ${r} 0 ${large} 1 ${point(r, end)} L ${point(inner, end)} A ${inner} ${inner} 0 ${large} 0 ${point(inner, angle)} Z`
190
+ : `M ${cx} ${cy} L ${point(r, angle)} A ${r} ${r} 0 ${large} 1 ${point(r, end)} Z`;
191
+ add('path', { d, fill: colorOf(0, index), 'fill-rule': 'evenodd', stroke: '#FFFFFF', 'stroke-width': 1 });
192
+ if (showValues) {
193
+ const middle = angle + sweep / 2;
194
+ const at = inner ? (r + inner) / 2 : r * 0.65;
195
+ label(cx + at * Math.cos(middle), cy + at * Math.sin(middle), format(value), { bold: true });
196
+ }
197
+ angle = end;
198
+ });
199
+ element.replaceChildren(svg);
200
+ drawn += 1;
201
+ continue;
202
+ }
203
+
204
+ // Axis charts: the value scale, then the plot area the labels leave.
205
+ const stacked = Boolean(spec.stacked) && ['bar', 'col', 'column', 'area'].includes(type);
206
+ const n = Math.max(labels.length, ...series.map((entry) => entry.values.length));
207
+ const extent = (pick) =>
208
+ stacked
209
+ ? Array.from({ length: n }, (_, i) =>
210
+ series.reduce((sum, entry) => sum + (pick(entry.values[i] || 0) ? entry.values[i] || 0 : 0), 0)
211
+ )
212
+ : series.flatMap((entry) => entry.values).filter((value) => pick(value));
213
+ const highest = Math.max(0, ...extent((value) => value > 0));
214
+ const lowest = Math.min(0, ...extent((value) => value < 0));
215
+ const below0 = niceStep(highest - lowest);
216
+ const min = spec.min != null ? Number(spec.min) : lowest < 0 ? -Math.ceil(-lowest / below0) * below0 : 0;
217
+ const step = niceStep((highest || 1) - min);
218
+ const max = spec.max != null ? Number(spec.max) : Math.ceil((highest * 1.05 - min) / step) * step + min || 1;
219
+ const ticks = [];
220
+ for (let value = min; value <= max + step * 1e-6; value += step) ticks.push(Number(value.toPrecision(12)));
221
+ const valueWidth = Math.max(
222
+ ...[...ticks, ...series.flatMap((entry) => entry.values)].map((value) => textWidth(format(value), true))
223
+ );
224
+ const categoryWidth = Math.max(0, ...labels.map((name) => textWidth(name)));
225
+ const showCategories = spec.categoryAxis !== false;
226
+ const pad = size * 0.4;
227
+ let plot;
228
+ if (spec.plot) {
229
+ plot = {
230
+ x: (Number(spec.plot.x) || 0) * W,
231
+ y: (Number(spec.plot.y) || 0) * H,
232
+ w: (Number(spec.plot.w) || 1) * W,
233
+ h: (Number(spec.plot.h) || 1) * H,
234
+ };
235
+ } else if (horizontal) {
236
+ const left = showCategories ? categoryWidth + pad * 2 : pad;
237
+ const right = showValues ? valueWidth + pad * 2 : pad;
238
+ const bottom = (spec.valueAxis ? size * 1.6 : pad) + legendHeight;
239
+ plot = { x: left, y: pad, w: Math.max(1, W - left - right), h: Math.max(1, H - pad - bottom) };
240
+ } else {
241
+ const left = spec.valueAxis ? valueWidth * 0.8 + pad * 2 : pad;
242
+ const top = showValues ? size * 1.6 : pad;
243
+ const bottom = (showCategories ? size * 2.2 : pad) + legendHeight;
244
+ plot = { x: left, y: top, w: Math.max(1, W - left - pad), h: Math.max(1, H - top - bottom) };
245
+ }
246
+ const span = max - min || 1;
247
+ // Value → position along the value axis: up the plot for columns and lines, rightward for bars.
248
+ const valueAt = (value) =>
249
+ horizontal
250
+ ? plot.x + ((Math.min(max, Math.max(min, value)) - min) / span) * plot.w
251
+ : plot.y + plot.h - ((Math.min(max, Math.max(min, value)) - min) / span) * plot.h;
252
+ const zero = valueAt(Math.max(min, Math.min(max, 0)));
253
+ const band = (horizontal ? plot.h : plot.w) / Math.max(1, n);
254
+ // Category i's band centre: bars run top-down (PowerPoint's maxMin), columns and points left to right.
255
+ const centre = (i) => (horizontal ? plot.y : plot.x) + (i + 0.5) * band;
256
+
257
+ if (spec.grid) {
258
+ for (const value of ticks) {
259
+ const at = valueAt(value);
260
+ add(
261
+ 'line',
262
+ horizontal
263
+ ? { x1: at, x2: at, y1: plot.y, y2: plot.y + plot.h, stroke: hex(spec.grid), 'stroke-width': 1 }
264
+ : { x1: plot.x, x2: plot.x + plot.w, y1: at, y2: at, stroke: hex(spec.grid), 'stroke-width': 1 }
265
+ );
266
+ }
267
+ }
268
+ if (spec.valueAxis) {
269
+ for (const value of ticks) {
270
+ const at = valueAt(value);
271
+ if (horizontal) label(at, plot.y + plot.h + size * 0.9, format(value));
272
+ else label(plot.x - pad, at, format(value), { anchor: 'end' });
273
+ }
274
+ }
275
+
276
+ if (['bar', 'col', 'column'].includes(type)) {
277
+ const gap = Number(spec.gap ?? 60) / 100;
278
+ const groups = stacked ? 1 : series.length;
279
+ const thick = band / (groups + gap);
280
+ for (let i = 0; i < n; i += 1) {
281
+ let base = 0;
282
+ let negativeBase = 0;
283
+ series.forEach((entry, s) => {
284
+ const value = entry.values[i];
285
+ if (value == null || Number.isNaN(value)) return;
286
+ const from = stacked ? (value < 0 ? negativeBase : base) : 0;
287
+ const to = from + value;
288
+ if (stacked) {
289
+ if (value < 0) negativeBase = to;
290
+ else base = to;
291
+ }
292
+ const offset = centre(i) - (groups * thick) / 2 + (stacked ? 0 : s * thick);
293
+ const a = valueAt(from);
294
+ const b = valueAt(to);
295
+ const lo = Math.min(a, b);
296
+ const length = Math.abs(b - a);
297
+ add(
298
+ 'rect',
299
+ horizontal
300
+ ? { x: lo, y: offset, width: length, height: thick, fill: colorOf(s, i) }
301
+ : { x: offset, y: lo, width: thick, height: length, fill: colorOf(s, i) }
302
+ );
303
+ if (!showValues) return;
304
+ const text = format(value);
305
+ if (stacked) {
306
+ const mid = (a + b) / 2;
307
+ if (horizontal) label(mid, offset + thick / 2, text, { bold: true });
308
+ else label(offset + thick / 2, mid, text, { bold: true });
309
+ } else if (horizontal) {
310
+ label(value < 0 ? lo - pad : lo + length + pad, offset + thick / 2, text, {
311
+ anchor: value < 0 ? 'end' : 'start',
312
+ bold: true,
313
+ });
314
+ } else {
315
+ label(offset + thick / 2, value < 0 ? lo + length + size * 0.8 : lo - size * 0.8, text, { bold: true });
316
+ }
317
+ });
318
+ }
319
+ } else {
320
+ // line and area: one point per category at the band centre.
321
+ const lineWidth = (Number(spec.lineSize) || 3) * (96 / 72);
322
+ let below = Array.from({ length: n }, () => 0);
323
+ series.forEach((entry, s) => {
324
+ const color = colorOf(s, 0);
325
+ const tops = Array.from({ length: n }, (_, i) => (stacked ? below[i] : 0) + (entry.values[i] || 0));
326
+ const points = tops.map((value, i) => [centre(i), valueAt(value)]);
327
+ if (type === 'area') {
328
+ const floor = stacked
329
+ ? below.map((value, i) => [centre(i), valueAt(value)]).reverse()
330
+ : [
331
+ [centre(n - 1), zero],
332
+ [centre(0), zero],
333
+ ];
334
+ add('polygon', {
335
+ points: [...points, ...floor].map((p) => p.join(',')).join(' '),
336
+ fill: color,
337
+ 'fill-opacity': single ? 1 : 0.85,
338
+ });
339
+ } else {
340
+ add('polyline', {
341
+ points: points.map((p) => p.join(',')).join(' '),
342
+ fill: 'none',
343
+ stroke: color,
344
+ 'stroke-width': lineWidth,
345
+ 'stroke-linejoin': 'round',
346
+ });
347
+ if (spec.markers !== false) {
348
+ for (const [x, y] of points) add('circle', { cx: x, cy: y, r: lineWidth * 1.4, fill: color });
349
+ }
350
+ }
351
+ if (showValues) {
352
+ entry.values.forEach((value, i) => {
353
+ if (value == null || Number.isNaN(value)) return;
354
+ label(points[i][0], points[i][1] - size * 0.9, format(value), { bold: true });
355
+ });
356
+ }
357
+ if (stacked) below = tops;
358
+ });
359
+ }
360
+
361
+ if (spec.axisLine) {
362
+ add(
363
+ 'line',
364
+ horizontal
365
+ ? { x1: zero, x2: zero, y1: plot.y, y2: plot.y + plot.h, stroke: ink, 'stroke-width': 1 }
366
+ : { x1: plot.x, x2: plot.x + plot.w, y1: zero, y2: zero, stroke: ink, 'stroke-width': 1 }
367
+ );
368
+ }
369
+ if (showCategories) {
370
+ labels.forEach((name, i) => {
371
+ if (horizontal) label(plot.x - pad, centre(i), name, { anchor: 'end' });
372
+ else label(centre(i), plot.y + plot.h + size * 1.15, name);
373
+ });
374
+ }
375
+ element.replaceChildren(svg);
376
+ drawn += 1;
377
+ }
378
+ return { drawn, warnings };
379
+ }