ggaction 0.0.2 → 0.0.3
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 +28 -0
- package/README.md +12 -1
- package/package.json +6 -1
- package/src/actions/boxPlots/components.js +3 -3
- package/src/actions/boxPlots/edit.js +277 -0
- package/src/actions/boxPlots/index.js +2 -0
- package/src/actions/boxPlots/materialize.js +12 -1
- package/src/actions/boxPlots/options.js +25 -14
- package/src/actions/canvas/actions.js +21 -17
- package/src/actions/composition/actions.js +247 -0
- package/src/actions/composition/index.js +1 -0
- package/src/actions/coordinates/actions.js +2 -5
- package/src/actions/data/density.js +7 -1
- package/src/actions/data/filter.js +3 -1
- package/src/actions/encodings/appearance.js +11 -0
- package/src/actions/encodings/barWidth.js +5 -3
- package/src/actions/encodings/color.js +120 -25
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +90 -72
- package/src/actions/encodings/position/apply.js +66 -0
- package/src/actions/encodings/position/policies/arc.js +26 -0
- package/src/actions/encodings/position/policies/bar.js +9 -5
- package/src/actions/encodings/position/policies/index.js +5 -1
- package/src/actions/encodings/position/policies/line.js +20 -3
- package/src/actions/encodings/position/resolve.js +57 -8
- package/src/actions/encodings/position.js +44 -90
- package/src/actions/encodings/ranged.js +28 -6
- package/src/actions/encodings/shared.js +17 -21
- package/src/actions/encodings/text.js +73 -0
- package/src/actions/errorBands/create.js +37 -29
- package/src/actions/errorBands/edit.js +206 -0
- package/src/actions/errorBands/index.js +9 -0
- package/src/actions/errorBands/options.js +39 -0
- package/src/actions/errorBars/create.js +50 -43
- package/src/actions/errorBars/edit.js +130 -0
- package/src/actions/errorBars/index.js +3 -0
- package/src/actions/errorBars/options.js +52 -0
- package/src/actions/facets/actions.js +162 -0
- package/src/actions/facets/derive.js +200 -0
- package/src/actions/facets/guides.js +16 -0
- package/src/actions/facets/index.js +1 -0
- package/src/actions/facets/replay.js +69 -0
- package/src/actions/guides/applicability.js +113 -0
- package/src/actions/guides/axes/axes.js +70 -16
- package/src/actions/guides/axes/edit.js +126 -0
- package/src/actions/guides/axes/index.js +12 -0
- package/src/actions/guides/axes/labels.js +21 -18
- package/src/actions/guides/axes/remove.js +60 -0
- package/src/actions/guides/axes/tickGroups.js +7 -5
- package/src/actions/guides/axes/ticks.js +9 -4
- package/src/actions/guides/axes/titles.js +7 -5
- package/src/actions/guides/grids/grid.js +144 -15
- package/src/actions/guides/grids/resolve.js +5 -13
- package/src/actions/guides/guides.js +17 -75
- package/src/actions/guides/index.js +6 -0
- package/src/actions/guides/legends/categorical/actions.js +48 -9
- package/src/actions/guides/legends/categorical/components.js +16 -16
- package/src/actions/guides/legends/categorical/index.js +14 -14
- package/src/actions/guides/legends/categorical/layout.js +3 -1
- package/src/actions/guides/legends/categorical/options.js +2 -5
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +14 -14
- package/src/actions/guides/legends/continuous/common.js +7 -7
- package/src/actions/guides/legends/continuous/interval.js +5 -5
- package/src/actions/guides/legends/edit.js +2 -6
- package/src/actions/guides/legends/focused.js +63 -0
- package/src/actions/guides/legends/index.js +4 -0
- package/src/actions/guides/legends/remove.js +64 -0
- package/src/actions/guides/legends/size.js +12 -14
- package/src/actions/guides/polar/axes/facade.js +219 -0
- package/src/actions/guides/polar/axes/labels.js +188 -0
- package/src/actions/guides/polar/axes/lines.js +113 -0
- package/src/actions/guides/polar/axes/shared.js +112 -0
- package/src/actions/guides/polar/axes/ticks.js +164 -0
- package/src/actions/guides/polar/axes/titles.js +165 -0
- package/src/actions/guides/polar/axes.js +55 -0
- package/src/actions/guides/polar/grids.js +252 -0
- package/src/actions/guides/polar/index.js +2 -0
- package/src/actions/guides/polar/resolve.js +269 -0
- package/src/actions/index.js +4 -0
- package/src/actions/marks/arc/actions.js +239 -0
- package/src/actions/marks/arc/index.js +1 -0
- package/src/actions/marks/{area.js → area/actions.js} +51 -38
- package/src/actions/marks/area/index.js +4 -0
- package/src/actions/marks/bar/create.js +26 -5
- package/src/actions/marks/bar/materialize.js +25 -31
- package/src/actions/marks/index.js +12 -4
- package/src/actions/marks/lifecycle.js +27 -0
- package/src/actions/marks/line/actions.js +373 -0
- package/src/actions/marks/line/index.js +1 -0
- package/src/actions/marks/{point.js → point/actions.js} +67 -108
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/rect/actions.js +165 -0
- package/src/actions/marks/rect/index.js +1 -0
- package/src/actions/marks/remove.js +214 -0
- package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
- package/src/actions/marks/rule/index.js +1 -0
- package/src/actions/marks/shared.js +156 -25
- package/src/actions/marks/text/actions.js +192 -0
- package/src/actions/marks/text/index.js +1 -0
- package/src/actions/primitives/createGraphics.js +13 -6
- package/src/actions/primitives/editGraphics.js +10 -3
- package/src/actions/primitives/semantic.js +26 -3
- package/src/actions/primitives/semanticValue.js +25 -3
- package/src/actions/regression/components.js +1 -1
- package/src/actions/regression/create.js +18 -1
- package/src/actions/regression/edit.js +247 -0
- package/src/actions/regression/index.js +3 -0
- package/src/actions/regression/resolve.js +3 -1
- package/src/actions/scales/consumers.js +15 -6
- package/src/actions/scales/definitions.js +13 -4
- package/src/actions/scales/edit.js +42 -19
- package/src/actions/scales/materialize.js +57 -408
- package/src/actions/selection/actions.js +38 -24
- package/src/actions/selection/index.js +2 -0
- package/src/actions/titles/actions.js +63 -2
- package/src/actions/titles/index.js +2 -0
- package/src/actions/titles/resolve.js +12 -17
- package/src/composition.js +10 -0
- package/src/core/ChartProgram.js +200 -2
- package/src/core/action.js +10 -0
- package/src/core/validation.js +16 -0
- package/src/core/vocabulary.js +154 -4
- package/src/grammar/arcs.js +154 -0
- package/src/grammar/bars/aggregate.js +2 -2
- package/src/grammar/bars/geometry.js +3 -3
- package/src/grammar/bars/policy.js +7 -1
- package/src/grammar/coordinates.js +3 -7
- package/src/grammar/density.js +20 -1
- package/src/grammar/facets/dependencies.js +185 -0
- package/src/grammar/facets/guides.js +257 -0
- package/src/grammar/facets/scales.js +254 -0
- package/src/grammar/facets.js +122 -0
- package/src/grammar/lineSeries.js +111 -3
- package/src/grammar/polar.js +102 -0
- package/src/grammar/polarGuides.js +243 -0
- package/src/grammar/polarLineCommands.js +43 -0
- package/src/grammar/polarPaths.js +161 -0
- package/src/grammar/positionCompatibility.js +18 -2
- package/src/grammar/rects.js +26 -0
- package/src/grammar/scales/continuous.js +6 -1
- package/src/grammar/scales/fields.js +7 -0
- package/src/grammar/scales/ordinal.js +5 -2
- package/src/grammar/scales/policies.js +1 -1
- package/src/grammar/schemas/graphic.js +1 -1
- package/src/grammar/schemas/graphicBounds.js +64 -9
- package/src/grammar/schemas/graphicTree.js +44 -10
- package/src/grammar/schemas/semanticPath.js +31 -9
- package/src/grammar/seriesLayout.js +13 -2
- package/src/grammar/text.js +71 -0
- package/src/grammar/ticks.js +50 -13
- package/src/index.js +1 -0
- package/src/layout/composition.js +310 -0
- package/src/layout/facets.js +131 -0
- package/src/layout/title.js +6 -0
- package/src/materialization/bars/aggregate.js +5 -2
- package/src/materialization/bars/grouped.js +75 -37
- package/src/materialization/composition.js +152 -0
- package/src/materialization/compositionSnapshot.js +114 -0
- package/src/materialization/dependencies.js +71 -16
- package/src/materialization/encodings.js +14 -28
- package/src/materialization/facetGuides.js +426 -0
- package/src/materialization/facets.js +214 -0
- package/src/materialization/graphicHierarchy.js +15 -5
- package/src/materialization/guides/resources.js +46 -0
- package/src/materialization/marks.js +220 -14
- package/src/materialization/planner.js +105 -12
- package/src/materialization/rect.js +138 -0
- package/src/materialization/rectConfig.js +38 -0
- package/src/materialization/rowEncoding.js +33 -0
- package/src/materialization/scales/map.js +40 -0
- package/src/materialization/scales/policies/arc.js +37 -0
- package/src/materialization/scales/policies/bar.js +132 -0
- package/src/materialization/scales/policies/series.js +76 -0
- package/src/materialization/scales/resolve.js +326 -0
- package/src/materialization/selection/items/arc.js +29 -0
- package/src/materialization/selection/items/bar.js +255 -0
- package/src/materialization/selection/items/common.js +131 -0
- package/src/materialization/selection/items/index.js +6 -0
- package/src/materialization/selection/items/path.js +59 -0
- package/src/materialization/selection/items/point.js +55 -0
- package/src/materialization/selection/items/rect.js +23 -0
- package/src/materialization/selection/items/rule.js +25 -0
- package/src/materialization/selection/policies/arc.js +10 -0
- package/src/materialization/selection/policies/area.js +1 -1
- package/src/materialization/selection/policies/bar.js +1 -1
- package/src/materialization/selection/policies/index.js +5 -1
- package/src/materialization/selection/policies/line.js +1 -1
- package/src/materialization/selection/policies/point.js +1 -1
- package/src/materialization/selection/policies/rect.js +10 -0
- package/src/materialization/selection/policies/rule.js +1 -1
- package/src/materialization/selection/styles.js +24 -16
- package/src/materialization/text.js +156 -0
- package/src/renderers/canvas/index.js +68 -22
- package/types/extension.d.ts +1 -0
- package/types/index.d.ts +56 -1
- package/types/program.d.ts +664 -27
- package/src/actions/marks/line.js +0 -248
- package/src/materialization/selection/items.js +0 -503
|
@@ -64,8 +64,13 @@ function readSeriesFields(rows, layer) {
|
|
|
64
64
|
const encoding = layer.encoding?.[channel];
|
|
65
65
|
|
|
66
66
|
if (encoding?.field === undefined) continue;
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const categorical = channel === "color"
|
|
68
|
+
? ["nominal", "ordinal"].includes(encoding.fieldType)
|
|
69
|
+
: encoding.fieldType === "nominal";
|
|
70
|
+
if (!categorical) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Line ${channel} encoding on mark "${layer.id}" must be categorical.`
|
|
73
|
+
);
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
if (!values.has(encoding.field)) {
|
|
@@ -81,7 +86,7 @@ function groupKey(values) {
|
|
|
81
86
|
return JSON.stringify(values);
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
function deriveCartesianLineSeries(rows, layer) {
|
|
85
90
|
const { x, y, isAggregate, directTemporal } = requireLineEncoding(layer);
|
|
86
91
|
if (isAggregate) {
|
|
87
92
|
validateAggregateFieldValues(rows, y.field, y.fieldType);
|
|
@@ -195,3 +200,106 @@ export function deriveLineSeries(rows, layer) {
|
|
|
195
200
|
series
|
|
196
201
|
});
|
|
197
202
|
}
|
|
203
|
+
|
|
204
|
+
function requirePolarLineEncoding(layer) {
|
|
205
|
+
if (layer?.mark?.type !== "line") {
|
|
206
|
+
throw new Error("Polar line series derivation requires a semantic line mark.");
|
|
207
|
+
}
|
|
208
|
+
const theta = layer.encoding?.theta;
|
|
209
|
+
const radius = layer.encoding?.radius;
|
|
210
|
+
if (theta === undefined || radius === undefined) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Polar line mark "${layer.id}" requires theta and radius encodings.`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (!["quantitative", "temporal", "ordinal", "nominal"].includes(
|
|
216
|
+
theta.fieldType
|
|
217
|
+
)) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Polar line mark "${layer.id}" has unsupported theta field type.`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (radius.fieldType !== "quantitative") {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Polar line mark "${layer.id}" requires a quantitative radius encoding.`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return { theta, radius };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readThetaValues(rows, encoding) {
|
|
231
|
+
if (["nominal", "ordinal"].includes(encoding.fieldType)) {
|
|
232
|
+
return readNominalField(rows, encoding.field);
|
|
233
|
+
}
|
|
234
|
+
return encoding.fieldType === "temporal"
|
|
235
|
+
? readTemporalField(rows, encoding.field)
|
|
236
|
+
: readQuantitativeField(rows, encoding.field);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function thetaOrder(values, fieldType, domain) {
|
|
240
|
+
if (!["nominal", "ordinal"].includes(fieldType)) return values;
|
|
241
|
+
const order = domain ?? [...new Set(values)];
|
|
242
|
+
const indices = new Map(order.map((value, index) => [value, index]));
|
|
243
|
+
for (const value of values) {
|
|
244
|
+
if (!indices.has(value)) {
|
|
245
|
+
throw new Error(`Polar line theta domain does not contain "${value}".`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return values.map(value => indices.get(value));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function derivePolarLineSeries(rows, layer, { thetaDomain } = {}) {
|
|
252
|
+
const { theta, radius } = requirePolarLineEncoding(layer);
|
|
253
|
+
const thetaValues = readThetaValues(rows, theta);
|
|
254
|
+
const radiusValues = readQuantitativeField(rows, radius.field);
|
|
255
|
+
const sortValues = thetaOrder(thetaValues, theta.fieldType, thetaDomain);
|
|
256
|
+
const seriesFields = readSeriesFields(rows, layer);
|
|
257
|
+
const groups = new Map();
|
|
258
|
+
|
|
259
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
260
|
+
const dimensions = seriesFields.fields.map(
|
|
261
|
+
field => seriesFields.values.get(field)[index]
|
|
262
|
+
);
|
|
263
|
+
const key = groupKey(dimensions);
|
|
264
|
+
const series = groups.get(key) ?? {
|
|
265
|
+
key: Object.fromEntries(
|
|
266
|
+
seriesFields.fields.map((field, item) => [field, dimensions[item]])
|
|
267
|
+
),
|
|
268
|
+
values: []
|
|
269
|
+
};
|
|
270
|
+
series.values.push({
|
|
271
|
+
theta: thetaValues[index],
|
|
272
|
+
radius: radiusValues[index],
|
|
273
|
+
order: sortValues[index],
|
|
274
|
+
sourceIndex: index
|
|
275
|
+
});
|
|
276
|
+
groups.set(key, series);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const series = [...groups.values()].flatMap(item => {
|
|
280
|
+
const values = item.values.sort(
|
|
281
|
+
(left, right) => left.order - right.order ||
|
|
282
|
+
left.sourceIndex - right.sourceIndex
|
|
283
|
+
);
|
|
284
|
+
return values.length < 2 ? [] : [{ key: item.key, values }];
|
|
285
|
+
});
|
|
286
|
+
if (series.length === 0) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Polar line series on mark "${layer.id}" requires at least two points.`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return cloneAndFreeze({
|
|
292
|
+
thetaFieldType: theta.fieldType,
|
|
293
|
+
thetaValues: series.flatMap(item => item.values.map(value => value.theta)),
|
|
294
|
+
radiusValues: series.flatMap(item => item.values.map(value => value.radius)),
|
|
295
|
+
series
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function deriveLineSeries(rows, layer, options) {
|
|
300
|
+
const polar = layer?.encoding?.theta !== undefined ||
|
|
301
|
+
layer?.encoding?.radius !== undefined;
|
|
302
|
+
return polar
|
|
303
|
+
? derivePolarLineSeries(rows, layer, options)
|
|
304
|
+
: deriveCartesianLineSeries(rows, layer);
|
|
305
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validatePair } from "./scales/validation.js";
|
|
3
|
+
|
|
4
|
+
function validateBounds(bounds) {
|
|
5
|
+
if (
|
|
6
|
+
!isPlainObject(bounds) ||
|
|
7
|
+
![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) ||
|
|
8
|
+
bounds.width < 0 ||
|
|
9
|
+
bounds.height < 0
|
|
10
|
+
) {
|
|
11
|
+
throw new TypeError(
|
|
12
|
+
"Polar geometry requires finite, non-negative graphical bounds."
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return bounds;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolvePolarFrame(bounds) {
|
|
19
|
+
validateBounds(bounds);
|
|
20
|
+
return cloneAndFreeze({
|
|
21
|
+
centerX: bounds.x + bounds.width / 2,
|
|
22
|
+
centerY: bounds.y + bounds.height / 2,
|
|
23
|
+
availableRadius: Math.min(bounds.width, bounds.height) / 2
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function validateThetaRange(range) {
|
|
28
|
+
if (range === "auto") return range;
|
|
29
|
+
const validated = validatePair(range, "Theta scale range");
|
|
30
|
+
if (Math.abs(validated[1] - validated[0]) > 360) {
|
|
31
|
+
throw new RangeError("Theta scale range span must not exceed 360 degrees.");
|
|
32
|
+
}
|
|
33
|
+
return validated;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateRadialRange(range, availableRadius) {
|
|
37
|
+
if (range === "auto") return range;
|
|
38
|
+
const validated = validatePair(range, "Radius scale range");
|
|
39
|
+
if (validated.some(value => value < 0)) {
|
|
40
|
+
throw new RangeError("Radius scale range values must be non-negative.");
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
availableRadius !== undefined &&
|
|
44
|
+
validated.some(value => value > availableRadius)
|
|
45
|
+
) {
|
|
46
|
+
throw new RangeError(
|
|
47
|
+
`Radius scale range must fit within the available radius ${availableRadius}.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return validated;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolvePolarScaleRange(range, channel, bounds) {
|
|
54
|
+
if (channel === "theta") {
|
|
55
|
+
return range === "auto"
|
|
56
|
+
? cloneAndFreeze([0, 360])
|
|
57
|
+
: validateThetaRange(range);
|
|
58
|
+
}
|
|
59
|
+
if (channel !== "radius") {
|
|
60
|
+
throw new Error(`Unknown Polar position channel "${channel}".`);
|
|
61
|
+
}
|
|
62
|
+
const frame = resolvePolarFrame(bounds);
|
|
63
|
+
return range === "auto"
|
|
64
|
+
? cloneAndFreeze([0, frame.availableRadius])
|
|
65
|
+
: validateRadialRange(range, frame.availableRadius);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function polarDirection(theta) {
|
|
69
|
+
if (!Number.isFinite(theta)) {
|
|
70
|
+
throw new TypeError("Polar theta must be a finite number of degrees.");
|
|
71
|
+
}
|
|
72
|
+
const angle = theta * Math.PI / 180;
|
|
73
|
+
return cloneAndFreeze({
|
|
74
|
+
x: Math.sin(angle),
|
|
75
|
+
y: -Math.cos(angle)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function polarToCartesian({ theta, radius, frame }) {
|
|
80
|
+
const direction = polarDirection(theta);
|
|
81
|
+
if (!Number.isFinite(radius) || radius < 0) {
|
|
82
|
+
throw new TypeError("Polar radius must be a non-negative finite number.");
|
|
83
|
+
}
|
|
84
|
+
const resolvedFrame = frame?.availableRadius === undefined
|
|
85
|
+
? resolvePolarFrame(frame)
|
|
86
|
+
: frame;
|
|
87
|
+
if (
|
|
88
|
+
![resolvedFrame.centerX, resolvedFrame.centerY,
|
|
89
|
+
resolvedFrame.availableRadius].every(Number.isFinite)
|
|
90
|
+
) {
|
|
91
|
+
throw new TypeError("Polar frame must contain finite geometry.");
|
|
92
|
+
}
|
|
93
|
+
if (radius > resolvedFrame.availableRadius) {
|
|
94
|
+
throw new RangeError(
|
|
95
|
+
`Polar radius ${radius} exceeds the available radius ${resolvedFrame.availableRadius}.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return cloneAndFreeze({
|
|
99
|
+
x: resolvedFrame.centerX + radius * direction.x,
|
|
100
|
+
y: resolvedFrame.centerY + radius * direction.y
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { freezeOwned } from "../core/immutable.js";
|
|
2
|
+
import { polarDirection } from "./polar.js";
|
|
3
|
+
import { buildPolarCircleCommands } from "./polarPaths.js";
|
|
4
|
+
|
|
5
|
+
export { buildPolarCircleCommands } from "./polarPaths.js";
|
|
6
|
+
|
|
7
|
+
function validateFrame(frame) {
|
|
8
|
+
if (
|
|
9
|
+
frame === null ||
|
|
10
|
+
typeof frame !== "object" ||
|
|
11
|
+
![frame.centerX, frame.centerY, frame.availableRadius].every(Number.isFinite) ||
|
|
12
|
+
frame.availableRadius < 0
|
|
13
|
+
) {
|
|
14
|
+
throw new TypeError(
|
|
15
|
+
"Polar guide frame requires finite centerX, centerY, and non-negative availableRadius."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return frame;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateFiniteValues(values, label) {
|
|
22
|
+
if (!Array.isArray(values) || !values.every(Number.isFinite)) {
|
|
23
|
+
throw new TypeError(`${label} must be an array of finite numbers.`);
|
|
24
|
+
}
|
|
25
|
+
return values;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateNonNegative(value, label) {
|
|
29
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
30
|
+
throw new RangeError(`${label} must be a non-negative finite number.`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function own(value) {
|
|
36
|
+
return freezeOwned(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pointAt(frame, theta, radius) {
|
|
40
|
+
validateNonNegative(radius, "Polar guide radius");
|
|
41
|
+
const direction = polarDirection(theta);
|
|
42
|
+
return {
|
|
43
|
+
x: frame.centerX + direction.x * radius,
|
|
44
|
+
y: frame.centerY + direction.y * radius
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveThetaSpokes({ frame, angles }) {
|
|
49
|
+
validateFrame(frame);
|
|
50
|
+
validateFiniteValues(angles, "Polar spoke angles");
|
|
51
|
+
const end = angles.map(theta => pointAt(frame, theta, frame.availableRadius));
|
|
52
|
+
return own({
|
|
53
|
+
x1: angles.map(() => frame.centerX),
|
|
54
|
+
y1: angles.map(() => frame.centerY),
|
|
55
|
+
x2: end.map(point => point.x),
|
|
56
|
+
y2: end.map(point => point.y)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveRadialCircles({ frame, radii }) {
|
|
61
|
+
validateFrame(frame);
|
|
62
|
+
validateFiniteValues(radii, "Polar grid radii");
|
|
63
|
+
if (radii.some(radius => radius < 0 || radius > frame.availableRadius)) {
|
|
64
|
+
throw new RangeError("Polar grid radii must be inside the Polar frame.");
|
|
65
|
+
}
|
|
66
|
+
const visibleRadii = radii.filter(radius => radius > 0);
|
|
67
|
+
return own({
|
|
68
|
+
radii: visibleRadii,
|
|
69
|
+
commands: visibleRadii.map(radius => buildPolarCircleCommands(frame, radius))
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveThetaAxisTicks({ frame, angles, length }) {
|
|
74
|
+
validateFrame(frame);
|
|
75
|
+
validateFiniteValues(angles, "Theta-axis angles");
|
|
76
|
+
validateNonNegative(length, "Theta-axis tick length");
|
|
77
|
+
const start = angles.map(theta => pointAt(frame, theta, frame.availableRadius));
|
|
78
|
+
const end = angles.map(theta => pointAt(
|
|
79
|
+
frame,
|
|
80
|
+
theta,
|
|
81
|
+
frame.availableRadius + length
|
|
82
|
+
));
|
|
83
|
+
return own({
|
|
84
|
+
x1: start.map(point => point.x),
|
|
85
|
+
y1: start.map(point => point.y),
|
|
86
|
+
x2: end.map(point => point.x),
|
|
87
|
+
y2: end.map(point => point.y)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function perimeterAlignment(theta) {
|
|
92
|
+
const radians = theta * Math.PI / 180;
|
|
93
|
+
const horizontal = Math.sin(radians);
|
|
94
|
+
const vertical = Math.cos(radians);
|
|
95
|
+
return {
|
|
96
|
+
textAlign: horizontal > 0.25
|
|
97
|
+
? "left"
|
|
98
|
+
: horizontal < -0.25
|
|
99
|
+
? "right"
|
|
100
|
+
: "center",
|
|
101
|
+
textBaseline: vertical > 0.25
|
|
102
|
+
? "bottom"
|
|
103
|
+
: vertical < -0.25
|
|
104
|
+
? "top"
|
|
105
|
+
: "middle"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resolveThetaAxisLabels({ frame, angles, offset }) {
|
|
110
|
+
validateFrame(frame);
|
|
111
|
+
validateFiniteValues(angles, "Theta-axis label angles");
|
|
112
|
+
validateNonNegative(offset, "Theta-axis label offset");
|
|
113
|
+
const points = angles.map(theta => pointAt(
|
|
114
|
+
frame,
|
|
115
|
+
theta,
|
|
116
|
+
frame.availableRadius + offset
|
|
117
|
+
));
|
|
118
|
+
const alignment = angles.map(perimeterAlignment);
|
|
119
|
+
return own({
|
|
120
|
+
x: points.map(point => point.x),
|
|
121
|
+
y: points.map(point => point.y),
|
|
122
|
+
textAlign: alignment.map(value => value.textAlign),
|
|
123
|
+
textBaseline: alignment.map(value => value.textBaseline)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function radialVectors(angle) {
|
|
128
|
+
let direction;
|
|
129
|
+
try {
|
|
130
|
+
direction = polarDirection(angle);
|
|
131
|
+
} catch {
|
|
132
|
+
throw new TypeError("Radial-axis angle must be finite degrees.");
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
direction,
|
|
136
|
+
normal: { x: -direction.y, y: direction.x }
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resolveRadialAxisLine({ frame, angle, startRadius = 0 }) {
|
|
141
|
+
validateFrame(frame);
|
|
142
|
+
validateNonNegative(startRadius, "Radial-axis start radius");
|
|
143
|
+
if (startRadius > frame.availableRadius) {
|
|
144
|
+
throw new RangeError("Radial-axis start radius must be inside the Polar frame.");
|
|
145
|
+
}
|
|
146
|
+
const { direction } = radialVectors(angle);
|
|
147
|
+
return own({
|
|
148
|
+
x1: frame.centerX + direction.x * startRadius,
|
|
149
|
+
y1: frame.centerY + direction.y * startRadius,
|
|
150
|
+
x2: frame.centerX + direction.x * frame.availableRadius,
|
|
151
|
+
y2: frame.centerY + direction.y * frame.availableRadius
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function resolveRadialAxisTicks({ frame, angle, radii, length }) {
|
|
156
|
+
validateFrame(frame);
|
|
157
|
+
validateFiniteValues(radii, "Radial-axis tick radii");
|
|
158
|
+
validateNonNegative(length, "Radial-axis tick length");
|
|
159
|
+
if (radii.some(radius => radius < 0 || radius > frame.availableRadius)) {
|
|
160
|
+
throw new RangeError("Radial-axis tick radii must be inside the Polar frame.");
|
|
161
|
+
}
|
|
162
|
+
const { direction, normal } = radialVectors(angle);
|
|
163
|
+
const half = length / 2;
|
|
164
|
+
const centers = radii.map(radius => ({
|
|
165
|
+
x: frame.centerX + direction.x * radius,
|
|
166
|
+
y: frame.centerY + direction.y * radius
|
|
167
|
+
}));
|
|
168
|
+
return own({
|
|
169
|
+
x1: centers.map(point => point.x - normal.x * half),
|
|
170
|
+
y1: centers.map(point => point.y - normal.y * half),
|
|
171
|
+
x2: centers.map(point => point.x + normal.x * half),
|
|
172
|
+
y2: centers.map(point => point.y + normal.y * half)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function resolveRadialAxisLabels({ frame, angle, radii, offset }) {
|
|
177
|
+
validateFrame(frame);
|
|
178
|
+
validateFiniteValues(radii, "Radial-axis label radii");
|
|
179
|
+
validateNonNegative(offset, "Radial-axis label offset");
|
|
180
|
+
if (radii.some(radius => radius < 0 || radius > frame.availableRadius)) {
|
|
181
|
+
throw new RangeError("Radial-axis label radii must be inside the Polar frame.");
|
|
182
|
+
}
|
|
183
|
+
const { direction, normal } = radialVectors(angle);
|
|
184
|
+
return own({
|
|
185
|
+
x: radii.map(radius =>
|
|
186
|
+
frame.centerX + direction.x * radius - normal.x * offset
|
|
187
|
+
),
|
|
188
|
+
y: radii.map(radius =>
|
|
189
|
+
frame.centerY + direction.y * radius - normal.y * offset
|
|
190
|
+
),
|
|
191
|
+
textAlign: radii.map(() => "center"),
|
|
192
|
+
textBaseline: radii.map(() => "bottom")
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function resolveRadialAxisTitle({
|
|
197
|
+
frame,
|
|
198
|
+
angle,
|
|
199
|
+
radius = frame.availableRadius / 2,
|
|
200
|
+
offset,
|
|
201
|
+
position = "inside"
|
|
202
|
+
}) {
|
|
203
|
+
validateFrame(frame);
|
|
204
|
+
validateNonNegative(offset, "Radial-axis title offset");
|
|
205
|
+
if (!["inside", "outside"].includes(position)) {
|
|
206
|
+
throw new Error(`Unknown radial-axis title position "${position}".`);
|
|
207
|
+
}
|
|
208
|
+
if (position === "outside") {
|
|
209
|
+
const { direction } = radialVectors(angle);
|
|
210
|
+
return own({
|
|
211
|
+
x: frame.centerX + direction.x * (frame.availableRadius + offset),
|
|
212
|
+
y: frame.centerY + direction.y * (frame.availableRadius + offset),
|
|
213
|
+
textAlign: direction.x > 0.25
|
|
214
|
+
? "left"
|
|
215
|
+
: direction.x < -0.25 ? "right" : "center",
|
|
216
|
+
textBaseline: direction.y > 0.25
|
|
217
|
+
? "top"
|
|
218
|
+
: direction.y < -0.25 ? "bottom" : "middle"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
validateNonNegative(radius, "Radial-axis title radius");
|
|
222
|
+
if (radius > frame.availableRadius) {
|
|
223
|
+
throw new RangeError("Radial-axis title radius must be inside the Polar frame.");
|
|
224
|
+
}
|
|
225
|
+
const { direction, normal } = radialVectors(angle);
|
|
226
|
+
return own({
|
|
227
|
+
x: frame.centerX + direction.x * radius + normal.x * offset,
|
|
228
|
+
y: frame.centerY + direction.y * radius + normal.y * offset,
|
|
229
|
+
textAlign: "center",
|
|
230
|
+
textBaseline: "top"
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function resolveThetaAxisTitle({ frame, offset }) {
|
|
235
|
+
validateFrame(frame);
|
|
236
|
+
validateNonNegative(offset, "Theta-axis title offset");
|
|
237
|
+
return own({
|
|
238
|
+
x: frame.centerX,
|
|
239
|
+
y: frame.centerY + frame.availableRadius + offset,
|
|
240
|
+
textAlign: "center",
|
|
241
|
+
textBaseline: "middle"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { buildLinearPathCommands } from "./pathCommands.js";
|
|
2
|
+
import { polarToCartesian } from "./polar.js";
|
|
3
|
+
import {
|
|
4
|
+
mapContinuousScaleValues,
|
|
5
|
+
mapOrdinalPositionValues
|
|
6
|
+
} from "./scales.js";
|
|
7
|
+
|
|
8
|
+
function mappedTheta(values, fieldType, scale) {
|
|
9
|
+
return ["nominal", "ordinal"].includes(fieldType)
|
|
10
|
+
? mapOrdinalPositionValues(values, scale)
|
|
11
|
+
: mapContinuousScaleValues(values, scale);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildPolarLinePathCommands({
|
|
15
|
+
series,
|
|
16
|
+
thetaFieldType,
|
|
17
|
+
thetaScale,
|
|
18
|
+
radiusScale,
|
|
19
|
+
frame,
|
|
20
|
+
closed = false
|
|
21
|
+
}) {
|
|
22
|
+
if (!Array.isArray(series) || series.length < 2) {
|
|
23
|
+
throw new TypeError("Polar line commands require at least two series values.");
|
|
24
|
+
}
|
|
25
|
+
if (typeof closed !== "boolean") {
|
|
26
|
+
throw new TypeError("Polar line closed must be a boolean.");
|
|
27
|
+
}
|
|
28
|
+
const theta = mappedTheta(
|
|
29
|
+
series.map(value => value.theta),
|
|
30
|
+
thetaFieldType,
|
|
31
|
+
thetaScale
|
|
32
|
+
);
|
|
33
|
+
const radius = mapContinuousScaleValues(
|
|
34
|
+
series.map(value => value.radius),
|
|
35
|
+
radiusScale
|
|
36
|
+
);
|
|
37
|
+
const points = series.map((_, index) => polarToCartesian({
|
|
38
|
+
theta: theta[index],
|
|
39
|
+
radius: radius[index],
|
|
40
|
+
frame
|
|
41
|
+
}));
|
|
42
|
+
return buildLinearPathCommands(points, { close: closed });
|
|
43
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { freezeOwned } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const KAPPA = 0.5522847498307936;
|
|
4
|
+
const MAX_ARC_SEGMENT = 90;
|
|
5
|
+
|
|
6
|
+
function validateFrame(frame) {
|
|
7
|
+
if (
|
|
8
|
+
frame === null ||
|
|
9
|
+
typeof frame !== "object" ||
|
|
10
|
+
![frame.centerX, frame.centerY, frame.availableRadius].every(Number.isFinite) ||
|
|
11
|
+
frame.availableRadius < 0
|
|
12
|
+
) {
|
|
13
|
+
throw new TypeError(
|
|
14
|
+
"Polar path frame requires finite centerX, centerY, and non-negative availableRadius."
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return frame;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateRadius(value, label, frame) {
|
|
21
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
22
|
+
throw new RangeError(`${label} must be a non-negative finite number.`);
|
|
23
|
+
}
|
|
24
|
+
if (value > frame.availableRadius) {
|
|
25
|
+
throw new RangeError(`${label} must fit inside the Polar frame.`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pointAt(frame, theta, radius) {
|
|
31
|
+
const radians = theta * Math.PI / 180;
|
|
32
|
+
return {
|
|
33
|
+
x: frame.centerX + radius * Math.sin(radians),
|
|
34
|
+
y: frame.centerY - radius * Math.cos(radians)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function arcCommands(frame, radius, startTheta, endTheta) {
|
|
39
|
+
const sweep = endTheta - startTheta;
|
|
40
|
+
const segments = Math.ceil(Math.abs(sweep) / MAX_ARC_SEGMENT);
|
|
41
|
+
const delta = sweep / segments;
|
|
42
|
+
const commands = [];
|
|
43
|
+
for (let index = 0; index < segments; index += 1) {
|
|
44
|
+
const start = startTheta + delta * index;
|
|
45
|
+
const end = start + delta;
|
|
46
|
+
const startRadians = start * Math.PI / 180;
|
|
47
|
+
const endRadians = end * Math.PI / 180;
|
|
48
|
+
const deltaRadians = endRadians - startRadians;
|
|
49
|
+
const control = 4 / 3 * Math.tan(deltaRadians / 4);
|
|
50
|
+
const from = pointAt(frame, start, radius);
|
|
51
|
+
const to = pointAt(frame, end, radius);
|
|
52
|
+
const fromDerivative = {
|
|
53
|
+
x: radius * Math.cos(startRadians),
|
|
54
|
+
y: radius * Math.sin(startRadians)
|
|
55
|
+
};
|
|
56
|
+
const toDerivative = {
|
|
57
|
+
x: radius * Math.cos(endRadians),
|
|
58
|
+
y: radius * Math.sin(endRadians)
|
|
59
|
+
};
|
|
60
|
+
commands.push({
|
|
61
|
+
op: "C",
|
|
62
|
+
x1: from.x + control * fromDerivative.x,
|
|
63
|
+
y1: from.y + control * fromDerivative.y,
|
|
64
|
+
x2: to.x - control * toDerivative.x,
|
|
65
|
+
y2: to.y - control * toDerivative.y,
|
|
66
|
+
x: to.x,
|
|
67
|
+
y: to.y
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return commands;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildPolarCircleCommands(frame, radius) {
|
|
74
|
+
validateFrame(frame);
|
|
75
|
+
validateRadius(radius, "Polar circle radius", frame);
|
|
76
|
+
const control = radius * KAPPA;
|
|
77
|
+
const { centerX: cx, centerY: cy } = frame;
|
|
78
|
+
return freezeOwned([
|
|
79
|
+
{ op: "M", x: cx, y: cy - radius },
|
|
80
|
+
{
|
|
81
|
+
op: "C",
|
|
82
|
+
x1: cx + control,
|
|
83
|
+
y1: cy - radius,
|
|
84
|
+
x2: cx + radius,
|
|
85
|
+
y2: cy - control,
|
|
86
|
+
x: cx + radius,
|
|
87
|
+
y: cy
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
op: "C",
|
|
91
|
+
x1: cx + radius,
|
|
92
|
+
y1: cy + control,
|
|
93
|
+
x2: cx + control,
|
|
94
|
+
y2: cy + radius,
|
|
95
|
+
x: cx,
|
|
96
|
+
y: cy + radius
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
op: "C",
|
|
100
|
+
x1: cx - control,
|
|
101
|
+
y1: cy + radius,
|
|
102
|
+
x2: cx - radius,
|
|
103
|
+
y2: cy + control,
|
|
104
|
+
x: cx - radius,
|
|
105
|
+
y: cy
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
op: "C",
|
|
109
|
+
x1: cx - radius,
|
|
110
|
+
y1: cy - control,
|
|
111
|
+
x2: cx - control,
|
|
112
|
+
y2: cy - radius,
|
|
113
|
+
x: cx,
|
|
114
|
+
y: cy - radius
|
|
115
|
+
},
|
|
116
|
+
{ op: "Z" }
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildAnnularSectorCommands({
|
|
121
|
+
frame,
|
|
122
|
+
startTheta,
|
|
123
|
+
endTheta,
|
|
124
|
+
innerRadius = 0,
|
|
125
|
+
outerRadius,
|
|
126
|
+
padAngle = 0
|
|
127
|
+
}) {
|
|
128
|
+
validateFrame(frame);
|
|
129
|
+
if (![startTheta, endTheta].every(Number.isFinite)) {
|
|
130
|
+
throw new TypeError("Polar sector angles must be finite numbers.");
|
|
131
|
+
}
|
|
132
|
+
validateRadius(innerRadius, "Polar sector innerRadius", frame);
|
|
133
|
+
validateRadius(outerRadius, "Polar sector outerRadius", frame);
|
|
134
|
+
if (outerRadius <= innerRadius) {
|
|
135
|
+
throw new RangeError("Polar sector outerRadius must be greater than innerRadius.");
|
|
136
|
+
}
|
|
137
|
+
const sweep = endTheta - startTheta;
|
|
138
|
+
if (sweep === 0 || Math.abs(sweep) > 360) {
|
|
139
|
+
throw new RangeError("Polar sector sweep must be non-zero and at most 360 degrees.");
|
|
140
|
+
}
|
|
141
|
+
if (!Number.isFinite(padAngle) || padAngle < 0 || padAngle >= Math.abs(sweep)) {
|
|
142
|
+
throw new RangeError(
|
|
143
|
+
"Polar sector padAngle must be non-negative and smaller than its sweep."
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const direction = Math.sign(sweep);
|
|
147
|
+
const start = startTheta + direction * padAngle / 2;
|
|
148
|
+
const end = endTheta - direction * padAngle / 2;
|
|
149
|
+
const commands = [
|
|
150
|
+
{ op: "M", ...pointAt(frame, start, outerRadius) },
|
|
151
|
+
...arcCommands(frame, outerRadius, start, end)
|
|
152
|
+
];
|
|
153
|
+
if (innerRadius === 0) {
|
|
154
|
+
commands.push({ op: "L", x: frame.centerX, y: frame.centerY });
|
|
155
|
+
} else {
|
|
156
|
+
commands.push({ op: "L", ...pointAt(frame, end, innerRadius) });
|
|
157
|
+
commands.push(...arcCommands(frame, innerRadius, end, start));
|
|
158
|
+
}
|
|
159
|
+
commands.push({ op: "Z" });
|
|
160
|
+
return freezeOwned(commands);
|
|
161
|
+
}
|