ggaction 0.0.4 → 0.0.5
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 +30 -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 +31 -35
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +8 -1
- 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/coordinates/parallel.js +50 -0
- package/src/actions/data/bin2d.js +137 -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 +29 -2
- package/src/actions/data/window.js +50 -0
- 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 +193 -88
- package/src/actions/encodings/horizon/resolve.js +283 -0
- package/src/actions/encodings/horizon.js +364 -0
- package/src/actions/encodings/index.js +6 -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/ruleAppearance.js +88 -10
- package/src/actions/facets/actions.js +1 -2
- 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 +153 -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/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 +48 -11
- package/src/actions/guides/legends/edit.js +5 -0
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +4 -11
- package/src/actions/guides/legends/strokeWidth.js +170 -0
- package/src/actions/index.js +6 -0
- 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 +85 -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} +175 -170
- package/src/actions/marks/rect/actions.js +3 -0
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +25 -3
- 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 +19 -21
- 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 +28 -3
- 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 +7 -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 +76 -0
- package/types/program.d.ts +632 -10
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
validateAggregateFieldType,
|
|
11
11
|
validateAggregateFieldValues,
|
|
12
12
|
} from "./aggregate.js";
|
|
13
|
+
import { stableOrderPathValues } from "./pathOrder.js";
|
|
13
14
|
|
|
14
15
|
const SERIES_CHANNELS = Object.freeze(["group", "color", "strokeDash"]);
|
|
15
16
|
|
|
@@ -101,6 +102,51 @@ function deriveCartesianLineSeries(rows, layer) {
|
|
|
101
102
|
: readQuantitativeField(rows, y.field);
|
|
102
103
|
const seriesFields = readSeriesFields(rows, layer);
|
|
103
104
|
|
|
105
|
+
const pathOrder = layer.encoding?.pathOrder;
|
|
106
|
+
if (pathOrder !== undefined) {
|
|
107
|
+
if (isAggregate) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Line mark "${layer.id}" cannot combine aggregate y and path order.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const orderValues = readQuantitativeField(rows, pathOrder.field);
|
|
113
|
+
const groups = new Map();
|
|
114
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
115
|
+
const dimensions = seriesFields.fields.map(
|
|
116
|
+
field => seriesFields.values.get(field)[index]
|
|
117
|
+
);
|
|
118
|
+
const key = groupKey(dimensions);
|
|
119
|
+
const series = groups.get(key) ?? {
|
|
120
|
+
key: Object.fromEntries(
|
|
121
|
+
seriesFields.fields.map((field, item) => [field, dimensions[item]])
|
|
122
|
+
),
|
|
123
|
+
values: [],
|
|
124
|
+
orderValues: []
|
|
125
|
+
};
|
|
126
|
+
series.values.push({ x: xValues[index], y: yValues[index] });
|
|
127
|
+
series.orderValues.push(orderValues[index]);
|
|
128
|
+
groups.set(key, series);
|
|
129
|
+
}
|
|
130
|
+
const series = [...groups.values()].flatMap(item => {
|
|
131
|
+
const values = stableOrderPathValues(
|
|
132
|
+
item.values,
|
|
133
|
+
item.orderValues,
|
|
134
|
+
pathOrder.order
|
|
135
|
+
);
|
|
136
|
+
return values.length < 2 ? [] : [{ key: item.key, values }];
|
|
137
|
+
});
|
|
138
|
+
if (series.length === 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Line series on mark "${layer.id}" requires at least two ordered points.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return cloneAndFreeze({
|
|
144
|
+
xValues: series.flatMap(item => item.values.map(value => value.x)),
|
|
145
|
+
yValues: series.flatMap(item => item.values.map(value => value.y)),
|
|
146
|
+
series
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
104
150
|
if (directTemporal) {
|
|
105
151
|
const groups = new Map();
|
|
106
152
|
for (let index = 0; index < rows.length; index += 1) {
|
|
@@ -249,6 +295,11 @@ function thetaOrder(values, fieldType, domain) {
|
|
|
249
295
|
}
|
|
250
296
|
|
|
251
297
|
export function derivePolarLineSeries(rows, layer, { thetaDomain } = {}) {
|
|
298
|
+
if (layer.encoding?.pathOrder !== undefined) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Polar line mark "${layer.id}" does not support path order.`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
252
303
|
const { theta, radius } = requirePolarLineEncoding(layer);
|
|
253
304
|
const thetaValues = readThetaValues(rows, theta);
|
|
254
305
|
const radiusValues = readQuantitativeField(rows, radius.field);
|
|
@@ -303,3 +354,21 @@ export function deriveLineSeries(rows, layer, options) {
|
|
|
303
354
|
? derivePolarLineSeries(rows, layer, options)
|
|
304
355
|
: deriveCartesianLineSeries(rows, layer);
|
|
305
356
|
}
|
|
357
|
+
|
|
358
|
+
export function deriveLineSeriesFieldValues(rows, layer, derived, field) {
|
|
359
|
+
const values = readQuantitativeField(rows, field);
|
|
360
|
+
if (values.some(value => value < 0)) {
|
|
361
|
+
throw new RangeError(`Line strokeWidth field "${field}" cannot contain negative values.`);
|
|
362
|
+
}
|
|
363
|
+
return cloneAndFreeze(derived.series.map(series => {
|
|
364
|
+
const matching = values.filter((_, index) => Object.entries(series.key)
|
|
365
|
+
.every(([key, value]) => rows[index]?.[key] === value));
|
|
366
|
+
const unique = [...new Set(matching)];
|
|
367
|
+
if (unique.length !== 1) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Line strokeWidth field "${field}" must have one value within each series.`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return unique[0];
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const PAINT_KEYS = Object.freeze(["type", "from", "to", "stops"]);
|
|
4
|
+
const POINT_KEYS = Object.freeze(["x", "y"]);
|
|
5
|
+
const STOP_KEYS = Object.freeze(["offset", "color"]);
|
|
6
|
+
|
|
7
|
+
function hasExactKeys(value, keys) {
|
|
8
|
+
const actual = Object.keys(value);
|
|
9
|
+
return actual.length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function validateUnitPoint(value, label) {
|
|
13
|
+
if (!isPlainObject(value) || !hasExactKeys(value, POINT_KEYS)) {
|
|
14
|
+
throw new TypeError(`${label} must be a plain { x, y } object.`);
|
|
15
|
+
}
|
|
16
|
+
for (const key of POINT_KEYS) {
|
|
17
|
+
if (!Number.isFinite(value[key]) || value[key] < 0 || value[key] > 1) {
|
|
18
|
+
throw new RangeError(`${label}.${key} must be a finite number from 0 to 1.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateStop(stop, index) {
|
|
24
|
+
const label = `Linear gradient stops[${index}]`;
|
|
25
|
+
if (!isPlainObject(stop) || !hasExactKeys(stop, STOP_KEYS)) {
|
|
26
|
+
throw new TypeError(`${label} must be a plain { offset, color } object.`);
|
|
27
|
+
}
|
|
28
|
+
if (!Number.isFinite(stop.offset) || stop.offset < 0 || stop.offset > 1) {
|
|
29
|
+
throw new RangeError(`${label}.offset must be a finite number from 0 to 1.`);
|
|
30
|
+
}
|
|
31
|
+
if (typeof stop.color !== "string" || stop.color.length === 0) {
|
|
32
|
+
throw new TypeError(`${label}.color must be a non-empty string.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isLinearGradientPaint(value) {
|
|
37
|
+
return isPlainObject(value) && value.type === "linear-gradient";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validateFillPaint(value, label = "fill") {
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
if (value.length === 0) throw new TypeError(`${label} must be a non-empty string or linear gradient.`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
if (!isPlainObject(value) || !hasExactKeys(value, PAINT_KEYS)) {
|
|
46
|
+
throw new TypeError(`${label} must be a non-empty string or linear gradient.`);
|
|
47
|
+
}
|
|
48
|
+
if (value.type !== "linear-gradient") {
|
|
49
|
+
throw new Error(`Unsupported fill paint type "${value.type}".`);
|
|
50
|
+
}
|
|
51
|
+
validateUnitPoint(value.from, "Linear gradient from");
|
|
52
|
+
validateUnitPoint(value.to, "Linear gradient to");
|
|
53
|
+
if (value.from.x === value.to.x && value.from.y === value.to.y) {
|
|
54
|
+
throw new Error("Linear gradient from and to must differ.");
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(value.stops) || value.stops.length < 2) {
|
|
57
|
+
throw new TypeError("Linear gradient stops must contain at least two entries.");
|
|
58
|
+
}
|
|
59
|
+
value.stops.forEach(validateStop);
|
|
60
|
+
for (let index = 1; index < value.stops.length; index += 1) {
|
|
61
|
+
if (value.stops[index].offset < value.stops[index - 1].offset) {
|
|
62
|
+
throw new Error("Linear gradient stop offsets must be nondecreasing.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveLinearGradientCoordinates(paint, bounds) {
|
|
69
|
+
validateFillPaint(paint, "Linear gradient paint");
|
|
70
|
+
if (!isLinearGradientPaint(paint)) {
|
|
71
|
+
throw new TypeError("Linear gradient coordinates require a structured paint.");
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
!isPlainObject(bounds) ||
|
|
75
|
+
![bounds.left, bounds.right, bounds.top, bounds.bottom].every(Number.isFinite) ||
|
|
76
|
+
bounds.left > bounds.right ||
|
|
77
|
+
bounds.top > bounds.bottom
|
|
78
|
+
) {
|
|
79
|
+
throw new TypeError("Linear gradient bounds must be finite and ordered.");
|
|
80
|
+
}
|
|
81
|
+
const width = bounds.right - bounds.left;
|
|
82
|
+
const height = bounds.bottom - bounds.top;
|
|
83
|
+
const point = value => Object.freeze({
|
|
84
|
+
x: bounds.left + width * value.x,
|
|
85
|
+
y: bounds.top + height * value.y
|
|
86
|
+
});
|
|
87
|
+
return Object.freeze({ from: point(paint.from), to: point(paint.to) });
|
|
88
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { validateUserId } from "../core/identifiers.js";
|
|
3
|
+
import { validateOptionObject } from "../core/validation.js";
|
|
4
|
+
import {
|
|
5
|
+
mapContinuousScaleValues,
|
|
6
|
+
mapOrdinalPositionValues,
|
|
7
|
+
validateSemanticFieldType
|
|
8
|
+
} from "./scales/index.js";
|
|
9
|
+
|
|
10
|
+
const DIMENSION_OPTIONS = Object.freeze([
|
|
11
|
+
"field", "fieldType", "title", "scale"
|
|
12
|
+
]);
|
|
13
|
+
const FIELD_TYPES = new Set(["quantitative", "ordinal"]);
|
|
14
|
+
const MISSING_POLICIES = new Set(["break", "drop-row", "error"]);
|
|
15
|
+
const DIMENSION_SCALE_OPTIONS = Object.freeze([
|
|
16
|
+
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
17
|
+
"base", "exponent", "constant", "paddingInner", "paddingOuter",
|
|
18
|
+
"padding", "align", "unknown"
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function nonEmptyString(value, label) {
|
|
22
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
23
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validateParallelKeyField(value) {
|
|
29
|
+
return nonEmptyString(value, "Parallel key field");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function inferFieldType(rows, field) {
|
|
33
|
+
const values = rows
|
|
34
|
+
.map(row => row[field])
|
|
35
|
+
.filter(value => value !== undefined && value !== null);
|
|
36
|
+
if (values.length === 0) {
|
|
37
|
+
throw new Error(`Cannot infer Parallel field type for "${field}".`);
|
|
38
|
+
}
|
|
39
|
+
if (values.every(Number.isFinite)) return "quantitative";
|
|
40
|
+
if (values.every(value => typeof value === "string")) return "ordinal";
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Parallel dimension "${field}" requires an explicit compatible fieldType.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateScaleOptions(value, index) {
|
|
47
|
+
if (value === undefined) return {};
|
|
48
|
+
if (!isPlainObject(value)) {
|
|
49
|
+
throw new TypeError(`Parallel dimension ${index} scale must be a plain object.`);
|
|
50
|
+
}
|
|
51
|
+
validateOptionObject(
|
|
52
|
+
value,
|
|
53
|
+
DIMENSION_SCALE_OPTIONS,
|
|
54
|
+
`Parallel dimension ${index} scale`
|
|
55
|
+
);
|
|
56
|
+
return { ...value };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateParallelMissingPolicy(value) {
|
|
60
|
+
if (!MISSING_POLICIES.has(value)) {
|
|
61
|
+
throw new Error(`Unsupported Parallel missing policy "${value}".`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function validateParallelDimensions(dimensions, { normalized = false } = {}) {
|
|
67
|
+
if (!Array.isArray(dimensions) || dimensions.length < 2) {
|
|
68
|
+
throw new RangeError("Parallel coordinates require at least two dimensions.");
|
|
69
|
+
}
|
|
70
|
+
const fields = new Set();
|
|
71
|
+
for (const [index, dimension] of dimensions.entries()) {
|
|
72
|
+
if (!isPlainObject(dimension)) {
|
|
73
|
+
throw new TypeError(`Parallel dimension ${index} must be an object.`);
|
|
74
|
+
}
|
|
75
|
+
const supported = normalized
|
|
76
|
+
? ["field", "fieldType", "title", "scale"]
|
|
77
|
+
: DIMENSION_OPTIONS;
|
|
78
|
+
validateOptionObject(dimension, supported, `Parallel dimension ${index}`);
|
|
79
|
+
const field = nonEmptyString(
|
|
80
|
+
dimension.field,
|
|
81
|
+
`Parallel dimension ${index} field`
|
|
82
|
+
);
|
|
83
|
+
if (fields.has(field)) {
|
|
84
|
+
throw new Error("Parallel dimensions must use unique fields.");
|
|
85
|
+
}
|
|
86
|
+
fields.add(field);
|
|
87
|
+
validateSemanticFieldType(dimension.fieldType);
|
|
88
|
+
if (!FIELD_TYPES.has(dimension.fieldType)) {
|
|
89
|
+
throw new Error(`Unsupported Parallel field type "${dimension.fieldType}".`);
|
|
90
|
+
}
|
|
91
|
+
nonEmptyString(dimension.title, `Parallel dimension ${index} title`);
|
|
92
|
+
validateUserId(dimension.scale, `Parallel dimension ${index} scale id`);
|
|
93
|
+
}
|
|
94
|
+
return dimensions;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function normalizeParallelDimensions(rows, dimensions, target) {
|
|
98
|
+
if (!Array.isArray(dimensions) || dimensions.length < 2) {
|
|
99
|
+
throw new RangeError("Parallel coordinates require at least two dimensions.");
|
|
100
|
+
}
|
|
101
|
+
const normalized = dimensions.map((input, index) => {
|
|
102
|
+
const dimension = typeof input === "string" ? { field: input } : input;
|
|
103
|
+
if (!isPlainObject(dimension)) {
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
`Parallel dimension ${index} must be a field string or plain object.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
validateOptionObject(
|
|
109
|
+
dimension,
|
|
110
|
+
DIMENSION_OPTIONS,
|
|
111
|
+
`Parallel dimension ${index}`
|
|
112
|
+
);
|
|
113
|
+
const field = nonEmptyString(
|
|
114
|
+
dimension.field,
|
|
115
|
+
`Parallel dimension ${index} field`
|
|
116
|
+
);
|
|
117
|
+
const fieldType = dimension.fieldType ?? inferFieldType(rows, field);
|
|
118
|
+
if (!FIELD_TYPES.has(fieldType)) {
|
|
119
|
+
throw new Error(`Unsupported Parallel field type "${fieldType}".`);
|
|
120
|
+
}
|
|
121
|
+
const scale = validateScaleOptions(dimension.scale, index);
|
|
122
|
+
return {
|
|
123
|
+
field,
|
|
124
|
+
fieldType,
|
|
125
|
+
title: dimension.title ?? field,
|
|
126
|
+
scale: `${target}-parallel-${index}`,
|
|
127
|
+
scaleOptions: scale
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
const fields = normalized.map(dimension => dimension.field);
|
|
131
|
+
if (new Set(fields).size !== fields.length) {
|
|
132
|
+
throw new Error("Parallel dimensions must use unique fields.");
|
|
133
|
+
}
|
|
134
|
+
return cloneAndFreeze(normalized);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validDimensionValue(value, fieldType) {
|
|
138
|
+
return fieldType === "quantitative"
|
|
139
|
+
? Number.isFinite(value)
|
|
140
|
+
: typeof value === "string" || Number.isFinite(value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function commandsFromVertices(vertices) {
|
|
144
|
+
const commands = [];
|
|
145
|
+
let fragment = [];
|
|
146
|
+
const flush = () => {
|
|
147
|
+
if (fragment.length >= 2) {
|
|
148
|
+
fragment.forEach((point, index) => commands.push({
|
|
149
|
+
op: index === 0 ? "M" : "L",
|
|
150
|
+
x: point.x,
|
|
151
|
+
y: point.y
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
fragment = [];
|
|
155
|
+
};
|
|
156
|
+
for (const vertex of vertices) {
|
|
157
|
+
if (vertex === undefined) flush();
|
|
158
|
+
else fragment.push(vertex);
|
|
159
|
+
}
|
|
160
|
+
flush();
|
|
161
|
+
return commands;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function validateParallelRows(rows, dimensions, { key, missing }) {
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
rows.forEach((row, index) => {
|
|
167
|
+
if (key !== undefined) {
|
|
168
|
+
const value = row[key];
|
|
169
|
+
if (value === undefined || value === null || value === "") {
|
|
170
|
+
throw new Error(`Parallel key is missing at row ${index}.`);
|
|
171
|
+
}
|
|
172
|
+
const token = `${typeof value}:${String(value)}`;
|
|
173
|
+
if (seen.has(token)) throw new Error(`Duplicate Parallel key "${value}".`);
|
|
174
|
+
seen.add(token);
|
|
175
|
+
}
|
|
176
|
+
const incomplete = dimensions.some(dimension =>
|
|
177
|
+
!validDimensionValue(row[dimension.field], dimension.fieldType)
|
|
178
|
+
);
|
|
179
|
+
if (incomplete && missing === "error") {
|
|
180
|
+
throw new Error(`Parallel row ${index} has a missing dimension.`);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function materializeParallelRows(rows, dimensions, resolvedScales, bounds, {
|
|
186
|
+
key,
|
|
187
|
+
missing
|
|
188
|
+
}) {
|
|
189
|
+
validateParallelRows(rows, dimensions, { key, missing });
|
|
190
|
+
const step = bounds.width / (dimensions.length - 1);
|
|
191
|
+
const items = [];
|
|
192
|
+
rows.forEach((row, sourceRowIndex) => {
|
|
193
|
+
const vertices = dimensions.map((dimension, index) => {
|
|
194
|
+
const value = row[dimension.field];
|
|
195
|
+
if (!validDimensionValue(value, dimension.fieldType)) return undefined;
|
|
196
|
+
const scale = resolvedScales[dimension.scale];
|
|
197
|
+
const mapped = dimension.fieldType === "quantitative"
|
|
198
|
+
? mapContinuousScaleValues([value], scale)[0]
|
|
199
|
+
: mapOrdinalPositionValues([value], scale)[0];
|
|
200
|
+
return Number.isFinite(mapped)
|
|
201
|
+
? { x: bounds.x + step * index, y: mapped, value, dimension: dimension.field }
|
|
202
|
+
: undefined;
|
|
203
|
+
});
|
|
204
|
+
const incomplete = vertices.some(vertex => vertex === undefined);
|
|
205
|
+
if (incomplete && missing === "drop-row") return;
|
|
206
|
+
items.push({
|
|
207
|
+
key: key === undefined ? `source:${sourceRowIndex}` : row[key],
|
|
208
|
+
sourceRowIndex,
|
|
209
|
+
commands: commandsFromVertices(vertices)
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
return cloneAndFreeze(items);
|
|
213
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
export const PATH_ORDER_DIRECTIONS = Object.freeze([
|
|
4
|
+
"ascending",
|
|
5
|
+
"descending"
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export function validatePathOrderDirection(value) {
|
|
9
|
+
if (!PATH_ORDER_DIRECTIONS.includes(value)) {
|
|
10
|
+
throw new Error(`Unsupported path order "${value}".`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function stableOrderPathValues(values, orderValues, order = "ascending") {
|
|
16
|
+
validatePathOrderDirection(order);
|
|
17
|
+
if (!Array.isArray(values) || !Array.isArray(orderValues)) {
|
|
18
|
+
throw new TypeError("Path ordering requires value and order arrays.");
|
|
19
|
+
}
|
|
20
|
+
if (values.length !== orderValues.length) {
|
|
21
|
+
throw new Error("Path value and order arrays must have equal length.");
|
|
22
|
+
}
|
|
23
|
+
const direction = order === "ascending" ? 1 : -1;
|
|
24
|
+
const ordered = values.map((value, sourceIndex) => {
|
|
25
|
+
const orderValue = orderValues[sourceIndex];
|
|
26
|
+
if (!Number.isFinite(orderValue)) {
|
|
27
|
+
throw new TypeError("Path order values must be finite numbers.");
|
|
28
|
+
}
|
|
29
|
+
return { value, orderValue, sourceIndex };
|
|
30
|
+
}).sort((left, right) =>
|
|
31
|
+
direction * (left.orderValue - right.orderValue) ||
|
|
32
|
+
left.sourceIndex - right.sourceIndex
|
|
33
|
+
);
|
|
34
|
+
return cloneAndFreeze(ordered.map(item => item.value));
|
|
35
|
+
}
|
|
@@ -162,3 +162,27 @@ export function createPointShapeGraphic({
|
|
|
162
162
|
}
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
|
+
|
|
166
|
+
export function resolvePointShapeExtent({ shape, area, strokeWidth = 0 }) {
|
|
167
|
+
const validated = validatePointShape(shape);
|
|
168
|
+
if (!Number.isFinite(area) || area < 0) {
|
|
169
|
+
throw new TypeError("Point shape area must be finite and non-negative.");
|
|
170
|
+
}
|
|
171
|
+
if (!Number.isFinite(strokeWidth) || strokeWidth < 0) {
|
|
172
|
+
throw new TypeError("Point shape strokeWidth must be finite and non-negative.");
|
|
173
|
+
}
|
|
174
|
+
const strokeExtent = strokeWidth / 2;
|
|
175
|
+
if (validated === "circle") {
|
|
176
|
+
const radius = Math.sqrt(area / Math.PI) + strokeExtent;
|
|
177
|
+
return cloneAndFreeze({ x: radius, y: radius });
|
|
178
|
+
}
|
|
179
|
+
if (validated === "square") {
|
|
180
|
+
const halfSide = Math.sqrt(area) / 2 + strokeExtent;
|
|
181
|
+
return cloneAndFreeze({ x: halfSide, y: halfSide });
|
|
182
|
+
}
|
|
183
|
+
const points = normalizePolygon(shapePolygon(validated), 0, 0, area);
|
|
184
|
+
return cloneAndFreeze({
|
|
185
|
+
x: Math.max(0, ...points.map(point => Math.abs(point.x))) + strokeExtent,
|
|
186
|
+
y: Math.max(0, ...points.map(point => Math.abs(point.y))) + strokeExtent
|
|
187
|
+
});
|
|
188
|
+
}
|
package/src/grammar/rules.js
CHANGED
|
@@ -20,6 +20,7 @@ export const NAMED_DASH_PATTERNS = cloneAndFreeze({
|
|
|
20
20
|
export { POINT_SHAPES } from "../pointShapes.js";
|
|
21
21
|
export const DEFAULT_SIZE_RANGE = cloneAndFreeze([24, 196]);
|
|
22
22
|
export const DEFAULT_OPACITY_RANGE = cloneAndFreeze([0.2, 1]);
|
|
23
|
+
export const DEFAULT_STROKE_WIDTH_RANGE = cloneAndFreeze([1, 8]);
|
|
23
24
|
|
|
24
25
|
export function validateColorRange(range) {
|
|
25
26
|
if (range === "auto") return range;
|
|
@@ -123,6 +124,21 @@ export function validateOpacityRange(range) {
|
|
|
123
124
|
return cloneAndFreeze(range);
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
export function validateStrokeWidthRange(range) {
|
|
128
|
+
if (range === "auto") return range;
|
|
129
|
+
if (
|
|
130
|
+
!Array.isArray(range) ||
|
|
131
|
+
range.length !== 2 ||
|
|
132
|
+
!range.every(value => Number.isFinite(value) && value >= 0) ||
|
|
133
|
+
range[0] > range[1]
|
|
134
|
+
) {
|
|
135
|
+
throw new TypeError(
|
|
136
|
+
"StrokeWidth range must be an ascending pair of non-negative finite widths."
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return cloneAndFreeze(range);
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
export function validateOpacityValue(value, label = "Opacity") {
|
|
127
143
|
return validateUnitInterval(value, label);
|
|
128
144
|
}
|
|
@@ -166,6 +182,11 @@ export function resolveOpacityRange(range) {
|
|
|
166
182
|
return validated === "auto" ? DEFAULT_OPACITY_RANGE : validated;
|
|
167
183
|
}
|
|
168
184
|
|
|
185
|
+
export function resolveStrokeWidthRange(range) {
|
|
186
|
+
const validated = validateStrokeWidthRange(range);
|
|
187
|
+
return validated === "auto" ? DEFAULT_STROKE_WIDTH_RANGE : validated;
|
|
188
|
+
}
|
|
189
|
+
|
|
169
190
|
export function mapOrdinalValues(values, domain, range, options = {}) {
|
|
170
191
|
const hasUnknown = Object.hasOwn(options, "unknown");
|
|
171
192
|
const indices = new Map(domain.map((value, index) => [value, index]));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isPlainObject } from "../../core/immutable.js";
|
|
2
2
|
import { validatePathCommands } from "../pathCommands.js";
|
|
3
|
+
import { validateFillPaint } from "../paint.js";
|
|
3
4
|
import { validateGraphicProperty } from "./graphic.js";
|
|
4
5
|
|
|
5
6
|
const FINITE_PROPERTIES = new Set([
|
|
@@ -10,7 +11,7 @@ const NON_NEGATIVE_PROPERTIES = new Set([
|
|
|
10
11
|
"width", "height", "radius", "strokeWidth", "fontSize", "gap"
|
|
11
12
|
]);
|
|
12
13
|
const STRING_PROPERTIES = new Set([
|
|
13
|
-
"background", "
|
|
14
|
+
"background", "stroke", "fontFamily", "text", "textAlign",
|
|
14
15
|
"textBaseline"
|
|
15
16
|
]);
|
|
16
17
|
const TEXT_ALIGNS = new Set(["left", "right", "center", "start", "end"]);
|
|
@@ -19,6 +20,15 @@ const TEXT_BASELINES = new Set([
|
|
|
19
20
|
]);
|
|
20
21
|
|
|
21
22
|
export function validateConcreteGraphicValue(type, property, value) {
|
|
23
|
+
if (property === "fill") {
|
|
24
|
+
if (typeof value === "string") {
|
|
25
|
+
validateFillPaint(value, `${type}.fill`);
|
|
26
|
+
} else if (type === "rect" || type === "path") {
|
|
27
|
+
validateFillPaint(value, `${type}.fill`);
|
|
28
|
+
} else {
|
|
29
|
+
throw new TypeError(`${type}.fill must be a non-empty string.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
22
32
|
if (FINITE_PROPERTIES.has(property) && !Number.isFinite(value)) {
|
|
23
33
|
throw new TypeError(`${type}.${property} must be a finite number.`);
|
|
24
34
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveTextBounds } from "../../core/textMetrics.js";
|
|
2
2
|
import { findGraphic, requireGraphic } from "./graphicTree.js";
|
|
3
3
|
|
|
4
4
|
function unionBounds(bounds) {
|
|
@@ -74,6 +74,17 @@ function pathPoints(commands) {
|
|
|
74
74
|
return points;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export function resolvePathCommandBounds(commands) {
|
|
78
|
+
const points = pathPoints(commands);
|
|
79
|
+
if (points.length === 0) return undefined;
|
|
80
|
+
return {
|
|
81
|
+
left: Math.min(...points.map(point => point.x)),
|
|
82
|
+
right: Math.max(...points.map(point => point.x)),
|
|
83
|
+
top: Math.min(...points.map(point => point.y)),
|
|
84
|
+
bottom: Math.max(...points.map(point => point.y))
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
function primitiveBounds(type, properties = {}) {
|
|
78
89
|
const strokeExtent = (properties.strokeWidth ?? 0) / 2;
|
|
79
90
|
if (type === "circle") {
|
|
@@ -115,47 +126,18 @@ function primitiveBounds(type, properties = {}) {
|
|
|
115
126
|
!Number.isFinite(properties.fontSize) ||
|
|
116
127
|
typeof properties.text !== "string"
|
|
117
128
|
) return undefined;
|
|
118
|
-
|
|
119
|
-
const align = properties.textAlign ?? "left";
|
|
120
|
-
const left = align === "center" ? -width / 2 :
|
|
121
|
-
["right", "end"].includes(align) ? -width : 0;
|
|
122
|
-
const baseline = properties.textBaseline ?? "alphabetic";
|
|
123
|
-
const [top, bottom] = baseline === "middle"
|
|
124
|
-
? [-properties.fontSize / 2, properties.fontSize / 2]
|
|
125
|
-
: ["top", "hanging"].includes(baseline)
|
|
126
|
-
? [0, properties.fontSize]
|
|
127
|
-
: ["bottom", "ideographic"].includes(baseline)
|
|
128
|
-
? [-properties.fontSize, 0]
|
|
129
|
-
: [-properties.fontSize * 0.8, properties.fontSize * 0.2];
|
|
130
|
-
const rotation = properties.rotation ?? 0;
|
|
131
|
-
const cosine = Math.cos(rotation);
|
|
132
|
-
const sine = Math.sin(rotation);
|
|
133
|
-
const corners = [
|
|
134
|
-
[left, top],
|
|
135
|
-
[left + width, top],
|
|
136
|
-
[left + width, bottom],
|
|
137
|
-
[left, bottom]
|
|
138
|
-
].map(([x, y]) => ({
|
|
139
|
-
x: properties.x + x * cosine - y * sine,
|
|
140
|
-
y: properties.y + x * sine + y * cosine
|
|
141
|
-
}));
|
|
142
|
-
return {
|
|
143
|
-
left: Math.min(...corners.map(point => point.x)),
|
|
144
|
-
right: Math.max(...corners.map(point => point.x)),
|
|
145
|
-
top: Math.min(...corners.map(point => point.y)),
|
|
146
|
-
bottom: Math.max(...corners.map(point => point.y))
|
|
147
|
-
};
|
|
129
|
+
return resolveTextBounds(properties);
|
|
148
130
|
}
|
|
149
131
|
if (type === "path") {
|
|
150
132
|
const commands = properties.commands;
|
|
151
133
|
if (!Array.isArray(commands)) return undefined;
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
134
|
+
const bounds = resolvePathCommandBounds(commands);
|
|
135
|
+
if (bounds === undefined) return undefined;
|
|
154
136
|
return {
|
|
155
|
-
left:
|
|
156
|
-
right:
|
|
157
|
-
top:
|
|
158
|
-
bottom:
|
|
137
|
+
left: bounds.left - strokeExtent,
|
|
138
|
+
right: bounds.right + strokeExtent,
|
|
139
|
+
top: bounds.top - strokeExtent,
|
|
140
|
+
bottom: bounds.bottom + strokeExtent
|
|
159
141
|
};
|
|
160
142
|
}
|
|
161
143
|
return undefined;
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "../../core/vocabulary.js";
|
|
7
7
|
|
|
8
8
|
const ENCODING_PATHS = Object.freeze([
|
|
9
|
-
...ENCODING_CHANNELS.flatMap(channel => [
|
|
9
|
+
...ENCODING_CHANNELS.filter(channel => channel !== "pathOrder").flatMap(channel => [
|
|
10
10
|
`encoding.${channel}.field`,
|
|
11
11
|
`encoding.${channel}.datum`,
|
|
12
12
|
`encoding.${channel}.fieldType`,
|
|
@@ -20,8 +20,15 @@ const ENCODING_PATHS = Object.freeze([
|
|
|
20
20
|
"encoding.x.bin.boundaries",
|
|
21
21
|
"encoding.color.layout",
|
|
22
22
|
"encoding.color.aggregate",
|
|
23
|
+
"encoding.pathOrder.field",
|
|
24
|
+
"encoding.pathOrder.fieldType",
|
|
25
|
+
"encoding.pathOrder.order",
|
|
23
26
|
"encoding.theta.aggregate",
|
|
27
|
+
"encoding.theta.weight",
|
|
24
28
|
"encoding.text.format",
|
|
29
|
+
"encoding.parallel.dimensions",
|
|
30
|
+
"encoding.parallel.key",
|
|
31
|
+
"encoding.parallel.missing",
|
|
25
32
|
...CARTESIAN_POSITION_CHANNELS.flatMap(channel => [
|
|
26
33
|
`encoding.${channel}.aggregate`,
|
|
27
34
|
`encoding.${channel}.stack`
|
|
@@ -36,7 +43,8 @@ const ENTITY_PATHS = Object.freeze({
|
|
|
36
43
|
layer: {
|
|
37
44
|
collection: "layers",
|
|
38
45
|
removableContainers: new Set(
|
|
39
|
-
ENCODING_CHANNELS.map(channel => `encoding.${channel}`)
|
|
46
|
+
[...ENCODING_CHANNELS.map(channel => `encoding.${channel}`),
|
|
47
|
+
"encoding.parallel"]
|
|
40
48
|
),
|
|
41
49
|
properties: new Set([
|
|
42
50
|
"data",
|
|
@@ -74,10 +82,15 @@ const GUIDE_PATHS = new Set([
|
|
|
74
82
|
"axis.radius.scale",
|
|
75
83
|
"axis.radius.coordinate",
|
|
76
84
|
"axis.radius.title",
|
|
85
|
+
"axis.parallel.target",
|
|
86
|
+
"axis.parallel.coordinate",
|
|
87
|
+
"axis.parallel.scales",
|
|
77
88
|
"legend.color.scale",
|
|
78
89
|
"legend.color.title",
|
|
79
90
|
"legend.size.scale",
|
|
80
91
|
"legend.size.title",
|
|
92
|
+
"legend.strokeWidth.scale",
|
|
93
|
+
"legend.strokeWidth.title",
|
|
81
94
|
"legend.opacity.scale",
|
|
82
95
|
"legend.opacity.title",
|
|
83
96
|
"legend.series.channels",
|
|
@@ -98,12 +111,14 @@ const GUIDE_REMOVABLE_CONTAINERS = new Set([
|
|
|
98
111
|
"axis.y",
|
|
99
112
|
"axis.theta",
|
|
100
113
|
"axis.radius",
|
|
114
|
+
"axis.parallel",
|
|
101
115
|
"grid.horizontal",
|
|
102
116
|
"grid.vertical",
|
|
103
117
|
"grid.theta",
|
|
104
118
|
"grid.radial",
|
|
105
119
|
"legend.color",
|
|
106
120
|
"legend.size",
|
|
121
|
+
"legend.strokeWidth",
|
|
107
122
|
"legend.opacity",
|
|
108
123
|
"legend.series"
|
|
109
124
|
]);
|