ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
2
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
3
|
+
import { resolveEligibleLayer } from "../../selectors/layers.js";
|
|
4
|
+
import { hasMaterializedLegend } from "../../materialization/legends.js";
|
|
5
|
+
import { findSemanticScale } from "../../selectors/scales.js";
|
|
6
|
+
import { validateKeys } from "../../core/validation.js";
|
|
7
|
+
|
|
8
|
+
export function validateOptions(args, supported, operation) {
|
|
9
|
+
validateKeys(args, supported, operation);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validateLineSeriesCompatibility(layer, channel, field) {
|
|
13
|
+
if (layer.mark?.type !== "line") return;
|
|
14
|
+
for (const companion of ["group", "color", "strokeDash"]) {
|
|
15
|
+
if (companion === channel) continue;
|
|
16
|
+
const companionField = layer.encoding?.[companion]?.field;
|
|
17
|
+
if (companionField !== undefined && companionField !== field) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Line ${channel} field "${field}" must match ${companion} field "${companionField}".`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveReassignmentScaleOptions(encoding, options) {
|
|
26
|
+
if (encoding?.scale === undefined || options?.id !== undefined) return options;
|
|
27
|
+
return { ...options, id: encoding.scale };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function applyEncodingScale(
|
|
31
|
+
program,
|
|
32
|
+
definition,
|
|
33
|
+
options = {},
|
|
34
|
+
{ reassignment = false } = {}
|
|
35
|
+
) {
|
|
36
|
+
const existing = findSemanticScale(program, definition.id);
|
|
37
|
+
if (existing === undefined) {
|
|
38
|
+
return program.createScale(definition);
|
|
39
|
+
}
|
|
40
|
+
if (existing.type !== definition.type) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Scale "${definition.id}" cannot change type from "${existing.type}" to "${definition.type}".`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const patch = { id: definition.id };
|
|
47
|
+
for (const property of [
|
|
48
|
+
"domain", "range", "nice", "zero", "clamp", "reverse",
|
|
49
|
+
"base", "exponent", "constant", "paddingInner", "paddingOuter",
|
|
50
|
+
"padding", "align", "unknown", "interpolate"
|
|
51
|
+
]) {
|
|
52
|
+
if (Object.hasOwn(options, property)) patch[property] = definition[property];
|
|
53
|
+
}
|
|
54
|
+
if (Object.hasOwn(options, "palette")) patch.range = definition.range;
|
|
55
|
+
|
|
56
|
+
return Object.keys(patch).length === 1 || !reassignment
|
|
57
|
+
? program.createScale(definition)
|
|
58
|
+
: program.editScale(patch);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function rebindPositionGuides(
|
|
62
|
+
program,
|
|
63
|
+
channel,
|
|
64
|
+
previousScale,
|
|
65
|
+
nextScale,
|
|
66
|
+
target
|
|
67
|
+
) {
|
|
68
|
+
if (previousScale === undefined || previousScale === nextScale) return program;
|
|
69
|
+
|
|
70
|
+
const axis = program.semanticSpec.guides.axis?.[channel];
|
|
71
|
+
const direction = channel === "x" ? "vertical" : "horizontal";
|
|
72
|
+
const grid = program.semanticSpec.guides.grid?.[direction];
|
|
73
|
+
const ownsAxis = axis?.scale === previousScale;
|
|
74
|
+
const ownsGrid = grid?.scale === previousScale;
|
|
75
|
+
if (!ownsAxis && !ownsGrid) return program;
|
|
76
|
+
|
|
77
|
+
const remaining = program.semanticSpec.layers.some(layer =>
|
|
78
|
+
layer.id !== target && layer.encoding?.[channel]?.scale === previousScale
|
|
79
|
+
);
|
|
80
|
+
if (remaining) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Cannot rebind ${channel} guides from shared scale "${previousScale}" while it has other consumers.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let next = program;
|
|
87
|
+
if (ownsAxis) {
|
|
88
|
+
next = next.editSemantic({
|
|
89
|
+
property: `guide.axis.${channel}.scale`,
|
|
90
|
+
value: nextScale
|
|
91
|
+
});
|
|
92
|
+
for (const component of ["ticks", "labels", "title"]) {
|
|
93
|
+
const config = next.guideConfigs.axis?.[channel]?.[component];
|
|
94
|
+
if (config?.scale === previousScale) {
|
|
95
|
+
next = next._withGuideConfig(channel, component, {
|
|
96
|
+
...config,
|
|
97
|
+
scale: nextScale
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (ownsGrid) {
|
|
103
|
+
next = next
|
|
104
|
+
.editSemantic({
|
|
105
|
+
property: `guide.grid.${direction}.scale`,
|
|
106
|
+
value: nextScale
|
|
107
|
+
})
|
|
108
|
+
._withGridConfig(direction, {
|
|
109
|
+
...next.guideConfigs.grid[direction],
|
|
110
|
+
scale: nextScale
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return next;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function rematerializeExistingLegend(program) {
|
|
117
|
+
return hasMaterializedLegend(program)
|
|
118
|
+
? program.rematerializeLegend()
|
|
119
|
+
: program;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function resolveTarget(
|
|
123
|
+
program,
|
|
124
|
+
target,
|
|
125
|
+
supportedTypes = ["point", "line"],
|
|
126
|
+
label = "position mark"
|
|
127
|
+
) {
|
|
128
|
+
const requested = target === undefined
|
|
129
|
+
? undefined
|
|
130
|
+
: validateUserId(target, "Mark id");
|
|
131
|
+
const layer = resolveEligibleLayer(program, {
|
|
132
|
+
target: requested,
|
|
133
|
+
predicate: candidate => supportedTypes.includes(candidate.mark?.type),
|
|
134
|
+
label
|
|
135
|
+
});
|
|
136
|
+
const id = layer.id;
|
|
137
|
+
|
|
138
|
+
const dataset = findDataset(program, layer.data);
|
|
139
|
+
|
|
140
|
+
if (dataset === undefined) {
|
|
141
|
+
throw new Error(`Mark "${id}" requires an existing dataset.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const expectedGraphic = {
|
|
145
|
+
point: ["circle", "rect", "collection"],
|
|
146
|
+
line: "path",
|
|
147
|
+
bar: "rect",
|
|
148
|
+
area: "path",
|
|
149
|
+
rule: "line"
|
|
150
|
+
}[layer.mark.type];
|
|
151
|
+
|
|
152
|
+
const graphicType = program.graphicSpec.objects[id]?.type;
|
|
153
|
+
const matches = Array.isArray(expectedGraphic)
|
|
154
|
+
? expectedGraphic.includes(graphicType)
|
|
155
|
+
: graphicType === expectedGraphic;
|
|
156
|
+
if (!matches) {
|
|
157
|
+
const label = Array.isArray(expectedGraphic)
|
|
158
|
+
? expectedGraphic.join(" or ")
|
|
159
|
+
: expectedGraphic;
|
|
160
|
+
throw new Error(`Mark "${id}" requires ${label} graphics.`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { id, dataset, layer };
|
|
164
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import {
|
|
3
|
+
normalizeStrokeDashPattern,
|
|
4
|
+
readNominalField,
|
|
5
|
+
validateNominalFieldType
|
|
6
|
+
} from "../../grammar/scales.js";
|
|
7
|
+
import { resolveStrokeDashScaleDefinition } from "../scales/definitions.js";
|
|
8
|
+
import { applyMaterializationPlan } from "../../materialization/dependencies.js";
|
|
9
|
+
import { planEncodingRematerialization } from "../../materialization/encodings.js";
|
|
10
|
+
import {
|
|
11
|
+
applyEncodingScale,
|
|
12
|
+
resolveReassignmentScaleOptions,
|
|
13
|
+
resolveTarget,
|
|
14
|
+
validateLineSeriesCompatibility,
|
|
15
|
+
validateOptions
|
|
16
|
+
} from "./shared.js";
|
|
17
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
18
|
+
|
|
19
|
+
const STROKE_DASH_ENCODING_OPTIONS = Object.freeze([
|
|
20
|
+
"field", "value", "target", "fieldType", "scale"
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const clearStrokeDashEncoding = action(
|
|
24
|
+
{
|
|
25
|
+
op: "clearStrokeDashEncoding",
|
|
26
|
+
description: "Remove the current semantic stroke-dash assignment."
|
|
27
|
+
},
|
|
28
|
+
function ({ target } = {}) {
|
|
29
|
+
const layer = findLayer(this, target);
|
|
30
|
+
if (layer?.encoding?.strokeDash === undefined) return this;
|
|
31
|
+
return this.editSemantic({
|
|
32
|
+
property: `layer[${target}].encoding.strokeDash`,
|
|
33
|
+
remove: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
function reconcileLegendAfterDashRemoval(program, target) {
|
|
39
|
+
const config = program.guideConfigs.legend?.series;
|
|
40
|
+
if (
|
|
41
|
+
config?.target !== target ||
|
|
42
|
+
!config.channels.includes("strokeDash")
|
|
43
|
+
) {
|
|
44
|
+
return program;
|
|
45
|
+
}
|
|
46
|
+
const layer = findLayer(program, target);
|
|
47
|
+
const channels = config.channels.filter(
|
|
48
|
+
channel =>
|
|
49
|
+
channel !== "strokeDash" &&
|
|
50
|
+
layer.encoding?.[channel]?.scale !== undefined
|
|
51
|
+
);
|
|
52
|
+
if (channels.length === 0) return program.removeCategoricalLegend();
|
|
53
|
+
return program
|
|
54
|
+
.editSemantic({
|
|
55
|
+
property: "guide.legend.series.channels",
|
|
56
|
+
value: channels
|
|
57
|
+
})
|
|
58
|
+
.rematerializeLegend();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const encodeStrokeDash = action(
|
|
62
|
+
{
|
|
63
|
+
op: "encodeStrokeDash",
|
|
64
|
+
description: "Encode a nominal field as line stroke dash."
|
|
65
|
+
},
|
|
66
|
+
function (args = {}) {
|
|
67
|
+
validateOptions(args, STROKE_DASH_ENCODING_OPTIONS, "encodeStrokeDash");
|
|
68
|
+
const hasField = Object.hasOwn(args, "field");
|
|
69
|
+
const hasValue = Object.hasOwn(args, "value");
|
|
70
|
+
if (hasField === hasValue) {
|
|
71
|
+
throw new Error("encodeStrokeDash requires exactly one of field or value.");
|
|
72
|
+
}
|
|
73
|
+
if (hasValue && (args.fieldType !== undefined || args.scale !== undefined)) {
|
|
74
|
+
throw new Error("Constant stroke dash does not accept fieldType or scale.");
|
|
75
|
+
}
|
|
76
|
+
const { id: target, dataset, layer } = resolveTarget(
|
|
77
|
+
this,
|
|
78
|
+
args.target,
|
|
79
|
+
["line", "rule"],
|
|
80
|
+
"line or rule mark"
|
|
81
|
+
);
|
|
82
|
+
if (hasValue) {
|
|
83
|
+
normalizeStrokeDashPattern(args.value);
|
|
84
|
+
let next = layer.encoding?.strokeDash === undefined
|
|
85
|
+
? this
|
|
86
|
+
: this.clearStrokeDashEncoding({ target });
|
|
87
|
+
next = next.editSemantic({
|
|
88
|
+
property: `layer[${target}].encoding.strokeDash.datum`,
|
|
89
|
+
value: args.value
|
|
90
|
+
});
|
|
91
|
+
next = reconcileLegendAfterDashRemoval(next, target);
|
|
92
|
+
return layer.mark.type === "rule"
|
|
93
|
+
? next.rematerializeRuleMark({ id: target })
|
|
94
|
+
: next.rematerializeLineMark({ id: target });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fieldType = validateNominalFieldType(args.fieldType ?? "nominal");
|
|
98
|
+
validateLineSeriesCompatibility(layer, "strokeDash", args.field);
|
|
99
|
+
const previous = layer.encoding?.strokeDash;
|
|
100
|
+
const requestedScale = previous?.field === args.field
|
|
101
|
+
? resolveReassignmentScaleOptions(previous, args.scale ?? {})
|
|
102
|
+
: args.scale ?? {};
|
|
103
|
+
const scale = resolveStrokeDashScaleDefinition(this, requestedScale);
|
|
104
|
+
if (Object.hasOwn(scale, "unknown")) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"StrokeDash scale unknown is not supported for grouped line or rule paths."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
readNominalField(dataset.values, args.field);
|
|
110
|
+
|
|
111
|
+
let next = previous === undefined
|
|
112
|
+
? this
|
|
113
|
+
: this.clearStrokeDashEncoding({ target });
|
|
114
|
+
next = next
|
|
115
|
+
.editSemantic({
|
|
116
|
+
property: `layer[${target}].encoding.strokeDash.field`,
|
|
117
|
+
value: args.field
|
|
118
|
+
})
|
|
119
|
+
.editSemantic({
|
|
120
|
+
property: `layer[${target}].encoding.strokeDash.fieldType`,
|
|
121
|
+
value: fieldType
|
|
122
|
+
})
|
|
123
|
+
.editSemantic({
|
|
124
|
+
property: `layer[${target}].encoding.strokeDash.scale`,
|
|
125
|
+
value: scale.id
|
|
126
|
+
});
|
|
127
|
+
next = applyEncodingScale(next, scale, requestedScale, {
|
|
128
|
+
reassignment: previous?.scale === scale.id
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return applyMaterializationPlan(
|
|
132
|
+
next,
|
|
133
|
+
planEncodingRematerialization(next, {
|
|
134
|
+
target,
|
|
135
|
+
channel: "strokeDash",
|
|
136
|
+
scale: scale.id
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
export function registerStrokeDashEncodingActions(ProgramClass) {
|
|
143
|
+
ProgramClass.prototype.encodeStrokeDash = encodeStrokeDash;
|
|
144
|
+
ProgramClass.prototype.clearStrokeDashEncoding = clearStrokeDashEncoding;
|
|
145
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
|
|
3
|
+
function positionOptions({ target, field, fieldType, coordinate, scale }) {
|
|
4
|
+
return {
|
|
5
|
+
target,
|
|
6
|
+
field,
|
|
7
|
+
fieldType,
|
|
8
|
+
coordinate,
|
|
9
|
+
scale: { id: scale }
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const createErrorBandBoundary = action(
|
|
14
|
+
{
|
|
15
|
+
op: "createErrorBandBoundary",
|
|
16
|
+
description: "Create one lower or upper error-band boundary line."
|
|
17
|
+
},
|
|
18
|
+
function ({
|
|
19
|
+
id,
|
|
20
|
+
data,
|
|
21
|
+
orientation,
|
|
22
|
+
bound,
|
|
23
|
+
position,
|
|
24
|
+
coordinate,
|
|
25
|
+
intervalScale,
|
|
26
|
+
positionScale,
|
|
27
|
+
groupBy,
|
|
28
|
+
stroke,
|
|
29
|
+
strokeWidth,
|
|
30
|
+
strokeDash,
|
|
31
|
+
opacity,
|
|
32
|
+
curve
|
|
33
|
+
} = {}) {
|
|
34
|
+
let next = this.createLineMark({ id, data, strokeWidth, curve });
|
|
35
|
+
if (orientation === "vertical") {
|
|
36
|
+
next = next
|
|
37
|
+
.encodeY(positionOptions({
|
|
38
|
+
target: id,
|
|
39
|
+
field: bound,
|
|
40
|
+
fieldType: "quantitative",
|
|
41
|
+
coordinate,
|
|
42
|
+
scale: intervalScale
|
|
43
|
+
}))
|
|
44
|
+
.encodeX(positionOptions({
|
|
45
|
+
target: id,
|
|
46
|
+
field: position.field,
|
|
47
|
+
fieldType: position.fieldType,
|
|
48
|
+
coordinate,
|
|
49
|
+
scale: positionScale
|
|
50
|
+
}));
|
|
51
|
+
} else {
|
|
52
|
+
next = next
|
|
53
|
+
.encodeY(positionOptions({
|
|
54
|
+
target: id,
|
|
55
|
+
field: position.field,
|
|
56
|
+
fieldType: position.fieldType,
|
|
57
|
+
coordinate,
|
|
58
|
+
scale: positionScale
|
|
59
|
+
}))
|
|
60
|
+
.encodeX(positionOptions({
|
|
61
|
+
target: id,
|
|
62
|
+
field: bound,
|
|
63
|
+
fieldType: "quantitative",
|
|
64
|
+
coordinate,
|
|
65
|
+
scale: intervalScale
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
if (groupBy !== undefined) {
|
|
69
|
+
next = next.encodeGroup({ target: id, field: groupBy });
|
|
70
|
+
}
|
|
71
|
+
next = next
|
|
72
|
+
.editGraphics({ target: id, property: "stroke", value: stroke })
|
|
73
|
+
.editGraphics({
|
|
74
|
+
target: id,
|
|
75
|
+
property: "strokeDash",
|
|
76
|
+
value: next.graphicSpec.objects[id].items.map(() => strokeDash)
|
|
77
|
+
})
|
|
78
|
+
.editGraphics({ target: id, property: "opacity", value: opacity });
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
81
|
+
);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import {
|
|
4
|
+
validateKeys,
|
|
5
|
+
validateNonEmptyString,
|
|
6
|
+
validateNonNegativeFinite,
|
|
7
|
+
validateUnitInterval
|
|
8
|
+
} from "../../core/validation.js";
|
|
9
|
+
import { DEFAULT_COLORS } from "../../theme/defaults.js";
|
|
10
|
+
import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
|
|
11
|
+
import { normalizeStrokeDashPattern } from "../../grammar/scales.js";
|
|
12
|
+
import { resolveErrorBand } from "./resolve.js";
|
|
13
|
+
|
|
14
|
+
const OPTIONS = Object.freeze([
|
|
15
|
+
"id",
|
|
16
|
+
"target",
|
|
17
|
+
"data",
|
|
18
|
+
"x",
|
|
19
|
+
"y",
|
|
20
|
+
"groupBy",
|
|
21
|
+
"coordinate",
|
|
22
|
+
"fill",
|
|
23
|
+
"opacity",
|
|
24
|
+
"curve",
|
|
25
|
+
"boundaries"
|
|
26
|
+
]);
|
|
27
|
+
const BOUNDARY_OPTIONS = Object.freeze([
|
|
28
|
+
"stroke", "strokeWidth", "strokeDash", "opacity", "curve"
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function resolveBoundaries(value, areaCurve) {
|
|
32
|
+
if (value === undefined || value === false) return undefined;
|
|
33
|
+
if (!isPlainObject(value)) {
|
|
34
|
+
throw new TypeError("createErrorBand boundaries must be false or a plain object.");
|
|
35
|
+
}
|
|
36
|
+
validateKeys(value, BOUNDARY_OPTIONS, "createErrorBand boundaries");
|
|
37
|
+
const stroke = value.stroke ?? DEFAULT_COLORS.mark;
|
|
38
|
+
const strokeWidth = value.strokeWidth ?? 1;
|
|
39
|
+
const strokeDash = value.strokeDash ?? "solid";
|
|
40
|
+
const opacity = value.opacity ?? 1;
|
|
41
|
+
const curve = validateCurveInterpolation(value.curve ?? areaCurve);
|
|
42
|
+
validateNonEmptyString(stroke, "Error-band boundary stroke");
|
|
43
|
+
validateNonNegativeFinite(
|
|
44
|
+
strokeWidth,
|
|
45
|
+
"Error-band boundary strokeWidth"
|
|
46
|
+
);
|
|
47
|
+
const resolvedStrokeDash = normalizeStrokeDashPattern(strokeDash);
|
|
48
|
+
validateUnitInterval(opacity, "Error-band boundary opacity");
|
|
49
|
+
return {
|
|
50
|
+
stroke,
|
|
51
|
+
strokeWidth,
|
|
52
|
+
strokeDash: resolvedStrokeDash,
|
|
53
|
+
opacity,
|
|
54
|
+
curve
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function positionArgs(resolved) {
|
|
59
|
+
return {
|
|
60
|
+
target: resolved.id,
|
|
61
|
+
field: resolved.position.field,
|
|
62
|
+
fieldType: resolved.position.fieldType,
|
|
63
|
+
coordinate: resolved.coordinate,
|
|
64
|
+
scale: resolved.position.scale
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function rangeArgs(resolved) {
|
|
69
|
+
return {
|
|
70
|
+
target: resolved.id,
|
|
71
|
+
lower: resolved.fields.lower,
|
|
72
|
+
upper: resolved.fields.upper,
|
|
73
|
+
fieldType: "quantitative",
|
|
74
|
+
coordinate: resolved.coordinate,
|
|
75
|
+
scale: resolved.interval.scale
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const createErrorBand = action(
|
|
80
|
+
{
|
|
81
|
+
op: "createErrorBand",
|
|
82
|
+
description: "Create a statistical or explicit interval band."
|
|
83
|
+
},
|
|
84
|
+
function (args = {}) {
|
|
85
|
+
validateKeys(args, OPTIONS, "createErrorBand");
|
|
86
|
+
const resolved = resolveErrorBand(this, args);
|
|
87
|
+
const curve = validateCurveInterpolation(args.curve ?? "linear");
|
|
88
|
+
const boundaries = resolveBoundaries(args.boundaries, curve);
|
|
89
|
+
let next = this;
|
|
90
|
+
|
|
91
|
+
if (resolved.interval.mode === "statistical") {
|
|
92
|
+
next = next.createIntervalData({
|
|
93
|
+
id: resolved.dataId,
|
|
94
|
+
source: resolved.source,
|
|
95
|
+
field: resolved.interval.field,
|
|
96
|
+
groupBy: resolved.groupBy,
|
|
97
|
+
center: resolved.interval.center,
|
|
98
|
+
extent: resolved.interval.extent,
|
|
99
|
+
level: resolved.interval.level,
|
|
100
|
+
as: resolved.fields
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
next = next.createAreaMark({
|
|
105
|
+
id: resolved.id,
|
|
106
|
+
data: resolved.dataId,
|
|
107
|
+
...(args.fill === undefined ? {} : { fill: args.fill }),
|
|
108
|
+
...(args.opacity === undefined ? {} : { opacity: args.opacity }),
|
|
109
|
+
...(Object.hasOwn(args, "curve") ? { curve } : {})
|
|
110
|
+
});
|
|
111
|
+
next = resolved.orientation === "vertical"
|
|
112
|
+
? next
|
|
113
|
+
.encodeX(positionArgs(resolved))
|
|
114
|
+
.encodeYRange(rangeArgs(resolved))
|
|
115
|
+
: next
|
|
116
|
+
.encodeY(positionArgs(resolved))
|
|
117
|
+
.encodeXRange(rangeArgs(resolved));
|
|
118
|
+
|
|
119
|
+
if (resolved.interval.mode === "explicit") {
|
|
120
|
+
next = next.editSemantic({
|
|
121
|
+
property: `layer[${resolved.id}].encoding.${resolved.interval.channel}.title`,
|
|
122
|
+
value: resolved.interval.title
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (resolved.groupField !== undefined) {
|
|
126
|
+
next = next.encodeGroup({
|
|
127
|
+
target: resolved.id,
|
|
128
|
+
field: resolved.groupField,
|
|
129
|
+
fieldType: "nominal"
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (boundaries !== undefined) {
|
|
133
|
+
const shared = {
|
|
134
|
+
data: resolved.dataId,
|
|
135
|
+
orientation: resolved.orientation,
|
|
136
|
+
position: resolved.position,
|
|
137
|
+
coordinate: resolved.coordinate,
|
|
138
|
+
intervalScale:
|
|
139
|
+
resolved.interval.scale.id ?? resolved.interval.channel,
|
|
140
|
+
positionScale:
|
|
141
|
+
resolved.position.scale.id ?? resolved.position.channel,
|
|
142
|
+
groupBy: resolved.groupField,
|
|
143
|
+
...boundaries
|
|
144
|
+
};
|
|
145
|
+
next = next
|
|
146
|
+
.createErrorBandBoundary({
|
|
147
|
+
...shared,
|
|
148
|
+
id: `${resolved.id}LowerBoundary`,
|
|
149
|
+
bound: resolved.fields.lower
|
|
150
|
+
})
|
|
151
|
+
.createErrorBandBoundary({
|
|
152
|
+
...shared,
|
|
153
|
+
id: `${resolved.id}UpperBoundary`,
|
|
154
|
+
bound: resolved.fields.upper
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return next;
|
|
158
|
+
}
|
|
159
|
+
);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createErrorBand } from "./create.js";
|
|
2
|
+
import { createErrorBandBoundary } from "./components.js";
|
|
3
|
+
|
|
4
|
+
export function registerErrorBandActions(ProgramClass) {
|
|
5
|
+
ProgramClass.prototype.createErrorBand = createErrorBand;
|
|
6
|
+
ProgramClass.prototype.createErrorBandBoundary = createErrorBandBoundary;
|
|
7
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolveIntervalComposite } from "../intervals/resolve.js";
|
|
2
|
+
|
|
3
|
+
const ERROR_BAND_POLICY = Object.freeze({
|
|
4
|
+
operation: "createErrorBand",
|
|
5
|
+
resourceLabel: "error-band",
|
|
6
|
+
defaultId: "errorBand",
|
|
7
|
+
ownerLabel: "Error-band id",
|
|
8
|
+
positionTypes: Object.freeze(["quantitative", "temporal"]),
|
|
9
|
+
defaultPositionType: "quantitative",
|
|
10
|
+
defaultIntervalChannel: "y",
|
|
11
|
+
scaleDefaults: fieldType => fieldType === "temporal"
|
|
12
|
+
? { nice: true }
|
|
13
|
+
: { nice: true, zero: false },
|
|
14
|
+
intervalScaleDefaults: Object.freeze({ nice: true, zero: false }),
|
|
15
|
+
allowExplicitGrouping: true,
|
|
16
|
+
ambiguousMessage:
|
|
17
|
+
"createErrorBand cannot infer the interval axis when both positions are quantitative; provide an interval option."
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export function resolveErrorBand(program, args) {
|
|
21
|
+
const resolved = resolveIntervalComposite(program, args, ERROR_BAND_POLICY);
|
|
22
|
+
if (resolved.groupField === resolved.position.field) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"createErrorBand groupBy must differ from the independent position field."
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return resolved;
|
|
28
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateKeys } from "../../core/validation.js";
|
|
3
|
+
|
|
4
|
+
const OPTIONS = Object.freeze([
|
|
5
|
+
"id",
|
|
6
|
+
"data",
|
|
7
|
+
"orientation",
|
|
8
|
+
"positionField",
|
|
9
|
+
"positionFieldType",
|
|
10
|
+
"intervalField",
|
|
11
|
+
"coordinate",
|
|
12
|
+
"positionScale",
|
|
13
|
+
"intervalScale",
|
|
14
|
+
"capSize",
|
|
15
|
+
"stroke",
|
|
16
|
+
"strokeWidth",
|
|
17
|
+
"strokeDash",
|
|
18
|
+
"opacity"
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const createErrorBarCap = action(
|
|
22
|
+
{
|
|
23
|
+
op: "createErrorBarCap",
|
|
24
|
+
description: "Create one fixed-pixel error-bar cap."
|
|
25
|
+
},
|
|
26
|
+
function (args = {}) {
|
|
27
|
+
validateKeys(args, OPTIONS, "createErrorBarCap");
|
|
28
|
+
if (!["vertical", "horizontal"].includes(args.orientation)) {
|
|
29
|
+
throw new Error(`Unsupported error-bar orientation "${args.orientation}".`);
|
|
30
|
+
}
|
|
31
|
+
const positionChannel = args.orientation === "vertical" ? "x" : "y";
|
|
32
|
+
const intervalChannel = args.orientation === "vertical" ? "y" : "x";
|
|
33
|
+
const positionAction = positionChannel === "x" ? "encodeX" : "encodeY";
|
|
34
|
+
const intervalAction = intervalChannel === "x" ? "encodeX" : "encodeY";
|
|
35
|
+
|
|
36
|
+
return this
|
|
37
|
+
.createRuleMark({ id: args.id, data: args.data })
|
|
38
|
+
[positionAction]({
|
|
39
|
+
target: args.id,
|
|
40
|
+
field: args.positionField,
|
|
41
|
+
fieldType: args.positionFieldType,
|
|
42
|
+
coordinate: args.coordinate,
|
|
43
|
+
scale: { id: args.positionScale }
|
|
44
|
+
})
|
|
45
|
+
[intervalAction]({
|
|
46
|
+
target: args.id,
|
|
47
|
+
field: args.intervalField,
|
|
48
|
+
fieldType: "quantitative",
|
|
49
|
+
coordinate: args.coordinate,
|
|
50
|
+
scale: { id: args.intervalScale }
|
|
51
|
+
})
|
|
52
|
+
.encodeStroke({ target: args.id, value: args.stroke })
|
|
53
|
+
.encodeStrokeWidth({ target: args.id, value: args.strokeWidth })
|
|
54
|
+
.encodeStrokeDash({ target: args.id, value: args.strokeDash })
|
|
55
|
+
.encodeOpacity({ target: args.id, value: args.opacity })
|
|
56
|
+
.materializeRuleSpan({
|
|
57
|
+
id: args.id,
|
|
58
|
+
orientation: args.orientation === "vertical" ? "horizontal" : "vertical",
|
|
59
|
+
size: args.capSize
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
);
|