@chengxianglibra/dsh-data-analysis 0.1.1 → 0.1.2

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 (195) hide show
  1. package/README.md +83 -34
  2. package/lib/bin/environment.js +1 -1
  3. package/lib/bin/presentation-lint.js +21 -0
  4. package/lib/bridges.js +2 -2
  5. package/lib/client/credentials/create-datasource.js +67 -0
  6. package/lib/client/credentials/install.js +191 -0
  7. package/lib/client/credentials/model.js +494 -0
  8. package/lib/client/credentials/styles.js +17 -0
  9. package/lib/client/workspace-header-action.js +11 -0
  10. package/lib/client.js +1133 -736
  11. package/lib/compatibility.js +1 -1
  12. package/lib/datasource/authoring.js +70 -0
  13. package/lib/datasource/bridge-programs.js +30 -18
  14. package/lib/datasource/bridge.js +100 -8
  15. package/lib/datasource/index.js +3 -2
  16. package/lib/datasource/python.js +115 -0
  17. package/lib/datasource/resolver-program.js +112 -0
  18. package/lib/datasource/rpc.js +88 -0
  19. package/lib/datasource/service.js +687 -0
  20. package/lib/datasource/shell-env.js +2 -333
  21. package/lib/datasource/test.js +24 -51
  22. package/lib/environment/binding.js +54 -9
  23. package/lib/environment/runtime.js +89 -78
  24. package/lib/environment/subprocess.js +6 -1
  25. package/lib/environment/summary.js +3 -3
  26. package/lib/environment/workspace.js +5 -0
  27. package/lib/index.js +1 -1
  28. package/lib/plugin.js +168 -51
  29. package/lib/presentation/assets/portable.js +606 -0
  30. package/lib/presentation/assets/static.js +945 -0
  31. package/lib/presentation/build/index.js +71 -0
  32. package/lib/presentation/commit.js +156 -0
  33. package/lib/presentation/contracts/charts.js +476 -0
  34. package/lib/presentation/contracts/editing.js +58 -0
  35. package/lib/presentation/contracts/index.js +651 -0
  36. package/lib/presentation/contracts/interaction.js +62 -0
  37. package/lib/presentation/contracts/types.js +15 -0
  38. package/lib/presentation/contracts/values.js +28 -0
  39. package/lib/presentation/delivery.js +56 -0
  40. package/lib/presentation/files.js +83 -0
  41. package/lib/presentation/index.js +6 -0
  42. package/lib/presentation/lint.js +46 -0
  43. package/lib/presentation/projection/files.js +85 -0
  44. package/lib/presentation/projection/index.js +195 -0
  45. package/lib/presentation/projection/program.js +144 -0
  46. package/lib/presentation/projection/sql-code.js +79 -0
  47. package/lib/presentation/receipt.js +34 -0
  48. package/lib/presentation/reports.js +122 -0
  49. package/lib/presentation/rpc.js +148 -0
  50. package/lib/presentation/tool.js +122 -0
  51. package/lib/python-execution-contracts.js +1 -0
  52. package/lib/python-execution.js +232 -0
  53. package/lib/semantic-browser/contracts.js +80 -0
  54. package/lib/semantic-browser/definition.js +64 -0
  55. package/lib/semantic-browser/program.js +119 -0
  56. package/lib/semantic-browser/service.js +72 -0
  57. package/lib/semantic-reference/bridge.js +118 -0
  58. package/lib/semantic-reference/contracts.js +113 -0
  59. package/lib/semantic-reference/index.js +3 -0
  60. package/lib/semantic-reference/rpc.js +122 -0
  61. package/lib/semantic-reference/search.js +94 -0
  62. package/lib/semantic-reference/usage.js +143 -0
  63. package/lib/types/bridges.d.ts +2 -2
  64. package/lib/types/client/credentials/create-datasource.d.ts +5 -0
  65. package/lib/types/client/credentials/install.d.ts +1 -0
  66. package/lib/types/client/credentials/model.d.ts +59 -0
  67. package/lib/types/client/credentials/styles.d.ts +1 -0
  68. package/lib/types/client/presentation/ask-dsh.d.ts +9 -0
  69. package/lib/types/client/presentation/chart-explorer.d.ts +12 -0
  70. package/lib/types/client/presentation/chart-geometry.d.ts +30 -0
  71. package/lib/types/client/presentation/chart-renderer.d.ts +10 -0
  72. package/lib/types/client/presentation/chart-specials.d.ts +10 -0
  73. package/lib/types/client/presentation/chart-styles.d.ts +1 -0
  74. package/lib/types/client/presentation/chart-tooltip.d.ts +13 -0
  75. package/lib/types/client/presentation/chart-view.d.ts +23 -0
  76. package/lib/types/client/presentation/copy-context.d.ts +5 -0
  77. package/lib/types/client/presentation/delivery-model.d.ts +54 -0
  78. package/lib/types/client/presentation/delivery.d.ts +77 -0
  79. package/lib/types/client/presentation/editor-controls.d.ts +12 -0
  80. package/lib/types/client/presentation/global-filters.d.ts +6 -0
  81. package/lib/types/client/presentation/host-entry.d.ts +12 -0
  82. package/lib/types/client/presentation/icons.d.ts +4 -0
  83. package/lib/types/client/presentation/install.d.ts +15 -0
  84. package/lib/types/client/presentation/markdown.d.ts +5 -0
  85. package/lib/types/client/presentation/model.d.ts +62 -0
  86. package/lib/types/client/presentation/reader.d.ts +11 -0
  87. package/lib/types/client/presentation/source-code-model.d.ts +16 -0
  88. package/lib/types/client/presentation/source-code.d.ts +11 -0
  89. package/lib/types/client/presentation/source-dialog.d.ts +13 -0
  90. package/lib/types/client/presentation/source-facts.d.ts +20 -0
  91. package/lib/types/client/presentation/source-format.d.ts +5 -0
  92. package/lib/types/client/presentation/source-highlight.d.ts +7 -0
  93. package/lib/types/client/presentation/sources.d.ts +13 -0
  94. package/lib/types/client/presentation/styles.d.ts +1 -0
  95. package/lib/types/client/presentation/table.d.ts +12 -0
  96. package/lib/types/client/presentation/use-element-width.d.ts +5 -0
  97. package/lib/types/client/workspace-header-action.d.ts +7 -0
  98. package/lib/types/client.d.ts +4 -122
  99. package/lib/types/compatibility.d.ts +2 -2
  100. package/lib/types/datasource/authoring.d.ts +25 -0
  101. package/lib/types/datasource/bridge.d.ts +21 -3
  102. package/lib/types/datasource/index.d.ts +3 -5
  103. package/lib/types/datasource/python.d.ts +4 -0
  104. package/lib/types/datasource/resolver-program.d.ts +6 -0
  105. package/lib/types/datasource/rpc.d.ts +4 -0
  106. package/lib/types/datasource/service.d.ts +112 -0
  107. package/lib/types/datasource/shell-env.d.ts +0 -29
  108. package/lib/types/datasource/test.d.ts +6 -29
  109. package/lib/types/environment/binding.d.ts +5 -1
  110. package/lib/types/environment/errors.d.ts +1 -1
  111. package/lib/types/environment/runtime.d.ts +5 -5
  112. package/lib/types/environment/types.d.ts +13 -2
  113. package/lib/types/index.d.ts +1 -1
  114. package/lib/types/plugin.d.ts +11 -5
  115. package/lib/types/presentation/build/index.d.ts +11 -0
  116. package/lib/types/presentation/commit.d.ts +4 -0
  117. package/lib/types/presentation/contracts/charts.d.ts +13 -0
  118. package/lib/types/presentation/contracts/editing.d.ts +8 -0
  119. package/lib/types/presentation/contracts/index.d.ts +11 -0
  120. package/lib/types/presentation/contracts/interaction.d.ts +8 -0
  121. package/lib/types/presentation/contracts/types.d.ts +252 -0
  122. package/lib/types/presentation/contracts/values.d.ts +9 -0
  123. package/lib/types/presentation/delivery.d.ts +3 -0
  124. package/lib/types/presentation/files.d.ts +7 -0
  125. package/lib/types/presentation/index.d.ts +6 -0
  126. package/lib/types/presentation/lint.d.ts +13 -0
  127. package/lib/types/presentation/projection/files.d.ts +2 -0
  128. package/lib/types/presentation/projection/index.d.ts +20 -0
  129. package/lib/types/presentation/projection/program.d.ts +2 -0
  130. package/lib/types/presentation/projection/sql-code.d.ts +2 -0
  131. package/lib/types/presentation/receipt.d.ts +14 -0
  132. package/lib/types/presentation/reports.d.ts +5 -0
  133. package/lib/types/presentation/rpc.d.ts +24 -0
  134. package/lib/types/presentation/tool.d.ts +12 -0
  135. package/lib/types/python-execution-contracts.d.ts +10 -0
  136. package/lib/types/python-execution.d.ts +10 -0
  137. package/lib/types/semantic-browser/contracts.d.ts +40 -0
  138. package/lib/types/semantic-browser/definition.d.ts +20 -0
  139. package/lib/types/semantic-browser/program.d.ts +2 -0
  140. package/lib/types/semantic-browser/service.d.ts +20 -0
  141. package/lib/types/semantic-reference/bridge.d.ts +11 -0
  142. package/lib/types/semantic-reference/contracts.d.ts +51 -0
  143. package/lib/types/semantic-reference/index.d.ts +3 -0
  144. package/lib/types/semantic-reference/rpc.d.ts +15 -0
  145. package/lib/types/semantic-reference/search.d.ts +10 -0
  146. package/lib/types/semantic-reference/usage.d.ts +43 -0
  147. package/package.json +43 -19
  148. package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
  149. package/skills/dsh-data-analysis-presentation/SKILL.md +24 -0
  150. package/skills/dsh-data-analysis-presentation/references/chart-examples.md +18 -0
  151. package/skills/dsh-data-analysis-presentation/references/charts.md +55 -0
  152. package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +27 -0
  153. package/skills/dsh-data-analysis-presentation/references/examples/charts-bins.dataset.json +33 -0
  154. package/skills/dsh-data-analysis-presentation/references/examples/charts-boxes.dataset.json +45 -0
  155. package/skills/dsh-data-analysis-presentation/references/examples/charts-composition.dataset.json +33 -0
  156. package/skills/dsh-data-analysis-presentation/references/examples/charts-funnel.dataset.json +27 -0
  157. package/skills/dsh-data-analysis-presentation/references/examples/charts-precision.dataset.json +21 -0
  158. package/skills/dsh-data-analysis-presentation/references/examples/charts-series.dataset.json +58 -0
  159. package/skills/dsh-data-analysis-presentation/references/examples/charts-shares.dataset.json +33 -0
  160. package/skills/dsh-data-analysis-presentation/references/examples/charts-waterfall.dataset.json +44 -0
  161. package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +392 -0
  162. package/skills/dsh-data-analysis-presentation/references/examples/comparison-selected.dataset.json +24 -0
  163. package/skills/dsh-data-analysis-presentation/references/examples/comparison-summary.dataset.json +22 -0
  164. package/skills/dsh-data-analysis-presentation/references/examples/comparison.dataset.json +18 -0
  165. package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +79 -0
  166. package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +35 -0
  167. package/skills/dsh-data-analysis-presentation/references/examples/computed.dataset.json +12 -0
  168. package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +57 -0
  169. package/skills/dsh-data-analysis-presentation/references/examples/interaction-detail.dataset.json +30 -0
  170. package/skills/dsh-data-analysis-presentation/references/examples/interaction-summary.dataset.json +22 -0
  171. package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +278 -0
  172. package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +19 -0
  173. package/skills/dsh-data-analysis-presentation/references/examples/write-charts.py +73 -0
  174. package/skills/dsh-data-analysis-presentation/references/examples/write-comparison.py +42 -0
  175. package/skills/dsh-data-analysis-presentation/references/examples/write-computed.py +21 -0
  176. package/skills/dsh-data-analysis-presentation/references/examples/write-interaction.py +33 -0
  177. package/skills/dsh-data-analysis-presentation/references/examples.md +42 -0
  178. package/skills/dsh-data-analysis-presentation/references/interaction.md +56 -0
  179. package/skills/dsh-data-analysis-presentation/references/narrative.md +37 -0
  180. package/skills/dsh-data-analysis-presentation/references/schema.md +138 -0
  181. package/lib/datasource/access.js +0 -60
  182. package/lib/datasource/credentials.js +0 -24
  183. package/lib/evidence/bridge-program.js +0 -153
  184. package/lib/evidence/bridge.js +0 -166
  185. package/lib/evidence/index.js +0 -2
  186. package/lib/evidence/sources.js +0 -263
  187. package/lib/types/datasource/access.d.ts +0 -27
  188. package/lib/types/evidence/bridge.d.ts +0 -40
  189. package/lib/types/evidence/index.d.ts +0 -4
  190. package/lib/types/evidence/sources.d.ts +0 -57
  191. package/python/report-kit/dist/dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl +0 -0
  192. package/skills/dsh-data-analysis-report/SKILL.md +0 -57
  193. package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +0 -113
  194. package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +0 -1201
  195. package/skills/dsh-data-analysis-report/assets/report-data.js +0 -449
@@ -0,0 +1,476 @@
1
+ import { PRESENTATION_BUDGETS, } from "./types.js";
2
+ import { chartNumber, PresentationContractError } from "./values.js";
3
+ export const CHART_TYPES = [
4
+ 'line',
5
+ 'area',
6
+ 'stackedArea',
7
+ 'sparkline',
8
+ 'bar',
9
+ 'horizontalBar',
10
+ 'stackedBar',
11
+ 'stackedBar100',
12
+ 'horizontalStackedBar',
13
+ 'horizontalStackedBar100',
14
+ 'histogram',
15
+ 'boxPlot',
16
+ 'scatter',
17
+ 'heatmap',
18
+ 'pie',
19
+ 'leaderboard',
20
+ 'funnel',
21
+ 'waterfall',
22
+ ];
23
+ export const CHART_LABELS = {
24
+ line: '折线图',
25
+ area: '面积图',
26
+ stackedArea: '堆叠面积图',
27
+ sparkline: '迷你趋势图',
28
+ bar: '柱状图',
29
+ horizontalBar: '横向条形图',
30
+ stackedBar: '堆叠柱状图',
31
+ stackedBar100: '百分比堆叠柱状图',
32
+ horizontalStackedBar: '横向堆叠条形图',
33
+ horizontalStackedBar100: '横向百分比堆叠条形图',
34
+ histogram: '直方图',
35
+ boxPlot: '箱线图',
36
+ scatter: '散点图',
37
+ heatmap: '热力图',
38
+ pie: '环形图',
39
+ leaderboard: '排行榜',
40
+ funnel: '漏斗图',
41
+ waterfall: '瀑布图',
42
+ };
43
+ const trendTypes = ['line', 'area', 'stackedArea', 'sparkline'];
44
+ const ratioTypes = ['stackedBar100', 'horizontalStackedBar100'];
45
+ const singleSeriesTypes = [
46
+ 'sparkline',
47
+ 'histogram',
48
+ 'boxPlot',
49
+ 'scatter',
50
+ 'pie',
51
+ 'leaderboard',
52
+ 'funnel',
53
+ 'waterfall',
54
+ ];
55
+ const commonScaleTypes = [
56
+ 'stackedArea',
57
+ 'stackedBar',
58
+ 'horizontalStackedBar',
59
+ ...ratioTypes,
60
+ 'heatmap',
61
+ ];
62
+ const simpleTypes = [
63
+ ...trendTypes,
64
+ 'bar',
65
+ 'horizontalBar',
66
+ 'stackedBar',
67
+ 'horizontalStackedBar',
68
+ 'heatmap',
69
+ 'scatter',
70
+ ];
71
+ const bindingSpec = {
72
+ histogram: { required: ['binStart', 'binEnd'] },
73
+ boxPlot: { required: ['minimum', 'q1', 'q3', 'maximum'] },
74
+ waterfall: { required: ['start', 'end', 'role'] },
75
+ pie: { required: ['share'] },
76
+ funnel: { required: ['share'] },
77
+ leaderboard: { required: ['rank'] },
78
+ stackedBar100: { required: ['denominator'] },
79
+ horizontalStackedBar100: { required: ['denominator'] },
80
+ scatter: { required: [], optional: ['size', 'label', 'color'] },
81
+ };
82
+ function fail(path, message, code = 'invalid_value') {
83
+ throw new PresentationContractError(code, path, message);
84
+ }
85
+ function pointer(path, key) {
86
+ return `${path}/${String(key).replaceAll('~', '~0').replaceAll('/', '~1')}`;
87
+ }
88
+ function object(value, path) {
89
+ if (!value || typeof value !== 'object' || Array.isArray(value))
90
+ fail(path, 'Expected an object.');
91
+ return value;
92
+ }
93
+ function keys(value, required, optional, path) {
94
+ for (const key of required) {
95
+ if (!Object.hasOwn(value, key))
96
+ fail(pointer(path, key), 'Required field is missing.');
97
+ }
98
+ for (const key of Object.keys(value)) {
99
+ if (![...required, ...optional].includes(key))
100
+ fail(pointer(path, key), 'Unknown field.', 'unknown_field');
101
+ }
102
+ }
103
+ function string(value, path, max = 256) {
104
+ if (typeof value !== 'string' ||
105
+ !value.length ||
106
+ value.length > max ||
107
+ value.includes('\0') ||
108
+ /[\uD800-\uDFFF]/u.test(value))
109
+ fail(path, `Expected a nonempty string of at most ${max} characters.`);
110
+ }
111
+ function choice(value, choices, path) {
112
+ if (typeof value !== 'string' || !choices.includes(value))
113
+ fail(path, `Expected ${choices.join(', ')}.`);
114
+ }
115
+ function decimal(value) {
116
+ const [mantissa, exponent = '0'] = String(value).toLowerCase().split('e');
117
+ const [whole, fraction = ''] = mantissa.split('.');
118
+ return {
119
+ coefficient: BigInt(`${whole}${fraction}`),
120
+ exponent: BigInt(exponent) - BigInt(fraction.length),
121
+ };
122
+ }
123
+ /** Decimal comparisons must inspect original strings even when geometry is approximate. */
124
+ function sumSign(values) {
125
+ const parts = values
126
+ .map(([value, sign]) => {
127
+ const part = decimal(value);
128
+ return { ...part, coefficient: part.coefficient * BigInt(sign) };
129
+ })
130
+ .filter((part) => part.coefficient !== 0n)
131
+ .sort((a, b) => (a.exponent > b.exponent ? -1 : a.exponent < b.exponent ? 1 : 0));
132
+ let coefficient = 0n;
133
+ let exponent = parts[0]?.exponent ?? 0n;
134
+ for (const part of parts) {
135
+ const shift = exponent - part.exponent;
136
+ // Inputs have <= 256 digits and datasets <= 5000 rows. Beyond this gap,
137
+ // lower terms cannot change a nonzero sign; do not allocate enormous powers.
138
+ if (shift > 1024n && coefficient !== 0n)
139
+ return coefficient > 0n ? 1 : -1;
140
+ coefficient =
141
+ coefficient === 0n ? part.coefficient : coefficient * 10n ** shift + part.coefficient;
142
+ exponent = part.exponent;
143
+ }
144
+ return coefficient > 0n ? 1 : coefficient < 0n ? -1 : 0;
145
+ }
146
+ function compare(a, b) {
147
+ return sumSign([
148
+ [a, 1],
149
+ [b, -1],
150
+ ]);
151
+ }
152
+ /** Strictly validates one binding. A containing block handles id/kind/preparedViews. */
153
+ export function parseChartViewShape(value, path = '') {
154
+ const view = object(value, path);
155
+ keys(view, ['datasetId', 'chart', 'x', 'y', 'numericMode'], ['bindings', 'options'], path);
156
+ string(view.datasetId, `${path}/datasetId`);
157
+ string(view.x, `${path}/x`);
158
+ choice(view.chart, CHART_TYPES, `${path}/chart`);
159
+ choice(view.numericMode, ['exact', 'approximate'], `${path}/numericMode`);
160
+ if (!Array.isArray(view.y) || view.y.length < 1 || view.y.length > PRESENTATION_BUDGETS.columns)
161
+ fail(`${path}/y`, 'Expected one or more bounded series identifiers.');
162
+ view.y.forEach((id, i) => {
163
+ string(id, `${path}/y/${i}`);
164
+ });
165
+ if (new Set(view.y).size !== view.y.length)
166
+ fail(`${path}/y`, 'Duplicate identifiers.', 'duplicate_id');
167
+ const chart = view.chart;
168
+ if (singleSeriesTypes.includes(chart) && view.y.length !== 1)
169
+ fail(`${path}/y`, `${chart} requires exactly one series.`);
170
+ const spec = bindingSpec[chart];
171
+ if (view.bindings !== undefined || spec?.required.length) {
172
+ const bindings = object(view.bindings, `${path}/bindings`);
173
+ keys(bindings, spec?.required ?? [], spec?.optional ?? [], `${path}/bindings`);
174
+ for (const [name, id] of Object.entries(bindings))
175
+ string(id, `${path}/bindings/${name}`);
176
+ }
177
+ if (view.options === undefined)
178
+ return;
179
+ const options = object(view.options, `${path}/options`);
180
+ keys(options, [], ['showPoints', 'series', 'valueLabels', 'referenceLines'], `${path}/options`);
181
+ if (options.showPoints !== undefined) {
182
+ if (!trendTypes.includes(chart))
183
+ fail(`${path}/options/showPoints`, 'Point styles apply to trend charts only.');
184
+ choice(options.showPoints, ['auto', 'always', 'never'], `${path}/options/showPoints`);
185
+ }
186
+ if (options.series !== undefined) {
187
+ if (!trendTypes.includes(chart))
188
+ fail(`${path}/options/series`, 'Line styles apply to trend charts only.');
189
+ const series = object(options.series, `${path}/options/series`);
190
+ for (const [id, value] of Object.entries(series)) {
191
+ const seriesPath = pointer(`${path}/options/series`, id);
192
+ if (!view.y.includes(id))
193
+ fail(seriesPath, 'Style must name a selected series.', 'invalid_reference');
194
+ const style = object(value, seriesPath);
195
+ keys(style, [], ['lineStyle', 'role'], seriesPath);
196
+ if (style.lineStyle !== undefined)
197
+ choice(style.lineStyle, ['solid', 'dashed', 'dotted'], `${seriesPath}/lineStyle`);
198
+ if (style.role !== undefined)
199
+ choice(style.role, ['actual', 'baseline', 'target', 'forecast', 'plan', 'comparison'], `${seriesPath}/role`);
200
+ }
201
+ }
202
+ if (options.valueLabels !== undefined)
203
+ choice(options.valueLabels, ['none', 'auto', 'all'], `${path}/options/valueLabels`);
204
+ if (options.referenceLines !== undefined) {
205
+ if (!Array.isArray(options.referenceLines) || options.referenceLines.length > 16)
206
+ fail(`${path}/options/referenceLines`, 'Expected at most 16 reference lines.');
207
+ const axes = chart === 'scatter' || chart === 'histogram'
208
+ ? ['x', 'y']
209
+ : ['pie', 'funnel', 'heatmap', 'sparkline'].includes(chart)
210
+ ? []
211
+ : [
212
+ 'horizontalBar',
213
+ 'horizontalStackedBar',
214
+ 'horizontalStackedBar100',
215
+ 'leaderboard',
216
+ 'boxPlot',
217
+ ].includes(chart)
218
+ ? ['x']
219
+ : ['y'];
220
+ options.referenceLines.forEach((value, i) => {
221
+ const refPath = `${path}/options/referenceLines/${i}`;
222
+ const ref = object(value, refPath);
223
+ keys(ref, ['axis', 'value'], ['label'], refPath);
224
+ choice(ref.axis, axes, `${refPath}/axis`);
225
+ if (typeof ref.value !== 'number' || !Number.isFinite(ref.value))
226
+ fail(`${refPath}/value`, 'Expected a finite number.');
227
+ if (ref.label !== undefined)
228
+ string(ref.label, `${refPath}/label`);
229
+ });
230
+ }
231
+ }
232
+ /** Exact source columns, including every auxiliary field used by geometry or tooltip. */
233
+ export function chartColumns(view) {
234
+ return [...new Set([view.x, ...view.y, ...Object.values(view.bindings ?? {})])];
235
+ }
236
+ export function chartSeriesFields(view) {
237
+ return [...view.y];
238
+ }
239
+ /** Verifies supplied values only: never bins, ranks, accumulates, normalizes or sorts. */
240
+ export function validateChartView(view, dataset, path = '', dataPath = '/data', rowIndices) {
241
+ const rows = rowIndices ? rowIndices.map((index) => dataset.rows[index]) : dataset.rows;
242
+ parseChartViewShape({
243
+ datasetId: view.datasetId,
244
+ chart: view.chart,
245
+ x: view.x,
246
+ y: view.y,
247
+ numericMode: view.numericMode,
248
+ ...(view.bindings === undefined ? {} : { bindings: view.bindings }),
249
+ ...(view.options === undefined ? {} : { options: view.options }),
250
+ }, path);
251
+ const columns = new Map(dataset.columns.map((column, index) => [column.id, { column, index }]));
252
+ for (const id of chartColumns(view)) {
253
+ if (!columns.has(id))
254
+ fail(path, `Unknown column ${id}.`, 'invalid_reference');
255
+ }
256
+ const bindings = view.bindings ?? {};
257
+ const numeric = new Set(view.y);
258
+ if (view.chart === 'scatter')
259
+ numeric.add(view.x);
260
+ for (const [role, id] of Object.entries(bindings)) {
261
+ if (!['role', 'color', 'label'].includes(role))
262
+ numeric.add(id);
263
+ }
264
+ for (const id of numeric) {
265
+ const { column, index } = columns.get(id);
266
+ if (!['float64', 'decimal', 'int64'].includes(column.type))
267
+ fail(path, `Column ${id} must be numeric.`);
268
+ rows.forEach((row, i) => {
269
+ chartNumber(row[index], column, view.numericMode, `${dataPath}/rows/${rowIndices?.[i] ?? i}/${index}`);
270
+ });
271
+ }
272
+ if (bindings.role && columns.get(bindings.role).column.type !== 'string')
273
+ fail(`${path}/bindings/role`, 'Waterfall roles must use a string column.');
274
+ if (bindings.color &&
275
+ !['string', 'boolean', 'date', 'datetime'].includes(columns.get(bindings.color).column.type))
276
+ fail(`${path}/bindings/color`, 'Scatter color must use a categorical column.');
277
+ function commonUnit(ids) {
278
+ if (new Set(ids.map((id) => columns.get(id).column.unit ?? '')).size > 1)
279
+ fail(path, 'A shared chart scale requires matching units.');
280
+ }
281
+ if (commonScaleTypes.includes(view.chart))
282
+ commonUnit(view.y);
283
+ if (view.chart === 'boxPlot')
284
+ commonUnit([bindings.minimum, bindings.q1, view.y[0], bindings.q3, bindings.maximum]);
285
+ if (view.chart === 'histogram')
286
+ commonUnit([bindings.binStart, bindings.binEnd]);
287
+ if (view.chart === 'waterfall')
288
+ commonUnit([view.y[0], bindings.start, bindings.end]);
289
+ if (new Set(view.y.map((id) => columns.get(id).column.unit ?? '')).size > 1) {
290
+ const numericAxis = view.chart.startsWith('horizontal') ? 'x' : 'y';
291
+ view.options?.referenceLines?.forEach((reference, index) => {
292
+ if (reference.axis === numericAxis)
293
+ fail(`${path}/options/referenceLines/${index}`, 'Reference lines across different series units are ambiguous. Use a separate chart or prepared view for each unit.');
294
+ });
295
+ }
296
+ const raw = (row, id) => row[columns.get(id).index];
297
+ let previousBinEnd = null;
298
+ let previousRank = null;
299
+ let previousEnd = null;
300
+ let totalShare = 0;
301
+ const pieShares = [[1, -1]];
302
+ const exactStatistics = view.y.some((id) => columns.get(id).column.type !== 'float64') ||
303
+ (view.chart === 'waterfall' &&
304
+ [bindings.start, bindings.end].some((id) => columns.get(id).column.type !== 'float64'));
305
+ const exactShares = bindings.share !== undefined && columns.get(bindings.share).column.type !== 'float64';
306
+ const close = (a, b) => Math.abs(a - b) <= Number.EPSILON * 16 * Math.max(1, Math.abs(a), Math.abs(b));
307
+ const equal = (a, b) => exactStatistics ? compare(a, b) === 0 : close(Number(a), Number(b));
308
+ rows.forEach((row, i) => {
309
+ const rowPath = `${dataPath}/rows/${rowIndices?.[i] ?? i}`;
310
+ const values = view.y.map((id) => raw(row, id));
311
+ const value = values[0];
312
+ if (['stackedArea', 'stackedBar', 'horizontalStackedBar'].includes(view.chart)) {
313
+ let positive = 0;
314
+ let negative = 0;
315
+ for (const value of values) {
316
+ if (value === null)
317
+ continue;
318
+ const coordinate = Number(value);
319
+ if (coordinate >= 0)
320
+ positive += coordinate;
321
+ else
322
+ negative += coordinate;
323
+ if (!Number.isFinite(positive) || !Number.isFinite(negative))
324
+ fail(rowPath, 'Stacked chart coordinates must remain finite.', 'numeric_precision');
325
+ }
326
+ }
327
+ if (bindings.size) {
328
+ const size = raw(row, bindings.size);
329
+ if (size !== null && compare(size, 0) < 0)
330
+ fail(rowPath, 'Scatter size cannot be negative.');
331
+ }
332
+ if (ratioTypes.includes(view.chart)) {
333
+ const denominator = raw(row, bindings.denominator);
334
+ if (denominator !== null && compare(denominator, 0) < 0)
335
+ fail(rowPath, 'A percentage denominator cannot be negative.');
336
+ if (denominator !== null &&
337
+ compare(denominator, 0) === 0 &&
338
+ values.some((value) => value !== null))
339
+ fail(rowPath, 'Zero denominators require missing fractions.');
340
+ if (denominator === null && values.some((value) => value !== null))
341
+ fail(rowPath, 'Provided fractions require a positive denominator.');
342
+ if (values.some((value) => value !== null && (compare(value, 0) < 0 || compare(value, 1) > 0)))
343
+ fail(rowPath, 'Prepared fractions must be between zero and one.');
344
+ if (exactStatistics
345
+ ? sumSign([
346
+ ...values
347
+ .filter((value) => value !== null)
348
+ .map((value) => [value, 1]),
349
+ [1, -1],
350
+ ]) > 0
351
+ : values.reduce((sum, value) => sum + Number(value ?? 0), 0) >
352
+ 1 + Number.EPSILON * view.y.length * 4)
353
+ fail(rowPath, 'Prepared fractions cannot exceed one in total.');
354
+ }
355
+ if (bindings.share) {
356
+ const share = raw(row, bindings.share);
357
+ if (value !== null && compare(value, 0) < 0)
358
+ fail(rowPath, 'Part-to-whole values cannot be negative.');
359
+ if (share !== null && (compare(share, 0) < 0 || compare(share, 1) > 0))
360
+ fail(rowPath, 'Prepared share must be between zero and one.');
361
+ if (view.chart === 'pie' && share !== null) {
362
+ totalShare += Number(share);
363
+ pieShares.push([share, 1]);
364
+ }
365
+ }
366
+ if (view.chart === 'histogram') {
367
+ const start = raw(row, bindings.binStart);
368
+ const end = raw(row, bindings.binEnd);
369
+ if (value !== null && compare(value, 0) < 0)
370
+ fail(rowPath, 'Histogram frequency cannot be negative.');
371
+ if (start !== null && end !== null) {
372
+ if (compare(start, end) >= 0)
373
+ fail(rowPath, 'Histogram intervals require binStart < binEnd.');
374
+ if (previousBinEnd !== null && compare(start, previousBinEnd) < 0)
375
+ fail(rowPath, 'Histogram intervals must be ordered and nonoverlapping.');
376
+ previousBinEnd = end;
377
+ }
378
+ }
379
+ if (view.chart === 'boxPlot') {
380
+ const five = [
381
+ bindings.minimum,
382
+ bindings.q1,
383
+ view.y[0],
384
+ bindings.q3,
385
+ bindings.maximum,
386
+ ].map((id) => raw(row, id));
387
+ const present = five.filter((entry) => entry !== null);
388
+ if (present.some((entry, j) => j > 0 && compare(entry, present[j - 1]) < 0))
389
+ fail(rowPath, 'Five-number summary must be ordered minimum <= q1 <= median <= q3 <= maximum.');
390
+ }
391
+ if (view.chart === 'leaderboard') {
392
+ const rankValue = raw(row, bindings.rank);
393
+ if (rankValue !== null) {
394
+ const rank = Number(rankValue);
395
+ if (!Number.isSafeInteger(rank) || rank < 1 || compare(rankValue, rank) !== 0)
396
+ fail(rowPath, 'Prepared rank must be a positive safe integer.');
397
+ if (previousRank !== null && rank < previousRank)
398
+ fail(rowPath, 'Prepared ranks must already be ordered.');
399
+ previousRank = rank;
400
+ }
401
+ }
402
+ if (view.chart === 'waterfall') {
403
+ const start = raw(row, bindings.start);
404
+ const end = raw(row, bindings.end);
405
+ const role = row[columns.get(bindings.role).index];
406
+ if (role !== null && !['start', 'delta', 'subtotal', 'total'].includes(String(role)))
407
+ fail(rowPath, 'Unknown waterfall step role.');
408
+ if (start !== null && end !== null && value !== null && role !== null) {
409
+ if (role === 'delta') {
410
+ if (exactStatistics
411
+ ? sumSign([
412
+ [end, 1],
413
+ [start, -1],
414
+ [value, -1],
415
+ ]) !== 0
416
+ : !close(Number(end) - Number(start), Number(value)))
417
+ fail(rowPath, 'Waterfall delta must equal end minus start.');
418
+ if (previousEnd !== null && !equal(start, previousEnd))
419
+ fail(rowPath, 'Waterfall delta must start at the preceding end.');
420
+ }
421
+ else {
422
+ if (!equal(start, 0) || !equal(end, value))
423
+ fail(rowPath, 'Waterfall start and totals require start zero and end equal to value.');
424
+ if (role === 'start' && i !== 0)
425
+ fail(rowPath, 'A waterfall start must be the first step.');
426
+ if (role !== 'start' && previousEnd !== null && !equal(end, previousEnd))
427
+ fail(rowPath, 'Waterfall totals must match the preceding end.');
428
+ }
429
+ previousEnd = end;
430
+ }
431
+ else
432
+ previousEnd = null;
433
+ }
434
+ });
435
+ if (exactShares
436
+ ? sumSign(pieShares) > 0
437
+ : totalShare > 1 + Number.EPSILON * Math.max(1, rows.length) * 4)
438
+ fail(dataPath, 'Prepared pie shares cannot exceed one in total.');
439
+ }
440
+ /** Changes geometry only. Statistical views must be declared with prepared bindings. */
441
+ export function chartTransition(view, chart, dataset) {
442
+ if (view.chart !== chart &&
443
+ !simpleTypes.includes(chart) &&
444
+ !(ratioTypes.includes(view.chart) && ratioTypes.includes(chart)))
445
+ return undefined;
446
+ const next = {
447
+ datasetId: view.datasetId,
448
+ chart,
449
+ x: view.x,
450
+ y: [...view.y],
451
+ numericMode: view.numericMode,
452
+ ...(view.bindings === undefined ? {} : { bindings: { ...view.bindings } }),
453
+ ...(view.options === undefined ? {} : { options: structuredClone(view.options) }),
454
+ };
455
+ if (view.chart !== chart) {
456
+ if (!ratioTypes.includes(chart))
457
+ delete next.bindings;
458
+ if (next.options) {
459
+ delete next.options.referenceLines;
460
+ if (!trendTypes.includes(chart)) {
461
+ delete next.options.showPoints;
462
+ delete next.options.series;
463
+ }
464
+ }
465
+ }
466
+ try {
467
+ parseChartViewShape(next);
468
+ validateChartView(next, dataset);
469
+ return next;
470
+ }
471
+ catch (error) {
472
+ if (error instanceof PresentationContractError)
473
+ return undefined;
474
+ throw error;
475
+ }
476
+ }
@@ -0,0 +1,58 @@
1
+ import { PRESENTATION_BUDGETS, parsePresentationDocument } from "./index.js";
2
+ import { blockRegion, editedInteraction } from "./interaction.js";
3
+ export function presentationEdits(document) {
4
+ return structuredClone({ title: document.title, blocks: document.blocks });
5
+ }
6
+ /** Only presentation fields are accepted. Saved data, sources and captured code stay server-owned. */
7
+ export function applyPresentationEdits(base, value) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value))
9
+ throw new Error('invalid-report-edits');
10
+ const edits = value;
11
+ if (Object.keys(edits).sort().join(',') !== 'blocks,title' ||
12
+ !Array.isArray(edits.blocks) ||
13
+ new TextEncoder().encode(JSON.stringify(edits)).length > PRESENTATION_BUDGETS.documentBytes)
14
+ throw new Error('invalid-report-edits');
15
+ const regions = edits.blocks.map((block) => blockRegion(base, block.id));
16
+ if (regions.some((region, i) => i > 0 && region < regions[i - 1]))
17
+ throw new Error('invalid-report-edits: cannot move across interaction boundaries');
18
+ const candidate = { ...structuredClone(base), ...structuredClone(edits) };
19
+ const interaction = editedInteraction(base, edits.blocks);
20
+ if (interaction)
21
+ candidate.interaction = interaction;
22
+ else
23
+ delete candidate.interaction;
24
+ const next = parsePresentationDocument(candidate);
25
+ for (const block of next.blocks) {
26
+ const original = base.blocks.find((entry) => entry.id === block.id);
27
+ if (!original || original.kind !== block.kind)
28
+ throw new Error('invalid-report-edits');
29
+ const fixed = (entry) => {
30
+ switch (entry.kind) {
31
+ case 'markdown':
32
+ return { id: entry.id, kind: entry.kind };
33
+ case 'metric': {
34
+ const { label: _, ...identity } = entry;
35
+ return identity;
36
+ }
37
+ case 'table': {
38
+ const { columns: _, ...identity } = entry;
39
+ return identity;
40
+ }
41
+ case 'chart':
42
+ return { id: entry.id, kind: entry.kind, preparedViews: entry.preparedViews };
43
+ case 'source':
44
+ return entry;
45
+ }
46
+ };
47
+ if (JSON.stringify(fixed(block)) !== JSON.stringify(fixed(original)))
48
+ throw new Error('invalid-report-edits');
49
+ if (block.kind === 'chart' &&
50
+ original.kind === 'chart' &&
51
+ ![
52
+ original.datasetId,
53
+ ...(original.preparedViews ?? []).map((view) => view.datasetId),
54
+ ].includes(block.datasetId))
55
+ throw new Error('invalid-report-edits');
56
+ }
57
+ return next;
58
+ }