ggaction 0.0.17 → 0.0.18
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 +46 -0
- package/README.md +2 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +3 -3
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +16 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +4 -4
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/edit.js +1 -1
- package/src/actions/data/regression.js +2 -1
- package/src/actions/data/shared.js +3 -2
- package/src/actions/data/stack.js +3 -1
- package/src/actions/encodings/appearance.js +37 -29
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +1 -1
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +3 -2
- package/src/actions/encodings/ranged.js +17 -0
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/text.js +6 -3
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +10 -4
- package/src/actions/marks/bar/edit.js +8 -4
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +7 -4
- package/src/actions/marks/text/actions.js +17 -5
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +3 -3
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +9 -4
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +8 -3
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +2 -2
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +81 -27
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +2 -12
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +3 -7
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +7 -1
- package/src/grammar/regression/models.js +83 -55
- package/src/grammar/regression/parameters.js +14 -3
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -197
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +2 -1
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +3 -26
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +22 -1
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +5 -3
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +13 -4
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +18 -5
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/svg.js +4 -1
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +1 -0
- package/types/index.d.ts +7 -0
- package/types/program.d.ts +347 -85
package/src/core/textMetrics.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateGeneratedItemLimit } from "./validation.js";
|
|
1
2
|
import { DEFAULT_FONT_FAMILY, textMetricFontWeight, textMetricKey } from "./font.js";
|
|
2
3
|
|
|
3
4
|
import { isOwned } from "./immutable.js";
|
|
@@ -13,7 +14,7 @@ function measuredWidth(profile, text, style) {
|
|
|
13
14
|
if (isOwned(profile)) measurementIndexes.set(profile, index);
|
|
14
15
|
}
|
|
15
16
|
return index.get(textMetricKey({ text, fontFamily: style.fontFamily ?? DEFAULT_FONT_FAMILY,
|
|
16
|
-
fontSize: style.fontSize, fontWeight: textMetricFontWeight(style.fontWeight) }));
|
|
17
|
+
fontSize: style.fontSize, fontWeight: textMetricFontWeight(style.fontWeight), fontStyle: style.fontStyle }));
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function codePointWidth(codePoint) {
|
|
@@ -43,7 +44,7 @@ export function textBoundsFitCanvas(bounds, canvas) {
|
|
|
43
44
|
|
|
44
45
|
export function measureTextWidth(
|
|
45
46
|
text,
|
|
46
|
-
{ fontSize, fontFamily, fontWeight } = {},
|
|
47
|
+
{ fontSize, fontFamily, fontWeight, fontStyle } = {},
|
|
47
48
|
profile
|
|
48
49
|
) {
|
|
49
50
|
if (typeof text !== "string") {
|
|
@@ -52,7 +53,7 @@ export function measureTextWidth(
|
|
|
52
53
|
if (!Number.isFinite(fontSize) || fontSize <= 0) {
|
|
53
54
|
throw new RangeError("Text measurement requires a positive fontSize.");
|
|
54
55
|
}
|
|
55
|
-
const measured = measuredWidth(profile, text, { fontSize, fontFamily, fontWeight });
|
|
56
|
+
const measured = measuredWidth(profile, text, { fontSize, fontFamily, fontWeight, fontStyle });
|
|
56
57
|
if (measured !== undefined) return measured;
|
|
57
58
|
let joined = false;
|
|
58
59
|
let width = 0;
|
|
@@ -91,9 +92,11 @@ export function resolveTextBounds({
|
|
|
91
92
|
x,
|
|
92
93
|
y,
|
|
93
94
|
text,
|
|
95
|
+
lines,
|
|
94
96
|
fontSize,
|
|
95
97
|
fontFamily,
|
|
96
98
|
fontWeight,
|
|
99
|
+
fontStyle,
|
|
97
100
|
textAlign = "left",
|
|
98
101
|
textBaseline = "alphabetic",
|
|
99
102
|
rotation = 0
|
|
@@ -101,7 +104,19 @@ export function resolveTextBounds({
|
|
|
101
104
|
if (![x, y, rotation].every(Number.isFinite)) {
|
|
102
105
|
throw new TypeError("Text bounds require finite x, y, and rotation values.");
|
|
103
106
|
}
|
|
104
|
-
|
|
107
|
+
if (lines !== undefined) {
|
|
108
|
+
const cosine = Math.cos(rotation), sine = Math.sin(rotation);
|
|
109
|
+
const bounds = lines.map(line => resolveTextBounds({
|
|
110
|
+
x: x + line.x * cosine - line.y * sine, y: y + line.x * sine + line.y * cosine,
|
|
111
|
+
text: line.text, fontSize, fontFamily, fontWeight, fontStyle, textAlign, textBaseline, rotation
|
|
112
|
+
}, profile));
|
|
113
|
+
const union = bounds.reduce((result, bound) => ({
|
|
114
|
+
left: Math.min(result.left, bound.left), right: Math.max(result.right, bound.right),
|
|
115
|
+
top: Math.min(result.top, bound.top), bottom: Math.max(result.bottom, bound.bottom)
|
|
116
|
+
}), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
|
|
117
|
+
return Object.freeze(requireFiniteBounds(union, "Text"));
|
|
118
|
+
}
|
|
119
|
+
const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight, fontStyle }, profile);
|
|
105
120
|
const [left, right] = textAlign === "center"
|
|
106
121
|
? [-width / 2, width / 2]
|
|
107
122
|
: ["right", "end"].includes(textAlign) ? [-width, 0] : [0, width];
|
|
@@ -126,3 +141,25 @@ export function resolveTextBounds({
|
|
|
126
141
|
};
|
|
127
142
|
return Object.freeze(requireFiniteBounds(bounds, "Text"));
|
|
128
143
|
}
|
|
144
|
+
|
|
145
|
+
export function unionBounds(bounds) {
|
|
146
|
+
return bounds.reduce((union, item) => ({
|
|
147
|
+
left: Math.min(union.left, item.left),
|
|
148
|
+
right: Math.max(union.right, item.right),
|
|
149
|
+
top: Math.min(union.top, item.top),
|
|
150
|
+
bottom: Math.max(union.bottom, item.bottom)
|
|
151
|
+
}), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Resolve explicit hard breaks once for text marks and guide layout.
|
|
155
|
+
export function resolveTextLines(text, config) {
|
|
156
|
+
const parts = text.split(/\r\n|\r|\n/u);
|
|
157
|
+
if (parts.length === 1 && config.blockAlign !== "middle") return undefined;
|
|
158
|
+
validateGeneratedItemLimit(parts.length, "Text line count");
|
|
159
|
+
const lineHeight = config.lineHeight ?? config.fontSize * 1.2;
|
|
160
|
+
const bounds = resolveTextBounds({ x: 0, y: 0, text: "", fontSize: config.fontSize,
|
|
161
|
+
textBaseline: config.baseline, textAlign: config.align });
|
|
162
|
+
const offset = config.blockAlign === "middle"
|
|
163
|
+
? -(bounds.top + bounds.bottom + (parts.length - 1) * lineHeight) / 2 : 0;
|
|
164
|
+
return parts.map((text, index) => ({ text, x: 0, y: offset + index * lineHeight }));
|
|
165
|
+
}
|
package/src/core/validation.js
CHANGED
|
@@ -98,3 +98,29 @@ export function validateNonNegativeFinite(value, label) {
|
|
|
98
98
|
}
|
|
99
99
|
return value;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
export function requireStringValue(value, label) {
|
|
103
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
104
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function rejectUnknownProperties(value, supported, label) {
|
|
110
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
111
|
+
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function validateFontWeight(value, label, numberType = "number") {
|
|
115
|
+
if (typeof value !== "string" && !Number.isFinite(value)) {
|
|
116
|
+
throw new TypeError(`${label} must be a string or ${numberType}.`);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function validateTextStyle(config, label) {
|
|
122
|
+
validateNonEmptyString(config.color, `${label} color`);
|
|
123
|
+
validatePositiveFinite(config.fontSize, `${label} fontSize`);
|
|
124
|
+
validateNonEmptyString(config.fontFamily, `${label} fontFamily`);
|
|
125
|
+
validateFontWeight(config.fontWeight, `${label} fontWeight`);
|
|
126
|
+
}
|
package/src/core/vocabulary.js
CHANGED
|
@@ -235,3 +235,7 @@ export const LEGEND_CONFIG_KINDS = Object.freeze([
|
|
|
235
235
|
export function includesVocabulary(vocabulary, value) {
|
|
236
236
|
return vocabulary.includes(value);
|
|
237
237
|
}
|
|
238
|
+
|
|
239
|
+
export const PLOT_LEGEND_POSITIONS = Object.freeze(["top-left", "top-right", "bottom-left", "bottom-right"]);
|
|
240
|
+
export const LEGEND_POSITIONS = Object.freeze(["left", "right", "top", "bottom", ...PLOT_LEGEND_POSITIONS]);
|
|
241
|
+
export const LEGEND_BLOCK_LAYOUTS = Object.freeze(["row", "column", "wrap"]);
|
package/src/grammar/aggregate.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { requireStringValue as nonEmptyString } from "../core/validation.js";
|
|
2
|
+
import { isGroupValue as isNominal } from "./transformKeys.js";
|
|
1
3
|
import { isPlainObject } from "../core/immutable.js";
|
|
2
4
|
import {
|
|
3
|
-
|
|
5
|
+
quantileSorted,
|
|
4
6
|
numericExtent,
|
|
5
7
|
requireFiniteResult,
|
|
6
8
|
stableFiniteDeviation,
|
|
@@ -25,11 +27,6 @@ const PARAMETERIZED_OPERATIONS = [
|
|
|
25
27
|
"quantile", "first", "last", "ciLower", "ciUpper"
|
|
26
28
|
];
|
|
27
29
|
|
|
28
|
-
function nonEmptyString(value, label) {
|
|
29
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
30
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
30
|
|
|
34
31
|
export function isScalarAggregate(value) {
|
|
35
32
|
return typeof value === "string" && SCALAR_OPERATIONS.has(value);
|
|
@@ -137,10 +134,6 @@ function isMissing(value) {
|
|
|
137
134
|
(typeof value === "number" && Number.isNaN(value));
|
|
138
135
|
}
|
|
139
136
|
|
|
140
|
-
function isNominal(value) {
|
|
141
|
-
return typeof value === "string" || typeof value === "boolean" ||
|
|
142
|
-
(typeof value === "number" && Number.isFinite(value));
|
|
143
|
-
}
|
|
144
137
|
|
|
145
138
|
export function validateAggregateFieldValues(rows, field, fieldType) {
|
|
146
139
|
if (!Array.isArray(rows)) {
|
|
@@ -165,14 +158,7 @@ function finiteValues(values) {
|
|
|
165
158
|
|
|
166
159
|
function quantile(values, probability) {
|
|
167
160
|
const ordered = [...values].sort((left, right) => left - right);
|
|
168
|
-
|
|
169
|
-
const lower = Math.floor(position);
|
|
170
|
-
const upper = Math.ceil(position);
|
|
171
|
-
return interpolateNumber(
|
|
172
|
-
ordered[lower],
|
|
173
|
-
ordered[upper],
|
|
174
|
-
position - lower
|
|
175
|
-
);
|
|
161
|
+
return quantileSorted(ordered, probability);
|
|
176
162
|
}
|
|
177
163
|
|
|
178
164
|
function comparableOrderKey(value) {
|
package/src/grammar/arcs.js
CHANGED
|
@@ -1,386 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
mapContinuousScaleValues,
|
|
4
|
-
mapOrdinalPositionValues,
|
|
5
|
-
readNominalField,
|
|
6
|
-
readQuantitativeField
|
|
7
|
-
} from "./scales/index.js";
|
|
8
|
-
import {
|
|
9
|
-
interpolateNumber,
|
|
10
|
-
normalizedFiniteSum,
|
|
11
|
-
requireFiniteResult,
|
|
12
|
-
stableFiniteSum
|
|
13
|
-
} from "./numeric.js";
|
|
14
|
-
|
|
15
|
-
function requireArcLayer(layer) {
|
|
16
|
-
if (layer?.mark?.type !== "arc") {
|
|
17
|
-
throw new Error("Arc derivation requires a semantic arc mark.");
|
|
18
|
-
}
|
|
19
|
-
const theta = layer.encoding?.theta;
|
|
20
|
-
if (
|
|
21
|
-
theta === undefined ||
|
|
22
|
-
!["quantitative", "nominal", "ordinal"].includes(theta.fieldType)
|
|
23
|
-
) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
`Arc mark "${layer.id}" requires quantitative or categorical theta.`
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return theta;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function requireContinuousThetaScale(scale, label) {
|
|
32
|
-
if (
|
|
33
|
-
scale?.type !== "linear" ||
|
|
34
|
-
!Array.isArray(scale.range) ||
|
|
35
|
-
scale.range.length !== 2 ||
|
|
36
|
-
!scale.range.every(Number.isFinite) ||
|
|
37
|
-
scale.range[0] === scale.range[1]
|
|
38
|
-
) {
|
|
39
|
-
throw new Error(`${label} requires a resolved linear scale.`);
|
|
40
|
-
}
|
|
41
|
-
return scale;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function requireBandScale(scale, label) {
|
|
45
|
-
if (
|
|
46
|
-
scale?.type !== "band" ||
|
|
47
|
-
!Array.isArray(scale.domain) ||
|
|
48
|
-
!Array.isArray(scale.range) ||
|
|
49
|
-
scale.range.length !== 2 ||
|
|
50
|
-
!scale.range.every(Number.isFinite) ||
|
|
51
|
-
scale.range[0] === scale.range[1] ||
|
|
52
|
-
!Number.isFinite(scale.bandwidth) ||
|
|
53
|
-
scale.bandwidth <= 0
|
|
54
|
-
) {
|
|
55
|
-
throw new Error(`${label} requires a resolved band scale.`);
|
|
56
|
-
}
|
|
57
|
-
return scale;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function colorValues(rows, encoding) {
|
|
61
|
-
return encoding === undefined
|
|
62
|
-
? rows.map(() => undefined)
|
|
63
|
-
: readNominalField(rows, encoding.field);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function readNonNegativeArcThetaValues(rows, field, layerId, role) {
|
|
67
|
-
if (!Array.isArray(rows)) throw new TypeError(`Arc theta ${role}s require rows.`);
|
|
68
|
-
if (typeof field !== "string" || field.length === 0) {
|
|
69
|
-
throw new TypeError(`Arc theta ${role} must be a non-empty string.`);
|
|
70
|
-
}
|
|
71
|
-
const values = rows.map((row, index) => {
|
|
72
|
-
const value = row?.[field];
|
|
73
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
74
|
-
throw new TypeError(
|
|
75
|
-
`Arc theta ${role} field "${field}" must contain non-negative finite numbers at row ${index}.`
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
return value;
|
|
79
|
-
});
|
|
80
|
-
if (values.every(value => value === 0)) {
|
|
81
|
-
throw new Error(`Arc mark "${layerId}" theta ${role}s must have a positive total.`);
|
|
82
|
-
}
|
|
83
|
-
return cloneAndFreeze(values);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function readArcThetaWeights(rows, field, layerId = "arc") {
|
|
87
|
-
return readNonNegativeArcThetaValues(rows, field, layerId, "weight");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function readArcThetaValues(rows, field, layerId = "arc") {
|
|
91
|
-
return readNonNegativeArcThetaValues(rows, field, layerId, "value");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function resolveProportionalRanges(values, range, labelAt) {
|
|
95
|
-
const total = values.reduce((sum, value) => sum + value, 0);
|
|
96
|
-
const normalizedTotal = Number.isFinite(total)
|
|
97
|
-
? undefined
|
|
98
|
-
: normalizedFiniteSum(values, "Arc theta values");
|
|
99
|
-
if (total === 0 || normalizedTotal?.total <= 0) {
|
|
100
|
-
throw new Error("Arc theta values must have a positive total.");
|
|
101
|
-
}
|
|
102
|
-
const span = range[1] - range[0];
|
|
103
|
-
let cursor = range[0];
|
|
104
|
-
let cumulative = 0;
|
|
105
|
-
let cumulativeCorrection = 0;
|
|
106
|
-
return values.map((value, index) => {
|
|
107
|
-
const normalizedValue = normalizedTotal === undefined
|
|
108
|
-
? value
|
|
109
|
-
: value / normalizedTotal.scale;
|
|
110
|
-
const next = cumulative + normalizedValue;
|
|
111
|
-
if (normalizedTotal !== undefined) {
|
|
112
|
-
cumulativeCorrection += Math.abs(cumulative) >= Math.abs(normalizedValue)
|
|
113
|
-
? cumulative - next + normalizedValue
|
|
114
|
-
: normalizedValue - next + cumulative;
|
|
115
|
-
}
|
|
116
|
-
cumulative = next;
|
|
117
|
-
const directEnd = cursor + value / total * span;
|
|
118
|
-
const endTheta = index === values.length - 1
|
|
119
|
-
? range[1]
|
|
120
|
-
: normalizedTotal === undefined && Number.isFinite(directEnd)
|
|
121
|
-
? directEnd
|
|
122
|
-
: interpolateNumber(
|
|
123
|
-
range[0],
|
|
124
|
-
range[1],
|
|
125
|
-
(cumulative + cumulativeCorrection) /
|
|
126
|
-
(normalizedTotal?.total ?? total)
|
|
127
|
-
);
|
|
128
|
-
if (!Number.isFinite(endTheta) || endTheta === cursor) {
|
|
129
|
-
throw new RangeError(
|
|
130
|
-
`Arc theta ${labelAt(index)} cannot resolve a distinct finite sector.`
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
const result = { startTheta: cursor, endTheta };
|
|
134
|
-
cursor = endTheta;
|
|
135
|
-
return result;
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function resolveProportionalRadii(layer, frame, innerRadiusRatio) {
|
|
140
|
-
if (!Number.isFinite(frame?.availableRadius) || frame.availableRadius < 0) {
|
|
141
|
-
throw new RangeError(`Arc mark "${layer.id}" requires a finite available radius.`);
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
innerRadius: requireFiniteResult(
|
|
145
|
-
frame.availableRadius * innerRadiusRatio,
|
|
146
|
-
`Arc mark "${layer.id}" inner radius`
|
|
147
|
-
),
|
|
148
|
-
outerRadius: frame.availableRadius
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
153
|
-
const theta = layer.encoding.theta;
|
|
154
|
-
if (!["count", "sum"].includes(theta.aggregate)) {
|
|
155
|
-
throw new Error(`Arc mark "${layer.id}" requires count, sum, or radial layout.`);
|
|
156
|
-
}
|
|
157
|
-
const values = readNominalField(rows, theta.field);
|
|
158
|
-
const colors = colorValues(rows, layer.encoding?.color);
|
|
159
|
-
const weights = theta.aggregate === "count"
|
|
160
|
-
? rows.map(() => 1)
|
|
161
|
-
: readArcThetaWeights(rows, theta.weight, layer.id);
|
|
162
|
-
const groups = new Map(thetaScale.domain.map(value => [value, []]));
|
|
163
|
-
for (let index = 0; index < rows.length; index += 1) {
|
|
164
|
-
const group = groups.get(values[index]);
|
|
165
|
-
if (group === undefined) {
|
|
166
|
-
throw new Error(`Arc theta value "${values[index]}" is outside the scale domain.`);
|
|
167
|
-
}
|
|
168
|
-
group.push({
|
|
169
|
-
color: colors[index],
|
|
170
|
-
sourceIndex: index,
|
|
171
|
-
weight: weights[index]
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
|
|
175
|
-
const sectors = [];
|
|
176
|
-
const positiveValues = thetaScale.domain.filter(value =>
|
|
177
|
-
groups.get(value).some(item => item.weight > 0)
|
|
178
|
-
);
|
|
179
|
-
const aggregateValues = positiveValues.map(value => stableFiniteSum(
|
|
180
|
-
groups.get(value).map(item => item.weight),
|
|
181
|
-
`Arc theta group "${String(value)}" aggregate`
|
|
182
|
-
));
|
|
183
|
-
const ranges = resolveProportionalRanges(
|
|
184
|
-
aggregateValues,
|
|
185
|
-
thetaScale.range,
|
|
186
|
-
index => `group "${String(positiveValues[index])}"`
|
|
187
|
-
);
|
|
188
|
-
for (const [positiveIndex, value] of positiveValues.entries()) {
|
|
189
|
-
const group = groups.get(value);
|
|
190
|
-
const aggregateValue = aggregateValues[positiveIndex];
|
|
191
|
-
const { startTheta, endTheta } = ranges[positiveIndex];
|
|
192
|
-
const distinctColors = [...new Set(group.map(item => item.color))];
|
|
193
|
-
if (distinctColors.length > 1) {
|
|
194
|
-
throw new Error(
|
|
195
|
-
`Aggregated arc theta group "${value}" must resolve to one color value.`
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
sectors.push({
|
|
199
|
-
key: value,
|
|
200
|
-
theta: value,
|
|
201
|
-
count: group.length,
|
|
202
|
-
aggregateValue,
|
|
203
|
-
color: distinctColors[0],
|
|
204
|
-
startTheta,
|
|
205
|
-
endTheta,
|
|
206
|
-
...radii,
|
|
207
|
-
sourceIndices: group.map(item => item.sourceIndex)
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
return sectors;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function quantitativeSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
214
|
-
const theta = layer.encoding.theta;
|
|
215
|
-
if (layer.encoding?.radius !== undefined) {
|
|
216
|
-
throw new Error(
|
|
217
|
-
`Arc mark "${layer.id}" cannot combine quantitative theta with radius.`
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
const values = readArcThetaValues(rows, theta.field, layer.id);
|
|
221
|
-
const colors = colorValues(rows, layer.encoding?.color);
|
|
222
|
-
const positiveIndices = values
|
|
223
|
-
.map((value, index) => value > 0 ? index : undefined)
|
|
224
|
-
.filter(index => index !== undefined);
|
|
225
|
-
const positiveValues = positiveIndices.map(index => values[index]);
|
|
226
|
-
const ranges = resolveProportionalRanges(
|
|
227
|
-
positiveValues,
|
|
228
|
-
thetaScale.range,
|
|
229
|
-
index => `row ${positiveIndices[index]}`
|
|
230
|
-
);
|
|
231
|
-
const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
|
|
232
|
-
return positiveIndices.map((sourceIndex, index) => ({
|
|
233
|
-
key: `${String(values[sourceIndex])}:${sourceIndex}`,
|
|
234
|
-
theta: values[sourceIndex],
|
|
235
|
-
count: 1,
|
|
236
|
-
aggregateValue: values[sourceIndex],
|
|
237
|
-
color: colors[sourceIndex],
|
|
238
|
-
...ranges[index],
|
|
239
|
-
...radii,
|
|
240
|
-
sourceIndices: [sourceIndex]
|
|
241
|
-
}));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export function deriveMeasuredArcValues(rows, layer) {
|
|
245
|
-
const theta = requireArcLayer(layer);
|
|
246
|
-
const radius = layer.encoding?.radius;
|
|
247
|
-
if (!["nominal", "ordinal"].includes(theta.fieldType) || theta.aggregate !== undefined ||
|
|
248
|
-
!["count", "sum"].includes(radius?.aggregate) || radius.fieldType !== "quantitative") {
|
|
249
|
-
throw new Error("Measured Arc requires equal-angle categorical theta and count or sum radius.");
|
|
250
|
-
}
|
|
251
|
-
if (radius.aggregate === "count" && Object.hasOwn(radius, "field")) {
|
|
252
|
-
throw new Error("Measured count radius does not accept a field.");
|
|
253
|
-
}
|
|
254
|
-
const categories = readNominalField(rows, theta.field);
|
|
255
|
-
const measures = radius.aggregate === "count" ? rows.map(() => 1)
|
|
256
|
-
: readQuantitativeField(rows, radius.field);
|
|
257
|
-
if (measures.some(value => value < 0)) {
|
|
258
|
-
throw new RangeError("Measured Arc radius inputs must be non-negative.");
|
|
259
|
-
}
|
|
260
|
-
const colors = colorValues(rows, layer.encoding?.color);
|
|
261
|
-
const groups = new Map();
|
|
262
|
-
for (const [index, category] of categories.entries()) {
|
|
263
|
-
const group = groups.get(category) ?? { key: category, sourceIndices: [], values: [], colors: [] };
|
|
264
|
-
group.sourceIndices.push(index);
|
|
265
|
-
group.values.push(measures[index]);
|
|
266
|
-
group.colors.push(colors[index]);
|
|
267
|
-
groups.set(category, group);
|
|
268
|
-
}
|
|
269
|
-
const result = [...groups.values()].map(group => {
|
|
270
|
-
const distinctColors = [...new Set(group.colors)];
|
|
271
|
-
if (distinctColors.length > 1) {
|
|
272
|
-
throw new Error(`Measured Arc category "${group.key}" must resolve to one color value.`);
|
|
273
|
-
}
|
|
274
|
-
return { key: group.key, theta: group.key, count: group.sourceIndices.length,
|
|
275
|
-
radius: stableFiniteSum(group.values, "Measured Arc radius aggregate"),
|
|
276
|
-
color: distinctColors[0], sourceIndices: group.sourceIndices };
|
|
277
|
-
});
|
|
278
|
-
if (!result.some(group => group.radius > 0)) {
|
|
279
|
-
throw new Error("Measured Arc radius requires at least one positive category aggregate.");
|
|
280
|
-
}
|
|
281
|
-
return cloneAndFreeze(result);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function measuredRadialSectors(rows, layer, thetaScale, radiusScale) {
|
|
285
|
-
const items = deriveMeasuredArcValues(rows, layer);
|
|
286
|
-
const centers = mapOrdinalPositionValues(items.map(item => item.theta), thetaScale);
|
|
287
|
-
const radii = mapContinuousScaleValues(items.map(item => item.radius), radiusScale);
|
|
288
|
-
const halfBand = (Math.sign(thetaScale.step) || 1) * thetaScale.bandwidth / 2;
|
|
289
|
-
const byCategory = new Map(items.map((item, index) => [item.theta, {
|
|
290
|
-
...item, startTheta: centers[index] - halfBand, endTheta: centers[index] + halfBand,
|
|
291
|
-
innerRadius: radiusScale.range[0], outerRadius: radii[index]
|
|
292
|
-
}]));
|
|
293
|
-
return thetaScale.domain.flatMap(category => {
|
|
294
|
-
const item = byCategory.get(category);
|
|
295
|
-
return item === undefined || item.radius === 0 ? [] : [item];
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function radialSectors(rows, layer, thetaScale, radiusScale) {
|
|
300
|
-
const theta = layer.encoding.theta;
|
|
301
|
-
const radius = layer.encoding?.radius;
|
|
302
|
-
if (radius?.fieldType !== "quantitative") {
|
|
303
|
-
throw new Error(`Arc mark "${layer.id}" requires quantitative radius.`);
|
|
304
|
-
}
|
|
305
|
-
const thetaValues = readNominalField(rows, theta.field);
|
|
306
|
-
const radiusValues = readQuantitativeField(rows, radius.field);
|
|
307
|
-
const colors = colorValues(rows, layer.encoding?.color);
|
|
308
|
-
const centers = mapOrdinalPositionValues(thetaValues, thetaScale);
|
|
309
|
-
const outerRadii = mapContinuousScaleValues(radiusValues, radiusScale);
|
|
310
|
-
const innerRadius = Math.min(...radiusScale.range);
|
|
311
|
-
const direction = Math.sign(thetaScale.step) || 1;
|
|
312
|
-
const halfBand = direction * thetaScale.bandwidth / 2;
|
|
313
|
-
const grouped = new Map(thetaScale.domain.map(value => [value, []]));
|
|
314
|
-
for (let index = 0; index < rows.length; index += 1) {
|
|
315
|
-
if (outerRadii[index] <= innerRadius) continue;
|
|
316
|
-
const group = grouped.get(thetaValues[index]);
|
|
317
|
-
if (group === undefined) {
|
|
318
|
-
throw new Error(
|
|
319
|
-
`Arc theta value "${thetaValues[index]}" is outside the scale domain.`
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
group.push({
|
|
323
|
-
key: `${String(thetaValues[index])}:${index}`,
|
|
324
|
-
theta: thetaValues[index],
|
|
325
|
-
radius: radiusValues[index],
|
|
326
|
-
color: colors[index],
|
|
327
|
-
startTheta: centers[index] - halfBand,
|
|
328
|
-
endTheta: centers[index] + halfBand,
|
|
329
|
-
innerRadius,
|
|
330
|
-
outerRadius: outerRadii[index],
|
|
331
|
-
sourceIndices: [index]
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
const sectors = [];
|
|
335
|
-
for (const value of thetaScale.domain) {
|
|
336
|
-
const group = grouped.get(value);
|
|
337
|
-
group.sort((left, right) =>
|
|
338
|
-
right.outerRadius - left.outerRadius ||
|
|
339
|
-
left.sourceIndices[0] - right.sourceIndices[0]
|
|
340
|
-
);
|
|
341
|
-
sectors.push(...group);
|
|
342
|
-
}
|
|
343
|
-
return sectors;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
export function deriveArcSectors(rows, layer, {
|
|
347
|
-
thetaScale,
|
|
348
|
-
radiusScale,
|
|
349
|
-
frame,
|
|
350
|
-
innerRadiusRatio = 0
|
|
351
|
-
} = {}) {
|
|
352
|
-
if (!Array.isArray(rows)) throw new TypeError("Arc derivation requires rows.");
|
|
353
|
-
const theta = requireArcLayer(layer);
|
|
354
|
-
if (
|
|
355
|
-
!Number.isFinite(innerRadiusRatio) ||
|
|
356
|
-
innerRadiusRatio < 0 ||
|
|
357
|
-
innerRadiusRatio >= 1
|
|
358
|
-
) {
|
|
359
|
-
throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
|
|
360
|
-
}
|
|
361
|
-
const sectors = radiusScale?.radialMapping !== undefined
|
|
362
|
-
? measuredRadialSectors(rows, layer, requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`), radiusScale)
|
|
363
|
-
: theta.fieldType === "quantitative"
|
|
364
|
-
? quantitativeSectors(
|
|
365
|
-
rows,
|
|
366
|
-
layer,
|
|
367
|
-
requireContinuousThetaScale(thetaScale, `Arc mark "${layer.id}" theta`),
|
|
368
|
-
frame,
|
|
369
|
-
innerRadiusRatio
|
|
370
|
-
)
|
|
371
|
-
: ["count", "sum"].includes(theta.aggregate)
|
|
372
|
-
? proportionalSectors(
|
|
373
|
-
rows,
|
|
374
|
-
layer,
|
|
375
|
-
requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
|
|
376
|
-
frame,
|
|
377
|
-
innerRadiusRatio
|
|
378
|
-
)
|
|
379
|
-
: radialSectors(
|
|
380
|
-
rows,
|
|
381
|
-
layer,
|
|
382
|
-
requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
|
|
383
|
-
radiusScale
|
|
384
|
-
);
|
|
385
|
-
return cloneAndFreeze({ sectors });
|
|
386
|
-
}
|
|
1
|
+
import{validateOptionObject as P,validateNonNegativeFinite as j}from"../core/validation.js";import{cloneAndFreeze as R,isPlainObject as B}from"../core/immutable.js";import{mapContinuousScaleValues as y,mapOrdinalPositionValues as M,readNominalField as b,readQuantitativeField as I}from"./scales/index.js";import{interpolateNumber as S,normalizedFiniteSum as _,stableFiniteSum as q}from"./numeric.js";function k(e){if(e?.mark?.type!=="arc")throw new Error("Arc derivation requires a semantic arc mark.");const t=e.encoding?.theta;if(t===void 0||!["quantitative","nominal","ordinal"].includes(t.fieldType))throw new Error(`Arc mark "${e.id}" requires quantitative or categorical theta.`);return t}function G(e,t){if(e?.type!=="linear"||!Array.isArray(e.range)||e.range.length!==2||!e.range.every(Number.isFinite)||e.range[0]===e.range[1])throw new Error(`${t} requires a resolved linear scale.`);return e}function L(e,t){if(e?.type!=="band"||!Array.isArray(e.domain)||!Array.isArray(e.range)||e.range.length!==2||!e.range.every(Number.isFinite)||e.range[0]===e.range[1]||!Number.isFinite(e.bandwidth)||e.bandwidth<=0)throw new Error(`${t} requires a resolved band scale.`);return e}function $(e,t){return t===void 0?e.map(()=>{}):b(e,t.field)}function F(e,t,r,n){if(!Array.isArray(e))throw new TypeError(`Arc theta ${n}s require rows.`);if(typeof t!="string"||t.length===0)throw new TypeError(`Arc theta ${n} must be a non-empty string.`);const s=e.map((u,a)=>{const i=u?.[t];if(!Number.isFinite(i)||i<0)throw new TypeError(`Arc theta ${n} field "${t}" must contain non-negative finite numbers at row ${a}.`);return i});if(s.every(u=>u===0))throw new Error(`Arc mark "${r}" theta ${n}s must have a positive total.`);return R(s)}function N(e,t,r="arc"){return F(e,t,r,"weight")}function V(e,t,r="arc"){return F(e,t,r,"value")}function Q(e,t,r){const n=e.reduce((c,d)=>c+d,0),s=Number.isFinite(n)?void 0:_(e,"Arc theta values");if(n===0||s?.total<=0)throw new Error("Arc theta values must have a positive total.");const u=t[1]-t[0];let a=t[0],i=0,o=0;return e.map((c,d)=>{const h=s===void 0?c:c/s.scale,v=i+h;s!==void 0&&(o+=Math.abs(i)>=Math.abs(h)?i-v+h:h-v+i),i=v;const p=a+c/n*u,l=d===e.length-1?t[1]:s===void 0&&Number.isFinite(p)?p:S(t[0],t[1],(i+o)/(s?.total??n));if(!Number.isFinite(l)||l===a)throw new RangeError(`Arc theta ${r(d)} cannot resolve a distinct finite sector.`);const m={startTheta:a,endTheta:l};return a=l,m})}function W(e,t,r){if(!Number.isFinite(t?.availableRadius)||t.availableRadius<0)throw new RangeError(`Arc mark "${e.id}" requires a finite available radius.`);return{innerRadius:U(r,t.availableRadius),outerRadius:t.availableRadius}}function D(e,t,r,n,s){const u=t.encoding.theta,a=u.fieldType==="quantitative";if(!a&&!["count","sum"].includes(u.aggregate))throw new Error(`Arc mark "${t.id}" requires count, sum, or radial layout.`);const i=a?V(e,u.field,t.id):u.aggregate==="count"?e.map(()=>1):N(e,u.weight,t.id),o=a?e.map((g,w)=>w):b(e,u.field),c=$(e,t.encoding?.color),d=new Map(z(o,i).map(g=>[g.key,g])),h=a?o:r.domain,v=new Set(h);for(const g of d.keys())if(!v.has(g))throw new Error(`Arc theta value "${g}" is outside the scale domain.`);const p=h.filter(g=>d.get(g)?.values.some(w=>w>0)),l=g=>a?`row ${p[g]}`:`group "${String(p[g])}"`,m=p.map((g,w)=>a?i[g]:q(d.get(g).values,`Arc theta ${l(w)} aggregate`)),f=Q(m,r.range,l),A=W(t,n,s);return p.map((g,w)=>{const E=d.get(g),T=[...new Set(E.sourceIndices.map(O=>c[O]))];if(T.length>1)throw new Error(`Aggregated arc theta group "${g}" must resolve to one color value.`);return{key:a?`${String(i[g])}:${g}`:g,theta:a?i[g]:g,count:E.sourceIndices.length,aggregateValue:m[w],color:T[0],...f[w],...A,sourceIndices:E.sourceIndices}})}function x(e,t,r){const n=t.aggregate==="count"?e.map(()=>1):I(e,t.field);return z(r,n)}function z(e,t){const r=new Map;for(const[n,s]of e.entries()){const u=r.get(s)??{key:s,sourceIndices:[],values:[]};u.sourceIndices.push(n),u.values.push(t[n]),r.set(s,u)}return[...r.values()]}function H(e,t){const r=k(t),n=t.encoding?.radius;if(!["nominal","ordinal"].includes(r.fieldType)||r.aggregate!==void 0||!["count","sum"].includes(n?.aggregate)||n.fieldType!=="quantitative")throw new Error("Measured Arc requires equal-angle categorical theta and count or sum radius.");if(n.aggregate==="count"&&Object.hasOwn(n,"field"))throw new Error("Measured count radius does not accept a field.");const s=b(e,r.field),u=$(e,t.encoding?.color),a=x(e,n,s);if(a.some(o=>o.values.some(c=>c<0)))throw new RangeError("Measured Arc radius inputs must be non-negative.");const i=a.map(o=>{const c=[...new Set(o.sourceIndices.map(d=>u[d]))];if(c.length>1)throw new Error(`Measured Arc category "${o.key}" must resolve to one color value.`);return{key:o.key,theta:o.key,count:o.sourceIndices.length,radius:q(o.values,"Measured Arc radius aggregate"),color:c[0],sourceIndices:o.sourceIndices}});if(!i.some(o=>o.radius>0))throw new Error("Measured Arc radius requires at least one positive category aggregate.");return R(i)}function J(e,t,r,n){const s=H(e,t),u=M(s.map(c=>c.theta),r),a=y(s.map(c=>c.radius),n),i=(Math.sign(r.step)||1)*r.bandwidth/2,o=new Map(s.map((c,d)=>[c.theta,{...c,startTheta:u[d]-i,endTheta:u[d]+i,innerRadius:n.range[0],outerRadius:a[d]}]));return r.domain.flatMap(c=>{const d=o.get(c);return d===void 0||d.radius===0?[]:[d]})}function K(e,t,r,n){const s=t.encoding.theta,u=t.encoding?.radius;if(u?.fieldType!=="quantitative")throw new Error(`Arc mark "${t.id}" requires quantitative radius.`);const a=b(e,s.field),i=I(e,u.field),o=$(e,t.encoding?.color),c=M(a,r),d=y(i,n),h=Math.min(...n.range),p=(Math.sign(r.step)||1)*r.bandwidth/2,l=new Map(r.domain.map(f=>[f,[]]));for(let f=0;f<e.length;f+=1){if(d[f]<=h)continue;const A=l.get(a[f]);if(A===void 0)throw new Error(`Arc theta value "${a[f]}" is outside the scale domain.`);A.push({key:`${String(a[f])}:${f}`,theta:a[f],radius:i[f],color:o[f],startTheta:c[f]-p,endTheta:c[f]+p,innerRadius:h,outerRadius:d[f],sourceIndices:[f]})}const m=[];for(const f of r.domain){const A=l.get(f);A.sort((g,w)=>w.outerRadius-g.outerRadius||g.sourceIndices[0]-w.sourceIndices[0]),m.push(...A)}return m}function re(e,t,{thetaScale:r,radiusScale:n,frame:s,innerRadius:u=0}={}){if(!Array.isArray(e))throw new TypeError("Arc derivation requires rows.");const a=k(t);C(u);const i=n?.radialMapping!==void 0,o=a.fieldType==="quantitative",c=(o&&!i?G:L)(r,`Arc mark "${t.id}" theta`);let d=i?J(e,t,c,n):o||["count","sum"].includes(a.aggregate)?D(e,t,c,s,u):K(e,t,c,n);if(n!==void 0&&!i&&(o||a.aggregate!==void 0)){const h=X(e,t),v=y(h.map(l=>l.radius),n),p=new Map(h.map((l,m)=>[l.sourceIndices[0],{radius:l.radius,outerRadius:v[m]}]));d=d.map(l=>{const m=p.get(l.sourceIndices[0]);if(m===void 0||!Number.isFinite(m.outerRadius)||m.outerRadius<=l.innerRadius)throw new RangeError("Arc outer radius must exceed inner radius.");return{...l,...m}})}return R({sectors:d})}function C(e){if(B(e)){if(P(e,["unit","value"],"Arc innerRadius"),e.unit!=="px")throw new RangeError("Arc innerRadius unit must be px.");return R({unit:"px",value:j(e.value,"Arc innerRadius")})}if(!Number.isFinite(e)||e<0||e>=1)throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");return e}function U(e,t){const r=C(e);if(typeof r=="number")return r*t;if(r.value>=t)throw new RangeError("Arc pixel innerRadius must be smaller than its outer radius.");return r.value}function X(e,t){const r=t.encoding?.theta,n=t.encoding?.radius;if(r===void 0)return[];const s=r.fieldType==="quantitative";if(s?n.aggregate!==void 0:!["count","sum"].includes(r.aggregate))throw new Error("Grouped arc radius requires aggregated categorical theta.");const u=s?V(e,r.field,t.id):r.aggregate==="count"?e.map(()=>1):N(e,r.weight,t.id),a=s?e.map((i,o)=>o):b(e,r.field);return x(e,n,a).filter(i=>i.sourceIndices.some(o=>u[o]>0)).map(({sourceIndices:i,values:o})=>{if(n.aggregate===void 0&&new Set(o).size!==1)throw new Error("Arc radius requires one value per theta group or explicit aggregation.");return{sourceIndices:i,radius:n.aggregate===void 0?o[0]:q(o,"Arc radius aggregate")}})}export{re as deriveArcSectors,H as deriveMeasuredArcValues,X as deriveProportionalArcRadiusValues,C as normalizeArcInnerRadius,V as readArcThetaValues,N as readArcThetaWeights,U as resolveArcInnerRadius};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { isLinearGradientPaint, validateFillPaint } from "./paint.js";
|
|
2
|
+
|
|
3
|
+
// Area fill shares the concrete paint grammar; outlines remain solid colors.
|
|
4
|
+
export function validateAreaPaintConfig(config) {
|
|
5
|
+
if (config.fill !== undefined) validateFillPaint(config.fill, "Area fill");
|
|
6
|
+
if (config.strokeFromFill === true && isLinearGradientPaint(config.fill)) {
|
|
7
|
+
throw new Error("Area stroke-from-fill requires a solid fill; specify a solid stroke for a gradient area.");
|
|
8
|
+
}
|
|
9
|
+
return config;
|
|
10
|
+
}
|