ggaction 0.0.6 → 0.0.8
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 +43 -0
- package/README.md +77 -14
- package/package.json +19 -2
- package/src/BasicChartProgram.js +10 -0
- package/src/actions/basic.js +21 -0
- package/src/actions/canvas/actions.js +42 -0
- package/src/actions/canvas/index.js +5 -1
- package/src/actions/categoryOrder/index.js +111 -0
- package/src/actions/charts/basic.js +13 -0
- package/src/actions/data/basic.js +11 -0
- package/src/actions/data/basicBin2d.js +43 -0
- package/src/actions/data/bin2d.js +1 -26
- package/src/actions/data/bin2dMaterialize.js +29 -0
- package/src/actions/data/index.js +6 -0
- package/src/actions/data/timeUnit.js +48 -0
- package/src/actions/encodings/angle.js +77 -0
- package/src/actions/encodings/appearance.js +6 -2
- package/src/actions/encodings/basic.js +21 -0
- package/src/actions/encodings/color/index.js +17 -13
- package/src/actions/encodings/color/layout.js +2 -0
- package/src/actions/encodings/color/policy.js +3 -0
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/position/index.js +5 -1
- package/src/actions/encodings/position/policies/area.js +40 -2
- package/src/actions/encodings/position/policies/bar.js +6 -0
- package/src/actions/encodings/position/policies/index.js +2 -0
- package/src/actions/encodings/position/policies/tick.js +19 -0
- package/src/actions/encodings/ranged.js +6 -2
- package/src/actions/encodings/remove.js +1 -1
- package/src/actions/guides/axes/index.js +10 -2
- package/src/actions/guides/basic.js +19 -0
- package/src/actions/guides/grids/grid.js +7 -3
- package/src/actions/guides/grids/index.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +23 -6
- package/src/actions/guides/legends/categorical/index.js +14 -2
- package/src/actions/guides/legends/continuous/common.js +17 -5
- package/src/actions/guides/legends/continuous/gradient.js +12 -8
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/opacity.js +70 -17
- package/src/actions/guides/legends/edit.js +13 -8
- package/src/actions/guides/legends/lane.js +468 -0
- package/src/actions/guides/legends/remove.js +3 -7
- package/src/actions/index.js +2 -0
- package/src/actions/marks/area/actions.js +3 -1
- package/src/actions/marks/area/materialize.js +12 -3
- package/src/actions/marks/bar/index.js +5 -1
- package/src/actions/marks/basic.js +11 -0
- package/src/actions/marks/index.js +2 -0
- package/src/actions/marks/line/actions.js +5 -1
- package/src/actions/marks/line/index.js +4 -1
- package/src/actions/marks/point/index.js +5 -1
- package/src/actions/marks/point/materialize.js +17 -5
- package/src/actions/marks/rect/actions.js +5 -1
- package/src/actions/marks/rect/index.js +4 -1
- package/src/actions/marks/tick/actions.js +225 -0
- package/src/actions/marks/tick/index.js +1 -0
- package/src/actions/primitives/semanticValidation/layer.js +2 -0
- package/src/actions/scales/consumers/index.js +8 -0
- package/src/actions/scales/consumers/seriesLayout.js +22 -2
- package/src/actions/scales/index.js +5 -1
- package/src/actions/scales/materialize.js +2 -0
- package/src/actions/selection/actions.js +5 -2
- package/src/basic.js +2 -0
- package/src/core/vocabulary.js +7 -3
- package/src/grammar/areaSeries.js +112 -2
- package/src/grammar/categoryOrder.js +138 -0
- package/src/grammar/direction.js +46 -0
- package/src/grammar/facets/index.js +1 -1
- package/src/grammar/pointShapes.js +34 -6
- package/src/grammar/positionCompatibility.js +4 -0
- package/src/grammar/schemas/semanticPath.js +2 -1
- package/src/grammar/seriesLayout.js +10 -2
- package/src/grammar/timeUnit.js +108 -0
- package/src/grammar/transforms.js +6 -0
- package/src/grammar/window.js +73 -7
- package/src/layout/legendLane.js +338 -0
- package/src/materialization/marks/capabilities.js +9 -0
- package/src/materialization/marks/index.js +2 -1
- package/src/materialization/marks/policies.js +13 -0
- package/src/materialization/scales/policies/series.js +9 -0
- package/src/materialization/scales/resolve.js +25 -3
- package/src/materialization/selection/items/index.js +1 -0
- package/src/materialization/selection/items/path.js +4 -1
- package/src/materialization/selection/items/tick.js +42 -0
- package/src/materialization/selection/policies/index.js +2 -0
- package/src/materialization/selection/policies/tick.js +10 -0
- package/src/renderers/canvas/index.js +96 -60
- package/src/renderers/canvas/text.js +3 -14
- package/src/renderers/pdf.js +112 -0
- package/src/renderers/svg.js +559 -0
- package/src/renderers/text.js +11 -0
- package/types/basic.d.ts +107 -0
- package/types/index.d.ts +10 -0
- package/types/pdf.d.ts +26 -0
- package/types/program.d.ts +96 -3
- package/types/svg.d.ts +11 -0
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
resolvePointJitter
|
|
13
13
|
} from "../../../grammar/jitter.js";
|
|
14
14
|
import { polarToCartesian, resolvePolarFrame } from "../../../grammar/polar.js";
|
|
15
|
+
import { resolveDirectionValues } from "../../../grammar/direction.js";
|
|
15
16
|
import { resolveGraphicBounds } from "../../../layout/canvas.js";
|
|
16
17
|
import { validateMarkOptions } from "../shared.js";
|
|
17
18
|
import {
|
|
@@ -60,8 +61,10 @@ function compactProperties(properties) {
|
|
|
60
61
|
);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function incompleteShapeGraphic(shape, properties) {
|
|
64
|
-
const type =
|
|
64
|
+
function incompleteShapeGraphic(shape, properties, angle) {
|
|
65
|
+
const type = angle !== undefined && shape === "square"
|
|
66
|
+
? "path"
|
|
67
|
+
: getPointGraphicType(shape);
|
|
65
68
|
const incomplete = type === "path"
|
|
66
69
|
? { fill: properties.fill, opacity: properties.opacity }
|
|
67
70
|
: properties;
|
|
@@ -99,6 +102,7 @@ function resolveJitterEntries({
|
|
|
99
102
|
y,
|
|
100
103
|
area,
|
|
101
104
|
shapes,
|
|
105
|
+
angles,
|
|
102
106
|
config,
|
|
103
107
|
graphic,
|
|
104
108
|
existingChildren
|
|
@@ -131,7 +135,8 @@ function resolveJitterEntries({
|
|
|
131
135
|
area: resolvedArea,
|
|
132
136
|
strokeWidth: typeof config.stroke === "string"
|
|
133
137
|
? config.strokeWidth ?? 1
|
|
134
|
-
: 0
|
|
138
|
+
: 0,
|
|
139
|
+
angle: angles?.[index]
|
|
135
140
|
});
|
|
136
141
|
return [{
|
|
137
142
|
index,
|
|
@@ -150,6 +155,7 @@ function applyPointJitter(program, {
|
|
|
150
155
|
y,
|
|
151
156
|
area,
|
|
152
157
|
shapes,
|
|
158
|
+
angles,
|
|
153
159
|
config,
|
|
154
160
|
graphic,
|
|
155
161
|
existingChildren
|
|
@@ -172,6 +178,7 @@ function applyPointJitter(program, {
|
|
|
172
178
|
y,
|
|
173
179
|
area,
|
|
174
180
|
shapes,
|
|
181
|
+
angles,
|
|
175
182
|
config,
|
|
176
183
|
graphic,
|
|
177
184
|
existingChildren
|
|
@@ -234,6 +241,7 @@ export const rematerializePointMark = action(
|
|
|
234
241
|
const area = resolveRowEncodingValues(this, layer, dataset, "size");
|
|
235
242
|
const encodedShape = resolveRowEncodingValues(this, layer, dataset, "shape");
|
|
236
243
|
const encodedOpacity = resolveRowEncodingValues(this, layer, dataset, "opacity");
|
|
244
|
+
const angles = resolveDirectionValues(dataset.values, layer.encoding?.angle);
|
|
237
245
|
const config = this.markConfigs[id] ?? {};
|
|
238
246
|
const fill = mappedFill ?? config.fill ?? DEFAULT_POINT_FILL;
|
|
239
247
|
const shapes = encodedShape ?? dataset.values.map(() => config.shape ?? "circle");
|
|
@@ -241,6 +249,7 @@ export const rematerializePointMark = action(
|
|
|
241
249
|
const constantShape = validatePointShape(config.shape ?? "circle");
|
|
242
250
|
const requiresMixedCollection =
|
|
243
251
|
encodedShape !== undefined ||
|
|
252
|
+
angles !== undefined ||
|
|
244
253
|
graphic.type === "collection" ||
|
|
245
254
|
getPointGraphicType(constantShape) !== graphic.type;
|
|
246
255
|
const jittered = applyPointJitter(this, {
|
|
@@ -251,6 +260,7 @@ export const rematerializePointMark = action(
|
|
|
251
260
|
y: positions.y,
|
|
252
261
|
area,
|
|
253
262
|
shapes,
|
|
263
|
+
angles,
|
|
254
264
|
config,
|
|
255
265
|
graphic,
|
|
256
266
|
existingChildren
|
|
@@ -273,6 +283,7 @@ export const rematerializePointMark = action(
|
|
|
273
283
|
);
|
|
274
284
|
const centerX = x?.[index] ?? existing.x;
|
|
275
285
|
const centerY = y?.[index] ?? existing.y;
|
|
286
|
+
const angle = angles?.[index];
|
|
276
287
|
if (
|
|
277
288
|
resolvedArea === undefined ||
|
|
278
289
|
!Number.isFinite(centerX) ||
|
|
@@ -283,7 +294,7 @@ export const rematerializePointMark = action(
|
|
|
283
294
|
y: centerY,
|
|
284
295
|
fill: color,
|
|
285
296
|
opacity
|
|
286
|
-
});
|
|
297
|
+
}, angle);
|
|
287
298
|
}
|
|
288
299
|
return createPointShapeGraphic({
|
|
289
300
|
shape,
|
|
@@ -295,7 +306,8 @@ export const rematerializePointMark = action(
|
|
|
295
306
|
...(config.strokeWidth === undefined
|
|
296
307
|
? {}
|
|
297
308
|
: { strokeWidth: config.strokeWidth }),
|
|
298
|
-
opacity
|
|
309
|
+
opacity,
|
|
310
|
+
angle
|
|
299
311
|
});
|
|
300
312
|
});
|
|
301
313
|
return jittered.program.editGraphics({
|
|
@@ -162,7 +162,11 @@ const editRectMark = action(
|
|
|
162
162
|
);
|
|
163
163
|
|
|
164
164
|
export function registerRectMarkActions(ProgramClass) {
|
|
165
|
-
ProgramClass.prototype.createRectMark = createRectMark;
|
|
166
165
|
ProgramClass.prototype.editRectMark = editRectMark;
|
|
166
|
+
registerBasicRectMarkActions(ProgramClass);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function registerBasicRectMarkActions(ProgramClass) {
|
|
170
|
+
ProgramClass.prototype.createRectMark = createRectMark;
|
|
167
171
|
ProgramClass.prototype.rematerializeRectMark = rematerializeRectMark;
|
|
168
172
|
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { action } from "../../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
|
+
import {
|
|
4
|
+
validateNonEmptyString,
|
|
5
|
+
validateNonNegativeFinite,
|
|
6
|
+
validatePositiveFinite,
|
|
7
|
+
validateUnitInterval
|
|
8
|
+
} from "../../../core/validation.js";
|
|
9
|
+
import {
|
|
10
|
+
centeredDirectionalSegment,
|
|
11
|
+
resolveDirectionValues
|
|
12
|
+
} from "../../../grammar/direction.js";
|
|
13
|
+
import { resolveMarkGraphicPlacement } from
|
|
14
|
+
"../../../materialization/graphicHierarchy.js";
|
|
15
|
+
import { resolveRowEncodingValues } from
|
|
16
|
+
"../../../materialization/rowEncoding.js";
|
|
17
|
+
import { findDataset } from "../../../selectors/datasets.js";
|
|
18
|
+
import { findLayer } from "../../../selectors/layers.js";
|
|
19
|
+
import { DEFAULT_COLORS } from "../../../theme/defaults.js";
|
|
20
|
+
import { rematerializeHighlightBaseline } from "../lifecycle.js";
|
|
21
|
+
import {
|
|
22
|
+
applyLayeredMarkInheritance,
|
|
23
|
+
assertMarkAvailable,
|
|
24
|
+
materializeInheritedMark,
|
|
25
|
+
resolveLayeredMarkInheritance,
|
|
26
|
+
resolveMarkData,
|
|
27
|
+
resolveMarkId,
|
|
28
|
+
validateMarkOptions
|
|
29
|
+
} from "../shared.js";
|
|
30
|
+
|
|
31
|
+
const CREATE_OPTIONS = Object.freeze([
|
|
32
|
+
"id", "data", "length", "stroke", "strokeWidth", "opacity"
|
|
33
|
+
]);
|
|
34
|
+
const EDIT_OPTIONS = Object.freeze([
|
|
35
|
+
"target", "length", "stroke", "strokeWidth", "opacity"
|
|
36
|
+
]);
|
|
37
|
+
const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_TICK_CONFIG = Object.freeze({
|
|
40
|
+
length: 14,
|
|
41
|
+
stroke: DEFAULT_COLORS.mark,
|
|
42
|
+
strokeWidth: 2,
|
|
43
|
+
opacity: 1
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function resolveTick(program, requested, operation) {
|
|
47
|
+
const candidates = program.semanticSpec.layers.filter(
|
|
48
|
+
layer => layer.mark?.type === "tick"
|
|
49
|
+
);
|
|
50
|
+
if (requested !== undefined) {
|
|
51
|
+
const id = validateUserId(requested, "Tick mark id");
|
|
52
|
+
const layer = findLayer(program, id);
|
|
53
|
+
if (layer?.mark?.type !== "tick") {
|
|
54
|
+
throw new Error(`Unknown tick mark "${id}".`);
|
|
55
|
+
}
|
|
56
|
+
return layer;
|
|
57
|
+
}
|
|
58
|
+
const current = findLayer(program, program.context.currentMark);
|
|
59
|
+
if (current?.mark?.type === "tick") return current;
|
|
60
|
+
if (candidates.length === 1) return candidates[0];
|
|
61
|
+
if (candidates.length === 0) {
|
|
62
|
+
throw new Error(`${operation} requires an existing tick mark.`);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`${operation} target is ambiguous; provide target.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateTickConfig(args, current = DEFAULT_TICK_CONFIG) {
|
|
68
|
+
return {
|
|
69
|
+
length: Object.hasOwn(args, "length")
|
|
70
|
+
? validatePositiveFinite(args.length, "Tick length")
|
|
71
|
+
: current.length,
|
|
72
|
+
stroke: Object.hasOwn(args, "stroke")
|
|
73
|
+
? validateNonEmptyString(args.stroke, "Tick stroke")
|
|
74
|
+
: current.stroke,
|
|
75
|
+
strokeWidth: Object.hasOwn(args, "strokeWidth")
|
|
76
|
+
? validateNonNegativeFinite(args.strokeWidth, "Tick strokeWidth")
|
|
77
|
+
: current.strokeWidth,
|
|
78
|
+
opacity: Object.hasOwn(args, "opacity")
|
|
79
|
+
? validateUnitInterval(args.opacity, "Tick opacity")
|
|
80
|
+
: current.opacity
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const createTickMark = action(
|
|
85
|
+
{
|
|
86
|
+
op: "createTickMark",
|
|
87
|
+
description: "Create a centered fixed-length Tick mark."
|
|
88
|
+
},
|
|
89
|
+
function (args = {}) {
|
|
90
|
+
validateMarkOptions(args, CREATE_OPTIONS, "createTickMark");
|
|
91
|
+
const id = resolveMarkId(this, args.id, {
|
|
92
|
+
defaultId: "tick",
|
|
93
|
+
label: "Tick mark id",
|
|
94
|
+
markType: "tick",
|
|
95
|
+
operation: "createTickMark"
|
|
96
|
+
});
|
|
97
|
+
const inherited = resolveLayeredMarkInheritance(this, args, "tick");
|
|
98
|
+
const { data } = resolveMarkData(this, {
|
|
99
|
+
...args,
|
|
100
|
+
...(args.data === undefined && this.context.currentData === undefined &&
|
|
101
|
+
inherited?.data !== undefined ? { data: inherited.data } : {})
|
|
102
|
+
});
|
|
103
|
+
const config = validateTickConfig(args);
|
|
104
|
+
assertMarkAvailable(this, id);
|
|
105
|
+
|
|
106
|
+
let created = this
|
|
107
|
+
.editSemantic({ property: `layer[${id}].mark.type`, value: "tick" })
|
|
108
|
+
.editSemantic({ property: `layer[${id}].data`, value: data });
|
|
109
|
+
created = applyLayeredMarkInheritance(created, id, inherited)
|
|
110
|
+
.createGraphics({
|
|
111
|
+
id,
|
|
112
|
+
type: "line",
|
|
113
|
+
length: 0,
|
|
114
|
+
...resolveMarkGraphicPlacement(created, { data, markType: "tick" })
|
|
115
|
+
})
|
|
116
|
+
._withMarkConfig(id, DEFAULT_TICK_CONFIG);
|
|
117
|
+
const materialized = materializeInheritedMark(created, id);
|
|
118
|
+
const appearance = Object.fromEntries(
|
|
119
|
+
["length", "stroke", "strokeWidth", "opacity"]
|
|
120
|
+
.filter(property => Object.hasOwn(args, property))
|
|
121
|
+
.map(property => [property, config[property]])
|
|
122
|
+
);
|
|
123
|
+
return Object.keys(appearance).length === 0
|
|
124
|
+
? materialized
|
|
125
|
+
: materialized.editTickMark({ target: id, ...appearance });
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
export const editTickMark = action(
|
|
130
|
+
{
|
|
131
|
+
op: "editTickMark",
|
|
132
|
+
description: "Edit Tick length and constant line appearance."
|
|
133
|
+
},
|
|
134
|
+
function (args = {}) {
|
|
135
|
+
validateMarkOptions(args, EDIT_OPTIONS, "editTickMark");
|
|
136
|
+
const editable = ["length", "stroke", "strokeWidth", "opacity"];
|
|
137
|
+
if (!editable.some(property => Object.hasOwn(args, property))) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"editTickMark requires length, stroke, strokeWidth, or opacity."
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const layer = resolveTick(this, args.target, "editTickMark");
|
|
143
|
+
const config = validateTickConfig(args, {
|
|
144
|
+
...DEFAULT_TICK_CONFIG,
|
|
145
|
+
...this.markConfigs[layer.id]
|
|
146
|
+
});
|
|
147
|
+
return this
|
|
148
|
+
._withMarkConfig(layer.id, config)
|
|
149
|
+
.rematerializeTickMark({ id: layer.id });
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
export const rematerializeTickMark = action(
|
|
154
|
+
{
|
|
155
|
+
op: "rematerializeTickMark",
|
|
156
|
+
description: "Recompute concrete centered Tick endpoints and appearance."
|
|
157
|
+
},
|
|
158
|
+
function (args = {}) {
|
|
159
|
+
validateMarkOptions(
|
|
160
|
+
args,
|
|
161
|
+
REMATERIALIZE_OPTIONS,
|
|
162
|
+
"rematerializeTickMark"
|
|
163
|
+
);
|
|
164
|
+
const id = validateUserId(args.id, "Tick mark id");
|
|
165
|
+
const highlighted = rematerializeHighlightBaseline(this, {
|
|
166
|
+
target: id,
|
|
167
|
+
operation: "rematerializeTickMark",
|
|
168
|
+
resetProperty: "length",
|
|
169
|
+
resetValue: 0
|
|
170
|
+
});
|
|
171
|
+
if (highlighted !== undefined) return highlighted;
|
|
172
|
+
const layer = findLayer(this, id);
|
|
173
|
+
if (layer?.mark?.type !== "tick") {
|
|
174
|
+
throw new Error(`Unknown tick mark "${id}".`);
|
|
175
|
+
}
|
|
176
|
+
const graphic = this.graphicSpec.objects[id];
|
|
177
|
+
if (graphic?.type !== "line" || !Array.isArray(graphic.items)) {
|
|
178
|
+
throw new Error(`Tick mark "${id}" requires line collection graphics.`);
|
|
179
|
+
}
|
|
180
|
+
const dataset = findDataset(this, layer.data);
|
|
181
|
+
if (dataset === undefined) {
|
|
182
|
+
throw new Error(`Tick mark "${id}" requires an existing dataset.`);
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
layer.encoding?.x?.scale === undefined ||
|
|
186
|
+
layer.encoding?.y?.scale === undefined
|
|
187
|
+
) {
|
|
188
|
+
return graphic.items.length === 0
|
|
189
|
+
? this
|
|
190
|
+
: this.editGraphics({ target: id, property: "length", value: 0 });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const x = resolveRowEncodingValues(this, layer, dataset, "x");
|
|
194
|
+
const y = resolveRowEncodingValues(this, layer, dataset, "y");
|
|
195
|
+
const angles = resolveDirectionValues(dataset.values, layer.encoding?.angle);
|
|
196
|
+
const config = validateTickConfig({}, {
|
|
197
|
+
...DEFAULT_TICK_CONFIG,
|
|
198
|
+
...this.markConfigs[id]
|
|
199
|
+
});
|
|
200
|
+
const segments = dataset.values.map((_, index) =>
|
|
201
|
+
centeredDirectionalSegment({
|
|
202
|
+
x: x[index],
|
|
203
|
+
y: y[index],
|
|
204
|
+
degrees: angles?.[index] ?? 0,
|
|
205
|
+
length: config.length
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return this
|
|
210
|
+
.editGraphics({ target: id, property: "length", value: segments.length })
|
|
211
|
+
.editGraphics({ target: id, property: "x1", value: segments.map(item => item.x1) })
|
|
212
|
+
.editGraphics({ target: id, property: "y1", value: segments.map(item => item.y1) })
|
|
213
|
+
.editGraphics({ target: id, property: "x2", value: segments.map(item => item.x2) })
|
|
214
|
+
.editGraphics({ target: id, property: "y2", value: segments.map(item => item.y2) })
|
|
215
|
+
.editGraphics({ target: id, property: "stroke", value: config.stroke })
|
|
216
|
+
.editGraphics({ target: id, property: "strokeWidth", value: config.strokeWidth })
|
|
217
|
+
.editGraphics({ target: id, property: "opacity", value: config.opacity });
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
export function registerTickMarkActions(ProgramClass) {
|
|
222
|
+
ProgramClass.prototype.createTickMark = createTickMark;
|
|
223
|
+
ProgramClass.prototype.editTickMark = editTickMark;
|
|
224
|
+
ProgramClass.prototype.rematerializeTickMark = rematerializeTickMark;
|
|
225
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerTickMarkActions } from "./actions.js";
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
validateParallelMissingPolicy
|
|
17
17
|
} from "../../../grammar/parallelCoordinates.js";
|
|
18
18
|
import { validatePathOrderDirection } from "../../../grammar/pathOrder.js";
|
|
19
|
+
import { normalizeCategoryOrder } from "../../../grammar/categoryOrder.js";
|
|
19
20
|
import { validateSemanticFieldType } from "../../../grammar/scales/index.js";
|
|
20
21
|
import { findLayer } from "../../../selectors/layers.js";
|
|
21
22
|
import { validateNonEmptySemanticString } from "./shared.js";
|
|
@@ -50,6 +51,7 @@ export function validateLayerSemanticValue(program, parsed, value) {
|
|
|
50
51
|
}
|
|
51
52
|
if (property.endsWith(".fieldType")) validateSemanticFieldType(value);
|
|
52
53
|
if (property === "encoding.pathOrder.order") validatePathOrderDirection(value);
|
|
54
|
+
if (property.endsWith(".categoryOrder")) normalizeCategoryOrder(value);
|
|
53
55
|
if (property === "encoding.parallel.dimensions") {
|
|
54
56
|
validateParallelDimensions(value, { normalized: true });
|
|
55
57
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
requireConsumerDataset
|
|
7
7
|
} from "./common.js";
|
|
8
8
|
import { resolveMarkFamilyConsumerValues } from "./families.js";
|
|
9
|
+
import { resolveCategoryOrder } from "../../../grammar/categoryOrder.js";
|
|
9
10
|
|
|
10
11
|
export { findScale, findScaleConsumers } from "./common.js";
|
|
11
12
|
export {
|
|
@@ -39,3 +40,10 @@ export function resolveConsumerValues(program, consumer) {
|
|
|
39
40
|
? family.values
|
|
40
41
|
: readConsumerFieldValues(program, consumer, dataset, scale);
|
|
41
42
|
}
|
|
43
|
+
|
|
44
|
+
export function resolveConsumerCategoryOrder(program, consumer) {
|
|
45
|
+
const order = consumer.encoding.categoryOrder;
|
|
46
|
+
if (order === undefined) return undefined;
|
|
47
|
+
const dataset = requireConsumerDataset(program, consumer);
|
|
48
|
+
return resolveCategoryOrder(dataset.values, consumer.encoding.field, order);
|
|
49
|
+
}
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
findHistogramBinIndex,
|
|
11
11
|
resolveHistogramBins
|
|
12
12
|
} from "../../../grammar/histogram.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
deriveCenteredAreaSeries,
|
|
15
|
+
deriveDensityAreaSeries
|
|
16
|
+
} from "../../../grammar/areaSeries.js";
|
|
14
17
|
import {
|
|
15
18
|
resolveSeriesLayoutDomainValues
|
|
16
19
|
} from "../../../grammar/seriesLayout.js";
|
|
@@ -122,6 +125,16 @@ function resolveAreaPartitions(program, consumer) {
|
|
|
122
125
|
dataset.transform[0].type === "density"
|
|
123
126
|
? dataset.transform[0]
|
|
124
127
|
: undefined;
|
|
128
|
+
if (
|
|
129
|
+
transform === undefined &&
|
|
130
|
+
layer.encoding?.y?.stack === "center" &&
|
|
131
|
+
layer.encoding?.x !== undefined
|
|
132
|
+
) {
|
|
133
|
+
const derived = deriveCenteredAreaSeries(dataset.values, layer);
|
|
134
|
+
return Array.from({ length: derived.series[0].values.length }, (_, index) =>
|
|
135
|
+
derived.series.map(series => series.values[index].y)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
125
138
|
if (transform === undefined) return undefined;
|
|
126
139
|
const derived = deriveDensityAreaSeries(dataset.values, layer, transform);
|
|
127
140
|
if (derived.mode !== "y-density") return undefined;
|
|
@@ -153,10 +166,17 @@ export function resolveSeriesLayoutScaleValues(program, consumer) {
|
|
|
153
166
|
}
|
|
154
167
|
if (consumer.channel !== "y") return undefined;
|
|
155
168
|
if (consumer.layer.mark?.type === "area") {
|
|
156
|
-
const layout = consumer.layer.encoding?.
|
|
169
|
+
const layout = consumer.layer.encoding?.y?.stack === "center"
|
|
170
|
+
? "center"
|
|
171
|
+
: consumer.layer.encoding?.color?.layout;
|
|
157
172
|
if (layout === undefined || layout === "overlay") return undefined;
|
|
158
173
|
const partitions = resolveAreaPartitions(program, consumer);
|
|
159
174
|
if (partitions === undefined) {
|
|
175
|
+
if (
|
|
176
|
+
layout === "center" &&
|
|
177
|
+
consumer.layer.encoding?.y?.stack === "center" &&
|
|
178
|
+
consumer.layer.encoding?.x === undefined
|
|
179
|
+
) return undefined;
|
|
160
180
|
throw new Error(
|
|
161
181
|
`Area layout "${layout}" currently requires vertical density series.`
|
|
162
182
|
);
|
|
@@ -4,8 +4,12 @@ import { rematerializeScale } from "./materialize.js";
|
|
|
4
4
|
import { setQuantitativeColorScale } from "./quantitativeColor.js";
|
|
5
5
|
|
|
6
6
|
export function registerScaleActions(ProgramClass) {
|
|
7
|
-
ProgramClass.prototype.createScale = createScale;
|
|
8
7
|
ProgramClass.prototype.editScale = editScale;
|
|
8
|
+
registerBasicScaleActions(ProgramClass);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function registerBasicScaleActions(ProgramClass) {
|
|
12
|
+
ProgramClass.prototype.createScale = createScale;
|
|
9
13
|
ProgramClass.prototype.rematerializeScale = rematerializeScale;
|
|
10
14
|
ProgramClass.prototype.setQuantitativeColorScale = setQuantitativeColorScale;
|
|
11
15
|
}
|
|
@@ -19,6 +19,7 @@ import { resolveScaleMaterialization } from
|
|
|
19
19
|
import {
|
|
20
20
|
findScale,
|
|
21
21
|
findScaleConsumers,
|
|
22
|
+
resolveConsumerCategoryOrder,
|
|
22
23
|
resolveConsumerValues,
|
|
23
24
|
resolveSeriesLayoutScaleValues
|
|
24
25
|
} from "./consumers/index.js";
|
|
@@ -62,6 +63,7 @@ export const rematerializeScale = action(
|
|
|
62
63
|
const valuesByConsumer = consumers.map(consumer => ({
|
|
63
64
|
consumer,
|
|
64
65
|
values: resolveConsumerValues(this, consumer),
|
|
66
|
+
categoryOrder: resolveConsumerCategoryOrder(this, consumer),
|
|
65
67
|
seriesLayout: resolveSeriesLayoutScaleValues(this, consumer)
|
|
66
68
|
}));
|
|
67
69
|
const resolvedScale = resolveScaleMaterialization({
|
|
@@ -340,8 +340,11 @@ export const applyRuleHighlight = action(
|
|
|
340
340
|
validateKeys(args, INTERNAL_SELECTION_OPTIONS, "applyRuleHighlight");
|
|
341
341
|
const resolved = resolveStoredSelection(this, args.selection);
|
|
342
342
|
const keys = selectedKeys(args, resolved);
|
|
343
|
-
if (
|
|
344
|
-
|
|
343
|
+
if (
|
|
344
|
+
resolved.items.length > 0 &&
|
|
345
|
+
!["rule", "tick"].includes(resolved.items[0]?.markType)
|
|
346
|
+
) {
|
|
347
|
+
throw new Error("applyRuleHighlight requires a rule or Tick selection.");
|
|
345
348
|
}
|
|
346
349
|
if (keys.length === 0) return this;
|
|
347
350
|
const selected = new Set(keys);
|
package/src/basic.js
ADDED
package/src/core/vocabulary.js
CHANGED
|
@@ -5,6 +5,7 @@ export const MARK_TYPES = Object.freeze([
|
|
|
5
5
|
"area",
|
|
6
6
|
"arc",
|
|
7
7
|
"rule",
|
|
8
|
+
"tick",
|
|
8
9
|
"text",
|
|
9
10
|
"rect"
|
|
10
11
|
]);
|
|
@@ -16,6 +17,7 @@ export const MARK_GRAPHIC_TYPES = Object.freeze({
|
|
|
16
17
|
area: Object.freeze(["path"]),
|
|
17
18
|
arc: Object.freeze(["path"]),
|
|
18
19
|
rule: Object.freeze(["line"]),
|
|
20
|
+
tick: Object.freeze(["line"]),
|
|
19
21
|
text: Object.freeze(["text"]),
|
|
20
22
|
rect: Object.freeze(["rect"])
|
|
21
23
|
});
|
|
@@ -34,6 +36,7 @@ export const ENCODING_CHANNELS = Object.freeze([
|
|
|
34
36
|
"strokeWidth",
|
|
35
37
|
"size",
|
|
36
38
|
"shape",
|
|
39
|
+
"angle",
|
|
37
40
|
"group",
|
|
38
41
|
"pathOrder",
|
|
39
42
|
"opacity",
|
|
@@ -41,7 +44,7 @@ export const ENCODING_CHANNELS = Object.freeze([
|
|
|
41
44
|
]);
|
|
42
45
|
|
|
43
46
|
const CARTESIAN_MARK_TYPES = Object.freeze([
|
|
44
|
-
"point", "line", "bar", "area", "rule", "text", "rect"
|
|
47
|
+
"point", "line", "bar", "area", "rule", "tick", "text", "rect"
|
|
45
48
|
]);
|
|
46
49
|
const POLAR_MARK_TYPES = Object.freeze(["point", "line", "arc"]);
|
|
47
50
|
|
|
@@ -114,7 +117,7 @@ export const POSITION_CHANNEL_DEFINITIONS = Object.freeze({
|
|
|
114
117
|
|
|
115
118
|
export const SCALED_ENCODING_CHANNELS = Object.freeze(
|
|
116
119
|
ENCODING_CHANNELS.filter(channel =>
|
|
117
|
-
!["group", "pathOrder", "text"].includes(channel)
|
|
120
|
+
!["angle", "group", "pathOrder", "text"].includes(channel)
|
|
118
121
|
)
|
|
119
122
|
);
|
|
120
123
|
|
|
@@ -185,12 +188,13 @@ export function getMarkGraphicTypes(markType) {
|
|
|
185
188
|
export const COLOR_LAYOUTS = Object.freeze([
|
|
186
189
|
"stack",
|
|
187
190
|
"fill",
|
|
191
|
+
"center",
|
|
188
192
|
"group",
|
|
189
193
|
"overlay",
|
|
190
194
|
"diverging"
|
|
191
195
|
]);
|
|
192
196
|
|
|
193
|
-
export const STACK_MODES = Object.freeze(["zero", "normalize"]);
|
|
197
|
+
export const STACK_MODES = Object.freeze(["zero", "normalize", "center"]);
|
|
194
198
|
|
|
195
199
|
export const CATEGORICAL_LEGEND_CHANNELS = Object.freeze([
|
|
196
200
|
"color",
|
|
@@ -120,6 +120,110 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
export function deriveCenteredAreaSeries(rows, layer) {
|
|
124
|
+
if (layer?.mark?.type !== "area") {
|
|
125
|
+
throw new Error("Centered area series derivation requires an area mark.");
|
|
126
|
+
}
|
|
127
|
+
const { x, y, x2, y2, group, color } = layer.encoding ?? {};
|
|
128
|
+
if (
|
|
129
|
+
!["quantitative", "temporal"].includes(x?.fieldType) ||
|
|
130
|
+
y?.fieldType !== "quantitative" ||
|
|
131
|
+
y?.stack !== "center" ||
|
|
132
|
+
x2 !== undefined ||
|
|
133
|
+
y2 !== undefined
|
|
134
|
+
) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Centered area mark "${layer.id}" requires one x field and one quantitative y field without ranged endpoints.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (group?.fieldType !== "nominal") {
|
|
140
|
+
throw new Error(`Centered area mark "${layer.id}" requires a nominal group encoding.`);
|
|
141
|
+
}
|
|
142
|
+
if (color !== undefined && color.field !== group.field) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Centered area color on mark "${layer.id}" must match its group field.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const xValues = x.fieldType === "temporal"
|
|
148
|
+
? readTemporalField(rows, x.field)
|
|
149
|
+
: readQuantitativeField(rows, x.field);
|
|
150
|
+
const yValues = readQuantitativeField(rows, y.field);
|
|
151
|
+
const groups = readNominalField(rows, group.field);
|
|
152
|
+
const groupOrder = [];
|
|
153
|
+
const positions = new Set();
|
|
154
|
+
const byGroup = new Map();
|
|
155
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
156
|
+
const key = groups[index];
|
|
157
|
+
if (!byGroup.has(key)) {
|
|
158
|
+
groupOrder.push(key);
|
|
159
|
+
byGroup.set(key, new Map());
|
|
160
|
+
}
|
|
161
|
+
const values = byGroup.get(key);
|
|
162
|
+
if (values.has(xValues[index])) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Centered area mark "${layer.id}" has duplicate ${group.field}/${x.field} rows.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
values.set(xValues[index], yValues[index]);
|
|
168
|
+
positions.add(xValues[index]);
|
|
169
|
+
}
|
|
170
|
+
const orderedPositions = [...positions].sort((left, right) => left - right);
|
|
171
|
+
if (groupOrder.length === 0 || orderedPositions.length < 2) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Centered area mark "${layer.id}" requires at least two aligned positions.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
for (const [key, values] of byGroup) {
|
|
177
|
+
if (
|
|
178
|
+
values.size !== orderedPositions.length ||
|
|
179
|
+
orderedPositions.some(position => !values.has(position))
|
|
180
|
+
) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Centered area series "${String(key)}" requires one aligned value at every x position.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const valuesBySeries = groupOrder.map(() => []);
|
|
188
|
+
for (const position of orderedPositions) {
|
|
189
|
+
const partition = groupOrder.map(key => byGroup.get(key).get(position));
|
|
190
|
+
const segments = new Map(
|
|
191
|
+
layoutSeriesPartition(partition, "center").map(segment => [
|
|
192
|
+
segment.index,
|
|
193
|
+
segment
|
|
194
|
+
])
|
|
195
|
+
);
|
|
196
|
+
let endpoint = -partition.reduce((sum, value) => sum + value, 0) / 2;
|
|
197
|
+
for (let index = 0; index < groupOrder.length; index += 1) {
|
|
198
|
+
const segment = segments.get(index);
|
|
199
|
+
valuesBySeries[index].push({
|
|
200
|
+
x: position,
|
|
201
|
+
y: partition[index],
|
|
202
|
+
lower: segment?.start ?? endpoint,
|
|
203
|
+
upper: segment?.end ?? endpoint
|
|
204
|
+
});
|
|
205
|
+
if (segment !== undefined) endpoint = segment.end;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const series = groupOrder.map((key, index) => ({
|
|
209
|
+
key: {
|
|
210
|
+
[group.field]: key,
|
|
211
|
+
...(color === undefined ? {} : { [color.field]: key })
|
|
212
|
+
},
|
|
213
|
+
values: valuesBySeries[index]
|
|
214
|
+
}));
|
|
215
|
+
return cloneAndFreeze({
|
|
216
|
+
mode: "y-center",
|
|
217
|
+
orientation: "vertical",
|
|
218
|
+
xValues: orderedPositions,
|
|
219
|
+
yValues: series.flatMap(item => item.values.flatMap(value => [
|
|
220
|
+
value.lower,
|
|
221
|
+
value.upper
|
|
222
|
+
])),
|
|
223
|
+
series
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
123
227
|
export function deriveDensityAreaSeries(rows, layer, transform) {
|
|
124
228
|
if (layer?.mark?.type !== "area") {
|
|
125
229
|
throw new Error("Density area derivation requires a semantic area mark.");
|
|
@@ -233,13 +337,19 @@ export function layoutDensityAreaSeries(derived, layout = "overlay") {
|
|
|
233
337
|
segment
|
|
234
338
|
])
|
|
235
339
|
);
|
|
340
|
+
let zeroThicknessEndpoint = layout === "center"
|
|
341
|
+
? -densities.reduce((sum, value) => sum + value, 0) / 2
|
|
342
|
+
: 0;
|
|
236
343
|
for (let index = 0; index < derived.series.length; index += 1) {
|
|
237
344
|
const segment = segments.get(index);
|
|
238
345
|
valuesBySeries[index].push({
|
|
239
346
|
x,
|
|
240
|
-
lower: segment?.start ??
|
|
241
|
-
upper: segment?.end ??
|
|
347
|
+
lower: segment?.start ?? zeroThicknessEndpoint,
|
|
348
|
+
upper: segment?.end ?? zeroThicknessEndpoint
|
|
242
349
|
});
|
|
350
|
+
if (segment !== undefined && ["stack", "fill", "center"].includes(layout)) {
|
|
351
|
+
zeroThicknessEndpoint = segment.end;
|
|
352
|
+
}
|
|
243
353
|
}
|
|
244
354
|
}
|
|
245
355
|
|