ggaction 0.0.13 → 0.0.15
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 +78 -0
- package/README.md +20 -11
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +8 -3
- package/src/actions/basic.js +2 -2
- package/src/actions/boxPlots/components.js +14 -4
- package/src/actions/boxPlots/edit.js +31 -5
- package/src/actions/boxPlots/materialize.js +11 -3
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/categoryOrder/index.js +15 -4
- package/src/actions/charts/area.js +5 -1
- package/src/actions/charts/bar.js +2 -1
- package/src/actions/charts/density.js +6 -2
- package/src/actions/charts/ecdf.js +4 -1
- package/src/actions/charts/endpoints.js +25 -3
- package/src/actions/charts/heatmap.js +2 -1
- package/src/actions/charts/histogram.js +4 -3
- package/src/actions/charts/horizon.js +3 -1
- package/src/actions/charts/interval-regression.js +5 -2
- package/src/actions/charts/line.js +3 -1
- package/src/actions/charts/parallel.js +8 -2
- package/src/actions/charts/pie.js +10 -2
- package/src/actions/charts/polar.js +15 -4
- package/src/actions/charts/radar.js +8 -2
- package/src/actions/charts/radial.js +10 -2
- package/src/actions/charts/rug-strip.js +6 -2
- package/src/actions/charts/scatter.js +3 -1
- package/src/actions/composition/actions.js +13 -4
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/bin2d.js +57 -185
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +2 -2
- package/src/actions/data/create.js +2 -2
- package/src/actions/data/density.js +12 -20
- package/src/actions/data/derived.js +47 -17
- package/src/actions/data/edit.js +539 -0
- package/src/actions/data/filter.js +14 -2
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +14 -0
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +16 -21
- package/src/actions/data/revisionBindings.js +145 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/summary.js +1 -1
- package/src/actions/data/timeUnit.js +4 -1
- package/src/actions/data/window.js +1 -1
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/index.js +2 -1
- package/src/actions/encodings/density.js +5 -2
- package/src/actions/encodings/histogram.js +3 -2
- package/src/actions/encodings/index.js +4 -0
- package/src/actions/encodings/offset.js +20 -13
- package/src/actions/encodings/position/apply.js +53 -7
- package/src/actions/encodings/position/index.js +10 -2
- package/src/actions/encodings/position/policies/bar.js +9 -0
- package/src/actions/encodings/position/resolve.js +24 -7
- package/src/actions/encodings/remove.js +9 -4
- package/src/actions/encodings/ruleAppearance.js +1 -23
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +82 -14
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/errorBands/create.js +23 -3
- package/src/actions/errorBands/edit.js +21 -6
- package/src/actions/errorBars/create.js +35 -7
- package/src/actions/errorBars/edit.js +19 -4
- package/src/actions/facets/actions.js +237 -57
- package/src/actions/facets/derive.js +91 -26
- package/src/actions/gradientPlots/components.js +10 -2
- package/src/actions/gradientPlots/edit.js +14 -4
- package/src/actions/gradientPlots/materialize.js +3 -1
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/guides/applicability.js +8 -2
- package/src/actions/guides/axes/labels.js +58 -12
- package/src/actions/guides/axes/lines.js +6 -2
- package/src/actions/guides/axes/parallel/resolve.js +6 -2
- package/src/actions/guides/axes/tickGroups.js +2 -1
- package/src/actions/guides/axes/ticks.js +6 -2
- package/src/actions/guides/axes/titles.js +11 -3
- package/src/actions/guides/grids/resolve.js +7 -2
- package/src/actions/guides/guides.js +19 -3
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +134 -48
- package/src/actions/guides/legends/categorical/components.js +17 -12
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +97 -15
- 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 +85 -30
- package/src/actions/guides/legends/continuous/common.js +30 -12
- package/src/actions/guides/legends/continuous/gradient.js +10 -7
- package/src/actions/guides/legends/continuous/index.js +2 -0
- package/src/actions/guides/legends/continuous/interval.js +8 -3
- package/src/actions/guides/legends/continuous/opacity.js +8 -5
- package/src/actions/guides/legends/continuous/stroke.js +525 -0
- package/src/actions/guides/legends/creation.js +14 -1
- package/src/actions/guides/legends/edit.js +158 -59
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +54 -19
- package/src/actions/guides/legends/lifecycle.js +19 -8
- package/src/actions/guides/legends/remove.js +13 -3
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +93 -40
- package/src/actions/guides/legends/strokeWidth.js +34 -28
- 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 +4 -4
- package/src/actions/guides/polar/axes/labels.js +24 -5
- package/src/actions/guides/polar/axes/lines.js +10 -4
- package/src/actions/guides/polar/axes/shared.js +16 -0
- package/src/actions/guides/polar/axes/ticks.js +1 -1
- package/src/actions/guides/polar/axes/titles.js +1 -1
- package/src/actions/guides/polar/grids.js +1 -1
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/index.js +14 -3
- package/src/actions/marks/arc/actions.js +77 -19
- package/src/actions/marks/area/actions.js +34 -9
- package/src/actions/marks/area/materialize.js +17 -1
- package/src/actions/marks/bar/create.js +11 -2
- package/src/actions/marks/bar/edit.js +20 -3
- package/src/actions/marks/bar/materialize.js +22 -2
- package/src/actions/marks/index.js +10 -1
- package/src/actions/marks/line/actions.js +37 -14
- package/src/actions/marks/line/materialize.js +29 -10
- package/src/actions/marks/point/create.js +8 -2
- package/src/actions/marks/point/edit.js +18 -6
- package/src/actions/marks/point/materialize.js +49 -15
- package/src/actions/marks/rect/actions.js +33 -12
- package/src/actions/marks/references.js +601 -30
- package/src/actions/marks/remove.js +71 -1
- package/src/actions/marks/rule/actions.js +47 -10
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +74 -9
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +154 -6
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +73 -0
- package/src/actions/marks/tick/actions.js +22 -5
- package/src/actions/primitives/editGraphics.js +26 -0
- package/src/actions/primitives/semantic.js +2 -1
- package/src/actions/primitives/semanticAction.js +83 -10
- 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 +26 -11
- package/src/actions/regression/create.js +10 -2
- package/src/actions/regression/edit.js +21 -7
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +172 -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 +54 -22
- package/src/actions/scales/definitions.js +94 -40
- package/src/actions/scales/edit.js +66 -11
- package/src/actions/scales/editPolicy.js +81 -11
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +4 -3
- 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/selection/actions.js +154 -30
- package/src/actions/selection/index.js +3 -0
- package/src/actions/theme/actions.js +68 -13
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +9 -2
- package/src/actions/theme/reconcile.js +315 -67
- package/src/actions/theme/state.js +160 -0
- package/src/actions/violinPlots/create.js +5 -2
- package/src/actions/violinPlots/edit.js +1 -0
- package/src/core/action.js +10 -0
- package/src/core/compositionState.js +6 -1
- package/src/core/programState.js +20 -0
- package/src/core/resourceReferences.js +332 -0
- package/src/core/vocabulary.js +16 -3
- 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 +340 -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/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +179 -30
- 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 +343 -0
- 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 +8 -2
- package/src/materialization/facetGuides/preparation.js +12 -7
- package/src/materialization/facetHeaders.js +346 -0
- package/src/materialization/facets.js +15 -65
- 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/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 +19 -4
- 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/renderers/canvas/circle.js +2 -3
- 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/svg.js +19 -0
- package/src/selectors/datasets.js +32 -1
- package/src/selectors/layers.js +4 -0
- package/src/theme/defaults.js +101 -0
- package/types/basic.d.ts +6 -0
- package/types/index.d.ts +85 -0
- package/types/program.d.ts +910 -117
package/src/layout/labels.js
CHANGED
|
@@ -19,6 +19,349 @@ 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 });
|
|
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 });
|
|
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 } = {}) {
|
|
332
|
+
const normalized = normalizeMarkLabelPlacement(placement);
|
|
333
|
+
validatePlacementGeometry(geometry, normalized.anchor);
|
|
334
|
+
let anchor = normalized.anchor;
|
|
335
|
+
let candidate = candidateForAnchor(anchor, geometry, normalized, text);
|
|
336
|
+
const requiresFit = anchor === "center" || anchor.startsWith("inside");
|
|
337
|
+
if (requiresFit && !fitsSource(candidate, geometry, text)) {
|
|
338
|
+
if (normalized.overflow === "hide") {
|
|
339
|
+
return cloneAndFreeze({ visible: false, anchor, fallback: false });
|
|
340
|
+
}
|
|
341
|
+
if (normalized.overflow === "outside") {
|
|
342
|
+
anchor = outsideFallback(anchor);
|
|
343
|
+
candidate = candidateForAnchor(anchor, geometry, normalized, text);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (
|
|
347
|
+
anchor === "outsideStart" &&
|
|
348
|
+
normalized.overflow !== "allow" &&
|
|
349
|
+
arcOutsideStartCrossesCenter(candidate, geometry, text)
|
|
350
|
+
) {
|
|
351
|
+
return cloneAndFreeze({
|
|
352
|
+
visible: false,
|
|
353
|
+
anchor,
|
|
354
|
+
fallback: anchor !== normalized.anchor
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return cloneAndFreeze({
|
|
358
|
+
visible: true,
|
|
359
|
+
anchor,
|
|
360
|
+
fallback: anchor !== normalized.anchor,
|
|
361
|
+
...candidate,
|
|
362
|
+
bounds: candidateBounds(candidate, text)
|
|
363
|
+
});
|
|
364
|
+
}
|
|
22
365
|
|
|
23
366
|
export const DEFAULT_LABEL_LAYOUT_GEOMETRY = cloneAndFreeze({
|
|
24
367
|
axis: "both",
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readSeriesIdentity } from "../../grammar/pathSeries.js";
|
|
2
2
|
import {
|
|
3
|
-
countHistogramBins,
|
|
4
3
|
findHistogramBinIndex,
|
|
5
4
|
resolveHistogramBins
|
|
6
5
|
} from "../../grammar/histogram.js";
|
|
@@ -8,14 +7,55 @@ import {
|
|
|
8
7
|
mapContinuousScaleValues,
|
|
9
8
|
mapOrdinalValues,
|
|
10
9
|
readNominalField,
|
|
11
|
-
readQuantitativeField
|
|
10
|
+
readQuantitativeField,
|
|
11
|
+
readScaleField
|
|
12
12
|
} from "../../grammar/scales/index.js";
|
|
13
|
+
import { mapScaleConsumerValues } from "../scales/map.js";
|
|
13
14
|
import { resolveBarColorLayout } from "../../grammar/bars/policy.js";
|
|
14
15
|
import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
|
|
15
16
|
import {
|
|
16
17
|
DEFAULT_BAR_FILL,
|
|
17
18
|
resolveBarAppearance
|
|
18
19
|
} from "./resolve.js";
|
|
20
|
+
import {
|
|
21
|
+
readStatisticalWeights,
|
|
22
|
+
sumEntryWeights,
|
|
23
|
+
summarizeStatisticalWeights,
|
|
24
|
+
validateWeightedNumericFields
|
|
25
|
+
} from "../../grammar/weightedStatistics.js";
|
|
26
|
+
|
|
27
|
+
export function resolveHistogramInput(dataset, xEncoding) {
|
|
28
|
+
const allValues = readQuantitativeField(dataset.values, xEncoding.field);
|
|
29
|
+
if (xEncoding.weight === undefined) {
|
|
30
|
+
return {
|
|
31
|
+
rows: dataset.values,
|
|
32
|
+
values: allValues,
|
|
33
|
+
entries: dataset.values.map((row, index) => ({ row, index })),
|
|
34
|
+
mass(entries) {
|
|
35
|
+
return entries.length;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const weights = readStatisticalWeights(
|
|
40
|
+
dataset.values,
|
|
41
|
+
xEncoding.weight,
|
|
42
|
+
"Histogram"
|
|
43
|
+
);
|
|
44
|
+
validateWeightedNumericFields(weights.entries, [xEncoding.field], "Histogram");
|
|
45
|
+
const summary = summarizeStatisticalWeights(
|
|
46
|
+
weights.entries,
|
|
47
|
+
weights.definition.kind,
|
|
48
|
+
"Histogram data"
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
rows: summary.positive.map(entry => entry.row),
|
|
52
|
+
values: summary.positive.map(entry => entry.row[xEncoding.field]),
|
|
53
|
+
entries: summary.positive,
|
|
54
|
+
mass(entries) {
|
|
55
|
+
return sumEntryWeights(entries, summary, "Histogram bin mass");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
19
59
|
|
|
20
60
|
export function deriveHistogramSegments({
|
|
21
61
|
dataset,
|
|
@@ -24,7 +64,8 @@ export function deriveHistogramSegments({
|
|
|
24
64
|
xScale,
|
|
25
65
|
resolvedScales
|
|
26
66
|
}) {
|
|
27
|
-
const
|
|
67
|
+
const input = resolveHistogramInput(dataset, xEncoding);
|
|
68
|
+
const { rows, values: xValues, entries } = input;
|
|
28
69
|
const bins = resolveHistogramBins({
|
|
29
70
|
values: xValues,
|
|
30
71
|
bin: xEncoding.bin,
|
|
@@ -36,13 +77,13 @@ export function deriveHistogramSegments({
|
|
|
36
77
|
const layout = resolveBarColorLayout(layer);
|
|
37
78
|
|
|
38
79
|
if (layer.encoding?.group !== undefined) {
|
|
39
|
-
const identity = readSeriesIdentity(
|
|
80
|
+
const identity = readSeriesIdentity(rows, layer);
|
|
40
81
|
const cellRows = bins.boundaries.slice(0, -1).map(() => identity.domain.map(() => []));
|
|
41
82
|
const index = new Map(identity.domain.map((value, i) => [value, i]));
|
|
42
83
|
xValues.forEach((value, i) => { const bin = findHistogramBinIndex(value, bins.boundaries);
|
|
43
|
-
if (bin !== -1) cellRows[bin][index.get(identity.values[i])].push(
|
|
44
|
-
return cellRows.flatMap((cells, bin) => layoutSeriesPartition(cells.map(
|
|
45
|
-
const members = cells[segment.index];
|
|
84
|
+
if (bin !== -1) cellRows[bin][index.get(identity.values[i])].push(entries[i]); });
|
|
85
|
+
return cellRows.flatMap((cells, bin) => layoutSeriesPartition(cells.map(input.mass), layout).map(segment => {
|
|
86
|
+
const members = cells[segment.index].map(entry => entry.row);
|
|
46
87
|
let color, colorValue;
|
|
47
88
|
if (colorEncoding !== undefined) {
|
|
48
89
|
const values = readNominalField(members, colorEncoding.field);
|
|
@@ -59,8 +100,13 @@ export function deriveHistogramSegments({
|
|
|
59
100
|
}
|
|
60
101
|
|
|
61
102
|
if (colorEncoding?.scale === undefined) {
|
|
62
|
-
|
|
63
|
-
|
|
103
|
+
const cellRows = bins.boundaries.slice(0, -1).map(() => []);
|
|
104
|
+
xValues.forEach((value, index) => {
|
|
105
|
+
const bin = findHistogramBinIndex(value, bins.boundaries);
|
|
106
|
+
if (bin !== -1) cellRows[bin].push(entries[index]);
|
|
107
|
+
});
|
|
108
|
+
return cellRows.flatMap((cell, bin) =>
|
|
109
|
+
layoutSeriesPartition([input.mass(cell)], layout).map(segment => ({
|
|
64
110
|
bin,
|
|
65
111
|
start: bins.boundaries[bin],
|
|
66
112
|
end: bins.boundaries[bin + 1],
|
|
@@ -68,9 +114,7 @@ export function deriveHistogramSegments({
|
|
|
68
114
|
categoryCount: 1,
|
|
69
115
|
stackStart: segment.start,
|
|
70
116
|
stackEnd: segment.end,
|
|
71
|
-
members:
|
|
72
|
-
findHistogramBinIndex(xValues[index], bins.boundaries) === bin
|
|
73
|
-
)
|
|
117
|
+
members: cell.map(entry => entry.row)
|
|
74
118
|
}))
|
|
75
119
|
);
|
|
76
120
|
}
|
|
@@ -81,24 +125,24 @@ export function deriveHistogramSegments({
|
|
|
81
125
|
`Bar mark "${layer.id}" requires a resolved color scale.`
|
|
82
126
|
);
|
|
83
127
|
}
|
|
84
|
-
const colorValues = readNominalField(
|
|
128
|
+
const colorValues = readNominalField(rows, colorEncoding.field);
|
|
85
129
|
mapOrdinalValues(colorValues, colorScale.domain, colorScale.range);
|
|
86
130
|
const categoryIndex = new Map(
|
|
87
131
|
colorScale.domain.map((value, index) => [value, index])
|
|
88
132
|
);
|
|
89
|
-
const
|
|
90
|
-
colorScale.domain.map(() =>
|
|
133
|
+
const cells = bins.boundaries.slice(0, -1).map(() =>
|
|
134
|
+
colorScale.domain.map(() => [])
|
|
91
135
|
);
|
|
92
136
|
|
|
93
137
|
for (let index = 0; index < xValues.length; index += 1) {
|
|
94
138
|
const bin = findHistogramBinIndex(xValues[index], bins.boundaries);
|
|
95
139
|
const category = categoryIndex.get(colorValues[index]);
|
|
96
|
-
if (bin !== -1 && category !== undefined)
|
|
140
|
+
if (bin !== -1 && category !== undefined) cells[bin][category].push(entries[index]);
|
|
97
141
|
}
|
|
98
142
|
|
|
99
143
|
const segments = [];
|
|
100
|
-
for (let bin = 0; bin <
|
|
101
|
-
const partition = layoutSeriesPartition(
|
|
144
|
+
for (let bin = 0; bin < cells.length; bin += 1) {
|
|
145
|
+
const partition = layoutSeriesPartition(cells[bin].map(input.mass), layout);
|
|
102
146
|
for (const segment of partition) {
|
|
103
147
|
const category = segment.index;
|
|
104
148
|
segments.push({
|
|
@@ -106,13 +150,10 @@ export function deriveHistogramSegments({
|
|
|
106
150
|
start: bins.boundaries[bin],
|
|
107
151
|
end: bins.boundaries[bin + 1],
|
|
108
152
|
category,
|
|
109
|
-
categoryCount:
|
|
153
|
+
categoryCount: cells[bin].length,
|
|
110
154
|
stackStart: segment.start,
|
|
111
155
|
stackEnd: segment.end,
|
|
112
|
-
members:
|
|
113
|
-
findHistogramBinIndex(xValues[index], bins.boundaries) === bin &&
|
|
114
|
-
categoryIndex.get(colorValues[index]) === category
|
|
115
|
-
),
|
|
156
|
+
members: cells[bin][category].map(entry => entry.row),
|
|
116
157
|
color: mapOrdinalValues(
|
|
117
158
|
[colorScale.domain[category]],
|
|
118
159
|
colorScale.domain,
|
|
@@ -135,6 +176,24 @@ export function deriveHistogramRectangles(required, resolved) {
|
|
|
135
176
|
const layout = resolveBarColorLayout(required.layer);
|
|
136
177
|
const config = resolved.markConfigs[required.layer.id] ?? {};
|
|
137
178
|
const appearance = config.barAppearance ?? {};
|
|
179
|
+
const stroke = required.layer.encoding?.stroke;
|
|
180
|
+
const strokeValues = stroke === undefined ? undefined : segments.map(segment => {
|
|
181
|
+
const values = readScaleField(
|
|
182
|
+
segment.members,
|
|
183
|
+
stroke.field,
|
|
184
|
+
stroke.fieldType,
|
|
185
|
+
{ temporalUnit: stroke.temporalUnit }
|
|
186
|
+
);
|
|
187
|
+
if (new Set(values).size !== 1) {
|
|
188
|
+
throw new Error("Histogram stroke requires one value within each bin/series cell.");
|
|
189
|
+
}
|
|
190
|
+
return values[0];
|
|
191
|
+
});
|
|
192
|
+
const strokes = strokeValues === undefined ? undefined : mapScaleConsumerValues(
|
|
193
|
+
strokeValues,
|
|
194
|
+
resolved.resolvedScales[stroke.scale],
|
|
195
|
+
"stroke"
|
|
196
|
+
);
|
|
138
197
|
|
|
139
198
|
return segments.map((segment, index) => {
|
|
140
199
|
const [x1, x2] = mapContinuousScaleValues(
|
|
@@ -164,7 +223,8 @@ export function deriveHistogramRectangles(required, resolved) {
|
|
|
164
223
|
config.fill ??
|
|
165
224
|
existing[index]?.properties.fill ??
|
|
166
225
|
DEFAULT_BAR_FILL,
|
|
167
|
-
...resolveBarAppearance(config, existing[index]?.properties)
|
|
226
|
+
...resolveBarAppearance(config, existing[index]?.properties),
|
|
227
|
+
...(strokes === undefined ? {} : { stroke: strokes[index] })
|
|
168
228
|
};
|
|
169
229
|
});
|
|
170
230
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mapContinuousScaleValues,
|
|
3
|
+
mapOrdinalPositionValues,
|
|
4
|
+
mapOrdinalValues,
|
|
5
|
+
readScaleField
|
|
6
|
+
} from "../../grammar/scales/index.js";
|
|
7
|
+
import { mapScaleConsumerValues } from "../scales/map.js";
|
|
2
8
|
import { resolveBarWidth } from "../../grammar/bars/geometry.js";
|
|
3
9
|
import {
|
|
4
10
|
DEFAULT_BAR_FILL,
|
|
@@ -36,6 +42,14 @@ export function deriveRangedRectangles(required, program, width) {
|
|
|
36
42
|
const appearanceConfig = color === undefined
|
|
37
43
|
? config
|
|
38
44
|
: { ...config, stroke: undefined };
|
|
45
|
+
const stroke = layer.encoding?.stroke;
|
|
46
|
+
const strokes = stroke === undefined ? undefined : mapScaleConsumerValues(
|
|
47
|
+
readScaleField(dataset.values, stroke.field, stroke.fieldType, {
|
|
48
|
+
temporalUnit: stroke.temporalUnit
|
|
49
|
+
}),
|
|
50
|
+
program.resolvedScales[stroke.scale],
|
|
51
|
+
"stroke"
|
|
52
|
+
);
|
|
39
53
|
return dataset.values.map((_, index) => vertical ? {
|
|
40
54
|
x: centers[index] - band / 2, y: Math.min(first[index], second[index]), width: band,
|
|
41
55
|
height: Math.abs(second[index] - first[index]), fill: fills[index],
|
|
@@ -44,7 +58,8 @@ export function deriveRangedRectangles(required, program, width) {
|
|
|
44
58
|
undefined,
|
|
45
59
|
color === undefined ? DEFAULT_BAR_STROKE : fills[index],
|
|
46
60
|
1
|
|
47
|
-
)
|
|
61
|
+
),
|
|
62
|
+
...(strokes === undefined ? {} : { stroke: strokes[index] })
|
|
48
63
|
} : {
|
|
49
64
|
x: Math.min(first[index], second[index]), y: centers[index] - band / 2,
|
|
50
65
|
width: Math.abs(second[index] - first[index]), height: band, fill: fills[index],
|
|
@@ -53,6 +68,7 @@ export function deriveRangedRectangles(required, program, width) {
|
|
|
53
68
|
undefined,
|
|
54
69
|
color === undefined ? DEFAULT_BAR_STROKE : fills[index],
|
|
55
70
|
1
|
|
56
|
-
)
|
|
71
|
+
),
|
|
72
|
+
...(strokes === undefined ? {} : { stroke: strokes[index] })
|
|
57
73
|
});
|
|
58
74
|
}
|
|
@@ -3,6 +3,7 @@ import { findDataset } from "../../selectors/datasets.js";
|
|
|
3
3
|
import { findLayer } from "../../selectors/layers.js";
|
|
4
4
|
import { findSemanticScale } from "../../selectors/scales.js";
|
|
5
5
|
import { BAR_GRAINS, resolveBarGrain } from "../../grammar/bars/policy.js";
|
|
6
|
+
import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_BAR_FILL = DEFAULT_COLORS.mark;
|
|
8
9
|
export const DEFAULT_BAR_STROKE = "white";
|
|
@@ -25,7 +26,8 @@ export function resolveBarAppearance(
|
|
|
25
26
|
? 0
|
|
26
27
|
: appearance.strokeWidth ?? config.strokeWidth ??
|
|
27
28
|
existing?.strokeWidth ?? DEFAULT_BAR_STROKE_WIDTH,
|
|
28
|
-
...(opacity === undefined ? {} : { opacity })
|
|
29
|
+
...(opacity === undefined ? {} : { opacity }),
|
|
30
|
+
...requestedStrokeDetails(appearance, "Bar mark")
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -40,9 +42,10 @@ export function requireCompleteBar(program, id) {
|
|
|
40
42
|
throw new Error(`Bar mark "${id}" requires an existing dataset.`);
|
|
41
43
|
}
|
|
42
44
|
const graphic = program.graphicSpec.objects[id];
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const roundedCollection = graphic?.type === "collection" &&
|
|
46
|
+
Object.hasOwn(program.markConfigs[id]?.barAppearance ?? {}, "cornerRadius") &&
|
|
47
|
+
graphic.items?.every(child => ["rect", "path"].includes(child.type));
|
|
48
|
+
if (graphic?.type !== "rect" && !roundedCollection) {
|
|
46
49
|
throw new Error(`Bar mark "${id}" requires rect graphics.`);
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -121,6 +121,8 @@ export function clearCompositionChildren(program) {
|
|
|
121
121
|
export function materializeCompositionGraphics(program) {
|
|
122
122
|
program._assertCompositionProgram("materializeCompositionGraphics");
|
|
123
123
|
const layout = resolveCompositionProgramLayout(program);
|
|
124
|
+
const background = program.graphicSpec.objects.canvas?.properties
|
|
125
|
+
?.background ?? "white";
|
|
124
126
|
let next = clearCompositionChildren(program);
|
|
125
127
|
if (next.graphicSpec.objects.canvas === undefined) {
|
|
126
128
|
next = next.createGraphics({ id: "canvas", type: "canvas" });
|
|
@@ -128,7 +130,7 @@ export function materializeCompositionGraphics(program) {
|
|
|
128
130
|
for (const [property, value] of Object.entries({
|
|
129
131
|
width: layout.width,
|
|
130
132
|
height: layout.height,
|
|
131
|
-
background
|
|
133
|
+
background
|
|
132
134
|
})) {
|
|
133
135
|
next = next.editGraphics({ target: "canvas", property, value });
|
|
134
136
|
}
|