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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateNonNegativeFinite } from "../../core/validation.js";
|
|
3
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
4
|
+
import {
|
|
5
|
+
applyFacadeGuides,
|
|
6
|
+
normalizeAppearance,
|
|
7
|
+
normalizeEncoding,
|
|
8
|
+
normalizeFieldEncoding,
|
|
9
|
+
normalizeGuides,
|
|
10
|
+
resolveFacadeData,
|
|
11
|
+
resolveFacadeId,
|
|
12
|
+
targetArgs,
|
|
13
|
+
validateFacadeOptions
|
|
14
|
+
} from "./shared.js";
|
|
15
|
+
|
|
16
|
+
const INTERVAL_OPTIONS = Object.freeze([
|
|
17
|
+
"id", "data", "coordinate", "x", "y", "xOffset", "yOffset", "groupBy",
|
|
18
|
+
"color", "point", "errorBar", "guides"
|
|
19
|
+
]);
|
|
20
|
+
const POINT_OPTIONS = Object.freeze([
|
|
21
|
+
"shape", "fill", "opacity", "stroke", "strokeWidth", "radius"
|
|
22
|
+
]);
|
|
23
|
+
const ERROR_BAR_OPTIONS = Object.freeze([
|
|
24
|
+
"caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function intervalChannel(value, operation, channel, id) {
|
|
28
|
+
const encoding = normalizeFieldEncoding(value, `${operation} ${channel}`);
|
|
29
|
+
return {
|
|
30
|
+
...encoding,
|
|
31
|
+
scale: {
|
|
32
|
+
...(encoding.scale ?? {}),
|
|
33
|
+
id: encoding.scale?.id ?? `${id}${channel.toUpperCase()}`
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function centerField(program, config) {
|
|
39
|
+
if (config.intervalMode === "explicit") return config.centerField;
|
|
40
|
+
const dataset = findDataset(program, config.data);
|
|
41
|
+
const transform = dataset?.transform?.[0];
|
|
42
|
+
if (transform?.type !== "interval") {
|
|
43
|
+
throw new Error(`Interval plot requires interval provenance for dataset "${config.data}".`);
|
|
44
|
+
}
|
|
45
|
+
return transform.as.center;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function applyIntervalPointPositions(program, id, config) {
|
|
49
|
+
const vertical = config.orientation === "vertical";
|
|
50
|
+
const positionChannel = vertical ? "x" : "y";
|
|
51
|
+
const intervalChannelName = vertical ? "y" : "x";
|
|
52
|
+
let next = program[positionChannel === "x" ? "encodeX" : "encodeY"]({
|
|
53
|
+
target: id,
|
|
54
|
+
field: config.positionField,
|
|
55
|
+
fieldType: config.positionFieldType,
|
|
56
|
+
...(config.positionTemporalUnit === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { temporalUnit: config.positionTemporalUnit }),
|
|
59
|
+
coordinate: config.coordinate,
|
|
60
|
+
scale: { id: config.positionScale }
|
|
61
|
+
});
|
|
62
|
+
next = next[intervalChannelName === "x" ? "encodeX" : "encodeY"]({
|
|
63
|
+
target: id,
|
|
64
|
+
field: centerField(next, config),
|
|
65
|
+
fieldType: "quantitative",
|
|
66
|
+
coordinate: config.coordinate,
|
|
67
|
+
scale: { id: config.intervalScale }
|
|
68
|
+
});
|
|
69
|
+
if (config.offset !== undefined) {
|
|
70
|
+
next = next[config.offset.channel === "xOffset" ? "encodeXOffset" : "encodeYOffset"]({
|
|
71
|
+
target: id,
|
|
72
|
+
field: config.offset.field,
|
|
73
|
+
fieldType: config.offset.fieldType,
|
|
74
|
+
scale: { id: config.offset.scale },
|
|
75
|
+
paddingInner: config.offset.paddingInner,
|
|
76
|
+
paddingOuter: config.offset.paddingOuter
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const createIntervalPlot = action(
|
|
83
|
+
{
|
|
84
|
+
op: "createIntervalPlot",
|
|
85
|
+
description: "Create a center point and matching statistical or explicit interval."
|
|
86
|
+
},
|
|
87
|
+
function (args = {}) {
|
|
88
|
+
const operation = "createIntervalPlot";
|
|
89
|
+
validateFacadeOptions(args, INTERVAL_OPTIONS, operation);
|
|
90
|
+
const id = resolveFacadeId(this, args.id, { defaultId: "intervalPlot", operation });
|
|
91
|
+
const data = resolveFacadeData(this, args.data, operation);
|
|
92
|
+
const x = intervalChannel(args.x, operation, "x", id);
|
|
93
|
+
const y = intervalChannel(args.y, operation, "y", id);
|
|
94
|
+
const point = normalizeAppearance(args.point, POINT_OPTIONS, `${operation} point`);
|
|
95
|
+
const { radius, ...pointAppearance } = point;
|
|
96
|
+
if (radius !== undefined) validateNonNegativeFinite(radius, `${operation} point radius`);
|
|
97
|
+
const errorBar = normalizeAppearance(
|
|
98
|
+
args.errorBar,
|
|
99
|
+
ERROR_BAR_OPTIONS,
|
|
100
|
+
`${operation} errorBar`
|
|
101
|
+
);
|
|
102
|
+
const color = normalizeEncoding(args.color, `${operation} color`);
|
|
103
|
+
const guides = normalizeGuides(args.guides, operation);
|
|
104
|
+
const intervalId = `${id}Interval`;
|
|
105
|
+
|
|
106
|
+
let next = this.createErrorBar({
|
|
107
|
+
id: intervalId,
|
|
108
|
+
data,
|
|
109
|
+
x,
|
|
110
|
+
y,
|
|
111
|
+
...(args.xOffset === undefined ? {} : { xOffset: args.xOffset }),
|
|
112
|
+
...(args.yOffset === undefined ? {} : { yOffset: args.yOffset }),
|
|
113
|
+
...(Object.hasOwn(args, "groupBy") ? { groupBy: args.groupBy } : {}),
|
|
114
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
115
|
+
...errorBar
|
|
116
|
+
});
|
|
117
|
+
const config = next.markConfigs[intervalId]?.errorBar;
|
|
118
|
+
if (config === undefined) {
|
|
119
|
+
throw new Error(`Interval plot child "${intervalId}" has no interval owner configuration.`);
|
|
120
|
+
}
|
|
121
|
+
next = next.createPointMark({ id, data: config.data, ...pointAppearance });
|
|
122
|
+
next = applyIntervalPointPositions(next, id, config);
|
|
123
|
+
if (radius !== undefined) next = next.encodePointRadius({ target: id, value: radius });
|
|
124
|
+
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
125
|
+
next = next._withMarkConfig(id, {
|
|
126
|
+
...next.markConfigs[id],
|
|
127
|
+
intervalPlot: { intervalId, materialized: true }
|
|
128
|
+
});
|
|
129
|
+
return applyFacadeGuides(next, guides, id, guides);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const REGRESSION_OPTIONS = Object.freeze([
|
|
134
|
+
"id", "data", "coordinate", "x", "y", "color", "size", "shape", "point",
|
|
135
|
+
"groupBy", "method", "degree", "span", "confidenceMethod", "level", "confidence",
|
|
136
|
+
"interval", "band", "line", "guides"
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
export const createRegressionPlot = action(
|
|
140
|
+
{
|
|
141
|
+
op: "createRegressionPlot",
|
|
142
|
+
description: "Create a scatter plot with a fitted regression line and optional interval band."
|
|
143
|
+
},
|
|
144
|
+
function (args = {}) {
|
|
145
|
+
const operation = "createRegressionPlot";
|
|
146
|
+
validateFacadeOptions(args, REGRESSION_OPTIONS, operation);
|
|
147
|
+
const id = resolveFacadeId(this, args.id, { defaultId: "regressionPlot", operation });
|
|
148
|
+
const data = resolveFacadeData(this, args.data, operation);
|
|
149
|
+
const guides = normalizeGuides(args.guides, operation);
|
|
150
|
+
let next = this.createScatterPlot({
|
|
151
|
+
id,
|
|
152
|
+
data,
|
|
153
|
+
x: args.x,
|
|
154
|
+
y: args.y,
|
|
155
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
156
|
+
...(args.color === undefined ? {} : { color: args.color }),
|
|
157
|
+
...(args.size === undefined ? {} : { size: args.size }),
|
|
158
|
+
...(args.shape === undefined ? {} : { shape: args.shape }),
|
|
159
|
+
...(args.point === undefined ? {} : { point: args.point }),
|
|
160
|
+
guides: false
|
|
161
|
+
});
|
|
162
|
+
const regression = Object.fromEntries([
|
|
163
|
+
"groupBy", "method", "degree", "span", "confidenceMethod", "level",
|
|
164
|
+
"confidence", "interval", "band", "line"
|
|
165
|
+
].filter(key => Object.hasOwn(args, key)).map(key => [key, args[key]]));
|
|
166
|
+
next = next.createRegression({ target: id, ...regression });
|
|
167
|
+
return applyFacadeGuides(next, guides, id, guides);
|
|
168
|
+
}
|
|
169
|
+
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { action } from "../../core/action.js";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeGroupFields } from "../../grammar/pathSeries.js";
|
|
3
3
|
import {
|
|
4
4
|
applyFacadeGuides,
|
|
5
5
|
normalizeAppearance,
|
|
@@ -50,20 +50,20 @@ export const createLinePlot = action(
|
|
|
50
50
|
const strokeDash = normalizeStrokeDashEncoding(args.strokeDash);
|
|
51
51
|
const groupBy = args.groupBy === undefined
|
|
52
52
|
? undefined
|
|
53
|
-
:
|
|
53
|
+
: normalizeGroupFields(args.groupBy);
|
|
54
54
|
const guides = normalizeGuides(args.guides, "createLinePlot");
|
|
55
55
|
|
|
56
56
|
let next = this
|
|
57
57
|
.createLineMark({ id, data, ...line })
|
|
58
58
|
.encodeX(positionArgs(x, { target: id, coordinate: args.coordinate }))
|
|
59
59
|
.encodeY(positionArgs(y, { target: id, coordinate: args.coordinate }));
|
|
60
|
-
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
61
60
|
if (groupBy !== undefined) {
|
|
62
|
-
next = next.encodeGroup({
|
|
61
|
+
next = next.encodeGroup({ fields: groupBy, target: id });
|
|
63
62
|
}
|
|
63
|
+
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
64
64
|
if (strokeDash !== undefined) {
|
|
65
65
|
next = next.encodeStrokeDash(targetArgs(strokeDash, id));
|
|
66
66
|
}
|
|
67
|
-
return applyFacadeGuides(next, guides);
|
|
67
|
+
return applyFacadeGuides(next, guides, id);
|
|
68
68
|
}
|
|
69
69
|
);
|
|
@@ -105,6 +105,6 @@ export const createParallelCoordinates = action(
|
|
|
105
105
|
if (strokeDash !== undefined) {
|
|
106
106
|
next = next.encodeStrokeDash(targetArgs(strokeDash, id));
|
|
107
107
|
}
|
|
108
|
-
return applyFacadeGuides(next, guides);
|
|
108
|
+
return applyFacadeGuides(next, guides, id);
|
|
109
109
|
}
|
|
110
110
|
);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import {
|
|
3
|
+
applyFacadeGuides, normalizeAppearance, normalizeCategoricalColor, normalizeCategoricalGuides,
|
|
4
|
+
normalizeArcCategory, normalizeCategoryAggregate, omitUndefinedOptions,
|
|
5
|
+
resolveFacadeData, resolveFacadeId, validateFacadeOptions
|
|
6
|
+
} from "./shared.js";
|
|
7
|
+
|
|
8
|
+
const OPERATION = "createPiePlot";
|
|
9
|
+
const OPTIONS = ["id", "data", "coordinate", "category", "value", "aggregate", "color", "arc", "guides"];
|
|
10
|
+
const ARC_OPTIONS = ["innerRadius", "padAngle", "fill", "opacity", "stroke", "strokeWidth"];
|
|
11
|
+
function guideOptions(value, color) {
|
|
12
|
+
const guides = normalizeCategoricalGuides(value, OPERATION, color);
|
|
13
|
+
if (guides === false) return false;
|
|
14
|
+
for (const key of ["axes", "grid"]) {
|
|
15
|
+
if (guides[key] !== undefined && guides[key] !== false) {
|
|
16
|
+
throw new TypeError(`${OPERATION} guides.${key} only accepts false.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { ...guides, axes: false, grid: false };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const createPiePlot = action({
|
|
23
|
+
op: OPERATION, description: "Create a categorical count or weighted-sum pie or donut plot."
|
|
24
|
+
}, function (args = {}) {
|
|
25
|
+
validateFacadeOptions(args, OPTIONS, OPERATION);
|
|
26
|
+
const id = resolveFacadeId(this, args.id, { defaultId: "piePlot", operation: OPERATION });
|
|
27
|
+
const data = resolveFacadeData(this, args.data, OPERATION);
|
|
28
|
+
const category = normalizeArcCategory(args.category, OPERATION);
|
|
29
|
+
const aggregate = normalizeCategoryAggregate(args, OPERATION);
|
|
30
|
+
const arc = omitUndefinedOptions(normalizeAppearance(args.arc, ARC_OPTIONS, `${OPERATION} arc`));
|
|
31
|
+
const color = args.color === false ? undefined
|
|
32
|
+
: normalizeCategoricalColor(args.color === undefined ? category.field : args.color, `${OPERATION} color`);
|
|
33
|
+
if (color !== undefined && arc.fill !== undefined) {
|
|
34
|
+
throw new Error(`${OPERATION} arc.fill cannot be combined with color; use color:false.`);
|
|
35
|
+
}
|
|
36
|
+
const guides = guideOptions(args.guides, color);
|
|
37
|
+
let next = this.createArcMark({ id, data, ...arc }).encodeTheta({
|
|
38
|
+
...category, target: id, aggregate,
|
|
39
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
40
|
+
...(aggregate === "sum" ? { weight: args.value } : {})
|
|
41
|
+
});
|
|
42
|
+
if (color !== undefined) next = next.encodeColor({ ...color, target: id });
|
|
43
|
+
return applyFacadeGuides(next, guides, id, args.guides ?? {});
|
|
44
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateNonNegativeFinite, validateOptionObject } from
|
|
3
|
+
"../../core/validation.js";
|
|
4
|
+
import { normalizeGroupFields } from "../../grammar/pathSeries.js";
|
|
5
|
+
import {
|
|
6
|
+
applyFacadeGuides,
|
|
7
|
+
normalizeAppearance,
|
|
8
|
+
normalizeEncoding,
|
|
9
|
+
normalizeFieldEncoding,
|
|
10
|
+
normalizeGuides,
|
|
11
|
+
normalizeStrokeDashEncoding,
|
|
12
|
+
positionArgs,
|
|
13
|
+
resolveFacadeData,
|
|
14
|
+
resolveFacadeId,
|
|
15
|
+
targetArgs,
|
|
16
|
+
validateFacadeOptions
|
|
17
|
+
} from "./shared.js";
|
|
18
|
+
|
|
19
|
+
const POSITION_OPTIONS = Object.freeze([
|
|
20
|
+
"field", "fieldType", "temporalUnit", "scale"
|
|
21
|
+
]);
|
|
22
|
+
const POINT_OPTIONS = Object.freeze([
|
|
23
|
+
"shape", "fill", "opacity", "stroke", "strokeWidth", "radius"
|
|
24
|
+
]);
|
|
25
|
+
const LINE_OPTIONS = Object.freeze([
|
|
26
|
+
"strokeWidth", "curve", "stroke", "opacity", "closed"
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function position(value, operation, channel) {
|
|
30
|
+
const resolved = normalizeFieldEncoding(value, `${operation} ${channel}`);
|
|
31
|
+
validateOptionObject(
|
|
32
|
+
resolved,
|
|
33
|
+
channel === "theta" ? POSITION_OPTIONS : ["field", "fieldType", "scale"],
|
|
34
|
+
`${operation} ${channel}`
|
|
35
|
+
);
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const createPolarScatterPlot = action(
|
|
40
|
+
{
|
|
41
|
+
op: "createPolarScatterPlot",
|
|
42
|
+
description: "Create a Polar scatter plot with distinct radial position and glyph size."
|
|
43
|
+
},
|
|
44
|
+
function (args = {}) {
|
|
45
|
+
const operation = "createPolarScatterPlot";
|
|
46
|
+
validateFacadeOptions(args, [
|
|
47
|
+
"id", "data", "coordinate", "theta", "radius", "color", "size",
|
|
48
|
+
"shape", "point", "guides"
|
|
49
|
+
], operation);
|
|
50
|
+
const id = resolveFacadeId(this, args.id, {
|
|
51
|
+
defaultId: "polarScatterPlot",
|
|
52
|
+
operation
|
|
53
|
+
});
|
|
54
|
+
const data = resolveFacadeData(this, args.data, operation);
|
|
55
|
+
const theta = position(args.theta, operation, "theta");
|
|
56
|
+
const radiusPosition = position(args.radius, operation, "radius");
|
|
57
|
+
const color = normalizeEncoding(args.color, `${operation} color`);
|
|
58
|
+
const size = normalizeEncoding(args.size, `${operation} size`);
|
|
59
|
+
const shape = normalizeEncoding(args.shape, `${operation} shape`);
|
|
60
|
+
const { radius, ...point } = normalizeAppearance(
|
|
61
|
+
args.point,
|
|
62
|
+
POINT_OPTIONS,
|
|
63
|
+
`${operation} point`
|
|
64
|
+
);
|
|
65
|
+
const guides = normalizeGuides(args.guides, operation);
|
|
66
|
+
if (radius !== undefined) {
|
|
67
|
+
validateNonNegativeFinite(radius, `${operation} point radius`);
|
|
68
|
+
if (size !== undefined) {
|
|
69
|
+
throw new Error(`${operation} point radius conflicts with size.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let next = this
|
|
74
|
+
.createPointMark({ id, data, ...point })
|
|
75
|
+
.encodeTheta(positionArgs(theta, { target: id, coordinate: args.coordinate }))
|
|
76
|
+
.encodeR(positionArgs(radiusPosition, { target: id, coordinate: args.coordinate }));
|
|
77
|
+
if (radius !== undefined) next = next.encodePointRadius({ target: id, value: radius });
|
|
78
|
+
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
79
|
+
if (size !== undefined) next = next.encodeSize(targetArgs(size, id));
|
|
80
|
+
if (shape !== undefined) next = next.encodeShape(targetArgs(shape, id));
|
|
81
|
+
return applyFacadeGuides(next, guides, id);
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export const createPolarLinePlot = action(
|
|
86
|
+
{
|
|
87
|
+
op: "createPolarLinePlot",
|
|
88
|
+
description: "Create an open or explicitly closed Polar line plot."
|
|
89
|
+
},
|
|
90
|
+
function (args = {}) {
|
|
91
|
+
const operation = "createPolarLinePlot";
|
|
92
|
+
validateFacadeOptions(args, [
|
|
93
|
+
"id", "data", "coordinate", "theta", "radius", "color", "groupBy",
|
|
94
|
+
"strokeDash", "line", "guides"
|
|
95
|
+
], operation);
|
|
96
|
+
const id = resolveFacadeId(this, args.id, {
|
|
97
|
+
defaultId: "polarLinePlot",
|
|
98
|
+
operation
|
|
99
|
+
});
|
|
100
|
+
const data = resolveFacadeData(this, args.data, operation);
|
|
101
|
+
const theta = position(args.theta, operation, "theta");
|
|
102
|
+
const radius = position(args.radius, operation, "radius");
|
|
103
|
+
const color = normalizeEncoding(args.color, `${operation} color`);
|
|
104
|
+
const strokeDash = normalizeStrokeDashEncoding(
|
|
105
|
+
args.strokeDash,
|
|
106
|
+
`${operation} strokeDash`
|
|
107
|
+
);
|
|
108
|
+
const groupBy = args.groupBy === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: normalizeGroupFields(args.groupBy);
|
|
111
|
+
const line = normalizeAppearance(args.line, LINE_OPTIONS, `${operation} line`);
|
|
112
|
+
const guides = normalizeGuides(args.guides, operation);
|
|
113
|
+
|
|
114
|
+
let next = this
|
|
115
|
+
.createLineMark({ id, data, ...line })
|
|
116
|
+
.encodeTheta(positionArgs(theta, { target: id, coordinate: args.coordinate }))
|
|
117
|
+
.encodeR(positionArgs(radius, { target: id, coordinate: args.coordinate }));
|
|
118
|
+
if (groupBy !== undefined) next = next.encodeGroup({ target: id, fields: groupBy });
|
|
119
|
+
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
120
|
+
if (strokeDash !== undefined) next = next.encodeStrokeDash(targetArgs(strokeDash, id));
|
|
121
|
+
return applyFacadeGuides(next, guides, id);
|
|
122
|
+
}
|
|
123
|
+
);
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import { validateNonEmptyString, validateOptionObject } from
|
|
4
|
+
"../../core/validation.js";
|
|
5
|
+
import { normalizeGroupFields } from "../../grammar/pathSeries.js";
|
|
6
|
+
import { readNominalField, readQuantitativeField } from
|
|
7
|
+
"../../grammar/scales/fields.js";
|
|
8
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
9
|
+
import {
|
|
10
|
+
normalizeAppearance,
|
|
11
|
+
normalizeEncoding,
|
|
12
|
+
normalizeFieldEncoding,
|
|
13
|
+
normalizeGuides,
|
|
14
|
+
normalizeStrokeDashEncoding,
|
|
15
|
+
omitUndefinedOptions,
|
|
16
|
+
resolveFacadeData,
|
|
17
|
+
resolveFacadeId,
|
|
18
|
+
validateFacadeOptions
|
|
19
|
+
} from "./shared.js";
|
|
20
|
+
|
|
21
|
+
const OPTIONS = Object.freeze([
|
|
22
|
+
"id", "data", "coordinate", "category", "value", "wide", "groupBy",
|
|
23
|
+
"order", "color", "strokeDash", "line", "guides"
|
|
24
|
+
]);
|
|
25
|
+
const CATEGORY_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
|
|
26
|
+
const VALUE_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
|
|
27
|
+
const CATEGORY_SCALE_OPTIONS = Object.freeze([
|
|
28
|
+
"id", "type", "domain", "range", "reverse", "paddingInner", "paddingOuter",
|
|
29
|
+
"padding", "align"
|
|
30
|
+
]);
|
|
31
|
+
const WIDE_OPTIONS = Object.freeze(["fields", "as"]);
|
|
32
|
+
const WIDE_AS_OPTIONS = Object.freeze(["key", "value"]);
|
|
33
|
+
const LINE_OPTIONS = Object.freeze([
|
|
34
|
+
"strokeWidth", "curve", "stroke", "opacity", "closed"
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function normalizeCategory(value, operation) {
|
|
38
|
+
const category = normalizeFieldEncoding(value, `${operation} category`);
|
|
39
|
+
validateOptionObject(category, CATEGORY_OPTIONS, `${operation} category`);
|
|
40
|
+
validateNonEmptyString(category.field, `${operation} category field`);
|
|
41
|
+
const fieldType = category.fieldType ?? "nominal";
|
|
42
|
+
if (!["nominal", "ordinal"].includes(fieldType)) {
|
|
43
|
+
throw new TypeError(`${operation} category must be nominal or ordinal.`);
|
|
44
|
+
}
|
|
45
|
+
if (category.scale !== undefined) {
|
|
46
|
+
validateOptionObject(
|
|
47
|
+
category.scale,
|
|
48
|
+
CATEGORY_SCALE_OPTIONS,
|
|
49
|
+
`${operation} category.scale`
|
|
50
|
+
);
|
|
51
|
+
if (
|
|
52
|
+
category.scale.type !== undefined &&
|
|
53
|
+
!["band", "point"].includes(category.scale.type)
|
|
54
|
+
) {
|
|
55
|
+
throw new TypeError(`${operation} category.scale.type must be band or point.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...omitUndefinedOptions(category),
|
|
60
|
+
fieldType,
|
|
61
|
+
...(category.scale === undefined
|
|
62
|
+
? {}
|
|
63
|
+
: { scale: omitUndefinedOptions(category.scale) })
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeValue(value, operation) {
|
|
68
|
+
const result = normalizeFieldEncoding(value, `${operation} value`);
|
|
69
|
+
validateOptionObject(result, VALUE_OPTIONS, `${operation} value`);
|
|
70
|
+
validateNonEmptyString(result.field, `${operation} value field`);
|
|
71
|
+
if (result.fieldType !== undefined && result.fieldType !== "quantitative") {
|
|
72
|
+
throw new TypeError(`${operation} value must be quantitative.`);
|
|
73
|
+
}
|
|
74
|
+
return { ...omitUndefinedOptions(result), fieldType: "quantitative" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeWide(value, id, operation) {
|
|
78
|
+
if (!isPlainObject(value)) {
|
|
79
|
+
throw new TypeError(`${operation} wide must be a plain object.`);
|
|
80
|
+
}
|
|
81
|
+
validateOptionObject(value, WIDE_OPTIONS, `${operation} wide`);
|
|
82
|
+
if (!Array.isArray(value.fields) || value.fields.length < 3) {
|
|
83
|
+
throw new Error(`${operation} wide.fields must contain at least three fields.`);
|
|
84
|
+
}
|
|
85
|
+
const fields = [...value.fields];
|
|
86
|
+
for (const field of fields) validateNonEmptyString(field, `${operation} wide field`);
|
|
87
|
+
if (new Set(fields).size !== fields.length) {
|
|
88
|
+
throw new Error(`${operation} wide.fields must be unique.`);
|
|
89
|
+
}
|
|
90
|
+
const aliases = value.as === undefined ? {} : value.as;
|
|
91
|
+
if (!isPlainObject(aliases)) {
|
|
92
|
+
throw new TypeError(`${operation} wide.as must be a plain object.`);
|
|
93
|
+
}
|
|
94
|
+
validateOptionObject(aliases, WIDE_AS_OPTIONS, `${operation} wide.as`);
|
|
95
|
+
const key = aliases.key ?? `${id}Dimension`;
|
|
96
|
+
const foldedValue = aliases.value ?? `${id}Value`;
|
|
97
|
+
validateNonEmptyString(key, `${operation} wide.as.key`);
|
|
98
|
+
validateNonEmptyString(foldedValue, `${operation} wide.as.value`);
|
|
99
|
+
if (key === foldedValue || fields.includes(key) || fields.includes(foldedValue)) {
|
|
100
|
+
throw new Error(`${operation} wide aliases must be distinct from selected fields.`);
|
|
101
|
+
}
|
|
102
|
+
return { fields, as: { key, value: foldedValue } };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function orderedCategories(rows, field, requested, operation) {
|
|
106
|
+
const observed = [...new Set(readNominalField(rows, field))];
|
|
107
|
+
if (!Array.isArray(requested) && requested !== undefined) {
|
|
108
|
+
throw new TypeError(`${operation} order must be an array.`);
|
|
109
|
+
}
|
|
110
|
+
const order = requested === undefined ? observed : [...requested];
|
|
111
|
+
if (order.length < 3 || new Set(order).size !== order.length) {
|
|
112
|
+
throw new Error(`${operation} order must contain at least three distinct categories.`);
|
|
113
|
+
}
|
|
114
|
+
const observedSet = new Set(observed);
|
|
115
|
+
if (
|
|
116
|
+
order.length !== observed.length ||
|
|
117
|
+
order.some(value => !observedSet.has(value))
|
|
118
|
+
) {
|
|
119
|
+
throw new Error(`${operation} order must contain every category exactly once.`);
|
|
120
|
+
}
|
|
121
|
+
return order;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function seriesKey(row, fields) {
|
|
125
|
+
return JSON.stringify(fields.map(field => row[field]));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function radarSeriesFields(groupBy, color, strokeDash, operation) {
|
|
129
|
+
if (groupBy.length > 0) return groupBy;
|
|
130
|
+
const colorField = color?.field;
|
|
131
|
+
const dashField = strokeDash?.field;
|
|
132
|
+
if (
|
|
133
|
+
colorField !== undefined && dashField !== undefined &&
|
|
134
|
+
colorField !== dashField
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`${operation} color and strokeDash fields must match unless groupBy defines the series.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const field = colorField ?? dashField;
|
|
141
|
+
return field === undefined ? [] : [field];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateRadarRows(rows, { category, value, groupBy, order }, operation) {
|
|
145
|
+
readQuantitativeField(rows, value);
|
|
146
|
+
for (const field of groupBy) readNominalField(rows, field);
|
|
147
|
+
const expected = new Set(order);
|
|
148
|
+
const series = new Map();
|
|
149
|
+
for (const row of rows) {
|
|
150
|
+
const key = seriesKey(row, groupBy);
|
|
151
|
+
const categories = series.get(key) ?? new Set();
|
|
152
|
+
const current = row[category];
|
|
153
|
+
if (!expected.has(current)) {
|
|
154
|
+
throw new Error(`${operation} found a category outside order.`);
|
|
155
|
+
}
|
|
156
|
+
if (categories.has(current)) {
|
|
157
|
+
throw new Error(`${operation} requires one row per series and category.`);
|
|
158
|
+
}
|
|
159
|
+
categories.add(current);
|
|
160
|
+
series.set(key, categories);
|
|
161
|
+
}
|
|
162
|
+
if (series.size === 0) throw new Error(`${operation} requires radar rows.`);
|
|
163
|
+
for (const categories of series.values()) {
|
|
164
|
+
if (
|
|
165
|
+
categories.size !== order.length ||
|
|
166
|
+
order.some(value => !categories.has(value))
|
|
167
|
+
) {
|
|
168
|
+
throw new Error(`${operation} requires every category in every series.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function scaleWithOrder(category, order, operation) {
|
|
174
|
+
const scale = category.scale ?? {};
|
|
175
|
+
if (
|
|
176
|
+
scale.domain !== undefined && scale.domain !== "auto" &&
|
|
177
|
+
(!Array.isArray(scale.domain) ||
|
|
178
|
+
scale.domain.length !== order.length ||
|
|
179
|
+
scale.domain.some((value, index) => value !== order[index]))
|
|
180
|
+
) {
|
|
181
|
+
throw new Error(`${operation} category.scale.domain conflicts with order.`);
|
|
182
|
+
}
|
|
183
|
+
return { ...category, scale: { ...scale, domain: order } };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const createRadarPlot = action(
|
|
187
|
+
{
|
|
188
|
+
op: "createRadarPlot",
|
|
189
|
+
description: "Create a closed Radar plot from long rows or an explicit wide fold."
|
|
190
|
+
},
|
|
191
|
+
function (args = {}) {
|
|
192
|
+
const operation = "createRadarPlot";
|
|
193
|
+
validateFacadeOptions(args, OPTIONS, operation);
|
|
194
|
+
const hasWide = args.wide !== undefined;
|
|
195
|
+
const hasCategory = args.category !== undefined;
|
|
196
|
+
const hasValue = args.value !== undefined;
|
|
197
|
+
if (
|
|
198
|
+
(hasWide && (hasCategory || hasValue)) ||
|
|
199
|
+
(!hasWide && (!hasCategory || !hasValue))
|
|
200
|
+
) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`${operation} requires category and value, or wide, but not both.`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const longForm = !hasWide;
|
|
206
|
+
const id = resolveFacadeId(this, args.id, {
|
|
207
|
+
defaultId: "radarPlot",
|
|
208
|
+
operation
|
|
209
|
+
});
|
|
210
|
+
const source = resolveFacadeData(this, args.data, operation);
|
|
211
|
+
const sourceRows = findDataset(this, source).values;
|
|
212
|
+
const groupBy = args.groupBy === undefined
|
|
213
|
+
? []
|
|
214
|
+
: normalizeGroupFields(args.groupBy);
|
|
215
|
+
const color = normalizeEncoding(args.color, `${operation} color`);
|
|
216
|
+
const strokeDash = normalizeStrokeDashEncoding(
|
|
217
|
+
args.strokeDash,
|
|
218
|
+
`${operation} strokeDash`
|
|
219
|
+
);
|
|
220
|
+
const seriesFields = radarSeriesFields(
|
|
221
|
+
groupBy,
|
|
222
|
+
color,
|
|
223
|
+
strokeDash,
|
|
224
|
+
operation
|
|
225
|
+
);
|
|
226
|
+
const line = normalizeAppearance(args.line, LINE_OPTIONS, `${operation} line`);
|
|
227
|
+
if (line.closed === false) {
|
|
228
|
+
throw new Error(`${operation} requires a closed line.`);
|
|
229
|
+
}
|
|
230
|
+
const guides = normalizeGuides(args.guides, operation);
|
|
231
|
+
|
|
232
|
+
let next = this;
|
|
233
|
+
let data = source;
|
|
234
|
+
let category;
|
|
235
|
+
let radarValue;
|
|
236
|
+
let order;
|
|
237
|
+
if (longForm) {
|
|
238
|
+
category = normalizeCategory(args.category, operation);
|
|
239
|
+
radarValue = normalizeValue(args.value, operation);
|
|
240
|
+
order = orderedCategories(sourceRows, category.field, args.order, operation);
|
|
241
|
+
validateRadarRows(sourceRows, {
|
|
242
|
+
category: category.field,
|
|
243
|
+
value: radarValue.field,
|
|
244
|
+
groupBy: seriesFields,
|
|
245
|
+
order
|
|
246
|
+
}, operation);
|
|
247
|
+
} else {
|
|
248
|
+
const wide = normalizeWide(args.wide, id, operation);
|
|
249
|
+
if (sourceRows.length > 1 && groupBy.length === 0) {
|
|
250
|
+
throw new Error(`${operation} wide rows require groupBy as series identity.`);
|
|
251
|
+
}
|
|
252
|
+
for (const field of wide.fields) readQuantitativeField(sourceRows, field);
|
|
253
|
+
for (const field of groupBy) readNominalField(sourceRows, field);
|
|
254
|
+
if (!Array.isArray(args.order) && args.order !== undefined) {
|
|
255
|
+
throw new TypeError(`${operation} order must be an array.`);
|
|
256
|
+
}
|
|
257
|
+
order = args.order === undefined ? wide.fields : [...args.order];
|
|
258
|
+
if (
|
|
259
|
+
order.length !== wide.fields.length ||
|
|
260
|
+
new Set(order).size !== order.length ||
|
|
261
|
+
order.some(field => !wide.fields.includes(field))
|
|
262
|
+
) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`${operation} order must contain every wide field exactly once.`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
data = `${id}FoldData`;
|
|
268
|
+
next = next.createFoldData({
|
|
269
|
+
id: data,
|
|
270
|
+
source,
|
|
271
|
+
fields: wide.fields,
|
|
272
|
+
as: wide.as
|
|
273
|
+
});
|
|
274
|
+
const foldedRows = findDataset(next, data).values;
|
|
275
|
+
category = { field: wide.as.key, fieldType: "nominal" };
|
|
276
|
+
radarValue = { field: wide.as.value, fieldType: "quantitative" };
|
|
277
|
+
validateRadarRows(foldedRows, {
|
|
278
|
+
category: category.field,
|
|
279
|
+
value: radarValue.field,
|
|
280
|
+
groupBy: seriesFields,
|
|
281
|
+
order
|
|
282
|
+
}, operation);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
category = scaleWithOrder(category, order, operation);
|
|
286
|
+
return next.createPolarLinePlot({
|
|
287
|
+
id,
|
|
288
|
+
data,
|
|
289
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
290
|
+
theta: category,
|
|
291
|
+
radius: radarValue,
|
|
292
|
+
...(groupBy.length === 0 ? {} : { groupBy }),
|
|
293
|
+
...(color === undefined ? {} : { color }),
|
|
294
|
+
...(strokeDash === undefined ? {} : { strokeDash }),
|
|
295
|
+
line: { ...line, closed: true },
|
|
296
|
+
guides
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
);
|