ggaction 0.0.9 → 0.0.11
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 +84 -0
- package/README.md +5 -4
- package/knowledge/action-card.schema.json +1 -0
- package/knowledge/action-cards.json +1 -10941
- package/knowledge/action-cards.schema.json +1 -0
- package/knowledge/extension-authoring.md +248 -0
- package/knowledge/intent-taxonomy.json +1 -183
- package/knowledge/intent-taxonomy.schema.json +1 -0
- package/knowledge/mcp-resources.json +1 -95
- package/knowledge/mcp-resources.schema.json +1 -0
- package/knowledge/task-packet.schema.json +1 -159
- package/knowledge/task-resolver.js +1 -1230
- package/package.json +19 -2
- package/src/actions/boxPlots/edit.js +45 -298
- package/src/actions/boxPlots/options.js +46 -77
- package/src/actions/boxPlots/resolve.js +1 -1
- package/src/actions/charts/bar.js +3 -0
- package/src/actions/charts/heatmap.js +73 -83
- package/src/actions/charts/index.js +1 -1
- package/src/actions/charts/parallel.js +35 -1
- package/src/actions/charts/shared.js +20 -0
- package/src/actions/composition/actions.js +46 -49
- package/src/actions/data/bin2d.js +105 -2
- package/src/actions/data/box.js +17 -17
- package/src/actions/data/density.js +42 -78
- package/src/actions/data/gradientProfile.js +22 -52
- package/src/actions/data/horizon.js +16 -41
- package/src/actions/data/index.js +10 -10
- package/src/actions/data/interval.js +16 -38
- package/src/actions/data/intervalEdit.js +55 -0
- package/src/actions/data/regression.js +7 -17
- package/src/actions/data/shared.js +47 -0
- package/src/actions/data/timeUnit.js +14 -38
- package/src/actions/data/window.js +14 -38
- package/src/actions/distributions/revision.js +154 -34
- package/src/actions/encodings/angle.js +9 -10
- package/src/actions/encodings/appearance.js +17 -51
- package/src/actions/encodings/density/resolve.js +3 -1
- package/src/actions/encodings/density.js +1 -15
- package/src/actions/encodings/horizon/resolve.js +13 -3
- package/src/actions/encodings/horizon.js +8 -7
- package/src/actions/encodings/index.js +9 -9
- package/src/actions/encodings/offset.js +8 -24
- package/src/actions/encodings/parallel.js +9 -10
- package/src/actions/encodings/pathOrder.js +6 -13
- package/src/actions/encodings/position/policies/index.js +4 -4
- package/src/actions/encodings/position/policies/line.js +47 -17
- package/src/actions/encodings/position/resolve.js +24 -5
- package/src/actions/encodings/ranged.js +18 -23
- package/src/actions/encodings/remove.js +15 -25
- package/src/actions/encodings/ruleAppearance.js +9 -24
- package/src/actions/encodings/shared.js +37 -0
- package/src/actions/encodings/strokeDash.js +8 -23
- package/src/actions/encodings/text.js +9 -10
- package/src/actions/errorBands/create.js +105 -39
- package/src/actions/errorBands/edit.js +89 -93
- package/src/actions/errorBands/index.js +1 -2
- package/src/actions/errorBars/create.js +78 -27
- package/src/actions/errorBars/edit.js +81 -48
- package/src/actions/errorBars/index.js +1 -2
- package/src/actions/facets/actions.js +45 -69
- package/src/actions/facets/derive.js +38 -53
- package/src/actions/facets/replay.js +0 -1
- package/src/actions/gradientPlots/components.js +59 -48
- package/src/actions/gradientPlots/edit.js +35 -216
- package/src/actions/gradientPlots/index.js +11 -10
- package/src/actions/gradientPlots/materialize.js +21 -35
- package/src/actions/gradientPlots/options.js +5 -7
- package/src/actions/gradientPlots/paint.js +9 -6
- package/src/actions/gradientPlots/resolve.js +1 -1
- package/src/actions/guides/axes/index.js +3 -3
- package/src/actions/guides/axes/labels.js +68 -26
- package/src/actions/guides/axes/lines.js +22 -2
- package/src/actions/guides/axes/policy.js +28 -8
- package/src/actions/guides/axes/ticks.js +27 -9
- package/src/actions/guides/axes/titles.js +113 -65
- package/src/actions/guides/grids/resolve.js +14 -0
- package/src/actions/guides/guides.js +2 -4
- package/src/actions/guides/index.js +2 -2
- package/src/actions/guides/legends/categorical/actions.js +44 -68
- package/src/actions/guides/legends/categorical/components.js +6 -1
- package/src/actions/guides/legends/categorical/index.js +2 -2
- package/src/actions/guides/legends/categorical/layout.js +148 -175
- package/src/actions/guides/legends/categorical/recipes.js +5 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -1
- package/src/actions/guides/legends/continuous/common.js +117 -66
- package/src/actions/guides/legends/continuous/gradient.js +38 -37
- package/src/actions/guides/legends/continuous/interval.js +97 -76
- package/src/actions/guides/legends/continuous/opacity.js +28 -38
- package/src/actions/guides/legends/edit.js +14 -1
- package/src/actions/guides/legends/index.js +1 -1
- package/src/actions/guides/legends/lane.js +2 -1
- package/src/actions/guides/legends/remove.js +8 -0
- package/src/actions/guides/legends/size.js +36 -43
- package/src/actions/guides/legends/strokeWidth.js +20 -26
- package/src/actions/guides/polar/axes/labels.js +17 -1
- package/src/actions/guides/polar/axes/shared.js +7 -1
- package/src/actions/guides/polar/axes/titles.js +19 -2
- package/src/actions/guides/polar/grids.js +4 -0
- package/src/actions/guides/polar/resolve.js +6 -0
- package/src/actions/index.js +13 -13
- package/src/actions/intervals/resolve.js +87 -117
- package/src/actions/marks/arc/actions.js +31 -36
- package/src/actions/marks/area/actions.js +23 -31
- package/src/actions/marks/index.js +6 -6
- package/src/actions/marks/line/actions.js +33 -52
- package/src/actions/marks/line/materialize.js +7 -5
- package/src/actions/marks/point/materialize.js +5 -3
- package/src/actions/marks/remove.js +21 -0
- package/src/actions/marks/rule/actions.js +53 -53
- package/src/actions/marks/shared.js +8 -4
- package/src/actions/marks/text/actions.js +14 -6
- package/src/actions/marks/text/layout.js +21 -45
- package/src/actions/marks/tick/actions.js +22 -21
- package/src/actions/primitives/createGraphics.js +4 -0
- package/src/actions/primitives/editGraphics.js +4 -0
- package/src/actions/primitives/index.js +3 -3
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +2 -2
- package/src/actions/primitives/semanticValidation/layer.js +1 -1
- package/src/actions/regression/edit.js +3 -43
- package/src/actions/scales/consumers/families.js +24 -4
- package/src/actions/scales/consumers/index.js +1 -1
- package/src/actions/scales/consumers/seriesLayout.js +17 -0
- package/src/actions/scales/definitions.js +100 -103
- package/src/actions/scales/materialize.js +5 -0
- package/src/actions/selection/actions.js +79 -121
- package/src/actions/titles/actions.js +48 -94
- package/src/actions/titles/resolve.js +14 -27
- package/src/actions/violinPlots/create.js +37 -44
- package/src/core/action.js +20 -5
- package/src/core/extensionRegistry.js +104 -0
- package/src/core/textMetrics.js +74 -37
- package/src/core/validation.js +18 -0
- package/src/extension.js +8 -1
- package/src/grammar/aggregate.js +48 -28
- package/src/grammar/arcs.js +58 -11
- package/src/grammar/areaSeries.js +14 -16
- package/src/grammar/bin2d.js +37 -23
- package/src/grammar/boxPlot.js +14 -13
- package/src/grammar/categoricalDensity.js +14 -14
- package/src/grammar/categoryOrder.js +8 -14
- package/src/grammar/curveCommands.js +87 -20
- package/src/grammar/density.js +162 -141
- package/src/grammar/facets/index.js +14 -0
- package/src/grammar/facets/scales.js +7 -4
- package/src/grammar/gradientProfile.js +12 -15
- package/src/grammar/histogram.js +145 -83
- package/src/grammar/horizon.js +60 -23
- package/src/grammar/interval.js +14 -13
- package/src/grammar/lineSeries.js +91 -61
- package/src/grammar/numeric.js +290 -0
- package/src/grammar/paint.js +3 -4
- package/src/grammar/palettes.js +21 -32
- package/src/grammar/pathCommands.js +6 -0
- package/src/grammar/polarPaths.js +5 -5
- package/src/grammar/regression/derive.js +48 -9
- package/src/grammar/regression/models.js +248 -69
- package/src/grammar/regression/parameters.js +8 -0
- package/src/grammar/scales/color.js +35 -26
- package/src/grammar/scales/continuous.js +71 -24
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/discretized.js +42 -12
- package/src/grammar/scales/fields.js +21 -4
- package/src/grammar/scales/ordinal.js +117 -61
- package/src/grammar/scales/temporal.js +40 -11
- package/src/grammar/scales/transformed.js +199 -52
- package/src/grammar/schemas/graphicBounds.js +270 -79
- package/src/grammar/seriesLayout.js +50 -18
- package/src/grammar/ticks.js +123 -53
- package/src/grammar/timeUnit.js +18 -8
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/window.js +70 -57
- package/src/index.js +1 -1
- package/src/layout/canvas.js +4 -8
- package/src/layout/composition.js +39 -40
- package/src/layout/facets.js +10 -14
- package/src/layout/labels.js +91 -15
- package/src/layout/legend.js +22 -20
- package/src/layout/legendLane.js +54 -40
- package/src/layout/title.js +7 -7
- package/src/materialization/bars/aggregate.js +14 -40
- package/src/materialization/bars/grouped.js +7 -18
- package/src/materialization/bars/histogram.js +2 -19
- package/src/materialization/bars/ranged.js +20 -15
- package/src/materialization/bars/resolve.js +21 -0
- package/src/materialization/facetGuides/legacyCategorical.js +2 -1
- package/src/materialization/facetGuides/placement.js +34 -19
- package/src/materialization/facetGuides/preparation.js +9 -5
- package/src/materialization/facets.js +39 -27
- package/src/materialization/guides/resources.js +2 -1
- package/src/materialization/marks/capabilities.js +5 -2
- package/src/materialization/marks/index.js +24 -4
- package/src/materialization/scales/policies/bar.js +61 -59
- package/src/materialization/scales/policies/binnedPosition.js +34 -0
- package/src/materialization/scales/policies/series.js +13 -7
- package/src/materialization/scales/resolve.js +4 -5
- package/src/materialization/selection/items/arc.js +16 -2
- package/src/materialization/selection/items/bar.js +4 -2
- package/src/materialization/selection/items/common.js +21 -1
- package/src/materialization/selection/items/path.js +19 -2
- package/src/materialization/selection/items/point.js +4 -16
- package/src/materialization/selection/items/tick.js +13 -21
- package/src/materialization/selection/point.js +11 -4
- package/src/materialization/selection/styles.js +19 -27
- package/src/materialization/text.js +4 -1
- package/src/mcp/adapter.js +8 -3
- package/src/renderers/canvas/fill.js +2 -2
- package/src/renderers/canvas/index.js +35 -2
- package/src/renderers/canvas/native.js +160 -0
- package/src/renderers/pdf.js +14 -0
- package/src/renderers/svg.js +37 -4
- package/types/extension.d.ts +19 -0
- package/types/index.d.ts +3 -0
- package/types/program.d.ts +509 -131
- package/types/svg.d.ts +1 -0
- package/src/actions/errorBands/components.js +0 -81
- package/src/actions/errorBands/options.js +0 -39
- package/src/actions/errorBands/resolve.js +0 -28
- package/src/actions/errorBars/components.js +0 -62
- package/src/actions/errorBars/options.js +0 -58
- package/src/actions/errorBars/resolve.js +0 -25
|
@@ -85,18 +85,16 @@ export function deriveCategoricalDensitySeries(rows, layer, transform) {
|
|
|
85
85
|
if (placement.split !== undefined && resolvedSplitDomain === undefined) {
|
|
86
86
|
throw new Error("Categorical density split requires a resolved two-value domain.");
|
|
87
87
|
}
|
|
88
|
+
const resolvedPlacement = {
|
|
89
|
+
...placement,
|
|
90
|
+
...(resolvedSplitDomain === undefined ? {} : { resolvedSplitDomain })
|
|
91
|
+
};
|
|
88
92
|
return cloneAndFreeze({
|
|
89
93
|
mode: placement.channel === "x" ? "category-x" : "category-y",
|
|
90
|
-
placement:
|
|
91
|
-
...placement,
|
|
92
|
-
...(resolvedSplitDomain === undefined ? {} : { resolvedSplitDomain })
|
|
93
|
-
},
|
|
94
|
+
placement: resolvedPlacement,
|
|
94
95
|
series: series.map(item => ({
|
|
95
96
|
...item,
|
|
96
|
-
side: seriesSide(
|
|
97
|
-
...placement,
|
|
98
|
-
...(resolvedSplitDomain === undefined ? {} : { resolvedSplitDomain })
|
|
99
|
-
}, item.split),
|
|
97
|
+
side: seriesSide(resolvedPlacement, item.split),
|
|
100
98
|
values: item.values.sort((left, right) => left.value - right.value)
|
|
101
99
|
}))
|
|
102
100
|
});
|
|
@@ -109,9 +107,12 @@ function maximumFor(derived, series) {
|
|
|
109
107
|
candidate.category,
|
|
110
108
|
series.category
|
|
111
109
|
));
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
let maximum = -Infinity;
|
|
111
|
+
for (const candidate of candidates) {
|
|
112
|
+
for (const value of candidate.values) {
|
|
113
|
+
maximum = Math.max(maximum, value.density);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
115
116
|
if (!Number.isFinite(maximum) || maximum <= 0) {
|
|
116
117
|
throw new Error("Categorical density width requires a positive maximum.");
|
|
117
118
|
}
|
|
@@ -163,7 +164,7 @@ export function buildCategoricalDensityPaths(
|
|
|
163
164
|
derived.series.map(series => series.category),
|
|
164
165
|
categoryScale
|
|
165
166
|
);
|
|
166
|
-
|
|
167
|
+
return cloneAndFreeze(derived.series.map((series, index) => {
|
|
167
168
|
const boundaries = pathBoundaries(
|
|
168
169
|
series,
|
|
169
170
|
centers[index],
|
|
@@ -187,6 +188,5 @@ export function buildCategoricalDensityPaths(
|
|
|
187
188
|
],
|
|
188
189
|
{ close: true }
|
|
189
190
|
);
|
|
190
|
-
});
|
|
191
|
-
return cloneAndFreeze(commands);
|
|
191
|
+
}));
|
|
192
192
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { isNominalValue, readNominalField } from "./scales/fields.js";
|
|
3
|
+
import { aggregateScalarValues } from "./aggregate.js";
|
|
3
4
|
|
|
4
|
-
const DIRECTIONS =
|
|
5
|
-
const AGGREGATES =
|
|
5
|
+
const DIRECTIONS = ["ascending", "descending"];
|
|
6
|
+
const AGGREGATES = ["sum", "mean", "min", "max"];
|
|
6
7
|
|
|
7
8
|
function nonEmptyField(value, label) {
|
|
8
9
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -11,9 +12,7 @@ function nonEmptyField(value, label) {
|
|
|
11
12
|
return value;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
return Object.is(left, right);
|
|
16
|
-
}
|
|
15
|
+
const sameValue = Object.is;
|
|
17
16
|
|
|
18
17
|
export function normalizeCategoryOrder({ values, by, direction } = {}) {
|
|
19
18
|
const hasValues = values !== undefined;
|
|
@@ -94,15 +93,10 @@ function summaryValues(rows, categoryField, summary) {
|
|
|
94
93
|
values.push(value);
|
|
95
94
|
groups.set(category, values);
|
|
96
95
|
}
|
|
97
|
-
return new Map([...groups].map(([category, values]) =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
value = values.reduce((sum, item) => sum + item, 0) / values.length;
|
|
102
|
-
} else if (summary.aggregate === "min") value = Math.min(...values);
|
|
103
|
-
else value = Math.max(...values);
|
|
104
|
-
return [category, value];
|
|
105
|
-
}));
|
|
96
|
+
return new Map([...groups].map(([category, values]) => [
|
|
97
|
+
category,
|
|
98
|
+
aggregateScalarValues(values, summary.aggregate)
|
|
99
|
+
]));
|
|
106
100
|
}
|
|
107
101
|
|
|
108
102
|
export function resolveCategoryOrder(rows, categoryField, order) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { freezeOwned, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateGeneratedItemLimit } from "../core/validation.js";
|
|
2
3
|
import { buildLinearPathCommands } from "./pathCommands.js";
|
|
3
4
|
|
|
4
5
|
export const CURVE_INTERPOLATIONS = Object.freeze([
|
|
@@ -154,11 +155,12 @@ function buildCardinalCommands(points) {
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
function monotoneTangents(points) {
|
|
158
|
+
const direction = Math.sign(points[1].x - points[0].x);
|
|
157
159
|
const intervals = points.slice(1).map((point, index) => {
|
|
158
160
|
const previous = points[index];
|
|
159
161
|
const width = point.x - previous.x;
|
|
160
|
-
if (
|
|
161
|
-
throw new Error("Monotone curve requires strictly
|
|
162
|
+
if (width === 0 || Math.sign(width) !== direction) {
|
|
163
|
+
throw new Error("Monotone curve requires strictly monotonic x values.");
|
|
162
164
|
}
|
|
163
165
|
return { width, slope: (point.y - previous.y) / width };
|
|
164
166
|
});
|
|
@@ -263,26 +265,83 @@ function buildNaturalCommands(points) {
|
|
|
263
265
|
return ownCommands(commands);
|
|
264
266
|
}
|
|
265
267
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
(SMOOTH_CURVES.has(validatedCurve) &&
|
|
271
|
-
validatedCurve !== "monotone" &&
|
|
268
|
+
function buildValidatedCurvePathCommands(points, curve) {
|
|
269
|
+
if (curve === "linear" ||
|
|
270
|
+
(SMOOTH_CURVES.has(curve) &&
|
|
271
|
+
curve !== "monotone" &&
|
|
272
272
|
points.length < 3)) {
|
|
273
|
-
return buildLinearPathCommands(
|
|
273
|
+
return buildLinearPathCommands(points);
|
|
274
274
|
}
|
|
275
|
-
if (
|
|
276
|
-
return buildStepCommands(
|
|
275
|
+
if (curve.startsWith("step")) {
|
|
276
|
+
return buildStepCommands(points, curve);
|
|
277
277
|
}
|
|
278
|
-
if (
|
|
279
|
-
if (
|
|
280
|
-
return buildCardinalCommands(
|
|
278
|
+
if (curve === "basis") return buildBasisCommands(points);
|
|
279
|
+
if (curve === "cardinal") {
|
|
280
|
+
return buildCardinalCommands(points);
|
|
281
281
|
}
|
|
282
|
-
if (
|
|
283
|
-
return buildMonotoneCommands(
|
|
282
|
+
if (curve === "monotone") {
|
|
283
|
+
return buildMonotoneCommands(points);
|
|
284
|
+
}
|
|
285
|
+
return buildNaturalCommands(points);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function finiteCommands(commands) {
|
|
289
|
+
return commands.every(command => Object.values(command).every(value =>
|
|
290
|
+
typeof value !== "number" || Number.isFinite(value)
|
|
291
|
+
));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function curveCommandCount(pointCount, curve) {
|
|
295
|
+
if (curve === "step") return pointCount * 3 - 2;
|
|
296
|
+
if (curve === "step-before" || curve === "step-after") {
|
|
297
|
+
return pointCount * 2 - 1;
|
|
284
298
|
}
|
|
285
|
-
return
|
|
299
|
+
return curve === "basis" && pointCount > 2
|
|
300
|
+
? pointCount + 2
|
|
301
|
+
: pointCount;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function buildCurvePathCommands(points, curve = "linear") {
|
|
305
|
+
const validatedPoints = validatePoints(points);
|
|
306
|
+
const validatedCurve = validateCurveInterpolation(curve);
|
|
307
|
+
validateGeneratedItemLimit(
|
|
308
|
+
curveCommandCount(validatedPoints.length, validatedCurve),
|
|
309
|
+
"Curve path command count"
|
|
310
|
+
);
|
|
311
|
+
const direct = buildValidatedCurvePathCommands(
|
|
312
|
+
validatedPoints,
|
|
313
|
+
validatedCurve
|
|
314
|
+
);
|
|
315
|
+
if (finiteCommands(direct)) return direct;
|
|
316
|
+
|
|
317
|
+
let xScale = 0;
|
|
318
|
+
let yScale = 0;
|
|
319
|
+
for (const point of validatedPoints) {
|
|
320
|
+
xScale = Math.max(xScale, Math.abs(point.x));
|
|
321
|
+
yScale = Math.max(yScale, Math.abs(point.y));
|
|
322
|
+
}
|
|
323
|
+
xScale ||= 1;
|
|
324
|
+
yScale ||= 1;
|
|
325
|
+
const normalized = buildValidatedCurvePathCommands(
|
|
326
|
+
validatedPoints.map(point => ({
|
|
327
|
+
x: point.x / xScale,
|
|
328
|
+
y: point.y / yScale
|
|
329
|
+
})),
|
|
330
|
+
validatedCurve
|
|
331
|
+
);
|
|
332
|
+
const restored = normalized.map(command => {
|
|
333
|
+
const copy = {};
|
|
334
|
+
for (const [key, value] of Object.entries(command)) {
|
|
335
|
+
copy[key] = typeof value === "number"
|
|
336
|
+
? value * (key[0] === "x" ? xScale : yScale)
|
|
337
|
+
: value;
|
|
338
|
+
}
|
|
339
|
+
return copy;
|
|
340
|
+
});
|
|
341
|
+
if (!finiteCommands(restored)) {
|
|
342
|
+
throw new RangeError("Curve interpolation exceeds the finite numeric range.");
|
|
343
|
+
}
|
|
344
|
+
return ownCommands(restored);
|
|
286
345
|
}
|
|
287
346
|
|
|
288
347
|
function reverseOpenPathCommands(commands) {
|
|
@@ -341,13 +400,21 @@ export function buildAreaCurvePathCommands(
|
|
|
341
400
|
curve = "linear",
|
|
342
401
|
{ independentAxis = "x" } = {}
|
|
343
402
|
) {
|
|
403
|
+
const validatedCurve = validateCurveInterpolation(curve);
|
|
404
|
+
const validatedLower = validatePoints(lowerPoints);
|
|
405
|
+
const validatedUpper = validatePoints(upperPoints);
|
|
406
|
+
validateGeneratedItemLimit(
|
|
407
|
+
curveCommandCount(validatedLower.length, validatedCurve) +
|
|
408
|
+
curveCommandCount(validatedUpper.length, validatedCurve) + 1,
|
|
409
|
+
"Area path command count"
|
|
410
|
+
);
|
|
344
411
|
const lower = buildOrientedCurvePathCommands(
|
|
345
|
-
|
|
346
|
-
|
|
412
|
+
validatedLower,
|
|
413
|
+
validatedCurve,
|
|
347
414
|
independentAxis
|
|
348
415
|
);
|
|
349
416
|
const upper = reverseOpenPathCommands(
|
|
350
|
-
buildOrientedCurvePathCommands(
|
|
417
|
+
buildOrientedCurvePathCommands(validatedUpper, validatedCurve, independentAxis)
|
|
351
418
|
);
|
|
352
419
|
return ownCommands([
|
|
353
420
|
...lower,
|
package/src/grammar/density.js
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
validateGeneratedItemLimit,
|
|
4
|
+
validateNonEmptyString as requireField,
|
|
5
|
+
validateWorkLimit
|
|
6
|
+
} from "../core/validation.js";
|
|
2
7
|
import { isNominalValue } from "./scales/index.js";
|
|
8
|
+
import { interpolateNumber } from "./numeric.js";
|
|
9
|
+
import {
|
|
10
|
+
requireFiniteResult,
|
|
11
|
+
stableFiniteDeviation
|
|
12
|
+
} from "./numeric.js";
|
|
3
13
|
|
|
4
14
|
const SQRT_TWO_PI = Math.sqrt(2 * Math.PI);
|
|
5
15
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
const KERNEL_FUNCTIONS = Object.freeze({
|
|
17
|
+
gaussian(value) {
|
|
18
|
+
return Math.exp(-0.5 * value ** 2) / SQRT_TWO_PI;
|
|
19
|
+
},
|
|
20
|
+
epanechnikov(value) {
|
|
21
|
+
return Math.abs(value) <= 1 ? 0.75 * (1 - value ** 2) : 0;
|
|
22
|
+
},
|
|
23
|
+
uniform(value) {
|
|
24
|
+
return Math.abs(value) <= 1 ? 0.5 : 0;
|
|
25
|
+
},
|
|
26
|
+
triangular(value) {
|
|
27
|
+
return Math.abs(value) <= 1 ? 1 - Math.abs(value) : 0;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const DENSITY_KERNELS = Object.freeze(Object.keys(KERNEL_FUNCTIONS));
|
|
12
32
|
export const DENSITY_NORMALIZATIONS = Object.freeze(["unit", "count"]);
|
|
13
33
|
export const DENSITY_PLACEMENT_SIDES = Object.freeze([
|
|
14
34
|
"both", "left", "right", "top", "bottom"
|
|
@@ -17,6 +37,13 @@ export const DENSITY_WIDTH_RESOLUTIONS = Object.freeze([
|
|
|
17
37
|
"shared", "independent"
|
|
18
38
|
]);
|
|
19
39
|
|
|
40
|
+
export function validateDensitySteps(value, label = "Density steps") {
|
|
41
|
+
if (!Number.isInteger(value) || value < 2) {
|
|
42
|
+
throw new RangeError(`${label} must be an integer of at least 2.`);
|
|
43
|
+
}
|
|
44
|
+
return validateGeneratedItemLimit(value, label);
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
export function validateDensityKernel(value) {
|
|
21
48
|
if (!DENSITY_KERNELS.includes(value)) {
|
|
22
49
|
throw new Error(`Unsupported density kernel "${value}".`);
|
|
@@ -31,9 +58,26 @@ export function validateDensityNormalization(value) {
|
|
|
31
58
|
return value;
|
|
32
59
|
}
|
|
33
60
|
|
|
34
|
-
function
|
|
35
|
-
if (
|
|
36
|
-
throw new
|
|
61
|
+
function validateDensityBandwidth(value) {
|
|
62
|
+
if (value !== "auto" && (!Number.isFinite(value) || value <= 0)) {
|
|
63
|
+
throw new RangeError(
|
|
64
|
+
"Density bandwidth must be a positive finite number or auto."
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateDensityExtent(value) {
|
|
71
|
+
if (
|
|
72
|
+
value !== "auto" &&
|
|
73
|
+
(!Array.isArray(value) ||
|
|
74
|
+
value.length !== 2 ||
|
|
75
|
+
!value.every(Number.isFinite) ||
|
|
76
|
+
value[0] >= value[1])
|
|
77
|
+
) {
|
|
78
|
+
throw new RangeError(
|
|
79
|
+
"Density extent must be an ascending pair of finite numbers or auto."
|
|
80
|
+
);
|
|
37
81
|
}
|
|
38
82
|
return value;
|
|
39
83
|
}
|
|
@@ -52,6 +96,27 @@ function validateSplitDomain(value) {
|
|
|
52
96
|
return [...value];
|
|
53
97
|
}
|
|
54
98
|
|
|
99
|
+
function validateOutputFields(value, field, groupBy, placement) {
|
|
100
|
+
if (
|
|
101
|
+
!Array.isArray(value) ||
|
|
102
|
+
value.length !== 2 ||
|
|
103
|
+
!value.every(field => typeof field === "string" && field.length > 0) ||
|
|
104
|
+
value[0] === value[1]
|
|
105
|
+
) {
|
|
106
|
+
throw new TypeError("Density as must contain two distinct non-empty fields.");
|
|
107
|
+
}
|
|
108
|
+
const collisions = new Set([
|
|
109
|
+
field,
|
|
110
|
+
groupBy,
|
|
111
|
+
placement?.categoryField,
|
|
112
|
+
placement?.split?.field
|
|
113
|
+
].filter(Boolean));
|
|
114
|
+
if (value.some(field => collisions.has(field))) {
|
|
115
|
+
throw new Error("Density output fields must not collide with source or group fields.");
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
55
120
|
function validatePlacementWidth(value = {}) {
|
|
56
121
|
if (!isPlainObject(value)) {
|
|
57
122
|
throw new TypeError("Density placement width must be a plain object.");
|
|
@@ -183,10 +248,9 @@ function validateStoredDensityPlacement(value, groupBy) {
|
|
|
183
248
|
)
|
|
184
249
|
);
|
|
185
250
|
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
normalized.side !== value.side ||
|
|
251
|
+
["type", "channel", "categoryField", "side"].some(
|
|
252
|
+
property => normalized[property] !== value[property]
|
|
253
|
+
) ||
|
|
190
254
|
value.width === undefined ||
|
|
191
255
|
normalized.width.band !== value.width.band ||
|
|
192
256
|
normalized.width.resolve !== value.width.resolve ||
|
|
@@ -218,47 +282,15 @@ export function validateDensityTransform(transform) {
|
|
|
218
282
|
}
|
|
219
283
|
validateDensityKernel(transform.kernel ?? "gaussian");
|
|
220
284
|
validateDensityNormalization(transform.normalization ?? "unit");
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
"Density bandwidth must be a positive finite number or auto."
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
if (
|
|
230
|
-
transform.extent !== "auto" &&
|
|
231
|
-
(!Array.isArray(transform.extent) ||
|
|
232
|
-
transform.extent.length !== 2 ||
|
|
233
|
-
!transform.extent.every(Number.isFinite) ||
|
|
234
|
-
transform.extent[0] >= transform.extent[1])
|
|
235
|
-
) {
|
|
236
|
-
throw new RangeError(
|
|
237
|
-
"Density extent must be an ascending pair of finite numbers or auto."
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
if (!Number.isInteger(transform.steps) || transform.steps < 2) {
|
|
241
|
-
throw new RangeError("Density steps must be an integer of at least 2.");
|
|
242
|
-
}
|
|
243
|
-
if (
|
|
244
|
-
!Array.isArray(transform.as) ||
|
|
245
|
-
transform.as.length !== 2 ||
|
|
246
|
-
!transform.as.every(value => typeof value === "string" && value.length > 0) ||
|
|
247
|
-
transform.as[0] === transform.as[1]
|
|
248
|
-
) {
|
|
249
|
-
throw new TypeError("Density as must contain two distinct non-empty fields.");
|
|
250
|
-
}
|
|
251
|
-
const collisions = new Set([
|
|
285
|
+
validateDensityBandwidth(transform.bandwidth);
|
|
286
|
+
validateDensityExtent(transform.extent);
|
|
287
|
+
validateDensitySteps(transform.steps);
|
|
288
|
+
validateOutputFields(
|
|
289
|
+
transform.as,
|
|
252
290
|
transform.field,
|
|
253
291
|
transform.groupBy,
|
|
254
|
-
transform.placement
|
|
255
|
-
|
|
256
|
-
].filter(Boolean));
|
|
257
|
-
if (transform.as.some(value => collisions.has(value))) {
|
|
258
|
-
throw new Error(
|
|
259
|
-
"Density output fields must not collide with source or group fields."
|
|
260
|
-
);
|
|
261
|
-
}
|
|
292
|
+
transform.placement
|
|
293
|
+
);
|
|
262
294
|
if (transform.resolve !== "shared") {
|
|
263
295
|
throw new Error(`Unsupported density resolve "${transform.resolve}".`);
|
|
264
296
|
}
|
|
@@ -300,17 +332,11 @@ function quantile(sortedValues, probability) {
|
|
|
300
332
|
const upper = Math.ceil(index);
|
|
301
333
|
if (lower === upper) return sortedValues[lower];
|
|
302
334
|
const ratio = index - lower;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
309
|
-
const sumSquares = values.reduce(
|
|
310
|
-
(sum, value) => sum + (value - mean) ** 2,
|
|
311
|
-
0
|
|
312
|
-
);
|
|
313
|
-
return Math.sqrt(sumSquares / (values.length - 1));
|
|
335
|
+
const ordinary = sortedValues[lower] * (1 - ratio) +
|
|
336
|
+
sortedValues[upper] * ratio;
|
|
337
|
+
return Number.isFinite(ordinary)
|
|
338
|
+
? ordinary
|
|
339
|
+
: interpolateNumber(sortedValues[lower], sortedValues[upper], ratio);
|
|
314
340
|
}
|
|
315
341
|
|
|
316
342
|
export function estimateDensityBandwidth(values) {
|
|
@@ -321,13 +347,16 @@ export function estimateDensityBandwidth(values) {
|
|
|
321
347
|
throw new Error("Density auto bandwidth requires at least two values.");
|
|
322
348
|
}
|
|
323
349
|
const sorted = [...values].sort((left, right) => left - right);
|
|
324
|
-
const deviation =
|
|
350
|
+
const deviation = stableFiniteDeviation(sorted, {
|
|
351
|
+
sample: true,
|
|
352
|
+
label: "Density sample deviation"
|
|
353
|
+
}).deviation;
|
|
325
354
|
const interquartileRange = quantile(sorted, 0.75) - quantile(sorted, 0.25);
|
|
326
355
|
const robustDeviation = interquartileRange / 1.34;
|
|
327
356
|
const spread = robustDeviation > 0
|
|
328
357
|
? Math.min(deviation, robustDeviation)
|
|
329
358
|
: deviation;
|
|
330
|
-
const bandwidth = 1.06 *
|
|
359
|
+
const bandwidth = spread * (1.06 * sorted.length ** -0.2);
|
|
331
360
|
if (!Number.isFinite(bandwidth) || bandwidth <= 0) {
|
|
332
361
|
throw new Error("Density auto bandwidth requires varying finite values.");
|
|
333
362
|
}
|
|
@@ -338,72 +367,26 @@ function resolveBandwidth(value, sourceValues) {
|
|
|
338
367
|
if (value === undefined || value === "auto") {
|
|
339
368
|
return estimateDensityBandwidth(sourceValues);
|
|
340
369
|
}
|
|
341
|
-
|
|
342
|
-
throw new RangeError(
|
|
343
|
-
"Density bandwidth must be a positive finite number or auto."
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
return value;
|
|
370
|
+
return validateDensityBandwidth(value);
|
|
347
371
|
}
|
|
348
372
|
|
|
349
373
|
function resolveExtent(value, sourceValues) {
|
|
350
374
|
if (value === undefined || value === "auto") {
|
|
351
|
-
|
|
375
|
+
let lower = Infinity;
|
|
376
|
+
let upper = -Infinity;
|
|
377
|
+
for (const sourceValue of sourceValues) {
|
|
378
|
+
lower = Math.min(lower, sourceValue);
|
|
379
|
+
upper = Math.max(upper, sourceValue);
|
|
380
|
+
}
|
|
381
|
+
const extent = [lower, upper];
|
|
352
382
|
if (extent[0] === extent[1]) {
|
|
353
383
|
throw new Error("Density observed extent requires varying finite values.");
|
|
354
384
|
}
|
|
355
385
|
return extent;
|
|
356
386
|
}
|
|
357
|
-
|
|
358
|
-
!Array.isArray(value) ||
|
|
359
|
-
value.length !== 2 ||
|
|
360
|
-
!value.every(Number.isFinite) ||
|
|
361
|
-
value[0] >= value[1]
|
|
362
|
-
) {
|
|
363
|
-
throw new RangeError(
|
|
364
|
-
"Density extent must be an ascending pair of finite numbers or auto."
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
return [...value];
|
|
387
|
+
return [...validateDensityExtent(value)];
|
|
368
388
|
}
|
|
369
389
|
|
|
370
|
-
function resolveOutputFields(as, field, groupBy, placement) {
|
|
371
|
-
const resolved = as ?? [`${field}_value`, `${field}_density`];
|
|
372
|
-
if (
|
|
373
|
-
!Array.isArray(resolved) ||
|
|
374
|
-
resolved.length !== 2 ||
|
|
375
|
-
!resolved.every(value => typeof value === "string" && value.length > 0) ||
|
|
376
|
-
resolved[0] === resolved[1]
|
|
377
|
-
) {
|
|
378
|
-
throw new TypeError("Density as must contain two distinct non-empty fields.");
|
|
379
|
-
}
|
|
380
|
-
const collisions = new Set([
|
|
381
|
-
field,
|
|
382
|
-
groupBy,
|
|
383
|
-
placement?.categoryField,
|
|
384
|
-
placement?.split?.field
|
|
385
|
-
].filter(Boolean));
|
|
386
|
-
if (resolved.some(value => collisions.has(value))) {
|
|
387
|
-
throw new Error("Density output fields must not collide with source or group fields.");
|
|
388
|
-
}
|
|
389
|
-
return [...resolved];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const KERNEL_FUNCTIONS = Object.freeze({
|
|
393
|
-
gaussian(value) {
|
|
394
|
-
return Math.exp(-0.5 * value ** 2) / SQRT_TWO_PI;
|
|
395
|
-
},
|
|
396
|
-
epanechnikov(value) {
|
|
397
|
-
return Math.abs(value) <= 1 ? 0.75 * (1 - value ** 2) : 0;
|
|
398
|
-
},
|
|
399
|
-
uniform(value) {
|
|
400
|
-
return Math.abs(value) <= 1 ? 0.5 : 0;
|
|
401
|
-
},
|
|
402
|
-
triangular(value) {
|
|
403
|
-
return Math.abs(value) <= 1 ? 1 - Math.abs(value) : 0;
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
390
|
function estimateAt(sample, values, bandwidth, kernel, normalization) {
|
|
408
391
|
const sum = values.reduce(
|
|
409
392
|
(total, value) => total + KERNEL_FUNCTIONS[kernel](
|
|
@@ -414,7 +397,14 @@ function estimateAt(sample, values, bandwidth, kernel, normalization) {
|
|
|
414
397
|
const denominator = normalization === "unit"
|
|
415
398
|
? values.length * bandwidth
|
|
416
399
|
: bandwidth;
|
|
417
|
-
|
|
400
|
+
const ordinary = sum / denominator;
|
|
401
|
+
if (Number.isFinite(denominator) && Number.isFinite(ordinary)) {
|
|
402
|
+
return ordinary;
|
|
403
|
+
}
|
|
404
|
+
const stable = normalization === "unit"
|
|
405
|
+
? (sum / values.length) / bandwidth
|
|
406
|
+
: sum / bandwidth;
|
|
407
|
+
return requireFiniteResult(stable, "Density estimate");
|
|
418
408
|
}
|
|
419
409
|
|
|
420
410
|
export function deriveKernelDensity(values, {
|
|
@@ -435,13 +425,16 @@ export function deriveKernelDensity(values, {
|
|
|
435
425
|
const groupField = groupBy === undefined
|
|
436
426
|
? undefined
|
|
437
427
|
: requireField(groupBy, "Density groupBy");
|
|
438
|
-
|
|
439
|
-
throw new RangeError("Density steps must be an integer of at least 2.");
|
|
440
|
-
}
|
|
428
|
+
validateDensitySteps(steps);
|
|
441
429
|
if (placement !== undefined) {
|
|
442
430
|
validateStoredDensityPlacement(placement, groupField);
|
|
443
431
|
}
|
|
444
|
-
const outputFields =
|
|
432
|
+
const outputFields = [...validateOutputFields(
|
|
433
|
+
as ?? [`${sourceField}_value`, `${sourceField}_density`],
|
|
434
|
+
sourceField,
|
|
435
|
+
groupField,
|
|
436
|
+
placement
|
|
437
|
+
)];
|
|
445
438
|
const resolvedKernel = validateDensityKernel(kernel);
|
|
446
439
|
const resolvedNormalization = validateDensityNormalization(normalization);
|
|
447
440
|
const validRows = values.filter(row =>
|
|
@@ -454,12 +447,23 @@ export function deriveKernelDensity(values, {
|
|
|
454
447
|
if (validRows.length === 0) {
|
|
455
448
|
throw new Error("Density requires at least one valid field/group row.");
|
|
456
449
|
}
|
|
450
|
+
validateWorkLimit(validRows.length * steps, "Density computation");
|
|
457
451
|
const sourceValues = validRows.map(row => row[sourceField]);
|
|
458
452
|
const resolvedBandwidth = resolveBandwidth(bandwidth, sourceValues);
|
|
459
453
|
const resolvedExtent = resolveExtent(extent, sourceValues);
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
454
|
+
const groupedValues = new Map();
|
|
455
|
+
for (const row of validRows) {
|
|
456
|
+
const group = groupField === undefined ? undefined : row[groupField];
|
|
457
|
+
const split = placement?.split === undefined
|
|
458
|
+
? undefined
|
|
459
|
+
: row[placement.split.field];
|
|
460
|
+
const bySplit = groupedValues.get(group) ?? new Map();
|
|
461
|
+
const groupValues = bySplit.get(split) ?? [];
|
|
462
|
+
groupValues.push(row[sourceField]);
|
|
463
|
+
bySplit.set(split, groupValues);
|
|
464
|
+
groupedValues.set(group, bySplit);
|
|
465
|
+
}
|
|
466
|
+
const groups = [...groupedValues.keys()];
|
|
463
467
|
const observedSplits = placement?.split === undefined
|
|
464
468
|
? []
|
|
465
469
|
: [...new Set(validRows.map(row => row[placement.split.field]))];
|
|
@@ -477,27 +481,44 @@ export function deriveKernelDensity(values, {
|
|
|
477
481
|
throw new Error("Density split domain must include every observed split value.");
|
|
478
482
|
}
|
|
479
483
|
}
|
|
480
|
-
const
|
|
484
|
+
const splits = splitDomain ?? [undefined];
|
|
485
|
+
let outputGroups = 0;
|
|
486
|
+
for (const group of groups) {
|
|
487
|
+
for (const split of splits) {
|
|
488
|
+
if (groupedValues.get(group).has(split)) outputGroups += 1;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
validateGeneratedItemLimit(
|
|
492
|
+
outputGroups * steps,
|
|
493
|
+
"Density generated row count"
|
|
494
|
+
);
|
|
495
|
+
const extentSpan = resolvedExtent[1] - resolvedExtent[0];
|
|
496
|
+
const sampleStep = extentSpan / (steps - 1);
|
|
481
497
|
const samples = Array.from(
|
|
482
498
|
{ length: steps },
|
|
483
499
|
(_, index) => index === steps - 1
|
|
484
500
|
? resolvedExtent[1]
|
|
485
|
-
:
|
|
486
|
-
?
|
|
487
|
-
|
|
488
|
-
|
|
501
|
+
: Number.isFinite(extentSpan)
|
|
502
|
+
? placement === undefined
|
|
503
|
+
? resolvedExtent[0] + sampleStep * index
|
|
504
|
+
: resolvedExtent[0] + extentSpan * index / (steps - 1)
|
|
505
|
+
: interpolateNumber(
|
|
506
|
+
resolvedExtent[0],
|
|
507
|
+
resolvedExtent[1],
|
|
508
|
+
index / (steps - 1)
|
|
509
|
+
)
|
|
489
510
|
);
|
|
511
|
+
if (samples.some((sample, index) =>
|
|
512
|
+
!Number.isFinite(sample) ||
|
|
513
|
+
(index > 0 && sample <= samples[index - 1]))) {
|
|
514
|
+
throw new RangeError(
|
|
515
|
+
"Density extent cannot represent the requested finite sample grid."
|
|
516
|
+
);
|
|
517
|
+
}
|
|
490
518
|
const rows = [];
|
|
491
519
|
for (const group of groups) {
|
|
492
|
-
const splits = splitDomain ?? [undefined];
|
|
493
520
|
for (const split of splits) {
|
|
494
|
-
const groupValues =
|
|
495
|
-
.filter(row =>
|
|
496
|
-
(groupField === undefined || Object.is(row[groupField], group)) &&
|
|
497
|
-
(placement?.split === undefined ||
|
|
498
|
-
Object.is(row[placement.split.field], split))
|
|
499
|
-
)
|
|
500
|
-
.map(row => row[sourceField]);
|
|
521
|
+
const groupValues = groupedValues.get(group).get(split) ?? [];
|
|
501
522
|
if (groupValues.length === 0) continue;
|
|
502
523
|
for (const sample of samples) {
|
|
503
524
|
rows.push({
|