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