ggaction 0.0.23 → 0.0.26
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 +65 -0
- package/README.md +2 -2
- 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 +1 -1
- package/src/accessibility.js +13 -0
- package/src/actions/canvas/actions.js +2 -1
- package/src/actions/canvas/fitting.js +68 -13
- package/src/actions/charts/beeswarm.js +2 -1
- package/src/actions/charts/ecdf.js +2 -1
- package/src/actions/charts/endpoints.js +25 -16
- package/src/actions/charts/heatmap.js +6 -4
- package/src/actions/charts/missing.js +17 -0
- package/src/actions/charts/radar.js +70 -33
- package/src/actions/charts/raincloud.js +18 -4
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/shared.js +17 -0
- package/src/actions/composition/actions.js +24 -7
- package/src/actions/composition/adjacent.js +90 -0
- package/src/actions/composition/index.js +11 -1
- package/src/actions/composition/layer.js +42 -0
- package/src/actions/composition/layerPlan.js +176 -0
- package/src/actions/composition/targets.js +105 -0
- package/src/actions/data/basicBin2d.js +1 -1
- package/src/actions/data/bin2d.js +2 -2
- package/src/actions/data/derived.js +3 -0
- package/src/actions/data/filter.js +1 -1
- package/src/actions/data/raincloudFilter.js +3 -1
- package/src/actions/encodings/appearance.js +26 -7
- package/src/actions/encodings/channels.js +1 -0
- package/src/actions/encodings/color/continuous.js +37 -34
- package/src/actions/encodings/color/index.js +57 -43
- package/src/actions/encodings/color/policy.js +15 -0
- package/src/actions/encodings/density/resolve.js +3 -2
- package/src/actions/encodings/density.js +2 -2
- package/src/actions/encodings/offset.js +2 -0
- package/src/actions/encodings/pathOrder.js +2 -0
- package/src/actions/encodings/position/areaAlignment.js +42 -0
- package/src/actions/encodings/position/index.js +37 -3
- package/src/actions/encodings/position/policies/line.js +1 -1
- package/src/actions/encodings/ranged.js +4 -2
- package/src/actions/encodings/remove.js +13 -10
- package/src/actions/encodings/ruleAppearance.js +14 -2
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/guides/axes/axes.js +2 -1
- package/src/actions/guides/axes/components.js +5 -0
- package/src/actions/guides/axes/edit.js +1 -188
- package/src/actions/guides/axes/labels.js +2 -537
- package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
- package/src/actions/guides/axes/parallel/policy.js +5 -2
- package/src/actions/guides/axes/parallel/resolve.js +3 -1
- package/src/actions/guides/axes/parallel.js +9 -4
- package/src/actions/guides/axes/tickGroups.js +16 -1
- package/src/actions/guides/axes/ticks.js +33 -24
- package/src/actions/guides/axes/titles.js +1 -389
- package/src/actions/guides/facadeLegend.js +3 -2
- package/src/actions/guides/grids/grid.js +3 -2
- package/src/actions/guides/grids/resolve.js +2 -1
- package/src/actions/guides/guides.js +3 -2
- package/src/actions/guides/legends/automatic.js +28 -0
- package/src/actions/guides/legends/blocks.js +15 -21
- package/src/actions/guides/legends/categorical/actions.js +1 -1
- package/src/actions/guides/legends/categorical/components.js +4 -3
- package/src/actions/guides/legends/categorical/layout.js +5 -2
- package/src/actions/guides/legends/categorical/options.js +9 -14
- package/src/actions/guides/legends/categorical/recipes.js +3 -0
- package/src/actions/guides/legends/categorical/resolve.js +7 -5
- package/src/actions/guides/legends/colorSize.js +51 -14
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -1
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -2
- package/src/actions/guides/legends/remove.js +18 -4
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/transition.js +15 -6
- package/src/actions/guides/polar/axes/facade.js +4 -2
- package/src/actions/guides/polar/axes/labels.js +6 -1
- package/src/actions/guides/polar/axes/shared.js +4 -4
- package/src/actions/guides/polar/axes/titles.js +13 -2
- package/src/actions/guides/polar/grids.js +6 -3
- package/src/actions/guides/tickValues.js +7 -1
- package/src/actions/index.js +2 -0
- package/src/actions/marks/area/actions.js +9 -2
- package/src/actions/marks/area/materialize.js +1 -1
- package/src/actions/marks/bar/materialize.js +4 -4
- package/src/actions/marks/lifecycle.js +14 -0
- package/src/actions/marks/line/actions.js +3 -0
- package/src/actions/marks/line/materialize.js +4 -2
- package/src/actions/marks/point/edit.js +2 -0
- package/src/actions/marks/point/jitter.js +2 -0
- package/src/actions/marks/point/materialize.js +5 -4
- package/src/actions/marks/point/packing.js +2 -0
- package/src/actions/marks/references.js +5 -3
- package/src/actions/marks/remove.js +16 -0
- package/src/actions/marks/shared.js +2 -1
- package/src/actions/marks/text/actions.js +5 -2
- package/src/actions/overlays/index.js +168 -0
- package/src/actions/overlays/snapshot.js +45 -0
- package/src/actions/primitives/editGraphics.js +21 -27
- package/src/actions/primitives/semanticAction.js +4 -5
- package/src/actions/primitives/semanticValidation/guide.js +1 -0
- package/src/actions/primitives/semanticValidation/layer.js +7 -0
- package/src/actions/scales/consumers/common.js +10 -0
- package/src/actions/scales/consumers/families.js +1 -1
- package/src/actions/scales/preview.js +1 -1
- package/src/actions/selection/actions.js +63 -11
- package/src/actions/titles/actions.js +3 -14
- package/src/actions/titles/resolve.js +23 -10
- package/src/core/action.js +41 -25
- package/src/core/compositionScales.js +12 -0
- package/src/core/compositionState.js +20 -3
- package/src/core/facetGap.js +44 -0
- package/src/core/immutable.js +11 -6
- package/src/core/programState.js +16 -0
- package/src/core/resourceReferences.js +19 -3
- package/src/core/validation.js +7 -0
- package/src/core/vocabulary.js +8 -2
- package/src/grammar/actionTargets.js +24 -0
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bin2d.js +41 -4
- package/src/grammar/colorSizeLegend.js +56 -5
- package/src/grammar/displayLabels.js +10 -0
- package/src/grammar/facetHeaders.js +1 -1
- package/src/grammar/facets/dependencies.js +10 -3
- package/src/grammar/facets/guides.js +2 -1
- package/src/grammar/facets/legendIntent.js +7 -5
- package/src/grammar/legendBlocks.js +1 -1
- package/src/grammar/legendCombinations.js +1 -1
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendItemsOffset.js +12 -0
- package/src/grammar/legendOverflow.js +25 -0
- package/src/grammar/lineSeries.js +1 -1
- package/src/grammar/markSelection.js +23 -0
- package/src/grammar/overlay.js +51 -0
- package/src/grammar/parallelCoordinates.js +1 -208
- package/src/grammar/pathCommands.js +8 -2
- package/src/grammar/pathSeries.js +1 -1
- package/src/grammar/plotComposition.js +152 -0
- package/src/grammar/pointShapes.js +18 -0
- package/src/grammar/scales/transformed.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +8 -1
- package/src/grammar/schemas/graphicBounds.js +8 -1
- package/src/grammar/schemas/graphicTree.js +18 -2
- package/src/grammar/schemas/semanticPath.js +3 -0
- package/src/grammar/strokeStyle.js +14 -8
- package/src/grammar/text.js +72 -0
- package/src/inspection.js +1 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/composition.js +32 -10
- package/src/layout/compositionLegend.js +91 -0
- package/src/layout/facetContentFlow.js +15 -7
- package/src/layout/facetHeaderTitles.js +1 -1
- package/src/layout/facets.js +1 -1
- package/src/layout/labels.js +55 -27
- package/src/layout/legend.js +3 -2
- package/src/layout/legendGroup.js +1 -1
- package/src/layout/legendItems.js +7 -6
- package/src/layout/legendLane.js +1 -1
- package/src/layout/legendText.js +1 -1
- package/src/layout/title.js +1 -1
- package/src/materialization/adjacentSource.js +27 -0
- package/src/materialization/composition.js +165 -52
- package/src/materialization/compositionLegends.js +120 -0
- package/src/materialization/compositionScales.js +125 -0
- package/src/materialization/compositionTitle.js +96 -0
- package/src/materialization/facetGuides/edit.js +22 -2
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +4 -3
- package/src/materialization/facetGuides/ownerLayout.js +12 -6
- package/src/materialization/facetGuides/placement.js +19 -22
- package/src/materialization/facetGuides/preparation.js +17 -5
- package/src/materialization/facetHeaders.js +5 -1
- package/src/materialization/facets.js +1 -1
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/guides/visibility.js +105 -0
- package/src/materialization/legendClip.js +1 -1
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legendItemsOffset.js +68 -0
- package/src/materialization/legendSuppression.js +22 -0
- package/src/materialization/legends.js +3 -2
- package/src/materialization/marks/capabilities.js +4 -0
- package/src/materialization/marks/index.js +5 -1
- package/src/materialization/marks/pathOrder.js +1 -1
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/overlayItems.js +154 -0
- package/src/materialization/planner.js +19 -4
- package/src/materialization/scales/policies/binnedPosition.js +43 -5
- package/src/materialization/scales/policies/series.js +12 -4
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/bar.js +21 -3
- package/src/materialization/selection/items/common.js +18 -5
- package/src/materialization/selection/items/overlay.js +15 -0
- package/src/materialization/selection/items/path.js +1 -0
- package/src/materialization/selection/items/point.js +2 -3
- package/src/materialization/selection/items/tick.js +3 -12
- package/src/materialization/selection/path.js +2 -2
- package/src/materialization/selection/policies/bar.js +2 -0
- package/src/materialization/selection/policies/index.js +35 -1
- package/src/materialization/selection/policies/point.js +2 -0
- package/src/materialization/selection/policies/text.js +17 -0
- package/src/materialization/selection/rows.js +103 -0
- package/src/materialization/selection/state.js +19 -3
- package/src/materialization/selection/styles.js +8 -0
- package/src/materialization/text.js +6 -489
- package/src/materialization/textGeometry.js +503 -0
- package/src/materialization/textLines.js +20 -0
- package/src/persistence/validation.js +116 -25
- package/src/renderers/canvas/index.js +12 -7
- package/src/renderers/canvas/text.js +5 -5
- package/src/renderers/nativeBackend.js +25 -0
- package/src/renderers/pdf.js +3 -3
- package/src/renderers/png.js +4 -3
- package/src/renderers/svgSerialization.js +1 -643
- package/src/scene.js +5 -3
- package/src/selectors/plotComposition.js +115 -0
- package/src/selectors/selectionTargets.js +19 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +4 -2
- package/types/index.d.ts +14 -0
- package/types/inspection.d.ts +34 -0
- package/types/program.d.ts +572 -372
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { validateLegendOverflow } from "../grammar/legendOverflow.js";
|
|
2
|
+
import { normalizeTextOverrides } from "../grammar/text.js";
|
|
3
|
+
import { normalizeLegendDisplayLabelMap } from "../grammar/displayLabels.js";
|
|
1
4
|
import { validateFacetAxisPolicy } from "../core/facetAxisPolicy.js";
|
|
2
5
|
import { resolveFacetFamily } from "../grammar/facets/dependencies.js";
|
|
3
6
|
import { validateRegressionPlacement } from "../grammar/regression/parameters.js";
|
|
@@ -18,19 +21,18 @@ import { axisGraphicIds } from "../selectors/axes.js";
|
|
|
18
21
|
|
|
19
22
|
import {findLayer} from '../selectors/layers.js';
|
|
20
23
|
import {findCoordinate} from '../selectors/coordinates.js';
|
|
21
|
-
import {resolveColorSizeMapping,normalizeColorSizeSymbol} from '../grammar/colorSizeLegend.js';
|
|
24
|
+
import {resolveColorSizeMapping,normalizeColorSizeSymbol,colorSizeChannels,validateColorSizeShapeSamples} from '../grammar/colorSizeLegend.js';
|
|
22
25
|
import{validateLegendClip}from"../grammar/legendClip.js";
|
|
23
26
|
import {validateLegendLayerOpacity}from"../grammar/legendOpacity.js";
|
|
24
27
|
import { isSizeShapeChannels, resolveSizeShapeMapping } from "../grammar/legendCombinations.js";
|
|
25
|
-
import { describeLegendBlock } from "../grammar/legendBlocks.js";
|
|
28
|
+
import { describeLegendBlock, validateLegendChannels } from "../grammar/legendBlocks.js";
|
|
26
29
|
import { localLegendPosition } from "../grammar/legendGroups.js";
|
|
27
30
|
import { normalizeLegendBorderDash } from "../grammar/legendBorder.js";
|
|
28
|
-
import { normalizeNumericDisplayLabelMap } from "../grammar/displayLabels.js";
|
|
29
|
-
import { isContinuousSizeScaleType } from "../grammar/scales/size.js";
|
|
30
31
|
import { validateGraphicItemLimit } from "../core/validation.js";
|
|
31
32
|
import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
|
|
32
33
|
import { validateGuideDash, normalizeGuideDashMap } from "../grammar/guideDash.js";
|
|
33
|
-
import { validateAxisTextPlacement
|
|
34
|
+
import { validateAxisLabelFlushThreshold, validateAxisTextPlacement,
|
|
35
|
+
axisLabelHasText } from "../grammar/axisTextPlacement.js";
|
|
34
36
|
import { validateTickSelectionRounding, axisTickSelection } from "../grammar/ticks.js";
|
|
35
37
|
import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
|
|
36
38
|
import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
|
|
@@ -49,7 +51,8 @@ import { validateUserId } from "../core/identifiers.js";
|
|
|
49
51
|
import { collectAllResourceReferences } from "../core/resourceReferences.js";
|
|
50
52
|
import { requiredTraceExtension } from "../core/extensionRegistry.js";
|
|
51
53
|
import { resolveDatasetReference } from "../selectors/datasets.js";
|
|
52
|
-
import { normalizeMarkSelector } from "../grammar/markSelection.js";
|
|
54
|
+
import { normalizeMarkSelector, validateRowSelectionDataset } from "../grammar/markSelection.js";
|
|
55
|
+
import { highlightTargets } from "../selectors/selectionTargets.js";
|
|
53
56
|
import { parseSemanticPath } from "../grammar/schemas/semanticPath.js";
|
|
54
57
|
import { validateStoredSemanticValue } from "../actions/primitives/semantic.js";
|
|
55
58
|
import { validateGraphicType, isGraphicContainerType } from "../grammar/schemas/graphic.js";
|
|
@@ -226,6 +229,7 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
226
229
|
throw new TypeError("Facet row field title requires a row-column facet grid.");
|
|
227
230
|
}
|
|
228
231
|
}
|
|
232
|
+
validateGuideVisibilityState(program);
|
|
229
233
|
if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
|
|
230
234
|
for (const config of [
|
|
231
235
|
...Object.values(program.guideConfigs.axis ?? {}).map(axis => axis.ticks),
|
|
@@ -236,6 +240,23 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
236
240
|
}
|
|
237
241
|
const cartesianOwners = cartesianAxisOwners(program);
|
|
238
242
|
for (const [id, axis] of Object.entries(program.guideConfigs.axis ?? {})) {
|
|
243
|
+
if (isPlainObject(axis?.title?.offset)) {
|
|
244
|
+
if (!cartesianOwners.some(owner => owner.id === id)) throw invalidSnapshot("Relative title offset requires a Cartesian axis owner.", path);
|
|
245
|
+
validateAxisTextPlacement(axis.title, { title: true });
|
|
246
|
+
if (axis.title.inferredOffset === true) throw invalidSnapshot("Relative title offset cannot be inferred.", path);
|
|
247
|
+
const owner = cartesianOwners.find(owner => owner.id === id);
|
|
248
|
+
const labels = program.graphicSpec.objects[axisGraphicIds(owner.channel, id)[2]];
|
|
249
|
+
// Composition parents retain source intent, not local unit graphics.
|
|
250
|
+
// Their retained unit/children are validated through the same unit rule.
|
|
251
|
+
if (axis.labels?.position !== axis.title.position || (program.compositionSpec === undefined &&
|
|
252
|
+
!(labels?.items ?? (labels ? [labels] : [])).some(item => axisLabelHasText(item.properties)))) {
|
|
253
|
+
throw invalidSnapshot("Relative title offset requires nonempty same-side labels.", path);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (axis?.labels?.flushThreshold !== undefined) {
|
|
257
|
+
if (!cartesianOwners.some(owner => owner.id === id)) throw invalidSnapshot("Label flushThreshold requires a Cartesian axis owner.", path);
|
|
258
|
+
validateAxisLabelFlushThreshold(axis.labels.flushThreshold);
|
|
259
|
+
}
|
|
239
260
|
for (const config of [axis?.ticks, axis?.labels]) {
|
|
240
261
|
if (config === undefined || !Object.hasOwn(config, "rounding")) continue;
|
|
241
262
|
if (!cartesianOwners.some(owner => owner.id === id)) {
|
|
@@ -301,7 +322,7 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
301
322
|
}
|
|
302
323
|
const config = program.guideConfigs.axis?.[id];
|
|
303
324
|
requireObject(config, `${path}.guides.axis.${id}`);
|
|
304
|
-
validateKeys(config, ["binding", "frame", "line", "ticks", "labels", "title"], "Stored named axis config");
|
|
325
|
+
validateKeys(config, ["binding", "frame", "line", "ticks", "labels", "title", "visibility"], "Stored named axis config");
|
|
305
326
|
const components = ["line", "ticks", "labels", "title"];
|
|
306
327
|
if (!components.some(component => config[component] !== undefined)) {
|
|
307
328
|
throw invalidSnapshot("Named axes require at least one retained component.", path);
|
|
@@ -322,30 +343,50 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
322
343
|
coordinate: semanticAxis.coordinate, position: semanticAxis.position });
|
|
323
344
|
}
|
|
324
345
|
if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
|
|
346
|
+
if (program.guideConfigs.suppressedLegends !== undefined) {
|
|
347
|
+
requireObject(program.guideConfigs.suppressedLegends, `${path}.materializationConfigs.guides.suppressedLegends`);
|
|
348
|
+
for (const [target, config] of Object.entries(program.guideConfigs.suppressedLegends)) {
|
|
349
|
+
validateUserId(target, "Suppressed legend target");
|
|
350
|
+
requireObject(config, `${path}.materializationConfigs.guides.suppressedLegends.${target}`);
|
|
351
|
+
if (!semantic.layers.some(layer => layer.id === target) || Object.keys(config).some(key => key !== "channels")) {
|
|
352
|
+
throw invalidSnapshot("Suppressed legend requires an existing mark and channel selection.", path);
|
|
353
|
+
}
|
|
354
|
+
validateLegendChannels(config.channels, "Suppressed legend");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
325
357
|
for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
|
|
358
|
+
validateLegendOverflow(kind, config);
|
|
326
359
|
validateLegendClip(kind,config);
|
|
360
|
+
for (const [key, override] of Object.entries(config.blockOverrides ?? {})) {
|
|
361
|
+
if (override.labelMap !== undefined) {
|
|
362
|
+
const descriptor = describeLegendBlock(program, kind, config);
|
|
363
|
+
if (key !== descriptor.key) throw invalidSnapshot("Display labels require their existing legend block owner.", path);
|
|
364
|
+
normalizeLegendDisplayLabelMap(descriptor, override.labelMap, program.resolvedScales[descriptor.scaleIds[0]]?.type);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
327
367
|
validateLegendSpacing(kind,config);
|
|
328
368
|
if (localLegendPosition(config) !== undefined) {
|
|
329
369
|
const descriptor = describeLegendBlock(program, kind, config);
|
|
330
370
|
for (const [key, override] of Object.entries(config.blockOverrides)) {
|
|
331
|
-
if (override.position !== undefined && key !== descriptor.key) throw invalidSnapshot("Local position requires its existing block owner.", path);
|
|
371
|
+
if ((override.position !== undefined || override.alongOffset !== undefined) && key !== descriptor.key) throw invalidSnapshot("Local position requires its existing block owner.", path);
|
|
332
372
|
}
|
|
333
373
|
}
|
|
334
374
|
for(const layer of config.symbol?.layers??[])validateLegendLayerOpacity(layer.opacity);
|
|
335
375
|
if(kind==='colorSize') {
|
|
336
|
-
if(
|
|
376
|
+
if(!['color-size','color-size-shape'].includes(config.combine))throw invalidSnapshot('Color-size legend requires its combined owner.',path);
|
|
337
377
|
const layer=findLayer(program,config.target);
|
|
338
|
-
const mapping=resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer?.id],coordinate:findCoordinate(program,layer?.coordinate)});
|
|
339
|
-
for(const key of ['target','field','fieldType','colorScale','sizeScale','domain'])if(JSON.stringify(config[key])!==JSON.stringify(mapping[key]))throw invalidSnapshot('Stale color-size mapping.',path);
|
|
378
|
+
const mapping=resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer?.id],coordinate:findCoordinate(program,layer?.coordinate),combine:config.combine,channels:colorSizeChannels(config)});
|
|
379
|
+
for(const key of ['target','field','fieldType','colorScale','sizeScale','shapeScale','colorChannel','domain'])if(JSON.stringify(config[key])!==JSON.stringify(mapping[key]))throw invalidSnapshot('Stale color-size mapping.',path);
|
|
340
380
|
const guide=semantic.guides.legend?.colorSize;
|
|
341
|
-
if(guide?.colorScale!==mapping.colorScale||guide?.sizeScale!==mapping.sizeScale||guide.title!==config.title)throw invalidSnapshot('Stale color-size semantic owner.',path);
|
|
342
|
-
requireObject(config.symbol,path);normalizeColorSizeSymbol(config.symbol);
|
|
343
|
-
if(config.symbol.type!=='point'
|
|
381
|
+
if(guide?.colorScale!==mapping.colorScale||guide?.sizeScale!==mapping.sizeScale||guide.title!==config.title||guide.shapeScale!==mapping.shapeScale||guide.colorChannel!==mapping.colorChannel)throw invalidSnapshot('Stale color-size semantic owner.',path);
|
|
382
|
+
requireObject(config.symbol,path);normalizeColorSizeSymbol(config.symbol,undefined,config);
|
|
383
|
+
if(config.symbol.type!=='point'||config.combine==='color-size'&&(!Object.hasOwn(config.symbol,'strokeWidth')||!Object.hasOwn(config.symbol,'opacity')))throw invalidSnapshot('Color-size symbol requires point type.',path);
|
|
344
384
|
const sample=config.sampling;requireObject(sample,path);
|
|
345
385
|
exactKeys(sample,sample.mode==='values'?['mode','count','values']:['mode','count'],path);
|
|
346
386
|
if(!['auto','values'].includes(sample.mode)||!Number.isInteger(sample.count)||sample.count<2)throw invalidSnapshot('Invalid color-size samples.',path);
|
|
347
387
|
validateGeneratedItemLimit(sample.count,'Color-size samples');
|
|
348
|
-
if(
|
|
388
|
+
if(config.combine==='color-size-shape')validateColorSizeShapeSamples(sample,program.resolvedScales[mapping.colorScale]);
|
|
389
|
+
else if(sample.mode==='values'){
|
|
349
390
|
requireArray(sample.values,path);
|
|
350
391
|
if(!sample.values.length||sample.values.length>100||sample.values.some((v,i)=>!Number.isFinite(v)||i>0&&v<=sample.values[i-1]||v<Math.min(...mapping.domain)||v>Math.max(...mapping.domain)||[mapping.colorScale,mapping.sizeScale].some(id=>program.resolvedScales[id].type==='log'&&v<=0)))throw invalidSnapshot('Invalid exact color-size samples.',path);
|
|
351
392
|
}
|
|
@@ -367,15 +408,6 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
367
408
|
if (config.border && Object.hasOwn(config.border, "strokeDash")) {
|
|
368
409
|
normalizeLegendBorderDash(config.border.strokeDash, "Stored legend border strokeDash");
|
|
369
410
|
}
|
|
370
|
-
if (["size","colorSize"].includes(kind) && config.blockOverrides !== undefined) {
|
|
371
|
-
for (const [key, override] of Object.entries(config.blockOverrides)) {
|
|
372
|
-
if (override.labelMap === undefined) continue;
|
|
373
|
-
if (key !== (kind==='colorSize'?'["color","size"]':'["size"]') || !isContinuousSizeScaleType(program.resolvedScales[kind==='colorSize'?config.sizeScale:config.scale]?.type)) {
|
|
374
|
-
throw invalidSnapshot("Numeric display labels require a continuous size legend block.", path);
|
|
375
|
-
}
|
|
376
|
-
normalizeNumericDisplayLabelMap(override.labelMap, "Stored size legend labelMap");
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
411
|
if (config.titleStyle !== undefined) {
|
|
380
412
|
validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
|
|
381
413
|
validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
|
|
@@ -440,13 +472,25 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
440
472
|
}
|
|
441
473
|
}
|
|
442
474
|
|
|
475
|
+
for (const [id, config] of Object.entries(program.materializationConfigs.rowProvenance ?? {})) {
|
|
476
|
+
exactKeys(config, ["indices"], `${path}.materializationConfigs.rowProvenance.${id}`);
|
|
477
|
+
const { indices } = config;
|
|
478
|
+
const dataset = resources.get("data").get(id), source = resources.get("data").get(dataset?.source);
|
|
479
|
+
if (dataset?.transform?.[0]?.type !== "markFilter" || !source || !Array.isArray(indices) ||
|
|
480
|
+
indices.length !== dataset.values?.length || Array.from(indices).some((index, offset) =>
|
|
481
|
+
!Number.isInteger(index) || index < 0 || index >= source.values.length ||
|
|
482
|
+
(offset > 0 && index <= indices[offset - 1]) || !sameLookupValue(dataset.values[offset], source.values[index]))) {
|
|
483
|
+
throw invalidSnapshot("Invalid filtered row provenance.", path);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
443
486
|
const selections = program.materializationConfigs.selections ?? {};
|
|
444
487
|
requireObject(selections, `${path}.materializationConfigs.selections`);
|
|
445
488
|
for (const [id, selection] of Object.entries(selections)) {
|
|
446
489
|
validateUserId(id, "Selection id");
|
|
447
|
-
exactKeys(selection, ["target", "selector"], `${path}.materializationConfigs.selections.${id}`);
|
|
490
|
+
exactKeys(selection, ["target", "selector", ...(Object.hasOwn(selection, "sourceSelection") ? ["sourceSelection"] : [])], `${path}.materializationConfigs.selections.${id}`);
|
|
448
491
|
validateUserId(selection.target, "Selection target");
|
|
449
492
|
normalizeMarkSelector(selection.selector);
|
|
493
|
+
if (Object.hasOwn(selection, "sourceSelection")) validateUserId(selection.sourceSelection, "Composite selection owner");
|
|
450
494
|
}
|
|
451
495
|
resources.set("selection", new Map(Object.entries(selections)));
|
|
452
496
|
for (const reference of collectAllResourceReferences(program)) {
|
|
@@ -455,6 +499,24 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
455
499
|
throw invalidSnapshot(`Unknown ${reference.kind} "${reference.id}" referenced by ${reference.ownerKind} "${reference.ownerId}".`, path);
|
|
456
500
|
}
|
|
457
501
|
}
|
|
502
|
+
for (const selection of Object.values(selections)) {
|
|
503
|
+
if (selection.sourceSelection !== undefined) {
|
|
504
|
+
const owner = selections[selection.sourceSelection];
|
|
505
|
+
if (owner.sourceSelection !== undefined || owner.target === selection.target ||
|
|
506
|
+
!highlightTargets(program, owner.target).includes(selection.target)) {
|
|
507
|
+
throw invalidSnapshot("Invalid composite selection owner.", path);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (selection.selector.rows !== undefined) validateRowSelectionDataset(selection.selector.rows, resources.get("data").get(selection.selector.rows.data));
|
|
511
|
+
}
|
|
512
|
+
for (const config of Object.values(program.materializationConfigs.highlights ?? {})) {
|
|
513
|
+
if (config.groupSelection === undefined) continue;
|
|
514
|
+
const owner = selections[config.groupSelection], selection = selections[config.selection];
|
|
515
|
+
if (owner.sourceSelection !== undefined || selection.target !== config.target ||
|
|
516
|
+
(config.selection !== config.groupSelection && selection.sourceSelection !== config.groupSelection)) {
|
|
517
|
+
throw invalidSnapshot("Invalid composite highlight selection group.", path);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
458
520
|
for (const [id, scale] of Object.entries(program.resolvedScales)) {
|
|
459
521
|
requireObject(scale, `${path}.resolvedScales.${id}`);
|
|
460
522
|
requireArray(scale.domain, `${path}.resolvedScales.${id}.domain`);
|
|
@@ -468,6 +530,10 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
468
530
|
}
|
|
469
531
|
}
|
|
470
532
|
for (const id of Object.keys(program.markConfigs)) {
|
|
533
|
+
if (program.markConfigs[id].textOverrides !== undefined) {
|
|
534
|
+
if (resources.get("mark").get(id).mark.type !== "text") throw invalidSnapshot("Text overrides require a text mark.", path);
|
|
535
|
+
normalizeTextOverrides(program.markConfigs[id].textOverrides);
|
|
536
|
+
}
|
|
471
537
|
validateRegressionPlacement(program.markConfigs[id].regression?.placement);
|
|
472
538
|
requestedMarkClip(program.markConfigs[id]);
|
|
473
539
|
validateMarkClipCoordinate(program, resources.get("mark").get(id), program.markConfigs[id].clip);
|
|
@@ -499,3 +565,28 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
499
565
|
}));
|
|
500
566
|
}
|
|
501
567
|
}
|
|
568
|
+
|
|
569
|
+
function validateGuideVisibilityState(program) {
|
|
570
|
+
const check = (visibility, allowed) => {
|
|
571
|
+
if (visibility === undefined) return;
|
|
572
|
+
if (!isPlainObject(visibility) || Object.entries(visibility).some(([key, value]) => !allowed.includes(key) || typeof value !== "boolean"))
|
|
573
|
+
throw new TypeError("Stored guide visibility must contain only supported boolean flags.");
|
|
574
|
+
};
|
|
575
|
+
for (const config of Object.values(program.guideConfigs.axis ?? {})) {
|
|
576
|
+
check(config.visibility, ["all", "line", "ticks", "labels", "title"]);
|
|
577
|
+
for (const dimension of config.axes?.dimensions ?? []) check(dimension.visibility, ["all", "line", "ticks", "labels", "title"]);
|
|
578
|
+
}
|
|
579
|
+
for (const config of Object.values(program.guideConfigs.legend ?? {})) check(config.visibility, ["all", "labels", "titleStyle"]);
|
|
580
|
+
for (const config of Object.values(program.guideConfigs.grid ?? {})) if (Object.hasOwn(config, "visible") && typeof config.visible !== "boolean")
|
|
581
|
+
throw new TypeError("Stored grid visible must be a boolean.");
|
|
582
|
+
const paint = program.materializationConfigs.guideVisibilityPaint;
|
|
583
|
+
if (paint !== undefined) {
|
|
584
|
+
if (!isPlainObject(paint)) throw new TypeError("Stored guide visibility paint must be an object.");
|
|
585
|
+
for (const items of Object.values(paint)) {
|
|
586
|
+
if (!isPlainObject(items)) throw new TypeError("Stored guide visibility paint entries must be objects.");
|
|
587
|
+
for (const value of Object.values(items)) if (!isPlainObject(value) || Object.keys(value).length !== 1 ||
|
|
588
|
+
!Object.hasOwn(value, "opacity") || value.opacity !== null && (!Number.isFinite(value.opacity) || value.opacity < 0 || value.opacity > 1))
|
|
589
|
+
throw new TypeError("Stored guide visibility paint requires an original opacity or null.");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
@@ -26,6 +26,9 @@ const DRAWERS = Object.freeze({
|
|
|
26
26
|
path: drawPathGraphic
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
// Distinguish a completed empty preflight from a direct draw that still needs one.
|
|
30
|
+
const NO_ISOLATED_GLYPHS = Object.freeze({});
|
|
31
|
+
|
|
29
32
|
const DRAWING_CONTEXT_METHODS = Object.freeze([
|
|
30
33
|
"save",
|
|
31
34
|
"restore",
|
|
@@ -174,14 +177,14 @@ export function drawResolvedGraphicSpec(target, context, options = {}) {
|
|
|
174
177
|
return;
|
|
175
178
|
}
|
|
176
179
|
if (!Array.isArray(object.items)) {
|
|
177
|
-
drawConcreteGraphic(context, id, object, isolated);
|
|
180
|
+
drawConcreteGraphic(context, id, object, isolated, options);
|
|
178
181
|
}
|
|
179
182
|
},
|
|
180
183
|
item({ id, object, owner }) {
|
|
181
184
|
drawConcreteGraphic(context, id, {
|
|
182
185
|
...object,
|
|
183
186
|
type: object.type ?? owner.type
|
|
184
|
-
}, isolated);
|
|
187
|
+
}, isolated, options);
|
|
185
188
|
},
|
|
186
189
|
exit({ id, object }) {
|
|
187
190
|
if (
|
|
@@ -195,7 +198,7 @@ export function drawResolvedGraphicSpec(target, context, options = {}) {
|
|
|
195
198
|
});
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, createCanvas) {
|
|
201
|
+
export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, createCanvas, drawingOptions = {}) {
|
|
199
202
|
const graphicSpec = requireProgramGraphicSpec(program);
|
|
200
203
|
|
|
201
204
|
requireCanvasContext(context);
|
|
@@ -253,7 +256,9 @@ export function renderCanvasProgram(program, context, { pixelRatio = 1 } = {}, c
|
|
|
253
256
|
try {
|
|
254
257
|
context.scale(pixelRatio, pixelRatio);
|
|
255
258
|
context.clearRect(0, 0, width, height);
|
|
256
|
-
drawResolvedGraphicSpec(target, context, {
|
|
259
|
+
drawResolvedGraphicSpec(target, context, {
|
|
260
|
+
...drawingOptions, isolated: isolated ?? NO_ISOLATED_GLYPHS, pixelRatio, createCanvas
|
|
261
|
+
});
|
|
257
262
|
} finally {
|
|
258
263
|
context.restore();
|
|
259
264
|
}
|
|
@@ -263,10 +268,10 @@ export function render(program, context, options) {
|
|
|
263
268
|
return renderCanvasProgram(program, context, options);
|
|
264
269
|
}
|
|
265
270
|
|
|
266
|
-
function drawConcreteGraphic(context, id, graphic, isolated) {
|
|
271
|
+
function drawConcreteGraphic(context, id, graphic, isolated, options) {
|
|
267
272
|
if (graphic.type === "collection") {
|
|
268
273
|
for (const item of graphic.items ?? []) {
|
|
269
|
-
drawConcreteGraphic(context, item.id ?? id, item, isolated);
|
|
274
|
+
drawConcreteGraphic(context, item.id ?? id, item, isolated, options);
|
|
270
275
|
}
|
|
271
276
|
return;
|
|
272
277
|
}
|
|
@@ -277,6 +282,6 @@ function drawConcreteGraphic(context, id, graphic, isolated) {
|
|
|
277
282
|
if (["normal","difference"].includes(graphic.properties?.blend)) {
|
|
278
283
|
drawIsolatedGlyph(context, id, graphic, draw, isolated);
|
|
279
284
|
} else {
|
|
280
|
-
draw(context, id, graphic);
|
|
285
|
+
draw(context, id, graphic, options);
|
|
281
286
|
}
|
|
282
287
|
}
|
|
@@ -4,7 +4,7 @@ import { validateConcreteGraphicProperties, validateConcreteGraphicValue } from
|
|
|
4
4
|
"../../grammar/schemas/concreteGraphic.js";
|
|
5
5
|
import { normalizeRendererFontWeight } from "../text.js";
|
|
6
6
|
|
|
7
|
-
function drawText(context, child, collectionId) {
|
|
7
|
+
function drawText(context, child, collectionId, { resolveFontFamily } = {}) {
|
|
8
8
|
const properties = child.properties ?? {};
|
|
9
9
|
const graphicId = child.id ?? collectionId;
|
|
10
10
|
validateConcreteGraphicProperties("text", properties);
|
|
@@ -77,7 +77,7 @@ function drawText(context, child, collectionId) {
|
|
|
77
77
|
context.fillStyle = resolveCanvasSolidPaint(fill);
|
|
78
78
|
context.globalAlpha = opacity;
|
|
79
79
|
const rendererFontWeight = normalizeRendererFontWeight(fontWeight);
|
|
80
|
-
context.font = `${properties.fontStyle === "italic" ? "italic " : ""}${rendererFontWeight} ${fontSize}px ${fontFamily}`;
|
|
80
|
+
context.font = `${properties.fontStyle === "italic" ? "italic " : ""}${rendererFontWeight} ${fontSize}px ${resolveFontFamily?.(fontFamily) ?? fontFamily}`;
|
|
81
81
|
context.textAlign = textAlign;
|
|
82
82
|
context.textBaseline = textBaseline;
|
|
83
83
|
context.translate(x, y);
|
|
@@ -89,13 +89,13 @@ function drawText(context, child, collectionId) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export function drawTextGraphic(context, id, graphic) {
|
|
92
|
+
export function drawTextGraphic(context, id, graphic, options) {
|
|
93
93
|
if (graphic.items) {
|
|
94
94
|
for (const item of graphic.items) {
|
|
95
|
-
drawText(context, item, id);
|
|
95
|
+
drawText(context, item, id, options);
|
|
96
96
|
}
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
drawText(context, { id, properties: graphic.properties }, id);
|
|
100
|
+
drawText(context, { id, properties: graphic.properties }, id, options);
|
|
101
101
|
}
|
|
@@ -8,3 +8,28 @@ export async function loadNativeBackend() {
|
|
|
8
8
|
throw error;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
// Supply installed glyph fallbacks after the authored family, as browsers do.
|
|
13
|
+
// This resolves font resources only; no text or layout enters program state.
|
|
14
|
+
const SYSTEM_FALLBACKS = [
|
|
15
|
+
"Noto Sans CJK KR", "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic",
|
|
16
|
+
"Noto Sans CJK JP", "Noto Sans JP", "Yu Gothic", "Meiryo",
|
|
17
|
+
"Noto Sans CJK SC", "Noto Sans SC", "PingFang SC", "Microsoft YaHei",
|
|
18
|
+
"Noto Sans CJK TC", "PingFang TC", "Microsoft JhengHei",
|
|
19
|
+
"Arial Unicode MS", "Noto Sans", "DejaVu Sans",
|
|
20
|
+
"Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji"
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function nativeFontResolver(globalFonts) {
|
|
24
|
+
const installed = new Set(globalFonts.families.map(entry => entry.family));
|
|
25
|
+
const fallbacks = SYSTEM_FALLBACKS.filter(family => installed.has(family));
|
|
26
|
+
const cache = new Map();
|
|
27
|
+
return requested => {
|
|
28
|
+
if (cache.has(requested)) return cache.get(requested);
|
|
29
|
+
const present = new Set(requested.split(',').map(family => family.trim().replace(/^["']|["']$/g, '').toLowerCase()));
|
|
30
|
+
const extra = fallbacks.filter(family => !present.has(family.toLowerCase()));
|
|
31
|
+
const resolved = [requested, ...extra.map(family => `"${family}"`)].join(', ');
|
|
32
|
+
cache.set(requested, resolved);
|
|
33
|
+
return resolved;
|
|
34
|
+
};
|
|
35
|
+
}
|
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 { loadNativeBackend } from "./nativeBackend.js";
|
|
4
|
+
import { loadNativeBackend, nativeFontResolver } from "./nativeBackend.js";
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
drawResolvedGraphicSpec,
|
|
@@ -73,10 +73,10 @@ export async function renderToPDFBuffer(program, options = {}) {
|
|
|
73
73
|
const target = resolveGraphicRenderTarget(graphicSpec);
|
|
74
74
|
requirePDFPageDimensions(target);
|
|
75
75
|
preflightCanvasGraphicSpec(target);
|
|
76
|
-
const { PDFDocument, createCanvas } = await loadNativeBackend();
|
|
76
|
+
const { PDFDocument, createCanvas, GlobalFonts } = await loadNativeBackend();
|
|
77
77
|
const document = new PDFDocument(metadata);
|
|
78
78
|
const context = document.beginPage(target.width, target.height);
|
|
79
|
-
drawResolvedGraphicSpec(target, context, {createCanvas});
|
|
79
|
+
drawResolvedGraphicSpec(target, context, {createCanvas, resolveFontFamily: nativeFontResolver(GlobalFonts)});
|
|
80
80
|
document.endPage();
|
|
81
81
|
const buffer = document.close();
|
|
82
82
|
return Object.freeze({
|
package/src/renderers/png.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { renderCanvasProgram } from "./canvas/index.js";
|
|
4
4
|
import { validateRendererOptions } from "./options.js";
|
|
5
|
-
import { loadNativeBackend } from "./nativeBackend.js";
|
|
5
|
+
import { loadNativeBackend, nativeFontResolver } from "./nativeBackend.js";
|
|
6
6
|
|
|
7
7
|
const PNG_OPTIONS = new Set(["output", "pixelRatio"]);
|
|
8
8
|
const BUFFER_OPTIONS = new Set(["pixelRatio"]);
|
|
@@ -10,9 +10,10 @@ const BUFFER_OPTIONS = new Set(["pixelRatio"]);
|
|
|
10
10
|
export async function renderToPNGBuffer(program, options = {}) {
|
|
11
11
|
validateRendererOptions(options, BUFFER_OPTIONS, "renderToPNGBuffer options");
|
|
12
12
|
const { pixelRatio = 1 } = options;
|
|
13
|
-
const { createCanvas } = await loadNativeBackend();
|
|
13
|
+
const { createCanvas, GlobalFonts } = await loadNativeBackend();
|
|
14
14
|
const canvas = createCanvas(1, 1);
|
|
15
|
-
renderCanvasProgram(program, canvas.getContext("2d"), { pixelRatio }, createCanvas
|
|
15
|
+
renderCanvasProgram(program, canvas.getContext("2d"), { pixelRatio }, createCanvas,
|
|
16
|
+
{ resolveFontFamily: nativeFontResolver(GlobalFonts) });
|
|
16
17
|
// Canvas drawing is synchronous; encoding uses the native async worker.
|
|
17
18
|
const buffer = await canvas.encode("png");
|
|
18
19
|
return Object.freeze({
|