ggaction 0.0.13 → 0.0.16
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/CHANGELOG.md +146 -0
- package/README.md +41 -12
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-packet.schema.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +39 -7
- package/src/BasicChartProgram.js +3 -1
- package/src/ChartProgram.js +2 -0
- package/src/accessibility.js +157 -0
- package/src/actions/basic.js +2 -2
- package/src/actions/boxPlots/components.js +20 -12
- package/src/actions/boxPlots/create.js +4 -5
- package/src/actions/boxPlots/edit.js +34 -9
- package/src/actions/boxPlots/materialize.js +12 -4
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/canvas/actions.js +3 -3
- package/src/actions/canvas/fitting.js +1 -1
- package/src/actions/categoryOrder/index.js +21 -12
- package/src/actions/charts/area.js +6 -2
- package/src/actions/charts/bar.js +3 -2
- package/src/actions/charts/beeswarm.js +1 -1
- package/src/actions/charts/density.js +7 -3
- package/src/actions/charts/ecdf.js +6 -3
- package/src/actions/charts/endpoints.js +29 -7
- package/src/actions/charts/heatmap.js +3 -2
- package/src/actions/charts/histogram.js +5 -4
- package/src/actions/charts/horizon.js +4 -2
- package/src/actions/charts/interval-regression.js +7 -4
- package/src/actions/charts/line.js +4 -2
- package/src/actions/charts/parallel.js +9 -3
- package/src/actions/charts/pie.js +11 -3
- package/src/actions/charts/polar.js +17 -6
- package/src/actions/charts/radar.js +9 -3
- package/src/actions/charts/radial.js +12 -4
- package/src/actions/charts/raincloud.js +2 -2
- package/src/actions/charts/rug-strip.js +8 -4
- package/src/actions/charts/scatter.js +4 -2
- package/src/actions/composition/actions.js +29 -24
- package/src/actions/coordinates/actions.js +2 -2
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/basicBin2d.js +4 -5
- package/src/actions/data/bin.js +3 -3
- package/src/actions/data/bin2d.js +63 -193
- package/src/actions/data/bin2dMaterialize.js +4 -5
- package/src/actions/data/box.js +4 -4
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +4 -4
- package/src/actions/data/create.js +7 -14
- package/src/actions/data/density.js +15 -23
- package/src/actions/data/derived.js +57 -31
- package/src/actions/data/ecdf.js +2 -2
- package/src/actions/data/edit.js +551 -0
- package/src/actions/data/filter.js +28 -22
- package/src/actions/data/fold.js +2 -2
- package/src/actions/data/gradientProfile.js +2 -2
- package/src/actions/data/horizon.js +2 -2
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +62 -47
- package/src/actions/data/interval.js +2 -2
- package/src/actions/data/intervalEdit.js +26 -18
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +21 -27
- package/src/actions/data/revise.js +48 -0
- package/src/actions/data/revisionBindings.js +125 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/stack.js +2 -2
- package/src/actions/data/summary.js +3 -3
- package/src/actions/data/timeUnit.js +6 -3
- package/src/actions/data/window.js +3 -3
- package/src/actions/encodings/angle.js +1 -1
- package/src/actions/encodings/appearance.js +4 -4
- package/src/actions/encodings/barWidth.js +1 -1
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/index.js +3 -2
- package/src/actions/encodings/density.js +7 -4
- package/src/actions/encodings/histogram.js +4 -3
- package/src/actions/encodings/horizon.js +6 -8
- package/src/actions/encodings/index.js +4 -0
- package/src/actions/encodings/offset.js +22 -15
- package/src/actions/encodings/parallel.js +4 -5
- package/src/actions/encodings/pathOrder.js +6 -11
- package/src/actions/encodings/position/apply.js +53 -7
- package/src/actions/encodings/position/index.js +14 -6
- package/src/actions/encodings/position/policies/bar.js +9 -0
- package/src/actions/encodings/position/resolve.js +24 -7
- package/src/actions/encodings/ranged.js +5 -5
- package/src/actions/encodings/remove.js +13 -9
- package/src/actions/encodings/ruleAppearance.js +2 -24
- package/src/actions/encodings/seriesLayout.js +2 -1
- package/src/actions/encodings/shared.js +82 -14
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/encodings/strokeDash.js +2 -2
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/create.js +34 -20
- package/src/actions/errorBands/edit.js +34 -137
- package/src/actions/errorBars/create.js +43 -20
- package/src/actions/errorBars/edit.js +29 -125
- package/src/actions/facets/actions.js +267 -73
- package/src/actions/facets/derive.js +99 -28
- package/src/actions/facets/guides.js +4 -5
- package/src/actions/facets/replay.js +4 -5
- package/src/actions/gradientPlots/components.js +15 -8
- package/src/actions/gradientPlots/create.js +4 -5
- package/src/actions/gradientPlots/edit.js +18 -9
- package/src/actions/gradientPlots/materialize.js +5 -3
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/gradientPlots/rebind.js +4 -5
- package/src/actions/guides/applicability.js +8 -2
- package/src/actions/guides/axes/axes.js +23 -4
- package/src/actions/guides/axes/axis.js +2 -2
- package/src/actions/guides/axes/edit.js +2 -2
- package/src/actions/guides/axes/labels.js +67 -21
- package/src/actions/guides/axes/lines.js +15 -9
- package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
- package/src/actions/guides/axes/parallel/resolve.js +6 -2
- package/src/actions/guides/axes/parallel.js +5 -5
- package/src/actions/guides/axes/remove.js +4 -4
- package/src/actions/guides/axes/tickGroups.js +6 -5
- package/src/actions/guides/axes/ticks.js +19 -6
- package/src/actions/guides/axes/titles.js +21 -13
- package/src/actions/guides/grids/grid.js +18 -29
- package/src/actions/guides/grids/resolve.js +7 -2
- package/src/actions/guides/guides.js +20 -4
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +138 -52
- package/src/actions/guides/legends/categorical/components.js +55 -45
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +98 -16
- package/src/actions/guides/legends/categorical/options.js +2 -2
- package/src/actions/guides/legends/categorical/recipes.js +111 -20
- package/src/actions/guides/legends/categorical/resolve.js +12 -8
- package/src/actions/guides/legends/categorical/symbols.js +104 -52
- package/src/actions/guides/legends/continuous/common.js +76 -20
- package/src/actions/guides/legends/continuous/gradient.js +95 -158
- package/src/actions/guides/legends/continuous/index.js +2 -0
- package/src/actions/guides/legends/continuous/interval.js +21 -16
- package/src/actions/guides/legends/continuous/opacity.js +37 -83
- package/src/actions/guides/legends/continuous/stroke.js +225 -0
- package/src/actions/guides/legends/creation.js +14 -1
- package/src/actions/guides/legends/edit.js +159 -60
- package/src/actions/guides/legends/focused.js +5 -5
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +59 -24
- package/src/actions/guides/legends/lifecycle.js +19 -8
- package/src/actions/guides/legends/remove.js +17 -8
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +100 -108
- package/src/actions/guides/legends/strokeWidth.js +38 -92
- package/src/actions/guides/legends/target.js +112 -1
- package/src/actions/guides/legends/transition.js +165 -16
- package/src/actions/guides/polar/axes/facade.js +8 -8
- package/src/actions/guides/polar/axes/labels.js +29 -10
- package/src/actions/guides/polar/axes/lines.js +19 -19
- package/src/actions/guides/polar/axes/shared.js +16 -0
- package/src/actions/guides/polar/axes/ticks.js +10 -16
- package/src/actions/guides/polar/axes/titles.js +6 -6
- package/src/actions/guides/polar/grids.js +12 -14
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/index.js +16 -3
- package/src/actions/intervals/revision.js +126 -0
- package/src/actions/marks/arc/actions.js +80 -22
- package/src/actions/marks/area/actions.js +38 -13
- package/src/actions/marks/area/materialize.js +17 -1
- package/src/actions/marks/bar/create.js +12 -3
- package/src/actions/marks/bar/edit.js +21 -4
- package/src/actions/marks/bar/materialize.js +34 -40
- package/src/actions/marks/index.js +10 -1
- package/src/actions/marks/line/actions.js +40 -17
- package/src/actions/marks/line/materialize.js +29 -10
- package/src/actions/marks/point/create.js +9 -3
- package/src/actions/marks/point/edit.js +19 -7
- package/src/actions/marks/point/jitter.js +6 -8
- package/src/actions/marks/point/materialize.js +48 -22
- package/src/actions/marks/point/packing.js +6 -8
- package/src/actions/marks/rect/actions.js +36 -15
- package/src/actions/marks/references.js +602 -33
- package/src/actions/marks/remove.js +67 -41
- package/src/actions/marks/rule/actions.js +51 -14
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +79 -14
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +157 -17
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +72 -0
- package/src/actions/marks/tick/actions.js +25 -8
- package/src/actions/primitives/createGraphics.js +5 -4
- package/src/actions/primitives/editGraphics.js +31 -4
- package/src/actions/primitives/graphicProperties.js +6 -0
- package/src/actions/primitives/semantic.js +11 -3
- package/src/actions/primitives/semanticAction.js +90 -14
- package/src/actions/primitives/semanticValidation/dataset.js +2 -4
- package/src/actions/primitives/semanticValidation/guide.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +5 -0
- package/src/actions/primitives/semanticValidation/scale.js +28 -7
- package/src/actions/regression/components.js +36 -25
- package/src/actions/regression/create.js +13 -6
- package/src/actions/regression/edit.js +24 -11
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +173 -0
- package/src/actions/scales/channels.js +212 -0
- package/src/actions/scales/consumers/common.js +1 -1
- package/src/actions/scales/consumers/families.js +3 -2
- package/src/actions/scales/consumers/index.js +19 -2
- package/src/actions/scales/consumers/seriesLayout.js +28 -14
- package/src/actions/scales/create.js +55 -23
- package/src/actions/scales/definitions.js +94 -40
- package/src/actions/scales/edit.js +67 -12
- package/src/actions/scales/editPolicy.js +81 -11
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +5 -4
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/preview.js +111 -10
- package/src/actions/scales/quantitativeColor.js +4 -5
- package/src/actions/selection/actions.js +179 -66
- package/src/actions/selection/index.js +3 -0
- package/src/actions/textMetrics/index.js +56 -0
- package/src/actions/theme/actions.js +70 -15
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +9 -2
- package/src/actions/theme/reconcile.js +313 -121
- package/src/actions/theme/state.js +160 -0
- package/src/actions/titles/actions.js +8 -8
- package/src/actions/titles/resolve.js +3 -3
- package/src/actions/violinPlots/create.js +6 -3
- package/src/actions/violinPlots/edit.js +2 -1
- package/src/core/action.js +117 -35
- package/src/core/compositionState.js +6 -1
- package/src/core/diagnostics.js +16 -0
- package/src/core/extensionRegistry.js +27 -0
- package/src/core/font.js +18 -0
- package/src/core/identifiers.js +5 -4
- package/src/core/immutable.js +10 -3
- package/src/core/optionRequirements.js +8 -0
- package/src/core/programState.js +21 -1
- package/src/core/resourceReferences.js +341 -0
- package/src/core/textMetricProfile.js +28 -0
- package/src/core/textMetrics.js +24 -3
- package/src/core/validation.js +22 -10
- package/src/core/vocabulary.js +16 -3
- package/src/diagnostics.js +1 -0
- package/src/grammar/bars/aggregate.js +4 -1
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bin.js +54 -9
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +265 -64
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +7 -47
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/impute.js +341 -0
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +18 -1
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/pathSeries.js +26 -8
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/parameters.js +23 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/colorConsumers.js +20 -1
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +27 -4
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/scales/types.js +8 -0
- package/src/grammar/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +190 -41
- package/src/grammar/schemas/semanticPath.js +7 -2
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +78 -16
- package/src/grammar/timeUnit.js +216 -5
- package/src/grammar/transformTopology.js +4 -0
- package/src/grammar/transforms.js +394 -28
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +221 -16
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +59 -5
- package/src/layout/labels.js +351 -7
- package/src/layout/legend.js +4 -4
- package/src/layout/legendItems.js +6 -6
- package/src/layout/text.js +10 -9
- package/src/layout/title.js +9 -9
- package/src/materialization/bars/aggregate.js +25 -2
- package/src/materialization/bars/histogram.js +84 -24
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/composition.js +3 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +36 -0
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +9 -3
- package/src/materialization/facetGuides/preparation.js +13 -8
- package/src/materialization/facetHeaders.js +347 -0
- package/src/materialization/facets.js +15 -65
- package/src/materialization/guides/layout.js +3 -3
- package/src/materialization/guides/resources.js +26 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/index.js +11 -5
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/rect.js +25 -12
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/scaleGuideDependencies.js +10 -0
- package/src/materialization/scales/map.js +5 -3
- package/src/materialization/scales/policies/binnedPosition.js +2 -2
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +4 -1
- package/src/materialization/scales/resolve.js +22 -6
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +30 -3
- package/src/materialization/selection/items/common.js +20 -5
- package/src/materialization/selection/items/rect.js +28 -4
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +265 -14
- package/src/materialization/typography.js +31 -0
- package/src/mcp/cli.js +5 -3
- package/src/persistence/codec.js +65 -0
- package/src/persistence/validation.js +185 -0
- package/src/persistence.js +74 -0
- package/src/renderers/canvas/circle.js +2 -3
- package/src/renderers/canvas/index.js +5 -4
- package/src/renderers/canvas/line.js +2 -3
- package/src/renderers/canvas/native.js +4 -4
- package/src/renderers/canvas/path.js +2 -3
- package/src/renderers/canvas/rect.js +2 -3
- package/src/renderers/canvas/stroke.js +11 -0
- package/src/renderers/nativeBackend.js +10 -0
- package/src/renderers/options.js +13 -0
- package/src/renderers/pdf.js +25 -54
- package/src/renderers/png.js +22 -17
- package/src/renderers/svg.js +29 -14
- package/src/renderers/text.js +1 -11
- package/src/selectors/coordinates.js +3 -1
- package/src/selectors/datasets.js +38 -5
- package/src/selectors/layers.js +10 -4
- package/src/selectors/markOwners.js +46 -0
- package/src/selectors/scales.js +6 -4
- package/src/theme/defaults.js +103 -1
- package/src/version.js +2 -0
- package/types/accessibility.d.ts +37 -0
- package/types/basic.d.ts +10 -2
- package/types/diagnostics.d.ts +15 -0
- package/types/index.d.ts +90 -0
- package/types/pdf.d.ts +9 -0
- package/types/persistence.d.ts +7 -0
- package/types/png.d.ts +9 -0
- package/types/program.d.ts +944 -118
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ChartProgram } from "./ChartProgram.js";
|
|
2
|
+
import { BasicChartProgram } from "./BasicChartProgram.js";
|
|
3
|
+
import { cloneAndFreeze } from "./core/immutable.js";
|
|
4
|
+
import { hasRegisteredExtension, isBuiltinProgramClass } from "./core/extensionRegistry.js";
|
|
5
|
+
import { packageVersion } from "./version.js";
|
|
6
|
+
import { decodeValue, encodeValue, invalidSnapshot } from "./persistence/codec.js";
|
|
7
|
+
import { exactKeys, requireObject, STATE_KEYS, validateGraphic, validateProgramState, validateTrace } from "./persistence/validation.js";
|
|
8
|
+
|
|
9
|
+
function canonicalState(program, extensions, path = "payload") {
|
|
10
|
+
if (!(program instanceof ChartProgram || program instanceof BasicChartProgram) || !isBuiltinProgramClass(program.constructor)) {
|
|
11
|
+
throw invalidSnapshot(`Cannot persist an unregistered program class at ${path}.`, path);
|
|
12
|
+
}
|
|
13
|
+
if (program.actionStack.length !== 0) throw invalidSnapshot("Cannot persist an open actionStack.", `${path}.actionStack`);
|
|
14
|
+
const state = Object.fromEntries(STATE_KEYS.map(key => [key, program[key]]));
|
|
15
|
+
state.children = Object.fromEntries(Object.entries(program.children).map(([id, child]) => [id, canonicalState(child, extensions, `${path}.children.${id}`)]));
|
|
16
|
+
validateTrace(program.trace, program.constructor, extensions, `${path}.trace`);
|
|
17
|
+
validateProgramState(program, path);
|
|
18
|
+
return state;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function envelope(kind, payload, extensions = []) {
|
|
22
|
+
return JSON.stringify({ schemaVersion: 1, kind, packageVersion, extensions, payload: encodeValue(payload) });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readEnvelope(text, kind) {
|
|
26
|
+
if (typeof text !== "string") throw invalidSnapshot("Snapshot must be a JSON string.", "snapshot");
|
|
27
|
+
let value;
|
|
28
|
+
try { value = JSON.parse(text); } catch { throw invalidSnapshot("Snapshot must be valid JSON.", "snapshot"); }
|
|
29
|
+
exactKeys(value, ["schemaVersion", "kind", "packageVersion", "extensions", "payload"], "snapshot");
|
|
30
|
+
if (value.schemaVersion !== 1 || value.kind !== kind) throw invalidSnapshot(`Unsupported snapshot schema or kind; expected schema 1 ${kind}.`, "snapshot");
|
|
31
|
+
if (typeof value.packageVersion !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value.packageVersion)) throw invalidSnapshot("Invalid snapshot packageVersion.", "snapshot.packageVersion");
|
|
32
|
+
if (!Array.isArray(value.extensions) || new Set(value.extensions).size !== value.extensions.length ||
|
|
33
|
+
value.extensions.some(name => typeof name !== "string" || !hasRegisteredExtension(name)) || (kind === "graphic" && value.extensions.length)) {
|
|
34
|
+
throw invalidSnapshot("Snapshot requires unknown, duplicate, or inapplicable extensions.", "snapshot.extensions");
|
|
35
|
+
}
|
|
36
|
+
return { ...value, payload: decodeValue(value.payload) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function restoreState(state, extensions, path = "payload") {
|
|
40
|
+
exactKeys(state, STATE_KEYS, path);
|
|
41
|
+
if (!Array.isArray(state.actionStack) || state.actionStack.length) throw invalidSnapshot("Snapshot actionStack must be empty.", `${path}.actionStack`);
|
|
42
|
+
requireObject(state.children, `${path}.children`);
|
|
43
|
+
validateTrace(state.trace, ChartProgram, extensions, `${path}.trace`);
|
|
44
|
+
const children = Object.fromEntries(Object.entries(state.children).map(([id, child]) => [id, restoreState(child, extensions, `${path}.children.${id}`)]));
|
|
45
|
+
const program = new ChartProgram({ ...state, children });
|
|
46
|
+
validateProgramState(program, path);
|
|
47
|
+
return program;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function serializeProgram(program) {
|
|
51
|
+
const extensions = new Set();
|
|
52
|
+
return envelope("editable", canonicalState(program, extensions), [...extensions].sort());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function deserializeProgram(text) {
|
|
56
|
+
const stored = readEnvelope(text, "editable");
|
|
57
|
+
const extensions = new Set();
|
|
58
|
+
const program = restoreState(stored.payload, extensions);
|
|
59
|
+
if (extensions.size !== stored.extensions.length || stored.extensions.some(name => !extensions.has(name))) {
|
|
60
|
+
throw invalidSnapshot("Snapshot extensions must exactly match its action traces.", "snapshot.extensions");
|
|
61
|
+
}
|
|
62
|
+
return program;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function serializeGraphic(program) {
|
|
66
|
+
validateGraphic(program?.graphicSpec);
|
|
67
|
+
return envelope("graphic", program.graphicSpec);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function deserializeGraphic(text) {
|
|
71
|
+
const { payload } = readEnvelope(text, "graphic");
|
|
72
|
+
validateGraphic(payload);
|
|
73
|
+
return cloneAndFreeze({ graphicSpec: payload });
|
|
74
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requireFiniteProperty } from "./validation.js";
|
|
2
2
|
import { validateConcreteGraphicProperties } from
|
|
3
3
|
"../../grammar/schemas/concreteGraphic.js";
|
|
4
|
+
import { applyCanvasStroke } from "./stroke.js";
|
|
4
5
|
|
|
5
6
|
function drawCircle(context, child, collectionId) {
|
|
6
7
|
const properties = child.properties ?? {};
|
|
@@ -43,9 +44,7 @@ function drawCircle(context, child, collectionId) {
|
|
|
43
44
|
`Graphic "${graphicId}" requires a non-negative strokeWidth.`
|
|
44
45
|
);
|
|
45
46
|
}
|
|
46
|
-
context
|
|
47
|
-
context.lineWidth = strokeWidth;
|
|
48
|
-
context.setLineDash([]);
|
|
47
|
+
applyCanvasStroke(context, properties);
|
|
49
48
|
context.stroke();
|
|
50
49
|
}
|
|
51
50
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { annotateError } from "../../core/diagnostics.js";
|
|
1
2
|
import { drawCircleGraphic } from "./circle.js";
|
|
2
3
|
import { drawLineGraphic } from "./line.js";
|
|
3
4
|
import { drawPathGraphic } from "./path.js";
|
|
@@ -208,14 +209,14 @@ export function render(program, context, { pixelRatio = 1 } = {}) {
|
|
|
208
209
|
physicalWidth > MAX_RASTER_DIMENSION ||
|
|
209
210
|
physicalHeight > MAX_RASTER_DIMENSION
|
|
210
211
|
) {
|
|
211
|
-
throw new RangeError(
|
|
212
|
+
throw annotateError(new RangeError(
|
|
212
213
|
`render physical Canvas dimensions must not exceed ${MAX_RASTER_DIMENSION}.`
|
|
213
|
-
);
|
|
214
|
+
), { code: "resource-limit", operation: "render", optionPath: "physicalDimensions", limit: MAX_RASTER_DIMENSION, actual: Math.max(physicalWidth, physicalHeight) });
|
|
214
215
|
}
|
|
215
216
|
if (physicalWidth * physicalHeight > MAX_RASTER_PIXELS) {
|
|
216
|
-
throw new RangeError(
|
|
217
|
+
throw annotateError(new RangeError(
|
|
217
218
|
`render physical Canvas pixel count must not exceed ${MAX_RASTER_PIXELS}.`
|
|
218
|
-
);
|
|
219
|
+
), { code: "resource-limit", operation: "render", optionPath: "physicalPixels", limit: MAX_RASTER_PIXELS, actual: physicalWidth * physicalHeight });
|
|
219
220
|
}
|
|
220
221
|
preflightCanvasGraphicSpec(target, pixelRatio);
|
|
221
222
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requireFiniteProperty } from "./validation.js";
|
|
2
2
|
import { validateConcreteGraphicProperties } from
|
|
3
3
|
"../../grammar/schemas/concreteGraphic.js";
|
|
4
|
+
import { applyCanvasStroke } from "./stroke.js";
|
|
4
5
|
|
|
5
6
|
function drawLine(context, child, collectionId) {
|
|
6
7
|
const properties = child.properties ?? {};
|
|
@@ -32,10 +33,8 @@ function drawLine(context, child, collectionId) {
|
|
|
32
33
|
throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
context.strokeStyle = properties.stroke;
|
|
36
|
-
context.lineWidth = strokeWidth;
|
|
37
36
|
context.globalAlpha = opacity;
|
|
38
|
-
context
|
|
37
|
+
applyCanvasStroke(context, properties, properties.strokeDash ?? []);
|
|
39
38
|
context.beginPath();
|
|
40
39
|
context.moveTo(x1, y1);
|
|
41
40
|
context.lineTo(x2, y2);
|
|
@@ -9,10 +9,10 @@ import { validateConcreteGraphicProperties } from
|
|
|
9
9
|
const MAX_NATIVE_VALUE = 16_777_216;
|
|
10
10
|
const NATIVE_PROPERTIES = {
|
|
11
11
|
canvas: ["x", "y", "width", "height"],
|
|
12
|
-
circle: ["x", "y", "radius", "strokeWidth"],
|
|
13
|
-
rect: ["x", "y", "width", "height", "strokeWidth"],
|
|
14
|
-
line: ["x1", "y1", "x2", "y2", "strokeWidth"],
|
|
15
|
-
path: ["strokeWidth"],
|
|
12
|
+
circle: ["x", "y", "radius", "strokeWidth", "miterLimit"],
|
|
13
|
+
rect: ["x", "y", "width", "height", "strokeWidth", "miterLimit"],
|
|
14
|
+
line: ["x1", "y1", "x2", "y2", "strokeWidth", "miterLimit"],
|
|
15
|
+
path: ["strokeWidth", "miterLimit"],
|
|
16
16
|
text: ["x", "y", "fontSize"]
|
|
17
17
|
};
|
|
18
18
|
const COMMAND_PROPERTIES = ["x", "y", "x1", "y1", "x2", "y2"];
|
|
@@ -3,6 +3,7 @@ import { validateConcreteGraphicProperties } from
|
|
|
3
3
|
"../../grammar/schemas/concreteGraphic.js";
|
|
4
4
|
import { resolvePathCommandBounds } from "../../grammar/schemas/graphicBounds.js";
|
|
5
5
|
import { applyCanvasFill } from "./fill.js";
|
|
6
|
+
import { applyCanvasStroke } from "./stroke.js";
|
|
6
7
|
|
|
7
8
|
function drawPath(context, child, collectionId) {
|
|
8
9
|
const properties = child.properties ?? {};
|
|
@@ -82,9 +83,7 @@ function drawPath(context, child, collectionId) {
|
|
|
82
83
|
context.fill();
|
|
83
84
|
}
|
|
84
85
|
if (hasStroke) {
|
|
85
|
-
context
|
|
86
|
-
context.lineWidth = strokeWidth;
|
|
87
|
-
context.setLineDash(strokeDash);
|
|
86
|
+
applyCanvasStroke(context, properties, strokeDash);
|
|
88
87
|
context.stroke();
|
|
89
88
|
}
|
|
90
89
|
}
|
|
@@ -2,6 +2,7 @@ import { requireFiniteProperty } from "./validation.js";
|
|
|
2
2
|
import { validateConcreteGraphicProperties } from
|
|
3
3
|
"../../grammar/schemas/concreteGraphic.js";
|
|
4
4
|
import { applyCanvasFill } from "./fill.js";
|
|
5
|
+
import { applyCanvasStroke } from "./stroke.js";
|
|
5
6
|
|
|
6
7
|
function drawRect(context, child, collectionId) {
|
|
7
8
|
const properties = child.properties ?? {};
|
|
@@ -42,9 +43,7 @@ function drawRect(context, child, collectionId) {
|
|
|
42
43
|
bottom: y + height
|
|
43
44
|
}, graphicId);
|
|
44
45
|
context.fillRect(x, y, width, height);
|
|
45
|
-
context
|
|
46
|
-
context.lineWidth = strokeWidth;
|
|
47
|
-
context.setLineDash([]);
|
|
46
|
+
applyCanvasStroke(context, properties);
|
|
48
47
|
context.beginPath();
|
|
49
48
|
context.moveTo(x, y);
|
|
50
49
|
context.lineTo(x + width, y);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { resolveStrokeDetails } from "../../grammar/strokeStyle.js";
|
|
2
|
+
|
|
3
|
+
export function applyCanvasStroke(context, properties, strokeDash = []) {
|
|
4
|
+
const details = resolveStrokeDetails(properties);
|
|
5
|
+
context.strokeStyle = properties.stroke;
|
|
6
|
+
context.lineWidth = properties.strokeWidth;
|
|
7
|
+
context.lineCap = details.lineCap;
|
|
8
|
+
context.lineJoin = details.lineJoin;
|
|
9
|
+
context.miterLimit = details.miterLimit;
|
|
10
|
+
context.setLineDash(strokeDash);
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function loadNativeBackend() {
|
|
2
|
+
try {
|
|
3
|
+
return await import("@napi-rs/canvas");
|
|
4
|
+
} catch (error) {
|
|
5
|
+
if (error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes("@napi-rs/canvas")) {
|
|
6
|
+
throw new Error("PNG/PDF output requires @napi-rs/canvas. Install it with: npm install ggaction @napi-rs/canvas", { cause: error });
|
|
7
|
+
}
|
|
8
|
+
throw error;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { annotateError } from "../core/diagnostics.js";
|
|
2
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
3
|
+
|
|
4
|
+
export function validateRendererOptions(options, allowed, label) {
|
|
5
|
+
if (!isPlainObject(options)) {
|
|
6
|
+
throw annotateError(new TypeError(`${label} must be a plain object.`), { code: "invalid-option" });
|
|
7
|
+
}
|
|
8
|
+
for (const key of Object.keys(options)) {
|
|
9
|
+
if (!allowed.has(key)) {
|
|
10
|
+
throw annotateError(new TypeError(`${label} does not support option "${key}".`), { code: "invalid-option", optionPath: key });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/renderers/pdf.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { loadNativeBackend } from "./nativeBackend.js";
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
drawResolvedGraphicSpec,
|
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
resolveGraphicRenderTarget
|
|
10
10
|
} from "./canvas/index.js";
|
|
11
11
|
import { preflightCanvasGraphicSpec } from "./canvas/native.js";
|
|
12
|
+
import { validateRendererOptions } from "./options.js";
|
|
12
13
|
|
|
13
14
|
const PDF_OPTIONS = new Set(["output", "metadata"]);
|
|
15
|
+
const BUFFER_OPTIONS = new Set(["metadata"]);
|
|
14
16
|
const PDF_METADATA = new Set([
|
|
15
17
|
"title",
|
|
16
18
|
"author",
|
|
@@ -19,30 +21,9 @@ const PDF_METADATA = new Set([
|
|
|
19
21
|
]);
|
|
20
22
|
const MAX_PDF_DIMENSION = 16_777_216;
|
|
21
23
|
|
|
22
|
-
function requirePlainObject(value, label) {
|
|
23
|
-
if (
|
|
24
|
-
value === null ||
|
|
25
|
-
typeof value !== "object" ||
|
|
26
|
-
Array.isArray(value) ||
|
|
27
|
-
Object.getPrototypeOf(value) !== Object.prototype
|
|
28
|
-
) {
|
|
29
|
-
throw new TypeError(`${label} must be a plain object.`);
|
|
30
|
-
}
|
|
31
|
-
return value;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function requireClosedKeys(value, allowed, label) {
|
|
35
|
-
for (const key of Object.keys(value)) {
|
|
36
|
-
if (!allowed.has(key)) {
|
|
37
|
-
throw new TypeError(`${label} does not support option "${key}".`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
24
|
function requireMetadata(metadata) {
|
|
43
25
|
if (metadata === undefined) return undefined;
|
|
44
|
-
|
|
45
|
-
requireClosedKeys(metadata, PDF_METADATA, "renderToPDF metadata");
|
|
26
|
+
validateRendererOptions(metadata, PDF_METADATA, "renderToPDF metadata");
|
|
46
27
|
|
|
47
28
|
const resolved = {};
|
|
48
29
|
for (const key of ["title", "author", "subject"]) {
|
|
@@ -75,26 +56,6 @@ function requireMetadata(metadata) {
|
|
|
75
56
|
return resolved;
|
|
76
57
|
}
|
|
77
58
|
|
|
78
|
-
function requirePDFOptions(options) {
|
|
79
|
-
requirePlainObject(options, "renderToPDF options");
|
|
80
|
-
requireClosedKeys(options, PDF_OPTIONS, "renderToPDF");
|
|
81
|
-
if (typeof options.output !== "string" || options.output.length === 0) {
|
|
82
|
-
throw new TypeError("renderToPDF requires a non-empty output path.");
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
output: resolve(options.output),
|
|
86
|
-
metadata: requireMetadata(options.metadata)
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function renderPDFBuffer(target, metadata) {
|
|
91
|
-
const document = new PDFDocument(metadata);
|
|
92
|
-
const context = document.beginPage(target.width, target.height);
|
|
93
|
-
drawResolvedGraphicSpec(target, context);
|
|
94
|
-
document.endPage();
|
|
95
|
-
return document.close();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
59
|
function requirePDFPageDimensions(target) {
|
|
99
60
|
if (![target.width, target.height].every(value =>
|
|
100
61
|
Number.isSafeInteger(value) && value > 0 && value <= MAX_PDF_DIMENSION
|
|
@@ -105,22 +66,32 @@ function requirePDFPageDimensions(target) {
|
|
|
105
66
|
}
|
|
106
67
|
}
|
|
107
68
|
|
|
108
|
-
export async function
|
|
109
|
-
|
|
69
|
+
export async function renderToPDFBuffer(program, options = {}) {
|
|
70
|
+
validateRendererOptions(options, BUFFER_OPTIONS, "renderToPDFBuffer options");
|
|
71
|
+
const metadata = requireMetadata(options.metadata);
|
|
110
72
|
const graphicSpec = requireProgramGraphicSpec(program);
|
|
111
73
|
const target = resolveGraphicRenderTarget(graphicSpec);
|
|
112
74
|
requirePDFPageDimensions(target);
|
|
113
75
|
preflightCanvasGraphicSpec(target);
|
|
114
|
-
const
|
|
76
|
+
const { PDFDocument } = await loadNativeBackend();
|
|
77
|
+
const document = new PDFDocument(metadata);
|
|
78
|
+
const context = document.beginPage(target.width, target.height);
|
|
79
|
+
drawResolvedGraphicSpec(target, context);
|
|
80
|
+
document.endPage();
|
|
81
|
+
const buffer = document.close();
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
buffer, width: target.width, height: target.height, pages: 1, bytes: buffer.length
|
|
84
|
+
});
|
|
85
|
+
}
|
|
115
86
|
|
|
87
|
+
export async function renderToPDF(program, options = {}) {
|
|
88
|
+
validateRendererOptions(options, PDF_OPTIONS, "renderToPDF options");
|
|
89
|
+
if (typeof options.output !== "string" || options.output.length === 0) {
|
|
90
|
+
throw new TypeError("renderToPDF requires a non-empty output path.");
|
|
91
|
+
}
|
|
92
|
+
const { buffer, ...result } = await renderToPDFBuffer(program, { metadata: options.metadata });
|
|
93
|
+
const output = resolve(options.output);
|
|
116
94
|
await mkdir(dirname(output), { recursive: true });
|
|
117
95
|
await writeFile(output, buffer);
|
|
118
|
-
|
|
119
|
-
return Object.freeze({
|
|
120
|
-
output,
|
|
121
|
-
width: target.width,
|
|
122
|
-
height: target.height,
|
|
123
|
-
pages: 1,
|
|
124
|
-
bytes: buffer.length
|
|
125
|
-
});
|
|
96
|
+
return Object.freeze({ output, ...result });
|
|
126
97
|
}
|
package/src/renderers/png.js
CHANGED
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { render } from "./canvas/index.js";
|
|
4
|
+
import { validateRendererOptions } from "./options.js";
|
|
5
|
+
import { loadNativeBackend } from "./nativeBackend.js";
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
const PNG_OPTIONS = new Set(["output", "pixelRatio"]);
|
|
8
|
+
const BUFFER_OPTIONS = new Set(["pixelRatio"]);
|
|
5
9
|
|
|
6
|
-
|
|
10
|
+
export async function renderToPNGBuffer(program, options = {}) {
|
|
11
|
+
validateRendererOptions(options, BUFFER_OPTIONS, "renderToPNGBuffer options");
|
|
12
|
+
const { pixelRatio = 1 } = options;
|
|
13
|
+
const { createCanvas } = await loadNativeBackend();
|
|
14
|
+
const canvas = createCanvas(1, 1);
|
|
15
|
+
render(program, canvas.getContext("2d"), { pixelRatio });
|
|
16
|
+
// Canvas drawing is synchronous; encoding uses the native async worker.
|
|
17
|
+
const buffer = await canvas.encode("png");
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
buffer, width: canvas.width, height: canvas.height, pixelRatio, bytes: buffer.length
|
|
20
|
+
});
|
|
21
|
+
}
|
|
7
22
|
|
|
8
|
-
export async function renderToPNG(program,
|
|
23
|
+
export async function renderToPNG(program, options = {}) {
|
|
24
|
+
validateRendererOptions(options, PNG_OPTIONS, "renderToPNG options");
|
|
25
|
+
const { output, pixelRatio } = options;
|
|
9
26
|
if (typeof output !== "string" || output.length === 0) {
|
|
10
27
|
throw new TypeError("renderToPNG requires a non-empty output path.");
|
|
11
28
|
}
|
|
12
|
-
|
|
13
|
-
const canvas = createCanvas(1, 1);
|
|
14
|
-
const context = canvas.getContext("2d");
|
|
15
|
-
render(program, context, { pixelRatio });
|
|
16
|
-
|
|
29
|
+
const { buffer, ...result } = await renderToPNGBuffer(program, { pixelRatio });
|
|
17
30
|
const path = resolve(output);
|
|
18
|
-
const buffer = canvas.toBuffer("image/png");
|
|
19
31
|
await mkdir(dirname(path), { recursive: true });
|
|
20
32
|
await writeFile(path, buffer);
|
|
21
|
-
|
|
22
|
-
return Object.freeze({
|
|
23
|
-
output: path,
|
|
24
|
-
width: canvas.width,
|
|
25
|
-
height: canvas.height,
|
|
26
|
-
pixelRatio,
|
|
27
|
-
bytes: buffer.length
|
|
28
|
-
});
|
|
33
|
+
return Object.freeze({ output: path, ...result });
|
|
29
34
|
}
|
package/src/renderers/svg.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isOwned } from "../core/immutable.js";
|
|
1
2
|
import {
|
|
2
3
|
requireSingleOrderedGraphicByType,
|
|
3
4
|
walkGraphicTreeEvents
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
requireStringProperty
|
|
18
19
|
} from "./canvas/validation.js";
|
|
19
20
|
import { normalizeRendererFontWeight } from "./text.js";
|
|
21
|
+
import { validateRendererOptions } from "./options.js";
|
|
20
22
|
|
|
21
23
|
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
22
24
|
const SVG_OPTIONS = new Set(["title", "description", "resourceNamespace"]);
|
|
@@ -37,14 +39,20 @@ const DOMINANT_BASELINES = Object.freeze({
|
|
|
37
39
|
bottom: "text-after-edge"
|
|
38
40
|
});
|
|
39
41
|
|
|
42
|
+
const namespaceByGraphic = new WeakMap();
|
|
43
|
+
|
|
40
44
|
function resourceNamespace(graphicSpec) {
|
|
45
|
+
const cached = namespaceByGraphic.get(graphicSpec);
|
|
46
|
+
if (cached !== undefined) return cached;
|
|
41
47
|
const serialized = JSON.stringify(graphicSpec);
|
|
42
48
|
let hash = 0xcbf29ce484222325n;
|
|
43
49
|
for (let index = 0; index < serialized.length; index += 1) {
|
|
44
50
|
hash ^= BigInt(serialized.charCodeAt(index));
|
|
45
51
|
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
46
52
|
}
|
|
47
|
-
|
|
53
|
+
const namespace = hash.toString(36);
|
|
54
|
+
if (isOwned(graphicSpec)) namespaceByGraphic.set(graphicSpec, namespace);
|
|
55
|
+
return namespace;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
function escapeText(value) {
|
|
@@ -113,6 +121,21 @@ function requireStrokeDash(properties, graphicId) {
|
|
|
113
121
|
return strokeDash;
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
function strokeDetailAttributes(properties, strokeWidth) {
|
|
125
|
+
if (typeof properties.stroke !== "string" || !(strokeWidth > 0)) return [];
|
|
126
|
+
return [
|
|
127
|
+
["stroke-linecap", Object.hasOwn(properties, "lineCap")
|
|
128
|
+
? properties.lineCap
|
|
129
|
+
: undefined],
|
|
130
|
+
["stroke-linejoin", Object.hasOwn(properties, "lineJoin")
|
|
131
|
+
? properties.lineJoin
|
|
132
|
+
: undefined],
|
|
133
|
+
["stroke-miterlimit", Object.hasOwn(properties, "miterLimit")
|
|
134
|
+
? formatNumber(properties.miterLimit)
|
|
135
|
+
: undefined]
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
116
139
|
function requireCanvasGeometry(id, canvas, { nested }) {
|
|
117
140
|
const properties = canvas.properties ?? {};
|
|
118
141
|
const width = requireFiniteProperty(properties, "width", id);
|
|
@@ -200,6 +223,7 @@ function serializeCircle(state, graphicId, properties) {
|
|
|
200
223
|
["stroke-width", strokeWidth === undefined
|
|
201
224
|
? undefined
|
|
202
225
|
: formatNumber(strokeWidth)],
|
|
226
|
+
...strokeDetailAttributes(properties, strokeWidth),
|
|
203
227
|
["opacity", formatNumber(requireOpacity(properties, graphicId))]
|
|
204
228
|
]);
|
|
205
229
|
}
|
|
@@ -240,6 +264,7 @@ function serializeRect(state, graphicId, properties) {
|
|
|
240
264
|
["fill", fill],
|
|
241
265
|
["stroke", properties.stroke],
|
|
242
266
|
["stroke-width", formatNumber(strokeWidth)],
|
|
267
|
+
...strokeDetailAttributes(properties, strokeWidth),
|
|
243
268
|
["opacity", formatNumber(requireOpacity(properties, graphicId))]
|
|
244
269
|
]);
|
|
245
270
|
}
|
|
@@ -268,6 +293,7 @@ function serializeLine(state, graphicId, properties) {
|
|
|
268
293
|
["fill", "none"],
|
|
269
294
|
["stroke", properties.stroke],
|
|
270
295
|
["stroke-width", formatNumber(strokeWidth)],
|
|
296
|
+
...strokeDetailAttributes(properties, strokeWidth),
|
|
271
297
|
["stroke-dasharray", strokeDash.length === 0
|
|
272
298
|
? undefined
|
|
273
299
|
: strokeDash.map(formatNumber).join(" ")],
|
|
@@ -332,6 +358,7 @@ function serializePath(state, graphicId, properties) {
|
|
|
332
358
|
["stroke-width", strokeWidth === undefined
|
|
333
359
|
? undefined
|
|
334
360
|
: formatNumber(strokeWidth)],
|
|
361
|
+
...strokeDetailAttributes(properties, strokeWidth),
|
|
335
362
|
["stroke-dasharray", strokeDash.length === 0
|
|
336
363
|
? undefined
|
|
337
364
|
: strokeDash.map(formatNumber).join(" ")],
|
|
@@ -506,19 +533,7 @@ function serializeBody(graphicSpec, rootId, rootCanvas, width, height, state) {
|
|
|
506
533
|
}
|
|
507
534
|
|
|
508
535
|
function requireSVGOptions(options) {
|
|
509
|
-
|
|
510
|
-
options === null ||
|
|
511
|
-
typeof options !== "object" ||
|
|
512
|
-
Array.isArray(options) ||
|
|
513
|
-
Object.getPrototypeOf(options) !== Object.prototype
|
|
514
|
-
) {
|
|
515
|
-
throw new TypeError("renderToSVG options must be a plain object.");
|
|
516
|
-
}
|
|
517
|
-
for (const key of Object.keys(options)) {
|
|
518
|
-
if (!SVG_OPTIONS.has(key)) {
|
|
519
|
-
throw new TypeError(`renderToSVG does not support option "${key}".`);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
536
|
+
validateRendererOptions(options, SVG_OPTIONS, "renderToSVG options");
|
|
522
537
|
for (const key of SVG_OPTIONS) {
|
|
523
538
|
if (
|
|
524
539
|
options[key] !== undefined &&
|
package/src/renderers/text.js
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const MAX_RENDERER_NUMERIC_FONT_WEIGHT = 900;
|
|
3
|
-
|
|
4
|
-
export function normalizeRendererFontWeight(fontWeight) {
|
|
5
|
-
if (typeof fontWeight !== "number") return fontWeight;
|
|
6
|
-
const rounded = Math.round(fontWeight / 100) * 100;
|
|
7
|
-
return Math.min(
|
|
8
|
-
MAX_RENDERER_NUMERIC_FONT_WEIGHT,
|
|
9
|
-
Math.max(MIN_RENDERER_NUMERIC_FONT_WEIGHT, rounded)
|
|
10
|
-
);
|
|
11
|
-
}
|
|
1
|
+
export { normalizeRendererFontWeight } from "../core/font.js";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { annotateError } from "../core/diagnostics.js";
|
|
2
|
+
|
|
1
3
|
export function findCoordinate(program, id) {
|
|
2
4
|
return program.semanticSpec.coordinates.find(coordinate => coordinate.id === id);
|
|
3
5
|
}
|
|
@@ -8,6 +10,6 @@ export function hasCoordinate(program, id) {
|
|
|
8
10
|
|
|
9
11
|
export function requireCoordinate(program, id) {
|
|
10
12
|
const coordinate = findCoordinate(program, id);
|
|
11
|
-
if (coordinate === undefined) throw new Error(`Unknown coordinate "${id}".`);
|
|
13
|
+
if (coordinate === undefined) throw annotateError(new Error(`Unknown coordinate "${id}".`), { code: "missing-resource", resourceId: id });
|
|
12
14
|
return coordinate;
|
|
13
15
|
}
|
|
@@ -1,7 +1,40 @@
|
|
|
1
|
+
import { annotateError } from "../core/diagnostics.js";
|
|
2
|
+
|
|
1
3
|
export function findDataset(program, id) {
|
|
2
4
|
return program.semanticSpec.datasets.find(dataset => dataset.id === id);
|
|
3
5
|
}
|
|
4
6
|
|
|
7
|
+
function currentOwnerMatches(program, id) {
|
|
8
|
+
const families = program.materializationConfigs.data ?? {};
|
|
9
|
+
return Object.entries(families).flatMap(([family, owners]) =>
|
|
10
|
+
Object.entries(owners ?? {}).flatMap(([owner, config]) =>
|
|
11
|
+
owner === id && typeof config?.current === "string"
|
|
12
|
+
? [{ family, owner, current: config.current }]
|
|
13
|
+
: []
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hasDatasetOwner(program, id) {
|
|
19
|
+
return currentOwnerMatches(program, id).length > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveDatasetReference(program, id, label = "Dataset") {
|
|
23
|
+
const matches = currentOwnerMatches(program, id);
|
|
24
|
+
if (matches.length > 1) {
|
|
25
|
+
throw annotateError(new Error(`${label} logical owner "${id}" is ambiguous.`), { code: "ambiguous-resource", resourceId: id, candidates: matches.map(match => match.current) });
|
|
26
|
+
}
|
|
27
|
+
const resolved = matches.length === 1 ? matches[0].current : id;
|
|
28
|
+
const dataset = findDataset(program, resolved);
|
|
29
|
+
if (dataset === undefined) {
|
|
30
|
+
const kind = label.toLowerCase().includes("source")
|
|
31
|
+
? "source dataset"
|
|
32
|
+
: "dataset";
|
|
33
|
+
throw annotateError(new Error(`Unknown ${kind} "${id}" does not exist.`), { code: "missing-resource", resourceId: id });
|
|
34
|
+
}
|
|
35
|
+
return dataset;
|
|
36
|
+
}
|
|
37
|
+
|
|
5
38
|
export function hasDataset(program, id) {
|
|
6
39
|
return findDataset(program, id) !== undefined;
|
|
7
40
|
}
|
|
@@ -12,19 +45,19 @@ export function findDatasetConsumer(program, source) {
|
|
|
12
45
|
|
|
13
46
|
export function requireDataset(program, id, label = `Dataset "${id}"`) {
|
|
14
47
|
const dataset = findDataset(program, id);
|
|
15
|
-
if (dataset === undefined) throw new Error(`${label} does not exist.`);
|
|
48
|
+
if (dataset === undefined) throw annotateError(new Error(`${label} does not exist.`), { code: "missing-resource", resourceId: id });
|
|
16
49
|
return dataset;
|
|
17
50
|
}
|
|
18
51
|
|
|
19
52
|
export function requireMaterializedDataset(program, id) {
|
|
20
|
-
const dataset =
|
|
21
|
-
if (dataset === undefined) throw new Error(`Unknown dataset "${id}".`);
|
|
53
|
+
const dataset = resolveDatasetReference(program, id, "Dataset");
|
|
54
|
+
if (dataset === undefined) throw annotateError(new Error(`Unknown dataset "${id}".`), { code: "missing-resource", resourceId: id });
|
|
22
55
|
if (!Array.isArray(dataset.values)) {
|
|
23
|
-
throw new Error(
|
|
56
|
+
throw annotateError(new Error(
|
|
24
57
|
`Dataset "${id}" requires materialized values. ` +
|
|
25
58
|
"createDerivedData stores a definition only; use the corresponding " +
|
|
26
59
|
"value-producing data action before creating a chart or mark."
|
|
27
|
-
);
|
|
60
|
+
), { code: "incompatible-resource", resourceId: id });
|
|
28
61
|
}
|
|
29
62
|
return dataset;
|
|
30
63
|
}
|
package/src/selectors/layers.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import { annotateError } from "../core/diagnostics.js";
|
|
2
|
+
|
|
1
3
|
export function findLayer(program, id) {
|
|
2
4
|
return program.semanticSpec.layers.find(layer => layer.id === id);
|
|
3
5
|
}
|
|
4
6
|
|
|
7
|
+
export function findLayerMatching(program, predicate) {
|
|
8
|
+
return program.semanticSpec.layers.find(predicate);
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
export function hasLayer(program, id) {
|
|
6
12
|
return findLayer(program, id) !== undefined;
|
|
7
13
|
}
|
|
8
14
|
|
|
9
15
|
export function requireLayer(program, id, label = `Layer "${id}"`) {
|
|
10
16
|
const layer = findLayer(program, id);
|
|
11
|
-
if (layer === undefined) throw new Error(`${label} does not exist.`);
|
|
17
|
+
if (layer === undefined) throw annotateError(new Error(`${label} does not exist.`), { code: "missing-resource", resourceId: id });
|
|
12
18
|
return layer;
|
|
13
19
|
}
|
|
14
20
|
|
|
@@ -22,12 +28,12 @@ export function resolveEligibleLayer(program, {
|
|
|
22
28
|
const candidates = program.semanticSpec.layers.filter(predicate);
|
|
23
29
|
if (target !== undefined) {
|
|
24
30
|
const selected = candidates.find(layer => layer.id === target);
|
|
25
|
-
if (selected === undefined) throw new Error(`Unknown ${label} ${targetOption} "${target}".`);
|
|
31
|
+
if (selected === undefined) throw annotateError(new Error(`Unknown ${label} ${targetOption} "${target}".`), { code: findLayer(program, target) ? "incompatible-resource" : "missing-resource", resourceId: target, optionPath: targetOption });
|
|
26
32
|
return selected;
|
|
27
33
|
}
|
|
28
34
|
const selected = candidates.find(layer => layer.id === current);
|
|
29
35
|
if (selected !== undefined) return selected;
|
|
30
36
|
if (candidates.length === 1) return candidates[0];
|
|
31
|
-
if (candidates.length === 0) throw new Error(`${label} requires an eligible layer.`);
|
|
32
|
-
throw new Error(`${label} ${targetOption} is ambiguous; provide ${targetOption}.`);
|
|
37
|
+
if (candidates.length === 0) throw annotateError(new Error(`${label} requires an eligible layer.`), { code: "missing-resource", optionPath: targetOption });
|
|
38
|
+
throw annotateError(new Error(`${label} ${targetOption} is ambiguous; provide ${targetOption}.`), { code: "ambiguous-resource", optionPath: targetOption, candidates: candidates.map(layer => layer.id) });
|
|
33
39
|
}
|