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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// One constructor capability registry for layering, adjacent inference and
|
|
2
|
+
// inspection. Native constructors remain responsible for statistical work.
|
|
3
|
+
const xy = ['createScatterPlot','createLinePlot','createBarPlot','createAreaPlot','createStripPlot','createBeeswarmPlot','createRegressionPlot','createTextPlot'];
|
|
4
|
+
const summaryXY = ['createBoxPlot','createViolinPlot','createGradientPlot','createIntervalPlot'];
|
|
5
|
+
export const PLOT_COMPOSITION_ROLES = Object.freeze(Object.fromEntries([
|
|
6
|
+
...xy.map(a => [a, 'xy']), ...summaryXY.map(a => [a, 'summaryXY']),
|
|
7
|
+
...['createDotPlot','createLollipopPlot','createDumbbellPlot'].map(a => [a, 'endpoint']),
|
|
8
|
+
['createRaincloudPlot','categoryValue'], ['createHistogram','histogram'], ['createDensityPlot','density'],
|
|
9
|
+
['createECDFPlot','ecdf'], ['createRugPlot','rug'], ['createHeatmap','heatmap'], ['createHorizonPlot','horizon'],
|
|
10
|
+
...['createPolarScatterPlot','createPolarLinePlot'].map(a => [a, 'polar']),
|
|
11
|
+
...['createPiePlot','createRosePlot','createRadialBarPlot'].map(a => [a, 'radial']),
|
|
12
|
+
['createRadarPlot','radar'], ['createParallelCoordinates','parallel']
|
|
13
|
+
]));
|
|
14
|
+
export const COMPOSITION_PLOT_ACTIONS = Object.freeze(Object.keys(PLOT_COMPOSITION_ROLES));
|
|
15
|
+
export const fieldEncoding = value => typeof value === 'string' ? { field:value } : value;
|
|
16
|
+
const categorical = c => c && ['nominal','ordinal'].includes(c.fieldType);
|
|
17
|
+
const encoding = (c, aggregate = true) => c?.field ? { field:c.field, fieldType:c.fieldType,
|
|
18
|
+
...(aggregate && c.measurement.operations.length === 1 && ['mean','median','sum','min','max','count'].includes(c.measurement.operations[0]) ? { aggregate:c.measurement.operations[0] } : {}),
|
|
19
|
+
...(c.bin ? { bin:c.bin } : {}), ...(c.temporalUnit ? { temporalUnit:c.temporalUnit } : {}) } : undefined;
|
|
20
|
+
const plain = c => c?.field ? { field:c.field, fieldType:c.fieldType } : undefined;
|
|
21
|
+
const merge = (base, supplied) => {
|
|
22
|
+
const value=typeof supplied==='string'?{field:supplied}:supplied;
|
|
23
|
+
// An explicit new field owns its type/grain. Source defaults only fill an
|
|
24
|
+
// omitted role or an options-only patch on that inferred field.
|
|
25
|
+
if (value?.field !== undefined && value.field !== base?.field || value?.lower !== undefined || value?.upper !== undefined) return value;
|
|
26
|
+
const merged={ ...base, ...value };
|
|
27
|
+
if (value?.center !== undefined || value?.extent !== undefined) {
|
|
28
|
+
// Native interval measurements are not ordinary field encodings.
|
|
29
|
+
for (const key of ['fieldType','temporalUnit','aggregate','bin']) delete merged[key];
|
|
30
|
+
}
|
|
31
|
+
return merged;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function inferCompositionOptions(source, action, supplied = {}, inherit = {}) {
|
|
35
|
+
if (!inherit || typeof inherit !== 'object' || Array.isArray(inherit) ||
|
|
36
|
+
Object.keys(inherit).some(key => key !== 'color') ||
|
|
37
|
+
inherit.color !== undefined && typeof inherit.color !== 'boolean') {
|
|
38
|
+
throw new TypeError('Plot inheritance accepts color: true or false.');
|
|
39
|
+
}
|
|
40
|
+
const kind = PLOT_COMPOSITION_ROLES[action];
|
|
41
|
+
if (!kind) throw new Error(`Unknown plot constructor "${action}".`);
|
|
42
|
+
const c = source.channels;
|
|
43
|
+
const x = c.x ?? c.theta, y = c.y ?? c.radius;
|
|
44
|
+
const categoryAxis = categorical(x) ? 'x' : categorical(y) ? 'y' : undefined;
|
|
45
|
+
const category = categoryAxis === 'x' ? x : categoryAxis === 'y' ? y : undefined;
|
|
46
|
+
const measure = categoryAxis === 'x' ? y : categoryAxis === 'y' ? x : x;
|
|
47
|
+
let defaults = { data: source.data };
|
|
48
|
+
if (['xy','summaryXY','heatmap','horizon'].includes(kind)) {
|
|
49
|
+
defaults.x = kind === 'xy' && ['createLinePlot','createBarPlot'].includes(action) ? encoding(x) : plain(x);
|
|
50
|
+
defaults.y = kind === 'xy' && ['createLinePlot','createBarPlot'].includes(action) ? encoding(y) : plain(y);
|
|
51
|
+
if (kind === 'summaryXY') {
|
|
52
|
+
if (!categoryAxis && !(supplied.x !== undefined && supplied.y !== undefined)) throw new Error(`${action} requires one category axis and one quantitative measure.`);
|
|
53
|
+
if (categoryAxis && action === 'createIntervalPlot') defaults[categoryAxis === 'x' ? 'y' : 'x'] = { field:measure.field, center:'mean', extent:'ci', level:0.95 };
|
|
54
|
+
}
|
|
55
|
+
// Binning follows the new chart's roles, including explicitly supplied
|
|
56
|
+
// categorical/ordinal grids, rather than the source chart's old axes.
|
|
57
|
+
if (kind === 'heatmap' && !['x', 'y'].some(axis => categorical(
|
|
58
|
+
supplied[axis] === undefined ? defaults[axis] : merge(defaults[axis], supplied[axis])
|
|
59
|
+
))) defaults.bin = { bins:12 };
|
|
60
|
+
if (kind === 'horizon' && c.y?.measurement.parameters) Object.assign(defaults, c.y.measurement.parameters);
|
|
61
|
+
if (action === 'createTextPlot') defaults.text = { field:c.color?.field ?? y?.field ?? x?.field };
|
|
62
|
+
} else if (['endpoint','categoryValue'].includes(kind)) {
|
|
63
|
+
if (!categoryAxis && !(supplied.category !== undefined && (supplied.value !== undefined || supplied.end !== undefined))) throw new Error(`${action} requires one category axis and one quantitative measure.`);
|
|
64
|
+
if (categoryAxis) Object.assign(defaults, {category:plain(category), orientation: categoryAxis === 'x' ? 'vertical' : 'horizontal'});
|
|
65
|
+
if (action === 'createDumbbellPlot') { defaults.end=plain(measure); } else defaults.value=plain(measure);
|
|
66
|
+
if (kind === 'endpoint') defaults.summary=['mean','median','sum','min','max'].includes(measure?.measurement.operations.at(-1))?measure.measurement.operations.at(-1):'mean';
|
|
67
|
+
} else if (['histogram','density','ecdf'].includes(kind)) {
|
|
68
|
+
defaults.field = supplied.field ?? measure?.field;
|
|
69
|
+
if (kind === 'density' && c.x?.measurement.unit === 'density') defaults.densityChannel='x';
|
|
70
|
+
if (kind === 'histogram' && x?.bin) {
|
|
71
|
+
defaults.maxBins=x.bin.maxBins; defaults.binStep=x.bin.step; defaults.binBoundaries=x.bin.boundaries;
|
|
72
|
+
}
|
|
73
|
+
if (inherit.color !== false && kind === 'density' && c.color?.field && categorical(c.color)) defaults.groupBy=c.color.field;
|
|
74
|
+
} else if (kind === 'rug') {
|
|
75
|
+
const axis = supplied.x !== undefined ? 'x' : supplied.y !== undefined ? 'y'
|
|
76
|
+
: supplied.edge === 'left' || supplied.edge === 'right' || categorical(c.x) ? 'y' : 'x';
|
|
77
|
+
defaults[axis] = plain(c[axis]); defaults.edge = axis === 'x' ? 'bottom' : 'left';
|
|
78
|
+
} else if (kind === 'polar') {
|
|
79
|
+
defaults.theta=plain(c.theta); defaults.radius=plain(c.radius);
|
|
80
|
+
if (action === 'createPolarLinePlot' && source.groupBy) defaults.groupBy=source.groupBy;
|
|
81
|
+
} else if (kind === 'radial') {
|
|
82
|
+
defaults.category=plain(c.theta);
|
|
83
|
+
defaults.aggregate=c.radius?.measurement.operations.at(-1) ?? c.theta?.measurement.parameters?.aggregate ?? 'count';
|
|
84
|
+
if(defaults.aggregate==='sum') defaults.value=c.radius?.field ?? c.theta?.measurement.parameters?.weight;
|
|
85
|
+
} else if (kind === 'radar') {
|
|
86
|
+
if (c.theta?.fields) defaults.wide={fields:c.theta.fields};
|
|
87
|
+
else {defaults.category=plain(c.theta); defaults.value=plain(c.radius);}
|
|
88
|
+
if (source.groupBy) defaults.groupBy=source.groupBy;
|
|
89
|
+
} else if (kind === 'parallel') defaults.dimensions=source.dimensions?.map(plain);
|
|
90
|
+
if (inherit.color !== false && c.color?.field && !['heatmap', 'horizon', 'rug'].includes(kind)) {
|
|
91
|
+
const summaryCategoryOnly = ['endpoint','categoryValue','summaryXY'].includes(kind) && action !== 'createIntervalPlot';
|
|
92
|
+
const colorSurvives = !summaryCategoryOnly || c.color.field === category?.field ||
|
|
93
|
+
action === 'createViolinPlot' && supplied.split?.field === c.color.field;
|
|
94
|
+
if (colorSurvives) defaults.color=plain(c.color);
|
|
95
|
+
if (action === 'createIntervalPlot' && categorical(c.color) && c.color.field !== category?.field) defaults.groupBy=c.color.field;
|
|
96
|
+
}
|
|
97
|
+
if (action === 'createAreaPlot') {
|
|
98
|
+
// Explicit native X/Y roles follow the native default value channel. The
|
|
99
|
+
// source orientation only applies when the new roles are inferred.
|
|
100
|
+
const axis = supplied.valueChannel ?? (supplied.x !== undefined && supplied.y !== undefined ? 'y' : categoryAxis === 'y' ? 'x' : 'y'); defaults.valueChannel=axis;
|
|
101
|
+
if(defaults[axis]) defaults[axis]={field:defaults[axis].field};
|
|
102
|
+
}
|
|
103
|
+
if (['createLinePlot','createAreaPlot'].includes(action) && source.groupBy) defaults.groupBy=source.groupBy;
|
|
104
|
+
if (inherit.color !== false && c.color?.field &&
|
|
105
|
+
(kind==='ecdf' || action==='createAreaPlot' && !defaults.groupBy && categorical(c.color))) defaults.groupBy=[c.color.field];
|
|
106
|
+
const result={...defaults,...supplied};
|
|
107
|
+
for(const key of ['x','y','category','value','theta','radius','color','end']) {
|
|
108
|
+
if (supplied[key] !== undefined && defaults[key] !== undefined) result[key]=merge(defaults[key],supplied[key]);
|
|
109
|
+
}
|
|
110
|
+
return Object.fromEntries(Object.entries(result).filter(([,v])=>v!==undefined));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Describe prospective roles without constructing/rendering a candidate chart.
|
|
114
|
+
export function constructorChannels(action, options, source) {
|
|
115
|
+
const kind=PLOT_COMPOSITION_ROLES[action], c=source.channels, result={};
|
|
116
|
+
const add=(channel,value,path,operation) => {
|
|
117
|
+
const e=fieldEncoding(value); if(!e?.field) return;
|
|
118
|
+
const reference=c[channel]?.field===e.field?c[channel]:Object.values(c).find(r=>r.field===e.field);
|
|
119
|
+
result[channel]={ field:e.field,fieldType:e.fieldType??reference?.fieldType??'quantitative',
|
|
120
|
+
operations:operation ? [operation] : e.aggregate ? [e.aggregate] : [],
|
|
121
|
+
...(e.temporalUnit ? {temporalUnit:e.temporalUnit}:{}), ...(e.bin ? {bin:e.bin}:{}), path };
|
|
122
|
+
};
|
|
123
|
+
if(['xy','summaryXY','heatmap','horizon'].includes(kind)) {
|
|
124
|
+
add('x',options.x,'x.scale'); add('y',options.y,'y.scale');
|
|
125
|
+
if(kind==='horizon' && result.y) result.y.operations=['folded'];
|
|
126
|
+
} else if(['endpoint','categoryValue'].includes(kind)) {
|
|
127
|
+
const axis=options.orientation==='horizontal'?'y':'x', measureAxis=axis==='x'?'y':'x';
|
|
128
|
+
add(axis,options.category,'category.scale');
|
|
129
|
+
add(measureAxis,options.value??options.end,options.value?'value.scale':'end.scale',kind==='endpoint'?options.summary:undefined);
|
|
130
|
+
} else if(kind==='histogram') {
|
|
131
|
+
add('x',options.field,'xScale'); add('y',options.weight??options.field,'yScale',options.weight?'sum':'count');
|
|
132
|
+
} else if(kind==='density') {
|
|
133
|
+
const density=options.densityChannel??'y', value=density==='y'?'x':'y';
|
|
134
|
+
add(value,options.field,'valueScale'); add(density,options.field,'densityScale','density');
|
|
135
|
+
} else if(kind==='ecdf') {
|
|
136
|
+
const value=options.orientation==='horizontal'?'y':'x';
|
|
137
|
+
add(value,options.field,'valueScale'); add(value==='x'?'y':'x',options.field,'probabilityScale','probability');
|
|
138
|
+
} else if(kind==='rug') {
|
|
139
|
+
add('x',options.x,'x.scale'); add('y',options.y,'y.scale');
|
|
140
|
+
} else if(kind==='polar') {
|
|
141
|
+
add('theta',options.theta,'theta.scale'); add('radius',options.radius,'radius.scale');
|
|
142
|
+
} else if(kind==='radial') {
|
|
143
|
+
add('theta',options.category,'category.scale');
|
|
144
|
+
if(action!=='createPiePlot') {
|
|
145
|
+
if(options.aggregate==='count') result.radius={fieldType:'quantitative',operations:['count'],path:'radiusScale'};
|
|
146
|
+
else add('radius',options.value,'radiusScale',options.aggregate);
|
|
147
|
+
} else if(result.theta) result.theta.operations=['partition'];
|
|
148
|
+
} else if(kind==='radar' && !options.wide) {
|
|
149
|
+
add('theta',options.category,'category.scale'); add('radius',options.value,'value.scale');
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
@@ -20,6 +20,24 @@ export const POINT_SHAPES = cloneAndFreeze([
|
|
|
20
20
|
"arrow"
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
|
+
// Recover the anchor from concrete geometry, including jitter, packing and rotation.
|
|
24
|
+
// Path glyphs can be asymmetric, so their bounding-box center is not their anchor.
|
|
25
|
+
export function pointGraphicAnchor(graphic, shape = "circle", angle) {
|
|
26
|
+
const p = graphic?.properties ?? {};
|
|
27
|
+
if (graphic?.type === "circle") return { x: p.x, y: p.y };
|
|
28
|
+
if (graphic?.type === "rect") return { x: p.x + p.width / 2, y: p.y + p.height / 2 };
|
|
29
|
+
const template = createPointShapeGraphic({ shape, angle, x: 0, y: 0, area: 1, fill: "black" });
|
|
30
|
+
const points = p.commands?.filter(command => Number.isFinite(command.x) && Number.isFinite(command.y));
|
|
31
|
+
const origin = template.properties.commands?.filter(command => Number.isFinite(command.x) && Number.isFinite(command.y));
|
|
32
|
+
if (!points?.length || points.length !== origin?.length) throw new Error("Point glyph does not match its shape geometry.");
|
|
33
|
+
const bounds = values => ({ left: Math.min(...values.map(p => p.x)), right: Math.max(...values.map(p => p.x)),
|
|
34
|
+
top: Math.min(...values.map(p => p.y)), bottom: Math.max(...values.map(p => p.y)) });
|
|
35
|
+
const actual = bounds(points), unit = bounds(origin);
|
|
36
|
+
const factor = unit.right !== unit.left ? (actual.right - actual.left) / (unit.right - unit.left)
|
|
37
|
+
: (actual.bottom - actual.top) / (unit.bottom - unit.top);
|
|
38
|
+
return { x: actual.left - factor * unit.left, y: actual.top - factor * unit.top };
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
const SHAPE_SET = new Set(POINT_SHAPES);
|
|
24
42
|
|
|
25
43
|
export function validatePointShape(shape) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cloneAndFreeze as
|
|
1
|
+
import{cloneAndFreeze as M,isPlainObject as D}from"../../core/immutable.js";import{MAX_GENERATED_ITEMS as T}from"../../core/validation.js";import{interpolateNumber as R,inverseLerp as w,numericExtent as q}from"../numeric.js";import{niceTicks as $}from"../ticks.js";import{niceLinearDomain as j}from"./continuous.js";import{SCALE_ROLES as L,validateScaleTypeForRole as _}from"./types.js";import{validatePair as I}from"./validation.js";const y=Object.freeze(["log","pow","sqrt","symlog"]);function P(t,e){if(!Number.isFinite(t)||t<=0)throw new RangeError(`${e} must be a positive finite number.`);return t}function p(t,e={}){if(!y.includes(t)&&t!=="linear")throw new Error(`Scale type "${t}" is not a quantitative transform.`);if(!D(e))throw new TypeError("Scale transform options must be a plain object.");const o=t==="log"?["base"]:t==="pow"?["exponent"]:t==="symlog"?["constant"]:[];for(const r of Object.keys(e))if(!o.includes(r))throw new Error(`Scale type "${t}" does not support ${r}.`);if(t==="log"){const r=P(e.base??10,"Log scale base");if(r===1)throw new RangeError("Log scale base must not equal 1.");return M({base:r})}return t==="pow"?M({exponent:P(e.exponent??1,"Power scale exponent")}):t==="sqrt"?M({exponent:.5}):t==="symlog"?M({constant:P(e.constant??1,"Symlog scale constant")}):M({})}function S(t,e,o,r){const n=[e,o,t].map(i=>Math.sign(i)*r(Math.abs(i)));return n[0]!==n[1]&&n.every(Number.isFinite)?w(n[2],n[0],n[1]):void 0}function z(t,e,o){const[r,n]=e,i=S(t,r,n,c=>c**o);if(i!==void 0)return i;const s=Math.max(Math.abs(r),Math.abs(n)),a=S(t,r,n,c=>c===0?0:Math.exp(o*(Math.log(c)-Math.log(s))));if(a!==void 0)return a;if(r!==0&&n!==0&&t!==0&&Math.sign(r)===Math.sign(n)){const c=Math.log(Math.abs(n))-Math.log(Math.abs(r)),m=Math.log(Math.abs(t))-Math.log(Math.abs(r));if(Math.abs(o*c)<1e-8)return m/c;const f=Math.expm1(o*m)/Math.expm1(o*c);if(Number.isFinite(f))return f}return w(t,r,n)}function x(t,e){if(t===0)return 0;const o=Math.abs(t)/e;if(Number.isFinite(o))return Math.sign(t)*Math.log1p(o);const r=Math.log(Math.abs(t))-Math.log(e);return Math.sign(t)*(Math.max(0,r)+Math.log1p(Math.exp(-Math.abs(r))))}function V(t,e,o,r){const[n,i]=e;if(o==="linear")return w(t,n,i);if(o==="log"){const m=Math.log10(r.base);return S(t,n,i,g=>Math.log10(g)/m)??S(t,n,i,Math.log)??w(t,n,i)}if(o==="pow"||o==="sqrt")return z(t,e,r.exponent);const s=x(n,r.constant),a=x(i,r.constant);if(s===a)return w(t,n,i);const c=w(x(t,r.constant),s,a);return Number.isFinite(c)?c:w(t,n,i)}function N(t,e,o={}){_(t,L.quantitativePosition);const r=I(e,"Transformed scale domain");if(r[0]===r[1])throw new RangeError("Transformed scale domain values must be distinct.");if(t==="log"&&(r.includes(0)||Math.sign(r[0])!==Math.sign(r[1])))throw new RangeError("Log scale domain must be strictly positive or strictly negative.");return p(t,o),r}function A(t,e,o){const r=Number.isFinite(t)?t**e:Number.NaN;if(Number.isFinite(r)&&r>0)return r;const n=e*o;return n>Math.log(Number.MAX_VALUE)?Number.POSITIVE_INFINITY:n<Math.log(Number.MIN_VALUE)?0:Math.exp(n)}function O(t,e){const o=Math.min(...t),r=Math.max(...t),n=r<0;return[e<1?-Math.log(e):Math.log(e),e<1?1/e:e,t[0]>t[1],n,n?Math.abs(r):o,n?Math.abs(o):r]}function C(t,e){const[o,r,n,i,s,a]=O(t,e);let c=A(r,Math.floor(Math.log(s)/o),o),m=A(r,Math.ceil(Math.log(a)/o),o);(!Number.isFinite(c)||c<=0||c>s)&&(c=s),(!Number.isFinite(m)||m<a)&&(m=a);const f=i?[-m,-c]:[c,m];return M(n?f.reverse():f)}function U(t,e,o){const r=p(t,o);if(e===0){if(t==="log")throw new RangeError("Log scale domain must be strictly positive or strictly negative.");return[-1,1]}const n=Math.abs(e);let i=n/2,s=n*2;if(t==="pow"||t==="sqrt")s=n*(2-2**-r.exponent)**(1/r.exponent);else if(t==="symlog"){const a=Math.abs(x(e,r.constant));if(a===0)return[-1,1];s=r.constant*Math.expm1(2*a-x(i,r.constant))}return i<n&&s>n&&Number.isFinite(s)||(i=n/2,s=Number.isFinite(n*2)?n*2:n),t==="log"&&i===0&&(i=n),e>0?[i,s]:[-s,i===0?0:-i]}function W({type:t,domain:e="auto",values:o,nice:r=!1,zero:n=!1,...i}){if(_(t,L.quantitativePosition),!Array.isArray(o)||!o.every(Number.isFinite))throw new TypeError("Transformed scale values must be finite numbers.");if(typeof r!="boolean"||typeof n!="boolean")throw new TypeError("Scale nice and zero must be booleans.");if(t==="log"&&n)throw new Error('Scale type "log" does not support zero.');if(e!=="auto")return N(t,e,i);if(o.length===0)throw new Error("Cannot infer an automatic scale domain from no values.");let a=q(o);return n&&(a=[Math.min(0,a[0]),Math.max(0,a[1])]),a[0]===a[1]&&(a=U(t,a[0],i)),N(t,a,i),r&&(a=t==="log"?C(a,p(t,i).base):j(a)),N(t,a,i)}function X(t,{reverse:e=!1}={}){const o=I(t,"Scale range");if(typeof e!="boolean")throw new TypeError("Scale reverse must be a boolean.");return e?M([...o].reverse()):o}function Z(t,e,o,{type:r="linear",clamp:n=!1,reverse:i=!1,...s}={}){if(!Array.isArray(t))throw new TypeError("Scale values must be an array.");if(typeof n!="boolean")throw new TypeError("Scale clamp must be a boolean.");const a=Object.hasOwn(s,"unknown"),{unknown:c,...m}=s,f=p(r,m),g=N(r,e,m),l=X(o,{reverse:i}),d=l[1]-l[0];return M(t.map(h=>{if(!(Number.isFinite(h)&&!(r==="log"&&(h===0||Math.sign(h)!==Math.sign(g[0]))))){if(a)return c;throw new TypeError(`Scale type "${r}" received an invalid value.`)}let u=V(h,g,r,f);n&&(u=Math.max(0,Math.min(1,u)));const b=l[0]+u*d;return Number.isFinite(d)&&Number.isFinite(b)&&(u!==1||Math.abs(b-l[1])<=Number.EPSILON*Math.abs(l[1]))?b:R(l[0],l[1],u)}))}function tt(t,e,o,r={}){const n=N(t,e,r);if(!Number.isInteger(o)||o<=0)throw new RangeError("Tick count must be a positive integer.");if(t!=="log")return $(n,o);const{base:i}=p(t,r),[s,a,c,m,f,g]=O(n,i),l=Math.ceil(Math.log(f)/s-1e-12),d=Math.floor(Math.log(g)/s+1e-12),h=d-l+1,E=h<=T?h:Math.min(o,T),u=[];for(let b=0;b<E;b+=1){const k=h<=T?l+b:Math.round(l+(d-l)*b/Math.max(1,E-1)),F=A(a,k,s),v=(m?-1:1)*F;Number.isFinite(v)&&F>=f&&F<=g&&(u.length===0||v!==u.at(-1))&&u.push(v)}return m&&u.reverse(),c&&u.reverse(),M(u.length===0?[...n]:u)}function rt(t,e,o={}){const r=N("symlog",t,o);if(!Number.isInteger(e)||e<2||e>T)throw new RangeError(`Sample count must be an integer from 2 to ${T}.`);const{constant:n}=p("symlog",o),i=r.map(m=>x(m,n)),s=Math.min(...r),a=Math.max(...r),c=Array.from({length:e},(m,f)=>{if(f===0)return r[0];if(f===e-1)return r[1];const g=f/(e-1);if(i[0]===i[1])return R(...r,g);const l=R(...i,g),d=Math.abs(l),h=n*Math.expm1(d),E=Math.sign(l)*(Number.isFinite(h)?h:Math.exp(Math.log(n)+d+Math.log1p(-Math.exp(-d))));return Math.max(s,Math.min(a,E))});if(s<0&&a>0&&e>2){const m=Math.round(w(0,...i)*(e-1));m>0&&m<e-1&&(c[m]=0)}return M(c.filter((m,f)=>f===0||m!==c[f-1]))}function nt(t,e){if(t!=="log")return String(e);const o=Math.abs(e),n=[[1e9,"B"],[1e6,"M"],[1e3,"K"]].find(([s])=>o>=s);if(n===void 0)return String(e);const i=e/n[0];return`${Number(i.toPrecision(3))}${n[1]}`}export{nt as formatTransformedTick,Z as mapTransformedValues,p as normalizeTransformParameters,X as resolveMappingRange,W as resolveTransformedDomain,rt as sampleSymlogValues,tt as transformedTicks,N as validateTransformedDomain};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { validateGeneratedItemLimit } from "../../core/validation.js";
|
|
2
2
|
import { validateFontStyle } from "../../core/font.js";
|
|
3
|
-
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import { isOwned, isPlainObject } from "../../core/immutable.js";
|
|
4
4
|
import { validatePathCommands } from "../pathCommands.js";
|
|
5
5
|
import { validateFillPaint } from "../paint.js";
|
|
6
6
|
import {
|
|
@@ -25,6 +25,7 @@ const TEXT_ALIGNS = new Set(["left", "right", "center", "start", "end"]);
|
|
|
25
25
|
const TEXT_BASELINES = new Set([
|
|
26
26
|
"top", "hanging", "middle", "alphabetic", "ideographic", "bottom"
|
|
27
27
|
]);
|
|
28
|
+
const validatedProperties = new WeakMap();
|
|
28
29
|
|
|
29
30
|
export function validateConcreteGraphicValue(type, property, value) {
|
|
30
31
|
if (property === "lines") {
|
|
@@ -98,6 +99,8 @@ export function validateConcreteGraphicValue(type, property, value) {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
export function validateConcreteGraphicProperties(type, properties) {
|
|
102
|
+
const validatedTypes = validatedProperties.get(properties);
|
|
103
|
+
if (validatedTypes?.has(type)) return properties;
|
|
101
104
|
if (!isPlainObject(properties)) {
|
|
102
105
|
throw new TypeError(`${type}.properties must be a plain object.`);
|
|
103
106
|
}
|
|
@@ -105,5 +108,9 @@ export function validateConcreteGraphicProperties(type, properties) {
|
|
|
105
108
|
validateGraphicProperty(type, property);
|
|
106
109
|
validateConcreteGraphicValue(type, property, value);
|
|
107
110
|
}
|
|
111
|
+
if (isOwned(properties)) {
|
|
112
|
+
if (validatedTypes) validatedTypes.add(type);
|
|
113
|
+
else validatedProperties.set(properties, new Set([type]));
|
|
114
|
+
}
|
|
108
115
|
return properties;
|
|
109
116
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
resolveTextBounds
|
|
4
4
|
} from "../../core/textMetrics.js";
|
|
5
5
|
import { interpolateNumber } from "../numeric.js";
|
|
6
|
+
import { isOwned } from "../../core/immutable.js";
|
|
6
7
|
import { graphicClipBounds, intersectClipBounds } from "./graphicClip.js";
|
|
7
8
|
import { resolveStrokeDetails } from "../strokeStyle.js";
|
|
8
9
|
import {
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
} from "./graphicTree.js";
|
|
13
14
|
|
|
14
15
|
const EPSILON = Number.EPSILON * 64;
|
|
16
|
+
const pathBounds = new WeakMap();
|
|
15
17
|
|
|
16
18
|
function box(left, right, top, bottom, extent = 0) {
|
|
17
19
|
return {
|
|
@@ -111,6 +113,10 @@ function cubicExtrema(start, control1, control2, end) {
|
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
export function resolvePathCommandBounds(commands) {
|
|
116
|
+
if (pathBounds.has(commands)) {
|
|
117
|
+
const cached = pathBounds.get(commands);
|
|
118
|
+
return cached === undefined ? undefined : { ...cached };
|
|
119
|
+
}
|
|
114
120
|
let bounds;
|
|
115
121
|
let current;
|
|
116
122
|
for (const command of commands) {
|
|
@@ -145,7 +151,8 @@ export function resolvePathCommandBounds(commands) {
|
|
|
145
151
|
}
|
|
146
152
|
current = { x: command.x, y: command.y };
|
|
147
153
|
}
|
|
148
|
-
|
|
154
|
+
if (isOwned(commands)) pathBounds.set(commands, bounds);
|
|
155
|
+
return bounds === undefined ? undefined : { ...bounds };
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
function endpointDirection(start, command, end, reverse = false) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isPlainObject } from "../../core/immutable.js";
|
|
1
|
+
import { isOwned, isPlainObject } from "../../core/immutable.js";
|
|
2
2
|
import { isGraphicContainerType } from "./graphic.js";
|
|
3
3
|
|
|
4
4
|
export function resolveGraphicChildOrder(graphicSpec, target, value) {
|
|
@@ -34,7 +34,7 @@ export function findGraphic(graphicSpec, target) {
|
|
|
34
34
|
return { id: target, object, kind: "object" };
|
|
35
35
|
}
|
|
36
36
|
for (const [ownerId, owner] of Object.entries(graphicSpec.objects)) {
|
|
37
|
-
const itemIndex = owner.items
|
|
37
|
+
const itemIndex = findItemIndex(owner.items, target);
|
|
38
38
|
if (itemIndex !== undefined && itemIndex !== -1) {
|
|
39
39
|
return {
|
|
40
40
|
id: target,
|
|
@@ -49,6 +49,22 @@ export function findGraphic(graphicSpec, target) {
|
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Collections are immutable across unrelated edits. Index their items once,
|
|
53
|
+
// instead of scanning every point/path again for each label or inspection.
|
|
54
|
+
// Caller-owned arrays remain uncached, including shallow-frozen inputs.
|
|
55
|
+
const itemIndexes = new WeakMap();
|
|
56
|
+
function findItemIndex(items, target) {
|
|
57
|
+
if (!items) return undefined;
|
|
58
|
+
if (!isOwned(items)) return items.findIndex(item => item.id === target);
|
|
59
|
+
let index = itemIndexes.get(items);
|
|
60
|
+
if (!index) {
|
|
61
|
+
index = new Map();
|
|
62
|
+
items.forEach((item, i) => { if (!index.has(item.id)) index.set(item.id, i); });
|
|
63
|
+
itemIndexes.set(items, index);
|
|
64
|
+
}
|
|
65
|
+
return index.get(target) ?? -1;
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
export function requireGraphic(graphicSpec, target) {
|
|
53
69
|
const found = findGraphic(graphicSpec, target);
|
|
54
70
|
if (found === undefined) {
|
|
@@ -68,6 +68,7 @@ const ENTITY_PATHS = Object.freeze({
|
|
|
68
68
|
"mark.orientation",
|
|
69
69
|
"mark.blend",
|
|
70
70
|
"derivedBindings.regression",
|
|
71
|
+
"derivedBindings.overlay",
|
|
71
72
|
"layout.mode",
|
|
72
73
|
...ENCODING_PATHS
|
|
73
74
|
])
|
|
@@ -106,6 +107,8 @@ const GUIDE_PATHS = new Set([
|
|
|
106
107
|
"legend.colorSize.colorScale",
|
|
107
108
|
"legend.colorSize.sizeScale",
|
|
108
109
|
"legend.colorSize.title",
|
|
110
|
+
"legend.colorSize.shapeScale",
|
|
111
|
+
"legend.colorSize.colorChannel",
|
|
109
112
|
"legend.color.scale",
|
|
110
113
|
"legend.color.title",
|
|
111
114
|
"legend.color.order",
|
|
@@ -11,6 +11,11 @@ export const STROKE_STYLE_PROPERTIES = Object.freeze([
|
|
|
11
11
|
"lineJoin",
|
|
12
12
|
"miterLimit"
|
|
13
13
|
]);
|
|
14
|
+
// Bounds and collision checks resolve this small vocabulary for every mark.
|
|
15
|
+
// Share the nine common immutable results rather than allocating per item;
|
|
16
|
+
// nondefault miter limits remain uncached so arbitrary user values stay bounded.
|
|
17
|
+
const commonStrokeDetails = new Map(LINE_CAPS.flatMap(lineCap => LINE_JOINS.map(lineJoin =>
|
|
18
|
+
[`${lineCap}:${lineJoin}`, freezeOwned({ lineCap, lineJoin, miterLimit: DEFAULT_MITER_LIMIT })])));
|
|
14
19
|
|
|
15
20
|
function validateEnum(value, values, property, label) {
|
|
16
21
|
if (typeof value !== "string") {
|
|
@@ -46,17 +51,18 @@ export function resolveStrokeDetails(request = {}) {
|
|
|
46
51
|
if (!isPlainObject(request)) {
|
|
47
52
|
throw new TypeError("Stroke style details must be a plain object.");
|
|
48
53
|
}
|
|
49
|
-
|
|
50
|
-
lineCap: request.lineCap === undefined
|
|
54
|
+
const lineCap = request.lineCap === undefined
|
|
51
55
|
? DEFAULT_LINE_CAP
|
|
52
|
-
: validateLineCap(request.lineCap)
|
|
53
|
-
|
|
56
|
+
: validateLineCap(request.lineCap);
|
|
57
|
+
const lineJoin = request.lineJoin === undefined
|
|
54
58
|
? DEFAULT_LINE_JOIN
|
|
55
|
-
: validateLineJoin(request.lineJoin)
|
|
56
|
-
|
|
59
|
+
: validateLineJoin(request.lineJoin);
|
|
60
|
+
const miterLimit = request.miterLimit === undefined
|
|
57
61
|
? DEFAULT_MITER_LIMIT
|
|
58
|
-
: validateMiterLimit(request.miterLimit)
|
|
59
|
-
|
|
62
|
+
: validateMiterLimit(request.miterLimit);
|
|
63
|
+
return miterLimit === DEFAULT_MITER_LIMIT
|
|
64
|
+
? commonStrokeDetails.get(`${lineCap}:${lineJoin}`)
|
|
65
|
+
: freezeOwned({ lineCap, lineJoin, miterLimit });
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
export function requestedStrokeDetails(request = {}, label = "Stroke style") {
|
package/src/grammar/text.js
CHANGED
|
@@ -118,6 +118,8 @@ export function formatTextValue(value, format = "auto") {
|
|
|
118
118
|
|
|
119
119
|
export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
|
|
120
120
|
const config = { ...base };
|
|
121
|
+
if (options.textOverrides === "auto") delete config.textOverrides;
|
|
122
|
+
else if (Object.hasOwn(options, "textOverrides")) config.textOverrides = normalizeTextOverrides(options.textOverrides);
|
|
121
123
|
if (Object.hasOwn(options, "lineHeight")) {
|
|
122
124
|
if (!Number.isFinite(options.lineHeight) || options.lineHeight <= 0) throw new RangeError("Text lineHeight must be positive and finite.");
|
|
123
125
|
config.lineHeight = options.lineHeight;
|
|
@@ -158,3 +160,73 @@ export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
|
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
export { resolveTextLines } from "../core/textMetrics.js";
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
export const isTextIdentityValue = value => value === null || typeof value === "string" ||
|
|
166
|
+
typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
167
|
+
export const textIdentity = (fields = {}, channels = {}) => Object.fromEntries(
|
|
168
|
+
Object.entries({ fields, channels }).map(([part, values]) => [part,
|
|
169
|
+
Object.fromEntries(Object.entries(values).filter(([, value]) => isTextIdentityValue(value)))]));
|
|
170
|
+
export const textIdentityKey = match => JSON.stringify(["fields", "channels"].map(part =>
|
|
171
|
+
Object.entries(match[part] ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
|
|
172
|
+
|
|
173
|
+
export function normalizeTextOverrides(value) {
|
|
174
|
+
if (!Array.isArray(value)) throw new TypeError("textOverrides must be an array or auto.");
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
for (const entry of value) {
|
|
177
|
+
if (!isPlainObject(entry) || Object.keys(entry).length !== 3 ||
|
|
178
|
+
!isPlainObject(entry.match) || typeof entry.text !== "string" ||
|
|
179
|
+
!Number.isInteger(entry.occurrence) || entry.occurrence < 0) {
|
|
180
|
+
throw new TypeError("Each text override requires match, occurrence and text.");
|
|
181
|
+
}
|
|
182
|
+
if (!Object.keys(entry.match).length || Object.keys(entry.match).some(part =>
|
|
183
|
+
!["fields", "channels"].includes(part) || !isPlainObject(entry.match[part]) ||
|
|
184
|
+
!Object.keys(entry.match[part]).length || !Object.values(entry.match[part]).every(isTextIdentityValue))) {
|
|
185
|
+
throw new TypeError("Text match requires nonempty fields or channels containing scalar values.");
|
|
186
|
+
}
|
|
187
|
+
const key = `${textIdentityKey(entry.match)}/${entry.occurrence}`;
|
|
188
|
+
if (seen.has(key)) throw new Error("Duplicate text override selector.");
|
|
189
|
+
seen.add(key);
|
|
190
|
+
}
|
|
191
|
+
return cloneAndFreeze(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Count semantic matches before visibility/placement filtering. Never address a graphic index.
|
|
195
|
+
export function resolveTextOverrides(items, overrides) {
|
|
196
|
+
const result = new Map();
|
|
197
|
+
if (!overrides?.length) return result;
|
|
198
|
+
const groups = new Map(), priorities = new Map();
|
|
199
|
+
for (const [priority, { match, occurrence, text }] of overrides.entries()) {
|
|
200
|
+
const keys = ["fields", "channels"].flatMap(part => Object.keys(match[part] ?? {}).sort().map(key => [part, key]));
|
|
201
|
+
const shape = JSON.stringify(keys);
|
|
202
|
+
if (!groups.has(shape)) groups.set(shape, { keys, values: new Map() });
|
|
203
|
+
const values = groups.get(shape).values, key = JSON.stringify(keys.map(([part, field]) => match[part][field]));
|
|
204
|
+
if (!values.has(key)) values.set(key, new Map());
|
|
205
|
+
values.get(key).set(occurrence, { text, priority });
|
|
206
|
+
}
|
|
207
|
+
// Complete identities from the editor share one shape: O(items + overrides),
|
|
208
|
+
// rather than scanning all items once for every previously edited label.
|
|
209
|
+
for (const { keys, values } of groups.values()) {
|
|
210
|
+
const counts = new Map();
|
|
211
|
+
for (const item of items) {
|
|
212
|
+
if (!keys.every(([part, field]) => Object.hasOwn(item[part] ?? {}, field) && isTextIdentityValue(item[part][field]))) continue;
|
|
213
|
+
const key = JSON.stringify(keys.map(([part, field]) => item[part][field]));
|
|
214
|
+
const candidates = values.get(key); if (!candidates) continue;
|
|
215
|
+
const occurrence = counts.get(key) ?? 0; counts.set(key, occurrence + 1);
|
|
216
|
+
const chosen = candidates.get(occurrence);
|
|
217
|
+
if (chosen && chosen.priority > (priorities.get(item) ?? -1)) {
|
|
218
|
+
result.set(item, chosen.text); priorities.set(item, chosen.priority);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function textIdentityOccurrences(items) {
|
|
226
|
+
const counts = new Map();
|
|
227
|
+
return items.map(item => {
|
|
228
|
+
const identity = textIdentity(item.fields, item.channels), key = textIdentityKey(identity);
|
|
229
|
+
const occurrence = counts.get(key) ?? 0; counts.set(key, occurrence + 1);
|
|
230
|
+
return { identity, occurrence };
|
|
231
|
+
});
|
|
232
|
+
}
|
package/src/inspection.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveGraphicClipBounds as B,resolveGraphicParentOffset as W}from"./grammar/schemas/graphicBounds.js";import{resolveFacetDefinition as K,resolveFacetGridDefinition as L}from"./grammar/facets/index.js";import{APPEARANCE_MARK_TYPES as V}from"./core/vocabulary.js";import{ChartProgram as E}from"./ChartProgram.js";import{BasicChartProgram as C}from"./BasicChartProgram.js";import{getWrappedActionMetadata as J}from"./core/action.js";import{cloneAndFreeze as b,isOwned as _,isPlainObject as S}from"./core/immutable.js";import{encodeValue as Y}from"./persistence/codec.js";import{findDataset as z,hasDatasetOwner as Z,resolveDatasetReference as F}from"./selectors/datasets.js";import{findLayer as G}from"./selectors/layers.js";import{findSemanticScale as H}from"./selectors/scales.js";import{findCoordinate as Q}from"./selectors/coordinates.js";import{inferDatasetSchema as X}from"./grammar/datasetSchema.js";import{BUILTIN_ACTION_DESCRIPTOR_RECORDS as ee}from"./inspectionDescriptors.js";const ne=new Set(["data","mark","scale","coordinate","guide"]),ie=new Map(ee.map(([e,n,i,t,c])=>[e,{options:n.map(([m,l,k,d,a])=>({path:m,required:l,kind:k,...d===null?{}:{choices:d},...a===null?{}:{unit:a}})),idOptions:i,prerequisites:t,entryPoints:c}]));function P(e,n="Program"){if(!(e instanceof E||e instanceof C))throw new TypeError(`${n} must be a ggaction program.`);if(e.actionStack.length!==0)throw new Error(`${n} must have a closed action stack.`);return e}function $(e,n=[]){if(!Array.isArray(n))throw new TypeError("childPath must be an array.");let i=e;for(const t of n){if(typeof t!="string"||!Object.hasOwn(i.children,t))throw new Error(`Unknown program child "${String(t)}".`);i=i.children[t]}return i}function R(e){if(!S(e)||!ne.has(e.kind)||typeof e.id!="string"||e.id.length===0)throw new TypeError("Inspection target requires a supported kind and non-empty id.");return e}function I(e,n){const i=$(e,n.childPath);return n.kind==="data"?Z(i,n.id)?F(i,n.id,"Inspection target"):z(i,n.id):n.kind==="mark"?G(i,n.id):n.kind==="scale"?H(i,n.id):n.kind==="coordinate"?Q(i,n.id):i.semanticSpec.guides[n.id]}function g(e,n,i){return{kind:e,id:n,...i?.length?{childPath:[...i]}:{}}}function te(e,{data:n,childPath:i}={}){P(e);const t=$(e,i);if(typeof n!="string"||n.length===0)throw new TypeError("getDatasetSchema requires data.");const c=F(t,n,"Dataset schema data"),m=c.schema??X(c.values??[]);return b({data:c.id,schema:m})}function be(e,{action:n,target:i,options:t={}}={}){if(P(e),typeof n!="string"||n.length===0)throw new TypeError("describeAction requires action.");if(!S(t))throw new TypeError("describeAction options must be a plain object.");let c;if(i!==void 0){if(R(i),c=I(e,i),c===void 0)return b({version:1,action:n,applicability:"incompatible",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"failed",evidencePaths:[],findings:[]}],findings:[{severity:"error",code:"missing-resource",reason:"target-unavailable",message:`Unknown ${i.kind} "${i.id}".`,resource:g(i.kind,i.id,i.childPath)}]});if(t.target!==void 0&&t.target!==i.id)return b({version:1,action:n,applicability:"incompatible",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"failed",evidencePaths:["target"],findings:[]}],findings:[{severity:"error",code:"incompatible-resource",reason:"target-mismatch",message:`Inspection target "${i.id}" conflicts with options.target "${t.target}".`,resource:g(i.kind,i.id,i.childPath),optionPath:"target"}]});const f=V[n];if(f&&(i.kind!=="mark"||!f.includes(c.mark?.type)))return b({version:1,action:n,applicability:"unsupported",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"not_applicable",evidencePaths:["target"],findings:[]}],findings:[{severity:"info",code:"unsupported-combination",reason:"mark-type",message:`${n} supports ${f.join(", ")} marks.`,resource:g(i.kind,i.id,i.childPath)}]})}const m=e[n],l=typeof m=="function"?J(m):void 0;if(l===void 0)return b({version:1,action:n,applicability:typeof m=="function"?"unverified":"unsupported",parameterDefinitions:[],requirements:[],checks:[],findings:[{severity:"info",code:"unsupported-format",reason:typeof m=="function"?"extension-unverified":"action-unavailable",message:`Action "${n}" has no built-in inspection descriptor.`}]});const k=ie.get(n),d=l.options??k?.options.map(f=>f.path);if(d===void 0)return b({version:1,action:n,applicability:"unverified",parameterDefinitions:[],requirements:[],checks:[{name:"execution",status:"not_run",evidencePaths:[],findings:[]}],findings:[{severity:"info",code:"unsupported-format",reason:"descriptor-unavailable",message:`Action "${n}" is registered but its static option descriptor is unavailable.`}]});const a=Object.keys(t).find(f=>!d.includes(f)),o=(k?.options.filter(f=>f.required).map(f=>f.path)??[]).filter(f=>t[f]===void 0);let p=[];if(c!==void 0&&i.kind==="mark"){const f=$(e,i.childPath),O=n.startsWith("edit")&&n.endsWith("Plot")?Object.entries(f.markConfigs[i.id]??{}).find(([r])=>r.toLowerCase()===n.slice(4).toLowerCase())?.[1]:void 0,x=O?.source===void 0?c.data:t.data??O.source,y=te(e,{data:x,childPath:i.childPath}).schema;if(y.completeness==="known"){const r=new Set(y.fields.map(v=>v.name));p=(k?.options??[]).filter(v=>v.kind==="field"&&typeof t[v.path]=="string"&&!r.has(t[v.path])).map(v=>({severity:"error",code:"missing-resource",reason:"field-unavailable",message:`Dataset does not contain field "${t[v.path]}".`,resource:g("data",x,i.childPath),optionPath:v.path}))}}if(a===void 0&&o.length===0&&(n==="facet"&&t.field||n==="facetGrid"&&t.rows&&t.columns))try{(n==="facet"?K:L)(e.semanticSpec,t)}catch(f){p.push({severity:"error",code:"unsupported-combination",reason:"facet-partition",message:f.message,optionPath:"data"})}const u=a!==void 0?[{severity:"error",code:"invalid-option",reason:"unknown-option",message:`Unknown ${n} option "${a}".`,optionPath:a}]:[...o.map(f=>({severity:"info",code:"missing-input",reason:"required-option",message:`${n} requires option "${f}".`,optionPath:f})),...p],h=a!==void 0||p.length?"incompatible":o.length?"needs-input":"supported";return b({version:1,action:n,applicability:h,parameterDefinitions:k?.options.map(f=>({...f,active:!0}))??d.map(f=>({path:f,kind:"object",required:!1,active:!0})),requirements:o.map(f=>({reason:"required-option",optionPath:f})),checks:[{name:"option-shape",status:a===void 0?"passed":"failed",evidencePaths:Object.keys(t),findings:a===void 0?[]:u},{name:"execution",status:"not_run",evidencePaths:[],findings:[]}],findings:u})}function T(e){return Array.isArray(e)?e.map(T):S(e)?Object.fromEntries(Object.keys(e).sort().map(n=>[n,T(e[n])])):e}function j(e){return JSON.stringify(Y(T(e)))}const D=new WeakMap;function M(e,n){if(Object.is(e,n))return!0;if(e===null||n===null||typeof e!="object"||typeof n!="object"||Array.isArray(e)!==Array.isArray(n))return!1;if(!Array.isArray(e)&&(!S(e)||!S(n)))return j(e)===j(n);const i=_(e)&&_(n),t=i?D.get(e)?.get(n):void 0;if(t!==void 0)return t;const c=Object.keys(e),m=(!Array.isArray(e)||e.length===n.length)&&c.length===Object.keys(n).length&&c.every(l=>Object.hasOwn(n,l)&&M(e[l],n[l]));return i&&(D.has(e)||D.set(e,new WeakMap),D.get(e).set(n,m)),m}function N(e,n=[],i=[]){for(const t of e.semanticSpec.datasets)i.push({ref:g("data",t.id,n),value:t});for(const[t,c]of e.semanticSpec.layers.entries())i.push({ref:g("mark",c.id,n),value:{semantic:c,graphic:e.graphicSpec.objects[c.id],config:e.markConfigs[c.id],orderIndex:t}});for(const t of e.semanticSpec.scales)i.push({ref:g("scale",t.id,n),value:{semantic:t,resolved:e.resolvedScales[t.id]}});for(const t of e.semanticSpec.coordinates)i.push({ref:g("coordinate",t.id,n),value:t});for(const[t,c]of Object.entries(e.semanticSpec.guides))i.push({ref:g("guide",t,n),value:{semantic:c,graphic:e.graphicSpec.objects[t],config:e.guideConfigs[t]}});Object.keys(e.semanticSpec.title).length&&i.push({ref:g("guide","title",n),value:{semantic:e.semanticSpec.title,config:e.titleConfig}}),e.graphicSpec.objects.canvas!==void 0&&i.push({ref:g("canvas","canvas",n),value:e.graphicSpec.objects.canvas}),Object.keys(e.compositionSpec??{}).length>0&&i.push({ref:g("composition","layout",n),value:e.compositionSpec});for(const[t,c]of Object.entries(e.children))N(c,[...n,t],i);return i}function A(e){return`${e.childPath?.join("/")??""}|${e.kind}|${e.id}`}function se(e){return e==="data"?"data":e==="scale"?"scale":e==="guide"?"guide":["coordinate","canvas","composition"].includes(e)?"structure":"style"}function re(e,n,i){return i!=="mark"||e===void 0||n===void 0?se(i):e.value.orderIndex!==n.value.orderIndex?"structure":!M(e.value.semantic?.encoding,n.value.semantic?.encoding)||e.value.semantic?.data!==n.value.semantic?.data?"binding":M(e.value.semantic,n.value.semantic)?"style":"structure"}function Se(e,n,{target:i}={}){P(e,"Before program"),P(n,"After program"),i!==void 0&&R(i);const t=new Map(N(e).map(a=>[A(a.ref),a])),c=new Map(N(n).map(a=>[A(a.ref),a]));let l=[...new Set([...t.keys(),...c.keys()])].sort().flatMap(a=>{const s=t.get(a),o=c.get(a),p=s??o;if(s!==void 0&&o!==void 0&&M(s.value,o.value))return[];const u=p.ref.kind==="mark"&&s!==void 0&&o!==void 0?[...new Set([...Object.keys(s.value.semantic?.encoding??{}),...Object.keys(o.value.semantic?.encoding??{})])].filter(h=>!M(s.value.semantic?.encoding?.[h],o.value.semantic?.encoding?.[h])):[];return[{resource:p.ref,operation:s===void 0?"add":o===void 0?"remove":"modify",effect:re(s,o,p.ref.kind),roles:u}]});if(i!==void 0){const a=[e,n].flatMap(o=>{const p=I(o,i);return p===void 0?[]:[A(g(i.kind,i.kind==="data"?p.id:i.id,i.childPath))]});if(a.length===0)throw new Error(`Unknown ${i.kind} "${i.id}" in both programs.`);const s=new Set(a);for(const o of[t,c]){let p=!0;for(;p;){p=!1;for(const[u,h]of o){if(s.has(u))continue;const f=j(h.ref.childPath??[]);[...s].some(x=>{const y=o.get(x);return y===void 0||j(y.ref.childPath??[])!==f?!1:y.ref.kind==="data"?h.ref.kind==="data"?h.value.source===y.ref.id:h.ref.kind==="mark"&&h.value.semantic?.data===y.ref.id:y.ref.kind==="mark"?h.ref.kind==="mark"&&h.value.semantic?.source===y.ref.id||h.ref.kind==="guide"&&(h.value.semantic?.target===y.ref.id||h.value.config?.target===y.ref.id):y.ref.kind==="scale"?h.ref.kind==="mark"&&Object.values(h.value.semantic?.encoding??{}).some(r=>r?.scale===y.ref.id||r?.dimensions?.some(v=>v.scale===y.ref.id))||h.ref.kind==="guide"&&(h.value.semantic?.scale===y.ref.id||h.value.config?.scale===y.ref.id):y.ref.kind==="coordinate"&&h.ref.kind==="mark"&&h.value.semantic?.coordinate===y.ref.id})&&(s.add(u),p=!0)}}}l=l.filter(o=>s.has(A(o.resource)))}const k=![E,C].includes(e.constructor)||![E,C].includes(n.constructor),d=k?[{severity:"info",code:"unsupported-format",reason:"extension-unverified",message:"Extension-owned semantic equivalence could not be fully verified."}]:[];return b({version:1,equivalence:l.length?"different":k?"unknown":"equal",completeProgramComparison:i===void 0,changes:l,affected:l.map(a=>a.resource),findings:d})}function q(e,n){if(!S(e))return null;if(n==="circle")return!Number.isFinite(e.x)||!Number.isFinite(e.y)||!Number.isFinite(e.radius)||e.radius<0?null:{x:e.x-e.radius,y:e.y-e.radius,width:e.radius*2,height:e.radius*2};if(n==="rect")return[e.x,e.y,e.width,e.height].every(Number.isFinite)?{x:Math.min(e.x,e.x+e.width),y:Math.min(e.y,e.y+e.height),width:Math.abs(e.width),height:Math.abs(e.height)}:null;if(n==="path"&&Array.isArray(e.commands)){const i=e.commands.flatMap(m=>Number.isFinite(m.x)&&Number.isFinite(m.y)?[[m.x,m.y]]:[]);if(i.length===0)return null;const t=i.map(m=>m[0]),c=i.map(m=>m[1]);return{x:Math.min(...t),y:Math.min(...c),width:Math.max(...t)-Math.min(...t),height:Math.max(...c)-Math.min(...c)}}return[e.x1,e.y1,e.x2,e.y2].every(Number.isFinite)?{x:Math.min(e.x1,e.x2),y:Math.min(e.y1,e.y2),width:Math.abs(e.x2-e.x1),height:Math.abs(e.y2-e.y1)}:null}function ce(e,n,i){const t=e.graphicSpec.objects[n.id],c=z(e,n.data),m=new Set(["circle","rect","line","path"]),l=t?.items??(t?.properties?[{properties:t.properties}]:[]),k=l.map(r=>r.type??t?.type),d=t!==void 0&&k.every(r=>m.has(r)||r==="text"),a=d&&k.every(r=>m.has(r)),s=l.filter(r=>r.properties.opacity===0).length,o=l.filter(r=>{const v=q(r.properties,r.type??t?.type);return v!==null&&v.width===0&&v.height===0}).length,p=l.map(r=>q(r.properties,r.type??t?.type)).filter(Boolean),u=t===void 0?void 0:B(e.graphicSpec,n.id,{rootViewport:!0}),h=t===void 0?[0,0]:W(e.graphicSpec,n.id),f=u===void 0?null:l.flatMap((r,v)=>{const w=q(r.properties,r.type??t.type);return w!==null&&(u.right<=u.left||u.bottom<=u.top||w.x+h[0]+w.width<u.left||w.y+h[1]+w.height<u.top||w.x+h[0]>u.right||w.y+h[1]>u.bottom)?[v]:[]}),O=f?.length??null,x=p.length===0?null:{x:Math.min(...p.map(r=>r.x)),y:Math.min(...p.map(r=>r.y)),width:Math.max(...p.map(r=>r.x+r.width))-Math.min(...p.map(r=>r.x)),height:Math.max(...p.map(r=>r.y+r.height))-Math.min(...p.map(r=>r.y))},y=["line","area"].includes(n.mark?.type)?"series":"item";return{owner:g("mark",n.id,i),grain:y,rawRows:c?.values?.length??null,logicalDataItems:d?l.length:null,drawablePrimitives:d?l.length:null,visibleCandidates:a?Math.max(0,l.length-new Set([...l.map((r,v)=>r.properties.opacity===0?v:-1),...l.map((r,v)=>{const w=q(r.properties,r.type??t?.type);return w!==null&&w.width===0&&w.height===0?v:-1}),...f??[]].filter(r=>r>=0)).size):null,flags:[{reason:"opacity-zero",count:s},{reason:"zero-size",count:o},{reason:"outside-clip",count:O},{reason:"missing-skipped",count:n.mark?.missing==="skip"&&c?.values?Math.max(0,c.values.length-l.length):null}],bounds:x,sourceRefs:c===void 0?[]:[g("data",c.id,i)],checks:[{name:"concrete-structure",status:t===void 0&&!e.compositionSpec?.facet?"failed":d?"passed":"not_applicable",evidencePaths:[n.id],findings:[]},{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}]}}function U(e,n,i){const t=new Set(e.semanticSpec.layers.map(s=>s.id)),c=n==="axis"?/axis/iu:n==="grid"?/grid/iu:n==="legend"?/legend/iu:n==="title"?/title|subtitle/iu:new RegExp(n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"iu"),m=Object.keys(e.graphicSpec.objects).filter(s=>s!=="canvas"&&!s.startsWith("plot-")&&!t.has(s)&&c.test(s)),l=m.flatMap(s=>{const o=e.graphicSpec.objects[s];return Array.isArray(o?.items)?o.items:o?.properties===void 0?[]:[{properties:o.properties}]}),k=n==="title"?e.semanticSpec.title:e.semanticSpec.guides[n],d=[];function a(s){if(S(s)){typeof s.scale=="string"&&d.push(g("scale",s.scale,i)),typeof s.coordinate=="string"&&d.push(g("coordinate",s.coordinate,i));for(const o of Object.values(s))a(o)}}return a(k),{owner:g("guide",n,i),grain:"unknown",rawRows:null,logicalDataItems:l.length,drawablePrimitives:l.length,visibleCandidates:null,flags:[{reason:"opacity-zero",count:null},{reason:"zero-size",count:null},{reason:"outside-clip",count:null},{reason:"missing-skipped",count:null}],bounds:null,sourceRefs:[...new Map(d.map(s=>[A(s),s])).values()],checks:[{name:"concrete-structure",status:m.length>0?"passed":"not_applicable",evidencePaths:m,findings:[]},{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}]}}function xe(e,{target:n}={}){P(e),n!==void 0&&R(n);const i=[];function t(d,a=[]){const s=new Set,o=new Set;function p(u){if(o.has(u))throw new Error(`Graphic hierarchy contains a cycle at "${u}".`);if(s.has(u))throw new Error(`Graphic "${u}" is referenced more than once.`);const h=d.graphicSpec.objects[u];if(h===void 0)throw new Error(`Graphic hierarchy references missing object "${u}".`);s.add(u),o.add(u);for(const f of h.children??[])p(f);o.delete(u)}for(const u of d.graphicSpec.order)p(u);for(const u of d.semanticSpec.layers)i.push(ce(d,u,a));for(const u of Object.keys(d.semanticSpec.guides))i.push(U(d,u,a));Object.keys(d.semanticSpec.title).length>0&&i.push(U(d,"title",a));for(const[u,h]of Object.entries(d.children))t(h,[...a,u])}t(e);let c=i;if(n!==void 0){const d=$(e,n.childPath),a=I(e,n);if(a===void 0)throw new Error(`Unknown ${n.kind} "${n.id}".`);c=i.filter(s=>{if(j(s.owner.childPath??[])!==j(n.childPath??[]))return!1;if(n.kind==="mark")return s.owner.id===n.id;const o=G(d,s.owner.id);return n.kind==="data"?o?.data===a.id:n.kind==="coordinate"?o?.coordinate===n.id:n.kind==="scale"?Object.values(o?.encoding??{}).some(p=>p?.scale===n.id||p?.dimensions?.some(u=>u.scale===n.id)):n.kind==="guide"?s.owner.kind==="guide"&&s.owner.id===n.id:!1})}const m=[];function l(d,a=[]){for(const s of Object.values(d.materializationConfigs.calculations??{}))if(S(s))for(const o of Object.values(s))m.push(a.length===0?o:{...o,childPath:a});for(const[s,o]of Object.entries(d.children))l(o,[...a,s])}l(e);const k=c.filter(d=>d.logicalDataItems===null).map(d=>d.owner);return b({version:1,views:c,calculations:m,checks:[{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}],findings:[],coverage:{partial:c.some(d=>d.checks.some(a=>a.status!=="passed")),unsupported:k}})}export{Se as comparePrograms,be as describeAction,te as getDatasetSchema,xe as inspectProgram};
|
|
1
|
+
import{resolveMarkItems as Q}from"./materialization/selection/policies/index.js";import{highlightTargets as ee}from"./selectors/selectionTargets.js";import{resolvePathCommandBounds as ne,unionConcreteGraphicBounds as ie}from"./grammar/schemas/graphicBounds.js";import{facetUnitTemplate as U}from"./actions/facets/actions.js";import{plotComposition as te}from"./selectors/plotComposition.js";import{overlaySourceReason as L}from"./grammar/overlay.js";import{findUpstreamTransform as W}from"./materialization/dataProvenance.js";import{planLayerPlot as se}from"./actions/composition/layerPlan.js";import{resolveAdjacentSource as re}from"./materialization/adjacentSource.js";import{normalizeActionTargets as oe}from"./grammar/actionTargets.js";import{resolveGraphicClipBounds as ce,resolveGraphicParentOffset as ae}from"./grammar/schemas/graphicBounds.js";import{resolveFacetDefinition as de,resolveFacetGridDefinition as ue}from"./grammar/facets/index.js";import{APPEARANCE_MARK_TYPES as D,FIXED_APPEARANCE_MARK_TYPES as K}from"./core/vocabulary.js";import{ChartProgram as I}from"./ChartProgram.js";import{BasicChartProgram as R}from"./BasicChartProgram.js";import{getWrappedActionMetadata as fe,supportsScopedActionTargets as le}from"./core/action.js";import{cloneAndFreeze as S,isOwned as V,isPlainObject as M}from"./core/immutable.js";import{encodeValue as pe}from"./persistence/codec.js";import{findDataset as C,hasDatasetOwner as me,resolveDatasetReference as Y}from"./selectors/datasets.js";import{findLayer as _,resolveEligibleLayer as he}from"./selectors/layers.js";import"./selectors/markOwners.js";import{normalizeCompositionShares as ye}from"./core/compositionScales.js";import{findSemanticScale as ve}from"./selectors/scales.js";import{findCoordinate as F}from"./selectors/coordinates.js";import{inferDatasetSchema as ge}from"./grammar/datasetSchema.js";import{BUILTIN_ACTION_DESCRIPTOR_RECORDS as ke}from"./inspectionDescriptors.js";const we=new Set(["data","mark","scale","coordinate","guide"]),be=new Map(ke.map(([e,n,i,t,a])=>[e,{options:n.map(([h,m,g,l,d])=>({path:h,required:m,kind:g,...l===null?{}:{choices:l},...d===null?{}:{unit:d}})),idOptions:i,prerequisites:t,entryPoints:a}]));function O(e,n="Program"){if(!(e instanceof I||e instanceof R))throw new TypeError(`${n} must be a ggaction program.`);if(e.actionStack.length!==0)throw new Error(`${n} must have a closed action stack.`);return e}function P(e,n=[]){if(!Array.isArray(n))throw new TypeError("childPath must be an array.");let i=e;for(const t of n){if(typeof t!="string"||!Object.hasOwn(i.children,t))throw new Error(`Unknown program child "${String(t)}".`);i=i.children[t]}return i}function B(e){if(!M(e)||!we.has(e.kind)||typeof e.id!="string"||e.id.length===0)throw new TypeError("Inspection target requires a supported kind and non-empty id.");return e}function N(e,n){const i=P(e,n.childPath);return n.kind==="data"?me(i,n.id)?Y(i,n.id,"Inspection target"):C(i,n.id):n.kind==="mark"?_(i,n.id):n.kind==="scale"?ve(i,n.id):n.kind==="coordinate"?F(i,n.id):i.semanticSpec.guides[n.id]}function w(e,n,i){return{kind:e,id:n,...i?.length?{childPath:[...i]}:{}}}function en(e,{target:n,childPath:i}={}){return O(e),M(n)&&(i=n.childPath,n=n.id),S(te(P(e,i),n))}function Se(e,{data:n,childPath:i}={}){O(e);const t=P(e,i);if(typeof n!="string"||n.length===0)throw new TypeError("getDatasetSchema requires data.");const a=Y(t,n,"Dataset schema data"),h=a.schema??ge(a.values??[]);return S({data:a.id,schema:h})}function xe(e,{action:n,target:i,options:t={}}={}){if(O(e),!M(t))throw new TypeError("describeAction options must be a plain object.");if(M(t.target)||Array.isArray(t.target)){if(O(e),i!==void 0)throw new Error("Use one inspection target or explicit action target selectors, not both.");if(!le(e.constructor,n))return S({version:1,action:n,applicability:"unsupported",parameterDefinitions:[],requirements:[],checks:[],findings:[{severity:"info",code:"unsupported-combination",reason:"entry-point",message:"Scoped action targets require a Full domain action."}]});const{target:s,...x}=t,y=oe(s).map(v=>{let j=e;for(const H of v.childPath){if(j.compositionSpec?.type==="facet")throw new Error("Facet cells follow their source; use editFacetSource for persistent edits.");j=P(j,[H])}if(v.facetSource){if(j.compositionSpec?.type!=="facet")throw new Error("facetSource requires a facet view.");j=U(j,!0)}if(j.compositionSpec?.type==="facet"&&v.id!==void 0)throw new Error("Target a facet source through editFacetSource.");const Z=v.id===void 0||n.includes("CompositionChild")?void 0:{kind:"mark",id:v.id};return xe(j,{action:n,target:Z,options:{...x,...v.id===void 0?{}:{target:v.id}}})}),k=["incompatible","unsupported","needs-input","unverified","supported"].find(v=>y.some(j=>j.applicability===v));return S({...y[0],applicability:k,requirements:y.flatMap(v=>v.requirements),findings:y.flatMap(v=>v.findings),checks:y.flatMap(v=>v.checks)})}if(O(e),typeof n!="string"||n.length===0)throw new TypeError("describeAction requires action.");if(!M(t))throw new TypeError("describeAction options must be a plain object.");let a;if(i!==void 0){if(B(i),a=N(e,i),a===void 0)return S({version:1,action:n,applicability:"incompatible",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"failed",evidencePaths:[],findings:[]}],findings:[{severity:"error",code:"missing-resource",reason:"target-unavailable",message:`Unknown ${i.kind} "${i.id}".`,resource:w(i.kind,i.id,i.childPath)}]});if(t.target!==void 0&&t.target!==i.id)return S({version:1,action:n,applicability:"incompatible",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"failed",evidencePaths:["target"],findings:[]}],findings:[{severity:"error",code:"incompatible-resource",reason:"target-mismatch",message:`Inspection target "${i.id}" conflicts with options.target "${t.target}".`,resource:w(i.kind,i.id,i.childPath),optionPath:"target"}]});const s=t.field!==void 0?D[n]:K[n]??D[n];if(s&&(i.kind!=="mark"||!s.includes(a.mark?.type)))return S({version:1,action:n,applicability:"unsupported",parameterDefinitions:[],requirements:[],checks:[{name:"target",status:"not_applicable",evidencePaths:["target"],findings:[]}],findings:[{severity:"info",code:"unsupported-combination",reason:"mark-type",message:`${n} supports ${s.join(", ")} marks.`,resource:w(i.kind,i.id,i.childPath)}]})}const h=e[n],m=typeof h=="function"?fe(h):void 0;if(m===void 0)return S({version:1,action:n,applicability:typeof h=="function"?"unverified":"unsupported",parameterDefinitions:[],requirements:[],checks:[],findings:[{severity:"info",code:"unsupported-format",reason:typeof h=="function"?"extension-unverified":"action-unavailable",message:`Action "${n}" has no built-in inspection descriptor.`}]});let g=be.get(n);a?.mark&&K[n]?.includes(a.mark.type)&&!D[n].includes(a.mark.type)&&(g={...g,options:g.options.filter(s=>!["field","fieldType","scale"].includes(s.path))});const l=m.options??g?.options.map(s=>s.path);if(l===void 0)return S({version:1,action:n,applicability:"unverified",parameterDefinitions:[],requirements:[],checks:[{name:"execution",status:"not_run",evidencePaths:[],findings:[]}],findings:[{severity:"info",code:"unsupported-format",reason:"descriptor-unavailable",message:`Action "${n}" is registered but its static option descriptor is unavailable.`}]});const d=Object.keys(t).find(s=>!l.includes(s)),r=(g?.options.filter(s=>s.required).map(s=>s.path)??[]).filter(s=>t[s]===void 0);let p=[];if(n==="createLayerPlot")try{const s=P(e,i?.childPath);se(s.compositionSpec?.type==="facet"?U(s,!0):s,{...t,source:t.source??i?.id})}catch(s){p.push({severity:"error",code:"unsupported-combination",reason:"layer-compatibility",message:s.message})}if(n==="createAdjacentPlot"){const s=P(e,i?.childPath);try{re(s,t.source??i?.id)}catch(x){t.source||i?.id||s.compositionSpec&&s.compositionSpec.type!=="facet"?p.push({severity:"error",code:"missing-resource",reason:"adjacent-source",message:x.message}):r.push("source")}}if(n==="editCompositionScales"&&((!e.compositionSpec||e.compositionSpec.type==="facet")&&p.push({severity:"error",code:"unsupported-combination",reason:"composition-scope",message:"editCompositionScales requires a concat parent."}),t.share!==void 0))try{if(!Object.keys(ye(t.share)).length)throw new Error("At least one shared-channel policy is required.")}catch(s){p.push({severity:"error",code:"invalid-option",reason:"share-policy",message:s.message,optionPath:"share"})}if(["createOverlay","editOverlay"].includes(n)){const s=P(e,i?.childPath),x=n==="createOverlay"?t.source??i?.id??s.context.currentMark:t.target??i?.id??s.context.currentMark,b=_(s,x);b===void 0?r.push(n==="createOverlay"?"source":"target"):(n==="editOverlay"?!b.derivedBindings?.overlay:L(b,F(s,b.coordinate)?.type,W(s,C(s,b.data),"density")))?p.push({severity:"error",code:"unsupported-combination",reason:"overlay-source",message:n==="createOverlay"?L(b,F(s,b.coordinate)?.type,W(s,C(s,b.data),"density")):"editOverlay requires a linked overlay target."}):n==="createOverlay"&&b.mark.type==="point"&&t.type!=="point"&&!t.orderBy&&r.push("orderBy")}if(a!==void 0&&i.kind==="mark"){const s=P(e,i.childPath),x=n.startsWith("edit")&&n.endsWith("Plot")?Object.entries(s.markConfigs[i.id]??{}).find(([u])=>u.toLowerCase()===n.slice(4).toLowerCase())?.[1]:void 0,b=x?.source===void 0?a.data:t.data??x.source,y=Se(e,{data:b,childPath:i.childPath}).schema;if(y.completeness==="known"){const u=new Set(y.fields.map(k=>k.name));p.push(...(g?.options??[]).filter(k=>k.kind==="field"&&typeof t[k.path]=="string"&&!u.has(t[k.path])).map(k=>({severity:"error",code:"missing-resource",reason:"field-unavailable",message:`Dataset does not contain field "${t[k.path]}".`,resource:w("data",b,i.childPath),optionPath:k.path})))}}if(d===void 0&&r.length===0&&(n==="facet"&&t.field||n==="facetGrid"&&t.rows&&t.columns))try{(n==="facet"?de:ue)(e.semanticSpec,t)}catch(s){p.push({severity:"error",code:"unsupported-combination",reason:"facet-partition",message:s.message,optionPath:"data"})}const f=d!==void 0?[{severity:"error",code:"invalid-option",reason:"unknown-option",message:`Unknown ${n} option "${d}".`,optionPath:d}]:[...r.map(s=>({severity:"info",code:"missing-input",reason:"required-option",message:`${n} requires option "${s}".`,optionPath:s})),...p],c=d!==void 0||p.length?"incompatible":r.length?"needs-input":"supported";return S({version:1,action:n,applicability:c,parameterDefinitions:g?.options.map(s=>({...s,active:!0}))??l.map(s=>({path:s,kind:"object",required:!1,active:!0})),requirements:r.map(s=>({reason:"required-option",optionPath:s})),checks:[{name:"option-shape",status:d===void 0?"passed":"failed",evidencePaths:Object.keys(t),findings:d===void 0?[]:f},{name:"execution",status:"not_run",evidencePaths:[],findings:[]}],findings:f})}function z(e){return Array.isArray(e)?e.map(z):M(e)?Object.fromEntries(Object.keys(e).sort().map(n=>[n,z(e[n])])):e}function A(e){return JSON.stringify(pe(z(e)))}const $=new WeakMap;function E(e,n){if(Object.is(e,n))return!0;if(e===null||n===null||typeof e!="object"||typeof n!="object"||Array.isArray(e)!==Array.isArray(n))return!1;if(!Array.isArray(e)&&(!M(e)||!M(n)))return A(e)===A(n);const i=V(e)&&V(n),t=i?$.get(e)?.get(n):void 0;if(t!==void 0)return t;const a=Object.keys(e),h=(!Array.isArray(e)||e.length===n.length)&&a.length===Object.keys(n).length&&a.every(m=>Object.hasOwn(n,m)&&E(e[m],n[m]));return i&&($.has(e)||$.set(e,new WeakMap),$.get(e).set(n,h)),h}function G(e,n=[],i=[]){for(const t of e.semanticSpec.datasets)i.push({ref:w("data",t.id,n),value:t});for(const[t,a]of e.semanticSpec.layers.entries())i.push({ref:w("mark",a.id,n),value:{semantic:a,graphic:e.graphicSpec.objects[a.id],config:e.markConfigs[a.id],orderIndex:t}});for(const t of e.semanticSpec.scales)i.push({ref:w("scale",t.id,n),value:{semantic:t,resolved:e.resolvedScales[t.id]}});for(const t of e.semanticSpec.coordinates)i.push({ref:w("coordinate",t.id,n),value:t});for(const[t,a]of Object.entries(e.semanticSpec.guides))i.push({ref:w("guide",t,n),value:{semantic:a,graphic:e.graphicSpec.objects[t],config:e.guideConfigs[t]}});Object.keys(e.semanticSpec.title).length&&i.push({ref:w("guide","title",n),value:{semantic:e.semanticSpec.title,config:e.titleConfig}}),e.graphicSpec.objects.canvas!==void 0&&i.push({ref:w("canvas","canvas",n),value:e.graphicSpec.objects.canvas}),Object.keys(e.compositionSpec??{}).length>0&&i.push({ref:w("composition","layout",n),value:e.compositionSpec});for(const[t,a]of Object.entries(e.children))G(a,[...n,t],i);return i}function q(e){return`${e.childPath?.join("/")??""}|${e.kind}|${e.id}`}function Me(e){return e==="data"?"data":e==="scale"?"scale":e==="guide"?"guide":["coordinate","canvas","composition"].includes(e)?"structure":"style"}function je(e,n,i){return i!=="mark"||e===void 0||n===void 0?Me(i):e.value.orderIndex!==n.value.orderIndex?"structure":!E(e.value.semantic?.encoding,n.value.semantic?.encoding)||e.value.semantic?.data!==n.value.semantic?.data?"binding":E(e.value.semantic,n.value.semantic)?"style":"structure"}function nn(e,n,{target:i}={}){O(e,"Before program"),O(n,"After program"),i!==void 0&&B(i);const t=new Map(G(e).map(d=>[q(d.ref),d])),a=new Map(G(n).map(d=>[q(d.ref),d]));let m=[...new Set([...t.keys(),...a.keys()])].sort().flatMap(d=>{const o=t.get(d),r=a.get(d),p=o??r;if(o!==void 0&&r!==void 0&&E(o.value,r.value))return[];const f=p.ref.kind==="mark"&&o!==void 0&&r!==void 0?[...new Set([...Object.keys(o.value.semantic?.encoding??{}),...Object.keys(r.value.semantic?.encoding??{})])].filter(c=>!E(o.value.semantic?.encoding?.[c],r.value.semantic?.encoding?.[c])):[];return[{resource:p.ref,operation:o===void 0?"add":r===void 0?"remove":"modify",effect:je(o,r,p.ref.kind),roles:f}]});if(i!==void 0){const d=[e,n].flatMap(r=>{const p=N(r,i);return p===void 0?[]:[q(w(i.kind,i.kind==="data"?p.id:i.id,i.childPath))]});if(d.length===0)throw new Error(`Unknown ${i.kind} "${i.id}" in both programs.`);const o=new Set(d);for(const r of[t,a]){let p=!0;for(;p;){p=!1;for(const[f,c]of r){if(o.has(f))continue;const s=A(c.ref.childPath??[]);[...o].some(b=>{const y=r.get(b);return y===void 0||A(y.ref.childPath??[])!==s?!1:y.ref.kind==="data"?c.ref.kind==="data"?c.value.source===y.ref.id:c.ref.kind==="mark"&&c.value.semantic?.data===y.ref.id:y.ref.kind==="mark"?c.ref.kind==="mark"&&c.value.semantic?.source===y.ref.id||c.ref.kind==="guide"&&(c.value.semantic?.target===y.ref.id||c.value.config?.target===y.ref.id):y.ref.kind==="scale"?c.ref.kind==="mark"&&Object.values(c.value.semantic?.encoding??{}).some(u=>u?.scale===y.ref.id||u?.dimensions?.some(k=>k.scale===y.ref.id))||c.ref.kind==="guide"&&(c.value.semantic?.scale===y.ref.id||c.value.config?.scale===y.ref.id):y.ref.kind==="coordinate"&&c.ref.kind==="mark"&&c.value.semantic?.coordinate===y.ref.id})&&(o.add(f),p=!0)}}}m=m.filter(r=>o.has(q(r.resource)))}const g=![I,R].includes(e.constructor)||![I,R].includes(n.constructor),l=g?[{severity:"info",code:"unsupported-format",reason:"extension-unverified",message:"Extension-owned semantic equivalence could not be fully verified."}]:[];return S({version:1,equivalence:m.length?"different":g?"unknown":"equal",completeProgramComparison:i===void 0,changes:m,affected:m.map(d=>d.resource),findings:l})}function T(e,n){if(!M(e))return null;if(n==="circle")return!Number.isFinite(e.x)||!Number.isFinite(e.y)||!Number.isFinite(e.radius)||e.radius<0?null:{x:e.x-e.radius,y:e.y-e.radius,width:e.radius*2,height:e.radius*2};if(n==="rect")return[e.x,e.y,e.width,e.height].every(Number.isFinite)?{x:Math.min(e.x,e.x+e.width),y:Math.min(e.y,e.y+e.height),width:Math.abs(e.width),height:Math.abs(e.height)}:null;if(n==="path"&&Array.isArray(e.commands)){const i=e.commands.flatMap(h=>Number.isFinite(h.x)&&Number.isFinite(h.y)?[[h.x,h.y]]:[]);if(i.length===0)return null;const t=i.map(h=>h[0]),a=i.map(h=>h[1]);return{x:Math.min(...t),y:Math.min(...a),width:Math.max(...t)-Math.min(...t),height:Math.max(...a)-Math.min(...a)}}return[e.x1,e.y1,e.x2,e.y2].every(Number.isFinite)?{x:Math.min(e.x1,e.x2),y:Math.min(e.y1,e.y2),width:Math.abs(e.x2-e.x1),height:Math.abs(e.y2-e.y1)}:null}function Pe(e,n,i){const t=e.graphicSpec.objects[n.id],a=C(e,n.data),h=new Set(["circle","rect","line","path"]),m=t?.items??(t?.properties?[{properties:t.properties}]:[]),g=m.map(u=>u.type??t?.type),l=t!==void 0&&g.every(u=>h.has(u)||u==="text"),d=l&&g.every(u=>h.has(u)),o=m.filter(u=>u.properties.opacity===0).length,r=m.filter(u=>{const k=T(u.properties,u.type??t?.type);return k!==null&&k.width===0&&k.height===0}).length,p=m.map(u=>T(u.properties,u.type??t?.type)).filter(Boolean),f=t===void 0?void 0:ce(e.graphicSpec,n.id,{rootViewport:!0}),c=t===void 0?[0,0]:ae(e.graphicSpec,n.id),s=f===void 0?null:m.flatMap((u,k)=>{const v=T(u.properties,u.type??t.type);return v!==null&&(f.right<=f.left||f.bottom<=f.top||v.x+c[0]+v.width<f.left||v.y+c[1]+v.height<f.top||v.x+c[0]>f.right||v.y+c[1]>f.bottom)?[k]:[]}),x=s?.length??null,b=p.length===0?null:{x:Math.min(...p.map(u=>u.x)),y:Math.min(...p.map(u=>u.y)),width:Math.max(...p.map(u=>u.x+u.width))-Math.min(...p.map(u=>u.x)),height:Math.max(...p.map(u=>u.y+u.height))-Math.min(...p.map(u=>u.y))},y=["line","area"].includes(n.mark?.type)?"series":"item";return{owner:w("mark",n.id,i),grain:y,rawRows:a?.values?.length??null,logicalDataItems:l?m.length:null,drawablePrimitives:l?m.length:null,visibleCandidates:d?Math.max(0,m.length-new Set([...m.map((u,k)=>u.properties.opacity===0?k:-1),...m.map((u,k)=>{const v=T(u.properties,u.type??t?.type);return v!==null&&v.width===0&&v.height===0?k:-1}),...s??[]].filter(u=>u>=0)).size):null,flags:[{reason:"opacity-zero",count:o},{reason:"zero-size",count:r},{reason:"outside-clip",count:x},{reason:"missing-skipped",count:n.mark?.missing==="skip"&&a?.values?Math.max(0,a.values.length-m.length):null}],bounds:b,sourceRefs:[...a===void 0?[]:[w("data",a.id,i)],...n.derivedBindings?.overlay?[w("mark",n.derivedBindings.overlay.source,i)]:[]],checks:[{name:"concrete-structure",status:t===void 0&&!e.compositionSpec?.facet?"failed":l?"passed":"not_applicable",evidencePaths:[n.id],findings:[]},{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}]}}function J(e,n,i){const t=new Set(e.semanticSpec.layers.map(o=>o.id)),a=n==="axis"?/axis/iu:n==="grid"?/grid/iu:n==="legend"?/legend/iu:n==="title"?/title|subtitle/iu:new RegExp(n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"iu"),h=Object.keys(e.graphicSpec.objects).filter(o=>o!=="canvas"&&!o.startsWith("plot-")&&!t.has(o)&&a.test(o)),m=h.flatMap(o=>{const r=e.graphicSpec.objects[o];return Array.isArray(r?.items)?r.items:r?.properties===void 0?[]:[{properties:r.properties}]}),g=n==="title"?e.semanticSpec.title:e.semanticSpec.guides[n],l=[];function d(o){if(M(o)){typeof o.scale=="string"&&l.push(w("scale",o.scale,i)),typeof o.coordinate=="string"&&l.push(w("coordinate",o.coordinate,i));for(const r of Object.values(o))d(r)}}return d(g),{owner:w("guide",n,i),grain:"unknown",rawRows:null,logicalDataItems:m.length,drawablePrimitives:m.length,visibleCandidates:null,flags:[{reason:"opacity-zero",count:null},{reason:"zero-size",count:null},{reason:"outside-clip",count:null},{reason:"missing-skipped",count:null}],bounds:null,sourceRefs:[...new Map(l.map(o=>[q(o),o])).values()],checks:[{name:"concrete-structure",status:h.length>0?"passed":"not_applicable",evidencePaths:h,findings:[]},{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}]}}function tn(e,{target:n}={}){O(e),n!==void 0&&B(n);const i=[];function t(l,d=[]){const o=new Set,r=new Set;function p(f){if(r.has(f))throw new Error(`Graphic hierarchy contains a cycle at "${f}".`);if(o.has(f))throw new Error(`Graphic "${f}" is referenced more than once.`);const c=l.graphicSpec.objects[f];if(c===void 0)throw new Error(`Graphic hierarchy references missing object "${f}".`);o.add(f),r.add(f);for(const s of c.children??[])p(s);r.delete(f)}for(const f of l.graphicSpec.order)p(f);for(const f of l.semanticSpec.layers)i.push(Pe(l,f,d));for(const f of Object.keys(l.semanticSpec.guides))i.push(J(l,f,d));Object.keys(l.semanticSpec.title).length>0&&i.push(J(l,"title",d));for(const[f,c]of Object.entries(l.children))t(c,[...d,f])}t(e);let a=i;if(n!==void 0){const l=P(e,n.childPath),d=N(e,n);if(d===void 0)throw new Error(`Unknown ${n.kind} "${n.id}".`);a=i.filter(o=>{if(A(o.owner.childPath??[])!==A(n.childPath??[]))return!1;if(n.kind==="mark")return o.owner.id===n.id;const r=_(l,o.owner.id);return n.kind==="data"?r?.data===d.id:n.kind==="coordinate"?r?.coordinate===n.id:n.kind==="scale"?Object.values(r?.encoding??{}).some(p=>p?.scale===n.id||p?.dimensions?.some(f=>f.scale===n.id)):n.kind==="guide"?o.owner.kind==="guide"&&o.owner.id===n.id:!1})}const h=[];function m(l,d=[]){for(const o of Object.values(l.materializationConfigs.calculations??{}))if(M(o))for(const r of Object.values(o))h.push(d.length===0?r:{...r,childPath:d});for(const[o,r]of Object.entries(l.children))m(r,[...d,o])}m(e);const g=a.filter(l=>l.logicalDataItems===null).map(l=>l.owner);return S({version:1,views:a,calculations:h,checks:[{name:"pixel-visibility",status:"not_run",evidencePaths:[],findings:[]}],findings:[],coverage:{partial:a.some(l=>l.checks.some(d=>d.status!=="passed")),unsupported:g}})}const X=new WeakMap;function sn(e,{target:n,grain:i="item"}={}){O(e);const t=typeof n=="object"?P(e,n.childPath):e,h=(typeof n=="object"?n.id:n)??he(t,{label:"mark items",predicate:o=>o.data!==void 0}).id;let m=X.get(t);m||X.set(t,m=new Map);const g=`${h}:${i}`;if(m.has(g))return m.get(g);const l=new Map;for(const[o,r]of Object.entries(t.graphicSpec.objects)){l.set(o,r);for(const p of r.items??[])l.set(p.id,{type:p.type??r.type,properties:p.properties})}const d=S(ee(t,h).flatMap(o=>Q(t,o,i).map(r=>{const p=r.graphicIds.map(c=>l.get(c)).filter(Boolean).map(c=>({type:c.type,properties:c.properties})),f=p.map(c=>{const s=c.properties;return s.commands?ne(s.commands):c.type==="circle"?{left:s.x-s.radius,top:s.y-s.radius,right:s.x+s.radius,bottom:s.y+s.radius}:c.type==="line"?{left:Math.min(s.x1,s.x2),top:Math.min(s.y1,s.y2),right:Math.max(s.x1,s.x2),bottom:Math.max(s.y1,s.y2)}:c.type==="rect"?{left:s.x,top:s.y,right:s.x+s.width,bottom:s.y+s.height}:ie({objects:{item:c},order:["item"]},["item"],t.materializationConfigs.textMetrics)}).filter(Boolean);return{key:r.key,target:r.layer,markType:r.markType,grain:i,fields:r.fields,channels:r.channels,properties:r.properties,rows:r.rows,geometry:p,bounds:f.length?{left:Math.min(...f.map(c=>c.left)),top:Math.min(...f.map(c=>c.top)),right:Math.max(...f.map(c=>c.right)),bottom:Math.max(...f.map(c=>c.bottom))}:null}})));return m.set(g,d),d}export{nn as comparePrograms,xe as describeAction,Se as getDatasetSchema,sn as getMarkItems,en as getPlotComposition,tn as inspectProgram};
|