ggaction 0.0.3 → 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 +58 -3
- package/README.md +117 -83
- package/package.json +27 -11
- 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/regression.js +1 -1
- package/src/actions/data/window.js +50 -0
- package/src/actions/encodings/appearance.js +1 -1
- package/src/actions/encodings/color/continuous.js +137 -0
- package/src/actions/encodings/color/index.js +167 -0
- package/src/actions/encodings/color/layout.js +99 -0
- package/src/actions/encodings/color/policy.js +88 -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 +8 -2
- package/src/actions/encodings/offset.js +1 -1
- 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.js → position/index.js} +21 -8
- 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 +25 -3
- package/src/actions/encodings/ranged.js +5 -3
- package/src/actions/encodings/ruleAppearance.js +88 -10
- package/src/actions/encodings/strokeDash.js +1 -1
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/options.js +1 -1
- package/src/actions/errorBars/options.js +1 -1
- package/src/actions/facets/actions.js +2 -3
- package/src/actions/facets/derive.js +35 -5
- package/src/actions/facets/guides.js +1 -1
- 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 +12 -7
- package/src/actions/guides/axes/parallel/resolve.js +86 -0
- package/src/actions/guides/axes/parallel.js +228 -0
- package/src/actions/guides/axes/ticks.js +1 -1
- package/src/actions/guides/axes/titles.js +12 -9
- package/src/actions/guides/grids/resolve.js +1 -1
- package/src/actions/guides/guides.js +60 -5
- package/src/actions/guides/legends/categorical/actions.js +48 -11
- package/src/actions/guides/legends/categorical/layout.js +4 -2
- package/src/actions/guides/legends/continuous/interval.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +1 -1
- package/src/actions/guides/legends/edit.js +7 -20
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/remove.js +6 -31
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/strokeWidth.js +170 -0
- package/src/actions/guides/legends/target.js +25 -0
- package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
- package/src/actions/guides/polar/index.js +1 -1
- package/src/actions/guides/polar/resolve.js +1 -1
- package/src/actions/guides/tickValues.js +1 -1
- package/src/actions/index.js +6 -0
- package/src/actions/intervals/resolve.js +1 -1
- package/src/actions/marks/arc/actions.js +2 -2
- package/src/actions/marks/area/actions.js +61 -137
- package/src/actions/marks/area/materialize.js +163 -0
- package/src/actions/marks/bar/edit.js +1 -1
- package/src/actions/marks/index.js +5 -1
- package/src/actions/marks/line/actions.js +100 -81
- 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 +4 -1
- package/src/actions/marks/remove.js +13 -2
- package/src/actions/marks/rule/actions.js +26 -4
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +20 -5
- 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/components.js +1 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +20 -22
- package/src/actions/scales/consumers/common.js +109 -0
- package/src/actions/scales/consumers/families.js +58 -0
- package/src/actions/scales/consumers/index.js +41 -0
- package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
- package/src/actions/scales/create.js +1 -1
- package/src/actions/scales/definitions.js +32 -2
- package/src/actions/scales/edit.js +8 -206
- package/src/actions/scales/editPolicy.js +214 -0
- package/src/actions/scales/materialize.js +3 -3
- 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/ChartProgram.js +22 -236
- package/src/core/compositionState.js +169 -0
- package/src/core/materializationState.js +65 -0
- package/src/core/programState.js +28 -0
- package/src/core/textMetrics.js +47 -0
- package/src/core/vocabulary.js +7 -2
- package/src/grammar/arcs.js +50 -13
- package/src/grammar/areaSeries.js +42 -6
- package/src/grammar/bars/aggregate.js +1 -1
- 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 +253 -27
- package/src/grammar/facets/dependencies.js +3 -11
- package/src/grammar/{facets.js → facets/index.js} +8 -6
- package/src/grammar/gradientProfile.js +213 -0
- package/src/grammar/horizon.js +455 -0
- package/src/grammar/interval.js +2 -96
- package/src/grammar/jitter.js +197 -0
- package/src/grammar/lineSeries.js +70 -1
- package/src/grammar/paint.js +88 -0
- package/src/grammar/palettes.js +5 -7
- package/src/grammar/parallelCoordinates.js +213 -0
- package/src/grammar/pathOrder.js +35 -0
- package/src/grammar/pointShapes.js +24 -0
- package/src/grammar/polarLineCommands.js +1 -1
- package/src/grammar/regression/derive.js +97 -0
- package/src/grammar/regression/index.js +22 -0
- package/src/grammar/regression/models.js +282 -0
- package/src/grammar/regression/parameters.js +106 -0
- package/src/grammar/rules.js +2 -1
- package/src/grammar/scales/appearance.js +21 -0
- package/src/grammar/scales/color.js +4 -2
- 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/statistics/studentT.js +98 -0
- package/src/grammar/transforms.js +101 -12
- 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/bars/aggregate.js +1 -1
- package/src/materialization/bars/grouped.js +1 -1
- package/src/materialization/bars/histogram.js +1 -1
- package/src/materialization/bars/ranged.js +1 -1
- package/src/materialization/dataProvenance.js +56 -2
- package/src/materialization/density.js +1 -1
- package/src/materialization/dependencies.js +15 -101
- package/src/materialization/encodings.js +2 -2
- package/src/materialization/facetGuides/index.js +2 -0
- package/src/materialization/facetGuides/legacyCategorical.js +125 -0
- package/src/materialization/facetGuides/placement.js +126 -0
- package/src/materialization/facetGuides/preparation.js +188 -0
- package/src/materialization/facets.js +1 -1
- 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 +161 -0
- package/src/materialization/marks/index.js +108 -0
- package/src/materialization/marks/pathOrder.js +71 -0
- package/src/materialization/marks/policies.js +91 -0
- package/src/materialization/rect.js +1 -1
- package/src/materialization/rowEncoding.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +102 -0
- package/src/materialization/scales/map.js +1 -1
- package/src/materialization/scales/policies/series.js +1 -1
- package/src/materialization/scales/resolve.js +3 -1
- package/src/materialization/selection/items/bar.js +1 -1
- 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 +21 -10
- 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/renderers/canvas/text.js +14 -1
- package/src/selectors/datasets.js +4 -0
- package/src/theme/defaults.js +4 -0
- package/types/extension.d.ts +10 -1
- package/types/index.d.ts +76 -0
- package/types/program.d.ts +632 -10
- package/src/actions/encodings/color.js +0 -424
- package/src/grammar/regression.js +0 -578
- package/src/grammar/scales.js +0 -1
- package/src/materialization/facetGuides.js +0 -426
- package/src/materialization/marks.js +0 -316
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasOrdinalDomain,
|
|
3
|
+
isDiscretizedColorScaleType,
|
|
4
|
+
isDiscretePositionScaleType,
|
|
5
|
+
isTransformedScaleType,
|
|
6
|
+
normalizeScaleDefinition,
|
|
7
|
+
SCALE_ROLES,
|
|
8
|
+
validateColorRange,
|
|
9
|
+
validateDiscretizedColorDomain,
|
|
10
|
+
validateDiscretizedColorRange,
|
|
11
|
+
validateOrdinalDomain,
|
|
12
|
+
validateScaleDomain,
|
|
13
|
+
validateScaleRange,
|
|
14
|
+
validateScaleType,
|
|
15
|
+
validateScaleTypeForRole,
|
|
16
|
+
validateScaleUnknown,
|
|
17
|
+
validateSequentialColorRange,
|
|
18
|
+
validateShapeRange,
|
|
19
|
+
validateSizeRange,
|
|
20
|
+
validateStrokeDashRange,
|
|
21
|
+
validateStrokeWidthRange
|
|
22
|
+
} from "../../grammar/scales/index.js";
|
|
23
|
+
import {
|
|
24
|
+
validateRadialRange,
|
|
25
|
+
validateThetaRange
|
|
26
|
+
} from "../../grammar/polar.js";
|
|
27
|
+
import { normalizePositionScaleChannel } from "../../core/vocabulary.js";
|
|
28
|
+
|
|
29
|
+
export function resolveScaleConsumerChannel(consumers, id) {
|
|
30
|
+
const channels = new Set(
|
|
31
|
+
consumers.map(consumer => normalizePositionScaleChannel(consumer.channel))
|
|
32
|
+
);
|
|
33
|
+
if (channels.size > 1) {
|
|
34
|
+
throw new Error(`Scale "${id}" cannot be shared across channels.`);
|
|
35
|
+
}
|
|
36
|
+
return channels.values().next().value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateRangeForChannel(scale, channel, value) {
|
|
40
|
+
if (value === "auto") return value;
|
|
41
|
+
if (channel === "theta") return validateThetaRange(value);
|
|
42
|
+
if (channel === "radius") return validateRadialRange(value);
|
|
43
|
+
if (scale.type === "sequential") {
|
|
44
|
+
return validateSequentialColorRange(value);
|
|
45
|
+
}
|
|
46
|
+
if (isDiscretizedColorScaleType(scale.type)) {
|
|
47
|
+
return validateDiscretizedColorRange(value);
|
|
48
|
+
}
|
|
49
|
+
if (scale.type !== "ordinal") return validateScaleRange(value);
|
|
50
|
+
if (channel === "color") return validateColorRange(value);
|
|
51
|
+
if (channel === "shape") return validateShapeRange(value);
|
|
52
|
+
if (channel === "strokeDash") return validateStrokeDashRange(value);
|
|
53
|
+
if (channel === "size") return validateSizeRange(value);
|
|
54
|
+
if (channel === "strokeWidth") return validateStrokeWidthRange(value);
|
|
55
|
+
return validateScaleRange(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateTypeTransition(scale, nextType, channel, consumers) {
|
|
59
|
+
if (nextType === scale.type) return;
|
|
60
|
+
validateScaleType(nextType);
|
|
61
|
+
if (consumers.length === 0) return;
|
|
62
|
+
if (nextType === "sequential" || isDiscretizedColorScaleType(nextType)) {
|
|
63
|
+
const discretized = isDiscretizedColorScaleType(nextType);
|
|
64
|
+
if (channel !== "color" || consumers.some(consumer =>
|
|
65
|
+
["nominal", "ordinal"].includes(consumer.encoding.fieldType) ||
|
|
66
|
+
(discretized && (
|
|
67
|
+
consumer.encoding.fieldType !== "quantitative" ||
|
|
68
|
+
consumer.layer.mark?.type !== "point"
|
|
69
|
+
)) ||
|
|
70
|
+
(!discretized && !["point", "bar"].includes(consumer.layer.mark?.type))
|
|
71
|
+
)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (nextType === "time") {
|
|
79
|
+
if (
|
|
80
|
+
!["x", "y", "theta"].includes(channel) ||
|
|
81
|
+
consumers.some(consumer => consumer.encoding.fieldType !== "temporal")
|
|
82
|
+
) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Scale "${scale.id}" has a consumer incompatible with type "time".`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (isDiscretePositionScaleType(nextType)) {
|
|
90
|
+
if (
|
|
91
|
+
!["x", "y", "theta"].includes(channel) || consumers.some(consumer =>
|
|
92
|
+
!["nominal", "ordinal"].includes(consumer.encoding.fieldType)
|
|
93
|
+
)
|
|
94
|
+
) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
nextType === "point" &&
|
|
101
|
+
consumers.some(consumer => consumer.layer.mark?.type === "bar")
|
|
102
|
+
) {
|
|
103
|
+
throw new Error("Point scales cannot provide bar bandwidth.");
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const quantitative = nextType === "linear" || isTransformedScaleType(nextType);
|
|
108
|
+
if (
|
|
109
|
+
!quantitative ||
|
|
110
|
+
!["x", "y", "theta", "radius", "strokeWidth"].includes(channel)
|
|
111
|
+
) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"editScale type transition currently requires a quantitative position scale."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
validateScaleTypeForRole(nextType, SCALE_ROLES.quantitativePosition);
|
|
117
|
+
if (channel === "theta" && nextType !== "linear") {
|
|
118
|
+
throw new Error("Theta quantitative position currently requires a linear scale.");
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
channel === "strokeWidth" &&
|
|
122
|
+
consumers.some(consumer => !["line", "rule"].includes(consumer.layer.mark?.type))
|
|
123
|
+
) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (consumers.some(consumer => consumer.encoding.fieldType !== "quantitative")) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateLegendTypeTransition(program, scale, nextType) {
|
|
136
|
+
if (nextType === scale.type) return;
|
|
137
|
+
const legends = program.guideConfigs.legend ?? {};
|
|
138
|
+
if (legends.gradient?.scale === scale.id && nextType !== "sequential") {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Scale "${scale.id}" cannot change type while its gradient legend is active.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
legends.interval?.scale === scale.id &&
|
|
145
|
+
!isDiscretizedColorScaleType(nextType)
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Scale "${scale.id}" cannot change type while its interval legend is active.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeDefinition(scale, channel, consumers, patch) {
|
|
154
|
+
const type = patch.type ?? scale.type;
|
|
155
|
+
validateTypeTransition(scale, type, channel, consumers);
|
|
156
|
+
const definition = normalizeScaleDefinition({
|
|
157
|
+
type,
|
|
158
|
+
previous: scale,
|
|
159
|
+
patch,
|
|
160
|
+
retainCoreOnTypeChange: true,
|
|
161
|
+
retainCompatibleOnTypeChange: true,
|
|
162
|
+
validateDomain: (scaleType, value) =>
|
|
163
|
+
isDiscretizedColorScaleType(scaleType)
|
|
164
|
+
? validateDiscretizedColorDomain(scaleType, value)
|
|
165
|
+
: hasOrdinalDomain(scaleType)
|
|
166
|
+
? validateOrdinalDomain(value)
|
|
167
|
+
: validateScaleDomain(value),
|
|
168
|
+
validateRange: (scaleType, value) =>
|
|
169
|
+
validateRangeForChannel({ type: scaleType }, channel, value)
|
|
170
|
+
});
|
|
171
|
+
if (
|
|
172
|
+
channel === "strokeWidth" &&
|
|
173
|
+
definition.domain !== "auto" &&
|
|
174
|
+
definition.domain.some(value => value < 0)
|
|
175
|
+
) {
|
|
176
|
+
throw new RangeError("StrokeWidth scale domain cannot contain negative values.");
|
|
177
|
+
}
|
|
178
|
+
const typeChanged = type !== scale.type;
|
|
179
|
+
const unknown = Object.hasOwn(patch, "unknown")
|
|
180
|
+
? patch.unknown
|
|
181
|
+
: typeChanged ? undefined : scale.unknown;
|
|
182
|
+
if (unknown !== undefined) {
|
|
183
|
+
if (consumers.some(consumer => consumer.layer.mark?.type !== "point")) {
|
|
184
|
+
throw new Error("Scale unknown currently requires row-owned point consumers.");
|
|
185
|
+
}
|
|
186
|
+
definition.unknown = consumers.length === 0
|
|
187
|
+
? unknown
|
|
188
|
+
: validateScaleUnknown(channel, unknown);
|
|
189
|
+
}
|
|
190
|
+
return definition;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function prepareScaleEdit(program, scale, channel, consumers, args) {
|
|
194
|
+
const hasPalette = Object.hasOwn(args, "palette");
|
|
195
|
+
if (hasPalette && Object.hasOwn(args, "range")) {
|
|
196
|
+
throw new Error("editScale cannot specify both palette and range.");
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
hasPalette &&
|
|
200
|
+
channel !== "color" &&
|
|
201
|
+
scale.type !== "sequential" &&
|
|
202
|
+
!isDiscretizedColorScaleType(scale.type)
|
|
203
|
+
) {
|
|
204
|
+
throw new Error("editScale palette requires a color scale.");
|
|
205
|
+
}
|
|
206
|
+
const patch = !hasPalette
|
|
207
|
+
? args
|
|
208
|
+
: Object.fromEntries([
|
|
209
|
+
...Object.entries(args).filter(([key]) => key !== "palette"),
|
|
210
|
+
["range", { palette: args.palette }]
|
|
211
|
+
]);
|
|
212
|
+
validateLegendTypeTransition(program, scale, args.type ?? scale.type);
|
|
213
|
+
return normalizeDefinition(scale, channel, consumers, patch);
|
|
214
|
+
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
canDeferScaleConsumerApplication,
|
|
12
12
|
getMarkRematerializationStep,
|
|
13
13
|
getScaleConsumerMaterializationMode
|
|
14
|
-
} from "../../materialization/marks.js";
|
|
14
|
+
} from "../../materialization/marks/index.js";
|
|
15
15
|
import { mapScaleConsumerValues } from
|
|
16
16
|
"../../materialization/scales/map.js";
|
|
17
17
|
import { resolveScaleMaterialization } from
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
findScaleConsumers,
|
|
22
22
|
resolveConsumerValues,
|
|
23
23
|
resolveSeriesLayoutScaleValues
|
|
24
|
-
} from "./consumers.js";
|
|
24
|
+
} from "./consumers/index.js";
|
|
25
25
|
|
|
26
26
|
const OPTIONS = Object.freeze(["id", "guides", "marks"]);
|
|
27
27
|
|
|
@@ -70,7 +70,7 @@ export const rematerializeScale = action(
|
|
|
70
70
|
channel,
|
|
71
71
|
consumers,
|
|
72
72
|
valuesByConsumer,
|
|
73
|
-
bounds: ["color", "strokeDash", "shape", "size", "opacity", "xOffset", "yOffset"]
|
|
73
|
+
bounds: ["color", "strokeDash", "strokeWidth", "shape", "size", "opacity", "xOffset", "yOffset"]
|
|
74
74
|
.includes(channel)
|
|
75
75
|
? undefined
|
|
76
76
|
: resolveGraphicBounds(this),
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const SCALE_EDIT_PROPERTIES = Object.freeze([
|
|
2
|
+
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
3
|
+
"base", "exponent", "constant", "paddingInner", "paddingOuter",
|
|
4
|
+
"padding", "align", "interpolate", "unknown"
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
function sameValue(left, right) {
|
|
8
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function scaleEditPatch(id, definition) {
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
...Object.fromEntries(SCALE_EDIT_PROPERTIES
|
|
15
|
+
.filter(property => Object.hasOwn(definition, property))
|
|
16
|
+
.map(property => [property, definition[property]]))
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function changedScaleEditPatch(current, definition) {
|
|
21
|
+
const patch = scaleEditPatch(current.id, definition);
|
|
22
|
+
return Object.entries(patch).some(
|
|
23
|
+
([property, value]) => property !== "id" &&
|
|
24
|
+
!sameValue(current[property], value)
|
|
25
|
+
) ? patch : undefined;
|
|
26
|
+
}
|
|
@@ -117,7 +117,26 @@ export const applyPointHighlight = action(
|
|
|
117
117
|
}
|
|
118
118
|
);
|
|
119
119
|
|
|
120
|
-
function
|
|
120
|
+
function transformRectangularProperties(properties, style, offset = false) {
|
|
121
|
+
if (!offset || style.offset === undefined) {
|
|
122
|
+
return { ...properties, ...style };
|
|
123
|
+
}
|
|
124
|
+
const { offset: translation, ...appearance } = style;
|
|
125
|
+
return {
|
|
126
|
+
...properties,
|
|
127
|
+
...appearance,
|
|
128
|
+
x: properties.x + translation.x,
|
|
129
|
+
y: properties.y + translation.y
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applyRectangularHighlight(
|
|
134
|
+
program,
|
|
135
|
+
args,
|
|
136
|
+
operation,
|
|
137
|
+
markType,
|
|
138
|
+
{ offset = false } = {}
|
|
139
|
+
) {
|
|
121
140
|
validateKeys(args, INTERNAL_SELECTION_OPTIONS, operation);
|
|
122
141
|
const resolved = resolveStoredSelection(program, args.selection);
|
|
123
142
|
const keys = selectedKeys(args, resolved);
|
|
@@ -136,7 +155,7 @@ function applyRectangularHighlight(program, args, operation, markType) {
|
|
|
136
155
|
value: graphic.items.map(child => ({
|
|
137
156
|
type: child.type ?? graphic.type,
|
|
138
157
|
properties: selected.has(keyByGraphic.get(child.id))
|
|
139
|
-
?
|
|
158
|
+
? transformRectangularProperties(child.properties, args.style, offset)
|
|
140
159
|
: child.properties
|
|
141
160
|
}))
|
|
142
161
|
});
|
|
@@ -152,7 +171,13 @@ export const applyBarHighlight = action(
|
|
|
152
171
|
export const applyRectHighlight = action(
|
|
153
172
|
{ op: "applyRectHighlight", description: "Apply selected rect appearance." },
|
|
154
173
|
function (args = {}) {
|
|
155
|
-
return applyRectangularHighlight(
|
|
174
|
+
return applyRectangularHighlight(
|
|
175
|
+
this,
|
|
176
|
+
args,
|
|
177
|
+
"applyRectHighlight",
|
|
178
|
+
"rect",
|
|
179
|
+
{ offset: true }
|
|
180
|
+
);
|
|
156
181
|
}
|
|
157
182
|
);
|
|
158
183
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { action } from "../../core/action.js";
|
|
2
|
+
import { validateOptionObject } from "../../core/validation.js";
|
|
3
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
4
|
+
import {
|
|
5
|
+
applyFacadeGuides,
|
|
6
|
+
normalizeAppearance,
|
|
7
|
+
normalizeEncoding,
|
|
8
|
+
normalizeFieldEncoding,
|
|
9
|
+
normalizeGuides,
|
|
10
|
+
resolveFacadeData,
|
|
11
|
+
resolveFacadeId,
|
|
12
|
+
targetArgs,
|
|
13
|
+
validateFacadeOptions
|
|
14
|
+
} from "../charts/shared.js";
|
|
15
|
+
|
|
16
|
+
const OPTIONS = Object.freeze([
|
|
17
|
+
"id", "data", "coordinate", "x", "y", "split", "color", "density",
|
|
18
|
+
"area", "guides"
|
|
19
|
+
]);
|
|
20
|
+
const POSITION_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
|
|
21
|
+
const DENSITY_OPTIONS = Object.freeze([
|
|
22
|
+
"bandwidth", "extent", "steps", "kernel", "normalization", "width"
|
|
23
|
+
]);
|
|
24
|
+
const WIDTH_OPTIONS = Object.freeze(["band", "resolve"]);
|
|
25
|
+
const SPLIT_OPTIONS = Object.freeze(["field", "domain"]);
|
|
26
|
+
const AREA_OPTIONS = Object.freeze([
|
|
27
|
+
"fill", "opacity", "stroke", "strokeWidth", "curve"
|
|
28
|
+
]);
|
|
29
|
+
const CATEGORICAL_TYPES = Object.freeze(["nominal", "ordinal"]);
|
|
30
|
+
|
|
31
|
+
function inferFieldType(dataset, encoding, label) {
|
|
32
|
+
if (encoding.fieldType !== undefined) return encoding.fieldType;
|
|
33
|
+
if (typeof encoding.field !== "string" || encoding.field.length === 0) {
|
|
34
|
+
throw new TypeError(`${label} field must be a non-empty string.`);
|
|
35
|
+
}
|
|
36
|
+
const values = dataset.values
|
|
37
|
+
.map(row => row?.[encoding.field])
|
|
38
|
+
.filter(value => value !== undefined && value !== null && value !== "");
|
|
39
|
+
if (values.length === 0) {
|
|
40
|
+
throw new Error(`${label} field "${encoding.field}" has no values.`);
|
|
41
|
+
}
|
|
42
|
+
return values.every(Number.isFinite) ? "quantitative" : "nominal";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizePosition(dataset, value, label) {
|
|
46
|
+
const encoding = normalizeFieldEncoding(value, label);
|
|
47
|
+
validateOptionObject(encoding, POSITION_OPTIONS, label);
|
|
48
|
+
return {
|
|
49
|
+
...encoding,
|
|
50
|
+
fieldType: inferFieldType(dataset, encoding, label)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeDensity(value) {
|
|
55
|
+
if (value === undefined) return { options: {}, width: undefined };
|
|
56
|
+
validateOptionObject(value, DENSITY_OPTIONS, "createViolinPlot density");
|
|
57
|
+
if (value.width !== undefined) {
|
|
58
|
+
validateOptionObject(
|
|
59
|
+
value.width,
|
|
60
|
+
WIDTH_OPTIONS,
|
|
61
|
+
"createViolinPlot density.width"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const { width, ...options } = value;
|
|
65
|
+
return { options, width };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeSplit(value) {
|
|
69
|
+
if (value === undefined) return undefined;
|
|
70
|
+
validateOptionObject(value, SPLIT_OPTIONS, "createViolinPlot split");
|
|
71
|
+
if (typeof value.field !== "string" || value.field.length === 0) {
|
|
72
|
+
throw new TypeError("createViolinPlot split.field must be a non-empty string.");
|
|
73
|
+
}
|
|
74
|
+
return { ...value };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function axisGuideOptions(value, field) {
|
|
78
|
+
if (value === false) return false;
|
|
79
|
+
return {
|
|
80
|
+
...(value ?? {}),
|
|
81
|
+
title: {
|
|
82
|
+
text: field,
|
|
83
|
+
...(value?.title ?? {})
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function guideOptions(guides, x, y, category, color) {
|
|
89
|
+
if (guides === false) return false;
|
|
90
|
+
const axes = guides.axes === false
|
|
91
|
+
? false
|
|
92
|
+
: {
|
|
93
|
+
...(guides.axes ?? {}),
|
|
94
|
+
x: axisGuideOptions(guides.axes?.x, x.field),
|
|
95
|
+
y: axisGuideOptions(guides.axes?.y, y.field)
|
|
96
|
+
};
|
|
97
|
+
const redundantLegend = color?.field === category.field &&
|
|
98
|
+
!Object.hasOwn(guides, "legend");
|
|
99
|
+
return {
|
|
100
|
+
...guides,
|
|
101
|
+
axes,
|
|
102
|
+
...(redundantLegend ? { legend: false } : {})
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const createViolinPlot = action(
|
|
107
|
+
{
|
|
108
|
+
op: "createViolinPlot",
|
|
109
|
+
description: "Create a categorical kernel-density violin plot."
|
|
110
|
+
},
|
|
111
|
+
function (args = {}) {
|
|
112
|
+
validateFacadeOptions(args, OPTIONS, "createViolinPlot");
|
|
113
|
+
const id = resolveFacadeId(this, args.id, {
|
|
114
|
+
defaultId: "violinPlot",
|
|
115
|
+
operation: "createViolinPlot"
|
|
116
|
+
});
|
|
117
|
+
const data = resolveFacadeData(this, args.data, "createViolinPlot");
|
|
118
|
+
const dataset = findDataset(this, data);
|
|
119
|
+
const x = normalizePosition(dataset, args.x, "createViolinPlot x");
|
|
120
|
+
const y = normalizePosition(dataset, args.y, "createViolinPlot y");
|
|
121
|
+
const xCategorical = CATEGORICAL_TYPES.includes(x.fieldType);
|
|
122
|
+
const yCategorical = CATEGORICAL_TYPES.includes(y.fieldType);
|
|
123
|
+
if (xCategorical === yCategorical || (
|
|
124
|
+
x.fieldType !== "quantitative" && y.fieldType !== "quantitative"
|
|
125
|
+
)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
"createViolinPlot requires one categorical axis and one quantitative axis."
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const category = xCategorical ? x : y;
|
|
131
|
+
const value = xCategorical ? y : x;
|
|
132
|
+
if (value.fieldType !== "quantitative") {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"createViolinPlot requires one categorical axis and one quantitative axis."
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const split = normalizeSplit(args.split);
|
|
138
|
+
if (split?.field === category.field) {
|
|
139
|
+
throw new Error("createViolinPlot split field must differ from its category field.");
|
|
140
|
+
}
|
|
141
|
+
const color = normalizeEncoding(args.color, "createViolinPlot color");
|
|
142
|
+
if (
|
|
143
|
+
color !== undefined &&
|
|
144
|
+
![category.field, split?.field].includes(color.field)
|
|
145
|
+
) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"createViolinPlot color must encode its category or split field."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const density = normalizeDensity(args.density);
|
|
151
|
+
const area = normalizeAppearance(
|
|
152
|
+
args.area,
|
|
153
|
+
AREA_OPTIONS,
|
|
154
|
+
"createViolinPlot area"
|
|
155
|
+
);
|
|
156
|
+
if (color !== undefined && Object.hasOwn(area, "fill")) {
|
|
157
|
+
throw new Error("createViolinPlot area.fill cannot be combined with color.");
|
|
158
|
+
}
|
|
159
|
+
const guides = guideOptions(
|
|
160
|
+
normalizeGuides(args.guides, "createViolinPlot"),
|
|
161
|
+
x,
|
|
162
|
+
y,
|
|
163
|
+
category,
|
|
164
|
+
color
|
|
165
|
+
);
|
|
166
|
+
const { strokeWidth, ...areaCreate } = area;
|
|
167
|
+
const hasFillStroke = strokeWidth !== undefined && area.stroke === undefined;
|
|
168
|
+
let next = this.createAreaMark({
|
|
169
|
+
id,
|
|
170
|
+
data,
|
|
171
|
+
...areaCreate,
|
|
172
|
+
...(area.stroke === undefined || strokeWidth === undefined
|
|
173
|
+
? {}
|
|
174
|
+
: { strokeWidth })
|
|
175
|
+
});
|
|
176
|
+
if (hasFillStroke) {
|
|
177
|
+
next = next.configureAreaStrokeFromFill({ id, strokeWidth });
|
|
178
|
+
}
|
|
179
|
+
next = next.encodeDensity({
|
|
180
|
+
target: id,
|
|
181
|
+
source: data,
|
|
182
|
+
field: value.field,
|
|
183
|
+
groupBy: category.field,
|
|
184
|
+
densityChannel: xCategorical ? "x" : "y",
|
|
185
|
+
...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
|
|
186
|
+
...(value.scale === undefined ? {} : { valueScale: value.scale }),
|
|
187
|
+
...density.options,
|
|
188
|
+
placement: {
|
|
189
|
+
type: "category",
|
|
190
|
+
...(density.width === undefined ? {} : { width: density.width }),
|
|
191
|
+
...(split === undefined ? {} : { split }),
|
|
192
|
+
...(category.scale === undefined ? {} : { scale: category.scale })
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
|
|
196
|
+
return applyFacadeGuides(next, guides);
|
|
197
|
+
}
|
|
198
|
+
);
|