ggaction 0.0.12 → 0.0.13
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 +49 -0
- package/README.md +2 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -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 +4 -2
- package/src/actions/basic.js +2 -0
- package/src/actions/boxPlots/materialize.js +2 -1
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +3 -2
- package/src/actions/charts/area.js +47 -0
- package/src/actions/charts/bar.js +11 -5
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +44 -0
- package/src/actions/charts/ecdf.js +183 -0
- package/src/actions/charts/endpoints.js +451 -0
- package/src/actions/charts/heatmap.js +2 -2
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +71 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +169 -0
- package/src/actions/charts/line.js +5 -5
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +44 -0
- package/src/actions/charts/polar.js +123 -0
- package/src/actions/charts/radar.js +299 -0
- package/src/actions/charts/radial.js +42 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +279 -0
- package/src/actions/charts/scatter.js +9 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +118 -1
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/derived.js +66 -0
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/filter.js +212 -28
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/index.js +24 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/regression.js +5 -2
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +28 -22
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +120 -33
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +10 -16
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +10 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +14 -0
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +48 -13
- package/src/actions/encodings/position/resolve.js +67 -22
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +37 -32
- package/src/actions/encodings/ruleAppearance.js +23 -9
- package/src/actions/encodings/seriesLayout.js +104 -0
- package/src/actions/encodings/shared.js +25 -23
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +18 -5
- package/src/actions/errorBands/edit.js +341 -21
- package/src/actions/errorBars/create.js +21 -10
- package/src/actions/errorBars/edit.js +355 -19
- package/src/actions/facets/actions.js +410 -12
- package/src/actions/facets/derive.js +57 -8
- package/src/actions/gradientPlots/components.js +33 -11
- package/src/actions/gradientPlots/materialize.js +5 -7
- package/src/actions/guides/applicability.js +20 -17
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +129 -19
- package/src/actions/guides/axes/lines.js +7 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +44 -23
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +14 -8
- package/src/actions/guides/axes/ticks.js +5 -4
- package/src/actions/guides/axes/titles.js +21 -8
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +10 -2
- package/src/actions/guides/guides.js +35 -28
- package/src/actions/guides/legends/categorical/actions.js +193 -249
- package/src/actions/guides/legends/categorical/components.js +4 -5
- package/src/actions/guides/legends/categorical/layout.js +36 -349
- package/src/actions/guides/legends/categorical/options.js +30 -14
- package/src/actions/guides/legends/categorical/recipes.js +14 -0
- package/src/actions/guides/legends/categorical/resolve.js +21 -10
- package/src/actions/guides/legends/categorical/symbols.js +2 -3
- package/src/actions/guides/legends/continuous/common.js +69 -14
- package/src/actions/guides/legends/continuous/gradient.js +111 -83
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/interval.js +71 -92
- package/src/actions/guides/legends/continuous/opacity.js +105 -84
- package/src/actions/guides/legends/creation.js +27 -0
- package/src/actions/guides/legends/edit.js +198 -66
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/lane.js +98 -86
- package/src/actions/guides/legends/lifecycle.js +132 -0
- package/src/actions/guides/legends/remove.js +31 -76
- package/src/actions/guides/legends/size.js +149 -105
- package/src/actions/guides/legends/strokeWidth.js +106 -64
- package/src/actions/guides/legends/target.js +12 -0
- package/src/actions/guides/legends/transition.js +43 -0
- package/src/actions/guides/polar/axes/facade.js +77 -62
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/lines.js +2 -1
- package/src/actions/guides/polar/axes/shared.js +6 -0
- package/src/actions/guides/polar/axes/ticks.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/grids.js +7 -7
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +1 -1
- package/src/actions/marks/area/actions.js +18 -4
- package/src/actions/marks/area/materialize.js +5 -2
- package/src/actions/marks/bar/materialize.js +11 -7
- package/src/actions/marks/index.js +5 -0
- package/src/actions/marks/line/actions.js +43 -108
- package/src/actions/marks/line/materialize.js +34 -52
- package/src/actions/marks/point/edit.js +11 -15
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +106 -2
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/references.js +87 -0
- package/src/actions/marks/remove.js +17 -4
- package/src/actions/marks/rule/actions.js +34 -3
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +12 -12
- package/src/actions/marks/text/actions.js +224 -6
- package/src/actions/marks/text/layout.js +2 -2
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticAction.js +39 -2
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/layer.js +18 -1
- package/src/actions/primitives/semanticValidation/scale.js +10 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +19 -6
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/scales/consumers/common.js +20 -53
- package/src/actions/scales/consumers/families.js +27 -14
- package/src/actions/scales/consumers/index.js +23 -2
- package/src/actions/scales/consumers/seriesLayout.js +33 -2
- package/src/actions/scales/create.js +4 -4
- package/src/actions/scales/definitions.js +19 -3
- package/src/actions/scales/edit.js +46 -34
- package/src/actions/scales/editPolicy.js +21 -31
- package/src/actions/scales/materialize.js +10 -46
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +39 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/theme/actions.js +43 -0
- package/src/actions/theme/index.js +9 -0
- package/src/actions/theme/reconcile.js +882 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +86 -42
- package/src/actions/violinPlots/edit.js +191 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +22 -2
- package/src/core/compositionState.js +136 -8
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +58 -1
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +33 -12
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +181 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/computed.js +150 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/index.js +117 -6
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabels.js +93 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +91 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +7 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +40 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/rules.js +7 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +38 -0
- package/src/grammar/scales/definition.js +15 -1
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/semanticPath.js +17 -3
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/summary.js +183 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +21 -11
- package/src/grammar/transformTopology.js +6 -0
- package/src/grammar/transforms.js +36 -0
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/window.js +13 -7
- package/src/layout/facets.js +41 -7
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +38 -152
- package/src/materialization/bars/histogram.js +24 -0
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/dependencies.js +57 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facets.js +8 -1
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +2 -2
- package/src/materialization/legends.js +10 -1
- package/src/materialization/marks/capabilities.js +20 -5
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +58 -23
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +13 -13
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +1 -0
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +46 -14
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/bar.js +12 -68
- package/src/materialization/selection/items/common.js +8 -2
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +7 -4
- package/src/materialization/text.js +64 -5
- package/src/mcp/adapter.js +2 -2
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/layers.js +3 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +39 -0
- package/types/basic.d.ts +11 -2
- package/types/index.d.ts +126 -0
- package/types/program.d.ts +1320 -135
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -1,15 +1,9 @@
|
|
|
1
|
+
import { readSeriesIdentity } from "../pathSeries.js";
|
|
1
2
|
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
3
|
import { readNominalField, readTemporalField } from "../scales/index.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
validateAggregateFieldValues,
|
|
7
|
-
} from "../aggregate.js";
|
|
8
|
-
import {
|
|
9
|
-
BAR_GRAINS,
|
|
10
|
-
resolveBarChannels,
|
|
11
|
-
resolveBarGrain
|
|
12
|
-
} from "./policy.js";
|
|
4
|
+
import { aggregateRows, validateAggregateFieldType, validateAggregateFieldValues } from "../aggregate.js";
|
|
5
|
+
import { BAR_GRAINS, resolveBarChannels, resolveBarGrain, resolveBarColorLayout } from "./policy.js";
|
|
6
|
+
import { layoutSeriesPartition } from "../seriesLayout.js";
|
|
13
7
|
|
|
14
8
|
function requireAggregateBarEncoding(layer) {
|
|
15
9
|
if (layer?.mark?.type !== "bar") {
|
|
@@ -33,7 +27,7 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
33
27
|
const { channels, category, measure } = requireAggregateBarEncoding(layer);
|
|
34
28
|
validateAggregateFieldValues(rows, measure.field, measure.fieldType);
|
|
35
29
|
const categoryValues = category.fieldType === "temporal"
|
|
36
|
-
? readTemporalField(rows, category.field)
|
|
30
|
+
? readTemporalField(rows, category.field, category.temporalUnit)
|
|
37
31
|
: readNominalField(rows, category.field);
|
|
38
32
|
const color = layer.encoding?.color;
|
|
39
33
|
let colorValues;
|
|
@@ -52,17 +46,23 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
52
46
|
);
|
|
53
47
|
}
|
|
54
48
|
}
|
|
49
|
+
const identity = layer.encoding?.group === undefined ? undefined : readSeriesIdentity(rows, layer);
|
|
55
50
|
const groups = new Map();
|
|
56
51
|
|
|
57
52
|
for (let index = 0; index < rows.length; index += 1) {
|
|
58
53
|
const categoryValue = categoryValues[index];
|
|
59
54
|
const colorValue = colorValues?.[index];
|
|
60
|
-
const
|
|
55
|
+
const seriesValue = identity?.values[index];
|
|
56
|
+
const key = JSON.stringify([categoryValue, identity === undefined ? colorValue : seriesValue]);
|
|
61
57
|
const group = groups.get(key) ?? {
|
|
62
58
|
category: categoryValue,
|
|
59
|
+
...(identity === undefined ? {} : { series: seriesValue }),
|
|
63
60
|
...(colorValues === undefined ? {} : { color: colorValue }),
|
|
64
61
|
rows: []
|
|
65
62
|
};
|
|
63
|
+
if (identity !== undefined && colorValues !== undefined && group.color !== colorValue) {
|
|
64
|
+
throw new Error("Bar color requires one categorical value within each aggregate cell.");
|
|
65
|
+
}
|
|
66
66
|
group.rows.push(rows[index]);
|
|
67
67
|
groups.set(key, group);
|
|
68
68
|
}
|
|
@@ -82,6 +82,7 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
82
82
|
x: channels.category === "x" ? group.category : value,
|
|
83
83
|
y: channels.category === "y" ? group.category : value,
|
|
84
84
|
...(color === undefined ? {} : { color: aggregateColor }),
|
|
85
|
+
...(identity === undefined ? {} : { series: group.series }),
|
|
85
86
|
count: group.rows.length
|
|
86
87
|
}];
|
|
87
88
|
});
|
|
@@ -96,3 +97,23 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
96
97
|
values
|
|
97
98
|
});
|
|
98
99
|
}
|
|
100
|
+
|
|
101
|
+
export function deriveSeriesBarCells(rows, layer, categoryDomain, seriesDomain) {
|
|
102
|
+
const channels = resolveBarChannels(layer);
|
|
103
|
+
const values = deriveBarAggregates(rows, layer).values;
|
|
104
|
+
const identity = readSeriesIdentity(rows, layer);
|
|
105
|
+
const categories = categoryDomain ?? [...new Set(values.map(cell => cell[channels.category]))].sort((a, b) => a - b);
|
|
106
|
+
const seriesKey = cell => layer.encoding.group !== undefined ? cell.series : layer.encoding.color?.fieldType === "quantitative" ? undefined : cell.color;
|
|
107
|
+
const series = seriesDomain ?? (layer.encoding.group !== undefined ? identity.domain : [...new Set(values.map(seriesKey))]);
|
|
108
|
+
const cells = new Map(values.map(cell => [JSON.stringify([cell[channels.category], seriesKey(cell)]), cell]));
|
|
109
|
+
const layout = resolveBarColorLayout(layer);
|
|
110
|
+
return categories.flatMap(category => {
|
|
111
|
+
const partition = series.map(key => cells.get(JSON.stringify([category, key])));
|
|
112
|
+
const segments = layout === "group"
|
|
113
|
+
? partition.flatMap((cell, index) => cell === undefined ? [] : [{ index, start: 0, end: cell[channels.measure] }])
|
|
114
|
+
: layoutSeriesPartition(partition.map(cell => cell?.[channels.measure] ?? 0), layout);
|
|
115
|
+
return segments.flatMap(segment => partition[segment.index] === undefined ? [] : [{
|
|
116
|
+
cell: partition[segment.index], start: segment.start, end: segment.end, seriesIndex: segment.index
|
|
117
|
+
}]);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -11,7 +11,7 @@ export const BAR_ORIENTATIONS = Object.freeze({
|
|
|
11
11
|
horizontal: "horizontal"
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
function
|
|
14
|
+
export function isBarCategoryEncoding(encoding) {
|
|
15
15
|
return ["nominal", "ordinal", "temporal"].includes(encoding?.fieldType);
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -27,14 +27,14 @@ export function resolveBarOrientation(layer) {
|
|
|
27
27
|
if (x?.bin !== undefined && y?.aggregate === "count") {
|
|
28
28
|
return BAR_ORIENTATIONS.vertical;
|
|
29
29
|
}
|
|
30
|
-
if (
|
|
30
|
+
if (isBarCategoryEncoding(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") {
|
|
31
31
|
return BAR_ORIENTATIONS.vertical;
|
|
32
32
|
}
|
|
33
|
-
if (
|
|
33
|
+
if (isBarCategoryEncoding(y) && x?.fieldType === "quantitative" && layer.encoding?.x2?.fieldType === "quantitative") {
|
|
34
34
|
return BAR_ORIENTATIONS.horizontal;
|
|
35
35
|
}
|
|
36
|
-
if (
|
|
37
|
-
if (isMeasure(x) &&
|
|
36
|
+
if (isBarCategoryEncoding(x) && isMeasure(y)) return BAR_ORIENTATIONS.vertical;
|
|
37
|
+
if (isMeasure(x) && isBarCategoryEncoding(y)) return BAR_ORIENTATIONS.horizontal;
|
|
38
38
|
return undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -63,8 +63,8 @@ export function resolveBarGrain(layer) {
|
|
|
63
63
|
return BAR_GRAINS.histogram;
|
|
64
64
|
}
|
|
65
65
|
if (
|
|
66
|
-
(
|
|
67
|
-
(
|
|
66
|
+
(isBarCategoryEncoding(x) && y?.fieldType === "quantitative" && layer.encoding?.y2?.fieldType === "quantitative") ||
|
|
67
|
+
(isBarCategoryEncoding(y) && x?.fieldType === "quantitative" && layer.encoding?.x2?.fieldType === "quantitative")
|
|
68
68
|
) return BAR_GRAINS.ranged;
|
|
69
69
|
if (resolveBarOrientation(layer) !== undefined) {
|
|
70
70
|
return BAR_GRAINS.aggregate;
|
|
@@ -73,6 +73,7 @@ export function resolveBarGrain(layer) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export function inferBarColorLayout(layer) {
|
|
76
|
+
if (layer?.layout?.mode !== undefined) return layer.layout.mode;
|
|
76
77
|
if (layer?.encoding?.color?.layout !== undefined) {
|
|
77
78
|
return layer.encoding.color.layout;
|
|
78
79
|
}
|
|
@@ -84,6 +85,7 @@ export function inferBarColorLayout(layer) {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export function resolveBarColorLayout(layer) {
|
|
88
|
+
if (layer?.layout?.mode !== undefined) return layer.layout.mode;
|
|
87
89
|
if (layer?.encoding?.color?.layout !== undefined) {
|
|
88
90
|
return layer.encoding.color.layout;
|
|
89
91
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
findHistogramBinIndex,
|
|
4
|
+
normalizeHistogramBin,
|
|
5
|
+
resolveHistogramBins
|
|
6
|
+
} from "./histogram.js";
|
|
7
|
+
|
|
8
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
9
|
+
"type", "field", "bin", "extent", "nice", "zero", "includeEmpty",
|
|
10
|
+
"members", "as", "resolved"
|
|
11
|
+
]);
|
|
12
|
+
const AS_KEYS = Object.freeze(["lower", "upper", "count", "members"]);
|
|
13
|
+
const RESOLVED_KEYS = Object.freeze(["domain", "step", "boundaries"]);
|
|
14
|
+
|
|
15
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
16
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
17
|
+
if (unknown !== undefined) {
|
|
18
|
+
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireField(value, label) {
|
|
23
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
24
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeAs(field, value, members) {
|
|
30
|
+
const requested = value ?? {};
|
|
31
|
+
if (!isPlainObject(requested)) return requested;
|
|
32
|
+
const result = {
|
|
33
|
+
...requested,
|
|
34
|
+
lower: requested.lower ?? `${field}_start`,
|
|
35
|
+
upper: requested.upper ?? `${field}_end`,
|
|
36
|
+
count: requested.count ?? "count"
|
|
37
|
+
};
|
|
38
|
+
if (members) result.members = requested.members ?? "members";
|
|
39
|
+
else if (Object.hasOwn(requested, "members")) result.members = requested.members;
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeBinTransform({
|
|
44
|
+
field,
|
|
45
|
+
maxBins,
|
|
46
|
+
step,
|
|
47
|
+
boundaries,
|
|
48
|
+
extent,
|
|
49
|
+
nice,
|
|
50
|
+
zero,
|
|
51
|
+
includeEmpty,
|
|
52
|
+
members,
|
|
53
|
+
as
|
|
54
|
+
} = {}) {
|
|
55
|
+
const selected = [maxBins, step, boundaries].filter(value => value !== undefined);
|
|
56
|
+
if (selected.length > 1) {
|
|
57
|
+
throw new Error("Bin data accepts only one of maxBins, step, or boundaries.");
|
|
58
|
+
}
|
|
59
|
+
const bin = normalizeHistogramBin(
|
|
60
|
+
boundaries !== undefined ? { boundaries }
|
|
61
|
+
: step !== undefined ? { step }
|
|
62
|
+
: { maxBins: maxBins ?? 10 }
|
|
63
|
+
);
|
|
64
|
+
const resolvedMembers = members ?? false;
|
|
65
|
+
const transform = {
|
|
66
|
+
type: "bin",
|
|
67
|
+
field,
|
|
68
|
+
bin,
|
|
69
|
+
extent: extent ?? "auto",
|
|
70
|
+
nice: nice ?? true,
|
|
71
|
+
zero: zero ?? false,
|
|
72
|
+
includeEmpty: includeEmpty ?? true,
|
|
73
|
+
members: resolvedMembers,
|
|
74
|
+
as: normalizeAs(field, as, resolvedMembers)
|
|
75
|
+
};
|
|
76
|
+
validateBinTransform(transform);
|
|
77
|
+
return cloneAndFreeze(transform);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function validateBinTransform(transform) {
|
|
81
|
+
if (!isPlainObject(transform)) {
|
|
82
|
+
throw new TypeError("Bin transform must be a plain object.");
|
|
83
|
+
}
|
|
84
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "bin transform");
|
|
85
|
+
if (transform.type !== "bin") {
|
|
86
|
+
throw new Error(`Unsupported bin transform "${transform.type}".`);
|
|
87
|
+
}
|
|
88
|
+
requireField(transform.field, "Bin field");
|
|
89
|
+
normalizeHistogramBin(transform.bin);
|
|
90
|
+
if (
|
|
91
|
+
transform.extent !== "auto" &&
|
|
92
|
+
(!Array.isArray(transform.extent) || transform.extent.length !== 2 ||
|
|
93
|
+
!transform.extent.every(Number.isFinite) || transform.extent[0] >= transform.extent[1])
|
|
94
|
+
) {
|
|
95
|
+
throw new TypeError("Bin extent must be auto or two ascending finite numbers.");
|
|
96
|
+
}
|
|
97
|
+
for (const property of ["nice", "zero", "includeEmpty", "members"]) {
|
|
98
|
+
if (typeof transform[property] !== "boolean") {
|
|
99
|
+
throw new TypeError(`Bin ${property} must be a boolean.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!isPlainObject(transform.as)) {
|
|
103
|
+
throw new TypeError("Bin as must be a plain object.");
|
|
104
|
+
}
|
|
105
|
+
rejectUnknownKeys(transform.as, AS_KEYS, "bin as");
|
|
106
|
+
const required = ["lower", "upper", "count", ...(transform.members ? ["members"] : [])];
|
|
107
|
+
if (!transform.members && Object.hasOwn(transform.as, "members")) {
|
|
108
|
+
throw new Error("Bin as.members requires members: true.");
|
|
109
|
+
}
|
|
110
|
+
const outputs = required.map(key => requireField(transform.as[key], `Bin as.${key}`));
|
|
111
|
+
if (new Set(outputs).size !== outputs.length) {
|
|
112
|
+
throw new Error("Bin output fields must be unique.");
|
|
113
|
+
}
|
|
114
|
+
if (transform.resolved !== undefined) {
|
|
115
|
+
if (!isPlainObject(transform.resolved)) {
|
|
116
|
+
throw new TypeError("Bin resolved must be a plain object.");
|
|
117
|
+
}
|
|
118
|
+
rejectUnknownKeys(transform.resolved, RESOLVED_KEYS, "bin resolved");
|
|
119
|
+
resolveHistogramBins({
|
|
120
|
+
values: transform.resolved.boundaries ?? [],
|
|
121
|
+
bin: { boundaries: transform.resolved.boundaries },
|
|
122
|
+
domain: transform.resolved.domain,
|
|
123
|
+
nice: transform.nice,
|
|
124
|
+
zero: transform.zero
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return transform;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readValues(rows, field) {
|
|
131
|
+
return rows.map((row, index) => {
|
|
132
|
+
const value = row[field];
|
|
133
|
+
if (!Number.isFinite(value)) {
|
|
134
|
+
throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function deriveBinRows(rows, transform) {
|
|
141
|
+
validateBinTransform(transform);
|
|
142
|
+
const values = readValues(rows, transform.field);
|
|
143
|
+
if (
|
|
144
|
+
transform.extent !== "auto" &&
|
|
145
|
+
values.some(value => value < transform.extent[0] || value > transform.extent[1])
|
|
146
|
+
) {
|
|
147
|
+
throw new RangeError("Bin extent must contain the histogram data extent.");
|
|
148
|
+
}
|
|
149
|
+
const resolved = resolveHistogramBins({
|
|
150
|
+
values,
|
|
151
|
+
bin: transform.bin,
|
|
152
|
+
domain: transform.extent,
|
|
153
|
+
nice: transform.nice,
|
|
154
|
+
zero: transform.zero
|
|
155
|
+
});
|
|
156
|
+
const members = resolved.boundaries.slice(0, -1).map(() => []);
|
|
157
|
+
values.forEach((value, index) => {
|
|
158
|
+
const bin = findHistogramBinIndex(value, resolved.boundaries);
|
|
159
|
+
if (bin !== -1) members[bin].push(rows[index]);
|
|
160
|
+
});
|
|
161
|
+
const output = members.flatMap((group, index) =>
|
|
162
|
+
!transform.includeEmpty && group.length === 0 ? [] : [{
|
|
163
|
+
[transform.as.lower]: resolved.boundaries[index],
|
|
164
|
+
[transform.as.upper]: resolved.boundaries[index + 1],
|
|
165
|
+
[transform.as.count]: group.length,
|
|
166
|
+
...(transform.members ? { [transform.as.members]: group } : {})
|
|
167
|
+
}]
|
|
168
|
+
);
|
|
169
|
+
return {
|
|
170
|
+
values: output,
|
|
171
|
+
resolved: {
|
|
172
|
+
domain: resolved.domain,
|
|
173
|
+
...(resolved.step === undefined ? {} : { step: resolved.step }),
|
|
174
|
+
boundaries: resolved.boundaries
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function resolveBinTransform(result, transform) {
|
|
180
|
+
return cloneAndFreeze([{ ...transform, resolved: result.resolved }]);
|
|
181
|
+
}
|
|
@@ -2,6 +2,8 @@ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
|
2
2
|
import { isNominalValue, readNominalField } from "./scales/fields.js";
|
|
3
3
|
import { aggregateScalarValues } from "./aggregate.js";
|
|
4
4
|
|
|
5
|
+
export const CATEGORY_ORDER_CHANNELS = Object.freeze(["x", "y", "theta"]);
|
|
6
|
+
|
|
5
7
|
const DIRECTIONS = ["ascending", "descending"];
|
|
6
8
|
const AGGREGATES = ["sum", "mean", "min", "max"];
|
|
7
9
|
|
|
@@ -130,3 +132,35 @@ export function resolveCategoryOrder(rows, categoryField, order) {
|
|
|
130
132
|
return direction * comparison || first.get(left) - first.get(right);
|
|
131
133
|
}));
|
|
132
134
|
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
export function normalizeLegendOrder(order) {
|
|
138
|
+
if (order === "scale") return order;
|
|
139
|
+
if (!isPlainObject(order)) {
|
|
140
|
+
throw new TypeError('Legend order must be "scale", { values }, or { channel }.');
|
|
141
|
+
}
|
|
142
|
+
const keys = Object.keys(order);
|
|
143
|
+
if (keys.length !== 1 || !["values", "channel"].includes(keys[0])) {
|
|
144
|
+
throw new Error("Legend order requires exactly one of values or channel.");
|
|
145
|
+
}
|
|
146
|
+
if (keys[0] === "values") return normalizeCategoryOrder({ values: order.values });
|
|
147
|
+
if (!CATEGORY_ORDER_CHANNELS.includes(order.channel)) {
|
|
148
|
+
throw new Error("Legend order channel must be x, y, or theta.");
|
|
149
|
+
}
|
|
150
|
+
return cloneAndFreeze({ channel: order.channel });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function resolveLegendOrderDomain(domain, order, linked, observed = domain) {
|
|
154
|
+
if (order === undefined || order === "scale") return domain;
|
|
155
|
+
const normalized = normalizeLegendOrder(order);
|
|
156
|
+
if (normalized.values !== undefined) {
|
|
157
|
+
const seen = [...new Set(observed)].filter(value => domain.some(item => Object.is(item, value)));
|
|
158
|
+
const available = [...seen, ...domain.filter(value => !seen.some(item => Object.is(item, value)))];
|
|
159
|
+
return resolveCategoryOrder(available.map(value => ({ value })), "value", normalized);
|
|
160
|
+
}
|
|
161
|
+
if (!Array.isArray(linked) || linked.length !== domain.length ||
|
|
162
|
+
!domain.every(value => linked.some(other => Object.is(value, other)))) {
|
|
163
|
+
throw new Error("Linked legend order requires the same categorical domain.");
|
|
164
|
+
}
|
|
165
|
+
return cloneAndFreeze(linked);
|
|
166
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const TRANSFORM_KEYS = Object.freeze(["type", "as", "expression"]);
|
|
4
|
+
const BINARY_OPERATIONS = new Set(["add", "subtract", "multiply", "divide"]);
|
|
5
|
+
const UNARY_OPERATIONS = new Set(["negate", "absolute"]);
|
|
6
|
+
const MAX_DEPTH = 16;
|
|
7
|
+
const MAX_NODES = 128;
|
|
8
|
+
const MAX_WORK = 10_000_000;
|
|
9
|
+
|
|
10
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
11
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
12
|
+
if (unknown !== undefined) {
|
|
13
|
+
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
14
|
+
}
|
|
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
|
+
function validateExpression(expression, depth = 1, state = { nodes: 0 }) {
|
|
25
|
+
if (!isPlainObject(expression)) {
|
|
26
|
+
throw new TypeError("Computed expression nodes must be plain objects.");
|
|
27
|
+
}
|
|
28
|
+
state.nodes += 1;
|
|
29
|
+
if (state.nodes > MAX_NODES) {
|
|
30
|
+
throw new RangeError(`Computed expression cannot exceed ${MAX_NODES} nodes.`);
|
|
31
|
+
}
|
|
32
|
+
if (depth > MAX_DEPTH) {
|
|
33
|
+
throw new RangeError(`Computed expression cannot exceed depth ${MAX_DEPTH}.`);
|
|
34
|
+
}
|
|
35
|
+
const kinds = ["field", "constant", "op"].filter(key => Object.hasOwn(expression, key));
|
|
36
|
+
if (kinds.length !== 1) {
|
|
37
|
+
throw new Error("Computed expression requires exactly one of field, constant, or op.");
|
|
38
|
+
}
|
|
39
|
+
if (kinds[0] === "field") {
|
|
40
|
+
rejectUnknownKeys(expression, ["field"], "computed field expression");
|
|
41
|
+
requireField(expression.field, "Computed expression field");
|
|
42
|
+
} else if (kinds[0] === "constant") {
|
|
43
|
+
rejectUnknownKeys(expression, ["constant"], "computed constant expression");
|
|
44
|
+
if (!Number.isFinite(expression.constant)) {
|
|
45
|
+
throw new TypeError("Computed expression constant must be a finite number.");
|
|
46
|
+
}
|
|
47
|
+
} else if (BINARY_OPERATIONS.has(expression.op)) {
|
|
48
|
+
rejectUnknownKeys(expression, ["op", "left", "right"], "computed binary expression");
|
|
49
|
+
if (!Object.hasOwn(expression, "left") || !Object.hasOwn(expression, "right")) {
|
|
50
|
+
throw new TypeError(`Computed ${expression.op} requires left and right expressions.`);
|
|
51
|
+
}
|
|
52
|
+
validateExpression(expression.left, depth + 1, state);
|
|
53
|
+
validateExpression(expression.right, depth + 1, state);
|
|
54
|
+
} else if (UNARY_OPERATIONS.has(expression.op)) {
|
|
55
|
+
rejectUnknownKeys(expression, ["op", "operand"], "computed unary expression");
|
|
56
|
+
if (!Object.hasOwn(expression, "operand")) {
|
|
57
|
+
throw new TypeError(`Computed ${expression.op} requires an operand expression.`);
|
|
58
|
+
}
|
|
59
|
+
validateExpression(expression.operand, depth + 1, state);
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error(`Unsupported computed operation "${expression.op}".`);
|
|
62
|
+
}
|
|
63
|
+
return state.nodes;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeExpression(expression) {
|
|
67
|
+
if (!isPlainObject(expression)) return expression;
|
|
68
|
+
if (Object.hasOwn(expression, "left") || Object.hasOwn(expression, "right")) {
|
|
69
|
+
return {
|
|
70
|
+
...expression,
|
|
71
|
+
left: normalizeExpression(expression.left),
|
|
72
|
+
right: normalizeExpression(expression.right)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (Object.hasOwn(expression, "operand")) {
|
|
76
|
+
return { ...expression, operand: normalizeExpression(expression.operand) };
|
|
77
|
+
}
|
|
78
|
+
return { ...expression };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function normalizeComputedTransform({ as, expression } = {}) {
|
|
82
|
+
const transform = {
|
|
83
|
+
type: "computed",
|
|
84
|
+
as,
|
|
85
|
+
expression: normalizeExpression(expression)
|
|
86
|
+
};
|
|
87
|
+
validateComputedTransform(transform);
|
|
88
|
+
return cloneAndFreeze(transform);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function validateComputedTransform(transform) {
|
|
92
|
+
if (!isPlainObject(transform)) {
|
|
93
|
+
throw new TypeError("Computed transform must be a plain object.");
|
|
94
|
+
}
|
|
95
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "computed transform");
|
|
96
|
+
if (transform.type !== "computed") {
|
|
97
|
+
throw new Error(`Unsupported computed transform "${transform.type}".`);
|
|
98
|
+
}
|
|
99
|
+
requireField(transform.as, "Computed output field");
|
|
100
|
+
return validateExpression(transform.expression);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function evaluate(expression, row, rowIndex) {
|
|
104
|
+
if (Object.hasOwn(expression, "field")) {
|
|
105
|
+
if (!Object.hasOwn(row, expression.field)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Computed source does not contain field "${expression.field}" at row ${rowIndex}.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const value = row[expression.field];
|
|
111
|
+
if (!Number.isFinite(value)) {
|
|
112
|
+
throw new TypeError(
|
|
113
|
+
`Computed field "${expression.field}" must contain a finite number at row ${rowIndex}.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
if (Object.hasOwn(expression, "constant")) return expression.constant;
|
|
119
|
+
if (UNARY_OPERATIONS.has(expression.op)) {
|
|
120
|
+
const value = evaluate(expression.operand, row, rowIndex);
|
|
121
|
+
return expression.op === "negate" ? -value : Math.abs(value);
|
|
122
|
+
}
|
|
123
|
+
const left = evaluate(expression.left, row, rowIndex);
|
|
124
|
+
const right = evaluate(expression.right, row, rowIndex);
|
|
125
|
+
if (expression.op === "divide" && right === 0) {
|
|
126
|
+
throw new RangeError(`Computed divide by zero at row ${rowIndex}.`);
|
|
127
|
+
}
|
|
128
|
+
const result = expression.op === "add" ? left + right
|
|
129
|
+
: expression.op === "subtract" ? left - right
|
|
130
|
+
: expression.op === "multiply" ? left * right
|
|
131
|
+
: left / right;
|
|
132
|
+
if (!Number.isFinite(result)) {
|
|
133
|
+
throw new RangeError(`Computed ${expression.op} result is not finite at row ${rowIndex}.`);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function deriveComputedRows(rows, transform) {
|
|
139
|
+
const nodes = validateComputedTransform(transform);
|
|
140
|
+
if (rows.length * nodes > MAX_WORK) {
|
|
141
|
+
throw new RangeError(`Computed work cannot exceed ${MAX_WORK} row-nodes.`);
|
|
142
|
+
}
|
|
143
|
+
if (rows.some(row => Object.hasOwn(row, transform.as))) {
|
|
144
|
+
throw new Error(`Computed output field "${transform.as}" already exists.`);
|
|
145
|
+
}
|
|
146
|
+
return rows.map((row, index) => ({
|
|
147
|
+
...row,
|
|
148
|
+
[transform.as]: evaluate(transform.expression, row, index)
|
|
149
|
+
}));
|
|
150
|
+
}
|