ggaction 0.0.17 → 0.0.19
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 +96 -0
- package/README.md +6 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +3 -3
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +20 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +8 -7
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +3 -1
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +25 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +8 -4
- package/src/actions/data/stack.js +3 -1
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/appearance.js +37 -29
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +1 -1
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +3 -2
- package/src/actions/encodings/ranged.js +17 -0
- package/src/actions/encodings/remove.js +14 -8
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/encodings/text.js +6 -3
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +176 -247
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +72 -10
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +155 -71
- package/src/actions/guides/axes/lines.js +27 -24
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +13 -8
- package/src/actions/guides/axes/tickGroups.js +11 -7
- package/src/actions/guides/axes/ticks.js +36 -24
- package/src/actions/guides/axes/titles.js +107 -69
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +12 -4
- package/src/actions/guides/grids/grid.js +8 -2
- package/src/actions/guides/grids/resolve.js +102 -58
- package/src/actions/guides/legends/blocks.js +46 -20
- package/src/actions/guides/legends/categorical/actions.js +95 -19
- package/src/actions/guides/legends/categorical/components.js +15 -18
- package/src/actions/guides/legends/categorical/index.js +8 -0
- package/src/actions/guides/legends/categorical/layout.js +45 -20
- package/src/actions/guides/legends/categorical/options.js +39 -22
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +11 -4
- package/src/actions/guides/legends/categorical/symbols.js +38 -18
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +73 -46
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +33 -21
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +8 -7
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +9 -3
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +65 -47
- package/src/actions/guides/legends/strokeWidth.js +13 -29
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +44 -11
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +10 -4
- package/src/actions/marks/bar/edit.js +29 -14
- package/src/actions/marks/bar/materialize.js +15 -7
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +12 -10
- package/src/actions/marks/rule/actions.js +7 -4
- package/src/actions/marks/text/actions.js +17 -5
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +8 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +18 -6
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +9 -4
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -5
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +12 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +126 -45
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/compositionState.js +6 -3
- package/src/core/font.js +9 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +5 -2
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +35 -0
- package/src/core/vocabulary.js +7 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +2 -12
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +115 -26
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +13 -9
- package/src/grammar/displayLabels.js +36 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +58 -30
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +27 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +16 -2
- package/src/grammar/regression/models.js +142 -54
- package/src/grammar/regression/parameters.js +23 -4
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -197
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +14 -0
- package/src/grammar/schemas/graphic.js +6 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +19 -3
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +4 -27
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +22 -11
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +15 -2
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +22 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +21 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +71 -14
- package/src/materialization/facetGuides/preparation.js +22 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +2 -2
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +14 -15
- package/src/materialization/scales/map.js +6 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +7 -3
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +13 -4
- package/src/materialization/textLines.js +15 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +220 -0
- package/src/persistence.js +19 -6
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +22 -11
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +14 -4
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +3 -0
- package/types/index.d.ts +12 -0
- package/types/program.d.ts +536 -129
|
@@ -1,347 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
formatVisibleText,
|
|
4
|
-
measureTextWidth,
|
|
5
|
-
resolveTextBounds,
|
|
6
|
-
textBoundsIntersect
|
|
7
|
-
} from "../core/textMetrics.js";
|
|
8
|
-
import { resolveDisplayLabel } from "../grammar/displayLabels.js";
|
|
9
|
-
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../theme/defaults.js";
|
|
10
|
-
|
|
11
|
-
const SIDES = Object.freeze(["top", "right", "bottom", "left"]);
|
|
12
|
-
const ZERO_SIDES = Object.freeze({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
13
|
-
|
|
14
|
-
export function createDefaultFacetHeaders() {
|
|
15
|
-
return {
|
|
16
|
-
mode: "legacy",
|
|
17
|
-
common: {
|
|
18
|
-
fontSize: 12,
|
|
19
|
-
fontFamily: DEFAULT_FONT_FAMILY,
|
|
20
|
-
fontWeight: 600,
|
|
21
|
-
color: DEFAULT_COLORS.strongText,
|
|
22
|
-
offset: 10,
|
|
23
|
-
align: "center"
|
|
24
|
-
},
|
|
25
|
-
row: { side: "left" },
|
|
26
|
-
column: { side: "top" }
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function normalizeFacetHeadersConfig(value) {
|
|
31
|
-
if (!isPlainObject(value)) {
|
|
32
|
-
throw new TypeError("Facet headers config must be a plain object.");
|
|
33
|
-
}
|
|
34
|
-
if (value.common === undefined && value.mode === undefined) {
|
|
35
|
-
return {
|
|
36
|
-
...createDefaultFacetHeaders(),
|
|
37
|
-
common: {
|
|
38
|
-
...createDefaultFacetHeaders().common,
|
|
39
|
-
...value
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (!["legacy", "roles"].includes(value.mode) ||
|
|
44
|
-
!isPlainObject(value.common) || !isPlainObject(value.row) ||
|
|
45
|
-
!isPlainObject(value.column)) {
|
|
46
|
-
throw new Error("Facet headers config must contain canonical mode and role objects.");
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
mode: value.mode,
|
|
50
|
-
common: { ...value.common },
|
|
51
|
-
row: { ...value.row },
|
|
52
|
-
column: { ...value.column }
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function resolveFacetHeaderConfig(headers, role) {
|
|
57
|
-
const normalized = normalizeFacetHeadersConfig(headers);
|
|
58
|
-
return role === "all"
|
|
59
|
-
? { ...normalized.common }
|
|
60
|
-
: { ...normalized.common, ...normalized[role] };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function mappedText(value, config) {
|
|
64
|
-
return resolveDisplayLabel(value, config.labelMap, formatVisibleText);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function gridCellById(spec) {
|
|
68
|
-
return new Map((spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell]));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function legacyDescriptors(program, headers) {
|
|
72
|
-
const spec = program.compositionSpec;
|
|
73
|
-
const config = resolveFacetHeaderConfig(headers, "all");
|
|
74
|
-
const cells = gridCellById(spec);
|
|
75
|
-
return spec.children.map((id, index) => {
|
|
76
|
-
const cell = cells.get(id);
|
|
77
|
-
const text = cell === undefined
|
|
78
|
-
? mappedText(spec.facet.values[index], config)
|
|
79
|
-
: `${mappedText(cell.rowValue, config)} · ${mappedText(cell.columnValue, config)}`;
|
|
80
|
-
return { topology: "legacy", role: "column", id, text, config };
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function uniqueGridGroups(spec, role) {
|
|
85
|
-
const coordinate = role === "column" ? "column" : "row";
|
|
86
|
-
const valueKey = role === "column" ? "columnValue" : "rowValue";
|
|
87
|
-
const groups = new Map();
|
|
88
|
-
for (const cell of spec.facet.grid.cells) {
|
|
89
|
-
const existing = groups.get(cell[coordinate]);
|
|
90
|
-
if (existing === undefined) {
|
|
91
|
-
groups.set(cell[coordinate], {
|
|
92
|
-
coordinate: cell[coordinate],
|
|
93
|
-
value: cell[valueKey],
|
|
94
|
-
ids: [cell.id]
|
|
95
|
-
});
|
|
96
|
-
} else {
|
|
97
|
-
if (existing.value !== cell[valueKey]) {
|
|
98
|
-
throw new Error(`Facet ${role} header values conflict at coordinate ${cell[coordinate]}.`);
|
|
99
|
-
}
|
|
100
|
-
existing.ids.push(cell.id);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return [...groups.values()].sort((left, right) =>
|
|
104
|
-
left.coordinate - right.coordinate
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function roleDescriptors(program, headers) {
|
|
109
|
-
const spec = program.compositionSpec;
|
|
110
|
-
const column = resolveFacetHeaderConfig(headers, "column");
|
|
111
|
-
if (spec.facet.grid === undefined) {
|
|
112
|
-
return spec.children.map((id, index) => ({
|
|
113
|
-
topology: "cell-column",
|
|
114
|
-
role: "column",
|
|
115
|
-
id,
|
|
116
|
-
ids: [id],
|
|
117
|
-
row: Math.floor(index / spec.columns),
|
|
118
|
-
text: mappedText(spec.facet.values[index], column),
|
|
119
|
-
config: column
|
|
120
|
-
}));
|
|
121
|
-
}
|
|
122
|
-
const row = resolveFacetHeaderConfig(headers, "row");
|
|
123
|
-
const columns = uniqueGridGroups(spec, "column").map(group => ({
|
|
124
|
-
topology: "grid-column",
|
|
125
|
-
role: "column",
|
|
126
|
-
ids: group.ids,
|
|
127
|
-
coordinate: group.coordinate,
|
|
128
|
-
text: mappedText(group.value, column),
|
|
129
|
-
config: column
|
|
130
|
-
}));
|
|
131
|
-
const rows = uniqueGridGroups(spec, "row").map(group => ({
|
|
132
|
-
topology: "grid-row",
|
|
133
|
-
role: "row",
|
|
134
|
-
ids: group.ids,
|
|
135
|
-
coordinate: group.coordinate,
|
|
136
|
-
text: mappedText(group.value, row),
|
|
137
|
-
config: row
|
|
138
|
-
}));
|
|
139
|
-
return [...columns, ...rows];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function laneThickness(descriptors, role, profile) {
|
|
143
|
-
const visible = descriptors.filter(item => item.role === role && item.text !== "");
|
|
144
|
-
if (visible.length === 0) return 0;
|
|
145
|
-
if (role === "column") {
|
|
146
|
-
return Math.max(...visible.map(item => item.config.fontSize + item.config.offset));
|
|
147
|
-
}
|
|
148
|
-
return Math.max(...visible.map(item => measureTextWidth(item.text, item.config, profile) +
|
|
149
|
-
item.config.offset));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function prepareFacetHeaders(program, headersConfig) {
|
|
153
|
-
const headers = normalizeFacetHeadersConfig(headersConfig);
|
|
154
|
-
const descriptors = headers.mode === "legacy"
|
|
155
|
-
? legacyDescriptors(program, headers)
|
|
156
|
-
: roleDescriptors(program, headers);
|
|
157
|
-
const rows = program.compositionSpec.facet.grid === undefined
|
|
158
|
-
? Math.ceil(program.compositionSpec.children.length / program.compositionSpec.columns)
|
|
159
|
-
: Math.max(...program.compositionSpec.facet.grid.cells.map(cell => cell.row)) + 1;
|
|
160
|
-
const headerLayout = {
|
|
161
|
-
outer: { ...ZERO_SIDES },
|
|
162
|
-
cellRows: { top: Array(rows).fill(0), bottom: Array(rows).fill(0) }
|
|
163
|
-
};
|
|
164
|
-
if (headers.mode === "roles") {
|
|
165
|
-
const column = resolveFacetHeaderConfig(headers, "column");
|
|
166
|
-
const columnThickness = laneThickness(descriptors, "column", program.materializationConfigs.textMetrics);
|
|
167
|
-
if (program.compositionSpec.facet.grid === undefined) {
|
|
168
|
-
for (const descriptor of descriptors) {
|
|
169
|
-
if (descriptor.text !== "") {
|
|
170
|
-
headerLayout.cellRows[column.side][descriptor.row] = Math.max(
|
|
171
|
-
headerLayout.cellRows[column.side][descriptor.row],
|
|
172
|
-
descriptor.config.fontSize + descriptor.config.offset
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
headerLayout.outer[column.side] = columnThickness;
|
|
178
|
-
const row = resolveFacetHeaderConfig(headers, "row");
|
|
179
|
-
headerLayout.outer[row.side] = laneThickness(descriptors, "row", program.materializationConfigs.textMetrics);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return { headers, descriptors, headerLayout };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function horizontalAnchor(bounds, align) {
|
|
186
|
-
if (align === "start") return { x: bounds.left, textAlign: "left" };
|
|
187
|
-
if (align === "end") return { x: bounds.right, textAlign: "right" };
|
|
188
|
-
return { x: (bounds.left + bounds.right) / 2, textAlign: "center" };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function verticalAnchor(bounds, align) {
|
|
192
|
-
if (align === "start") return { y: bounds.top, textBaseline: "top" };
|
|
193
|
-
if (align === "end") return { y: bounds.bottom, textBaseline: "bottom" };
|
|
194
|
-
return { y: (bounds.top + bounds.bottom) / 2, textBaseline: "middle" };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function union(items) {
|
|
198
|
-
return {
|
|
199
|
-
left: Math.min(...items.map(item => item.left)),
|
|
200
|
-
right: Math.max(...items.map(item => item.right)),
|
|
201
|
-
top: Math.min(...items.map(item => item.top)),
|
|
202
|
-
bottom: Math.max(...items.map(item => item.bottom))
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function placedPlot(cell, plot) {
|
|
207
|
-
return {
|
|
208
|
-
left: cell.x + plot.x,
|
|
209
|
-
right: cell.x + plot.x + plot.width,
|
|
210
|
-
top: cell.y + plot.y,
|
|
211
|
-
bottom: cell.y + plot.y + plot.height
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function placedCell(cell) {
|
|
216
|
-
return {
|
|
217
|
-
left: cell.x,
|
|
218
|
-
right: cell.x + cell.width,
|
|
219
|
-
top: cell.y,
|
|
220
|
-
bottom: cell.y + cell.height
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function roleItem(descriptor, cells, plots) {
|
|
225
|
-
const selectedCells = descriptor.ids.map(id => cells.get(id));
|
|
226
|
-
const missingCell = descriptor.ids.find((id, index) =>
|
|
227
|
-
selectedCells[index] === undefined
|
|
228
|
-
);
|
|
229
|
-
const missingPlot = descriptor.ids.find(id => plots.get(id) === undefined);
|
|
230
|
-
if (missingCell !== undefined || missingPlot !== undefined) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`Facet header requires cell and plot bounds for "${missingCell ?? missingPlot}".`
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
const selectedPlots = descriptor.ids.map((id, index) =>
|
|
236
|
-
placedPlot(selectedCells[index], plots.get(id))
|
|
237
|
-
);
|
|
238
|
-
const plot = union(selectedPlots);
|
|
239
|
-
const snapshots = union(selectedCells.map(placedCell));
|
|
240
|
-
const config = descriptor.config;
|
|
241
|
-
let anchor;
|
|
242
|
-
if (descriptor.role === "column") {
|
|
243
|
-
anchor = horizontalAnchor(plot, config.align);
|
|
244
|
-
const top = config.side === "top";
|
|
245
|
-
anchor.y = top
|
|
246
|
-
? snapshots.top - config.offset - config.fontSize / 2
|
|
247
|
-
: snapshots.bottom + config.offset + config.fontSize / 2;
|
|
248
|
-
anchor.textBaseline = "middle";
|
|
249
|
-
} else {
|
|
250
|
-
anchor = verticalAnchor(plot, config.align);
|
|
251
|
-
const left = config.side === "left";
|
|
252
|
-
anchor.x = left
|
|
253
|
-
? snapshots.left - config.offset
|
|
254
|
-
: snapshots.right + config.offset;
|
|
255
|
-
anchor.textAlign = left ? "right" : "left";
|
|
256
|
-
}
|
|
257
|
-
return { descriptor, snapshots, properties: anchor };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function legacyItem(descriptor, cell, plot) {
|
|
261
|
-
if (cell === undefined || plot === undefined) {
|
|
262
|
-
throw new Error(`Facet header requires cell and plot bounds for "${descriptor.id}".`);
|
|
263
|
-
}
|
|
264
|
-
return {
|
|
265
|
-
descriptor,
|
|
266
|
-
snapshots: placedCell(cell),
|
|
267
|
-
properties: {
|
|
268
|
-
...horizontalAnchor(placedPlot(cell, plot), descriptor.config.align),
|
|
269
|
-
y: cell.y + descriptor.config.offset,
|
|
270
|
-
textBaseline: "middle"
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function styledItem(item) {
|
|
276
|
-
const { config } = item.descriptor;
|
|
277
|
-
return {
|
|
278
|
-
type: "text",
|
|
279
|
-
properties: {
|
|
280
|
-
...item.properties,
|
|
281
|
-
text: item.descriptor.text,
|
|
282
|
-
fill: config.color,
|
|
283
|
-
fontSize: config.fontSize,
|
|
284
|
-
fontFamily: config.fontFamily,
|
|
285
|
-
fontWeight: config.fontWeight
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function assertRoleHeadersFit(records, items, layout, profile) {
|
|
291
|
-
const bounds = items.map((item, index) => records[index].descriptor.text === ""
|
|
292
|
-
? undefined
|
|
293
|
-
: resolveTextBounds(item.properties, profile));
|
|
294
|
-
for (const [index, current] of bounds.entries()) {
|
|
295
|
-
if (current === undefined) continue;
|
|
296
|
-
if (current.left < -1e-9 || current.right > layout.width + 1e-9 ||
|
|
297
|
-
current.top < -1e-9 || current.bottom > layout.height + 1e-9 ||
|
|
298
|
-
bounds.some((other, otherIndex) => otherIndex < index &&
|
|
299
|
-
other !== undefined && textBoundsIntersect(other, current)) ||
|
|
300
|
-
layout.children.some(cell => textBoundsIntersect(current, placedCell(cell)))) {
|
|
301
|
-
throw new Error(
|
|
302
|
-
"Facet role headers require sufficient non-overlapping reserved space."
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function assertLegacyHeadersFit(items, layout, plotById, profile) {
|
|
309
|
-
const previous = [];
|
|
310
|
-
for (let index = 0; index < items.length; index += 1) {
|
|
311
|
-
if (items[index].properties.text === "") continue;
|
|
312
|
-
const bounds = resolveTextBounds(items[index].properties, profile);
|
|
313
|
-
const cell = layout.children[index];
|
|
314
|
-
const plot = plotById.get(cell.id);
|
|
315
|
-
if (bounds.left < 0 || bounds.right > layout.width ||
|
|
316
|
-
bounds.top < 0 || bounds.bottom > layout.height ||
|
|
317
|
-
previous.some(item => textBoundsIntersect(item, bounds)) ||
|
|
318
|
-
textBoundsIntersect(bounds, placedPlot(cell, plot))) {
|
|
319
|
-
throw new Error(
|
|
320
|
-
"Facet headers require sufficient non-overlapping space above every child plot."
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
previous.push(bounds);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export function materializeFacetHeaders(program, layout, plots, prepared) {
|
|
328
|
-
const profile = program.materializationConfigs.textMetrics;
|
|
329
|
-
const cells = new Map(layout.children.map(cell => [cell.id, cell]));
|
|
330
|
-
const plotById = new Map(plots.map(plot => [plot.id, plot]));
|
|
331
|
-
const records = prepared.descriptors.map(descriptor => {
|
|
332
|
-
if (descriptor.topology === "legacy") {
|
|
333
|
-
return legacyItem(descriptor, cells.get(descriptor.id), plotById.get(descriptor.id));
|
|
334
|
-
}
|
|
335
|
-
return roleItem(descriptor, cells, plotById);
|
|
336
|
-
});
|
|
337
|
-
const items = records.map(styledItem);
|
|
338
|
-
if (prepared.headers.mode === "legacy") {
|
|
339
|
-
assertLegacyHeadersFit(items, layout, plotById, profile);
|
|
340
|
-
} else {
|
|
341
|
-
assertRoleHeadersFit(records, items, layout, profile);
|
|
342
|
-
}
|
|
343
|
-
const id = `${program.compositionSpec.id}-headers`;
|
|
344
|
-
return program
|
|
345
|
-
.createGraphics({ id, type: "collection", parent: "canvas" })
|
|
346
|
-
.editGraphics({ target: id, property: "items", value: items });
|
|
347
|
-
}
|
|
1
|
+
import{isPlainObject as x}from"../core/immutable.js";import{formatVisibleText as E,measureTextWidth as S,resolveTextBounds as b,textBoundsIntersect as p}from"../core/textMetrics.js";import{resolveDisplayLabel as T}from"../grammar/displayLabels.js";import{DEFAULT_COLORS as I,DEFAULT_FONT_FAMILY as B}from"../theme/defaults.js";const K=Object.freeze(["top","right","bottom","left"]),D=Object.freeze({top:0,right:0,bottom:0,left:0});function F(){return{mode:"legacy",common:{fontSize:12,fontFamily:B,fontWeight:600,color:I.strongText,offset:10,align:"center"},row:{side:"left"},column:{side:"top"}}}function M(e){if(!x(e))throw new TypeError("Facet headers config must be a plain object.");if(e.common===void 0&&e.mode===void 0)return{...F(),common:{...F().common,...e}};if(!["legacy","roles"].includes(e.mode)||!x(e.common)||!x(e.row)||!x(e.column))throw new Error("Facet headers config must contain canonical mode and role objects.");return{mode:e.mode,common:{...e.common},row:{...e.row},column:{...e.column}}}function g(e,t){const n=M(e);return t==="all"?{...n.common}:{...n.common,...n[t]}}function u(e,t){return T(e,t.labelMap,E)}function H(e){return new Map((e.facet.grid?.cells??[]).map(t=>[t.id,t]))}function O(e,t){const n=e.compositionSpec,s=g(t,"all"),c=H(n);return n.children.map((i,o)=>{const r=c.get(i),f=r===void 0?u(n.facet.values[o],s):`${u(r.rowValue,s)} \xB7 ${u(r.columnValue,s)}`;return{topology:"legacy",role:"column",id:i,text:f,config:s}})}function z(e,t){const n=t==="column"?"column":"row",s=t==="column"?"columnValue":"rowValue",c=new Map;for(const i of e.facet.grid.cells){const o=c.get(i[n]);if(o===void 0)c.set(i[n],{coordinate:i[n],value:i[s],ids:[i.id]});else{if(o.value!==i[s])throw new Error(`Facet ${t} header values conflict at coordinate ${i[n]}.`);o.ids.push(i.id)}}return[...c.values()].sort((i,o)=>i.coordinate-o.coordinate)}function L(e,t){const n=e.compositionSpec,s=g(t,"column");if(n.facet.grid===void 0)return n.children.map((r,f)=>({topology:"cell-column",role:"column",id:r,ids:[r],row:Math.floor(f/n.columns),column:f%n.columns,text:u(n.facet.values[f],s),config:s}));const c=g(t,"row"),i=z(n,"column").map(r=>({topology:"grid-column",role:"column",ids:r.ids,coordinate:r.coordinate,text:u(r.value,s),config:s})),o=z(n,"row").map(r=>({topology:"grid-row",role:"row",ids:r.ids,coordinate:r.coordinate,text:u(r.value,c),config:c}));return[...i,...o]}function v(e,t,n){const s=e.filter(c=>c.role===t&&c.text!=="");return s.length===0?0:Math.max(...t==="column"?s.map(c=>c.config.fontSize+c.config.offset):s.map(c=>S(c.text,c.config,n)+c.config.offset))}function N(e,t){const n=M(t),s=n.mode==="legacy"?O(e,n):L(e,n),c=e.compositionSpec.facet.grid===void 0?Math.ceil(e.compositionSpec.children.length/e.compositionSpec.columns):Math.max(...e.compositionSpec.facet.grid.cells.map(o=>o.row))+1,i={outer:{...D},cellRows:{top:Array(c).fill(0),bottom:Array(c).fill(0)}};if(n.mode==="legacy"&&e.compositionSpec.spacing==="plot")for(const o of s){if(o.text==="")continue;const r=e.compositionSpec.children.indexOf(o.id),f=e.compositionSpec.facet.grid?.cells[r]?.row??Math.floor(r/e.compositionSpec.columns);i.cellRows.top[f]=Math.max(i.cellRows.top[f],o.config.offset+o.config.fontSize)}if(n.mode==="roles"){const o=g(n,"column"),r=v(s,"column",e.materializationConfigs.textMetrics);if(e.compositionSpec.facet.grid===void 0){const f=["left","right"].includes(o.side);f&&(i.cellColumns={left:Array(e.compositionSpec.columns).fill(0),right:Array(e.compositionSpec.columns).fill(0)});for(const l of s)if(l.text!==""){const a=f?i.cellColumns:i.cellRows,m=f?l.column:l.row,d=f?S(l.text,l.config,e.materializationConfigs.textMetrics):l.config.fontSize;a[o.side][m]=Math.max(a[o.side][m],d+l.config.offset)}}else{i.outer[o.side]=r;const f=g(n,"row");i.outer[f.side]=v(s,"row",e.materializationConfigs.textMetrics)}}return{headers:n,descriptors:s,headerLayout:i}}function A(e,t){return t==="start"?{x:e.left,textAlign:"left"}:t==="end"?{x:e.right,textAlign:"right"}:{x:(e.left+e.right)/2,textAlign:"center"}}function R(e,t){return t==="start"?{y:e.top,textBaseline:"top"}:t==="end"?{y:e.bottom,textBaseline:"bottom"}:{y:(e.top+e.bottom)/2,textBaseline:"middle"}}function C(e){return{left:Math.min(...e.map(t=>t.left)),right:Math.max(...e.map(t=>t.right)),top:Math.min(...e.map(t=>t.top)),bottom:Math.max(...e.map(t=>t.bottom))}}function h(e,t){return{left:e.x+t.x,right:e.x+t.x+t.width,top:e.y+t.y,bottom:e.y+t.y+t.height}}function y(e){return{left:e.x,right:e.x+e.width,top:e.y,bottom:e.y+e.height}}function $(e,t,n,s){const c=e.ids.map(d=>t.get(d)),i=e.ids.find((d,w)=>c[w]===void 0),o=e.ids.find(d=>n.get(d)===void 0);if(i!==void 0||o!==void 0)throw new Error(`Facet header requires cell and plot bounds for "${i??o}".`);const r=e.ids.map((d,w)=>h(c[w],n.get(d))),f=C(r),l=s==="plot"?f:C(c.map(y)),a=e.config;let m;if(["top","bottom"].includes(a.side)){m=A(f,a.align);const d=a.side==="top";m.y=d?l.top-a.offset-a.fontSize/2:l.bottom+a.offset+a.fontSize/2,m.textBaseline="middle"}else{m=R(f,a.align);const d=a.side==="left";m.x=d?l.left-a.offset:l.right+a.offset,m.textAlign=d?"right":"left"}return{descriptor:e,snapshots:l,properties:m}}function j(e,t,n,s){if(t===void 0||n===void 0)throw new Error(`Facet header requires cell and plot bounds for "${e.id}".`);return{descriptor:e,snapshots:y(t),properties:{...A(h(t,n),e.config.align),y:s==="plot"?t.y+n.y-e.config.offset-e.config.fontSize/2:t.y+e.config.offset,textBaseline:"middle"}}}function q(e){const{config:t}=e.descriptor;return{type:"text",properties:{...e.properties,text:e.descriptor.text,fill:t.color,fontSize:t.fontSize,fontFamily:t.fontFamily,fontWeight:t.fontWeight}}}function V(e,t,n,s,c){const i=t.map((o,r)=>e[r].descriptor.text===""?void 0:b(o.properties,s));for(const[o,r]of i.entries())if(r!==void 0&&(r.left<-1e-9||r.right>n.width+1e-9||r.top<-1e-9||r.bottom>n.height+1e-9||i.some((f,l)=>l<o&&f!==void 0&&p(f,r))||n.children.some(f=>p(r,n.spacing==="plot"?h(f,c.get(f.id)):y(f)))))throw new Error("Facet role headers require sufficient non-overlapping reserved space.")}function G(e,t,n,s){const c=[];for(let i=0;i<e.length;i+=1){if(e[i].properties.text==="")continue;const o=b(e[i].properties,s),r=t.children[i],f=n.get(r.id);if(o.left<0||o.right>t.width||o.top<0||o.bottom>t.height||c.some(l=>p(l,o))||(t.spacing==="plot"?t.children.some(l=>p(o,h(l,n.get(l.id)))):p(o,h(r,f))))throw new Error("Facet headers require sufficient non-overlapping space above every child plot.");c.push(o)}}function P(e,t,n){const s=new Map(e.children.map(o=>[o.id,o])),c=new Map(t.map(o=>[o.id,o])),i=n.descriptors.map(o=>o.topology==="legacy"?j(o,s.get(o.id),c.get(o.id),e.spacing):$(o,s,c,e.spacing));return{records:i,items:i.map(q),plotById:c}}function Y(e,t,n,s){const c=e.materializationConfigs.textMetrics,{records:i,items:o,plotById:r}=P(t,n,s);s.headers.mode==="legacy"?G(o,t,r,c):V(i,o,t,c,r);const f=`${e.compositionSpec.id}-headers`;return e.createGraphics({id:f,type:"collection",parent:"canvas"}).editGraphics({target:f,property:"items",value:o})}export{F as createDefaultFacetHeaders,Y as materializeFacetHeaders,M as normalizeFacetHeadersConfig,N as prepareFacetHeaders,g as resolveFacetHeaderConfig,P as resolveFacetHeaderItems};
|
|
@@ -1,185 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolvePlacedPlotBounds } from "../layout/composition.js";
|
|
3
|
-
import {
|
|
4
|
-
alignedTextAnchor,
|
|
5
|
-
buildTitleReadingBlock
|
|
6
|
-
} from "../layout/title.js";
|
|
7
|
-
import { resolveFacetLayout } from "../layout/facets.js";
|
|
8
|
-
import { namespaceGraphicSnapshot } from "./compositionSnapshot.js";
|
|
9
|
-
import { prepareSharedFacetLegend } from "./facetGuides/index.js";
|
|
10
|
-
import {
|
|
11
|
-
attachSnapshotObject,
|
|
12
|
-
clearCompositionChildren,
|
|
13
|
-
compositionChildDescriptor
|
|
14
|
-
} from "./composition.js";
|
|
15
|
-
import {
|
|
16
|
-
materializeFacetHeaders,
|
|
17
|
-
prepareFacetHeaders
|
|
18
|
-
} from "./facetHeaders.js";
|
|
19
|
-
|
|
20
|
-
const ZERO_MARGIN = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
21
|
-
|
|
22
|
-
function facetConfig(program) {
|
|
23
|
-
const config = program.materializationConfigs.facets?.[program.compositionSpec.id];
|
|
24
|
-
if (config === undefined) {
|
|
25
|
-
throw new Error(`Facet "${program.compositionSpec.id}" requires materialization config.`);
|
|
26
|
-
}
|
|
27
|
-
return config;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function titleLayout(program) {
|
|
31
|
-
if (program.semanticSpec.title.text === undefined) {
|
|
32
|
-
return { height: 0 };
|
|
33
|
-
}
|
|
34
|
-
const config = program.titleConfig;
|
|
35
|
-
if (config === undefined) {
|
|
36
|
-
throw new Error("Facet title requires chart title configuration.");
|
|
37
|
-
}
|
|
38
|
-
if (config.position !== "top") {
|
|
39
|
-
throw new Error("Facet parent title currently supports only top position.");
|
|
40
|
-
}
|
|
41
|
-
const block = buildTitleReadingBlock({
|
|
42
|
-
text: program.semanticSpec.title.text,
|
|
43
|
-
subtitle: program.semanticSpec.title.subtitle
|
|
44
|
-
}, config);
|
|
45
|
-
const top = 8 + config.offset;
|
|
46
|
-
if (top < 0) {
|
|
47
|
-
throw new Error("Facet parent title offset places text outside the Canvas.");
|
|
48
|
-
}
|
|
49
|
-
return { config, block, top, height: Math.ceil(top + block.height) };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function materializeTitleComponent(program, id, lines, centers, style, plot, top) {
|
|
53
|
-
if (lines.length === 0) return program;
|
|
54
|
-
const x = alignedTextAnchor(plot.x, plot.width, program.titleConfig.align);
|
|
55
|
-
const count = lines.length;
|
|
56
|
-
let next = program.createGraphics({
|
|
57
|
-
id,
|
|
58
|
-
type: "text",
|
|
59
|
-
...(count > 1 ? { length: count } : {}),
|
|
60
|
-
parent: "canvas"
|
|
61
|
-
});
|
|
62
|
-
for (const [property, value] of Object.entries({
|
|
63
|
-
x,
|
|
64
|
-
y: count === 1 ? top + centers[0] : centers.map(center => top + center),
|
|
65
|
-
text: count === 1 ? lines[0] : lines,
|
|
66
|
-
fill: style.color,
|
|
67
|
-
fontSize: style.fontSize,
|
|
68
|
-
fontFamily: style.fontFamily,
|
|
69
|
-
fontWeight: style.fontWeight,
|
|
70
|
-
textAlign: program.titleConfig.align,
|
|
71
|
-
textBaseline: "middle"
|
|
72
|
-
})) {
|
|
73
|
-
next = next.editGraphics({ target: id, property, value });
|
|
74
|
-
}
|
|
75
|
-
return next;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function materializeTitle(program, plot, title) {
|
|
79
|
-
let next = materializeTitleComponent(
|
|
80
|
-
program,
|
|
81
|
-
"chartTitle",
|
|
82
|
-
title.block.titleLines,
|
|
83
|
-
title.block.titleCenters,
|
|
84
|
-
title.config.titleStyle,
|
|
85
|
-
plot,
|
|
86
|
-
title.top
|
|
87
|
-
);
|
|
88
|
-
if (title.block.subtitleLines.length > 0) {
|
|
89
|
-
next = materializeTitleComponent(
|
|
90
|
-
next,
|
|
91
|
-
"chartSubtitle",
|
|
92
|
-
title.block.subtitleLines,
|
|
93
|
-
title.block.subtitleCenters,
|
|
94
|
-
title.config.subtitleStyle,
|
|
95
|
-
plot,
|
|
96
|
-
title.top
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
return next;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function resolveFacetProgramLayout(program, preparedLegend) {
|
|
103
|
-
program._assertCompositionProgram("resolveFacetProgramLayout");
|
|
104
|
-
if (program.compositionSpec.type !== "facet") {
|
|
105
|
-
throw new Error("resolveFacetProgramLayout requires a facet composition.");
|
|
106
|
-
}
|
|
107
|
-
const title = titleLayout(program);
|
|
108
|
-
const spec = program.compositionSpec;
|
|
109
|
-
const preparedHeaders = prepareFacetHeaders(
|
|
110
|
-
program,
|
|
111
|
-
facetConfig(program).headers
|
|
112
|
-
);
|
|
113
|
-
const gridCells = new Map(
|
|
114
|
-
(spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell])
|
|
115
|
-
);
|
|
116
|
-
const layout = resolveFacetLayout({
|
|
117
|
-
children: spec.children.map((id, index) => ({
|
|
118
|
-
...compositionChildDescriptor(id, program.children[id]),
|
|
119
|
-
value: spec.facet.values[index],
|
|
120
|
-
...(gridCells.has(id) ? {
|
|
121
|
-
row: gridCells.get(id).row,
|
|
122
|
-
column: gridCells.get(id).column
|
|
123
|
-
} : {})
|
|
124
|
-
})),
|
|
125
|
-
columns: spec.columns,
|
|
126
|
-
gap: spec.gap,
|
|
127
|
-
align: spec.align,
|
|
128
|
-
padding: spec.padding,
|
|
129
|
-
titleHeight: title.height,
|
|
130
|
-
headerLayout: preparedHeaders.headerLayout,
|
|
131
|
-
sharedLegend: spec.facet.guides.legend === "shared",
|
|
132
|
-
...(preparedLegend === undefined ? {} : {
|
|
133
|
-
sharedLegendGap: preparedLegend.reservation.gap,
|
|
134
|
-
sharedLegendWidth: preparedLegend.reservation.width,
|
|
135
|
-
sharedLegendHeight: preparedLegend.reservation.height,
|
|
136
|
-
sharedLegendPosition: preparedLegend.reservation.position
|
|
137
|
-
})
|
|
138
|
-
});
|
|
139
|
-
const plots = spec.children.map(id => ({
|
|
140
|
-
id,
|
|
141
|
-
...resolveGraphicBounds(program.children[id])
|
|
142
|
-
}));
|
|
143
|
-
const plot = resolvePlacedPlotBounds({
|
|
144
|
-
placements: layout.children,
|
|
145
|
-
plots
|
|
146
|
-
});
|
|
147
|
-
return { layout, title, plot, plots, preparedHeaders };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function materializeFacetGraphics(program) {
|
|
151
|
-
const preparedLegend = prepareSharedFacetLegend(program);
|
|
152
|
-
const { layout, title, plot, plots, preparedHeaders } = resolveFacetProgramLayout(
|
|
153
|
-
program,
|
|
154
|
-
preparedLegend
|
|
155
|
-
);
|
|
156
|
-
const background = program.graphicSpec.objects.canvas?.properties
|
|
157
|
-
?.background ?? "white";
|
|
158
|
-
let next = clearCompositionChildren(program);
|
|
159
|
-
if (next.graphicSpec.objects.canvas === undefined) {
|
|
160
|
-
next = next.createGraphics({ id: "canvas", type: "canvas" });
|
|
161
|
-
}
|
|
162
|
-
for (const [property, value] of Object.entries({
|
|
163
|
-
width: layout.width,
|
|
164
|
-
height: layout.height,
|
|
165
|
-
background
|
|
166
|
-
})) {
|
|
167
|
-
next = next.editGraphics({ target: "canvas", property, value });
|
|
168
|
-
}
|
|
169
|
-
for (const placement of layout.children) {
|
|
170
|
-
const child = program.children[placement.id];
|
|
171
|
-
const snapshot = namespaceGraphicSnapshot(child.graphicSpec, {
|
|
172
|
-
namespace: `${program.compositionSpec.id}-${placement.id}`,
|
|
173
|
-
x: placement.x,
|
|
174
|
-
y: placement.y
|
|
175
|
-
});
|
|
176
|
-
next = attachSnapshotObject(next, snapshot, snapshot.order[0], "canvas");
|
|
177
|
-
}
|
|
178
|
-
next = materializeFacetHeaders(next, layout, plots, preparedHeaders);
|
|
179
|
-
next = next.composeFacetGuides({ layout, plot });
|
|
180
|
-
if (title.height > 0) next = materializeTitle(next, plot, title);
|
|
181
|
-
return next._withCanvasConfig({
|
|
182
|
-
margin: ZERO_MARGIN,
|
|
183
|
-
size: { width: "auto", height: "auto" }
|
|
184
|
-
});
|
|
185
|
-
}
|
|
1
|
+
import{cartesianAxisOwners as C}from"../selectors/axes.js";import{resolveConcreteGraphicBounds as G}from"../grammar/schemas/graphicBounds.js";import{textBoundsFitCanvas as F,textBoundsIntersect as m,resolveTextBounds as k}from"../core/textMetrics.js";import{axisGraphicIds as L,allLegendGraphicIds as j}from"./guides/resources.js";import{resolveGraphicBounds as u}from"../layout/canvas.js";import{resolvePlacedPlotBounds as P}from"../layout/composition.js";import{alignedTextAnchor as z,buildTitleReadingBlock as T,paintedTitleComponent as B,resolveTitleComponentBounds as E,titleTextProperties as O}from"../layout/title.js";import{resolveFacetLayout as $}from"../layout/facets.js";import{namespaceGraphicId as b,namespaceGraphicSnapshot as M}from"./compositionSnapshot.js";import{prepareSharedFacetLegend as v}from"./facetGuides/index.js";import{resolvePromotedFacetLegendLayout as H}from"./facetGuides/layout.js";import{attachSnapshotObject as I,clearCompositionChildren as A,compositionChildDescriptor as q}from"./composition.js";import{materializeFacetHeaders as R,prepareFacetHeaders as _}from"./facetHeaders.js";const W={top:0,right:0,bottom:0,left:0};function D(e){const c=e.materializationConfigs.facets?.[e.compositionSpec.id];if(c===void 0)throw new Error(`Facet "${e.compositionSpec.id}" requires materialization config.`);return c}function N(e){if(e.semanticSpec.title.text===void 0)return{height:0};const c=e.titleConfig;if(c===void 0)throw new Error("Facet title requires chart title configuration.");if(c.position!=="top")throw new Error("Facet parent title currently supports only top position.");const i=T({text:e.semanticSpec.title.text,subtitle:e.semanticSpec.title.subtitle},c),o=8+c.offset;if(o<0)throw new Error("Facet parent title offset places text outside the Canvas.");return{config:c,block:i,top:o,height:Math.ceil(o+i.height)}}function w(e,c,i,o,l,d,s){if(i.length===0)return e;const r=z(d.x,d.width,e.titleConfig.align)+(e.titleConfig.alongOffset??0),t=B({x:r,y:o.map(h=>s+h),lines:i,textAlign:e.titleConfig.align}),n=E(t,l,e.materializationConfigs.textMetrics);if(!F(n,e.graphicSpec.objects.canvas.properties))throw new Error("Facet parent title places text outside the Canvas.");const a=t.lines.length;let p=e.createGraphics({id:c,type:"text",...a>1?{length:a}:{},parent:"canvas"});for(const[h,f]of Object.entries(O(t,l)))p=p.editGraphics({target:c,property:h,value:f});return p}function Z(e,c,i){let o=w(e,"chartTitle",i.block.titleLines,i.block.titleCenters,i.config.titleStyle,c,i.top);return i.block.subtitleLines.length>0&&(o=w(o,"chartSubtitle",i.block.subtitleLines,i.block.subtitleCenters,i.config.subtitleStyle,c,i.top)),o}function J(e,c){if(e._assertCompositionProgram("resolveFacetProgramLayout"),e.compositionSpec.type!=="facet")throw new Error("resolveFacetProgramLayout requires a facet composition.");c??=v(e);const i=N(e),o=e.compositionSpec,l=_(e,D(e).headers),d=new Map((o.facet.grid?.cells??[]).map(n=>[n.id,n]));let s=$({children:o.children.map((n,a)=>({...q(n,e.children[n]),value:o.facet.values[a],...d.has(n)?{row:d.get(n).row,column:d.get(n).column}:{}})),spacing:o.spacing,plots:o.children.map(n=>({id:n,...u(e.children[n])})),columns:o.columns,gap:o.gap,align:o.align,padding:o.padding,titleHeight:i.height,headerLayout:l.headerLayout,sharedLegend:o.facet.guides.legend==="shared"&&c?.mode!=="promoted",...c===void 0?{}:{sharedLegendGap:c.reservation.gap,sharedLegendWidth:c.reservation.width,sharedLegendHeight:c.reservation.height,sharedLegendPosition:c.reservation.position}});const r=o.children.map(n=>({id:n,...u(e.children[n])}));let t=P({placements:s.children,plots:r});return{layout:s,plot:t}=H(e,{layout:s,plot:t,plots:r,title:i,preparedHeaders:l,prepared:c}),{layout:s,title:i,plot:t,plots:r,preparedHeaders:l}}function K(e,c,i){const o=[],l=c.children.map((s,r)=>({id:s.id,left:s.x+i[r].x,right:s.x+i[r].x+i[r].width,top:s.y+i[r].y,bottom:s.y+i[r].y+i[r].height}));for(const s of c.children){const r=e.children[s.id],t=[...C(r).flatMap(n=>L(n.channel,n.id).slice(2)),...j(Object.keys(r.guideConfigs.legend??{}))];for(const n of t){const a=b(`${e.compositionSpec.id}-${s.id}`,n);if(e.graphicSpec.objects[a]===void 0)continue;const p=G(e.graphicSpec,a,e.materializationConfigs.textMetrics);p!==void 0&&o.push({owner:s.id,bounds:p})}}const d=e.graphicSpec.objects[`${e.compositionSpec.id}-headers`];for(const s of d?.items??[])s.properties.text!==""&&o.push({owner:"headers",bounds:k(s.properties,e.materializationConfigs.textMetrics)});for(let s=0;s<o.length;s+=1){const r=o[s];if(l.some(t=>t.id!==r.owner&&m(r.bounds,t))||o.slice(0,s).some(t=>t.owner!==r.owner&&m(r.bounds,t.bounds)))throw new Error("Plot spacing gap is too small for the retained facet guides or headers.")}}function ae(e){const c=v(e),{layout:i,title:o,plot:l,plots:d,preparedHeaders:s}=J(e,c),r=e.graphicSpec.objects.canvas?.properties?.background??"white";let t=A(e);t.graphicSpec.objects.canvas===void 0&&(t=t.createGraphics({id:"canvas",type:"canvas"}));for(const[n,a]of Object.entries({width:i.width,height:i.height,background:r}))t=t.editGraphics({target:"canvas",property:n,value:a});for(const n of i.children){const a=e.children[n.id],p=M(a.graphicSpec,{namespace:`${e.compositionSpec.id}-${n.id}`,x:n.x,y:n.y});if(t=I(t,p,p.order[0],"canvas"),i.spacing==="plot"){const h=p.order[0],f=b(`${e.compositionSpec.id}-${n.id}`,"plot-background"),y=u(a),g=t.graphicSpec.objects[h].children?.[0];t=t.editGraphics({target:h,property:"background",value:"transparent"}).createGraphics({id:f,type:"rect",parent:h,...g===void 0?{}:{before:g}});for(const[x,S]of Object.entries({...y,stroke:"none",strokeWidth:0,fill:a.graphicSpec.objects.canvas.properties.background??"transparent"}))t=t.editGraphics({target:f,property:x,value:S})}}return t=R(t,i,d,s),t=t.composeFacetGuides({layout:i,plot:l}),i.spacing==="plot"&&K(t,i,d),o.height>0&&(t=Z(t,l,o)),t._withCanvasConfig({margin:W,size:{width:"auto",height:"auto"}})}export{ae as materializeFacetGraphics,J as resolveFacetProgramLayout};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cartesianAxisOwners } from "../selectors/axes.js";
|
|
2
|
+
import { axisGraphicIds } from "./guides/resources.js";
|
|
1
3
|
import {
|
|
2
4
|
findGraphicParent,
|
|
3
5
|
graphicSiblings
|
|
@@ -55,8 +57,10 @@ export function resolveMarkGraphicPlacement(program, { data, markType }) {
|
|
|
55
57
|
? dataset.transform[0]
|
|
56
58
|
: undefined;
|
|
57
59
|
if (transform?.type !== "regression") {
|
|
60
|
+
const named = new Set(cartesianAxisOwners(program).filter(owner => owner.named)
|
|
61
|
+
.flatMap(owner => axisGraphicIds(owner.channel, owner.id)));
|
|
58
62
|
const axis = program.graphicSpec.objects[PLOT_GRAPHIC_ID]?.children?.find(
|
|
59
|
-
id => /^(x|y)Axis/.test(id)
|
|
63
|
+
id => id === "plotBorder" || /^(x|y)Axis/.test(id) || named.has(id)
|
|
60
64
|
);
|
|
61
65
|
return resolvePlotGraphicPlacement(
|
|
62
66
|
program,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
function translateCommands(commands, dx, dy) {
|
|
2
|
+
return commands.map(command => {
|
|
3
|
+
if (command.op === "Z") return { ...command };
|
|
4
|
+
const next = {
|
|
5
|
+
...command,
|
|
6
|
+
x: command.x + dx,
|
|
7
|
+
y: command.y + dy
|
|
8
|
+
};
|
|
9
|
+
if (command.op === "C") {
|
|
10
|
+
next.x1 = command.x1 + dx;
|
|
11
|
+
next.y1 = command.y1 + dy;
|
|
12
|
+
next.x2 = command.x2 + dx;
|
|
13
|
+
next.y2 = command.y2 + dy;
|
|
14
|
+
}
|
|
15
|
+
return next;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function translatedProperties(type, properties, dx, dy) {
|
|
20
|
+
const next = { ...properties };
|
|
21
|
+
if (["circle", "rect", "text", "canvas"].includes(type)) {
|
|
22
|
+
next.x = (next.x ?? 0) + dx;
|
|
23
|
+
next.y = (next.y ?? 0) + dy;
|
|
24
|
+
} else if (type === "line") {
|
|
25
|
+
next.x1 += dx;
|
|
26
|
+
next.x2 += dx;
|
|
27
|
+
next.y1 += dy;
|
|
28
|
+
next.y2 += dy;
|
|
29
|
+
} else if (type === "path") {
|
|
30
|
+
next.commands = translateCommands(next.commands, dx, dy);
|
|
31
|
+
}
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function translateGraphic(program, id, dx, dy) {
|
|
36
|
+
const graphic = program.graphicSpec.objects[id];
|
|
37
|
+
if (dx === 0 && dy === 0) return program;
|
|
38
|
+
if (graphic.items !== undefined) {
|
|
39
|
+
let next = program.editGraphics({
|
|
40
|
+
target: id,
|
|
41
|
+
property: "items",
|
|
42
|
+
value: graphic.items.map(item => ({
|
|
43
|
+
type: item.type ?? graphic.type,
|
|
44
|
+
properties: translatedProperties(
|
|
45
|
+
item.type ?? graphic.type,
|
|
46
|
+
item.properties,
|
|
47
|
+
dx,
|
|
48
|
+
dy
|
|
49
|
+
)
|
|
50
|
+
}))
|
|
51
|
+
});
|
|
52
|
+
if (graphic.type !== "canvas") {
|
|
53
|
+
for (const child of graphic.children ?? []) next = translateGraphic(next, child, dx, dy);
|
|
54
|
+
}
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
let next = program;
|
|
58
|
+
const properties = translatedProperties(
|
|
59
|
+
graphic.type,
|
|
60
|
+
graphic.properties,
|
|
61
|
+
dx,
|
|
62
|
+
dy
|
|
63
|
+
);
|
|
64
|
+
for (const [property, value] of Object.entries(properties)) {
|
|
65
|
+
if (value !== graphic.properties[property]) {
|
|
66
|
+
next = next.editGraphics({ target: id, property, value });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (graphic.type !== "canvas") {
|
|
70
|
+
for (const child of graphic.children ?? []) next = translateGraphic(next, child, dx, dy);
|
|
71
|
+
}
|
|
72
|
+
return next;
|
|
73
|
+
}
|