ggaction 0.0.17 → 0.0.19
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 +96 -0
- package/README.md +6 -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 +20 -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 +8 -7
- 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/derived.js +11 -1
- package/src/actions/data/edit.js +3 -1
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +25 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +8 -4
- package/src/actions/data/stack.js +3 -1
- package/src/actions/distributions/revision.js +24 -18
- 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 +14 -8
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +26 -24
- 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 +176 -247
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +72 -10
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +155 -71
- package/src/actions/guides/axes/lines.js +27 -24
- 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 +13 -8
- package/src/actions/guides/axes/tickGroups.js +11 -7
- package/src/actions/guides/axes/ticks.js +36 -24
- package/src/actions/guides/axes/titles.js +107 -69
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +12 -4
- package/src/actions/guides/grids/grid.js +8 -2
- package/src/actions/guides/grids/resolve.js +102 -58
- package/src/actions/guides/legends/blocks.js +46 -20
- package/src/actions/guides/legends/categorical/actions.js +95 -19
- package/src/actions/guides/legends/categorical/components.js +15 -18
- package/src/actions/guides/legends/categorical/index.js +8 -0
- package/src/actions/guides/legends/categorical/layout.js +45 -20
- package/src/actions/guides/legends/categorical/options.js +39 -22
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +11 -4
- package/src/actions/guides/legends/categorical/symbols.js +38 -18
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- 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 +73 -46
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +33 -21
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +8 -7
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +9 -3
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +65 -47
- package/src/actions/guides/legends/strokeWidth.js +13 -29
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +44 -11
- 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/guides/tickValues.js +8 -5
- 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 +29 -14
- package/src/actions/marks/bar/materialize.js +15 -7
- 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/point/materialize.js +5 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +12 -10
- 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/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +8 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +18 -6
- 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 +10 -5
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +12 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +126 -45
- 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 +6 -3
- package/src/core/font.js +9 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +5 -2
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +35 -0
- package/src/core/vocabulary.js +7 -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/axisTextPlacement.js +1 -0
- 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/colorSizeLegend.js +39 -0
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +115 -26
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +13 -9
- package/src/grammar/displayLabels.js +36 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +58 -30
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +27 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendOpacity.js +6 -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/pointShapes.js +16 -2
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +16 -2
- package/src/grammar/regression/models.js +142 -54
- package/src/grammar/regression/parameters.js +23 -4
- package/src/grammar/regression/statistics.js +64 -0
- 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 +14 -0
- package/src/grammar/schemas/graphic.js +6 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +19 -3
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +4 -27
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +22 -11
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +15 -2
- 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/guideCollisions.js +9 -1
- 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/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +21 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +71 -14
- package/src/materialization/facetGuides/preparation.js +22 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- 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/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +2 -2
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +14 -15
- package/src/materialization/scales/map.js +6 -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 +7 -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/materialization/typography.js +10 -2
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +220 -0
- package/src/persistence.js +19 -6
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +22 -11
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- 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/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +14 -4
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- 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 +3 -0
- package/types/index.d.ts +12 -0
- package/types/program.d.ts +536 -129
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateNonEmptyString } from "../core/validation.js";
|
|
2
3
|
|
|
3
4
|
const ENTRY_KEYS = Object.freeze(["value", "label"]);
|
|
4
5
|
|
|
@@ -43,6 +44,14 @@ export function normalizeDisplayLabelMap(value, label = "labelMap") {
|
|
|
43
44
|
return Object.freeze(normalized);
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export function normalizeNumericDisplayLabelMap(value, label = "labelMap") {
|
|
48
|
+
const map = normalizeDisplayLabelMap(value, label);
|
|
49
|
+
if (map.some(entry => typeof entry.value !== "number" || !Number.isFinite(entry.value))) {
|
|
50
|
+
throw new TypeError(`${label} values must be finite numbers.`);
|
|
51
|
+
}
|
|
52
|
+
return map;
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
export function resolveDisplayLabel(value, map, fallback) {
|
|
47
56
|
if (map !== undefined) {
|
|
48
57
|
for (const entry of map) {
|
|
@@ -55,3 +64,30 @@ export function resolveDisplayLabel(value, map, fallback) {
|
|
|
55
64
|
}
|
|
56
65
|
return resolved;
|
|
57
66
|
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
export function normalizeDisplayColorMap(value, label = "colorMap") {
|
|
70
|
+
if (!Array.isArray(value)) throw new TypeError(`${label} must be an array.`);
|
|
71
|
+
const normalized = [];
|
|
72
|
+
for (const [index, entry] of value.entries()) {
|
|
73
|
+
if (!isPlainObject(entry)) throw new TypeError(`${label}[${index}] must be a plain object.`);
|
|
74
|
+
if (Reflect.ownKeys(entry).length !== 2 ||
|
|
75
|
+
!Object.hasOwn(entry, "value") || !Object.hasOwn(entry, "color")) {
|
|
76
|
+
throw new Error(`${label}[${index}] must contain exactly value and color.`);
|
|
77
|
+
}
|
|
78
|
+
if (!isDisplayLabelValue(entry.value)) {
|
|
79
|
+
throw new TypeError(`${label}[${index}].value must be a string, boolean, null, or finite number.`);
|
|
80
|
+
}
|
|
81
|
+
validateNonEmptyString(entry.color, `${label}[${index}].color`);
|
|
82
|
+
if (normalized.some(previous => sameDisplayLabelValue(previous.value, entry.value))) {
|
|
83
|
+
throw new Error(`${label} contains a duplicate typed value.`);
|
|
84
|
+
}
|
|
85
|
+
normalized.push(Object.freeze({ value: entry.value, color: entry.color }));
|
|
86
|
+
}
|
|
87
|
+
return Object.freeze(normalized);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveDisplayLabelColor(value, map, fallback) {
|
|
91
|
+
const entry = map?.find(entry => sameDisplayLabelValue(entry.value, value));
|
|
92
|
+
return entry?.color ?? fallback;
|
|
93
|
+
}
|
package/src/grammar/ecdf.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { requireStringValue as nonEmptyString } from "../core/validation.js";
|
|
2
|
+
import { isGroupValue as groupValue } from "./transformKeys.js";
|
|
1
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
4
|
import { stableDecimal } from "./numeric.js";
|
|
3
5
|
|
|
@@ -6,11 +8,6 @@ const TRANSFORM_KEYS = Object.freeze([
|
|
|
6
8
|
]);
|
|
7
9
|
const OUTPUT_KEYS = Object.freeze(["value", "cumulative", "probability"]);
|
|
8
10
|
|
|
9
|
-
function nonEmptyString(value, label) {
|
|
10
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
11
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
11
|
|
|
15
12
|
function normalizeGroupBy(groupBy) {
|
|
16
13
|
const fields = groupBy === undefined
|
|
@@ -101,10 +98,6 @@ export function normalizeECDFTransform({ field, groupBy, weight, missing = "drop
|
|
|
101
98
|
return cloneAndFreeze(transform);
|
|
102
99
|
}
|
|
103
100
|
|
|
104
|
-
function groupValue(value) {
|
|
105
|
-
return typeof value === "string" || typeof value === "boolean" ||
|
|
106
|
-
(typeof value === "number" && Number.isFinite(value));
|
|
107
|
-
}
|
|
108
101
|
|
|
109
102
|
function rejectOrDrop(transform, index, reason) {
|
|
110
103
|
if (transform.missing === "error") {
|
|
@@ -6,12 +6,13 @@ import { resolveRectMode } from "../rects.js";
|
|
|
6
6
|
import { resolveRuleMode } from "../rules.js";
|
|
7
7
|
|
|
8
8
|
const CARTESIAN_MARKS = new Set([
|
|
9
|
-
"point", "line", "area", "bar", "rule", "tick", "rect"
|
|
9
|
+
"point", "line", "area", "bar", "rule", "tick", "rect", "text"
|
|
10
10
|
]);
|
|
11
11
|
const FACET_BAR_GRAINS = new Set([
|
|
12
12
|
BAR_GRAINS.histogram,
|
|
13
13
|
BAR_GRAINS.aggregate,
|
|
14
|
-
BAR_GRAINS.ranged
|
|
14
|
+
BAR_GRAINS.ranged,
|
|
15
|
+
BAR_GRAINS.centered
|
|
15
16
|
]);
|
|
16
17
|
const ORDINARY_SCALE_CHANNELS = Object.freeze([
|
|
17
18
|
["x", "x"], ["y", "y"], ["xOffset", "xOffset"], ["yOffset", "yOffset"],
|
|
@@ -66,6 +67,9 @@ function classifyPrimaryLayer(semanticSpec, layer) {
|
|
|
66
67
|
`facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
|
|
67
68
|
);
|
|
68
69
|
}
|
|
70
|
+
if (type === "text" && layer.encoding?.text === undefined) {
|
|
71
|
+
throw new Error(`Facet text layer "${layer.id}" requires a text encoding.`);
|
|
72
|
+
}
|
|
69
73
|
const complete = (
|
|
70
74
|
layer.encoding?.x?.scale !== undefined &&
|
|
71
75
|
layer.encoding?.y?.scale !== undefined
|
|
@@ -3,13 +3,15 @@ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
|
3
3
|
const LEGEND_FAMILIES = Object.freeze({
|
|
4
4
|
series: "categorical",
|
|
5
5
|
color: "categorical",
|
|
6
|
+
colorSize: "color-size",
|
|
6
7
|
stroke: "categorical",
|
|
7
8
|
gradient: "gradient",
|
|
8
9
|
interval: "discretized",
|
|
9
10
|
strokeGradient: "gradient",
|
|
10
11
|
strokeInterval: "discretized",
|
|
11
12
|
size: "size",
|
|
12
|
-
opacity: "opacity"
|
|
13
|
+
opacity: "opacity",
|
|
14
|
+
strokeWidth: "stroke-width"
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
function requirePlacements(placements) {
|
|
@@ -72,25 +74,34 @@ function translatedBounds(placement, bounds, channel) {
|
|
|
72
74
|
};
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
export function resolveFacetOuterSides({ placements } = {}) {
|
|
78
|
+
const cells = requirePlacements(placements);
|
|
79
|
+
const edges = { top: new Map(), bottom: new Map(), left: new Map(), right: new Map() };
|
|
80
|
+
for (const cell of cells) {
|
|
81
|
+
for (const [side, key, coordinate, better] of [
|
|
82
|
+
["top", cell.column, "row", (a, b) => a < b],
|
|
83
|
+
["bottom", cell.column, "row", (a, b) => a > b],
|
|
84
|
+
["left", cell.row, "column", (a, b) => a < b],
|
|
85
|
+
["right", cell.row, "column", (a, b) => a > b]
|
|
86
|
+
]) {
|
|
87
|
+
const previous = edges[side].get(key);
|
|
88
|
+
if (previous === undefined || better(cell[coordinate], previous[coordinate])) edges[side].set(key, cell);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return cloneAndFreeze(Object.fromEntries(Object.entries(edges).map(([side, owners]) => {
|
|
92
|
+
const ids = new Set([...owners.values()].map(cell => cell.id));
|
|
93
|
+
return [side, cells.filter(cell => ids.has(cell.id)).map(cell => cell.id)];
|
|
94
|
+
})));
|
|
95
|
+
}
|
|
96
|
+
|
|
75
97
|
export function resolveFacetOuterAxes({ placements, axisBounds = {} } = {}) {
|
|
76
98
|
const cells = requirePlacements(placements);
|
|
77
99
|
if (!isPlainObject(axisBounds)) {
|
|
78
100
|
throw new TypeError("Facet axisBounds must be a plain object.");
|
|
79
101
|
}
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const bottom = bottomByColumn.get(cell.column);
|
|
84
|
-
if (bottom === undefined || cell.row > bottom.row) {
|
|
85
|
-
bottomByColumn.set(cell.column, cell);
|
|
86
|
-
}
|
|
87
|
-
const left = leftByRow.get(cell.row);
|
|
88
|
-
if (left === undefined || cell.column < left.column) {
|
|
89
|
-
leftByRow.set(cell.row, cell);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const xOwners = new Set([...bottomByColumn.values()].map(cell => cell.id));
|
|
93
|
-
const yOwners = new Set([...leftByRow.values()].map(cell => cell.id));
|
|
102
|
+
const sides = resolveFacetOuterSides({ placements: cells });
|
|
103
|
+
const xOwners = new Set(sides.bottom);
|
|
104
|
+
const yOwners = new Set(sides.left);
|
|
94
105
|
return cloneAndFreeze({
|
|
95
106
|
x: cells.filter(cell => xOwners.has(cell.id)).map(cell => cell.id),
|
|
96
107
|
y: cells.filter(cell => yOwners.has(cell.id)).map(cell => cell.id),
|
|
@@ -129,7 +140,7 @@ function comparableConfig(config) {
|
|
|
129
140
|
return stableValue(rest);
|
|
130
141
|
}
|
|
131
142
|
|
|
132
|
-
function
|
|
143
|
+
export function sameFacetGuideValue(left, right) {
|
|
133
144
|
return JSON.stringify(stableValue(left)) === JSON.stringify(stableValue(right));
|
|
134
145
|
}
|
|
135
146
|
|
|
@@ -152,12 +163,13 @@ function requireLegendChildren(children) {
|
|
|
152
163
|
});
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
function scaleIds(config) {
|
|
156
|
-
const ids =
|
|
166
|
+
function scaleIds(config, kind) {
|
|
167
|
+
const ids = kind === "colorSize" ? [config.colorScale, config.sizeScale]
|
|
168
|
+
: config.scales ?? (config.scale === undefined ? [] : [config.scale]);
|
|
157
169
|
if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id =>
|
|
158
170
|
typeof id === "string" && id.length > 0
|
|
159
171
|
)) {
|
|
160
|
-
throw new Error(`Shared ${
|
|
172
|
+
throw new Error(`Shared ${kind} legend requires resolved scale ids.`);
|
|
161
173
|
}
|
|
162
174
|
return ids;
|
|
163
175
|
}
|
|
@@ -165,6 +177,11 @@ function scaleIds(config) {
|
|
|
165
177
|
export function resolveSharedFacetLegends(children) {
|
|
166
178
|
const normalized = requireLegendChildren(children);
|
|
167
179
|
const first = normalized[0];
|
|
180
|
+
for (const child of normalized.slice(1)) {
|
|
181
|
+
if (!sameFacetGuideValue(first.guideConfigs.legendGroups, child.guideConfigs.legendGroups)) {
|
|
182
|
+
throw new Error(`Facet child "${child.id}" has incompatible legend group placement.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
168
185
|
const kinds = Object.keys(first.legends);
|
|
169
186
|
const entries = kinds.map(kind => {
|
|
170
187
|
const family = LEGEND_FAMILIES[kind];
|
|
@@ -178,7 +195,7 @@ export function resolveSharedFacetLegends(children) {
|
|
|
178
195
|
) {
|
|
179
196
|
throw new Error(`Facet child "${first.id}" has invalid ${kind} legend config.`);
|
|
180
197
|
}
|
|
181
|
-
const scales = scaleIds(config);
|
|
198
|
+
const scales = scaleIds(config, kind);
|
|
182
199
|
const resolvedScales = Object.fromEntries(scales.map(id => {
|
|
183
200
|
const scale = first.resolvedScales[id];
|
|
184
201
|
if (!isPlainObject(scale)) {
|
|
@@ -190,14 +207,14 @@ export function resolveSharedFacetLegends(children) {
|
|
|
190
207
|
const candidate = child.legends[kind];
|
|
191
208
|
if (
|
|
192
209
|
!isPlainObject(candidate) ||
|
|
193
|
-
!
|
|
210
|
+
!sameFacetGuideValue(comparableConfig(config), comparableConfig(candidate))
|
|
194
211
|
) {
|
|
195
212
|
throw new Error(
|
|
196
213
|
`Facet child "${child.id}" has an incompatible ${kind} legend config.`
|
|
197
214
|
);
|
|
198
215
|
}
|
|
199
216
|
for (const id of scales) {
|
|
200
|
-
if (!
|
|
217
|
+
if (!sameFacetGuideValue(resolvedScales[id], child.resolvedScales[id])) {
|
|
201
218
|
throw new Error(
|
|
202
219
|
`Facet child "${child.id}" has an incompatible resolved legend scale "${id}".`
|
|
203
220
|
);
|
|
@@ -208,7 +225,7 @@ export function resolveSharedFacetLegends(children) {
|
|
|
208
225
|
});
|
|
209
226
|
for (const child of normalized.slice(1)) {
|
|
210
227
|
const childKinds = Object.keys(child.legends);
|
|
211
|
-
if (!
|
|
228
|
+
if (!sameFacetGuideValue([...kinds].sort(), [...childKinds].sort())) {
|
|
212
229
|
throw new Error(`Facet child "${child.id}" has incompatible legend kinds.`);
|
|
213
230
|
}
|
|
214
231
|
}
|
|
@@ -229,7 +246,7 @@ export function planFacetGuideOwnership({
|
|
|
229
246
|
if (!["each", "outer"].includes(axes)) {
|
|
230
247
|
throw new Error("Facet axes must be each or outer.");
|
|
231
248
|
}
|
|
232
|
-
if (![false, "shared"].includes(legend)) {
|
|
249
|
+
if (![false, "shared"].includes(legend) && !isPlainObject(legend)) {
|
|
233
250
|
throw new Error("Facet legend must be false or shared.");
|
|
234
251
|
}
|
|
235
252
|
const byId = new Map(children.map(child => [child.id, child]));
|
|
@@ -237,23 +254,34 @@ export function planFacetGuideOwnership({
|
|
|
237
254
|
throw new Error("Facet guide child ids must match occupied placements exactly.");
|
|
238
255
|
}
|
|
239
256
|
const outer = resolveFacetOuterAxes({ placements: cells });
|
|
240
|
-
|
|
257
|
+
const sides = resolveFacetOuterSides({ placements: cells });
|
|
258
|
+
if (legend !== false && !isPlainObject(sharedLegends)) {
|
|
241
259
|
throw new Error("Shared facet legend ownership requires a compatibility plan.");
|
|
242
260
|
}
|
|
243
261
|
return cloneAndFreeze({
|
|
244
262
|
children: Object.fromEntries(cells.map(cell => {
|
|
245
263
|
const descriptor = byId.get(cell.id);
|
|
246
264
|
const presentAxes = descriptor.axes ?? [];
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
265
|
+
const records = descriptor.axisOwners;
|
|
266
|
+
const removed = {};
|
|
267
|
+
const keepAxes = axes === "each" ? presentAxes : records === undefined
|
|
268
|
+
? presentAxes.filter(channel => outer.children[cell.id][channel])
|
|
269
|
+
: presentAxes.filter(id => {
|
|
270
|
+
const owner = records.find(owner => owner.id === id);
|
|
271
|
+
if (owner === undefined) throw new Error("Facet axis descriptors must identify every retained owner.");
|
|
272
|
+
const components = Object.entries(owner.components);
|
|
273
|
+
const remove = components.filter(([, side]) => !sides[side]?.includes(cell.id)).map(([component]) => component);
|
|
274
|
+
if (remove.length > 0 && remove.length < components.length) removed[id] = remove;
|
|
275
|
+
return remove.length < components.length;
|
|
276
|
+
});
|
|
250
277
|
return [cell.id, {
|
|
251
278
|
keepAxes,
|
|
252
279
|
removeAxes: presentAxes.filter(channel => !keepAxes.includes(channel)),
|
|
253
|
-
|
|
280
|
+
...(records === undefined ? {} : { removeAxisComponents: removed }),
|
|
281
|
+
removeLegends: legend !== false ? descriptor.legendKinds ?? [] : []
|
|
254
282
|
}];
|
|
255
283
|
})),
|
|
256
|
-
parent: legend
|
|
284
|
+
parent: legend !== false
|
|
257
285
|
? { legends: sharedLegends.entries, promoteFrom: sharedLegends.source }
|
|
258
286
|
: { legends: [] }
|
|
259
287
|
});
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
} from "../../core/validation.js";
|
|
7
7
|
import { planFacetDependencies, resolveFacetFamily } from "./dependencies.js";
|
|
8
8
|
import { readNominalField } from "../scales/index.js";
|
|
9
|
-
const MAX_FACET_CHILDREN = 100;
|
|
10
9
|
|
|
11
10
|
function requireFacetField(field, label = "facet") {
|
|
12
11
|
if (typeof field !== "string" || field.length === 0) {
|
|
@@ -100,8 +99,7 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
|
|
|
100
99
|
const values = resolveValues(observed, options.values);
|
|
101
100
|
validateGeneratedItemLimit(
|
|
102
101
|
values.length,
|
|
103
|
-
"Facet child count"
|
|
104
|
-
MAX_FACET_CHILDREN
|
|
102
|
+
"Facet child count"
|
|
105
103
|
);
|
|
106
104
|
validateWorkLimit(
|
|
107
105
|
dataset.values.length * values.length,
|
|
@@ -175,8 +173,7 @@ export function resolveFacetGridDefinition(semanticSpec, options = {}) {
|
|
|
175
173
|
}
|
|
176
174
|
validateGeneratedItemLimit(
|
|
177
175
|
selected.length,
|
|
178
|
-
"Facet grid child count"
|
|
179
|
-
MAX_FACET_CHILDREN
|
|
176
|
+
"Facet grid child count"
|
|
180
177
|
);
|
|
181
178
|
validateWorkLimit(
|
|
182
179
|
dataset.values.length * selected.length,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { validateOptionObject, validateNonEmptyString } from "../../core/validation.js";
|
|
3
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
4
|
+
import { LEGEND_CONFIG_KINDS, LEGEND_POSITIONS } from "../../core/vocabulary.js";
|
|
5
|
+
import { describeLegendBlock } from "../legendBlocks.js";
|
|
6
|
+
import { validateLegendGroupState } from "../legendGroups.js";
|
|
7
|
+
import { resolveSharedFacetLegends, sameFacetGuideValue as same } from "./guides.js";
|
|
8
|
+
|
|
9
|
+
const CONFIG_KEYS = Object.freeze([
|
|
10
|
+
"kind", "target", "channels", "scales", "scale", "field", "fieldType", "domain", "title", "titleVisible",
|
|
11
|
+
"inferredTitle", "inferredSymbol", "inferredStrokeWidth", "inferredOffset", "position", "align", "offset", "direction", "columns",
|
|
12
|
+
"titlePosition", "symbol", "labels", "titleStyle", "itemGap", "layout", "overflow", "border", "gradient",
|
|
13
|
+
"count", "sampling", "representation", "gradientDirection", "inheritAppearance", "inheritPosition", "blockOverrides",
|
|
14
|
+
"combine", "sizeAreas", "clipHeight"
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function isAuthoredFacetLegend(value) {
|
|
18
|
+
return isPlainObject(value) && value.type === "authored-legend";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateAuthoredFacetLegend(program, intent) {
|
|
22
|
+
validateOptionObject(intent, ["type", "blocks", "groups"], "Stored parent legend");
|
|
23
|
+
if (!isAuthoredFacetLegend(intent) || Object.keys(intent).length !== 3 ||
|
|
24
|
+
!isPlainObject(intent.blocks) || Object.keys(intent.blocks).length === 0 || !isPlainObject(intent.groups)) {
|
|
25
|
+
throw new Error("Stored parent legend requires its closed authored block intent.");
|
|
26
|
+
}
|
|
27
|
+
validateLegendGroupState(intent.groups);
|
|
28
|
+
const configs = {};
|
|
29
|
+
for (const [kind, block] of Object.entries(intent.blocks)) {
|
|
30
|
+
if (!LEGEND_CONFIG_KINDS.includes(kind)) throw new Error("Stored parent legend has an unknown block kind.");
|
|
31
|
+
validateOptionObject(block, ["config", "guide"], "Stored parent legend block");
|
|
32
|
+
validateOptionObject(block.config, kind === "colorSize" ? [...CONFIG_KEYS, "colorScale", "sizeScale"] : CONFIG_KEYS, "Stored parent legend config");
|
|
33
|
+
const config = block.config;
|
|
34
|
+
validateUserId(config.target, "Parent legend target");
|
|
35
|
+
if (!LEGEND_POSITIONS.includes(config.position)) throw new Error("Stored parent legend requires a native position.");
|
|
36
|
+
for (const flag of ["inferredTitle", "inferredSymbol", "inferredStrokeWidth", "inferredOffset", "titleVisible", "inheritAppearance", "inheritPosition"]) {
|
|
37
|
+
if (config[flag] !== undefined && typeof config[flag] !== "boolean") throw new Error(`Stored parent legend ${flag} must be boolean.`);
|
|
38
|
+
}
|
|
39
|
+
const descriptor = describeLegendBlock(program, kind, config);
|
|
40
|
+
if (block.guide !== undefined) {
|
|
41
|
+
validateOptionObject(block.guide, kind === "colorSize" ? ["colorScale", "sizeScale", "title"]
|
|
42
|
+
: ["scale", "scales", "channels", "title", "order"], "Stored parent legend meaning");
|
|
43
|
+
if (kind === "colorSize" && (block.guide.colorScale !== config.colorScale || block.guide.sizeScale !== config.sizeScale)) {
|
|
44
|
+
throw new Error("Stored parent color-size meaning must match both block scale bindings.");
|
|
45
|
+
}
|
|
46
|
+
if (block.guide.title !== undefined) validateNonEmptyString(block.guide.title, "Parent legend title");
|
|
47
|
+
if (block.guide.channels !== undefined && !same([...block.guide.channels].sort(), descriptor.channels)) {
|
|
48
|
+
throw new Error("Stored parent legend meaning must match its block channels.");
|
|
49
|
+
}
|
|
50
|
+
if (block.guide.scale !== undefined && block.guide.scale !== descriptor.scaleIds[0] ||
|
|
51
|
+
block.guide.scales !== undefined && !same(block.guide.scales, descriptor.scaleIds)) {
|
|
52
|
+
throw new Error("Stored parent legend meaning must match its block scale binding.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!same(config, program.guideConfigs.legend?.[kind])) throw new Error("Parent legend materialization must match its authored owner.");
|
|
56
|
+
configs[kind] = config;
|
|
57
|
+
}
|
|
58
|
+
if (!same(Object.keys(configs).sort(), Object.keys(program.guideConfigs.legend ?? {}).sort()) ||
|
|
59
|
+
!same(intent.groups, program.guideConfigs.legendGroups ?? {})) {
|
|
60
|
+
throw new Error("Parent legend materialization must contain exactly its authored blocks and groups.");
|
|
61
|
+
}
|
|
62
|
+
resolveSharedFacetLegends(program.compositionSpec.children.map(id => ({ id,
|
|
63
|
+
guideConfigs: { legend: configs, legendGroups: intent.groups }, resolvedScales: program.children[id].resolvedScales })));
|
|
64
|
+
return intent;
|
|
65
|
+
}
|
package/src/grammar/fold.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
2
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
4
|
|
|
3
5
|
const TRANSFORM_KEYS = Object.freeze(["type", "fields", "as"]);
|
|
@@ -5,19 +7,7 @@ const AS_KEYS = Object.freeze(["key", "value"]);
|
|
|
5
7
|
const MAX_FIELDS = 64;
|
|
6
8
|
const MAX_OUTPUT_ROWS = 10_000;
|
|
7
9
|
|
|
8
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
9
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
10
|
-
if (unknown !== undefined) {
|
|
11
|
-
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
10
|
|
|
15
|
-
function requireField(value, label) {
|
|
16
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
17
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
18
|
-
}
|
|
19
|
-
return value;
|
|
20
|
-
}
|
|
21
11
|
|
|
22
12
|
function normalizeAs(value) {
|
|
23
13
|
const requested = value ?? {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
2
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
3
|
import {
|
|
3
4
|
deriveKernelDensity,
|
|
@@ -23,12 +24,6 @@ const TRANSFORM_KEYS = [
|
|
|
23
24
|
];
|
|
24
25
|
const OUTPUT_KEYS = Object.keys(GRADIENT_PROFILE_FIELDS);
|
|
25
26
|
|
|
26
|
-
function requireField(value, label) {
|
|
27
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
28
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
29
|
-
}
|
|
30
|
-
return value;
|
|
31
|
-
}
|
|
32
27
|
|
|
33
28
|
function validatePair(value, label) {
|
|
34
29
|
if (
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isPlainObject as i}from"../core/immutable.js";import{validateGeneratedItemLimit as o}from"../core/validation.js";function n(e,s="Guide strokeDash"){if(!Array.isArray(e)||e.length%2!==0||!e.every(r=>Number.isFinite(r)&&r>=0))throw new TypeError(`${s} must be an even-length array of non-negative finite numbers.`);return o(e.length,s),e}function u(e,{reset:s=!0}={}){if(e==="auto"&&s)return;if(!Array.isArray(e))throw new TypeError("Guide strokeDashMap must be an array.");o(e.length,"Guide strokeDashMap count");const r=new Set,a=e.map(t=>{if(!i(t)||Reflect.ownKeys(t).length!==2||!Object.hasOwn(t,"value")||!Object.hasOwn(t,"strokeDash"))throw new TypeError("Guide strokeDashMap entries must contain exactly value and strokeDash.");if(!["string","boolean"].includes(typeof t.value)&&!Number.isFinite(t.value))throw new TypeError("Guide strokeDashMap values must be strings, booleans, or finite numbers.");if(r.has(t.value))throw new Error("Guide strokeDashMap contains a duplicate typed value.");return r.add(t.value),n(t.strokeDash),Object.freeze({value:t.value,strokeDash:Object.freeze([...t.strokeDash])})});return a.length===0?void 0:Object.freeze(a)}function d(e,s){const r={...e};if(Object.hasOwn(s,"strokeDash")&&(n(s.strokeDash),r.strokeDash=[...s.strokeDash]),Object.hasOwn(s,"strokeDashMap")){const a=u(s.strokeDashMap);a===void 0?delete r.strokeDashMap:r.strokeDashMap=a}return r}function D(e,s){const r=new Map(s.strokeDashMap?.map(a=>[a.value,a.strokeDash]));return e.map(a=>r.get(a)??s.strokeDash??[])}export{u as normalizeGuideDashMap,d as patchGuideDash,D as resolveGuideDashes,n as validateGuideDash};
|
package/src/grammar/impute.js
CHANGED
|
@@ -1,26 +1,16 @@
|
|
|
1
|
+
import { normalizeSortBy, validateSortBy } from "./transformKeys.js";
|
|
2
|
+
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
3
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
4
|
import { IMPUTE_REQUIRED_OPTIONS } from "../core/optionRequirements.js";
|
|
2
5
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
3
|
-
import { interpolateNumber, inverseLerp } from "./numeric.js";
|
|
6
|
+
import { interpolateNumber, inverseLerp, stableFiniteMean } from "./numeric.js";
|
|
4
7
|
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
5
8
|
|
|
6
9
|
const TRANSFORM_KEYS = Object.freeze([
|
|
7
10
|
"type", "fields", "groupBy", "sortBy", "method", "value", "edges", "maxGap"
|
|
8
11
|
]);
|
|
9
|
-
const SORT_KEYS = Object.freeze(["field", "order"]);
|
|
10
12
|
const METHODS = Object.freeze(Object.keys(IMPUTE_REQUIRED_OPTIONS));
|
|
11
13
|
|
|
12
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
13
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
14
|
-
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function requireField(value, label) {
|
|
18
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
19
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
20
|
-
}
|
|
21
|
-
return value;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
function validateFieldList(value, label) {
|
|
25
15
|
if (!Array.isArray(value) || value.length === 0 || value.some(field =>
|
|
26
16
|
typeof field !== "string" || field.length === 0
|
|
@@ -35,33 +25,6 @@ function normalizeFieldList(value, { allowEmpty = false } = {}) {
|
|
|
35
25
|
return Array.isArray(value) ? [...value] : [value];
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
function normalizeSortBy(value) {
|
|
39
|
-
if (value === undefined) return [];
|
|
40
|
-
if (!Array.isArray(value)) return value;
|
|
41
|
-
return value.map(sort => isPlainObject(sort)
|
|
42
|
-
? { ...sort, order: sort.order ?? "ascending" }
|
|
43
|
-
: sort
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function validateSortBy(sortBy) {
|
|
48
|
-
if (!Array.isArray(sortBy)) throw new TypeError("Impute sortBy must be an array.");
|
|
49
|
-
const fields = [];
|
|
50
|
-
sortBy.forEach((sort, index) => {
|
|
51
|
-
if (!isPlainObject(sort)) {
|
|
52
|
-
throw new TypeError(`Impute sortBy[${index}] must be a plain object.`);
|
|
53
|
-
}
|
|
54
|
-
rejectUnknownKeys(sort, SORT_KEYS, `impute sortBy[${index}]`);
|
|
55
|
-
fields.push(requireField(sort.field, `Impute sortBy[${index}].field`));
|
|
56
|
-
if (!["ascending", "descending"].includes(sort.order)) {
|
|
57
|
-
throw new Error(`Unsupported impute sort order "${sort.order}".`);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
if (new Set(fields).size !== fields.length) {
|
|
61
|
-
throw new Error("Impute sortBy fields must be unique.");
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
28
|
function scalarType(value, label) {
|
|
66
29
|
if (value === null || value === undefined) return undefined;
|
|
67
30
|
if (typeof value === "string" || typeof value === "boolean") return typeof value;
|
|
@@ -94,7 +57,7 @@ export function validateImputeTransform(transform) {
|
|
|
94
57
|
if (new Set(transform.groupBy).size !== transform.groupBy.length) {
|
|
95
58
|
throw new Error("Impute groupBy fields must be unique.");
|
|
96
59
|
}
|
|
97
|
-
validateSortBy(transform.sortBy);
|
|
60
|
+
validateSortBy(transform.sortBy, "Impute");
|
|
98
61
|
if (!METHODS.includes(transform.method)) {
|
|
99
62
|
throw new Error(`Unsupported impute method "${transform.method}".`);
|
|
100
63
|
}
|
|
@@ -107,7 +70,7 @@ export function validateImputeTransform(transform) {
|
|
|
107
70
|
scalarType(transform.value, "Impute constant value");
|
|
108
71
|
} else {
|
|
109
72
|
if (hasValue) throw new Error(`Impute value is not available for ${transform.method}.`);
|
|
110
|
-
if (transform.sortBy.length === 0) {
|
|
73
|
+
if (transform.method !== "mean" && transform.sortBy.length === 0) {
|
|
111
74
|
throw new Error(`Impute ${transform.method} requires non-empty sortBy.`);
|
|
112
75
|
}
|
|
113
76
|
}
|
|
@@ -132,7 +95,7 @@ export function normalizeImputeTransform(args = {}) {
|
|
|
132
95
|
fields: normalizeFieldList(args.fields),
|
|
133
96
|
groupBy: normalizeFieldList(args.groupBy, { allowEmpty: true }),
|
|
134
97
|
sortBy: normalizeSortBy(args.sortBy),
|
|
135
|
-
method: args.method,
|
|
98
|
+
method: args.method ?? "mean",
|
|
136
99
|
edges: args.edges ?? "keep"
|
|
137
100
|
};
|
|
138
101
|
if (Object.hasOwn(args, "value")) transform.value = args.value;
|
|
@@ -212,9 +175,9 @@ function validateFieldTypes(entries, transform) {
|
|
|
212
175
|
if (types.size > 1) {
|
|
213
176
|
throw new TypeError(`Impute field "${field}" must contain one scalar type.`);
|
|
214
177
|
}
|
|
215
|
-
if (transform.method
|
|
178
|
+
if (["linear", "mean"].includes(transform.method) &&
|
|
216
179
|
(types.size > 0 && !types.has("number"))) {
|
|
217
|
-
throw new TypeError(`Impute
|
|
180
|
+
throw new TypeError(`Impute ${transform.method} field "${field}" must contain finite numbers.`);
|
|
218
181
|
}
|
|
219
182
|
}
|
|
220
183
|
}
|
|
@@ -278,10 +241,18 @@ function setField(row, field, value) {
|
|
|
278
241
|
});
|
|
279
242
|
}
|
|
280
243
|
|
|
281
|
-
function fillRun(partition, field, run, transform, positions) {
|
|
244
|
+
function fillRun(partition, field, run, transform, positions, mean) {
|
|
282
245
|
const [start, end] = run;
|
|
283
246
|
const length = end - start + 1;
|
|
284
247
|
if (transform.maxGap !== undefined && length > transform.maxGap) return;
|
|
248
|
+
if (transform.method === "mean") {
|
|
249
|
+
if (mean === undefined) {
|
|
250
|
+
if (transform.edges === "error") throw new RangeError(`Impute mean requires an observed value in field "${field}".`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
for (let index = start; index <= end; index += 1) setField(partition[index].row, field, mean);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
285
256
|
if (transform.method === "constant") {
|
|
286
257
|
for (let index = start; index <= end; index += 1) {
|
|
287
258
|
setField(partition[index].row, field, transform.value);
|
|
@@ -332,8 +303,10 @@ export function deriveImputedRows(rows, transform) {
|
|
|
332
303
|
partition.sort((left, right) => compareEntries(left, right, transform.sortBy));
|
|
333
304
|
const positions = linearPositions(partition, transform);
|
|
334
305
|
for (const field of transform.fields) {
|
|
306
|
+
const observed = transform.method === "mean" ? partition.map(entry => entry.row[field]).filter(value => value !== null && value !== undefined) : [];
|
|
307
|
+
const mean = observed.length ? stableFiniteMean(observed, "Impute group mean") : undefined;
|
|
335
308
|
for (const run of missingRuns(partition, field)) {
|
|
336
|
-
fillRun(partition, field, run, transform, positions);
|
|
309
|
+
fillRun(partition, field, run, transform, positions, mean);
|
|
337
310
|
}
|
|
338
311
|
}
|
|
339
312
|
}
|
package/src/grammar/interval.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { requireStringValue as nonEmptyString } from "../core/validation.js";
|
|
2
|
+
import { isGroupValue } from "./transformKeys.js";
|
|
1
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
4
|
import { aggregateScalarValues } from "./aggregate.js";
|
|
3
5
|
import { requireFiniteResult, stableDecimal } from "./numeric.js";
|
|
@@ -21,11 +23,6 @@ const TRANSFORM_KEYS = [
|
|
|
21
23
|
];
|
|
22
24
|
const OUTPUT_KEYS = ["center", "lower", "upper"];
|
|
23
25
|
|
|
24
|
-
function nonEmptyString(value, label) {
|
|
25
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
26
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
26
|
|
|
30
27
|
function validateGrouping(groupBy) {
|
|
31
28
|
if (
|
|
@@ -166,10 +163,6 @@ function isMissing(value) {
|
|
|
166
163
|
(typeof value === "number" && Number.isNaN(value));
|
|
167
164
|
}
|
|
168
165
|
|
|
169
|
-
function isGroupValue(value) {
|
|
170
|
-
return typeof value === "string" || typeof value === "boolean" ||
|
|
171
|
-
(typeof value === "number" && Number.isFinite(value));
|
|
172
|
-
}
|
|
173
166
|
|
|
174
167
|
function deriveGroup(values, transform) {
|
|
175
168
|
if (transform.center === "median") {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LEGEND_CHANNELS as d,LEGEND_CONFIG_KINDS as h}from"../core/vocabulary.js";import{isEnumeratedSizeScaleType as u}from"./scales/size.js";function y(e,r){if(!Array.isArray(e))throw new TypeError(`${r} channels must be an array.`);if(e.length===0)throw new Error(`${r} channels must select at least one channel.`);const s=new Set;for(const t of e){if(!d.includes(t))throw new Error(`Unsupported legend channel "${t}".`);if(s.has(t))throw new Error(`${r} channels contains duplicate "${t}".`);s.add(t)}}const o=Object.freeze(["series","color","stroke"]),a=Object.freeze({size:"size",opacity:"opacity",strokeWidth:"strokeWidth",gradient:"color",interval:"color",strokeGradient:"stroke",strokeInterval:"stroke"}),c=(e,r)=>e<r?-1:e>r?1:0;function w(e,r){const s=e==="colorSize"?["color","size"]:o.includes(e)?r?.channels:a[e]===void 0?void 0:[a[e]];if(!Array.isArray(s)||s.length===0)throw new Error(`Legend block ${e} requires at least one channel.`);return y(s,`Legend block ${e}`),Object.freeze([...s].sort(c))}function p(e){if(!Array.isArray(e)||e.length===0)throw new TypeError("Legend block key requires a non-empty channel array.");return JSON.stringify([...e].sort(c))}function b(e,r,s){if(!h.includes(r))throw new Error(`Unknown legend block kind "${r}".`);const t=w(r,s),n=r==="colorSize"?[s.colorScale,s.sizeScale]:o.includes(r)?s?.scales:[s?.scale];if(!Array.isArray(n)||n.length===0||n.some(l=>typeof l!="string"||e.resolvedScales[l]===void 0))throw new Error(`Legend block ${r} requires resolved scales.`);const i=o.includes(r)?"categorical":r==="colorSize"?"sampled":r==="size"?u(e.resolvedScales[n[0]].type)?"discrete-size":"sampled":["opacity","strokeWidth"].includes(r)||r==="gradient"&&s.representation==="symbols"?"sampled":["gradient","strokeGradient"].includes(r)?"gradient":"interval";return Object.freeze({target:s.target,kind:r,family:i,key:p(t),channels:t,scaleIds:Object.freeze([...n]),config:s})}function g(e,r){return e.symbol.layers.length===1&&(e.kind==="series"&&r==="line"||["color","stroke"].includes(e.kind)&&r==="swatch")?`${e.kind}LegendSymbols`:`${e.kind}LegendSymbol${{line:"Lines",point:"Points",swatch:"Swatches"}[r]}`}export{g as categoricalLegendSymbolId,b as describeLegendBlock,w as legendBlockChannels,p as legendBlockKey,y as validateLegendChannels};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Legend borders use one closed rectangular path, so odd dash duplication is not implicit.
|
|
2
|
+
export function normalizeLegendBorderDash(value, label = "Legend border strokeDash") {
|
|
3
|
+
if (value === undefined) return undefined;
|
|
4
|
+
if (!Array.isArray(value) || value.length % 2 !== 0 ||
|
|
5
|
+
![...value].every(item => Number.isFinite(item) && item >= 0)) {
|
|
6
|
+
throw new TypeError(`${label} must be an even-length array of non-negative finite numbers.`);
|
|
7
|
+
}
|
|
8
|
+
return Object.freeze([...value]);
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function normalizeLegendClipHeight(value,{edit=false}={}){
|
|
2
|
+
if(value===undefined||edit&&value==='auto')return undefined;
|
|
3
|
+
if(!Number.isFinite(value)||value<=0)throw new RangeError('Legend clipHeight must be a positive finite number.');
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
export function validateLegendClip(kind,config){
|
|
7
|
+
if(config.clipHeight===undefined)return;
|
|
8
|
+
normalizeLegendClipHeight(config.clipHeight);
|
|
9
|
+
const supported=['size','opacity','colorSize'].includes(kind)||kind==='gradient'&&config.representation==='symbols'||
|
|
10
|
+
['series','color','stroke'].includes(kind)&&config.symbol?.layers?.some(layer=>layer.type==='point');
|
|
11
|
+
if(!supported)throw new Error('Legend clipHeight requires a point-bearing sample block.');
|
|
12
|
+
}
|
|
13
|
+
export function patchLegendClipHeight(config,value){
|
|
14
|
+
const {clipHeight:_old,...rest}=config,clipHeight=normalizeLegendClipHeight(value,{edit:true});
|
|
15
|
+
return clipHeight===undefined?rest:{...rest,clipHeight};
|
|
16
|
+
}
|
|
17
|
+
export function clippedSampleBounds(bounds,height){
|
|
18
|
+
return height===undefined?bounds:{...bounds,top:Math.max(bounds.top,-height/2),bottom:Math.min(bounds.bottom,height/2)};
|
|
19
|
+
}
|