ggaction 0.0.24 → 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 +47 -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 +5 -3
- 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/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 +2 -0
- 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/edit.js +1 -1
- package/src/actions/guides/axes/labels.js +2 -1
- 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/titles.js +1 -1
- 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 +12 -9
- package/src/core/action.js +41 -25
- package/src/core/compositionScales.js +12 -0
- package/src/core/compositionState.js +16 -2
- 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/bin2d.js +7 -0
- 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 +30 -4
- package/src/layout/compositionLegend.js +91 -0
- package/src/layout/facetHeaderTitles.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/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/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 +97 -24
- 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 +2 -2
- package/types/index.d.ts +11 -0
- package/types/inspection.d.ts +34 -0
- package/types/program.d.ts +553 -365
|
@@ -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,15 +21,13 @@ 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";
|
|
@@ -50,7 +51,8 @@ import { validateUserId } from "../core/identifiers.js";
|
|
|
50
51
|
import { collectAllResourceReferences } from "../core/resourceReferences.js";
|
|
51
52
|
import { requiredTraceExtension } from "../core/extensionRegistry.js";
|
|
52
53
|
import { resolveDatasetReference } from "../selectors/datasets.js";
|
|
53
|
-
import { normalizeMarkSelector } from "../grammar/markSelection.js";
|
|
54
|
+
import { normalizeMarkSelector, validateRowSelectionDataset } from "../grammar/markSelection.js";
|
|
55
|
+
import { highlightTargets } from "../selectors/selectionTargets.js";
|
|
54
56
|
import { parseSemanticPath } from "../grammar/schemas/semanticPath.js";
|
|
55
57
|
import { validateStoredSemanticValue } from "../actions/primitives/semantic.js";
|
|
56
58
|
import { validateGraphicType, isGraphicContainerType } from "../grammar/schemas/graphic.js";
|
|
@@ -227,6 +229,7 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
227
229
|
throw new TypeError("Facet row field title requires a row-column facet grid.");
|
|
228
230
|
}
|
|
229
231
|
}
|
|
232
|
+
validateGuideVisibilityState(program);
|
|
230
233
|
if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
|
|
231
234
|
for (const config of [
|
|
232
235
|
...Object.values(program.guideConfigs.axis ?? {}).map(axis => axis.ticks),
|
|
@@ -319,7 +322,7 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
319
322
|
}
|
|
320
323
|
const config = program.guideConfigs.axis?.[id];
|
|
321
324
|
requireObject(config, `${path}.guides.axis.${id}`);
|
|
322
|
-
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");
|
|
323
326
|
const components = ["line", "ticks", "labels", "title"];
|
|
324
327
|
if (!components.some(component => config[component] !== undefined)) {
|
|
325
328
|
throw invalidSnapshot("Named axes require at least one retained component.", path);
|
|
@@ -340,30 +343,50 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
340
343
|
coordinate: semanticAxis.coordinate, position: semanticAxis.position });
|
|
341
344
|
}
|
|
342
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
|
+
}
|
|
343
357
|
for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
|
|
358
|
+
validateLegendOverflow(kind, config);
|
|
344
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
|
+
}
|
|
345
367
|
validateLegendSpacing(kind,config);
|
|
346
368
|
if (localLegendPosition(config) !== undefined) {
|
|
347
369
|
const descriptor = describeLegendBlock(program, kind, config);
|
|
348
370
|
for (const [key, override] of Object.entries(config.blockOverrides)) {
|
|
349
|
-
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);
|
|
350
372
|
}
|
|
351
373
|
}
|
|
352
374
|
for(const layer of config.symbol?.layers??[])validateLegendLayerOpacity(layer.opacity);
|
|
353
375
|
if(kind==='colorSize') {
|
|
354
|
-
if(
|
|
376
|
+
if(!['color-size','color-size-shape'].includes(config.combine))throw invalidSnapshot('Color-size legend requires its combined owner.',path);
|
|
355
377
|
const layer=findLayer(program,config.target);
|
|
356
|
-
const mapping=resolveColorSizeMapping({layer,scales:program.resolvedScales,pointConfig:program.markConfigs[layer?.id],coordinate:findCoordinate(program,layer?.coordinate)});
|
|
357
|
-
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);
|
|
358
380
|
const guide=semantic.guides.legend?.colorSize;
|
|
359
|
-
if(guide?.colorScale!==mapping.colorScale||guide?.sizeScale!==mapping.sizeScale||guide.title!==config.title)throw invalidSnapshot('Stale color-size semantic owner.',path);
|
|
360
|
-
requireObject(config.symbol,path);normalizeColorSizeSymbol(config.symbol);
|
|
361
|
-
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);
|
|
362
384
|
const sample=config.sampling;requireObject(sample,path);
|
|
363
385
|
exactKeys(sample,sample.mode==='values'?['mode','count','values']:['mode','count'],path);
|
|
364
386
|
if(!['auto','values'].includes(sample.mode)||!Number.isInteger(sample.count)||sample.count<2)throw invalidSnapshot('Invalid color-size samples.',path);
|
|
365
387
|
validateGeneratedItemLimit(sample.count,'Color-size samples');
|
|
366
|
-
if(
|
|
388
|
+
if(config.combine==='color-size-shape')validateColorSizeShapeSamples(sample,program.resolvedScales[mapping.colorScale]);
|
|
389
|
+
else if(sample.mode==='values'){
|
|
367
390
|
requireArray(sample.values,path);
|
|
368
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);
|
|
369
392
|
}
|
|
@@ -385,15 +408,6 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
385
408
|
if (config.border && Object.hasOwn(config.border, "strokeDash")) {
|
|
386
409
|
normalizeLegendBorderDash(config.border.strokeDash, "Stored legend border strokeDash");
|
|
387
410
|
}
|
|
388
|
-
if (["size","colorSize"].includes(kind) && config.blockOverrides !== undefined) {
|
|
389
|
-
for (const [key, override] of Object.entries(config.blockOverrides)) {
|
|
390
|
-
if (override.labelMap === undefined) continue;
|
|
391
|
-
if (key !== (kind==='colorSize'?'["color","size"]':'["size"]') || !isContinuousSizeScaleType(program.resolvedScales[kind==='colorSize'?config.sizeScale:config.scale]?.type)) {
|
|
392
|
-
throw invalidSnapshot("Numeric display labels require a continuous size legend block.", path);
|
|
393
|
-
}
|
|
394
|
-
normalizeNumericDisplayLabelMap(override.labelMap, "Stored size legend labelMap");
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
411
|
if (config.titleStyle !== undefined) {
|
|
398
412
|
validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
|
|
399
413
|
validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
|
|
@@ -458,13 +472,25 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
458
472
|
}
|
|
459
473
|
}
|
|
460
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
|
+
}
|
|
461
486
|
const selections = program.materializationConfigs.selections ?? {};
|
|
462
487
|
requireObject(selections, `${path}.materializationConfigs.selections`);
|
|
463
488
|
for (const [id, selection] of Object.entries(selections)) {
|
|
464
489
|
validateUserId(id, "Selection id");
|
|
465
|
-
exactKeys(selection, ["target", "selector"], `${path}.materializationConfigs.selections.${id}`);
|
|
490
|
+
exactKeys(selection, ["target", "selector", ...(Object.hasOwn(selection, "sourceSelection") ? ["sourceSelection"] : [])], `${path}.materializationConfigs.selections.${id}`);
|
|
466
491
|
validateUserId(selection.target, "Selection target");
|
|
467
492
|
normalizeMarkSelector(selection.selector);
|
|
493
|
+
if (Object.hasOwn(selection, "sourceSelection")) validateUserId(selection.sourceSelection, "Composite selection owner");
|
|
468
494
|
}
|
|
469
495
|
resources.set("selection", new Map(Object.entries(selections)));
|
|
470
496
|
for (const reference of collectAllResourceReferences(program)) {
|
|
@@ -473,6 +499,24 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
473
499
|
throw invalidSnapshot(`Unknown ${reference.kind} "${reference.id}" referenced by ${reference.ownerKind} "${reference.ownerId}".`, path);
|
|
474
500
|
}
|
|
475
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
|
+
}
|
|
476
520
|
for (const [id, scale] of Object.entries(program.resolvedScales)) {
|
|
477
521
|
requireObject(scale, `${path}.resolvedScales.${id}`);
|
|
478
522
|
requireArray(scale.domain, `${path}.resolvedScales.${id}.domain`);
|
|
@@ -486,6 +530,10 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
486
530
|
}
|
|
487
531
|
}
|
|
488
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
|
+
}
|
|
489
537
|
validateRegressionPlacement(program.markConfigs[id].regression?.placement);
|
|
490
538
|
requestedMarkClip(program.markConfigs[id]);
|
|
491
539
|
validateMarkClipCoordinate(program, resources.get("mark").get(id), program.markConfigs[id].clip);
|
|
@@ -517,3 +565,28 @@ export function validateProgramState(program, path, { allowMissingLookupSchemas
|
|
|
517
565
|
}));
|
|
518
566
|
}
|
|
519
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({
|