ggaction 0.0.4 → 0.0.6
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 +55 -0
- package/README.md +35 -13
- package/package.json +18 -7
- package/src/actions/boxPlots/create.js +13 -7
- package/src/actions/boxPlots/edit.js +484 -36
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +11 -4
- package/src/actions/boxPlots/resolve.js +10 -2
- package/src/actions/charts/bar.js +50 -0
- package/src/actions/charts/heatmap.js +205 -0
- package/src/actions/charts/histogram.js +58 -0
- package/src/actions/charts/index.js +15 -0
- package/src/actions/charts/line.js +69 -0
- package/src/actions/charts/parallel.js +76 -0
- package/src/actions/charts/scatter.js +56 -0
- package/src/actions/charts/shared.js +121 -0
- package/src/actions/composition/actions.js +13 -3
- package/src/actions/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +260 -0
- package/src/actions/data/density.js +54 -13
- package/src/actions/data/derived.js +22 -0
- package/src/actions/data/filter.js +5 -0
- package/src/actions/data/gradientProfile.js +65 -0
- package/src/actions/data/horizon.js +71 -0
- package/src/actions/data/index.js +34 -2
- package/src/actions/data/intervalEdit.js +76 -0
- package/src/actions/data/window.js +50 -0
- package/src/actions/distributions/revision.js +326 -0
- package/src/actions/encodings/appearance.js +35 -1
- package/src/actions/encodings/color/index.js +17 -1
- package/src/actions/encodings/color/layout.js +8 -0
- package/src/actions/encodings/density/resolve.js +109 -0
- package/src/actions/encodings/density.js +330 -90
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +8 -0
- package/src/actions/encodings/parallel.js +115 -0
- package/src/actions/encodings/pathOrder.js +131 -0
- package/src/actions/encodings/position/apply.js +73 -4
- package/src/actions/encodings/position/index.js +15 -2
- package/src/actions/encodings/position/policies/arc.js +27 -4
- package/src/actions/encodings/position/policies/area.js +33 -2
- package/src/actions/encodings/position/policies/index.js +7 -2
- package/src/actions/encodings/position/policies/line.js +18 -2
- package/src/actions/encodings/position/resolve.js +24 -2
- package/src/actions/encodings/ranged.js +3 -1
- package/src/actions/encodings/remove.js +268 -0
- package/src/actions/encodings/ruleAppearance.js +88 -10
- package/src/actions/errorBands/edit.js +121 -13
- package/src/actions/errorBars/edit.js +45 -8
- package/src/actions/errorBars/options.js +7 -1
- package/src/actions/facets/actions.js +137 -3
- package/src/actions/facets/derive.js +33 -3
- package/src/actions/facets/replay.js +7 -36
- package/src/actions/gradientPlots/components.js +177 -0
- package/src/actions/gradientPlots/create.js +96 -0
- package/src/actions/gradientPlots/edit.js +501 -0
- package/src/actions/gradientPlots/index.js +24 -0
- package/src/actions/gradientPlots/materialize.js +287 -0
- package/src/actions/gradientPlots/options.js +120 -0
- package/src/actions/gradientPlots/paint.js +107 -0
- package/src/actions/gradientPlots/rebind.js +51 -0
- package/src/actions/gradientPlots/resolve.js +81 -0
- package/src/actions/guides/applicability.js +44 -17
- package/src/actions/guides/axes/axes.js +37 -3
- package/src/actions/guides/axes/edit.js +129 -47
- package/src/actions/guides/axes/index.js +2 -0
- package/src/actions/guides/axes/labels.js +11 -6
- package/src/actions/guides/axes/parallel/resolve.js +86 -0
- package/src/actions/guides/axes/parallel.js +228 -0
- package/src/actions/guides/axes/titles.js +11 -8
- package/src/actions/guides/guides.js +60 -5
- package/src/actions/guides/legends/categorical/actions.js +67 -19
- package/src/actions/guides/legends/continuous/common.js +3 -3
- package/src/actions/guides/legends/edit.js +63 -0
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +124 -30
- package/src/actions/guides/legends/strokeWidth.js +198 -0
- package/src/actions/index.js +6 -0
- package/src/actions/marks/arc/actions.js +46 -14
- package/src/actions/marks/area/actions.js +60 -136
- package/src/actions/marks/area/materialize.js +163 -0
- package/src/actions/marks/index.js +5 -1
- package/src/actions/marks/line/actions.js +99 -80
- package/src/actions/marks/line/materialize.js +146 -0
- package/src/actions/marks/point/create.js +76 -0
- package/src/actions/marks/point/edit.js +97 -0
- package/src/actions/marks/point/index.js +11 -1
- package/src/actions/marks/point/jitter.js +72 -0
- package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
- package/src/actions/marks/rect/actions.js +3 -0
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +29 -4
- package/src/actions/marks/text/actions.js +19 -4
- package/src/actions/marks/text/index.js +7 -1
- package/src/actions/marks/text/layout.js +321 -0
- package/src/actions/primitives/semanticValidation/dataset.js +22 -0
- package/src/actions/primitives/semanticValidation/guide.js +43 -0
- package/src/actions/primitives/semanticValidation/index.js +23 -0
- package/src/actions/primitives/semanticValidation/layer.js +74 -0
- package/src/actions/primitives/semanticValidation/scale.js +74 -0
- package/src/actions/primitives/semanticValidation/shared.js +5 -0
- package/src/actions/primitives/semanticValue.js +1 -204
- package/src/actions/regression/edit.js +247 -88
- package/src/actions/scales/consumers/common.js +18 -0
- package/src/actions/scales/consumers/families.js +14 -3
- package/src/actions/scales/definitions.js +30 -0
- package/src/actions/scales/edit.js +6 -204
- package/src/actions/scales/editPolicy.js +214 -0
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +26 -0
- package/src/actions/selection/actions.js +150 -5
- package/src/actions/selection/index.js +6 -0
- package/src/actions/violinPlots/create.js +198 -0
- package/src/actions/violinPlots/index.js +5 -0
- package/src/core/textMetrics.js +47 -0
- package/src/core/vocabulary.js +14 -2
- package/src/grammar/arcs.js +49 -12
- package/src/grammar/areaSeries.js +41 -5
- package/src/grammar/bin2d.js +333 -0
- package/src/grammar/categoricalDensity.js +192 -0
- package/src/grammar/coordinates.js +1 -1
- package/src/grammar/curveCommands.js +3 -1
- package/src/grammar/density.js +252 -26
- package/src/grammar/facets/dependencies.js +3 -11
- package/src/grammar/facets/index.js +3 -1
- package/src/grammar/gradientProfile.js +213 -0
- package/src/grammar/horizon.js +455 -0
- package/src/grammar/jitter.js +197 -0
- package/src/grammar/lineSeries.js +69 -0
- package/src/grammar/paint.js +88 -0
- package/src/grammar/parallelCoordinates.js +213 -0
- package/src/grammar/pathOrder.js +35 -0
- package/src/grammar/pointShapes.js +24 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/appearance.js +21 -0
- package/src/grammar/schemas/concreteGraphic.js +11 -1
- package/src/grammar/schemas/graphicBounds.js +19 -37
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/transforms.js +100 -11
- package/src/grammar/window.js +339 -0
- package/src/layout/labels.js +270 -0
- package/src/layout/text.js +4 -1
- package/src/layout/title.js +10 -20
- package/src/materialization/dataProvenance.js +56 -2
- package/src/materialization/dependencies.js +9 -14
- package/src/materialization/guides/resources.js +74 -24
- package/src/materialization/horizon.js +29 -0
- package/src/materialization/layout.js +15 -0
- package/src/materialization/marks/capabilities.js +21 -1
- package/src/materialization/marks/index.js +3 -0
- package/src/materialization/marks/pathOrder.js +71 -0
- package/src/materialization/marks/policies.js +6 -2
- package/src/materialization/scaleGuideDependencies.js +8 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/path.js +27 -1
- package/src/materialization/selection/items/point.js +1 -5
- package/src/materialization/selection/items/rect.js +21 -0
- package/src/materialization/selection/styles.js +20 -9
- package/src/materialization/text.js +12 -4
- package/src/renderers/canvas/fill.js +33 -0
- package/src/renderers/canvas/index.js +7 -0
- package/src/renderers/canvas/path.js +7 -4
- package/src/renderers/canvas/rect.js +9 -3
- package/src/selectors/datasets.js +4 -0
- package/src/theme/defaults.js +4 -0
- package/types/extension.d.ts +6 -0
- package/types/index.d.ts +77 -0
- package/types/program.d.ts +703 -17
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
findSelectionPolicy,
|
|
18
18
|
requireSelectionPolicy
|
|
19
19
|
} from "../../materialization/selection/policies/index.js";
|
|
20
|
+
import { legendGraphicIds } from "../../materialization/guides/resources.js";
|
|
20
21
|
import {
|
|
21
22
|
normalizeDimOthers,
|
|
22
23
|
validateUnitInterval
|
|
@@ -27,6 +28,8 @@ const SELECTOR_KEYS = Object.freeze([
|
|
|
27
28
|
"inclusive", "count", "groupBy", "ties"
|
|
28
29
|
]);
|
|
29
30
|
const SELECT_OPTIONS = Object.freeze(["id", "target", ...SELECTOR_KEYS]);
|
|
31
|
+
const EDIT_SELECTION_OPTIONS = Object.freeze(["selection", ...SELECTOR_KEYS]);
|
|
32
|
+
const REMOVE_SELECTION_OPTIONS = Object.freeze(["selection"]);
|
|
30
33
|
const HIGHLIGHT_OPTIONS = Object.freeze([
|
|
31
34
|
"id", "target", "select", "selection", "color", "opacity", "fill",
|
|
32
35
|
"stroke", "strokeWidth", "strokeDash", "shape", "size", "offset",
|
|
@@ -63,6 +66,51 @@ function hasLegendSelection(program, target, selection) {
|
|
|
63
66
|
selector.field === legend.field;
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
function categoricalLegendKind(program, target) {
|
|
70
|
+
return ["series", "color"].find(
|
|
71
|
+
kind => program.guideConfigs.legend?.[kind]?.target === target
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resetCategoricalLegendSymbols(program, target) {
|
|
76
|
+
const kind = categoricalLegendKind(program, target);
|
|
77
|
+
if (kind === undefined) return program;
|
|
78
|
+
let next = program;
|
|
79
|
+
for (const id of legendGraphicIds(kind).filter(id => id.includes("Symbol"))) {
|
|
80
|
+
const graphic = next.graphicSpec.objects[id];
|
|
81
|
+
if (graphic === undefined) continue;
|
|
82
|
+
next = graphic.type === "collection"
|
|
83
|
+
? next.editGraphics({ target: id, property: "items", value: [] })
|
|
84
|
+
: next.editGraphics({ target: id, property: "length", value: 0 });
|
|
85
|
+
}
|
|
86
|
+
return next.rematerializeLegendSymbols();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function targetHighlightEntries(program, target) {
|
|
90
|
+
return Object.entries(program.materializationConfigs.highlights ?? {})
|
|
91
|
+
.filter(([, config]) => config.target === target);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function rebuildTargetHighlights(program, target) {
|
|
95
|
+
const layer = resolveTarget(program, target, "highlight mark");
|
|
96
|
+
const highlights = targetHighlightEntries(program, target);
|
|
97
|
+
let baseline = program;
|
|
98
|
+
for (const [id] of highlights) {
|
|
99
|
+
baseline = baseline._withoutMaterializationConfig(["highlights", id]);
|
|
100
|
+
}
|
|
101
|
+
const graphic = baseline.graphicSpec.objects[target];
|
|
102
|
+
baseline = graphic.type === "collection"
|
|
103
|
+
? baseline.editGraphics({ target, property: "items", value: [] })
|
|
104
|
+
: baseline.editGraphics({ target, property: "length", value: 0 });
|
|
105
|
+
baseline = baseline[requireSelectionPolicy(
|
|
106
|
+
layer.mark.type
|
|
107
|
+
).rematerializeOp]({ id: target });
|
|
108
|
+
baseline = resetCategoricalLegendSymbols(baseline, target);
|
|
109
|
+
return highlights.length === 0
|
|
110
|
+
? baseline
|
|
111
|
+
: baseline.rematerializeMarkHighlights({ target, highlights });
|
|
112
|
+
}
|
|
113
|
+
|
|
66
114
|
function selectedKeys(args, resolved) {
|
|
67
115
|
if (args.keys === undefined) return resolved.keys;
|
|
68
116
|
if (!Array.isArray(args.keys) || !args.keys.every(key =>
|
|
@@ -86,6 +134,79 @@ export const selectMarks = action(
|
|
|
86
134
|
}
|
|
87
135
|
);
|
|
88
136
|
|
|
137
|
+
export const editMarkSelection = action(
|
|
138
|
+
{
|
|
139
|
+
op: "editMarkSelection",
|
|
140
|
+
description: "Replace one stored mark selector while preserving its identity and target."
|
|
141
|
+
},
|
|
142
|
+
function (args = {}) {
|
|
143
|
+
validateKeys(args, EDIT_SELECTION_OPTIONS, "editMarkSelection");
|
|
144
|
+
const current = resolveStoredSelection(this, args.selection);
|
|
145
|
+
const replacement = resolveMarkSelection(
|
|
146
|
+
this,
|
|
147
|
+
current.definition.target,
|
|
148
|
+
selectorFrom(args)
|
|
149
|
+
);
|
|
150
|
+
const next = this
|
|
151
|
+
._withSelectionConfig(current.id, {
|
|
152
|
+
target: current.definition.target,
|
|
153
|
+
selector: replacement.selector
|
|
154
|
+
})
|
|
155
|
+
._withContext({ currentSelection: current.id });
|
|
156
|
+
const hasDependentHighlight = Object.values(
|
|
157
|
+
next.materializationConfigs.highlights ?? {}
|
|
158
|
+
).some(config => config.selection === current.id);
|
|
159
|
+
return hasDependentHighlight
|
|
160
|
+
? rebuildTargetHighlights(next, current.definition.target)
|
|
161
|
+
: next;
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
export const removeMarkHighlight = action(
|
|
166
|
+
{
|
|
167
|
+
op: "removeMarkHighlight",
|
|
168
|
+
description: "Remove one stored highlight assignment and restore the clean mark baseline."
|
|
169
|
+
},
|
|
170
|
+
function (args = {}) {
|
|
171
|
+
validateKeys(args, REMOVE_SELECTION_OPTIONS, "removeMarkHighlight");
|
|
172
|
+
const current = resolveStoredSelection(this, args.selection);
|
|
173
|
+
const dependent = Object.entries(
|
|
174
|
+
this.materializationConfigs.highlights ?? {}
|
|
175
|
+
).filter(([, config]) => config.selection === current.id);
|
|
176
|
+
if (dependent.length === 0) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Selection "${current.id}" has no highlight assignment.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
let next = this;
|
|
182
|
+
for (const [id] of dependent) {
|
|
183
|
+
next = next._withoutMaterializationConfig(["highlights", id]);
|
|
184
|
+
}
|
|
185
|
+
return rebuildTargetHighlights(next, current.definition.target);
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
export const removeMarkSelection = action(
|
|
190
|
+
{
|
|
191
|
+
op: "removeMarkSelection",
|
|
192
|
+
description: "Remove one stored selection after removing its dependent highlight."
|
|
193
|
+
},
|
|
194
|
+
function (args = {}) {
|
|
195
|
+
validateKeys(args, REMOVE_SELECTION_OPTIONS, "removeMarkSelection");
|
|
196
|
+
const current = resolveStoredSelection(this, args.selection);
|
|
197
|
+
const hasDependentHighlight = Object.values(
|
|
198
|
+
this.materializationConfigs.highlights ?? {}
|
|
199
|
+
).some(config => config.selection === current.id);
|
|
200
|
+
let next = hasDependentHighlight
|
|
201
|
+
? this.removeMarkHighlight({ selection: current.id })
|
|
202
|
+
: this;
|
|
203
|
+
next = next._withoutMaterializationConfig(["selections", current.id]);
|
|
204
|
+
return this.context.currentSelection === current.id
|
|
205
|
+
? next._withContext({ currentSelection: undefined })
|
|
206
|
+
: next;
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
89
210
|
export const applyPointHighlight = action(
|
|
90
211
|
{ op: "applyPointHighlight", description: "Apply selected point appearance and geometry." },
|
|
91
212
|
function (args = {}) {
|
|
@@ -117,7 +238,26 @@ export const applyPointHighlight = action(
|
|
|
117
238
|
}
|
|
118
239
|
);
|
|
119
240
|
|
|
120
|
-
function
|
|
241
|
+
function transformRectangularProperties(properties, style, offset = false) {
|
|
242
|
+
if (!offset || style.offset === undefined) {
|
|
243
|
+
return { ...properties, ...style };
|
|
244
|
+
}
|
|
245
|
+
const { offset: translation, ...appearance } = style;
|
|
246
|
+
return {
|
|
247
|
+
...properties,
|
|
248
|
+
...appearance,
|
|
249
|
+
x: properties.x + translation.x,
|
|
250
|
+
y: properties.y + translation.y
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function applyRectangularHighlight(
|
|
255
|
+
program,
|
|
256
|
+
args,
|
|
257
|
+
operation,
|
|
258
|
+
markType,
|
|
259
|
+
{ offset = false } = {}
|
|
260
|
+
) {
|
|
121
261
|
validateKeys(args, INTERNAL_SELECTION_OPTIONS, operation);
|
|
122
262
|
const resolved = resolveStoredSelection(program, args.selection);
|
|
123
263
|
const keys = selectedKeys(args, resolved);
|
|
@@ -136,7 +276,7 @@ function applyRectangularHighlight(program, args, operation, markType) {
|
|
|
136
276
|
value: graphic.items.map(child => ({
|
|
137
277
|
type: child.type ?? graphic.type,
|
|
138
278
|
properties: selected.has(keyByGraphic.get(child.id))
|
|
139
|
-
?
|
|
279
|
+
? transformRectangularProperties(child.properties, args.style, offset)
|
|
140
280
|
: child.properties
|
|
141
281
|
}))
|
|
142
282
|
});
|
|
@@ -152,7 +292,13 @@ export const applyBarHighlight = action(
|
|
|
152
292
|
export const applyRectHighlight = action(
|
|
153
293
|
{ op: "applyRectHighlight", description: "Apply selected rect appearance." },
|
|
154
294
|
function (args = {}) {
|
|
155
|
-
return applyRectangularHighlight(
|
|
295
|
+
return applyRectangularHighlight(
|
|
296
|
+
this,
|
|
297
|
+
args,
|
|
298
|
+
"applyRectHighlight",
|
|
299
|
+
"rect",
|
|
300
|
+
{ offset: true }
|
|
301
|
+
);
|
|
156
302
|
}
|
|
157
303
|
);
|
|
158
304
|
|
|
@@ -368,8 +514,7 @@ export const highlightMarks = action(
|
|
|
368
514
|
const existing = next.materializationConfigs.highlights?.[selection];
|
|
369
515
|
if (existing !== undefined) {
|
|
370
516
|
next = next._withoutMaterializationConfig(["highlights", selection]);
|
|
371
|
-
|
|
372
|
-
next = next[policy.rematerializeOp]({ id: resolved.definition.target });
|
|
517
|
+
next = rebuildTargetHighlights(next, resolved.definition.target);
|
|
373
518
|
}
|
|
374
519
|
const policy = requireSelectionPolicy(layer.mark.type);
|
|
375
520
|
next = next[policy.applyHighlightOp]({ selection, style, keys });
|
|
@@ -5,14 +5,20 @@ import {
|
|
|
5
5
|
applyPointHighlight,
|
|
6
6
|
applyRuleHighlight,
|
|
7
7
|
dimUnselectedMarkItems,
|
|
8
|
+
editMarkSelection,
|
|
8
9
|
highlightMarks,
|
|
9
10
|
placeSelectedMarkItemsLast,
|
|
10
11
|
rematerializeMarkHighlights,
|
|
12
|
+
removeMarkHighlight,
|
|
13
|
+
removeMarkSelection,
|
|
11
14
|
selectMarks
|
|
12
15
|
} from "./actions.js";
|
|
13
16
|
|
|
14
17
|
export function registerSelectionActions(ProgramClass) {
|
|
15
18
|
ProgramClass.prototype.selectMarks = selectMarks;
|
|
19
|
+
ProgramClass.prototype.editMarkSelection = editMarkSelection;
|
|
20
|
+
ProgramClass.prototype.removeMarkHighlight = removeMarkHighlight;
|
|
21
|
+
ProgramClass.prototype.removeMarkSelection = removeMarkSelection;
|
|
16
22
|
ProgramClass.prototype.highlightMarks = highlightMarks;
|
|
17
23
|
ProgramClass.prototype.applyBarHighlight = applyBarHighlight;
|
|
18
24
|
ProgramClass.prototype.applyRectHighlight = applyRectHighlight;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateOptionObject } 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 "../charts/shared.js";
|
|
15
|
+
|
|
16
|
+
const OPTIONS = Object.freeze([
|
|
17
|
+
"id", "data", "coordinate", "x", "y", "split", "color", "density",
|
|
18
|
+
"area", "guides"
|
|
19
|
+
]);
|
|
20
|
+
const POSITION_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
|
|
21
|
+
const DENSITY_OPTIONS = Object.freeze([
|
|
22
|
+
"bandwidth", "extent", "steps", "kernel", "normalization", "width"
|
|
23
|
+
]);
|
|
24
|
+
const WIDTH_OPTIONS = Object.freeze(["band", "resolve"]);
|
|
25
|
+
const SPLIT_OPTIONS = Object.freeze(["field", "domain"]);
|
|
26
|
+
const AREA_OPTIONS = Object.freeze([
|
|
27
|
+
"fill", "opacity", "stroke", "strokeWidth", "curve"
|
|
28
|
+
]);
|
|
29
|
+
const CATEGORICAL_TYPES = Object.freeze(["nominal", "ordinal"]);
|
|
30
|
+
|
|
31
|
+
function inferFieldType(dataset, encoding, label) {
|
|
32
|
+
if (encoding.fieldType !== undefined) return encoding.fieldType;
|
|
33
|
+
if (typeof encoding.field !== "string" || encoding.field.length === 0) {
|
|
34
|
+
throw new TypeError(`${label} field must be a non-empty string.`);
|
|
35
|
+
}
|
|
36
|
+
const values = dataset.values
|
|
37
|
+
.map(row => row?.[encoding.field])
|
|
38
|
+
.filter(value => value !== undefined && value !== null && value !== "");
|
|
39
|
+
if (values.length === 0) {
|
|
40
|
+
throw new Error(`${label} field "${encoding.field}" has no values.`);
|
|
41
|
+
}
|
|
42
|
+
return values.every(Number.isFinite) ? "quantitative" : "nominal";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizePosition(dataset, value, label) {
|
|
46
|
+
const encoding = normalizeFieldEncoding(value, label);
|
|
47
|
+
validateOptionObject(encoding, POSITION_OPTIONS, label);
|
|
48
|
+
return {
|
|
49
|
+
...encoding,
|
|
50
|
+
fieldType: inferFieldType(dataset, encoding, label)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeDensity(value) {
|
|
55
|
+
if (value === undefined) return { options: {}, width: undefined };
|
|
56
|
+
validateOptionObject(value, DENSITY_OPTIONS, "createViolinPlot density");
|
|
57
|
+
if (value.width !== undefined) {
|
|
58
|
+
validateOptionObject(
|
|
59
|
+
value.width,
|
|
60
|
+
WIDTH_OPTIONS,
|
|
61
|
+
"createViolinPlot density.width"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const { width, ...options } = value;
|
|
65
|
+
return { options, width };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeSplit(value) {
|
|
69
|
+
if (value === undefined) return undefined;
|
|
70
|
+
validateOptionObject(value, SPLIT_OPTIONS, "createViolinPlot split");
|
|
71
|
+
if (typeof value.field !== "string" || value.field.length === 0) {
|
|
72
|
+
throw new TypeError("createViolinPlot split.field must be a non-empty string.");
|
|
73
|
+
}
|
|
74
|
+
return { ...value };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function axisGuideOptions(value, field) {
|
|
78
|
+
if (value === false) return false;
|
|
79
|
+
return {
|
|
80
|
+
...(value ?? {}),
|
|
81
|
+
title: {
|
|
82
|
+
text: field,
|
|
83
|
+
...(value?.title ?? {})
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function guideOptions(guides, x, y, category, color) {
|
|
89
|
+
if (guides === false) return false;
|
|
90
|
+
const axes = guides.axes === false
|
|
91
|
+
? false
|
|
92
|
+
: {
|
|
93
|
+
...(guides.axes ?? {}),
|
|
94
|
+
x: axisGuideOptions(guides.axes?.x, x.field),
|
|
95
|
+
y: axisGuideOptions(guides.axes?.y, y.field)
|
|
96
|
+
};
|
|
97
|
+
const redundantLegend = color?.field === category.field &&
|
|
98
|
+
!Object.hasOwn(guides, "legend");
|
|
99
|
+
return {
|
|
100
|
+
...guides,
|
|
101
|
+
axes,
|
|
102
|
+
...(redundantLegend ? { legend: false } : {})
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const createViolinPlot = action(
|
|
107
|
+
{
|
|
108
|
+
op: "createViolinPlot",
|
|
109
|
+
description: "Create a categorical kernel-density violin plot."
|
|
110
|
+
},
|
|
111
|
+
function (args = {}) {
|
|
112
|
+
validateFacadeOptions(args, OPTIONS, "createViolinPlot");
|
|
113
|
+
const id = resolveFacadeId(this, args.id, {
|
|
114
|
+
defaultId: "violinPlot",
|
|
115
|
+
operation: "createViolinPlot"
|
|
116
|
+
});
|
|
117
|
+
const data = resolveFacadeData(this, args.data, "createViolinPlot");
|
|
118
|
+
const dataset = findDataset(this, data);
|
|
119
|
+
const x = normalizePosition(dataset, args.x, "createViolinPlot x");
|
|
120
|
+
const y = normalizePosition(dataset, args.y, "createViolinPlot y");
|
|
121
|
+
const xCategorical = CATEGORICAL_TYPES.includes(x.fieldType);
|
|
122
|
+
const yCategorical = CATEGORICAL_TYPES.includes(y.fieldType);
|
|
123
|
+
if (xCategorical === yCategorical || (
|
|
124
|
+
x.fieldType !== "quantitative" && y.fieldType !== "quantitative"
|
|
125
|
+
)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
"createViolinPlot requires one categorical axis and one quantitative axis."
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const category = xCategorical ? x : y;
|
|
131
|
+
const value = xCategorical ? y : x;
|
|
132
|
+
if (value.fieldType !== "quantitative") {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"createViolinPlot requires one categorical axis and one quantitative axis."
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const split = normalizeSplit(args.split);
|
|
138
|
+
if (split?.field === category.field) {
|
|
139
|
+
throw new Error("createViolinPlot split field must differ from its category field.");
|
|
140
|
+
}
|
|
141
|
+
const color = normalizeEncoding(args.color, "createViolinPlot color");
|
|
142
|
+
if (
|
|
143
|
+
color !== undefined &&
|
|
144
|
+
![category.field, split?.field].includes(color.field)
|
|
145
|
+
) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"createViolinPlot color must encode its category or split field."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const density = normalizeDensity(args.density);
|
|
151
|
+
const area = normalizeAppearance(
|
|
152
|
+
args.area,
|
|
153
|
+
AREA_OPTIONS,
|
|
154
|
+
"createViolinPlot area"
|
|
155
|
+
);
|
|
156
|
+
if (color !== undefined && Object.hasOwn(area, "fill")) {
|
|
157
|
+
throw new Error("createViolinPlot area.fill cannot be combined with color.");
|
|
158
|
+
}
|
|
159
|
+
const guides = guideOptions(
|
|
160
|
+
normalizeGuides(args.guides, "createViolinPlot"),
|
|
161
|
+
x,
|
|
162
|
+
y,
|
|
163
|
+
category,
|
|
164
|
+
color
|
|
165
|
+
);
|
|
166
|
+
const { strokeWidth, ...areaCreate } = area;
|
|
167
|
+
const hasFillStroke = strokeWidth !== undefined && area.stroke === undefined;
|
|
168
|
+
let next = this.createAreaMark({
|
|
169
|
+
id,
|
|
170
|
+
data,
|
|
171
|
+
...areaCreate,
|
|
172
|
+
...(area.stroke === undefined || strokeWidth === undefined
|
|
173
|
+
? {}
|
|
174
|
+
: { strokeWidth })
|
|
175
|
+
});
|
|
176
|
+
if (hasFillStroke) {
|
|
177
|
+
next = next.configureAreaStrokeFromFill({ id, strokeWidth });
|
|
178
|
+
}
|
|
179
|
+
next = next.encodeDensity({
|
|
180
|
+
target: id,
|
|
181
|
+
source: data,
|
|
182
|
+
field: value.field,
|
|
183
|
+
groupBy: category.field,
|
|
184
|
+
densityChannel: xCategorical ? "x" : "y",
|
|
185
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
186
|
+
...(value.scale === undefined ? {} : { valueScale: value.scale }),
|
|
187
|
+
...density.options,
|
|
188
|
+
placement: {
|
|
189
|
+
type: "category",
|
|
190
|
+
...(density.width === undefined ? {} : { width: density.width }),
|
|
191
|
+
...(split === undefined ? {} : { split }),
|
|
192
|
+
...(category.scale === undefined ? {} : { scale: category.scale })
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
196
|
+
return applyFacadeGuides(next, guides);
|
|
197
|
+
}
|
|
198
|
+
);
|
package/src/core/textMetrics.js
CHANGED
|
@@ -21,3 +21,50 @@ export function measureTextWidth(text, { fontSize } = {}) {
|
|
|
21
21
|
0
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
function horizontalExtents(width, align) {
|
|
26
|
+
if (align === "center") return [-width / 2, width / 2];
|
|
27
|
+
if (["right", "end"].includes(align)) return [-width, 0];
|
|
28
|
+
return [0, width];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function verticalExtents(fontSize, baseline) {
|
|
32
|
+
if (baseline === "middle") return [-fontSize / 2, fontSize / 2];
|
|
33
|
+
if (["top", "hanging"].includes(baseline)) return [0, fontSize];
|
|
34
|
+
if (["bottom", "ideographic"].includes(baseline)) return [-fontSize, 0];
|
|
35
|
+
return [-fontSize * 0.8, fontSize * 0.2];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveTextBounds({
|
|
39
|
+
x,
|
|
40
|
+
y,
|
|
41
|
+
text,
|
|
42
|
+
fontSize,
|
|
43
|
+
textAlign = "left",
|
|
44
|
+
textBaseline = "alphabetic",
|
|
45
|
+
rotation = 0
|
|
46
|
+
} = {}) {
|
|
47
|
+
if (![x, y, rotation].every(Number.isFinite)) {
|
|
48
|
+
throw new TypeError("Text bounds require finite x, y, and rotation values.");
|
|
49
|
+
}
|
|
50
|
+
const width = measureTextWidth(text, { fontSize });
|
|
51
|
+
const [left, right] = horizontalExtents(width, textAlign);
|
|
52
|
+
const [top, bottom] = verticalExtents(fontSize, textBaseline);
|
|
53
|
+
const cosine = Math.cos(rotation);
|
|
54
|
+
const sine = Math.sin(rotation);
|
|
55
|
+
const corners = [
|
|
56
|
+
[left, top],
|
|
57
|
+
[right, top],
|
|
58
|
+
[right, bottom],
|
|
59
|
+
[left, bottom]
|
|
60
|
+
].map(([localX, localY]) => ({
|
|
61
|
+
x: x + localX * cosine - localY * sine,
|
|
62
|
+
y: y + localX * sine + localY * cosine
|
|
63
|
+
}));
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
left: Math.min(...corners.map(point => point.x)),
|
|
66
|
+
right: Math.max(...corners.map(point => point.x)),
|
|
67
|
+
top: Math.min(...corners.map(point => point.y)),
|
|
68
|
+
bottom: Math.max(...corners.map(point => point.y))
|
|
69
|
+
});
|
|
70
|
+
}
|
package/src/core/vocabulary.js
CHANGED
|
@@ -31,9 +31,11 @@ export const ENCODING_CHANNELS = Object.freeze([
|
|
|
31
31
|
"radius",
|
|
32
32
|
"color",
|
|
33
33
|
"strokeDash",
|
|
34
|
+
"strokeWidth",
|
|
34
35
|
"size",
|
|
35
36
|
"shape",
|
|
36
37
|
"group",
|
|
38
|
+
"pathOrder",
|
|
37
39
|
"opacity",
|
|
38
40
|
"text"
|
|
39
41
|
]);
|
|
@@ -111,7 +113,9 @@ export const POSITION_CHANNEL_DEFINITIONS = Object.freeze({
|
|
|
111
113
|
});
|
|
112
114
|
|
|
113
115
|
export const SCALED_ENCODING_CHANNELS = Object.freeze(
|
|
114
|
-
ENCODING_CHANNELS.filter(channel =>
|
|
116
|
+
ENCODING_CHANNELS.filter(channel =>
|
|
117
|
+
!["group", "pathOrder", "text"].includes(channel)
|
|
118
|
+
)
|
|
115
119
|
);
|
|
116
120
|
|
|
117
121
|
export const POSITION_ENCODING_CHANNELS = Object.freeze(
|
|
@@ -194,13 +198,21 @@ export const CATEGORICAL_LEGEND_CHANNELS = Object.freeze([
|
|
|
194
198
|
"shape"
|
|
195
199
|
]);
|
|
196
200
|
|
|
201
|
+
export const LEGEND_CHANNELS = Object.freeze([
|
|
202
|
+
...CATEGORICAL_LEGEND_CHANNELS,
|
|
203
|
+
"strokeWidth",
|
|
204
|
+
"size",
|
|
205
|
+
"opacity"
|
|
206
|
+
]);
|
|
207
|
+
|
|
197
208
|
export const LEGEND_CONFIG_KINDS = Object.freeze([
|
|
198
209
|
"series",
|
|
199
210
|
"color",
|
|
200
211
|
"size",
|
|
201
212
|
"gradient",
|
|
202
213
|
"interval",
|
|
203
|
-
"opacity"
|
|
214
|
+
"opacity",
|
|
215
|
+
"strokeWidth"
|
|
204
216
|
]);
|
|
205
217
|
|
|
206
218
|
export function includesVocabulary(vocabulary, value) {
|
package/src/grammar/arcs.js
CHANGED
|
@@ -36,42 +36,79 @@ function colorValues(rows, encoding) {
|
|
|
36
36
|
: readNominalField(rows, encoding.field);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function
|
|
39
|
+
export function readArcThetaWeights(rows, field, layerId = "arc") {
|
|
40
|
+
if (!Array.isArray(rows)) throw new TypeError("Arc theta weights require rows.");
|
|
41
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
42
|
+
throw new TypeError("Arc theta weight must be a non-empty string.");
|
|
43
|
+
}
|
|
44
|
+
const weights = rows.map((row, index) => {
|
|
45
|
+
const value = row?.[field];
|
|
46
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
47
|
+
throw new TypeError(
|
|
48
|
+
`Arc theta weight field "${field}" must contain non-negative finite numbers at row ${index}.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
});
|
|
53
|
+
if (weights.every(value => value === 0)) {
|
|
54
|
+
throw new Error(`Arc mark "${layerId}" theta weights must have a positive total.`);
|
|
55
|
+
}
|
|
56
|
+
return cloneAndFreeze(weights);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
40
60
|
const theta = layer.encoding.theta;
|
|
41
|
-
if (theta.aggregate
|
|
42
|
-
throw new Error(`Arc mark "${layer.id}" requires count or radial layout.`);
|
|
61
|
+
if (!["count", "sum"].includes(theta.aggregate)) {
|
|
62
|
+
throw new Error(`Arc mark "${layer.id}" requires count, sum, or radial layout.`);
|
|
43
63
|
}
|
|
44
64
|
const values = readNominalField(rows, theta.field);
|
|
45
65
|
const colors = colorValues(rows, layer.encoding?.color);
|
|
66
|
+
const weights = theta.aggregate === "count"
|
|
67
|
+
? rows.map(() => 1)
|
|
68
|
+
: readArcThetaWeights(rows, theta.weight, layer.id);
|
|
46
69
|
const groups = new Map(thetaScale.domain.map(value => [value, []]));
|
|
47
70
|
for (let index = 0; index < rows.length; index += 1) {
|
|
48
71
|
const group = groups.get(values[index]);
|
|
49
72
|
if (group === undefined) {
|
|
50
73
|
throw new Error(`Arc theta value "${values[index]}" is outside the scale domain.`);
|
|
51
74
|
}
|
|
52
|
-
group.push({
|
|
75
|
+
group.push({
|
|
76
|
+
row: rows[index],
|
|
77
|
+
color: colors[index],
|
|
78
|
+
sourceIndex: index,
|
|
79
|
+
weight: weights[index]
|
|
80
|
+
});
|
|
53
81
|
}
|
|
54
|
-
const total = [...groups.values()].reduce(
|
|
55
|
-
|
|
82
|
+
const total = [...groups.values()].reduce(
|
|
83
|
+
(sum, group) => sum + group.reduce((groupSum, item) => groupSum + item.weight, 0),
|
|
84
|
+
0
|
|
85
|
+
);
|
|
86
|
+
if (total === 0) throw new Error(`Arc mark "${layer.id}" has no positive theta weight.`);
|
|
56
87
|
const start = thetaScale.range[0];
|
|
57
88
|
const span = thetaScale.range[1] - thetaScale.range[0];
|
|
58
89
|
const innerRadius = frame.availableRadius * innerRadiusRatio;
|
|
59
90
|
let cursor = start;
|
|
60
91
|
const sectors = [];
|
|
61
|
-
|
|
92
|
+
const positiveValues = thetaScale.domain.filter(value =>
|
|
93
|
+
groups.get(value).some(item => item.weight > 0)
|
|
94
|
+
);
|
|
95
|
+
for (const [positiveIndex, value] of positiveValues.entries()) {
|
|
62
96
|
const group = groups.get(value);
|
|
63
|
-
|
|
64
|
-
const endTheta =
|
|
97
|
+
const aggregateValue = group.reduce((sum, item) => sum + item.weight, 0);
|
|
98
|
+
const endTheta = positiveIndex === positiveValues.length - 1
|
|
99
|
+
? thetaScale.range[1]
|
|
100
|
+
: cursor + aggregateValue / total * span;
|
|
65
101
|
const distinctColors = [...new Set(group.map(item => item.color))];
|
|
66
102
|
if (distinctColors.length > 1) {
|
|
67
103
|
throw new Error(
|
|
68
|
-
`
|
|
104
|
+
`Aggregated arc theta group "${value}" must resolve to one color value.`
|
|
69
105
|
);
|
|
70
106
|
}
|
|
71
107
|
sectors.push({
|
|
72
108
|
key: value,
|
|
73
109
|
theta: value,
|
|
74
110
|
count: group.length,
|
|
111
|
+
aggregateValue,
|
|
75
112
|
color: distinctColors[0],
|
|
76
113
|
startTheta: cursor,
|
|
77
114
|
endTheta,
|
|
@@ -147,8 +184,8 @@ export function deriveArcSectors(rows, layer, {
|
|
|
147
184
|
) {
|
|
148
185
|
throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
|
|
149
186
|
}
|
|
150
|
-
const sectors = theta.aggregate
|
|
151
|
-
?
|
|
187
|
+
const sectors = ["count", "sum"].includes(theta.aggregate)
|
|
188
|
+
? proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio)
|
|
152
189
|
: radialSectors(rows, layer, thetaScale, radiusScale);
|
|
153
190
|
return cloneAndFreeze({ sectors });
|
|
154
191
|
}
|