ggaction 0.0.17 → 0.0.18
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 +46 -0
- package/README.md +2 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +3 -3
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +16 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +4 -4
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/edit.js +1 -1
- package/src/actions/data/regression.js +2 -1
- package/src/actions/data/shared.js +3 -2
- package/src/actions/data/stack.js +3 -1
- package/src/actions/encodings/appearance.js +37 -29
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +1 -1
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +3 -2
- package/src/actions/encodings/ranged.js +17 -0
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/text.js +6 -3
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +10 -4
- package/src/actions/marks/bar/edit.js +8 -4
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +7 -4
- package/src/actions/marks/text/actions.js +17 -5
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +3 -3
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +9 -4
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +8 -3
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +2 -2
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +81 -27
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +2 -12
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +3 -7
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +7 -1
- package/src/grammar/regression/models.js +83 -55
- package/src/grammar/regression/parameters.js +14 -3
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -197
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +2 -1
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +3 -26
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +22 -1
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +5 -3
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +13 -4
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +18 -5
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/svg.js +4 -1
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +1 -0
- package/types/index.d.ts +7 -0
- package/types/program.d.ts +347 -85
|
@@ -27,11 +27,13 @@ export function resolveSeriesLayoutDomain({
|
|
|
27
27
|
seriesLayouts[index] === undefined &&
|
|
28
28
|
!isSourceOwnedText(consumer.layer)
|
|
29
29
|
);
|
|
30
|
+
const layout = activeLayouts[0].layout;
|
|
30
31
|
const compatibleDirect = directConsumers.every(({ consumer }) =>
|
|
31
32
|
layoutConsumers.every(({ consumer: layoutConsumer }) => {
|
|
32
33
|
const aggregate = layoutConsumer.encoding.aggregate;
|
|
33
34
|
const directEncoding = consumer.encoding;
|
|
34
35
|
return directEncoding.fieldType === "quantitative" && (
|
|
36
|
+
layout === "overlay" ||
|
|
35
37
|
Object.hasOwn(directEncoding, "datum") ||
|
|
36
38
|
(
|
|
37
39
|
aggregate !== "count" &&
|
|
@@ -45,7 +47,6 @@ export function resolveSeriesLayoutDomain({
|
|
|
45
47
|
`Series layout scale "${id}" cannot be shared with another policy.`
|
|
46
48
|
);
|
|
47
49
|
}
|
|
48
|
-
const layout = activeLayouts[0].layout;
|
|
49
50
|
const values = [
|
|
50
51
|
...activeLayouts.flatMap(item => item.values),
|
|
51
52
|
...directConsumers.flatMap(item => item.values)
|
|
@@ -160,7 +160,7 @@ function resolveContinuousScale({
|
|
|
160
160
|
}) {
|
|
161
161
|
return {
|
|
162
162
|
type: isSequentialColor
|
|
163
|
-
?
|
|
163
|
+
? scale.type
|
|
164
164
|
: isOrdinalAppearance
|
|
165
165
|
? validateOrdinalScaleType(scale.type)
|
|
166
166
|
: scale.type === "time"
|
|
@@ -377,7 +377,7 @@ export function resolveScaleMaterialization(options) {
|
|
|
377
377
|
validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales });
|
|
378
378
|
if (channel === "shape" && domain.length > range.length) {
|
|
379
379
|
throw new Error(
|
|
380
|
-
`Shape scale "${id}" requires at least one
|
|
380
|
+
`Shape scale "${id}" requires at least one shape entry per domain value.`
|
|
381
381
|
);
|
|
382
382
|
}
|
|
383
383
|
|
|
@@ -440,7 +440,9 @@ export function resolveScaleMaterialization(options) {
|
|
|
440
440
|
const temporalBarBand = resolveTemporalBarBand(
|
|
441
441
|
valuesByConsumer,
|
|
442
442
|
domain,
|
|
443
|
-
range
|
|
443
|
+
range,
|
|
444
|
+
markConfigs,
|
|
445
|
+
Array.isArray(scale.range)
|
|
444
446
|
);
|
|
445
447
|
if (temporalBarBand !== undefined) {
|
|
446
448
|
resolvedScale = {
|
|
@@ -16,7 +16,7 @@ export function resolveArcItems(program, layer, dataset) {
|
|
|
16
16
|
thetaScale,
|
|
17
17
|
...(radiusScale === undefined ? {} : { radiusScale }),
|
|
18
18
|
frame,
|
|
19
|
-
|
|
19
|
+
innerRadius: program.markConfigs[layer.id]?.innerRadius ?? 0
|
|
20
20
|
});
|
|
21
21
|
const graphic = program.graphicSpec.objects[layer.id];
|
|
22
22
|
const stroke = layer.encoding?.stroke;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasRectRadius } from "../../../grammar/roundedRect.js";
|
|
1
2
|
import { deriveSeriesBarCells } from "../../../grammar/bars/aggregate.js";
|
|
2
3
|
import { readSeriesIdentity } from "../../../grammar/pathSeries.js";
|
|
3
4
|
import {
|
|
@@ -128,9 +129,12 @@ function histogramDefinitions(program, layer, dataset) {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
function rangedDefinitions(layer, dataset) {
|
|
132
|
+
const measure = resolveBarGrain(layer) === BAR_GRAINS.centered ? resolveBarChannels(layer).measure : undefined;
|
|
131
133
|
return dataset.values.map(row => ({
|
|
132
134
|
fields: ownFields(row),
|
|
133
|
-
channels: channelMapFromRow(row, layer),
|
|
135
|
+
channels: { ...channelMapFromRow(row, layer),
|
|
136
|
+
...(measure !== undefined && layer.encoding[`${measure}2`] === undefined
|
|
137
|
+
? { [measure]: 0, [`${measure}2`]: row[layer.encoding[measure].field] } : {}) },
|
|
134
138
|
members: [row]
|
|
135
139
|
}));
|
|
136
140
|
}
|
|
@@ -205,16 +209,13 @@ export function resolveBarItems(program, layer, dataset, selectionGrain) {
|
|
|
205
209
|
? histogramDefinitions(program, layer, dataset)
|
|
206
210
|
: grain === BAR_GRAINS.aggregate
|
|
207
211
|
? aggregateCellDefinitions(program, layer, dataset)
|
|
208
|
-
:
|
|
212
|
+
: [BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(grain)
|
|
209
213
|
? rangedDefinitions(layer, dataset)
|
|
210
214
|
: undefined;
|
|
211
215
|
if (definitions === undefined) {
|
|
212
216
|
throw new Error(`Bar mark "${layer.id}" is incomplete for selection.`);
|
|
213
217
|
}
|
|
214
|
-
const graphicTypes =
|
|
215
|
-
program.markConfigs[layer.id]?.barAppearance ?? {},
|
|
216
|
-
"cornerRadius"
|
|
217
|
-
) ? ["rect", "path"] : "rect";
|
|
218
|
+
const graphicTypes = hasRectRadius(program.markConfigs[layer.id]?.barAppearance) ? ["rect", "path"] : "rect";
|
|
218
219
|
if (selectionGrain === "stack") {
|
|
219
220
|
return finalizeItems(
|
|
220
221
|
program,
|
|
@@ -59,15 +59,16 @@ export function resolveLineItems(program, layer, dataset) {
|
|
|
59
59
|
const derived = deriveLineSeries(
|
|
60
60
|
dataset.values,
|
|
61
61
|
layer,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
: {
|
|
62
|
+
{
|
|
63
|
+
xDomain: program.resolvedScales[x?.scale]?.domain,
|
|
64
|
+
...(x?.bin === undefined ? {} : {
|
|
65
65
|
xBinBoundaries: resolveLineBins(
|
|
66
66
|
dataset.values,
|
|
67
67
|
layer,
|
|
68
68
|
requireSemanticScale(program, x.scale)
|
|
69
69
|
).boundaries
|
|
70
|
-
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
71
72
|
);
|
|
72
73
|
return finalizeItems(
|
|
73
74
|
program,
|
|
@@ -82,8 +83,8 @@ export function resolveAreaItems(program, layer, dataset) {
|
|
|
82
83
|
const transform = findUpstreamTransform(program, dataset, "density");
|
|
83
84
|
const derived = transform === undefined
|
|
84
85
|
? layer.encoding?.y?.stack === "center"
|
|
85
|
-
? deriveCenteredAreaSeries(dataset.values, layer)
|
|
86
|
-
: deriveAreaSeries(dataset.values, layer)
|
|
86
|
+
? deriveCenteredAreaSeries(dataset.values, layer, { xDomain: program.resolvedScales[layer.encoding.x?.scale]?.domain, yDomain: program.resolvedScales[layer.encoding.y?.scale]?.domain })
|
|
87
|
+
: deriveAreaSeries(dataset.values, layer, { xDomain: program.resolvedScales[layer.encoding.x?.scale]?.domain, yDomain: program.resolvedScales[layer.encoding.y?.scale]?.domain })
|
|
87
88
|
: deriveDensityAreaSeries(dataset.values, layer, transform);
|
|
88
89
|
return finalizeItems(
|
|
89
90
|
program,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { rectUsesFields } from "../../../grammar/rects.js";
|
|
2
|
-
import { readRectItemGeometry } from "../../../grammar/roundedRect.js";
|
|
2
|
+
import { hasRectRadius, readRectItemGeometry } from "../../../grammar/roundedRect.js";
|
|
3
3
|
import { resolveRectRows } from "../../rect.js";
|
|
4
4
|
import {
|
|
5
5
|
channelMapFromRow,
|
|
@@ -40,7 +40,7 @@ function resolveGradientPlotItems(program, layer, dataset) {
|
|
|
40
40
|
|
|
41
41
|
export function resolveRectItems(program, layer, dataset) {
|
|
42
42
|
const graphic = program.graphicSpec.objects[layer.id];
|
|
43
|
-
const rounded =
|
|
43
|
+
const rounded = hasRectRadius(program.markConfigs[layer.id]);
|
|
44
44
|
const compatibleCollection = rounded && graphic?.type === "collection" &&
|
|
45
45
|
graphic.items?.every(item => ["rect", "path"].includes(item.type));
|
|
46
46
|
if (
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
resolveMarkLabelPlacement
|
|
6
6
|
} from "../layout/labels.js";
|
|
7
7
|
import { resolveMarkLabelValues } from "../grammar/markLabels.js";
|
|
8
|
-
import { formatTextValue } from "../grammar/text.js";
|
|
8
|
+
import { formatTextValue, resolveTextLines } from "../grammar/text.js";
|
|
9
9
|
import { normalizePositionDatum } from "../grammar/positionDatum.js";
|
|
10
10
|
import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
|
|
11
11
|
import { findDataset } from "../selectors/datasets.js";
|
|
@@ -274,12 +274,14 @@ function concreteTextItem(config, position, text, { offsets = true } = {}) {
|
|
|
274
274
|
if (text === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
275
275
|
return undefined;
|
|
276
276
|
}
|
|
277
|
+
const lines = resolveTextLines(text, config);
|
|
277
278
|
return {
|
|
278
279
|
type: "text",
|
|
279
280
|
properties: {
|
|
280
281
|
x: x + (offsets ? config.dx : 0),
|
|
281
282
|
y: y + (offsets ? config.dy : 0),
|
|
282
283
|
text,
|
|
284
|
+
...(lines === undefined ? {} : { lines }),
|
|
283
285
|
fill: config.fill,
|
|
284
286
|
opacity: config.opacity,
|
|
285
287
|
fontSize: config.fontSize,
|
|
@@ -310,6 +312,11 @@ function relativeLuminance(color) {
|
|
|
310
312
|
}
|
|
311
313
|
|
|
312
314
|
function sourceTextConfig(config, source, item) {
|
|
315
|
+
if (config.inheritColor && config.fillExplicit !== true) {
|
|
316
|
+
const fill = item.properties[config.inheritColor];
|
|
317
|
+
if (typeof fill !== "string") throw new Error(`Source mark "${source.id}" has no resolved ${config.inheritColor} color.`);
|
|
318
|
+
return { ...config, fill };
|
|
319
|
+
}
|
|
313
320
|
if (
|
|
314
321
|
!["arc", "rect"].includes(source.mark.type) ||
|
|
315
322
|
config.fillExplicit === true
|
|
@@ -383,6 +390,7 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
383
390
|
geometry: sourcePlacementGeometry(program, source, item),
|
|
384
391
|
text: {
|
|
385
392
|
text,
|
|
393
|
+
lines: resolveTextLines(text, resolvedConfig),
|
|
386
394
|
fontSize: resolvedConfig.fontSize,
|
|
387
395
|
fontFamily: resolvedConfig.fontFamily,
|
|
388
396
|
fontWeight: resolvedConfig.fontWeight,
|
|
@@ -396,7 +404,7 @@ function resolveSourceTextItems(program, layer, config) {
|
|
|
396
404
|
if (!resolved.visible) return [];
|
|
397
405
|
if (
|
|
398
406
|
resolved.anchor.startsWith("outside") &&
|
|
399
|
-
config.fillExplicit !== true
|
|
407
|
+
config.fillExplicit !== true && !config.inheritColor
|
|
400
408
|
) {
|
|
401
409
|
resolvedConfig = config;
|
|
402
410
|
}
|
|
@@ -420,7 +428,7 @@ function resolveRowTextItems(program, layer, config) {
|
|
|
420
428
|
throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
|
|
421
429
|
}
|
|
422
430
|
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
423
|
-
const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text];
|
|
431
|
+
const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color];
|
|
424
432
|
const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
|
|
425
433
|
? dataset.values.length
|
|
426
434
|
: 1;
|
|
@@ -454,9 +462,10 @@ function resolveRowTextItems(program, layer, config) {
|
|
|
454
462
|
const rows = length === 1 && dataset.values.length === 0
|
|
455
463
|
? [undefined]
|
|
456
464
|
: dataset.values.slice(0, length);
|
|
465
|
+
const fills = resolveRowEncodingValues(program, layer, dataset, "color");
|
|
457
466
|
return rows.flatMap((row, index) => {
|
|
458
467
|
const concrete = concreteItem(
|
|
459
|
-
config,
|
|
468
|
+
fills === undefined ? config : { ...config, fill: fills[index] },
|
|
460
469
|
{ x: x[index], y: y[index] },
|
|
461
470
|
contentValue(layer.encoding.text, row),
|
|
462
471
|
layer.encoding.text.format
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { freezeOwned } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
// Reconcile optional derived line geometry without recreating its text graphic.
|
|
4
|
+
// The owning wrapped domain materializer records the transition.
|
|
5
|
+
export function materializeTextLines(program, id, lines) {
|
|
6
|
+
if (lines !== undefined) return program.editGraphics({ target: id, property: "lines", value: lines });
|
|
7
|
+
const graphic = program.graphicSpec.objects[id];
|
|
8
|
+
if (!Object.hasOwn(graphic.properties, "lines")) return program;
|
|
9
|
+
const { lines: _lines, ...properties } = graphic.properties;
|
|
10
|
+
return program._clone({ graphicSpec: freezeOwned({ ...program.graphicSpec,
|
|
11
|
+
objects: freezeOwned({ ...program.graphicSpec.objects,
|
|
12
|
+
[id]: freezeOwned({ ...graphic, properties: freezeOwned(properties) })
|
|
13
|
+
})
|
|
14
|
+
}) });
|
|
15
|
+
}
|
package/src/persistence/codec.js
CHANGED
|
@@ -5,7 +5,7 @@ export function invalidSnapshot(message, optionPath) {
|
|
|
5
5
|
return annotateError(new TypeError(message), { code: "unsupported-format", optionPath });
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function encodeValue(value, path = "payload", ancestors = new WeakSet()) {
|
|
8
|
+
export function encodeValue(value, path = "payload", ancestors = new WeakSet(), shared) {
|
|
9
9
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
10
10
|
if (value === undefined) return ["undefined"];
|
|
11
11
|
if (typeof value === "bigint") return ["bigint", value.toString()];
|
|
@@ -17,6 +17,7 @@ export function encodeValue(value, path = "payload", ancestors = new WeakSet())
|
|
|
17
17
|
if (ancestors.has(value)) throw invalidSnapshot(`Cannot serialize a cycle at ${path}.`, path);
|
|
18
18
|
if (!Array.isArray(value) && !isPlainObject(value)) throw invalidSnapshot(`Cannot serialize a class instance at ${path}.`, path);
|
|
19
19
|
if (Object.getOwnPropertySymbols(value).length) throw invalidSnapshot(`Cannot serialize symbol keys at ${path}.`, path);
|
|
20
|
+
if (shared?.seen.has(value)) return shared.seen.get(value);
|
|
20
21
|
const keys = Object.getOwnPropertyNames(value);
|
|
21
22
|
if (Array.isArray(value) ? keys.some(key => key !== "length" && !(/^(0|[1-9]\d*)$/.test(key) && Number(key) < value.length)) :
|
|
22
23
|
keys.some(key => !Object.getOwnPropertyDescriptor(value, key).enumerable)) {
|
|
@@ -25,18 +26,48 @@ export function encodeValue(value, path = "payload", ancestors = new WeakSet())
|
|
|
25
26
|
ancestors.add(value);
|
|
26
27
|
const result = Array.isArray(value)
|
|
27
28
|
? ["array", Array.from({ length: value.length }, (_, index) => Object.hasOwn(value, index)
|
|
28
|
-
? encodeValue(value[index], `${path}[${index}]`, ancestors) : ["hole"])]
|
|
29
|
-
: ["object", Object.entries(value).map(([key, child]) => [key, encodeValue(child, `${path}.${key}`, ancestors)])];
|
|
29
|
+
? encodeValue(value[index], `${path}[${index}]`, ancestors, shared) : ["hole"])]
|
|
30
|
+
: ["object", Object.entries(value).map(([key, child]) => [key, encodeValue(child, `${path}.${key}`, ancestors, shared)])];
|
|
30
31
|
ancestors.delete(value);
|
|
32
|
+
if (shared !== undefined && Array.isArray(value) && value.length >= 32) {
|
|
33
|
+
const key = JSON.stringify(result);
|
|
34
|
+
let index = shared.keys.get(key);
|
|
35
|
+
if (index === undefined) {
|
|
36
|
+
index = shared.arrays.length;
|
|
37
|
+
shared.keys.set(key, index);
|
|
38
|
+
shared.arrays.push(result);
|
|
39
|
+
}
|
|
40
|
+
const reference = ["reference", index];
|
|
41
|
+
shared.seen.set(value, reference);
|
|
42
|
+
return reference;
|
|
43
|
+
}
|
|
31
44
|
return result;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
export function
|
|
47
|
+
export function encodeSharedValue(value) {
|
|
48
|
+
const shared = { arrays: [], keys: new Map(), seen: new WeakMap() };
|
|
49
|
+
const root = encodeValue(value, "payload", new WeakSet(), shared);
|
|
50
|
+
return ["shared", shared.arrays, root];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function decodeValue(value, path = "payload", references) {
|
|
35
54
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
36
55
|
if (typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return value;
|
|
37
56
|
const invalid = () => { throw invalidSnapshot(`Invalid encoded value at ${path}.`, path); };
|
|
38
57
|
if (!Array.isArray(value)) return invalid();
|
|
39
58
|
const [tag, payload] = value;
|
|
59
|
+
if (tag === "shared" && references === undefined && value.length === 3 && Array.isArray(payload)) {
|
|
60
|
+
const arrays = [];
|
|
61
|
+
for (const [index, encoded] of payload.entries()) {
|
|
62
|
+
if (!Array.isArray(encoded) || encoded[0] !== "array") return invalid();
|
|
63
|
+
arrays.push(decodeValue(encoded, `${path}.arrays[${index}]`, arrays));
|
|
64
|
+
}
|
|
65
|
+
return decodeValue(value[2], path, arrays);
|
|
66
|
+
}
|
|
67
|
+
if (tag === "reference" && references !== undefined && value.length === 2 &&
|
|
68
|
+
Number.isInteger(payload) && payload >= 0 && payload < references.length) {
|
|
69
|
+
return references[payload];
|
|
70
|
+
}
|
|
40
71
|
if (tag === "undefined" && value.length === 1) return undefined;
|
|
41
72
|
if (value.length !== 2) return invalid();
|
|
42
73
|
if (tag === "bigint" && typeof payload === "string" && /^(?:0|-?[1-9]\d*)$/.test(payload)) return BigInt(payload);
|
|
@@ -47,7 +78,7 @@ export function decodeValue(value, path = "payload") {
|
|
|
47
78
|
const result = new Array(payload.length);
|
|
48
79
|
payload.forEach((child, index) => {
|
|
49
80
|
if (Array.isArray(child) && child.length === 1 && child[0] === "hole") return;
|
|
50
|
-
result[index] = decodeValue(child, `${path}[${index}]
|
|
81
|
+
result[index] = decodeValue(child, `${path}[${index}]`, references ?? []);
|
|
51
82
|
});
|
|
52
83
|
return result;
|
|
53
84
|
}
|
|
@@ -56,7 +87,7 @@ export function decodeValue(value, path = "payload") {
|
|
|
56
87
|
for (const entry of payload) {
|
|
57
88
|
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || Object.hasOwn(result, entry[0])) return invalid();
|
|
58
89
|
Object.defineProperty(result, entry[0], {
|
|
59
|
-
value: decodeValue(entry[1], `${path}.${entry[0]}
|
|
90
|
+
value: decodeValue(entry[1], `${path}.${entry[0]}`, references ?? []), enumerable: true, writable: true, configurable: true
|
|
60
91
|
});
|
|
61
92
|
}
|
|
62
93
|
return result;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
|
|
2
|
+
import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
|
|
3
|
+
import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
|
|
4
|
+
import { isContinuousColorScaleType } from "../grammar/scales/types.js";
|
|
5
|
+
import { validateLegendGroupState } from "../grammar/legendGroups.js";
|
|
6
|
+
import { validateLegendGradient } from "../grammar/legendGradient.js";
|
|
1
7
|
import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
|
|
2
8
|
import { isPlainObject } from "../core/immutable.js";
|
|
3
9
|
import { validateUserId } from "../core/identifiers.js";
|
|
@@ -142,6 +148,44 @@ export function validateProgramState(program, path) {
|
|
|
142
148
|
for (const key of ["resolvedScales", "materializationConfigs", "context"]) requireObject(program[key], `${path}.${key}`);
|
|
143
149
|
for (const key of ["marks", "guides"]) requireObject(program.materializationConfigs[key], `${path}.materializationConfigs.${key}`);
|
|
144
150
|
if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
|
|
151
|
+
if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
|
|
152
|
+
for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
|
|
153
|
+
if (config.titleStyle !== undefined) {
|
|
154
|
+
validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
|
|
155
|
+
validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
|
|
156
|
+
}
|
|
157
|
+
if (config.inferredOffset !== undefined && typeof config.inferredOffset !== "boolean") throw invalidSnapshot("Invalid legend offset inference.", path);
|
|
158
|
+
if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols") {
|
|
159
|
+
validateLegendGradient(config.gradient, "Stored legend gradient");
|
|
160
|
+
}
|
|
161
|
+
if (config.representation === undefined) continue;
|
|
162
|
+
if (kind !== "gradient" || !["symbols", "gradient"].includes(config.representation)) throw invalidSnapshot("Invalid continuous color legend representation.", path);
|
|
163
|
+
if (config.representation === "symbols") {
|
|
164
|
+
if (config.gradient !== undefined) throw invalidSnapshot("Symbol color legend cannot store gradient dimensions.", path);
|
|
165
|
+
requireObject(config.sampling, path);
|
|
166
|
+
const sample = config.sampling;
|
|
167
|
+
exactKeys(sample, sample.mode === "values" ? ["mode", "values", "count"] : ["mode", "count"], path);
|
|
168
|
+
if (!["auto", "values"].includes(sample.mode)) throw invalidSnapshot("Invalid color legend sample mode.", path);
|
|
169
|
+
if (!Number.isInteger(sample.count) || sample.count < 2) throw invalidSnapshot("Invalid color legend sample count.", path);
|
|
170
|
+
validateGeneratedItemLimit(sample.count, "Color legend sample count");
|
|
171
|
+
const scale = program.resolvedScales[config.scale];
|
|
172
|
+
if (!isContinuousColorScaleType(scale?.type)) throw invalidSnapshot("Sampled color legend requires a continuous color scale.", path);
|
|
173
|
+
if (sample.mode === "values") {
|
|
174
|
+
requireArray(sample.values, path);
|
|
175
|
+
if (!sample.values.length || sample.values.length > 100 || sample.values.some((value, index) => !Number.isFinite(value) || index > 0 && value <= sample.values[index - 1] || value < Math.min(...scale.domain) || value > Math.max(...scale.domain) || scale.type === "log" && value <= 0)) throw invalidSnapshot("Invalid exact color legend samples.", path);
|
|
176
|
+
}
|
|
177
|
+
requireObject(config.symbol, path);
|
|
178
|
+
validateKeys(config.symbol, ["type", "radius", "stroke", "strokeWidth"], "Stored sampled color symbol");
|
|
179
|
+
if (config.symbol.type !== "point") throw invalidSnapshot("Invalid sampled color symbol type.", path);
|
|
180
|
+
validatePositiveFinite(config.symbol.radius, "Stored sampled color radius");
|
|
181
|
+
validateNonNegativeFinite(config.symbol.strokeWidth, "Stored sampled color strokeWidth");
|
|
182
|
+
if (config.symbol.stroke !== undefined) validateNonEmptyString(config.symbol.stroke, "Stored sampled color stroke");
|
|
183
|
+
if (!["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"].includes(config.position) || !["horizontal", "vertical"].includes(config.direction) || config.layout !== "edge" || !["left", "center", "right"].includes(config.align) || !["top", "left"].includes(config.titlePosition) || config.columns !== undefined && (!Number.isInteger(config.columns) || config.columns < 1)) throw invalidSnapshot("Invalid sampled color item layout.", path);
|
|
184
|
+
validatePositiveFinite(config.itemGap, "Stored sampled color itemGap");
|
|
185
|
+
validateNonNegativeFinite(config.offset, "Stored sampled color offset");
|
|
186
|
+
} else if (["sampling", "symbol", "columns", "direction", "layout"].some(key => config[key] !== undefined)) throw invalidSnapshot("Gradient color legend cannot store symbol representation settings.", path);
|
|
187
|
+
}
|
|
188
|
+
|
|
145
189
|
const selections = program.materializationConfigs.selections ?? {};
|
|
146
190
|
requireObject(selections, `${path}.materializationConfigs.selections`);
|
|
147
191
|
for (const [id, selection] of Object.entries(selections)) {
|
|
@@ -167,6 +211,9 @@ export function validateProgramState(program, path) {
|
|
|
167
211
|
}
|
|
168
212
|
for (const id of Object.keys(program.markConfigs)) {
|
|
169
213
|
if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
|
|
214
|
+
if (resources.get("mark").get(id).mark.type === "area") {
|
|
215
|
+
validateAreaPaintConfig(program.markConfigs[id]);
|
|
216
|
+
}
|
|
170
217
|
}
|
|
171
218
|
for (const kind of ["data", "mark"]) {
|
|
172
219
|
const visited = new Set();
|
package/src/persistence.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ChartProgram } from "./ChartProgram.js";
|
|
2
2
|
import { BasicChartProgram } from "./BasicChartProgram.js";
|
|
3
|
-
import { cloneAndFreeze } from "./core/immutable.js";
|
|
3
|
+
import { cloneAndFreeze, freezeOwned } from "./core/immutable.js";
|
|
4
4
|
import { hasRegisteredExtension, isBuiltinProgramClass } from "./core/extensionRegistry.js";
|
|
5
5
|
import { packageVersion } from "./version.js";
|
|
6
|
-
import { decodeValue, encodeValue, invalidSnapshot } from "./persistence/codec.js";
|
|
6
|
+
import { decodeValue, encodeValue, encodeSharedValue, invalidSnapshot } from "./persistence/codec.js";
|
|
7
7
|
import { exactKeys, requireObject, STATE_KEYS, validateGraphic, validateProgramState, validateTrace } from "./persistence/validation.js";
|
|
8
8
|
import {
|
|
9
9
|
deriveTransformSchema,
|
|
@@ -25,7 +25,7 @@ function canonicalState(program, extensions, path = "payload") {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function envelope(kind, payload, extensions = []) {
|
|
28
|
-
return JSON.stringify({ schemaVersion: kind === "editable" ?
|
|
28
|
+
return JSON.stringify({ schemaVersion: kind === "editable" ? 3 : 1, kind, packageVersion, extensions, payload: kind === "editable" ? encodeSharedValue(payload) : encodeValue(payload) });
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function readEnvelope(text, kind) {
|
|
@@ -33,7 +33,7 @@ function readEnvelope(text, kind) {
|
|
|
33
33
|
let value;
|
|
34
34
|
try { value = JSON.parse(text); } catch { throw invalidSnapshot("Snapshot must be valid JSON.", "snapshot"); }
|
|
35
35
|
exactKeys(value, ["schemaVersion", "kind", "packageVersion", "extensions", "payload"], "snapshot");
|
|
36
|
-
const versions = kind === "editable" ? [1, 2] : [1];
|
|
36
|
+
const versions = kind === "editable" ? [1, 2, 3] : [1];
|
|
37
37
|
if (!versions.includes(value.schemaVersion) || value.kind !== kind) {
|
|
38
38
|
throw invalidSnapshot(
|
|
39
39
|
`Unsupported snapshot schema or kind; expected schema ${versions.join(" or ")} ${kind}.`,
|
|
@@ -45,6 +45,9 @@ function readEnvelope(text, kind) {
|
|
|
45
45
|
value.extensions.some(name => typeof name !== "string" || !hasRegisteredExtension(name)) || (kind === "graphic" && value.extensions.length)) {
|
|
46
46
|
throw invalidSnapshot("Snapshot requires unknown, duplicate, or inapplicable extensions.", "snapshot.extensions");
|
|
47
47
|
}
|
|
48
|
+
if (value.schemaVersion < 3 && value.payload?.[0] === "shared") {
|
|
49
|
+
throw invalidSnapshot("Shared array payloads require editable snapshot version 3.", "snapshot.payload");
|
|
50
|
+
}
|
|
48
51
|
return { ...value, payload: decodeValue(value.payload) };
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -64,7 +67,7 @@ function restoreState(state, extensions, path = "payload", { requireSchemas = fa
|
|
|
64
67
|
if (requireSchemas) {
|
|
65
68
|
for (const dataset of program.semanticSpec.datasets) {
|
|
66
69
|
if (dataset.schema === undefined) {
|
|
67
|
-
throw invalidSnapshot(`Dataset "${dataset.id}" requires schema in editable snapshot version 2.`, `${path}.semanticSpec.datasets`);
|
|
70
|
+
throw invalidSnapshot(`Dataset "${dataset.id}" requires schema in editable snapshot version 2 or later.`, `${path}.semanticSpec.datasets`);
|
|
68
71
|
}
|
|
69
72
|
validateDatasetSchema(dataset.schema);
|
|
70
73
|
}
|
|
@@ -118,6 +121,15 @@ export function serializeProgram(program) {
|
|
|
118
121
|
return envelope("editable", canonicalState(program, extensions), [...extensions].sort());
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
// Decoding creates fresh plain containers. Own them once across the whole graph
|
|
125
|
+
// so child constructors retain shared immutable data instead of copying each pool.
|
|
126
|
+
function ownDecodedState(value, seen = new WeakSet()) {
|
|
127
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return;
|
|
128
|
+
seen.add(value);
|
|
129
|
+
for (const child of Object.values(value)) ownDecodedState(child, seen);
|
|
130
|
+
freezeOwned(value);
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
export function deserializeProgram(text) {
|
|
122
134
|
const stored = readEnvelope(text, "editable");
|
|
123
135
|
const extensions = new Set();
|
|
@@ -128,6 +140,7 @@ export function deserializeProgram(text) {
|
|
|
128
140
|
restoreState(payload, new Set());
|
|
129
141
|
payload = migrateStateSchemas(payload);
|
|
130
142
|
}
|
|
143
|
+
ownDecodedState(payload);
|
|
131
144
|
const program = restoreState(payload, extensions, "payload", { requireSchemas: true });
|
|
132
145
|
if (extensions.size !== stored.extensions.length || stored.extensions.some(name => !extensions.has(name))) {
|
|
133
146
|
throw invalidSnapshot("Snapshot extensions must exactly match its action traces.", "snapshot.extensions");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveCanvasSolidPaint } from "./fill.js";
|
|
1
2
|
import { requireFiniteProperty } from "./validation.js";
|
|
2
3
|
import { validateConcreteGraphicProperties } from
|
|
3
4
|
"../../grammar/schemas/concreteGraphic.js";
|
|
@@ -25,7 +26,7 @@ function drawCircle(context, child, collectionId) {
|
|
|
25
26
|
throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
context.fillStyle = properties.fill;
|
|
29
|
+
context.fillStyle = resolveCanvasSolidPaint(properties.fill);
|
|
29
30
|
context.globalAlpha = opacity;
|
|
30
31
|
context.beginPath();
|
|
31
32
|
context.arc(x, y, radius, 0, Math.PI * 2);
|
|
@@ -44,8 +45,10 @@ function drawCircle(context, child, collectionId) {
|
|
|
44
45
|
`Graphic "${graphicId}" requires a non-negative strokeWidth.`
|
|
45
46
|
);
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
if (strokeWidth > 0) {
|
|
49
|
+
applyCanvasStroke(context, properties);
|
|
50
|
+
context.stroke();
|
|
51
|
+
}
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -4,10 +4,16 @@ import {
|
|
|
4
4
|
} from "../../grammar/paint.js";
|
|
5
5
|
import { resolveCanvasGradientCoordinates } from "./native.js";
|
|
6
6
|
|
|
7
|
+
// SVG's no-paint keyword is not a valid Canvas color assignment. Assign a real
|
|
8
|
+
// transparent color so the backend cannot retain a previous opaque paint.
|
|
9
|
+
export function resolveCanvasSolidPaint(paint) {
|
|
10
|
+
return paint === "none" ? "rgba(0,0,0,0)" : paint;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
export function applyCanvasFill(context, fill, bounds, graphicId) {
|
|
8
14
|
validateFillPaint(fill, `${graphicId}.fill`);
|
|
9
15
|
if (!isLinearGradientPaint(fill)) {
|
|
10
|
-
context.fillStyle = fill;
|
|
16
|
+
context.fillStyle = resolveCanvasSolidPaint(fill);
|
|
11
17
|
return fill;
|
|
12
18
|
}
|
|
13
19
|
if (typeof context.createLinearGradient !== "function") {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveCanvasSolidPaint } from "./fill.js";
|
|
1
2
|
import { annotateError } from "../../core/diagnostics.js";
|
|
2
3
|
import { drawCircleGraphic } from "./circle.js";
|
|
3
4
|
import { drawLineGraphic } from "./line.js";
|
|
@@ -81,7 +82,7 @@ function fillCanvasBackground(context, id, canvas, width, height) {
|
|
|
81
82
|
);
|
|
82
83
|
}
|
|
83
84
|
context.globalAlpha = 1;
|
|
84
|
-
context.fillStyle = canvas.properties.background;
|
|
85
|
+
context.fillStyle = resolveCanvasSolidPaint(canvas.properties.background);
|
|
85
86
|
context.fillRect(0, 0, width, height);
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -33,6 +33,7 @@ function drawLine(context, child, collectionId) {
|
|
|
33
33
|
throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
if (strokeWidth === 0) return;
|
|
36
37
|
context.globalAlpha = opacity;
|
|
37
38
|
applyCanvasStroke(context, properties, properties.strokeDash ?? []);
|
|
38
39
|
context.beginPath();
|
|
@@ -82,7 +82,7 @@ function drawPath(context, child, collectionId) {
|
|
|
82
82
|
applyCanvasFill(context, properties.fill, bounds, graphicId);
|
|
83
83
|
context.fill();
|
|
84
84
|
}
|
|
85
|
-
if (hasStroke) {
|
|
85
|
+
if (hasStroke && strokeWidth > 0) {
|
|
86
86
|
applyCanvasStroke(context, properties, strokeDash);
|
|
87
87
|
context.stroke();
|
|
88
88
|
}
|
|
@@ -43,14 +43,17 @@ function drawRect(context, child, collectionId) {
|
|
|
43
43
|
bottom: y + height
|
|
44
44
|
}, graphicId);
|
|
45
45
|
context.fillRect(x, y, width, height);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
// Canvas ignores lineWidth = 0 and would retain a previous nonzero width.
|
|
47
|
+
if (strokeWidth > 0) {
|
|
48
|
+
applyCanvasStroke(context, properties);
|
|
49
|
+
context.beginPath();
|
|
50
|
+
context.moveTo(x, y);
|
|
51
|
+
context.lineTo(x + width, y);
|
|
52
|
+
context.lineTo(x + width, y + height);
|
|
53
|
+
context.lineTo(x, y + height);
|
|
54
|
+
context.lineTo(x, y);
|
|
55
|
+
context.stroke();
|
|
56
|
+
}
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export function drawRectGraphic(context, id, graphic) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { resolveCanvasSolidPaint } from "./fill.js";
|
|
1
2
|
import { resolveStrokeDetails } from "../../grammar/strokeStyle.js";
|
|
2
3
|
|
|
3
4
|
export function applyCanvasStroke(context, properties, strokeDash = []) {
|
|
4
5
|
const details = resolveStrokeDetails(properties);
|
|
5
|
-
context.strokeStyle = properties.stroke;
|
|
6
|
+
context.strokeStyle = resolveCanvasSolidPaint(properties.stroke);
|
|
6
7
|
context.lineWidth = properties.strokeWidth;
|
|
7
8
|
context.lineCap = details.lineCap;
|
|
8
9
|
context.lineJoin = details.lineJoin;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveCanvasSolidPaint } from "./fill.js";
|
|
1
2
|
import { requireFiniteProperty, requireStringProperty } from "./validation.js";
|
|
2
3
|
import { validateConcreteGraphicProperties } from
|
|
3
4
|
"../../grammar/schemas/concreteGraphic.js";
|
|
@@ -72,15 +73,16 @@ function drawText(context, child, collectionId) {
|
|
|
72
73
|
context.save();
|
|
73
74
|
|
|
74
75
|
try {
|
|
75
|
-
context.fillStyle = fill;
|
|
76
|
+
context.fillStyle = resolveCanvasSolidPaint(fill);
|
|
76
77
|
context.globalAlpha = opacity;
|
|
77
78
|
const rendererFontWeight = normalizeRendererFontWeight(fontWeight);
|
|
78
|
-
context.font = `${rendererFontWeight} ${fontSize}px ${fontFamily}`;
|
|
79
|
+
context.font = `${properties.fontStyle === "italic" ? "italic " : ""}${rendererFontWeight} ${fontSize}px ${fontFamily}`;
|
|
79
80
|
context.textAlign = textAlign;
|
|
80
81
|
context.textBaseline = textBaseline;
|
|
81
82
|
context.translate(x, y);
|
|
82
83
|
context.rotate(rotation);
|
|
83
|
-
context.fillText(text, 0, 0);
|
|
84
|
+
if (properties.lines === undefined) context.fillText(text, 0, 0);
|
|
85
|
+
else for (const line of properties.lines) context.fillText(line.text, line.x, line.y);
|
|
84
86
|
} finally {
|
|
85
87
|
context.restore();
|
|
86
88
|
}
|
package/src/renderers/svg.js
CHANGED
|
@@ -419,13 +419,16 @@ function serializeText(state, graphicId, properties) {
|
|
|
419
419
|
["font-family", fontFamily],
|
|
420
420
|
["font-size", formatNumber(fontSize)],
|
|
421
421
|
["font-weight", normalizeRendererFontWeight(fontWeight)],
|
|
422
|
+
["font-style", properties.fontStyle],
|
|
422
423
|
["text-anchor", TEXT_ANCHORS[textAlign]],
|
|
423
424
|
["dominant-baseline", DOMINANT_BASELINES[textBaseline]],
|
|
424
425
|
["opacity", formatNumber(opacity)],
|
|
425
426
|
["transform", rotation === 0
|
|
426
427
|
? undefined
|
|
427
428
|
: `rotate(${formatNumber(rotationDegrees)} ${formatNumber(x)} ${formatNumber(y)})`]
|
|
428
|
-
], escapeText(text)
|
|
429
|
+
], properties.lines === undefined ? escapeText(text) : properties.lines.map(line =>
|
|
430
|
+
element("tspan", [["x", formatNumber(x + line.x)], ["y", formatNumber(y + line.y)]], escapeText(line.text))
|
|
431
|
+
).join(""));
|
|
429
432
|
}
|
|
430
433
|
|
|
431
434
|
function serializeConcreteGraphic(state, id, graphic) {
|