ggaction 0.0.13 → 0.0.16
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 +146 -0
- package/README.md +41 -12
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-packet.schema.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +39 -7
- package/src/BasicChartProgram.js +3 -1
- package/src/ChartProgram.js +2 -0
- package/src/accessibility.js +157 -0
- package/src/actions/basic.js +2 -2
- package/src/actions/boxPlots/components.js +20 -12
- package/src/actions/boxPlots/create.js +4 -5
- package/src/actions/boxPlots/edit.js +34 -9
- package/src/actions/boxPlots/materialize.js +12 -4
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/canvas/actions.js +3 -3
- package/src/actions/canvas/fitting.js +1 -1
- package/src/actions/categoryOrder/index.js +21 -12
- package/src/actions/charts/area.js +6 -2
- package/src/actions/charts/bar.js +3 -2
- package/src/actions/charts/beeswarm.js +1 -1
- package/src/actions/charts/density.js +7 -3
- package/src/actions/charts/ecdf.js +6 -3
- package/src/actions/charts/endpoints.js +29 -7
- package/src/actions/charts/heatmap.js +3 -2
- package/src/actions/charts/histogram.js +5 -4
- package/src/actions/charts/horizon.js +4 -2
- package/src/actions/charts/interval-regression.js +7 -4
- package/src/actions/charts/line.js +4 -2
- package/src/actions/charts/parallel.js +9 -3
- package/src/actions/charts/pie.js +11 -3
- package/src/actions/charts/polar.js +17 -6
- package/src/actions/charts/radar.js +9 -3
- package/src/actions/charts/radial.js +12 -4
- package/src/actions/charts/raincloud.js +2 -2
- package/src/actions/charts/rug-strip.js +8 -4
- package/src/actions/charts/scatter.js +4 -2
- package/src/actions/composition/actions.js +29 -24
- package/src/actions/coordinates/actions.js +2 -2
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/basicBin2d.js +4 -5
- package/src/actions/data/bin.js +3 -3
- package/src/actions/data/bin2d.js +63 -193
- package/src/actions/data/bin2dMaterialize.js +4 -5
- package/src/actions/data/box.js +4 -4
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +4 -4
- package/src/actions/data/create.js +7 -14
- package/src/actions/data/density.js +15 -23
- package/src/actions/data/derived.js +57 -31
- package/src/actions/data/ecdf.js +2 -2
- package/src/actions/data/edit.js +551 -0
- package/src/actions/data/filter.js +28 -22
- package/src/actions/data/fold.js +2 -2
- package/src/actions/data/gradientProfile.js +2 -2
- package/src/actions/data/horizon.js +2 -2
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +62 -47
- package/src/actions/data/interval.js +2 -2
- package/src/actions/data/intervalEdit.js +26 -18
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +21 -27
- package/src/actions/data/revise.js +48 -0
- package/src/actions/data/revisionBindings.js +125 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/stack.js +2 -2
- package/src/actions/data/summary.js +3 -3
- package/src/actions/data/timeUnit.js +6 -3
- package/src/actions/data/window.js +3 -3
- package/src/actions/encodings/angle.js +1 -1
- package/src/actions/encodings/appearance.js +4 -4
- package/src/actions/encodings/barWidth.js +1 -1
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/index.js +3 -2
- package/src/actions/encodings/density.js +7 -4
- package/src/actions/encodings/histogram.js +4 -3
- package/src/actions/encodings/horizon.js +6 -8
- package/src/actions/encodings/index.js +4 -0
- package/src/actions/encodings/offset.js +22 -15
- package/src/actions/encodings/parallel.js +4 -5
- package/src/actions/encodings/pathOrder.js +6 -11
- package/src/actions/encodings/position/apply.js +53 -7
- package/src/actions/encodings/position/index.js +14 -6
- package/src/actions/encodings/position/policies/bar.js +9 -0
- package/src/actions/encodings/position/resolve.js +24 -7
- package/src/actions/encodings/ranged.js +5 -5
- package/src/actions/encodings/remove.js +13 -9
- package/src/actions/encodings/ruleAppearance.js +2 -24
- package/src/actions/encodings/seriesLayout.js +2 -1
- package/src/actions/encodings/shared.js +82 -14
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/encodings/strokeDash.js +2 -2
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/create.js +34 -20
- package/src/actions/errorBands/edit.js +34 -137
- package/src/actions/errorBars/create.js +43 -20
- package/src/actions/errorBars/edit.js +29 -125
- package/src/actions/facets/actions.js +267 -73
- package/src/actions/facets/derive.js +99 -28
- package/src/actions/facets/guides.js +4 -5
- package/src/actions/facets/replay.js +4 -5
- package/src/actions/gradientPlots/components.js +15 -8
- package/src/actions/gradientPlots/create.js +4 -5
- package/src/actions/gradientPlots/edit.js +18 -9
- package/src/actions/gradientPlots/materialize.js +5 -3
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/gradientPlots/rebind.js +4 -5
- package/src/actions/guides/applicability.js +8 -2
- package/src/actions/guides/axes/axes.js +23 -4
- package/src/actions/guides/axes/axis.js +2 -2
- package/src/actions/guides/axes/edit.js +2 -2
- package/src/actions/guides/axes/labels.js +67 -21
- package/src/actions/guides/axes/lines.js +15 -9
- package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
- package/src/actions/guides/axes/parallel/resolve.js +6 -2
- package/src/actions/guides/axes/parallel.js +5 -5
- package/src/actions/guides/axes/remove.js +4 -4
- package/src/actions/guides/axes/tickGroups.js +6 -5
- package/src/actions/guides/axes/ticks.js +19 -6
- package/src/actions/guides/axes/titles.js +21 -13
- package/src/actions/guides/grids/grid.js +18 -29
- package/src/actions/guides/grids/resolve.js +7 -2
- package/src/actions/guides/guides.js +20 -4
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +138 -52
- package/src/actions/guides/legends/categorical/components.js +55 -45
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +98 -16
- package/src/actions/guides/legends/categorical/options.js +2 -2
- package/src/actions/guides/legends/categorical/recipes.js +111 -20
- package/src/actions/guides/legends/categorical/resolve.js +12 -8
- package/src/actions/guides/legends/categorical/symbols.js +104 -52
- package/src/actions/guides/legends/continuous/common.js +76 -20
- package/src/actions/guides/legends/continuous/gradient.js +95 -158
- package/src/actions/guides/legends/continuous/index.js +2 -0
- package/src/actions/guides/legends/continuous/interval.js +21 -16
- package/src/actions/guides/legends/continuous/opacity.js +37 -83
- package/src/actions/guides/legends/continuous/stroke.js +225 -0
- package/src/actions/guides/legends/creation.js +14 -1
- package/src/actions/guides/legends/edit.js +159 -60
- package/src/actions/guides/legends/focused.js +5 -5
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +59 -24
- package/src/actions/guides/legends/lifecycle.js +19 -8
- package/src/actions/guides/legends/remove.js +17 -8
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +100 -108
- package/src/actions/guides/legends/strokeWidth.js +38 -92
- package/src/actions/guides/legends/target.js +112 -1
- package/src/actions/guides/legends/transition.js +165 -16
- package/src/actions/guides/polar/axes/facade.js +8 -8
- package/src/actions/guides/polar/axes/labels.js +29 -10
- package/src/actions/guides/polar/axes/lines.js +19 -19
- package/src/actions/guides/polar/axes/shared.js +16 -0
- package/src/actions/guides/polar/axes/ticks.js +10 -16
- package/src/actions/guides/polar/axes/titles.js +6 -6
- package/src/actions/guides/polar/grids.js +12 -14
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/index.js +16 -3
- package/src/actions/intervals/revision.js +126 -0
- package/src/actions/marks/arc/actions.js +80 -22
- package/src/actions/marks/area/actions.js +38 -13
- package/src/actions/marks/area/materialize.js +17 -1
- package/src/actions/marks/bar/create.js +12 -3
- package/src/actions/marks/bar/edit.js +21 -4
- package/src/actions/marks/bar/materialize.js +34 -40
- package/src/actions/marks/index.js +10 -1
- package/src/actions/marks/line/actions.js +40 -17
- package/src/actions/marks/line/materialize.js +29 -10
- package/src/actions/marks/point/create.js +9 -3
- package/src/actions/marks/point/edit.js +19 -7
- package/src/actions/marks/point/jitter.js +6 -8
- package/src/actions/marks/point/materialize.js +48 -22
- package/src/actions/marks/point/packing.js +6 -8
- package/src/actions/marks/rect/actions.js +36 -15
- package/src/actions/marks/references.js +602 -33
- package/src/actions/marks/remove.js +67 -41
- package/src/actions/marks/rule/actions.js +51 -14
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +79 -14
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +157 -17
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +72 -0
- package/src/actions/marks/tick/actions.js +25 -8
- package/src/actions/primitives/createGraphics.js +5 -4
- package/src/actions/primitives/editGraphics.js +31 -4
- package/src/actions/primitives/graphicProperties.js +6 -0
- package/src/actions/primitives/semantic.js +11 -3
- package/src/actions/primitives/semanticAction.js +90 -14
- package/src/actions/primitives/semanticValidation/dataset.js +2 -4
- package/src/actions/primitives/semanticValidation/guide.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +5 -0
- package/src/actions/primitives/semanticValidation/scale.js +28 -7
- package/src/actions/regression/components.js +36 -25
- package/src/actions/regression/create.js +13 -6
- package/src/actions/regression/edit.js +24 -11
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +173 -0
- package/src/actions/scales/channels.js +212 -0
- package/src/actions/scales/consumers/common.js +1 -1
- package/src/actions/scales/consumers/families.js +3 -2
- package/src/actions/scales/consumers/index.js +19 -2
- package/src/actions/scales/consumers/seriesLayout.js +28 -14
- package/src/actions/scales/create.js +55 -23
- package/src/actions/scales/definitions.js +94 -40
- package/src/actions/scales/edit.js +67 -12
- package/src/actions/scales/editPolicy.js +81 -11
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +5 -4
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/preview.js +111 -10
- package/src/actions/scales/quantitativeColor.js +4 -5
- package/src/actions/selection/actions.js +179 -66
- package/src/actions/selection/index.js +3 -0
- package/src/actions/textMetrics/index.js +56 -0
- package/src/actions/theme/actions.js +70 -15
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +9 -2
- package/src/actions/theme/reconcile.js +313 -121
- package/src/actions/theme/state.js +160 -0
- package/src/actions/titles/actions.js +8 -8
- package/src/actions/titles/resolve.js +3 -3
- package/src/actions/violinPlots/create.js +6 -3
- package/src/actions/violinPlots/edit.js +2 -1
- package/src/core/action.js +117 -35
- package/src/core/compositionState.js +6 -1
- package/src/core/diagnostics.js +16 -0
- package/src/core/extensionRegistry.js +27 -0
- package/src/core/font.js +18 -0
- package/src/core/identifiers.js +5 -4
- package/src/core/immutable.js +10 -3
- package/src/core/optionRequirements.js +8 -0
- package/src/core/programState.js +21 -1
- package/src/core/resourceReferences.js +341 -0
- package/src/core/textMetricProfile.js +28 -0
- package/src/core/textMetrics.js +24 -3
- package/src/core/validation.js +22 -10
- package/src/core/vocabulary.js +16 -3
- package/src/diagnostics.js +1 -0
- package/src/grammar/bars/aggregate.js +4 -1
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bin.js +54 -9
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +265 -64
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +7 -47
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/impute.js +341 -0
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +18 -1
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/pathSeries.js +26 -8
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/parameters.js +23 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/colorConsumers.js +20 -1
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +27 -4
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/scales/types.js +8 -0
- package/src/grammar/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +190 -41
- package/src/grammar/schemas/semanticPath.js +7 -2
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +78 -16
- package/src/grammar/timeUnit.js +216 -5
- package/src/grammar/transformTopology.js +4 -0
- package/src/grammar/transforms.js +394 -28
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +221 -16
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +59 -5
- package/src/layout/labels.js +351 -7
- package/src/layout/legend.js +4 -4
- package/src/layout/legendItems.js +6 -6
- package/src/layout/text.js +10 -9
- package/src/layout/title.js +9 -9
- package/src/materialization/bars/aggregate.js +25 -2
- package/src/materialization/bars/histogram.js +84 -24
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/composition.js +3 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +36 -0
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +9 -3
- package/src/materialization/facetGuides/preparation.js +13 -8
- package/src/materialization/facetHeaders.js +347 -0
- package/src/materialization/facets.js +15 -65
- package/src/materialization/guides/layout.js +3 -3
- package/src/materialization/guides/resources.js +26 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/index.js +11 -5
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/rect.js +25 -12
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/scaleGuideDependencies.js +10 -0
- package/src/materialization/scales/map.js +5 -3
- package/src/materialization/scales/policies/binnedPosition.js +2 -2
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +4 -1
- package/src/materialization/scales/resolve.js +22 -6
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +30 -3
- package/src/materialization/selection/items/common.js +20 -5
- package/src/materialization/selection/items/rect.js +28 -4
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +265 -14
- package/src/materialization/typography.js +31 -0
- package/src/mcp/cli.js +5 -3
- package/src/persistence/codec.js +65 -0
- package/src/persistence/validation.js +185 -0
- package/src/persistence.js +74 -0
- package/src/renderers/canvas/circle.js +2 -3
- package/src/renderers/canvas/index.js +5 -4
- package/src/renderers/canvas/line.js +2 -3
- package/src/renderers/canvas/native.js +4 -4
- package/src/renderers/canvas/path.js +2 -3
- package/src/renderers/canvas/rect.js +2 -3
- package/src/renderers/canvas/stroke.js +11 -0
- package/src/renderers/nativeBackend.js +10 -0
- package/src/renderers/options.js +13 -0
- package/src/renderers/pdf.js +25 -54
- package/src/renderers/png.js +22 -17
- package/src/renderers/svg.js +29 -14
- package/src/renderers/text.js +1 -11
- package/src/selectors/coordinates.js +3 -1
- package/src/selectors/datasets.js +38 -5
- package/src/selectors/layers.js +10 -4
- package/src/selectors/markOwners.js +46 -0
- package/src/selectors/scales.js +6 -4
- package/src/theme/defaults.js +103 -1
- package/src/version.js +2 -0
- package/types/accessibility.d.ts +37 -0
- package/types/basic.d.ts +10 -2
- package/types/diagnostics.d.ts +15 -0
- package/types/index.d.ts +90 -0
- package/types/pdf.d.ts +9 -0
- package/types/persistence.d.ts +7 -0
- package/types/png.d.ts +9 -0
- package/types/program.d.ts +944 -118
package/src/layout/labels.js
CHANGED
|
@@ -19,6 +19,350 @@ const MAX_EXHAUSTIVE_OFFSET_STEPS = 28;
|
|
|
19
19
|
const MAX_SEARCH_DISPLACEMENT = 1_000_000;
|
|
20
20
|
const DISTANT_RING_COUNT = 16;
|
|
21
21
|
const DISTANT_ANGLE_COUNT = 32;
|
|
22
|
+
const PLACEMENT_OPTIONS = Object.freeze([
|
|
23
|
+
"anchor", "gap", "overflow", "leader"
|
|
24
|
+
]);
|
|
25
|
+
const PLACEMENT_LEADER_OPTIONS = Object.freeze(["stroke", "strokeWidth"]);
|
|
26
|
+
const SEMANTIC_ANCHORS = new Set([
|
|
27
|
+
"center", "insideStart", "insideEnd", "outsideStart", "outsideEnd"
|
|
28
|
+
]);
|
|
29
|
+
const OVERFLOW_POLICIES = new Set(["hide", "outside", "allow"]);
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_MARK_LABEL_PLACEMENT = cloneAndFreeze({
|
|
32
|
+
gap: 4,
|
|
33
|
+
overflow: "hide",
|
|
34
|
+
leader: false
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export function markLabelPlacementLeaderId(target) {
|
|
38
|
+
return `${target}-placement-leaders`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizePlacementLeader(value) {
|
|
42
|
+
if (value === undefined || value === false) return false;
|
|
43
|
+
validateOptionObject(value, PLACEMENT_LEADER_OPTIONS, "mark label placement leader");
|
|
44
|
+
const leader = { stroke: "#94a3b8", strokeWidth: 1, ...value };
|
|
45
|
+
if (typeof leader.stroke !== "string" || leader.stroke.length === 0) {
|
|
46
|
+
throw new TypeError("Mark label placement leader stroke must be a non-empty string.");
|
|
47
|
+
}
|
|
48
|
+
validateNonNegativeFinite(
|
|
49
|
+
leader.strokeWidth,
|
|
50
|
+
"Mark label placement leader strokeWidth"
|
|
51
|
+
);
|
|
52
|
+
return leader;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeMarkLabelPlacement(value) {
|
|
56
|
+
validateOptionObject(value, PLACEMENT_OPTIONS, "mark label placement", {
|
|
57
|
+
allowEmpty: false,
|
|
58
|
+
emptyError: TypeError
|
|
59
|
+
});
|
|
60
|
+
if (!SEMANTIC_ANCHORS.has(value.anchor)) {
|
|
61
|
+
throw new Error(`Unsupported mark label anchor "${value.anchor}".`);
|
|
62
|
+
}
|
|
63
|
+
const gap = value.gap ?? DEFAULT_MARK_LABEL_PLACEMENT.gap;
|
|
64
|
+
const overflowPolicy = value.overflow ?? DEFAULT_MARK_LABEL_PLACEMENT.overflow;
|
|
65
|
+
validateNonNegativeFinite(gap, "Mark label placement gap");
|
|
66
|
+
if (!OVERFLOW_POLICIES.has(overflowPolicy)) {
|
|
67
|
+
throw new Error(`Unsupported mark label overflow "${overflowPolicy}".`);
|
|
68
|
+
}
|
|
69
|
+
return cloneAndFreeze({
|
|
70
|
+
anchor: value.anchor,
|
|
71
|
+
gap,
|
|
72
|
+
overflow: overflowPolicy,
|
|
73
|
+
leader: normalizePlacementLeader(value.leader)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function finitePoint(value, label) {
|
|
78
|
+
if (
|
|
79
|
+
!isPlainObject(value) ||
|
|
80
|
+
!Number.isFinite(value.x) ||
|
|
81
|
+
!Number.isFinite(value.y)
|
|
82
|
+
) {
|
|
83
|
+
throw new TypeError(`${label} requires finite x and y.`);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function unitVector(value, label) {
|
|
89
|
+
finitePoint(value, label);
|
|
90
|
+
const length = Math.hypot(value.x, value.y);
|
|
91
|
+
if (!Number.isFinite(length) || length <= 0) {
|
|
92
|
+
throw new RangeError(`${label} requires a non-zero direction.`);
|
|
93
|
+
}
|
|
94
|
+
return { x: value.x / length, y: value.y / length };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateIntervalBounds(bounds) {
|
|
98
|
+
if (
|
|
99
|
+
!isPlainObject(bounds) ||
|
|
100
|
+
![bounds.left, bounds.right, bounds.top, bounds.bottom].every(Number.isFinite) ||
|
|
101
|
+
bounds.right < bounds.left ||
|
|
102
|
+
bounds.bottom < bounds.top
|
|
103
|
+
) {
|
|
104
|
+
throw new RangeError("Interval label placement requires ordered finite bounds.");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function validatePlacementGeometry(geometry, anchor) {
|
|
109
|
+
if (!isPlainObject(geometry)) {
|
|
110
|
+
throw new TypeError("Mark label placement requires source geometry.");
|
|
111
|
+
}
|
|
112
|
+
if (geometry.kind === "interval") {
|
|
113
|
+
finitePoint(geometry.center, "Interval label center");
|
|
114
|
+
for (const role of ["start", "end"]) {
|
|
115
|
+
finitePoint(geometry[role]?.point, `Interval label ${role}`);
|
|
116
|
+
unitVector(geometry[role]?.outward, `Interval label ${role}`);
|
|
117
|
+
}
|
|
118
|
+
validateIntervalBounds(geometry.bounds);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (geometry.kind === "arc") {
|
|
122
|
+
if (![geometry.centerX, geometry.centerY, geometry.startTheta,
|
|
123
|
+
geometry.endTheta, geometry.innerRadius, geometry.outerRadius]
|
|
124
|
+
.every(Number.isFinite)) {
|
|
125
|
+
throw new TypeError("Arc label placement requires finite sector geometry.");
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
geometry.innerRadius < 0 ||
|
|
129
|
+
geometry.outerRadius <= geometry.innerRadius ||
|
|
130
|
+
geometry.startTheta === geometry.endTheta ||
|
|
131
|
+
Math.abs(geometry.endTheta - geometry.startTheta) > 360
|
|
132
|
+
) {
|
|
133
|
+
throw new RangeError("Arc label placement requires an ordered annular sector.");
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (geometry.kind === "point") {
|
|
138
|
+
finitePoint(geometry.center, "Point label center");
|
|
139
|
+
if (anchor === "outsideEnd") {
|
|
140
|
+
finitePoint(geometry.end?.point, "Polar Point label boundary");
|
|
141
|
+
unitVector(geometry.end?.outward, "Polar Point label direction");
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
throw new Error(`Unsupported mark label placement geometry "${geometry.kind}".`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function assertMarkLabelPlacementSupport({ markType, coordinateType, intervalAxis }, placement) {
|
|
149
|
+
const anchor = placement.anchor;
|
|
150
|
+
if (markType === "bar" || markType === "arc") return placement;
|
|
151
|
+
if (markType === "rect") {
|
|
152
|
+
if (anchor === "center" || intervalAxis !== undefined) return placement;
|
|
153
|
+
throw new Error("Rect semantic label placement requires one explicit interval axis.");
|
|
154
|
+
}
|
|
155
|
+
if (markType === "point") {
|
|
156
|
+
if (anchor === "center" || (coordinateType === "polar" && anchor === "outsideEnd")) {
|
|
157
|
+
return placement;
|
|
158
|
+
}
|
|
159
|
+
throw new Error(
|
|
160
|
+
coordinateType === "polar"
|
|
161
|
+
? `Polar Point does not support mark label anchor "${anchor}".`
|
|
162
|
+
: `Point does not support mark label anchor "${anchor}".`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (markType === "line") {
|
|
166
|
+
throw new Error("Line labels retain the endpoint API and do not accept semantic placement objects.");
|
|
167
|
+
}
|
|
168
|
+
if (markType === "rule") {
|
|
169
|
+
throw new Error("Rule labels do not support semantic placement objects.");
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`Mark type "${markType}" does not support semantic label placement.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function localTextBounds(text) {
|
|
175
|
+
return resolveTextBounds({ ...text, x: 0, y: 0 }, text.profile);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function projectionRange(bounds, direction) {
|
|
179
|
+
const values = [
|
|
180
|
+
{ x: bounds.left, y: bounds.top },
|
|
181
|
+
{ x: bounds.right, y: bounds.top },
|
|
182
|
+
{ x: bounds.right, y: bounds.bottom },
|
|
183
|
+
{ x: bounds.left, y: bounds.bottom }
|
|
184
|
+
].map(point => point.x * direction.x + point.y * direction.y);
|
|
185
|
+
return { minimum: Math.min(...values), maximum: Math.max(...values) };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function boundaryCandidate(boundary, direction, gap, text) {
|
|
189
|
+
const resolvedDirection = unitVector(direction, "Mark label placement direction");
|
|
190
|
+
const support = projectionRange(localTextBounds(text), resolvedDirection);
|
|
191
|
+
const distance = gap - support.minimum;
|
|
192
|
+
return {
|
|
193
|
+
x: boundary.x + resolvedDirection.x * distance + (text.dx ?? 0),
|
|
194
|
+
y: boundary.y + resolvedDirection.y * distance + (text.dy ?? 0),
|
|
195
|
+
sourceX: boundary.x,
|
|
196
|
+
sourceY: boundary.y
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function centerCandidate(center, text) {
|
|
201
|
+
return {
|
|
202
|
+
x: center.x + (text.dx ?? 0),
|
|
203
|
+
y: center.y + (text.dy ?? 0),
|
|
204
|
+
sourceX: center.x,
|
|
205
|
+
sourceY: center.y
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function candidateBounds(candidate, text) {
|
|
210
|
+
return resolveTextBounds({ ...text, x: candidate.x, y: candidate.y }, text.profile);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function intervalContains(bounds, container) {
|
|
214
|
+
const epsilon = 1e-9;
|
|
215
|
+
return bounds.left >= container.left - epsilon &&
|
|
216
|
+
bounds.right <= container.right + epsilon &&
|
|
217
|
+
bounds.top >= container.top - epsilon &&
|
|
218
|
+
bounds.bottom <= container.bottom + epsilon;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function distanceToRectangle(centerX, centerY, bounds) {
|
|
222
|
+
const x = Math.max(bounds.left, Math.min(centerX, bounds.right));
|
|
223
|
+
const y = Math.max(bounds.top, Math.min(centerY, bounds.bottom));
|
|
224
|
+
return Math.hypot(x - centerX, y - centerY);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function thetaForPoint(point, geometry) {
|
|
228
|
+
return Math.atan2(point.x - geometry.centerX, geometry.centerY - point.y) *
|
|
229
|
+
180 / Math.PI;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function angleWithin(theta, start, end) {
|
|
233
|
+
const sweep = end - start;
|
|
234
|
+
if (Math.abs(sweep) >= 360 - 1e-9) return true;
|
|
235
|
+
const direction = Math.sign(sweep);
|
|
236
|
+
const progress = ((direction * (theta - start)) % 360 + 360) % 360;
|
|
237
|
+
return progress <= Math.abs(sweep) + 1e-9;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function arcContains(bounds, geometry) {
|
|
241
|
+
const points = [
|
|
242
|
+
{ x: bounds.left, y: bounds.top },
|
|
243
|
+
{ x: bounds.right, y: bounds.top },
|
|
244
|
+
{ x: bounds.right, y: bounds.bottom },
|
|
245
|
+
{ x: bounds.left, y: bounds.bottom },
|
|
246
|
+
{ x: (bounds.left + bounds.right) / 2, y: bounds.top },
|
|
247
|
+
{ x: bounds.right, y: (bounds.top + bounds.bottom) / 2 },
|
|
248
|
+
{ x: (bounds.left + bounds.right) / 2, y: bounds.bottom },
|
|
249
|
+
{ x: bounds.left, y: (bounds.top + bounds.bottom) / 2 }
|
|
250
|
+
];
|
|
251
|
+
const minimumRadius = distanceToRectangle(
|
|
252
|
+
geometry.centerX,
|
|
253
|
+
geometry.centerY,
|
|
254
|
+
bounds
|
|
255
|
+
);
|
|
256
|
+
const maximumRadius = Math.max(...points.slice(0, 4).map(point =>
|
|
257
|
+
Math.hypot(point.x - geometry.centerX, point.y - geometry.centerY)
|
|
258
|
+
));
|
|
259
|
+
const epsilon = 1e-9;
|
|
260
|
+
return minimumRadius >= geometry.innerRadius - epsilon &&
|
|
261
|
+
maximumRadius <= geometry.outerRadius + epsilon &&
|
|
262
|
+
points.every(point => angleWithin(
|
|
263
|
+
thetaForPoint(point, geometry),
|
|
264
|
+
geometry.startTheta,
|
|
265
|
+
geometry.endTheta
|
|
266
|
+
));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function arcRole(geometry, role) {
|
|
270
|
+
const theta = geometry.startTheta +
|
|
271
|
+
(geometry.endTheta - geometry.startTheta) / 2;
|
|
272
|
+
const radians = theta * Math.PI / 180;
|
|
273
|
+
const radial = { x: Math.sin(radians), y: -Math.cos(radians) };
|
|
274
|
+
const radius = role === "start" ? geometry.innerRadius : geometry.outerRadius;
|
|
275
|
+
return {
|
|
276
|
+
point: {
|
|
277
|
+
x: geometry.centerX + radial.x * radius,
|
|
278
|
+
y: geometry.centerY + radial.y * radius
|
|
279
|
+
},
|
|
280
|
+
outward: role === "start"
|
|
281
|
+
? { x: -radial.x, y: -radial.y }
|
|
282
|
+
: radial
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function geometryRole(geometry, role) {
|
|
287
|
+
return geometry.kind === "arc" ? arcRole(geometry, role) : geometry[role];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function geometryCenter(geometry) {
|
|
291
|
+
if (geometry.kind !== "arc") return geometry.center;
|
|
292
|
+
const role = arcRole(geometry, "end");
|
|
293
|
+
const radius = (geometry.innerRadius + geometry.outerRadius) / 2;
|
|
294
|
+
return {
|
|
295
|
+
x: geometry.centerX + role.outward.x * radius,
|
|
296
|
+
y: geometry.centerY + role.outward.y * radius
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function candidateForAnchor(anchor, geometry, placement, text) {
|
|
301
|
+
if (anchor === "center") return centerCandidate(geometryCenter(geometry), text);
|
|
302
|
+
const roleName = anchor.endsWith("Start") ? "start" : "end";
|
|
303
|
+
const role = geometryRole(geometry, roleName);
|
|
304
|
+
const outside = anchor.startsWith("outside");
|
|
305
|
+
const direction = outside
|
|
306
|
+
? role.outward
|
|
307
|
+
: { x: -role.outward.x, y: -role.outward.y };
|
|
308
|
+
return boundaryCandidate(role.point, direction, placement.gap, text);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function arcOutsideStartCrossesCenter(candidate, geometry, text) {
|
|
312
|
+
if (geometry.kind !== "arc") return false;
|
|
313
|
+
const radial = arcRole(geometry, "end").outward;
|
|
314
|
+
const projections = projectionRange(candidateBounds(candidate, text), radial);
|
|
315
|
+
const centerProjection = geometry.centerX * radial.x + geometry.centerY * radial.y;
|
|
316
|
+
return projections.minimum < centerProjection - 1e-9;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function fitsSource(candidate, geometry, text) {
|
|
320
|
+
if (geometry.kind === "point") return true;
|
|
321
|
+
const bounds = candidateBounds(candidate, text);
|
|
322
|
+
return geometry.kind === "arc"
|
|
323
|
+
? arcContains(bounds, geometry)
|
|
324
|
+
: intervalContains(bounds, geometry.bounds);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function outsideFallback(anchor) {
|
|
328
|
+
return anchor === "insideStart" ? "outsideStart" : "outsideEnd";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function resolveMarkLabelPlacement({ placement, geometry, text } = {}, profile) {
|
|
332
|
+
text = { ...text, profile };
|
|
333
|
+
const normalized = normalizeMarkLabelPlacement(placement);
|
|
334
|
+
validatePlacementGeometry(geometry, normalized.anchor);
|
|
335
|
+
let anchor = normalized.anchor;
|
|
336
|
+
let candidate = candidateForAnchor(anchor, geometry, normalized, text);
|
|
337
|
+
const requiresFit = anchor === "center" || anchor.startsWith("inside");
|
|
338
|
+
if (requiresFit && !fitsSource(candidate, geometry, text)) {
|
|
339
|
+
if (normalized.overflow === "hide") {
|
|
340
|
+
return cloneAndFreeze({ visible: false, anchor, fallback: false });
|
|
341
|
+
}
|
|
342
|
+
if (normalized.overflow === "outside") {
|
|
343
|
+
anchor = outsideFallback(anchor);
|
|
344
|
+
candidate = candidateForAnchor(anchor, geometry, normalized, text);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (
|
|
348
|
+
anchor === "outsideStart" &&
|
|
349
|
+
normalized.overflow !== "allow" &&
|
|
350
|
+
arcOutsideStartCrossesCenter(candidate, geometry, text)
|
|
351
|
+
) {
|
|
352
|
+
return cloneAndFreeze({
|
|
353
|
+
visible: false,
|
|
354
|
+
anchor,
|
|
355
|
+
fallback: anchor !== normalized.anchor
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return cloneAndFreeze({
|
|
359
|
+
visible: true,
|
|
360
|
+
anchor,
|
|
361
|
+
fallback: anchor !== normalized.anchor,
|
|
362
|
+
...candidate,
|
|
363
|
+
bounds: candidateBounds(candidate, text)
|
|
364
|
+
});
|
|
365
|
+
}
|
|
22
366
|
|
|
23
367
|
export const DEFAULT_LABEL_LAYOUT_GEOMETRY = cloneAndFreeze({
|
|
24
368
|
axis: "both",
|
|
@@ -158,7 +502,7 @@ export function enumerateLabelOffsets({ axis, padding, maxDisplacement }) {
|
|
|
158
502
|
return cloneAndFreeze(candidates);
|
|
159
503
|
}
|
|
160
504
|
|
|
161
|
-
function textBounds(item, offset = { x: 0, y: 0 }) {
|
|
505
|
+
function textBounds(item, offset = { x: 0, y: 0 }, profile) {
|
|
162
506
|
return resolveTextBounds({
|
|
163
507
|
x: item.x + offset.x,
|
|
164
508
|
y: item.y + offset.y,
|
|
@@ -169,7 +513,7 @@ function textBounds(item, offset = { x: 0, y: 0 }) {
|
|
|
169
513
|
textAlign: item.textAlign,
|
|
170
514
|
textBaseline: item.textBaseline,
|
|
171
515
|
rotation: item.rotation
|
|
172
|
-
});
|
|
516
|
+
}, profile);
|
|
173
517
|
}
|
|
174
518
|
|
|
175
519
|
function candidateScore(bounds, boundary, placed, candidate, order) {
|
|
@@ -209,7 +553,7 @@ function overlapPairs(items) {
|
|
|
209
553
|
return pairs;
|
|
210
554
|
}
|
|
211
555
|
|
|
212
|
-
export function resolveLabelLayout({ items, bounds, ...options } = {}) {
|
|
556
|
+
export function resolveLabelLayout({ items, bounds, ...options } = {}, profile) {
|
|
213
557
|
if (!Array.isArray(items)) {
|
|
214
558
|
throw new TypeError("Label layout requires an item array.");
|
|
215
559
|
}
|
|
@@ -224,14 +568,14 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}) {
|
|
|
224
568
|
);
|
|
225
569
|
const base = items.map(item => ({
|
|
226
570
|
id: item.id,
|
|
227
|
-
collisionBounds: expanded(textBounds(item), policy.padding)
|
|
571
|
+
collisionBounds: expanded(textBounds(item, undefined, profile), policy.padding)
|
|
228
572
|
}));
|
|
229
573
|
const placed = [];
|
|
230
574
|
const resolved = [];
|
|
231
575
|
for (const item of items) {
|
|
232
576
|
let best;
|
|
233
577
|
for (const [order, candidate] of candidates.entries()) {
|
|
234
|
-
const boundsAtCandidate = textBounds(item, candidate);
|
|
578
|
+
const boundsAtCandidate = textBounds(item, candidate, profile);
|
|
235
579
|
const collisionBounds = expanded(boundsAtCandidate, policy.padding);
|
|
236
580
|
const score = candidateScore(
|
|
237
581
|
collisionBounds,
|
|
@@ -276,7 +620,7 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}) {
|
|
|
276
620
|
});
|
|
277
621
|
}
|
|
278
622
|
|
|
279
|
-
export function assertPolarTextLayout({ canvas, items, label }) {
|
|
623
|
+
export function assertPolarTextLayout({ canvas, items, label }, profile) {
|
|
280
624
|
const dimensions = canvas?.properties;
|
|
281
625
|
if (
|
|
282
626
|
![dimensions?.width, dimensions?.height].every(Number.isFinite) ||
|
|
@@ -284,7 +628,7 @@ export function assertPolarTextLayout({ canvas, items, label }) {
|
|
|
284
628
|
) {
|
|
285
629
|
throw new Error("Polar text layout requires finite Canvas dimensions.");
|
|
286
630
|
}
|
|
287
|
-
const bounds = items.map(resolveTextBounds);
|
|
631
|
+
const bounds = items.map(item => resolveTextBounds(item, profile));
|
|
288
632
|
const outside = bounds.some(item => !textBoundsFitCanvas(item, dimensions));
|
|
289
633
|
let overlap = false;
|
|
290
634
|
for (let first = 0; first < bounds.length && !overlap; first += 1) {
|
package/src/layout/legend.js
CHANGED
|
@@ -3,15 +3,15 @@ import {
|
|
|
3
3
|
measureTextWidth
|
|
4
4
|
} from "../core/textMetrics.js";
|
|
5
5
|
|
|
6
|
-
export function measureLegendTextWidth(value, style = { fontSize: 12 }) {
|
|
7
|
-
return measureTextWidth(formatVisibleText(value), style);
|
|
6
|
+
export function measureLegendTextWidth(value, style = { fontSize: 12 }, profile) {
|
|
7
|
+
return measureTextWidth(formatVisibleText(value), style, profile);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function resolveLegendGrid(config, width, count, symbolHeight) {
|
|
10
|
+
export function resolveLegendGrid(config, width, count, symbolHeight, profile) {
|
|
11
11
|
const labels = config.domain.map(formatVisibleText);
|
|
12
12
|
const itemWidths = labels.map(
|
|
13
13
|
label => width + config.labels.offset +
|
|
14
|
-
measureLegendTextWidth(label, config.labels)
|
|
14
|
+
measureLegendTextWidth(label, config.labels, profile)
|
|
15
15
|
);
|
|
16
16
|
const columnCount = Math.min(config.columns ?? count, count);
|
|
17
17
|
const rowCount = Math.ceil(count / columnCount);
|
|
@@ -3,7 +3,7 @@ import { alignLegendStart, resolveLegendGrid } from "./legend.js";
|
|
|
3
3
|
|
|
4
4
|
// Measure item content before choosing its edge. Family owners supply the
|
|
5
5
|
// sample dimensions and formatted labels; this module does not inspect scales.
|
|
6
|
-
export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
6
|
+
export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, profile) {
|
|
7
7
|
const strokes = labels.map((_, index) => Array.isArray(symbol.strokeWidth)
|
|
8
8
|
? symbol.strokeWidth[index] : symbol.strokeWidth ?? 0);
|
|
9
9
|
const sampleBounds = labels.map((_, index) => symbol.itemBounds?.[index] ?? {
|
|
@@ -19,17 +19,17 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
|
19
19
|
...sampleBounds.map(bounds => 2 * Math.max(-bounds.top, bounds.bottom)));
|
|
20
20
|
const side = ["left", "right"].includes(config.position);
|
|
21
21
|
const titleVisible = config.titleVisible !== false;
|
|
22
|
-
const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle) : 0;
|
|
22
|
+
const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle, profile) : 0;
|
|
23
23
|
const titleHeight = titleVisible ? config.titleStyle.fontSize : 0;
|
|
24
24
|
const itemWidth = sampleWidth + config.labels.offset + Math.max(...labels.map(
|
|
25
|
-
text => measureTextWidth(text, config.labels)
|
|
25
|
+
text => measureTextWidth(text, config.labels, profile)
|
|
26
26
|
));
|
|
27
27
|
let symbolX;
|
|
28
28
|
let labelX;
|
|
29
29
|
let itemY;
|
|
30
30
|
let title;
|
|
31
31
|
if (config.layout === "legacy-bottom") {
|
|
32
|
-
const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels));
|
|
32
|
+
const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels, profile));
|
|
33
33
|
const width = widths.reduce((sum, value) => sum + value, 0) + config.itemGap * (labels.length - 1);
|
|
34
34
|
const x = config.align === "center" ? (canvas.width - width) / 2 : alignLegendStart(plot, width, config.align);
|
|
35
35
|
let cursor = x;
|
|
@@ -54,7 +54,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
|
54
54
|
itemY = labels.map((_, index) => firstY + index * pitch);
|
|
55
55
|
title = { x, y: plot.y + 20, align: "left" };
|
|
56
56
|
} else {
|
|
57
|
-
const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight);
|
|
57
|
+
const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight, profile);
|
|
58
58
|
const inline = config.titlePosition === "left" && titleVisible;
|
|
59
59
|
const prefix = inline ? titleWidth + 20 : 0;
|
|
60
60
|
const width = inline ? prefix + grid.gridWidth : Math.max(titleWidth, grid.gridWidth);
|
|
@@ -79,7 +79,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
|
79
79
|
}
|
|
80
80
|
const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
|
|
81
81
|
x, y, text, ...style, textAlign: align, textBaseline: "middle"
|
|
82
|
-
});
|
|
82
|
+
}, profile);
|
|
83
83
|
const bounds = [
|
|
84
84
|
...(titleVisible ? [textBounds(title.x, title.y, config.title, config.titleStyle, title.align)] : []),
|
|
85
85
|
...labels.map((label, index) => textBounds(labelX[index], itemY[index], label, config.labels)),
|
package/src/layout/text.js
CHANGED
|
@@ -5,12 +5,12 @@ export {
|
|
|
5
5
|
resolveTextBounds
|
|
6
6
|
} from "../core/textMetrics.js";
|
|
7
7
|
|
|
8
|
-
function wrapCodePoints(text, maxWidth, style) {
|
|
8
|
+
function wrapCodePoints(text, maxWidth, style, profile) {
|
|
9
9
|
const lines = [];
|
|
10
10
|
let line = "";
|
|
11
11
|
for (const codePoint of [...text]) {
|
|
12
12
|
const candidate = line + codePoint;
|
|
13
|
-
if (line !== "" && measureTextWidth(candidate, style) > maxWidth) {
|
|
13
|
+
if (line !== "" && measureTextWidth(candidate, style, profile) > maxWidth) {
|
|
14
14
|
lines.push(line);
|
|
15
15
|
line = codePoint;
|
|
16
16
|
} else {
|
|
@@ -21,22 +21,22 @@ function wrapCodePoints(text, maxWidth, style) {
|
|
|
21
21
|
return lines;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function wrapWords(text, maxWidth, style) {
|
|
24
|
+
function wrapWords(text, maxWidth, style, profile) {
|
|
25
25
|
const lines = [];
|
|
26
26
|
let line = "";
|
|
27
27
|
for (const word of text.trim().split(/\s+/u)) {
|
|
28
|
-
if (measureTextWidth(word, style) > maxWidth) {
|
|
28
|
+
if (measureTextWidth(word, style, profile) > maxWidth) {
|
|
29
29
|
if (line !== "") {
|
|
30
30
|
lines.push(line);
|
|
31
31
|
line = "";
|
|
32
32
|
}
|
|
33
|
-
const fragments = wrapCodePoints(word, maxWidth, style);
|
|
33
|
+
const fragments = wrapCodePoints(word, maxWidth, style, profile);
|
|
34
34
|
lines.push(...fragments.slice(0, -1));
|
|
35
35
|
line = fragments.at(-1);
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
38
|
const candidate = line === "" ? word : `${line} ${word}`;
|
|
39
|
-
if (line !== "" && measureTextWidth(candidate, style) > maxWidth) {
|
|
39
|
+
if (line !== "" && measureTextWidth(candidate, style, profile) > maxWidth) {
|
|
40
40
|
lines.push(line);
|
|
41
41
|
line = word;
|
|
42
42
|
} else {
|
|
@@ -50,7 +50,8 @@ function wrapWords(text, maxWidth, style) {
|
|
|
50
50
|
export function wrapText(text, {
|
|
51
51
|
maxWidth,
|
|
52
52
|
mode = "word",
|
|
53
|
-
style
|
|
53
|
+
style,
|
|
54
|
+
profile
|
|
54
55
|
} = {}) {
|
|
55
56
|
if (maxWidth === undefined) return [text];
|
|
56
57
|
if (!Number.isFinite(maxWidth) || maxWidth <= 0) {
|
|
@@ -60,6 +61,6 @@ export function wrapText(text, {
|
|
|
60
61
|
throw new Error(`Unsupported text wrap mode "${mode}".`);
|
|
61
62
|
}
|
|
62
63
|
return mode === "word"
|
|
63
|
-
? wrapWords(text, maxWidth, style)
|
|
64
|
-
: wrapCodePoints(text, maxWidth, style);
|
|
64
|
+
? wrapWords(text, maxWidth, style, profile)
|
|
65
|
+
: wrapCodePoints(text, maxWidth, style, profile);
|
|
65
66
|
}
|
package/src/layout/title.js
CHANGED
|
@@ -4,16 +4,16 @@ function lineCenters(lines, style, lineHeight, start) {
|
|
|
4
4
|
return lines.map((_, index) => start + style.fontSize / 2 + index * lineHeight);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export function buildTitleReadingBlock({ text, subtitle }, config) {
|
|
7
|
+
export function buildTitleReadingBlock({ text, subtitle }, config, profile) {
|
|
8
8
|
const titleLines = wrapText(text, {
|
|
9
9
|
maxWidth: config.maxWidth,
|
|
10
10
|
mode: config.wrap,
|
|
11
|
-
style: config.titleStyle
|
|
11
|
+
style: config.titleStyle, profile
|
|
12
12
|
});
|
|
13
13
|
const subtitleLines = subtitle === undefined ? [] : wrapText(subtitle, {
|
|
14
14
|
maxWidth: config.maxWidth,
|
|
15
15
|
mode: config.wrap,
|
|
16
|
-
style: config.subtitleStyle
|
|
16
|
+
style: config.subtitleStyle, profile
|
|
17
17
|
});
|
|
18
18
|
const titleLineHeight = config.lineHeight ?? config.titleStyle.fontSize * 1.2;
|
|
19
19
|
const subtitleLineHeight = config.lineHeight ??
|
|
@@ -36,8 +36,8 @@ export function buildTitleReadingBlock({ text, subtitle }, config) {
|
|
|
36
36
|
? titleBottom
|
|
37
37
|
: subtitleCenters.at(-1) + config.subtitleStyle.fontSize / 2;
|
|
38
38
|
const widths = [
|
|
39
|
-
...titleLines.map(line => measureTextWidth(line, config.titleStyle)),
|
|
40
|
-
...subtitleLines.map(line => measureTextWidth(line, config.subtitleStyle))
|
|
39
|
+
...titleLines.map(line => measureTextWidth(line, config.titleStyle, profile)),
|
|
40
|
+
...subtitleLines.map(line => measureTextWidth(line, config.subtitleStyle, profile))
|
|
41
41
|
];
|
|
42
42
|
return {
|
|
43
43
|
titleLines,
|
|
@@ -61,7 +61,7 @@ export function alignedTextAnchor(start, length, align) {
|
|
|
61
61
|
return start + length;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
|
|
64
|
+
function axisAlignedTextBounds({ x, y, text, style, align, rotation }, profile) {
|
|
65
65
|
return resolveTextBounds({
|
|
66
66
|
x,
|
|
67
67
|
y,
|
|
@@ -70,7 +70,7 @@ function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
|
|
|
70
70
|
textAlign: align,
|
|
71
71
|
textBaseline: "middle",
|
|
72
72
|
rotation
|
|
73
|
-
});
|
|
73
|
+
}, profile);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function unionBounds(bounds) {
|
|
@@ -82,7 +82,7 @@ function unionBounds(bounds) {
|
|
|
82
82
|
}), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export function resolveTitleComponentBounds(component, style) {
|
|
85
|
+
export function resolveTitleComponentBounds(component, style, profile) {
|
|
86
86
|
return unionBounds(component.lines.map((text, index) => axisAlignedTextBounds({
|
|
87
87
|
x: Array.isArray(component.x) ? component.x[index] : component.x,
|
|
88
88
|
y: Array.isArray(component.y) ? component.y[index] : component.y,
|
|
@@ -90,7 +90,7 @@ export function resolveTitleComponentBounds(component, style) {
|
|
|
90
90
|
style,
|
|
91
91
|
align: component.textAlign,
|
|
92
92
|
rotation: component.rotation
|
|
93
|
-
})));
|
|
93
|
+
}, profile)));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export function unionTitleBounds(bounds) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { deriveSeriesBarCells } from "../../grammar/bars/aggregate.js";
|
|
2
2
|
import { resolveBarChannels, resolveBarColorLayout, resolveBarOffsetChannel } from "../../grammar/bars/policy.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
mapContinuousScaleValues,
|
|
5
|
+
mapOrdinalPositionValues,
|
|
6
|
+
readScaleField
|
|
7
|
+
} from "../../grammar/scales/index.js";
|
|
4
8
|
import { mapScaleConsumerValues } from "../scales/map.js";
|
|
5
9
|
import { DEFAULT_BAR_FILL, resolveBarAppearance } from "./resolve.js";
|
|
6
10
|
import { resolveBarWidth } from "../../grammar/bars/geometry.js";
|
|
@@ -19,6 +23,24 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
|
|
|
19
23
|
const segments = deriveSeriesBarCells(dataset.values, layer, temporal ? undefined : categoryScale.domain,
|
|
20
24
|
grouped ? offsetScale.domain : layer.encoding.group === undefined && color?.fieldType !== "quantitative" ? colorScale?.domain : undefined);
|
|
21
25
|
const fills = color === undefined ? undefined : mapScaleConsumerValues(segments.map(segment => segment.cell.color), colorScale, "color");
|
|
26
|
+
const stroke = layer.encoding?.stroke;
|
|
27
|
+
const strokeValues = stroke === undefined ? undefined : segments.map(segment => {
|
|
28
|
+
const values = readScaleField(
|
|
29
|
+
segment.cell.sourceIndices.map(index => dataset.values[index]),
|
|
30
|
+
stroke.field,
|
|
31
|
+
stroke.fieldType,
|
|
32
|
+
{ temporalUnit: stroke.temporalUnit }
|
|
33
|
+
);
|
|
34
|
+
if (new Set(values).size !== 1) {
|
|
35
|
+
throw new Error("Bar stroke requires one value within each aggregate cell.");
|
|
36
|
+
}
|
|
37
|
+
return values[0];
|
|
38
|
+
});
|
|
39
|
+
const strokes = strokeValues === undefined ? undefined : mapScaleConsumerValues(
|
|
40
|
+
strokeValues,
|
|
41
|
+
resolved.resolvedScales[stroke.scale],
|
|
42
|
+
"stroke"
|
|
43
|
+
);
|
|
22
44
|
const config = resolved.markConfigs[layer.id] ?? {};
|
|
23
45
|
const existing = resolved.graphicSpec.objects[layer.id].items;
|
|
24
46
|
const width = resolveBarWidth(widthConfig, grouped ? offsetScale.bandwidth : Math.abs(categoryScale.bandwidth ?? categoryScale.step));
|
|
@@ -42,6 +64,7 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
|
|
|
42
64
|
return { x: vertical ? position : Math.min(a, b), y: vertical ? Math.min(a, b) : position,
|
|
43
65
|
width: vertical ? width : Math.abs(a - b), height: vertical ? Math.abs(a - b) : width,
|
|
44
66
|
fill: fills?.[index] ?? config.barAppearance?.fill ?? config.fill ?? DEFAULT_BAR_FILL,
|
|
45
|
-
...resolveBarAppearance(config, existing[index]?.properties)
|
|
67
|
+
...resolveBarAppearance(config, existing[index]?.properties),
|
|
68
|
+
...(strokes === undefined ? {} : { stroke: strokes[index] }) };
|
|
46
69
|
});
|
|
47
70
|
}
|