ggaction 0.0.7 → 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 +20 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/actions/categoryOrder/index.js +111 -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/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/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/remove.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +23 -6
- package/src/actions/guides/legends/categorical/index.js +8 -0
- 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/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/index.js +2 -0
- package/src/actions/marks/point/materialize.js +17 -5
- 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/materialize.js +2 -0
- package/src/actions/selection/actions.js +5 -2
- 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/types/index.d.ts +10 -0
- package/types/program.d.ts +96 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes to `ggaction` are recorded in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.0.8] - 2026-08-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added immutable UTC time-unit derivation, semantic category ordering and reset, and partitioned moving mean/sum
|
|
12
|
+
windows with matching public actions, strict declarations, examples, and Current contracts.
|
|
13
|
+
- Added the Tick mark lifecycle, point/Tick angle encoding, and non-negative centered area stacking with stable
|
|
14
|
+
Canvas, SVG, PNG, and PDF evidence.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Expanded the public example and documentation corpus with actual-data temporal, ordering, directional Tick,
|
|
19
|
+
centered-area, and multi-legend charts, plus installed-package coverage for their supported workflows.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Replaced independent same-edge legend placement with deterministic shared right/left lanes and left-packed
|
|
24
|
+
top/bottom rows, aligning titles, symbols, and labels while preserving lifecycle convergence and compact margins.
|
|
25
|
+
|
|
7
26
|
## [0.0.7] - 2026-07-23
|
|
8
27
|
|
|
9
28
|
### Added
|
|
@@ -157,6 +176,7 @@ All notable changes to `ggaction` are recorded in this file.
|
|
|
157
176
|
- Cartesian charts are the complete current path. Polar semantic tokens exist only where explicitly documented and do
|
|
158
177
|
not imply complete polar rendering.
|
|
159
178
|
|
|
179
|
+
[0.0.8]: https://github.com/ggaction/ggaction/releases/tag/v0.0.8
|
|
160
180
|
[0.0.7]: https://github.com/ggaction/ggaction/releases/tag/v0.0.7
|
|
161
181
|
[0.0.6]: https://github.com/ggaction/ggaction/releases/tag/v0.0.6
|
|
162
182
|
[0.0.5]: https://github.com/ggaction/ggaction/releases/tag/v0.0.5
|
package/README.md
CHANGED
|
@@ -201,7 +201,7 @@ the extra discussion required before public API or architecture changes.
|
|
|
201
201
|
|
|
202
202
|
## Status and development
|
|
203
203
|
|
|
204
|
-
> **Status:** `0.0.
|
|
204
|
+
> **Status:** `0.0.8` is the current experimental public release. APIs may change before `1.0.0`; changes are recorded in the [changelog](./CHANGELOG.md).
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
207
|
npm install
|
package/package.json
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { validateKeys } from "../../core/validation.js";
|
|
4
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
5
|
+
import { resolveEligibleLayer } from "../../selectors/layers.js";
|
|
6
|
+
import { findScaleConsumers } from "../scales/consumers/index.js";
|
|
7
|
+
import {
|
|
8
|
+
applyMaterializationPlan,
|
|
9
|
+
planScaleGuideRematerialization
|
|
10
|
+
} from "../../materialization/dependencies.js";
|
|
11
|
+
import { getMarkMaterializationStep } from "../../materialization/marks/index.js";
|
|
12
|
+
import { buildMaterializationPlan } from "../../materialization/planner.js";
|
|
13
|
+
import {
|
|
14
|
+
normalizeCategoryOrder,
|
|
15
|
+
resolveCategoryOrder
|
|
16
|
+
} from "../../grammar/categoryOrder.js";
|
|
17
|
+
|
|
18
|
+
const ORDER_OPTIONS = Object.freeze(["target", "channel", "values", "by", "direction"]);
|
|
19
|
+
const REMOVE_OPTIONS = Object.freeze(["target", "channel"]);
|
|
20
|
+
|
|
21
|
+
function resolveAssignment(program, args, operation, { activeOnly = false } = {}) {
|
|
22
|
+
if (!["x", "y"].includes(args.channel)) {
|
|
23
|
+
throw new Error(`${operation} channel must be x or y.`);
|
|
24
|
+
}
|
|
25
|
+
const requested = args.target === undefined
|
|
26
|
+
? undefined
|
|
27
|
+
: validateUserId(args.target, "Mark id");
|
|
28
|
+
const layer = resolveEligibleLayer(program, {
|
|
29
|
+
target: requested,
|
|
30
|
+
label: "categorical position mark",
|
|
31
|
+
predicate: candidate => {
|
|
32
|
+
const encoding = candidate.encoding?.[args.channel];
|
|
33
|
+
return ["nominal", "ordinal"].includes(encoding?.fieldType) &&
|
|
34
|
+
typeof encoding.field === "string" && encoding.scale !== undefined &&
|
|
35
|
+
(!activeOnly || encoding.categoryOrder !== undefined);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const encoding = layer.encoding[args.channel];
|
|
39
|
+
const dataset = findDataset(program, layer.data);
|
|
40
|
+
if (dataset === undefined) {
|
|
41
|
+
throw new Error(`Mark "${layer.id}" requires an existing dataset.`);
|
|
42
|
+
}
|
|
43
|
+
const consumers = findScaleConsumers(program, encoding.scale);
|
|
44
|
+
const incompatible = consumers.find(consumer =>
|
|
45
|
+
consumer.layer.id !== layer.id &&
|
|
46
|
+
(consumer.layer.data !== layer.data || consumer.encoding.field !== encoding.field)
|
|
47
|
+
);
|
|
48
|
+
if (incompatible !== undefined) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Category order scale "${encoding.scale}" has an incompatible shared consumer.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return { layer, encoding, dataset };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function rematerializeAssignment(program, scaleId) {
|
|
57
|
+
const consumers = findScaleConsumers(program, scaleId);
|
|
58
|
+
return applyMaterializationPlan(program, buildMaterializationPlan({
|
|
59
|
+
scales: [{
|
|
60
|
+
op: "rematerializeScale",
|
|
61
|
+
args: { id: scaleId, guides: false, marks: false }
|
|
62
|
+
}],
|
|
63
|
+
marks: consumers.map(consumer =>
|
|
64
|
+
getMarkMaterializationStep(program, consumer.layer)
|
|
65
|
+
),
|
|
66
|
+
guides: planScaleGuideRematerialization(program, scaleId)
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const orderCategories = action(
|
|
71
|
+
{ op: "orderCategories", description: "Assign semantic order to a categorical position." },
|
|
72
|
+
function (args = {}) {
|
|
73
|
+
validateKeys(args, ORDER_OPTIONS, "orderCategories");
|
|
74
|
+
const { layer, encoding, dataset } = resolveAssignment(this, args, "orderCategories");
|
|
75
|
+
const order = normalizeCategoryOrder(args);
|
|
76
|
+
resolveCategoryOrder(dataset.values, encoding.field, order);
|
|
77
|
+
const edited = this.editSemantic({
|
|
78
|
+
property: `layer[${layer.id}].encoding.${args.channel}.categoryOrder`,
|
|
79
|
+
value: order
|
|
80
|
+
});
|
|
81
|
+
return rematerializeAssignment(edited, encoding.scale);
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export const removeCategoryOrder = action(
|
|
86
|
+
{ op: "removeCategoryOrder", description: "Restore automatic category order." },
|
|
87
|
+
function (args = {}) {
|
|
88
|
+
validateKeys(args, REMOVE_OPTIONS, "removeCategoryOrder");
|
|
89
|
+
const { layer, encoding } = resolveAssignment(
|
|
90
|
+
this,
|
|
91
|
+
args,
|
|
92
|
+
"removeCategoryOrder",
|
|
93
|
+
{ activeOnly: args.target === undefined }
|
|
94
|
+
);
|
|
95
|
+
if (encoding.categoryOrder === undefined) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Mark "${layer.id}" ${args.channel} encoding has no category order assignment.`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const edited = this.editSemantic({
|
|
101
|
+
property: `layer[${layer.id}].encoding.${args.channel}.categoryOrder`,
|
|
102
|
+
remove: true
|
|
103
|
+
});
|
|
104
|
+
return rematerializeAssignment(edited, encoding.scale);
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
export function registerCategoryOrderActions(ProgramClass) {
|
|
109
|
+
ProgramClass.prototype.orderCategories = orderCategories;
|
|
110
|
+
ProgramClass.prototype.removeCategoryOrder = removeCategoryOrder;
|
|
111
|
+
}
|
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
import { createIntervalData, materializeIntervalData } from "./interval.js";
|
|
24
24
|
import { createHorizonData, materializeHorizonData } from "./horizon.js";
|
|
25
25
|
import { createWindowData, materializeWindowData } from "./window.js";
|
|
26
|
+
import {
|
|
27
|
+
createTimeUnitData,
|
|
28
|
+
materializeTimeUnitData
|
|
29
|
+
} from "./timeUnit.js";
|
|
26
30
|
import {
|
|
27
31
|
createBin2DData,
|
|
28
32
|
editBin2DData,
|
|
@@ -54,6 +58,8 @@ export function registerDataActions(ProgramClass) {
|
|
|
54
58
|
ProgramClass.prototype.materializeHorizonData = materializeHorizonData;
|
|
55
59
|
ProgramClass.prototype.createWindowData = createWindowData;
|
|
56
60
|
ProgramClass.prototype.materializeWindowData = materializeWindowData;
|
|
61
|
+
ProgramClass.prototype.createTimeUnitData = createTimeUnitData;
|
|
62
|
+
ProgramClass.prototype.materializeTimeUnitData = materializeTimeUnitData;
|
|
57
63
|
ProgramClass.prototype.createBin2DData = createBin2DData;
|
|
58
64
|
ProgramClass.prototype.editBin2DData = editBin2DData;
|
|
59
65
|
ProgramClass.prototype.materializeBin2DData = materializeBin2DData;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
import { validateKeys } from "../../core/validation.js";
|
|
4
|
+
import {
|
|
5
|
+
deriveTimeUnitRows,
|
|
6
|
+
normalizeTimeUnitTransform
|
|
7
|
+
} from "../../grammar/timeUnit.js";
|
|
8
|
+
import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
|
|
9
|
+
|
|
10
|
+
const OPTIONS = Object.freeze(["id", "source", "field", "unit", "as"]);
|
|
11
|
+
|
|
12
|
+
export const materializeTimeUnitData = action(
|
|
13
|
+
{
|
|
14
|
+
op: "materializeTimeUnitData",
|
|
15
|
+
description: "Materialize one immutable UTC time-unit dataset."
|
|
16
|
+
},
|
|
17
|
+
function (args = {}) {
|
|
18
|
+
validateKeys(args, MATERIALIZE_OPTIONS, "materializeTimeUnitData");
|
|
19
|
+
const { id, source, transform } = requireDerivedDataset(
|
|
20
|
+
this,
|
|
21
|
+
args.id,
|
|
22
|
+
"timeUnit"
|
|
23
|
+
);
|
|
24
|
+
return this.editSemantic({
|
|
25
|
+
property: `dataset[${id}].values`,
|
|
26
|
+
value: deriveTimeUnitRows(source.values, transform)
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const createTimeUnitData = action(
|
|
32
|
+
{
|
|
33
|
+
op: "createTimeUnitData",
|
|
34
|
+
description: "Create immutable UTC calendar bucket values."
|
|
35
|
+
},
|
|
36
|
+
function (args = {}) {
|
|
37
|
+
validateKeys(args, OPTIONS, "createTimeUnitData");
|
|
38
|
+
const id = validateUserId(args.id, "Time-unit dataset id");
|
|
39
|
+
const source = validateUserId(
|
|
40
|
+
args.source ?? this.context.currentData,
|
|
41
|
+
"Source dataset id"
|
|
42
|
+
);
|
|
43
|
+
const transform = normalizeTimeUnitTransform(args);
|
|
44
|
+
return this
|
|
45
|
+
.createDerivedData({ id, source, transform: [transform] })
|
|
46
|
+
.materializeTimeUnitData({ id });
|
|
47
|
+
}
|
|
48
|
+
);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { readQuantitativeField } from "../../grammar/scales/index.js";
|
|
3
|
+
import { resolveTarget, validateOptions } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
const OPTIONS = Object.freeze(["target", "value", "field", "fieldType"]);
|
|
6
|
+
|
|
7
|
+
function rematerialize(program, layer, target) {
|
|
8
|
+
return layer.mark.type === "tick"
|
|
9
|
+
? program.rematerializeTickMark({ id: target })
|
|
10
|
+
: program.rematerializePointMark({ id: target });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const encodeAngle = action(
|
|
14
|
+
{
|
|
15
|
+
op: "encodeAngle",
|
|
16
|
+
description: "Assign direct clockwise degree rotation to point or Tick glyphs."
|
|
17
|
+
},
|
|
18
|
+
function (args = {}) {
|
|
19
|
+
validateOptions(args, OPTIONS, "encodeAngle");
|
|
20
|
+
const hasValue = Object.hasOwn(args, "value");
|
|
21
|
+
const hasField = Object.hasOwn(args, "field");
|
|
22
|
+
if (hasValue === hasField) {
|
|
23
|
+
throw new Error("encodeAngle requires exactly one of value or field.");
|
|
24
|
+
}
|
|
25
|
+
if (hasValue && args.fieldType !== undefined) {
|
|
26
|
+
throw new Error("Constant angle does not accept fieldType.");
|
|
27
|
+
}
|
|
28
|
+
const { id: target, dataset, layer } = resolveTarget(
|
|
29
|
+
this,
|
|
30
|
+
args.target,
|
|
31
|
+
["point", "tick"],
|
|
32
|
+
"point or Tick mark"
|
|
33
|
+
);
|
|
34
|
+
if (hasValue && !Number.isFinite(args.value)) {
|
|
35
|
+
throw new TypeError("encodeAngle value must be finite degrees.");
|
|
36
|
+
}
|
|
37
|
+
if (hasField) {
|
|
38
|
+
const fieldType = args.fieldType ?? "quantitative";
|
|
39
|
+
if (fieldType !== "quantitative") {
|
|
40
|
+
throw new Error("encodeAngle requires a quantitative field.");
|
|
41
|
+
}
|
|
42
|
+
readQuantitativeField(dataset.values, args.field);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const previous = layer.encoding?.angle;
|
|
46
|
+
let next = this;
|
|
47
|
+
for (const property of ["datum", "field", "fieldType"]) {
|
|
48
|
+
if (Object.hasOwn(previous ?? {}, property)) {
|
|
49
|
+
next = next.editSemantic({
|
|
50
|
+
property: `layer[${target}].encoding.angle.${property}`,
|
|
51
|
+
remove: true
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (hasValue) {
|
|
56
|
+
next = next.editSemantic({
|
|
57
|
+
property: `layer[${target}].encoding.angle.datum`,
|
|
58
|
+
value: args.value
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
next = next
|
|
62
|
+
.editSemantic({
|
|
63
|
+
property: `layer[${target}].encoding.angle.field`,
|
|
64
|
+
value: args.field
|
|
65
|
+
})
|
|
66
|
+
.editSemantic({
|
|
67
|
+
property: `layer[${target}].encoding.angle.fieldType`,
|
|
68
|
+
value: "quantitative"
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return rematerialize(next, layer, target);
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export function registerAngleEncodingAction(ProgramClass) {
|
|
76
|
+
ProgramClass.prototype.encodeAngle = encodeAngle;
|
|
77
|
+
}
|
|
@@ -70,8 +70,18 @@ const encodeColor = action(
|
|
|
70
70
|
const horizonColor = horizonTransform !== undefined &&
|
|
71
71
|
horizonTransform.as?.color === args.field &&
|
|
72
72
|
layer.encoding?.group?.field === horizonTransform.as?.group;
|
|
73
|
+
const barGrain = resolveBarGrain(layer);
|
|
74
|
+
if (layer.mark.type === "bar" && barGrain === undefined) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"Bar color encoding requires a complete histogram encoding or a complete ordinal aggregate encoding."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const layout = resolveColorLayout(layer, args.layout, barGrain);
|
|
80
|
+
const createsCenterGroup = layer.mark.type === "area" &&
|
|
81
|
+
layout === "center" && layer.encoding?.group?.field === undefined;
|
|
73
82
|
if (
|
|
74
83
|
layer.mark.type === "area" &&
|
|
84
|
+
!createsCenterGroup &&
|
|
75
85
|
(layer.encoding?.group?.field === undefined ||
|
|
76
86
|
layer.encoding.group.field !== args.field) &&
|
|
77
87
|
!densitySeriesFields.includes(args.field) &&
|
|
@@ -82,14 +92,6 @@ const encodeColor = action(
|
|
|
82
92
|
);
|
|
83
93
|
}
|
|
84
94
|
validateLineSeriesCompatibility(layer, "color", args.field);
|
|
85
|
-
|
|
86
|
-
const barGrain = resolveBarGrain(layer);
|
|
87
|
-
if (layer.mark.type === "bar" && barGrain === undefined) {
|
|
88
|
-
throw new Error(
|
|
89
|
-
"Bar color encoding requires a complete histogram encoding or a complete ordinal aggregate encoding."
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
const layout = resolveColorLayout(layer, args.layout, barGrain);
|
|
93
95
|
const requestedScale = resolveReassignmentScaleOptions(
|
|
94
96
|
layer.encoding?.color,
|
|
95
97
|
resolveColorScaleOptions(args)
|
|
@@ -112,11 +114,13 @@ const encodeColor = action(
|
|
|
112
114
|
readNominalField(dataset.values, args.field);
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
let next =
|
|
116
|
-
.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
117
|
+
let next = createsCenterGroup
|
|
118
|
+
? this.encodeGroup({ target, field: args.field })
|
|
119
|
+
: this;
|
|
120
|
+
next = next.editSemantic({
|
|
121
|
+
property: `layer[${target}].encoding.color.field`,
|
|
122
|
+
value: args.field
|
|
123
|
+
})
|
|
120
124
|
.editSemantic({
|
|
121
125
|
property: `layer[${target}].encoding.color.fieldType`,
|
|
122
126
|
value: fieldType
|
|
@@ -74,6 +74,9 @@ export function resolveColorLayout(layer, requested, barGrain) {
|
|
|
74
74
|
if (layer.mark.type === "area" && layout === "group") {
|
|
75
75
|
throw new Error('Area color layout does not support "group".');
|
|
76
76
|
}
|
|
77
|
+
if (layer.mark.type === "bar" && layout === "center") {
|
|
78
|
+
throw new Error("Centered bars are not supported.");
|
|
79
|
+
}
|
|
77
80
|
if (
|
|
78
81
|
layer.mark.type === "arc" &&
|
|
79
82
|
layout !== undefined &&
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerAppearanceEncodingAction } from "./appearance.js";
|
|
2
|
+
import { registerAngleEncodingAction } from "./angle.js";
|
|
2
3
|
import { registerBarWidthEncodingAction } from "./barWidth.js";
|
|
3
4
|
import { registerColorEncodingAction } from "./color/index.js";
|
|
4
5
|
import { registerDensityEncodingAction } from "./density.js";
|
|
@@ -26,6 +27,7 @@ export function registerEncodingActions(ProgramClass) {
|
|
|
26
27
|
registerColorEncodingAction(ProgramClass);
|
|
27
28
|
registerStrokeDashEncodingActions(ProgramClass);
|
|
28
29
|
registerAppearanceEncodingAction(ProgramClass);
|
|
30
|
+
registerAngleEncodingAction(ProgramClass);
|
|
29
31
|
registerRuleAppearanceEncodingActions(ProgramClass);
|
|
30
32
|
registerTextEncodingAction(ProgramClass);
|
|
31
33
|
registerParallelEncodingAction(ProgramClass);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { validateStack, emptyPositionPolicy } from "./common.js";
|
|
2
|
+
import { deriveCenteredAreaSeries } from "../../../../grammar/areaSeries.js";
|
|
3
|
+
import { readQuantitativeField } from "../../../../grammar/scales/index.js";
|
|
2
4
|
|
|
3
5
|
export function isCategoricalDensityPosition({
|
|
4
6
|
layer,
|
|
@@ -15,7 +17,14 @@ export function isCategoricalDensityPosition({
|
|
|
15
17
|
density.placement.categoryField === field;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export function resolveAreaPositionPolicy({
|
|
20
|
+
export function resolveAreaPositionPolicy({
|
|
21
|
+
dataset,
|
|
22
|
+
layer,
|
|
23
|
+
channel,
|
|
24
|
+
args,
|
|
25
|
+
fieldType,
|
|
26
|
+
field
|
|
27
|
+
}) {
|
|
19
28
|
const density = dataset.transform?.find(transform => transform.type === "density");
|
|
20
29
|
if (["nominal", "ordinal"].includes(fieldType)) {
|
|
21
30
|
if (
|
|
@@ -41,6 +50,35 @@ export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType, f
|
|
|
41
50
|
throw new Error("Area position encoding does not support aggregate or bin.");
|
|
42
51
|
}
|
|
43
52
|
if (args.stack === undefined) return emptyPositionPolicy();
|
|
53
|
+
const stack = validateStack(args.stack, "Area y encoding");
|
|
54
|
+
if (stack === "center") {
|
|
55
|
+
if (channel !== "y") {
|
|
56
|
+
throw new Error("Area center stack requires a y encoding.");
|
|
57
|
+
}
|
|
58
|
+
if (layer.encoding?.group?.fieldType !== "nominal") {
|
|
59
|
+
throw new Error("Area center stack requires a nominal group encoding.");
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
layer.encoding?.color?.layout !== undefined &&
|
|
63
|
+
layer.encoding.color.layout !== "center"
|
|
64
|
+
) {
|
|
65
|
+
throw new Error("Area center stack requires a matching center color layout.");
|
|
66
|
+
}
|
|
67
|
+
const values = readQuantitativeField(dataset.values, field);
|
|
68
|
+
if (values.some(value => value < 0)) {
|
|
69
|
+
throw new RangeError("Area center stack requires non-negative values.");
|
|
70
|
+
}
|
|
71
|
+
if (layer.encoding?.x !== undefined) {
|
|
72
|
+
deriveCenteredAreaSeries(dataset.values, {
|
|
73
|
+
...layer,
|
|
74
|
+
encoding: {
|
|
75
|
+
...layer.encoding,
|
|
76
|
+
y: { field, fieldType, stack: "center" }
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return { bin: undefined, aggregate: undefined, stack };
|
|
81
|
+
}
|
|
44
82
|
if (
|
|
45
83
|
channel !== "y" ||
|
|
46
84
|
density === undefined
|
|
@@ -50,6 +88,6 @@ export function resolveAreaPositionPolicy({ dataset, channel, args, fieldType, f
|
|
|
50
88
|
return {
|
|
51
89
|
bin: undefined,
|
|
52
90
|
aggregate: undefined,
|
|
53
|
-
stack
|
|
91
|
+
stack
|
|
54
92
|
};
|
|
55
93
|
}
|
|
@@ -52,6 +52,9 @@ export function resolveBarPositionPolicy({
|
|
|
52
52
|
throw new Error('Histogram bar y aggregate must be "count".');
|
|
53
53
|
}
|
|
54
54
|
stack = validateStack(stack, "Histogram bar y encoding");
|
|
55
|
+
if (stack === "center") {
|
|
56
|
+
throw new Error("Centered bars are not supported.");
|
|
57
|
+
}
|
|
55
58
|
} else if (fieldType === "quantitative") {
|
|
56
59
|
if (args.bin !== undefined) {
|
|
57
60
|
throw new Error(
|
|
@@ -80,6 +83,9 @@ export function resolveBarPositionPolicy({
|
|
|
80
83
|
aggregate = validateAggregate(aggregate);
|
|
81
84
|
validateAggregateFieldType(aggregate, fieldType);
|
|
82
85
|
stack = validateStack(stack, `Bar ${channel} encoding`);
|
|
86
|
+
if (stack === "center") {
|
|
87
|
+
throw new Error("Centered bars are not supported.");
|
|
88
|
+
}
|
|
83
89
|
} else {
|
|
84
90
|
throw new Error(
|
|
85
91
|
"Bar position requires quantitative, temporal, ordinal, or nominal fields."
|
|
@@ -9,6 +9,7 @@ import { resolveBarPositionPolicy } from "./bar.js";
|
|
|
9
9
|
import { resolveLinePositionPolicy } from "./line.js";
|
|
10
10
|
import { resolvePointPositionPolicy } from "./point.js";
|
|
11
11
|
import { resolveRulePositionPolicy } from "./rule.js";
|
|
12
|
+
import { resolveTickPositionPolicy } from "./tick.js";
|
|
12
13
|
|
|
13
14
|
const POSITION_POLICIES = Object.freeze({
|
|
14
15
|
area: resolveAreaPositionPolicy,
|
|
@@ -17,6 +18,7 @@ const POSITION_POLICIES = Object.freeze({
|
|
|
17
18
|
line: resolveLinePositionPolicy,
|
|
18
19
|
point: resolvePointPositionPolicy,
|
|
19
20
|
rule: resolveRulePositionPolicy,
|
|
21
|
+
tick: resolveTickPositionPolicy,
|
|
20
22
|
text: resolvePointPositionPolicy,
|
|
21
23
|
rect: resolvePointPositionPolicy
|
|
22
24
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { emptyPositionPolicy } from "./common.js";
|
|
2
|
+
|
|
3
|
+
export function resolveTickPositionPolicy({ args, fieldType }) {
|
|
4
|
+
if (!["quantitative", "temporal", "ordinal", "nominal"].includes(fieldType)) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"Tick position encoding requires quantitative, temporal, ordinal, or nominal fields."
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
if (args.aggregate !== undefined) {
|
|
10
|
+
throw new Error("Tick position encoding does not support aggregate.");
|
|
11
|
+
}
|
|
12
|
+
if (args.bin !== undefined) {
|
|
13
|
+
throw new Error("Tick position encoding does not support bin.");
|
|
14
|
+
}
|
|
15
|
+
if (args.stack !== undefined) {
|
|
16
|
+
throw new Error("Tick position encoding does not support stack.");
|
|
17
|
+
}
|
|
18
|
+
return emptyPositionPolicy();
|
|
19
|
+
}
|
|
@@ -21,7 +21,7 @@ const OPTIONS = Object.freeze(["target", "channel"]);
|
|
|
21
21
|
const REMOVABLE_CHANNELS = Object.freeze([
|
|
22
22
|
"x", "y", "x2", "y2", "xOffset", "yOffset", "theta", "radius",
|
|
23
23
|
"color", "strokeDash", "strokeWidth", "size", "shape", "group",
|
|
24
|
-
"opacity", "text"
|
|
24
|
+
"angle", "opacity", "text"
|
|
25
25
|
]);
|
|
26
26
|
const REMOVE_AXIS = Object.freeze({
|
|
27
27
|
x: "removeXAxis",
|
|
@@ -8,6 +8,11 @@ import { isSizeLegendPoint } from "../size.js";
|
|
|
8
8
|
import { isStrokeWidthLegendLayer } from "../strokeWidth.js";
|
|
9
9
|
import { legendResourcePolicies } from
|
|
10
10
|
"../../../../materialization/guides/resources.js";
|
|
11
|
+
import {
|
|
12
|
+
hasMultiHorizontalLegendLane,
|
|
13
|
+
hasMultiLegendLane,
|
|
14
|
+
hasMultiSideLegendLane
|
|
15
|
+
} from "../lane.js";
|
|
11
16
|
import {
|
|
12
17
|
resolveCurrentDefinition,
|
|
13
18
|
resolveDefinition,
|
|
@@ -91,6 +96,12 @@ export const rematerializeLegend = action(
|
|
|
91
96
|
next = next[policy.rematerializeOp]();
|
|
92
97
|
}
|
|
93
98
|
}
|
|
99
|
+
if (hasMultiSideLegendLane(next)) {
|
|
100
|
+
next = next.rematerializeSideLegendLane();
|
|
101
|
+
}
|
|
102
|
+
if (hasMultiHorizontalLegendLane(next)) {
|
|
103
|
+
next = next.rematerializeHorizontalLegendLane();
|
|
104
|
+
}
|
|
94
105
|
return next;
|
|
95
106
|
}
|
|
96
107
|
);
|
|
@@ -212,10 +223,11 @@ export const createLegend = action(
|
|
|
212
223
|
if (position !== undefined && position !== "right") {
|
|
213
224
|
throw new Error('Standalone size legends currently require position "right".');
|
|
214
225
|
}
|
|
215
|
-
|
|
226
|
+
const next = this.createSizeLegend({
|
|
216
227
|
...(target === undefined ? {} : { target }),
|
|
217
228
|
...(count === undefined ? {} : { count })
|
|
218
229
|
});
|
|
230
|
+
return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
|
|
219
231
|
}
|
|
220
232
|
const strokeWidthCandidates = this.semanticSpec.layers.filter(
|
|
221
233
|
isStrokeWidthLegendLayer
|
|
@@ -248,10 +260,11 @@ export const createLegend = action(
|
|
|
248
260
|
'Standalone stroke-width legends currently require position "right".'
|
|
249
261
|
);
|
|
250
262
|
}
|
|
251
|
-
|
|
263
|
+
const next = this.createStrokeWidthLegend({
|
|
252
264
|
...(target === undefined ? {} : { target }),
|
|
253
265
|
...(count === undefined ? {} : { count })
|
|
254
266
|
});
|
|
267
|
+
return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
|
|
255
268
|
}
|
|
256
269
|
const opacityCandidates = this.semanticSpec.layers.filter(layer =>
|
|
257
270
|
layer.mark?.type === "point" && layer.encoding?.opacity?.scale !== undefined
|
|
@@ -266,7 +279,8 @@ export const createLegend = action(
|
|
|
266
279
|
(channels === undefined && opacityCandidates.length === 1 &&
|
|
267
280
|
!hasOtherLegendCandidate)
|
|
268
281
|
) {
|
|
269
|
-
|
|
282
|
+
const next = this.createOpacityLegend(args);
|
|
283
|
+
return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
|
|
270
284
|
}
|
|
271
285
|
const continuousColorCandidates = this.semanticSpec.layers.filter(layer => {
|
|
272
286
|
const encoding = ["point", "bar", "rect"].includes(layer.mark?.type)
|
|
@@ -287,7 +301,8 @@ export const createLegend = action(
|
|
|
287
301
|
(channels?.length === 1 && channels[0] === "color" && continuousColor) ||
|
|
288
302
|
(channels === undefined && continuousColor)
|
|
289
303
|
) {
|
|
290
|
-
|
|
304
|
+
const next = this.createGradientLegend(args);
|
|
305
|
+
return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
|
|
291
306
|
}
|
|
292
307
|
const intervalColorCandidates = this.semanticSpec.layers.filter(layer => {
|
|
293
308
|
const encoding = layer.mark?.type === "point" ? layer.encoding?.color : undefined;
|
|
@@ -307,7 +322,8 @@ export const createLegend = action(
|
|
|
307
322
|
intervalColorCandidates.length > 0) ||
|
|
308
323
|
(channels === undefined && intervalColorCandidates.length > 0)
|
|
309
324
|
) {
|
|
310
|
-
|
|
325
|
+
const next = this.createIntervalLegend(args);
|
|
326
|
+
return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
|
|
311
327
|
}
|
|
312
328
|
const wantsShape = channels?.includes("shape") === true;
|
|
313
329
|
const pointCandidates = this.semanticSpec.layers.filter(layer =>
|
|
@@ -383,7 +399,8 @@ export const createLegend = action(
|
|
|
383
399
|
}
|
|
384
400
|
return next;
|
|
385
401
|
}
|
|
386
|
-
|
|
402
|
+
const next = this.createCategoricalLegend(args);
|
|
403
|
+
return hasMultiLegendLane(next) ? next.rematerializeLegend() : next;
|
|
387
404
|
}
|
|
388
405
|
);
|
|
389
406
|
|
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
rematerializeLegendHighlights
|
|
25
25
|
} from "./symbols.js";
|
|
26
26
|
import { editLegend } from "../edit.js";
|
|
27
|
+
import {
|
|
28
|
+
rematerializeHorizontalLegendLane,
|
|
29
|
+
rematerializeSideLegendLane
|
|
30
|
+
} from "../lane.js";
|
|
27
31
|
|
|
28
32
|
export function registerBasicCategoricalLegendActions(ProgramClass) {
|
|
29
33
|
ProgramClass.prototype.createLegend = createLegend;
|
|
@@ -45,6 +49,10 @@ export function registerBasicCategoricalLegendActions(ProgramClass) {
|
|
|
45
49
|
ProgramClass.prototype.createLegendTitle = createLegendTitle;
|
|
46
50
|
ProgramClass.prototype.rematerializeLegendTitle = rematerializeLegendTitle;
|
|
47
51
|
ProgramClass.prototype.rematerializeLegend = rematerializeLegend;
|
|
52
|
+
ProgramClass.prototype.rematerializeSideLegendLane =
|
|
53
|
+
rematerializeSideLegendLane;
|
|
54
|
+
ProgramClass.prototype.rematerializeHorizontalLegendLane =
|
|
55
|
+
rematerializeHorizontalLegendLane;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export function registerCategoricalLegendActions(ProgramClass) {
|