ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import {
|
|
4
|
+
BOX_FIELDS,
|
|
5
|
+
deriveBoxData,
|
|
6
|
+
normalizeBoxTransform
|
|
7
|
+
} from "../../grammar/boxPlot.js";
|
|
8
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
9
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
10
|
+
import { resolveBoxOrientation } from "./resolve.js";
|
|
11
|
+
import { resolvePlotGraphicPlacement } from
|
|
12
|
+
"../../materialization/graphicHierarchy.js";
|
|
13
|
+
|
|
14
|
+
export const materializeBoxPlot = action(
|
|
15
|
+
{
|
|
16
|
+
op: "materializeBoxPlot",
|
|
17
|
+
description: "Materialize a complete box-plot composite."
|
|
18
|
+
},
|
|
19
|
+
function ({ id } = {}) {
|
|
20
|
+
const ownerId = validateUserId(id, "Box-plot id");
|
|
21
|
+
const config = this.markConfigs[ownerId]?.boxPlot;
|
|
22
|
+
const layer = findLayer(this, ownerId);
|
|
23
|
+
if (config === undefined || layer?.mark?.type !== "bar") {
|
|
24
|
+
throw new Error(`Unknown box plot "${ownerId}".`);
|
|
25
|
+
}
|
|
26
|
+
if (config.materialized) return this;
|
|
27
|
+
const x = layer.encoding?.x;
|
|
28
|
+
const y = layer.encoding?.y;
|
|
29
|
+
if (x?.field === undefined || y?.field === undefined) return this;
|
|
30
|
+
const orientation = resolveBoxOrientation(x, y);
|
|
31
|
+
if (orientation === undefined) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"createBoxPlot requires one categorical axis and one quantitative axis."
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const category = orientation === "vertical" ? x : y;
|
|
37
|
+
const measure = orientation === "vertical" ? y : x;
|
|
38
|
+
const measureChannel = orientation === "vertical" ? "y" : "x";
|
|
39
|
+
const source = layer.data;
|
|
40
|
+
const summaryId = `${ownerId}SummaryData`;
|
|
41
|
+
const outlierDataId = `${ownerId}OutlierData`;
|
|
42
|
+
const whiskerId = `${ownerId}Whisker`;
|
|
43
|
+
const medianId = `${ownerId}Median`;
|
|
44
|
+
const outlierId = `${ownerId}Outliers`;
|
|
45
|
+
let next = this._withMarkConfig(ownerId, {
|
|
46
|
+
...this.markConfigs[ownerId],
|
|
47
|
+
boxPlot: {
|
|
48
|
+
...config,
|
|
49
|
+
materialized: true,
|
|
50
|
+
source,
|
|
51
|
+
orientation,
|
|
52
|
+
category: category.field,
|
|
53
|
+
measure: measure.field
|
|
54
|
+
},
|
|
55
|
+
barWidth: { band: config.width },
|
|
56
|
+
fill: config.box.fill,
|
|
57
|
+
opacity: config.box.opacity,
|
|
58
|
+
stroke: config.box.stroke,
|
|
59
|
+
strokeWidth: config.box.strokeWidth
|
|
60
|
+
}).createBoxSummaryData({
|
|
61
|
+
id: summaryId,
|
|
62
|
+
source,
|
|
63
|
+
category: category.field,
|
|
64
|
+
field: measure.field,
|
|
65
|
+
whisker: config.whisker.type,
|
|
66
|
+
...(config.whisker.factor === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: { factor: config.whisker.factor })
|
|
69
|
+
});
|
|
70
|
+
const sourceRows = findDataset(next, source).values;
|
|
71
|
+
const hasOutliers = config.outliers &&
|
|
72
|
+
config.whisker.type === "tukey" &&
|
|
73
|
+
deriveBoxData(sourceRows, normalizeBoxTransform({
|
|
74
|
+
type: "boxOutlier",
|
|
75
|
+
category: category.field,
|
|
76
|
+
field: measure.field,
|
|
77
|
+
whisker: config.whisker.type,
|
|
78
|
+
factor: config.whisker.factor
|
|
79
|
+
})).outliers.length > 0;
|
|
80
|
+
if (hasOutliers) {
|
|
81
|
+
next = next.createBoxOutlierData({
|
|
82
|
+
id: outlierDataId,
|
|
83
|
+
source,
|
|
84
|
+
category: category.field,
|
|
85
|
+
field: measure.field,
|
|
86
|
+
whisker: config.whisker.type,
|
|
87
|
+
factor: config.whisker.factor
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
next = next.editSemantic({
|
|
91
|
+
property: `layer[${ownerId}].data`,
|
|
92
|
+
value: summaryId
|
|
93
|
+
});
|
|
94
|
+
const rangeAction = orientation === "vertical" ? "encodeYRange" : "encodeXRange";
|
|
95
|
+
next = next[rangeAction]({
|
|
96
|
+
target: ownerId,
|
|
97
|
+
lower: BOX_FIELDS.q1,
|
|
98
|
+
upper: BOX_FIELDS.q3,
|
|
99
|
+
scale: { id: measure.scale }
|
|
100
|
+
}).editSemantic({
|
|
101
|
+
property: `layer[${ownerId}].encoding.${measureChannel}.title`,
|
|
102
|
+
value: measure.field
|
|
103
|
+
});
|
|
104
|
+
next = next.createErrorBar({
|
|
105
|
+
id: whiskerId,
|
|
106
|
+
data: summaryId,
|
|
107
|
+
...(orientation === "vertical"
|
|
108
|
+
? {
|
|
109
|
+
x: {
|
|
110
|
+
field: category.field,
|
|
111
|
+
fieldType: category.fieldType,
|
|
112
|
+
scale: { id: category.scale }
|
|
113
|
+
},
|
|
114
|
+
y: {
|
|
115
|
+
center: BOX_FIELDS.median,
|
|
116
|
+
lower: BOX_FIELDS.lowerWhisker,
|
|
117
|
+
upper: BOX_FIELDS.upperWhisker,
|
|
118
|
+
scale: { id: measure.scale }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
: {
|
|
122
|
+
x: {
|
|
123
|
+
center: BOX_FIELDS.median,
|
|
124
|
+
lower: BOX_FIELDS.lowerWhisker,
|
|
125
|
+
upper: BOX_FIELDS.upperWhisker,
|
|
126
|
+
scale: { id: measure.scale }
|
|
127
|
+
},
|
|
128
|
+
y: {
|
|
129
|
+
field: category.field,
|
|
130
|
+
fieldType: category.fieldType,
|
|
131
|
+
scale: { id: category.scale }
|
|
132
|
+
}
|
|
133
|
+
}),
|
|
134
|
+
coordinate: layer.coordinate,
|
|
135
|
+
stroke: "#111111",
|
|
136
|
+
strokeWidth: 1.5
|
|
137
|
+
});
|
|
138
|
+
next = next
|
|
139
|
+
.editSemantic({
|
|
140
|
+
property: `layer[${whiskerId}].encoding.${measureChannel}.title`,
|
|
141
|
+
value: measure.field
|
|
142
|
+
})
|
|
143
|
+
.editGraphics({ target: ownerId, remove: true })
|
|
144
|
+
.createGraphics({
|
|
145
|
+
id: ownerId,
|
|
146
|
+
type: "rect",
|
|
147
|
+
length: 0,
|
|
148
|
+
...resolvePlotGraphicPlacement(next)
|
|
149
|
+
})
|
|
150
|
+
.rematerializeBarMark({ id: ownerId })
|
|
151
|
+
.createBoxMedian({
|
|
152
|
+
id: medianId,
|
|
153
|
+
owner: ownerId,
|
|
154
|
+
data: summaryId,
|
|
155
|
+
category: category.field,
|
|
156
|
+
categoryType: category.fieldType,
|
|
157
|
+
measure: BOX_FIELDS.median,
|
|
158
|
+
orientation,
|
|
159
|
+
coordinate: layer.coordinate,
|
|
160
|
+
categoryScale: category.scale,
|
|
161
|
+
measureScale: measure.scale,
|
|
162
|
+
stroke: config.median.stroke,
|
|
163
|
+
strokeWidth: config.median.strokeWidth
|
|
164
|
+
});
|
|
165
|
+
if (hasOutliers) {
|
|
166
|
+
next = next.createBoxOutliers({
|
|
167
|
+
id: outlierId,
|
|
168
|
+
data: outlierDataId,
|
|
169
|
+
category: category.field,
|
|
170
|
+
categoryType: category.fieldType,
|
|
171
|
+
measure: measure.field,
|
|
172
|
+
orientation,
|
|
173
|
+
coordinate: layer.coordinate,
|
|
174
|
+
categoryScale: category.scale,
|
|
175
|
+
measureScale: measure.scale,
|
|
176
|
+
shape: config.outlier.shape,
|
|
177
|
+
radius: config.outlier.radius,
|
|
178
|
+
opacity: config.outlier.opacity
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return next._withContext({ currentMark: ownerId, currentData: source });
|
|
182
|
+
}
|
|
183
|
+
);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
validateKeys,
|
|
4
|
+
validateNonEmptyString,
|
|
5
|
+
validateNonNegativeFinite,
|
|
6
|
+
validatePositiveFinite,
|
|
7
|
+
validateUnitInterval
|
|
8
|
+
} from "../../core/validation.js";
|
|
9
|
+
import { validatePointShape } from "../../grammar/pointShapes.js";
|
|
10
|
+
import { DEFAULT_COLORS } from "../../theme/defaults.js";
|
|
11
|
+
|
|
12
|
+
export const BOX_PLOT_OPTIONS = Object.freeze([
|
|
13
|
+
"id", "target", "data", "x", "y", "coordinate", "whisker",
|
|
14
|
+
"width", "outliers", "box", "median", "outlier"
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const DEFAULT_BOX = Object.freeze({
|
|
18
|
+
fill: DEFAULT_COLORS.mark,
|
|
19
|
+
opacity: 1,
|
|
20
|
+
stroke: DEFAULT_COLORS.mark,
|
|
21
|
+
strokeWidth: 1.5
|
|
22
|
+
});
|
|
23
|
+
const DEFAULT_MEDIAN = Object.freeze({ stroke: "#1f2937", strokeWidth: 1.5 });
|
|
24
|
+
const DEFAULT_OUTLIER = Object.freeze({
|
|
25
|
+
shape: "diamond",
|
|
26
|
+
radius: 3,
|
|
27
|
+
opacity: 0.75
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function plainOptions(value, keys, label) {
|
|
31
|
+
if (value === undefined) return {};
|
|
32
|
+
if (!isPlainObject(value)) {
|
|
33
|
+
throw new TypeError(`createBoxPlot ${label} must be a plain object.`);
|
|
34
|
+
}
|
|
35
|
+
validateKeys(value, keys, `createBoxPlot ${label}`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveBoxPosition(value, label) {
|
|
40
|
+
if (value === undefined) return undefined;
|
|
41
|
+
if (!isPlainObject(value)) {
|
|
42
|
+
throw new TypeError(`createBoxPlot ${label} must be a plain object.`);
|
|
43
|
+
}
|
|
44
|
+
validateKeys(value, ["field", "fieldType", "scale"], `createBoxPlot ${label}`);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function boxEncodingArgs(value) {
|
|
49
|
+
return {
|
|
50
|
+
...value,
|
|
51
|
+
...(typeof value.scale === "string" ? { scale: { id: value.scale } } : {})
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveBoxWhisker(value) {
|
|
56
|
+
if (value === undefined) return Object.freeze({ type: "tukey", factor: 1.5 });
|
|
57
|
+
if (!isPlainObject(value)) {
|
|
58
|
+
throw new TypeError("createBoxPlot whisker must be a plain object.");
|
|
59
|
+
}
|
|
60
|
+
validateKeys(value, ["type", "factor"], "createBoxPlot whisker");
|
|
61
|
+
const type = value.type ?? "tukey";
|
|
62
|
+
if (!["tukey", "minmax"].includes(type)) {
|
|
63
|
+
throw new Error(`Unsupported createBoxPlot whisker type "${type}".`);
|
|
64
|
+
}
|
|
65
|
+
if (type === "minmax") {
|
|
66
|
+
if (value.factor !== undefined) {
|
|
67
|
+
throw new Error("createBoxPlot minmax whiskers do not accept factor.");
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze({ type });
|
|
70
|
+
}
|
|
71
|
+
const factor = value.factor ?? 1.5;
|
|
72
|
+
if (!Number.isFinite(factor) || factor <= 0) {
|
|
73
|
+
throw new RangeError(
|
|
74
|
+
"createBoxPlot whisker factor must be positive and finite."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return Object.freeze({ type, factor });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveBoxWidth(value) {
|
|
81
|
+
const options = plainOptions(value, ["band"], "width");
|
|
82
|
+
const band = options.band ?? 0.7;
|
|
83
|
+
if (!Number.isFinite(band) || band <= 0 || band >= 1) {
|
|
84
|
+
throw new RangeError(
|
|
85
|
+
"createBoxPlot width.band must be greater than 0 and less than 1."
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return band;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveBoxAppearance(value) {
|
|
92
|
+
const options = plainOptions(
|
|
93
|
+
value,
|
|
94
|
+
["fill", "opacity", "stroke", "strokeWidth"],
|
|
95
|
+
"box"
|
|
96
|
+
);
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
fill: options.fill === undefined
|
|
99
|
+
? DEFAULT_BOX.fill
|
|
100
|
+
: validateNonEmptyString(options.fill, "createBoxPlot box.fill"),
|
|
101
|
+
opacity: options.opacity === undefined
|
|
102
|
+
? DEFAULT_BOX.opacity
|
|
103
|
+
: validateUnitInterval(options.opacity, "createBoxPlot box.opacity"),
|
|
104
|
+
stroke: options.stroke === undefined
|
|
105
|
+
? DEFAULT_BOX.stroke
|
|
106
|
+
: validateNonEmptyString(options.stroke, "createBoxPlot box.stroke"),
|
|
107
|
+
strokeWidth: options.strokeWidth === undefined
|
|
108
|
+
? DEFAULT_BOX.strokeWidth
|
|
109
|
+
: validateNonNegativeFinite(
|
|
110
|
+
options.strokeWidth,
|
|
111
|
+
"createBoxPlot box.strokeWidth"
|
|
112
|
+
)
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveBoxMedianAppearance(value) {
|
|
117
|
+
const options = plainOptions(value, ["stroke", "strokeWidth"], "median");
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
stroke: options.stroke === undefined
|
|
120
|
+
? DEFAULT_MEDIAN.stroke
|
|
121
|
+
: validateNonEmptyString(options.stroke, "createBoxPlot median.stroke"),
|
|
122
|
+
strokeWidth: options.strokeWidth === undefined
|
|
123
|
+
? DEFAULT_MEDIAN.strokeWidth
|
|
124
|
+
: validateNonNegativeFinite(
|
|
125
|
+
options.strokeWidth,
|
|
126
|
+
"createBoxPlot median.strokeWidth"
|
|
127
|
+
)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function resolveBoxOutlierAppearance(value) {
|
|
132
|
+
const options = plainOptions(value, ["shape", "radius", "opacity"], "outlier");
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
shape: options.shape === undefined
|
|
135
|
+
? DEFAULT_OUTLIER.shape
|
|
136
|
+
: validatePointShape(options.shape),
|
|
137
|
+
radius: options.radius === undefined
|
|
138
|
+
? DEFAULT_OUTLIER.radius
|
|
139
|
+
: validatePositiveFinite(options.radius, "createBoxPlot outlier.radius"),
|
|
140
|
+
opacity: options.opacity === undefined
|
|
141
|
+
? DEFAULT_OUTLIER.opacity
|
|
142
|
+
: validateUnitInterval(
|
|
143
|
+
options.opacity,
|
|
144
|
+
"createBoxPlot outlier.opacity"
|
|
145
|
+
)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { resolveOptionalUserId, validateUserId } from "../../core/identifiers.js";
|
|
2
|
+
import { findLayer, hasLayer } from "../../selectors/layers.js";
|
|
3
|
+
|
|
4
|
+
const CATEGORY_TYPES = Object.freeze(["nominal", "ordinal", "temporal"]);
|
|
5
|
+
|
|
6
|
+
export function resolveBoxOrientation(x, y) {
|
|
7
|
+
const xType = x?.fieldType ?? "quantitative";
|
|
8
|
+
const yType = y?.fieldType ?? "quantitative";
|
|
9
|
+
if (CATEGORY_TYPES.includes(xType) && yType === "quantitative") return "vertical";
|
|
10
|
+
if (xType === "quantitative" && CATEGORY_TYPES.includes(yType)) return "horizontal";
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveBoxSourceLayer(program, target) {
|
|
15
|
+
if (target !== undefined) {
|
|
16
|
+
const layer = findLayer(program, validateUserId(target, "Box source layer id"));
|
|
17
|
+
if (layer === undefined) throw new Error(`Unknown box source layer "${target}".`);
|
|
18
|
+
return layer;
|
|
19
|
+
}
|
|
20
|
+
const current = findLayer(program, program.context.currentMark);
|
|
21
|
+
if (current?.encoding?.x !== undefined || current?.encoding?.y !== undefined) {
|
|
22
|
+
return current;
|
|
23
|
+
}
|
|
24
|
+
const eligible = program.semanticSpec.layers.filter(
|
|
25
|
+
layer => layer.encoding?.x !== undefined && layer.encoding?.y !== undefined
|
|
26
|
+
);
|
|
27
|
+
return eligible.length === 1 ? eligible[0] : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveBoxPlotId(program, requested) {
|
|
31
|
+
return resolveOptionalUserId(requested, {
|
|
32
|
+
defaultId: "boxPlot",
|
|
33
|
+
label: "Box-plot id",
|
|
34
|
+
operation: "createBoxPlot",
|
|
35
|
+
ambiguous: hasLayer(program, "boxPlot") ||
|
|
36
|
+
program.graphicSpec.objects.boxPlot !== undefined
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_CANVAS,
|
|
4
|
+
DEFAULT_MARGIN,
|
|
5
|
+
normalizeMargin,
|
|
6
|
+
validateCanvasState
|
|
7
|
+
} from "../../layout/canvas.js";
|
|
8
|
+
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
9
|
+
import {
|
|
10
|
+
applyMaterializationPlan,
|
|
11
|
+
planCanvasRematerialization
|
|
12
|
+
} from "../../materialization/dependencies.js";
|
|
13
|
+
import {
|
|
14
|
+
assertCanvasHierarchyAvailable,
|
|
15
|
+
CANVAS_GRAPHIC_ID,
|
|
16
|
+
PLOT_GRAPHIC_ID
|
|
17
|
+
} from "../../materialization/graphicHierarchy.js";
|
|
18
|
+
|
|
19
|
+
const CANVAS_OPTIONS = Object.freeze([
|
|
20
|
+
"width",
|
|
21
|
+
"height",
|
|
22
|
+
"background",
|
|
23
|
+
"margin"
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function validateOptions(args, operation, { allowEmpty = false } = {}) {
|
|
27
|
+
const keys = Object.keys(args);
|
|
28
|
+
|
|
29
|
+
if (!allowEmpty && keys.length === 0) {
|
|
30
|
+
throw new TypeError(`${operation} requires at least one option.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
if (!CANVAS_OPTIONS.includes(key)) {
|
|
35
|
+
throw new Error(`Unknown ${operation} option "${key}".`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requireCanvas(program) {
|
|
41
|
+
const canvas = program.graphicSpec.objects.canvas;
|
|
42
|
+
|
|
43
|
+
if (canvas?.type !== "canvas") {
|
|
44
|
+
throw new Error("editCanvas requires an existing canvas.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return canvas;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveCanvasState(program, args) {
|
|
51
|
+
const canvas = requireCanvas(program);
|
|
52
|
+
const baseMargin =
|
|
53
|
+
program.materializationConfigs.canvas?.margin ?? DEFAULT_MARGIN;
|
|
54
|
+
const state = {
|
|
55
|
+
width: Object.hasOwn(args, "width")
|
|
56
|
+
? args.width
|
|
57
|
+
: canvas.properties.width,
|
|
58
|
+
height: Object.hasOwn(args, "height")
|
|
59
|
+
? args.height
|
|
60
|
+
: canvas.properties.height,
|
|
61
|
+
background: Object.hasOwn(args, "background")
|
|
62
|
+
? args.background
|
|
63
|
+
: canvas.properties.background,
|
|
64
|
+
margin: Object.hasOwn(args, "margin")
|
|
65
|
+
? normalizeMargin(args.margin, baseMargin)
|
|
66
|
+
: baseMargin
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
validateCanvasState(state);
|
|
70
|
+
return cloneAndFreeze(state);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const editCanvas = action(
|
|
74
|
+
{
|
|
75
|
+
op: "editCanvas",
|
|
76
|
+
description: "Edit canvas properties and authoring bounds."
|
|
77
|
+
},
|
|
78
|
+
function (args = {}) {
|
|
79
|
+
validateOptions(args, "editCanvas");
|
|
80
|
+
const state = resolveCanvasState(this, args);
|
|
81
|
+
let next = this;
|
|
82
|
+
|
|
83
|
+
for (const property of ["width", "height", "background"]) {
|
|
84
|
+
if (Object.hasOwn(args, property)) {
|
|
85
|
+
next = next.editGraphics({
|
|
86
|
+
target: "canvas",
|
|
87
|
+
property,
|
|
88
|
+
value: state[property]
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
next = next._withCanvasConfig({ margin: state.margin });
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
Object.hasOwn(args, "width") ||
|
|
97
|
+
Object.hasOwn(args, "height") ||
|
|
98
|
+
Object.hasOwn(args, "margin")
|
|
99
|
+
) {
|
|
100
|
+
next = applyMaterializationPlan(
|
|
101
|
+
next,
|
|
102
|
+
planCanvasRematerialization(next)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return next;
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
export const createCanvas = action(
|
|
111
|
+
{
|
|
112
|
+
op: "createCanvas",
|
|
113
|
+
description: "Create and configure the chart canvas."
|
|
114
|
+
},
|
|
115
|
+
function (args = {}) {
|
|
116
|
+
validateOptions(args, "createCanvas", { allowEmpty: true });
|
|
117
|
+
|
|
118
|
+
assertCanvasHierarchyAvailable(this);
|
|
119
|
+
|
|
120
|
+
const options = {
|
|
121
|
+
width: Object.hasOwn(args, "width")
|
|
122
|
+
? args.width
|
|
123
|
+
: DEFAULT_CANVAS.width,
|
|
124
|
+
height: Object.hasOwn(args, "height")
|
|
125
|
+
? args.height
|
|
126
|
+
: DEFAULT_CANVAS.height,
|
|
127
|
+
background: Object.hasOwn(args, "background")
|
|
128
|
+
? args.background
|
|
129
|
+
: DEFAULT_CANVAS.background,
|
|
130
|
+
margin: Object.hasOwn(args, "margin")
|
|
131
|
+
? args.margin
|
|
132
|
+
: DEFAULT_CANVAS.margin
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return this
|
|
136
|
+
.createGraphics({ id: CANVAS_GRAPHIC_ID, type: "canvas" })
|
|
137
|
+
.createGraphics({
|
|
138
|
+
id: PLOT_GRAPHIC_ID,
|
|
139
|
+
type: "collection",
|
|
140
|
+
parent: CANVAS_GRAPHIC_ID
|
|
141
|
+
})
|
|
142
|
+
.editCanvas(options);
|
|
143
|
+
}
|
|
144
|
+
);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateCoordinateType } from "../../grammar/coordinates.js";
|
|
3
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
4
|
+
import { findCoordinate } from "../../selectors/coordinates.js";
|
|
5
|
+
import { findLayer, hasLayer } from "../../selectors/layers.js";
|
|
6
|
+
|
|
7
|
+
const COORDINATE_OPTIONS = Object.freeze(["id", "type", "layers"]);
|
|
8
|
+
|
|
9
|
+
function validateOptions(args) {
|
|
10
|
+
for (const key of Object.keys(args)) {
|
|
11
|
+
if (!COORDINATE_OPTIONS.includes(key)) {
|
|
12
|
+
throw new Error(`Unknown createCoordinate option "${key}".`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validateLayers(program, layers) {
|
|
18
|
+
if (!Array.isArray(layers)) {
|
|
19
|
+
throw new TypeError("createCoordinate layers must be an array.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ids = layers.map(layer => validateUserId(layer, "Layer id"));
|
|
23
|
+
|
|
24
|
+
if (new Set(ids).size !== ids.length) {
|
|
25
|
+
throw new Error("createCoordinate layers must not contain duplicates.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const id of ids) {
|
|
29
|
+
if (!hasLayer(program, id)) {
|
|
30
|
+
throw new Error(`Unknown layer "${id}".`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return ids;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const createCoordinate = action(
|
|
38
|
+
{
|
|
39
|
+
op: "createCoordinate",
|
|
40
|
+
description: "Create a semantic coordinate and attach layers to it."
|
|
41
|
+
},
|
|
42
|
+
function (args = {}) {
|
|
43
|
+
validateOptions(args);
|
|
44
|
+
const id = validateUserId(args.id ?? "main", "Coordinate id");
|
|
45
|
+
const type = validateCoordinateType(args.type ?? "cartesian");
|
|
46
|
+
const layers = validateLayers(this, args.layers ?? []);
|
|
47
|
+
const existing = findCoordinate(this, id);
|
|
48
|
+
|
|
49
|
+
if (existing !== undefined && existing.type !== type) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Coordinate "${id}" already exists with type "${existing.type}".`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const layerId of layers) {
|
|
56
|
+
const layer = findLayer(this, layerId);
|
|
57
|
+
|
|
58
|
+
if (layer.coordinate !== undefined && layer.coordinate !== id) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Layer "${layerId}" already uses coordinate "${layer.coordinate}".`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let program = existing === undefined
|
|
66
|
+
? this.editSemantic({
|
|
67
|
+
property: `coordinate[${id}].type`,
|
|
68
|
+
value: type
|
|
69
|
+
})
|
|
70
|
+
: this;
|
|
71
|
+
|
|
72
|
+
for (const layerId of layers) {
|
|
73
|
+
const layer = findLayer(program, layerId);
|
|
74
|
+
|
|
75
|
+
if (layer.coordinate === undefined) {
|
|
76
|
+
program = program.editSemantic({
|
|
77
|
+
property: `layer[${layerId}].coordinate`,
|
|
78
|
+
value: id
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return program;
|
|
84
|
+
}
|
|
85
|
+
);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { validateKeys } from "../../core/validation.js";
|
|
4
|
+
import { BOX_FIELDS, deriveBoxData, normalizeBoxTransform } from "../../grammar/boxPlot.js";
|
|
5
|
+
import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
|
|
6
|
+
|
|
7
|
+
const OPTIONS = Object.freeze([
|
|
8
|
+
"id", "source", "category", "field", "whisker", "factor", "as"
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function materializer(type, op, select) {
|
|
12
|
+
return action({ op, description: `Materialize ${type} box-plot data.` }, function (args = {}) {
|
|
13
|
+
validateKeys(args, MATERIALIZE_OPTIONS, op);
|
|
14
|
+
const { id, source, transform } = requireDerivedDataset(this, args.id, type);
|
|
15
|
+
return this.editSemantic({ property: `dataset[${id}].values`, value: select(deriveBoxData(source.values, transform)) });
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const materializeBoxSummaryData = materializer("boxSummary", "materializeBoxSummaryData", value => value.summaries);
|
|
20
|
+
export const materializeBoxOutlierData = materializer("boxOutlier", "materializeBoxOutlierData", value => value.outliers);
|
|
21
|
+
|
|
22
|
+
function creator(type, op, materialize) {
|
|
23
|
+
return action({ op, description: `Create immutable ${type} box-plot data.` }, function (args = {}) {
|
|
24
|
+
validateKeys(args, OPTIONS, op);
|
|
25
|
+
const id = validateUserId(args.id, "Box dataset id");
|
|
26
|
+
const source = validateUserId(args.source ?? this.context.currentData, "Box source dataset id");
|
|
27
|
+
const transform = normalizeBoxTransform({
|
|
28
|
+
type,
|
|
29
|
+
category: args.category,
|
|
30
|
+
field: args.field,
|
|
31
|
+
...(args.whisker === undefined ? {} : { whisker: args.whisker }),
|
|
32
|
+
...(args.factor === undefined ? {} : { factor: args.factor }),
|
|
33
|
+
as: args.as ?? BOX_FIELDS
|
|
34
|
+
});
|
|
35
|
+
return this.createDerivedData({ id, source, transform: [transform] })[materialize]({ id });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const createBoxSummaryData = creator("boxSummary", "createBoxSummaryData", "materializeBoxSummaryData");
|
|
40
|
+
export const createBoxOutlierData = creator("boxOutlier", "createBoxOutlierData", "materializeBoxOutlierData");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { resolveOptionalUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
4
|
+
import { validateKeys } from "../../core/validation.js";
|
|
5
|
+
import { hasDataset } from "../../selectors/index.js";
|
|
6
|
+
|
|
7
|
+
const OPTIONS = Object.freeze(["id", "values"]);
|
|
8
|
+
|
|
9
|
+
export const createData = action(
|
|
10
|
+
{ op: "createData", description: "Create an immutable named dataset." },
|
|
11
|
+
function (args = {}) {
|
|
12
|
+
validateKeys(args, OPTIONS, "createData");
|
|
13
|
+
const id = resolveOptionalUserId(args.id, {
|
|
14
|
+
defaultId: "data",
|
|
15
|
+
label: "Dataset id",
|
|
16
|
+
operation: "createData",
|
|
17
|
+
ambiguous: this.semanticSpec.datasets.length > 0
|
|
18
|
+
});
|
|
19
|
+
if (!Array.isArray(args.values)) {
|
|
20
|
+
throw new TypeError("createData requires values to be an array.");
|
|
21
|
+
}
|
|
22
|
+
if (!args.values.every(isPlainObject)) {
|
|
23
|
+
throw new TypeError("createData requires every row to be a plain object.");
|
|
24
|
+
}
|
|
25
|
+
if (hasDataset(this, id)) {
|
|
26
|
+
throw new Error(`Dataset "${id}" already exists.`);
|
|
27
|
+
}
|
|
28
|
+
return this.editSemantic({
|
|
29
|
+
property: `dataset[${id}].values`,
|
|
30
|
+
value: args.values
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
);
|