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/grammar/density.js
CHANGED
|
@@ -10,6 +10,15 @@ import {
|
|
|
10
10
|
requireFiniteResult,
|
|
11
11
|
stableFiniteDeviation
|
|
12
12
|
} from "./numeric.js";
|
|
13
|
+
import {
|
|
14
|
+
normalizeStatisticalWeight,
|
|
15
|
+
readStatisticalWeights,
|
|
16
|
+
summarizeStatisticalWeights,
|
|
17
|
+
validateWeightedNumericFields,
|
|
18
|
+
weightedBandwidth,
|
|
19
|
+
weightedEntriesExtent,
|
|
20
|
+
weightedKernelEstimate
|
|
21
|
+
} from "./weightedStatistics.js";
|
|
13
22
|
|
|
14
23
|
const SQRT_TWO_PI = Math.sqrt(2 * Math.PI);
|
|
15
24
|
|
|
@@ -37,6 +46,25 @@ export const DENSITY_WIDTH_RESOLUTIONS = Object.freeze([
|
|
|
37
46
|
"shared", "independent"
|
|
38
47
|
]);
|
|
39
48
|
|
|
49
|
+
export function normalizeDensityTransform(args = {}) {
|
|
50
|
+
const transform = {
|
|
51
|
+
type: "density",
|
|
52
|
+
field: args.field,
|
|
53
|
+
...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
|
|
54
|
+
bandwidth: args.bandwidth ?? "auto",
|
|
55
|
+
extent: args.extent ?? "auto",
|
|
56
|
+
steps: args.steps ?? 100,
|
|
57
|
+
kernel: args.kernel ?? "gaussian",
|
|
58
|
+
normalization: args.normalization ?? "unit",
|
|
59
|
+
as: args.as ?? [`${args.field}_value`, `${args.field}_density`],
|
|
60
|
+
resolve: args.resolve ?? "shared",
|
|
61
|
+
...(args.weight === undefined ? {} : { weight: args.weight }),
|
|
62
|
+
...(args.placement === undefined ? {} : { placement: args.placement })
|
|
63
|
+
};
|
|
64
|
+
validateDensityTransform(transform);
|
|
65
|
+
return cloneAndFreeze(transform);
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
export function validateDensitySteps(value, label = "Density steps") {
|
|
41
69
|
if (!Number.isInteger(value) || value < 2) {
|
|
42
70
|
throw new RangeError(`${label} must be an integer of at least 2.`);
|
|
@@ -261,10 +289,88 @@ function validateStoredDensityPlacement(value, groupBy) {
|
|
|
261
289
|
return value;
|
|
262
290
|
}
|
|
263
291
|
|
|
292
|
+
function validateResolvedBandwidths(value, transform) {
|
|
293
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
294
|
+
throw new TypeError(
|
|
295
|
+
"Density resolved bandwidths must be a non-empty array."
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const normalized = value.map((item, index) => {
|
|
299
|
+
if (!isPlainObject(item)) {
|
|
300
|
+
throw new TypeError(
|
|
301
|
+
`Density resolved bandwidth ${index} must be a plain object.`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const unknown = Object.keys(item).find(
|
|
305
|
+
key => !["group", "split", "bandwidth"].includes(key)
|
|
306
|
+
);
|
|
307
|
+
if (unknown !== undefined) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Unknown density resolved bandwidth property "${unknown}".`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (!Number.isFinite(item.bandwidth) || item.bandwidth <= 0) {
|
|
313
|
+
throw new RangeError(
|
|
314
|
+
`Density resolved bandwidth ${index} must be positive and finite.`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (transform.groupBy === undefined) {
|
|
318
|
+
if (Object.hasOwn(item, "group")) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
"Density resolved bandwidth cannot store a group without groupBy."
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
} else if (!Object.hasOwn(item, "group") || !isNominalValue(item.group)) {
|
|
324
|
+
throw new TypeError(
|
|
325
|
+
`Density resolved bandwidth ${index} requires a nominal group.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (transform.placement?.split === undefined) {
|
|
329
|
+
if (Object.hasOwn(item, "split")) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
"Density resolved bandwidth cannot store a split without split placement."
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
} else if (!Object.hasOwn(item, "split") || !isNominalValue(item.split)) {
|
|
335
|
+
throw new TypeError(
|
|
336
|
+
`Density resolved bandwidth ${index} requires a nominal split.`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return item;
|
|
340
|
+
});
|
|
341
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
342
|
+
const duplicate = normalized.slice(0, index).some(item =>
|
|
343
|
+
Object.is(item.group, normalized[index].group) &&
|
|
344
|
+
Object.is(item.split, normalized[index].split)
|
|
345
|
+
);
|
|
346
|
+
if (duplicate) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Density resolved bandwidth ${index} duplicates an earlier profile.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function hasValidResolvedBandwidthState(resolved, transform) {
|
|
356
|
+
const hasBandwidth = Object.hasOwn(resolved, "bandwidth");
|
|
357
|
+
const hasBandwidths = Object.hasOwn(resolved, "bandwidths");
|
|
358
|
+
if (hasBandwidth === hasBandwidths) return false;
|
|
359
|
+
if (hasBandwidth) {
|
|
360
|
+
return Number.isFinite(resolved.bandwidth) && resolved.bandwidth > 0;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
validateResolvedBandwidths(resolved.bandwidths, transform);
|
|
364
|
+
return true;
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
264
370
|
export function validateDensityTransform(transform) {
|
|
265
371
|
const supported = [
|
|
266
372
|
"type", "field", "groupBy", "bandwidth", "extent", "steps", "as",
|
|
267
|
-
"resolve", "kernel", "normalization", "placement", "resolved"
|
|
373
|
+
"resolve", "kernel", "normalization", "placement", "weight", "resolved"
|
|
268
374
|
];
|
|
269
375
|
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
270
376
|
if (unknown !== undefined) {
|
|
@@ -274,6 +380,9 @@ export function validateDensityTransform(transform) {
|
|
|
274
380
|
throw new Error(`Unsupported density transform "${transform.type}".`);
|
|
275
381
|
}
|
|
276
382
|
requireField(transform.field, "Density field");
|
|
383
|
+
if (transform.weight !== undefined) {
|
|
384
|
+
normalizeStatisticalWeight(transform.weight, "Density weight");
|
|
385
|
+
}
|
|
277
386
|
if (transform.groupBy !== undefined) {
|
|
278
387
|
requireField(transform.groupBy, "Density groupBy");
|
|
279
388
|
}
|
|
@@ -301,10 +410,9 @@ export function validateDensityTransform(transform) {
|
|
|
301
410
|
typeof resolved !== "object" ||
|
|
302
411
|
Array.isArray(resolved) ||
|
|
303
412
|
Object.keys(resolved).some(
|
|
304
|
-
key => !["bandwidth", "extent", "splitDomain"].includes(key)
|
|
413
|
+
key => !["bandwidth", "bandwidths", "extent", "splitDomain"].includes(key)
|
|
305
414
|
) ||
|
|
306
|
-
!
|
|
307
|
-
resolved.bandwidth <= 0 ||
|
|
415
|
+
!hasValidResolvedBandwidthState(resolved, transform) ||
|
|
308
416
|
!Array.isArray(resolved.extent) ||
|
|
309
417
|
resolved.extent.length !== 2 ||
|
|
310
418
|
!resolved.extent.every(Number.isFinite) ||
|
|
@@ -319,7 +427,7 @@ export function validateDensityTransform(transform) {
|
|
|
319
427
|
})())
|
|
320
428
|
) {
|
|
321
429
|
throw new TypeError(
|
|
322
|
-
"Density resolved provenance requires
|
|
430
|
+
"Density resolved provenance requires one positive bandwidth or profile bandwidth list, an ascending finite extent, and an optional two-value split domain."
|
|
323
431
|
);
|
|
324
432
|
}
|
|
325
433
|
}
|
|
@@ -363,22 +471,26 @@ export function estimateDensityBandwidth(values) {
|
|
|
363
471
|
return bandwidth;
|
|
364
472
|
}
|
|
365
473
|
|
|
366
|
-
function resolveBandwidth(value, sourceValues) {
|
|
474
|
+
function resolveBandwidth(value, sourceValues, weightSummary, field) {
|
|
367
475
|
if (value === undefined || value === "auto") {
|
|
368
|
-
return
|
|
476
|
+
return weightSummary === undefined
|
|
477
|
+
? estimateDensityBandwidth(sourceValues)
|
|
478
|
+
: weightedBandwidth(weightSummary, field);
|
|
369
479
|
}
|
|
370
480
|
return validateDensityBandwidth(value);
|
|
371
481
|
}
|
|
372
482
|
|
|
373
|
-
function resolveExtent(value, sourceValues) {
|
|
483
|
+
function resolveExtent(value, sourceValues, weightSummary, field) {
|
|
374
484
|
if (value === undefined || value === "auto") {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
485
|
+
const extent = weightSummary === undefined
|
|
486
|
+
? sourceValues.reduce(
|
|
487
|
+
([lower, upper], sourceValue) => [
|
|
488
|
+
Math.min(lower, sourceValue),
|
|
489
|
+
Math.max(upper, sourceValue)
|
|
490
|
+
],
|
|
491
|
+
[Infinity, -Infinity]
|
|
492
|
+
)
|
|
493
|
+
: weightedEntriesExtent(weightSummary, field);
|
|
382
494
|
if (extent[0] === extent[1]) {
|
|
383
495
|
throw new Error("Density observed extent requires varying finite values.");
|
|
384
496
|
}
|
|
@@ -407,6 +519,17 @@ function estimateAt(sample, values, bandwidth, kernel, normalization) {
|
|
|
407
519
|
return requireFiniteResult(stable, "Density estimate");
|
|
408
520
|
}
|
|
409
521
|
|
|
522
|
+
function estimateWeightedAt(sample, summary, field, bandwidth, kernel, normalization) {
|
|
523
|
+
return weightedKernelEstimate(
|
|
524
|
+
summary,
|
|
525
|
+
field,
|
|
526
|
+
sample,
|
|
527
|
+
bandwidth,
|
|
528
|
+
KERNEL_FUNCTIONS[kernel],
|
|
529
|
+
normalization
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
410
533
|
export function deriveKernelDensity(values, {
|
|
411
534
|
field,
|
|
412
535
|
groupBy,
|
|
@@ -416,7 +539,8 @@ export function deriveKernelDensity(values, {
|
|
|
416
539
|
kernel = "gaussian",
|
|
417
540
|
normalization = "unit",
|
|
418
541
|
as,
|
|
419
|
-
placement
|
|
542
|
+
placement,
|
|
543
|
+
weight
|
|
420
544
|
} = {}) {
|
|
421
545
|
if (!Array.isArray(values)) {
|
|
422
546
|
throw new TypeError("Density values must be an array.");
|
|
@@ -437,29 +561,66 @@ export function deriveKernelDensity(values, {
|
|
|
437
561
|
)];
|
|
438
562
|
const resolvedKernel = validateDensityKernel(kernel);
|
|
439
563
|
const resolvedNormalization = validateDensityNormalization(normalization);
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
(
|
|
445
|
-
|
|
446
|
-
|
|
564
|
+
const statisticalWeights = weight === undefined
|
|
565
|
+
? undefined
|
|
566
|
+
: readStatisticalWeights(values, weight, "Density");
|
|
567
|
+
if (statisticalWeights !== undefined) {
|
|
568
|
+
validateWeightedNumericFields(
|
|
569
|
+
statisticalWeights.entries,
|
|
570
|
+
[sourceField],
|
|
571
|
+
"Density"
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const validEntries = statisticalWeights === undefined
|
|
575
|
+
? undefined
|
|
576
|
+
: statisticalWeights.entries.filter(entry =>
|
|
577
|
+
(groupField === undefined || isNominalValue(entry.row[groupField])) &&
|
|
578
|
+
(placement?.split === undefined ||
|
|
579
|
+
isNominalValue(entry.row[placement.split.field]))
|
|
580
|
+
);
|
|
581
|
+
const validRows = statisticalWeights === undefined
|
|
582
|
+
? values.filter(row =>
|
|
583
|
+
row !== null &&
|
|
584
|
+
typeof row === "object" &&
|
|
585
|
+
Number.isFinite(row[sourceField]) &&
|
|
586
|
+
(groupField === undefined || isNominalValue(row[groupField])) &&
|
|
587
|
+
(placement?.split === undefined || isNominalValue(row[placement.split.field]))
|
|
588
|
+
)
|
|
589
|
+
: validEntries.map(entry => entry.row);
|
|
447
590
|
if (validRows.length === 0) {
|
|
448
591
|
throw new Error("Density requires at least one valid field/group row.");
|
|
449
592
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
593
|
+
const globalWeightSummary = statisticalWeights === undefined
|
|
594
|
+
? undefined
|
|
595
|
+
: summarizeStatisticalWeights(
|
|
596
|
+
validEntries,
|
|
597
|
+
statisticalWeights.definition.kind,
|
|
598
|
+
"Density data"
|
|
599
|
+
);
|
|
600
|
+
validateWorkLimit(
|
|
601
|
+
(globalWeightSummary?.positive.length ?? validRows.length) * steps,
|
|
602
|
+
"Density computation"
|
|
603
|
+
);
|
|
604
|
+
const sourceValues = globalWeightSummary === undefined
|
|
605
|
+
? validRows.map(row => row[sourceField])
|
|
606
|
+
: globalWeightSummary.positive.map(entry => entry.row[sourceField]);
|
|
607
|
+
const resolvedExtent = resolveExtent(
|
|
608
|
+
extent,
|
|
609
|
+
sourceValues,
|
|
610
|
+
globalWeightSummary,
|
|
611
|
+
sourceField
|
|
612
|
+
);
|
|
454
613
|
const groupedValues = new Map();
|
|
455
|
-
|
|
614
|
+
const groupedSource = statisticalWeights === undefined ? validRows : validEntries;
|
|
615
|
+
for (const source of groupedSource) {
|
|
616
|
+
const row = statisticalWeights === undefined ? source : source.row;
|
|
456
617
|
const group = groupField === undefined ? undefined : row[groupField];
|
|
457
618
|
const split = placement?.split === undefined
|
|
458
619
|
? undefined
|
|
459
620
|
: row[placement.split.field];
|
|
460
621
|
const bySplit = groupedValues.get(group) ?? new Map();
|
|
461
622
|
const groupValues = bySplit.get(split) ?? [];
|
|
462
|
-
groupValues.push(row[sourceField]);
|
|
623
|
+
groupValues.push(statisticalWeights === undefined ? row[sourceField] : source);
|
|
463
624
|
bySplit.set(split, groupValues);
|
|
464
625
|
groupedValues.set(group, bySplit);
|
|
465
626
|
}
|
|
@@ -492,6 +653,16 @@ export function deriveKernelDensity(values, {
|
|
|
492
653
|
outputGroups * steps,
|
|
493
654
|
"Density generated row count"
|
|
494
655
|
);
|
|
656
|
+
const profileBandwidths = statisticalWeights !== undefined &&
|
|
657
|
+
bandwidth === "auto" && outputGroups > 1;
|
|
658
|
+
const resolvedBandwidth = profileBandwidths
|
|
659
|
+
? undefined
|
|
660
|
+
: resolveBandwidth(
|
|
661
|
+
bandwidth,
|
|
662
|
+
sourceValues,
|
|
663
|
+
globalWeightSummary,
|
|
664
|
+
sourceField
|
|
665
|
+
);
|
|
495
666
|
const extentSpan = resolvedExtent[1] - resolvedExtent[0];
|
|
496
667
|
const sampleStep = extentSpan / (steps - 1);
|
|
497
668
|
const samples = Array.from(
|
|
@@ -516,10 +687,30 @@ export function deriveKernelDensity(values, {
|
|
|
516
687
|
);
|
|
517
688
|
}
|
|
518
689
|
const rows = [];
|
|
690
|
+
const resolvedBandwidths = [];
|
|
519
691
|
for (const group of groups) {
|
|
520
692
|
for (const split of splits) {
|
|
521
693
|
const groupValues = groupedValues.get(group).get(split) ?? [];
|
|
522
694
|
if (groupValues.length === 0) continue;
|
|
695
|
+
const groupWeightSummary = statisticalWeights === undefined
|
|
696
|
+
? undefined
|
|
697
|
+
: summarizeStatisticalWeights(
|
|
698
|
+
groupValues,
|
|
699
|
+
statisticalWeights.definition.kind,
|
|
700
|
+
"Density group"
|
|
701
|
+
);
|
|
702
|
+
const groupBandwidth = resolvedBandwidth ?? weightedBandwidth(
|
|
703
|
+
groupWeightSummary,
|
|
704
|
+
sourceField,
|
|
705
|
+
"Density group auto bandwidth"
|
|
706
|
+
);
|
|
707
|
+
if (profileBandwidths) {
|
|
708
|
+
resolvedBandwidths.push({
|
|
709
|
+
...(groupField === undefined ? {} : { group }),
|
|
710
|
+
...(placement?.split === undefined ? {} : { split }),
|
|
711
|
+
bandwidth: groupBandwidth
|
|
712
|
+
});
|
|
713
|
+
}
|
|
523
714
|
for (const sample of samples) {
|
|
524
715
|
rows.push({
|
|
525
716
|
...(groupField === undefined
|
|
@@ -531,13 +722,22 @@ export function deriveKernelDensity(values, {
|
|
|
531
722
|
? {}
|
|
532
723
|
: { [placement.split.field]: split }),
|
|
533
724
|
[outputFields[0]]: sample,
|
|
534
|
-
[outputFields[1]]:
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
725
|
+
[outputFields[1]]: groupWeightSummary === undefined
|
|
726
|
+
? estimateAt(
|
|
727
|
+
sample,
|
|
728
|
+
groupValues,
|
|
729
|
+
groupBandwidth,
|
|
730
|
+
resolvedKernel,
|
|
731
|
+
resolvedNormalization
|
|
732
|
+
)
|
|
733
|
+
: estimateWeightedAt(
|
|
734
|
+
sample,
|
|
735
|
+
groupWeightSummary,
|
|
736
|
+
sourceField,
|
|
737
|
+
groupBandwidth,
|
|
738
|
+
resolvedKernel,
|
|
739
|
+
resolvedNormalization
|
|
740
|
+
)
|
|
541
741
|
});
|
|
542
742
|
}
|
|
543
743
|
}
|
|
@@ -551,7 +751,9 @@ export function deriveKernelDensity(values, {
|
|
|
551
751
|
},
|
|
552
752
|
groups,
|
|
553
753
|
...(splitDomain === undefined ? {} : { splitDomain }),
|
|
554
|
-
|
|
754
|
+
...(resolvedBandwidth === undefined
|
|
755
|
+
? { bandwidths: resolvedBandwidths }
|
|
756
|
+
: { bandwidth: resolvedBandwidth }),
|
|
555
757
|
kernel: resolvedKernel,
|
|
556
758
|
normalization: resolvedNormalization,
|
|
557
759
|
extent: resolvedExtent,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const ENTRY_KEYS = Object.freeze(["value", "label"]);
|
|
4
|
+
|
|
5
|
+
export function sameDisplayLabelValue(left, right) {
|
|
6
|
+
return left === right;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isDisplayLabelValue(value) {
|
|
10
|
+
return value === null || typeof value === "string" ||
|
|
11
|
+
typeof value === "boolean" ||
|
|
12
|
+
(typeof value === "number" && Number.isFinite(value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeDisplayLabelMap(value, label = "labelMap") {
|
|
16
|
+
if (!Array.isArray(value)) {
|
|
17
|
+
throw new TypeError(`${label} must be an array.`);
|
|
18
|
+
}
|
|
19
|
+
const normalized = [];
|
|
20
|
+
for (const [index, entry] of value.entries()) {
|
|
21
|
+
if (!isPlainObject(entry)) {
|
|
22
|
+
throw new TypeError(`${label}[${index}] must be a plain object.`);
|
|
23
|
+
}
|
|
24
|
+
const keys = Reflect.ownKeys(entry);
|
|
25
|
+
if (keys.length !== ENTRY_KEYS.length ||
|
|
26
|
+
ENTRY_KEYS.some(key => !Object.hasOwn(entry, key))) {
|
|
27
|
+
throw new Error(`${label}[${index}] must contain exactly value and label.`);
|
|
28
|
+
}
|
|
29
|
+
if (!isDisplayLabelValue(entry.value)) {
|
|
30
|
+
throw new TypeError(
|
|
31
|
+
`${label}[${index}].value must be a string, boolean, null, or finite number.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (typeof entry.label !== "string") {
|
|
35
|
+
throw new TypeError(`${label}[${index}].label must be a string.`);
|
|
36
|
+
}
|
|
37
|
+
if (normalized.some(previous =>
|
|
38
|
+
sameDisplayLabelValue(previous.value, entry.value))) {
|
|
39
|
+
throw new Error(`${label} contains a duplicate typed value.`);
|
|
40
|
+
}
|
|
41
|
+
normalized.push(Object.freeze({ value: entry.value, label: entry.label }));
|
|
42
|
+
}
|
|
43
|
+
return Object.freeze(normalized);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveDisplayLabel(value, map, fallback) {
|
|
47
|
+
if (map !== undefined) {
|
|
48
|
+
for (const entry of map) {
|
|
49
|
+
if (sameDisplayLabelValue(entry.value, value)) return entry.label;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const resolved = fallback(value);
|
|
53
|
+
if (typeof resolved !== "string") {
|
|
54
|
+
throw new TypeError("Display label fallback must return a string.");
|
|
55
|
+
}
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
@@ -1,6 +1,137 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
2
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
3
|
import { findTransformPolicy } from "../transforms.js";
|
|
4
|
+
import { BAR_GRAINS, resolveBarGrain } from "../bars/policy.js";
|
|
5
|
+
import { resolveRectMode } from "../rects.js";
|
|
6
|
+
import { resolveRuleMode } from "../rules.js";
|
|
7
|
+
|
|
8
|
+
const CARTESIAN_MARKS = new Set([
|
|
9
|
+
"point", "line", "area", "bar", "rule", "tick", "rect"
|
|
10
|
+
]);
|
|
11
|
+
const FACET_BAR_GRAINS = new Set([
|
|
12
|
+
BAR_GRAINS.histogram,
|
|
13
|
+
BAR_GRAINS.aggregate,
|
|
14
|
+
BAR_GRAINS.ranged
|
|
15
|
+
]);
|
|
16
|
+
const ORDINARY_SCALE_CHANNELS = Object.freeze([
|
|
17
|
+
["x", "x"], ["y", "y"], ["xOffset", "xOffset"], ["yOffset", "yOffset"],
|
|
18
|
+
["theta", "theta"], ["r", "radius"], ["color", "color"],
|
|
19
|
+
["stroke", "stroke"], ["size", "size"], ["shape", "shape"],
|
|
20
|
+
["opacity", "opacity"], ["strokeDash", "strokeDash"]
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function coordinateType(semanticSpec, layer) {
|
|
24
|
+
if (layer.coordinate === undefined) return undefined;
|
|
25
|
+
return semanticSpec.coordinates?.find(value => value.id === layer.coordinate)?.type;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function classifyPrimaryLayer(semanticSpec, layer) {
|
|
29
|
+
const type = layer.mark?.type;
|
|
30
|
+
const parallel = layer.encoding?.parallel;
|
|
31
|
+
if (parallel !== undefined || coordinateType(semanticSpec, layer) === "parallel") {
|
|
32
|
+
const complete = type === "line" && Array.isArray(parallel?.dimensions) &&
|
|
33
|
+
parallel.dimensions.length >= 2 &&
|
|
34
|
+
parallel.dimensions.every(dimension =>
|
|
35
|
+
typeof dimension?.scale === "string" && dimension.scale.length > 0
|
|
36
|
+
);
|
|
37
|
+
if (!complete) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Facet layer "${layer.id}" must be a complete materializable Parallel mark.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return "parallel";
|
|
43
|
+
}
|
|
44
|
+
const polar = layer.encoding?.theta !== undefined ||
|
|
45
|
+
layer.encoding?.radius !== undefined || coordinateType(semanticSpec, layer) === "polar";
|
|
46
|
+
if (polar) {
|
|
47
|
+
const theta = layer.encoding?.theta?.scale !== undefined;
|
|
48
|
+
const radius = layer.encoding?.radius?.scale !== undefined;
|
|
49
|
+
const complete = (type === "point" || type === "line")
|
|
50
|
+
? theta && radius
|
|
51
|
+
: type === "arc" && theta;
|
|
52
|
+
if (!complete) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Facet layer "${layer.id}" must be a complete materializable Polar mark.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return "polar";
|
|
58
|
+
}
|
|
59
|
+
if (!CARTESIAN_MARKS.has(type)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`facet does not support mark "${layer.id}" of type ${type ?? "incomplete"}.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (type === "bar" && !FACET_BAR_GRAINS.has(resolveBarGrain(layer))) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const complete = (
|
|
70
|
+
layer.encoding?.x?.scale !== undefined &&
|
|
71
|
+
layer.encoding?.y?.scale !== undefined
|
|
72
|
+
) || (type === "rule" && resolveRuleMode(layer) !== undefined) ||
|
|
73
|
+
(type === "rect" && resolveRectMode(layer) !== undefined);
|
|
74
|
+
if (!complete) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Facet layer "${layer.id}" must be a complete materializable Cartesian mark.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return "cartesian";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function collectFacetScaleBindings(semanticSpec) {
|
|
83
|
+
if (!isPlainObject(semanticSpec) || !Array.isArray(semanticSpec.layers)) {
|
|
84
|
+
throw new TypeError("Facet scale bindings require a semantic spec with layers.");
|
|
85
|
+
}
|
|
86
|
+
const bindings = [];
|
|
87
|
+
for (const layer of semanticSpec.layers) {
|
|
88
|
+
for (const [policyKey, semanticChannel] of ORDINARY_SCALE_CHANNELS) {
|
|
89
|
+
const scaleId = layer.encoding?.[semanticChannel]?.scale;
|
|
90
|
+
if (scaleId === undefined) continue;
|
|
91
|
+
bindings.push({ layerId: layer.id, scaleId, policyKey, semanticChannel });
|
|
92
|
+
}
|
|
93
|
+
for (const [dimensionIndex, dimension] of
|
|
94
|
+
(layer.encoding?.parallel?.dimensions ?? []).entries()) {
|
|
95
|
+
if (dimension?.scale === undefined) continue;
|
|
96
|
+
bindings.push({
|
|
97
|
+
layerId: layer.id,
|
|
98
|
+
scaleId: dimension.scale,
|
|
99
|
+
policyKey: "parallelDimensions",
|
|
100
|
+
semanticChannel: "parallel",
|
|
101
|
+
dimensionField: dimension.field,
|
|
102
|
+
dimensionIndex
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return cloneAndFreeze(bindings);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resolveFacetFamily(semanticSpec) {
|
|
110
|
+
if (!isPlainObject(semanticSpec) || !Array.isArray(semanticSpec.layers) ||
|
|
111
|
+
semanticSpec.layers.length === 0) {
|
|
112
|
+
throw new Error("facet requires at least one materializable layer.");
|
|
113
|
+
}
|
|
114
|
+
const primary = semanticSpec.layers.filter(layer =>
|
|
115
|
+
!(layer.mark?.type === "text" && layer.source !== undefined)
|
|
116
|
+
);
|
|
117
|
+
if (primary.length === 0) {
|
|
118
|
+
throw new Error("facet requires at least one primary materializable layer.");
|
|
119
|
+
}
|
|
120
|
+
const families = primary.map(layer => classifyPrimaryLayer(semanticSpec, layer));
|
|
121
|
+
if (new Set(families).size !== 1) {
|
|
122
|
+
throw new Error("facet requires every primary layer to use one coordinate family.");
|
|
123
|
+
}
|
|
124
|
+
const family = families[0];
|
|
125
|
+
return cloneAndFreeze({
|
|
126
|
+
family,
|
|
127
|
+
primaryLayers: primary.map(layer => layer.id),
|
|
128
|
+
dependentLayers: semanticSpec.layers
|
|
129
|
+
.filter(layer => !primary.includes(layer))
|
|
130
|
+
.map(layer => layer.id),
|
|
131
|
+
scaleBindings: collectFacetScaleBindings(semanticSpec),
|
|
132
|
+
coordinates: [...new Set(primary.map(layer => layer.coordinate).filter(Boolean))]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
4
135
|
|
|
5
136
|
function requireCollection(value, label) {
|
|
6
137
|
if (!Array.isArray(value) || value.length === 0) {
|
|
@@ -172,6 +303,7 @@ export function planFacetDependencies(semanticSpec, options = {}) {
|
|
|
172
303
|
field,
|
|
173
304
|
anchor,
|
|
174
305
|
replay,
|
|
175
|
-
layers: layerPaths.map(({ layerId, data }) => ({ id: layerId, data }))
|
|
306
|
+
layers: layerPaths.map(({ layerId, data }) => ({ id: layerId, data })),
|
|
307
|
+
...(options.family === undefined ? {} : options.family)
|
|
176
308
|
});
|
|
177
309
|
}
|
|
@@ -3,8 +3,11 @@ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
|
3
3
|
const LEGEND_FAMILIES = Object.freeze({
|
|
4
4
|
series: "categorical",
|
|
5
5
|
color: "categorical",
|
|
6
|
+
stroke: "categorical",
|
|
6
7
|
gradient: "gradient",
|
|
7
8
|
interval: "discretized",
|
|
9
|
+
strokeGradient: "gradient",
|
|
10
|
+
strokeInterval: "discretized",
|
|
8
11
|
size: "size",
|
|
9
12
|
opacity: "opacity"
|
|
10
13
|
});
|