ggaction 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +31 -35
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +8 -1
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/coordinates/parallel.js +50 -0
  18. package/src/actions/data/bin2d.js +137 -0
  19. package/src/actions/data/density.js +54 -13
  20. package/src/actions/data/derived.js +22 -0
  21. package/src/actions/data/filter.js +5 -0
  22. package/src/actions/data/gradientProfile.js +65 -0
  23. package/src/actions/data/horizon.js +71 -0
  24. package/src/actions/data/index.js +29 -2
  25. package/src/actions/data/window.js +50 -0
  26. package/src/actions/encodings/color/index.js +17 -1
  27. package/src/actions/encodings/color/layout.js +8 -0
  28. package/src/actions/encodings/density/resolve.js +109 -0
  29. package/src/actions/encodings/density.js +193 -88
  30. package/src/actions/encodings/horizon/resolve.js +283 -0
  31. package/src/actions/encodings/horizon.js +364 -0
  32. package/src/actions/encodings/index.js +6 -0
  33. package/src/actions/encodings/parallel.js +115 -0
  34. package/src/actions/encodings/pathOrder.js +131 -0
  35. package/src/actions/encodings/position/apply.js +73 -4
  36. package/src/actions/encodings/position/index.js +15 -2
  37. package/src/actions/encodings/position/policies/arc.js +27 -4
  38. package/src/actions/encodings/position/policies/area.js +33 -2
  39. package/src/actions/encodings/position/policies/index.js +7 -2
  40. package/src/actions/encodings/position/policies/line.js +18 -2
  41. package/src/actions/encodings/position/resolve.js +24 -2
  42. package/src/actions/encodings/ranged.js +3 -1
  43. package/src/actions/encodings/ruleAppearance.js +88 -10
  44. package/src/actions/facets/actions.js +1 -2
  45. package/src/actions/facets/derive.js +33 -3
  46. package/src/actions/facets/replay.js +7 -36
  47. package/src/actions/gradientPlots/components.js +177 -0
  48. package/src/actions/gradientPlots/create.js +96 -0
  49. package/src/actions/gradientPlots/edit.js +153 -0
  50. package/src/actions/gradientPlots/index.js +24 -0
  51. package/src/actions/gradientPlots/materialize.js +287 -0
  52. package/src/actions/gradientPlots/options.js +120 -0
  53. package/src/actions/gradientPlots/paint.js +107 -0
  54. package/src/actions/gradientPlots/rebind.js +51 -0
  55. package/src/actions/gradientPlots/resolve.js +81 -0
  56. package/src/actions/guides/applicability.js +44 -17
  57. package/src/actions/guides/axes/axes.js +37 -3
  58. package/src/actions/guides/axes/index.js +2 -0
  59. package/src/actions/guides/axes/labels.js +11 -6
  60. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  61. package/src/actions/guides/axes/parallel.js +228 -0
  62. package/src/actions/guides/axes/titles.js +11 -8
  63. package/src/actions/guides/guides.js +60 -5
  64. package/src/actions/guides/legends/categorical/actions.js +48 -11
  65. package/src/actions/guides/legends/edit.js +5 -0
  66. package/src/actions/guides/legends/index.js +2 -0
  67. package/src/actions/guides/legends/remove.js +4 -11
  68. package/src/actions/guides/legends/strokeWidth.js +170 -0
  69. package/src/actions/index.js +6 -0
  70. package/src/actions/marks/area/actions.js +60 -136
  71. package/src/actions/marks/area/materialize.js +163 -0
  72. package/src/actions/marks/index.js +5 -1
  73. package/src/actions/marks/line/actions.js +99 -80
  74. package/src/actions/marks/line/materialize.js +146 -0
  75. package/src/actions/marks/point/create.js +76 -0
  76. package/src/actions/marks/point/edit.js +85 -0
  77. package/src/actions/marks/point/index.js +11 -1
  78. package/src/actions/marks/point/jitter.js +72 -0
  79. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  80. package/src/actions/marks/rect/actions.js +3 -0
  81. package/src/actions/marks/remove.js +13 -2
  82. package/src/actions/marks/rule/actions.js +25 -3
  83. package/src/actions/marks/text/actions.js +19 -4
  84. package/src/actions/marks/text/index.js +7 -1
  85. package/src/actions/marks/text/layout.js +321 -0
  86. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  87. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  88. package/src/actions/primitives/semanticValidation/index.js +23 -0
  89. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  90. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  91. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  92. package/src/actions/primitives/semanticValue.js +1 -204
  93. package/src/actions/regression/edit.js +19 -21
  94. package/src/actions/scales/consumers/common.js +18 -0
  95. package/src/actions/scales/consumers/families.js +14 -3
  96. package/src/actions/scales/definitions.js +30 -0
  97. package/src/actions/scales/edit.js +6 -204
  98. package/src/actions/scales/editPolicy.js +214 -0
  99. package/src/actions/scales/materialize.js +1 -1
  100. package/src/actions/scales/patch.js +26 -0
  101. package/src/actions/selection/actions.js +28 -3
  102. package/src/actions/violinPlots/create.js +198 -0
  103. package/src/actions/violinPlots/index.js +5 -0
  104. package/src/core/textMetrics.js +47 -0
  105. package/src/core/vocabulary.js +7 -2
  106. package/src/grammar/arcs.js +49 -12
  107. package/src/grammar/areaSeries.js +41 -5
  108. package/src/grammar/bin2d.js +333 -0
  109. package/src/grammar/categoricalDensity.js +192 -0
  110. package/src/grammar/coordinates.js +1 -1
  111. package/src/grammar/curveCommands.js +3 -1
  112. package/src/grammar/density.js +252 -26
  113. package/src/grammar/facets/dependencies.js +3 -11
  114. package/src/grammar/facets/index.js +3 -1
  115. package/src/grammar/gradientProfile.js +213 -0
  116. package/src/grammar/horizon.js +455 -0
  117. package/src/grammar/jitter.js +197 -0
  118. package/src/grammar/lineSeries.js +69 -0
  119. package/src/grammar/paint.js +88 -0
  120. package/src/grammar/parallelCoordinates.js +213 -0
  121. package/src/grammar/pathOrder.js +35 -0
  122. package/src/grammar/pointShapes.js +24 -0
  123. package/src/grammar/rules.js +1 -0
  124. package/src/grammar/scales/appearance.js +21 -0
  125. package/src/grammar/schemas/concreteGraphic.js +11 -1
  126. package/src/grammar/schemas/graphicBounds.js +19 -37
  127. package/src/grammar/schemas/semanticPath.js +17 -2
  128. package/src/grammar/transforms.js +100 -11
  129. package/src/grammar/window.js +339 -0
  130. package/src/layout/labels.js +270 -0
  131. package/src/layout/text.js +4 -1
  132. package/src/layout/title.js +10 -20
  133. package/src/materialization/dataProvenance.js +56 -2
  134. package/src/materialization/dependencies.js +9 -14
  135. package/src/materialization/guides/resources.js +74 -24
  136. package/src/materialization/horizon.js +29 -0
  137. package/src/materialization/layout.js +15 -0
  138. package/src/materialization/marks/capabilities.js +21 -1
  139. package/src/materialization/marks/index.js +3 -0
  140. package/src/materialization/marks/pathOrder.js +71 -0
  141. package/src/materialization/marks/policies.js +6 -2
  142. package/src/materialization/scaleGuideDependencies.js +8 -0
  143. package/src/materialization/scales/resolve.js +2 -0
  144. package/src/materialization/selection/items/path.js +27 -1
  145. package/src/materialization/selection/items/point.js +1 -5
  146. package/src/materialization/selection/items/rect.js +21 -0
  147. package/src/materialization/selection/styles.js +20 -9
  148. package/src/materialization/text.js +12 -4
  149. package/src/renderers/canvas/fill.js +33 -0
  150. package/src/renderers/canvas/index.js +7 -0
  151. package/src/renderers/canvas/path.js +7 -4
  152. package/src/renderers/canvas/rect.js +9 -3
  153. package/src/selectors/datasets.js +4 -0
  154. package/src/theme/defaults.js +4 -0
  155. package/types/extension.d.ts +6 -0
  156. package/types/index.d.ts +76 -0
  157. package/types/program.d.ts +632 -10
@@ -0,0 +1,287 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { isPlainObject } from "../../core/immutable.js";
4
+ import { GRADIENT_PROFILE_FIELDS } from "../../grammar/gradientProfile.js";
5
+ import {
6
+ mapContinuousScaleValues,
7
+ mapOrdinalPositionValues
8
+ } from "../../grammar/scales/index.js";
9
+ import { applyMaterializationPlan } from "../../materialization/dependencies.js";
10
+ import { getSourceDependentMarkSteps } from "../../materialization/marks/index.js";
11
+ import { buildMaterializationPlan } from "../../materialization/planner.js";
12
+ import { findDataset } from "../../selectors/datasets.js";
13
+ import { findLayer } from "../../selectors/layers.js";
14
+ import {
15
+ createGradientPlotPaint,
16
+ resolveGradientPlotCategoryColors
17
+ } from "./paint.js";
18
+ import { resolveGradientOrientation } from "./resolve.js";
19
+
20
+ function position(values, scale) {
21
+ return ["ordinal", "band", "point"].includes(scale.type)
22
+ ? mapOrdinalPositionValues(values, scale)
23
+ : mapContinuousScaleValues(values, scale);
24
+ }
25
+
26
+ function profileValue(row, key) {
27
+ return row[GRADIENT_PROFILE_FIELDS[key]];
28
+ }
29
+
30
+ function axisWithTitle(option, text) {
31
+ if (option === false) return false;
32
+ const axis = option ?? {};
33
+ return {
34
+ ...axis,
35
+ title: {
36
+ ...(axis.title ?? {}),
37
+ ...(axis.title?.text === undefined ? { text } : {})
38
+ }
39
+ };
40
+ }
41
+
42
+ function gradientAxesOptions(config) {
43
+ if (config.guides.axes === false) return false;
44
+ const axes = config.guides.axes ?? {};
45
+ return {
46
+ ...axes,
47
+ x: axisWithTitle(
48
+ axes.x,
49
+ config.orientation === "vertical" ? config.category : config.measure
50
+ ),
51
+ y: axisWithTitle(
52
+ axes.y,
53
+ config.orientation === "vertical" ? config.measure : config.category
54
+ )
55
+ };
56
+ }
57
+
58
+ function bodyItems(program, layer, dataset, config) {
59
+ const categoryChannel = config.orientation === "vertical" ? "x" : "y";
60
+ const measureChannel = config.orientation === "vertical" ? "y" : "x";
61
+ const categoryEncoding = layer.encoding[categoryChannel];
62
+ const measureEncoding = layer.encoding[measureChannel];
63
+ const categoryScale = program.resolvedScales[categoryEncoding.scale];
64
+ const measureScale = program.resolvedScales[measureEncoding.scale];
65
+ const categories = dataset.values.map(row => row[config.category]);
66
+ const centers = position(categories, categoryScale);
67
+ const lower = position(
68
+ dataset.values.map(row => profileValue(row, "lower")),
69
+ measureScale
70
+ );
71
+ const upper = position(
72
+ dataset.values.map(row => profileValue(row, "upper")),
73
+ measureScale
74
+ );
75
+ const categoryColors = resolveGradientPlotCategoryColors(
76
+ program,
77
+ layer,
78
+ categories
79
+ );
80
+ const thickness = categoryScale.bandwidth * config.width.band;
81
+ if (!Number.isFinite(thickness) || thickness <= 0) {
82
+ throw new Error(`Gradient plot "${layer.id}" requires a positive band width.`);
83
+ }
84
+ return dataset.values.map((row, index) => {
85
+ const a = lower[index];
86
+ const b = upper[index];
87
+ const geometry = config.orientation === "vertical"
88
+ ? {
89
+ x: centers[index] - thickness / 2,
90
+ y: Math.min(a, b),
91
+ width: thickness,
92
+ height: Math.abs(a - b)
93
+ }
94
+ : {
95
+ x: Math.min(a, b),
96
+ y: centers[index] - thickness / 2,
97
+ width: Math.abs(a - b),
98
+ height: thickness
99
+ };
100
+ return {
101
+ type: "rect",
102
+ properties: {
103
+ ...geometry,
104
+ fill: createGradientPlotPaint({
105
+ values: profileValue(row, "values"),
106
+ intensities: profileValue(row, "intensities")
107
+ }, {
108
+ orientation: config.orientation,
109
+ valueScale: measureScale,
110
+ intensityDomain: config.intensityDomain,
111
+ palette: config.gradient.palette,
112
+ opacity: config.gradient.opacity,
113
+ ...(categoryColors === undefined
114
+ ? {}
115
+ : { baseColor: categoryColors[index] })
116
+ }),
117
+ opacity: 1,
118
+ stroke: "transparent",
119
+ strokeWidth: 0
120
+ }
121
+ };
122
+ });
123
+ }
124
+
125
+ export const materializeGradientPlotFill = action(
126
+ {
127
+ op: "materializeGradientPlotFill",
128
+ description: "Materialize categorical density profiles as gradient strips."
129
+ },
130
+ function ({ id } = {}) {
131
+ const owner = validateUserId(id, "Gradient-plot id");
132
+ const layer = findLayer(this, owner);
133
+ const config = this.markConfigs[owner]?.gradientPlot;
134
+ if (layer?.mark?.type !== "rect" || config?.materialized !== true) {
135
+ throw new Error(`Unknown materialized gradient plot "${owner}".`);
136
+ }
137
+ const dataset = findDataset(this, layer.data);
138
+ if (dataset === undefined) {
139
+ throw new Error(`Gradient plot "${owner}" requires profile data.`);
140
+ }
141
+ const scaleIds = [...new Set([
142
+ layer.encoding?.x?.scale,
143
+ layer.encoding?.y?.scale,
144
+ layer.encoding?.color?.scale
145
+ ].filter(Boolean))];
146
+ let next = this;
147
+ for (const scale of scaleIds) {
148
+ next = next.rematerializeScale({ id: scale, marks: false, guides: false });
149
+ }
150
+ next = next.editGraphics({
151
+ target: owner,
152
+ property: "items",
153
+ value: bodyItems(next, findLayer(next, owner), dataset, config)
154
+ });
155
+ next = applyMaterializationPlan(
156
+ next,
157
+ buildMaterializationPlan({
158
+ marks: getSourceDependentMarkSteps(next, owner)
159
+ })
160
+ );
161
+ if (
162
+ config.guides?.legend !== false &&
163
+ config.guides?.legend !== undefined &&
164
+ next.graphicSpec.objects[`${owner}DensityLegend`] !== undefined
165
+ ) {
166
+ next = next.rematerializeGradientPlotLegend({ owner });
167
+ }
168
+ return next._withContext({ currentMark: owner, currentData: config.source });
169
+ }
170
+ );
171
+
172
+ export const materializeGradientPlot = action(
173
+ {
174
+ op: "materializeGradientPlot",
175
+ description: "Materialize one complete categorical gradient plot."
176
+ },
177
+ function ({ id } = {}) {
178
+ const owner = validateUserId(id, "Gradient-plot id");
179
+ const layer = findLayer(this, owner);
180
+ const config = this.markConfigs[owner]?.gradientPlot;
181
+ if (layer?.mark?.type !== "rect" || config === undefined) {
182
+ throw new Error(`Unknown gradient plot "${owner}".`);
183
+ }
184
+ if (config.materialized) return this.materializeGradientPlotFill({ id: owner });
185
+ const x = layer.encoding?.x;
186
+ const y = layer.encoding?.y;
187
+ if (x?.field === undefined || y?.field === undefined) return this;
188
+ const orientation = resolveGradientOrientation(x, y);
189
+ if (orientation === undefined) {
190
+ throw new Error(
191
+ "createGradientPlot requires one categorical axis and one quantitative axis."
192
+ );
193
+ }
194
+ const category = orientation === "vertical" ? x : y;
195
+ const measure = orientation === "vertical" ? y : x;
196
+ const profileId = `${owner}ProfileData`;
197
+ const centerId = `${owner}Center`;
198
+ let next = this.createGradientProfileData({
199
+ id: profileId,
200
+ source: layer.data,
201
+ category: category.field,
202
+ field: measure.field,
203
+ ...config.density,
204
+ center: config.center === false ? "median" : config.center.type
205
+ });
206
+ const profile = findDataset(next, profileId);
207
+ const transform = profile.transform[0];
208
+ next = next
209
+ .rebindLayerData({ id: owner, data: profileId })
210
+ ._withMarkConfig(owner, {
211
+ ...next.markConfigs[owner],
212
+ gradientPlot: {
213
+ ...config,
214
+ materialized: true,
215
+ source: layer.data,
216
+ orientation,
217
+ category: category.field,
218
+ categoryType: category.fieldType,
219
+ measure: measure.field,
220
+ profileId,
221
+ centerId,
222
+ intensityDomain: transform.resolved.intensityDomain
223
+ }
224
+ });
225
+ const measureChannel = orientation === "vertical" ? "y" : "x";
226
+ const secondaryChannel = `${measureChannel}2`;
227
+ next = next
228
+ .editSemantic({
229
+ property: `layer[${owner}].encoding.${measureChannel}.field`,
230
+ value: GRADIENT_PROFILE_FIELDS.lower
231
+ })
232
+ .editSemantic({
233
+ property: `layer[${owner}].encoding.${measureChannel}.fieldType`,
234
+ value: "quantitative"
235
+ })
236
+ .editSemantic({
237
+ property: `layer[${owner}].encoding.${secondaryChannel}.field`,
238
+ value: GRADIENT_PROFILE_FIELDS.upper
239
+ })
240
+ .editSemantic({
241
+ property: `layer[${owner}].encoding.${secondaryChannel}.fieldType`,
242
+ value: "quantitative"
243
+ })
244
+ .editSemantic({
245
+ property: `layer[${owner}].encoding.${secondaryChannel}.scale`,
246
+ value: measure.scale
247
+ });
248
+ next = next.materializeGradientPlotFill({ id: owner });
249
+ if (config.center !== false) {
250
+ const categoryScale = next.resolvedScales[category.scale];
251
+ next = next.createGradientPlotCenter({
252
+ id: centerId,
253
+ owner,
254
+ data: profileId,
255
+ category: category.field,
256
+ categoryType: category.fieldType,
257
+ coordinate: layer.coordinate,
258
+ categoryScale: category.scale,
259
+ measureScale: measure.scale,
260
+ orientation,
261
+ size: Math.max(1, categoryScale.bandwidth * config.width.band - 16),
262
+ stroke: config.center.stroke,
263
+ strokeWidth: config.center.strokeWidth
264
+ });
265
+ }
266
+ const guides = config.guides;
267
+ if (guides !== false) {
268
+ const wantsStandard = guides.axes !== false || guides.grid !== false;
269
+ if (wantsStandard) {
270
+ next = next.createGuides({
271
+ axes: gradientAxesOptions(next.markConfigs[owner].gradientPlot),
272
+ grid: guides.grid,
273
+ legend: false
274
+ });
275
+ }
276
+ if (guides.legend !== false) {
277
+ const legend = isPlainObject(guides.legend) ? guides.legend : {};
278
+ next = next.createGradientPlotLegend({
279
+ owner,
280
+ title: legend.title ?? "Relative density",
281
+ position: legend.position ?? "right"
282
+ });
283
+ }
284
+ }
285
+ return next._withContext({ currentMark: owner, currentData: layer.data });
286
+ }
287
+ );
@@ -0,0 +1,120 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+ import {
3
+ validateKeys,
4
+ validateNonEmptyString,
5
+ validateNonNegativeFinite,
6
+ validateUnitInterval
7
+ } from "../../core/validation.js";
8
+ import { normalizePalette } from "../../grammar/palettes.js";
9
+
10
+ export const GRADIENT_PLOT_OPTIONS = Object.freeze([
11
+ "id", "target", "data", "x", "y", "coordinate", "density", "width",
12
+ "gradient", "center", "guides"
13
+ ]);
14
+
15
+ function plain(value, keys, label, operation) {
16
+ if (value === undefined) return {};
17
+ if (!isPlainObject(value)) {
18
+ throw new TypeError(`${operation} ${label} must be a plain object.`);
19
+ }
20
+ validateKeys(value, keys, `${operation} ${label}`);
21
+ return value;
22
+ }
23
+
24
+ export function resolveGradientPosition(value, label, operation = "createGradientPlot") {
25
+ if (value === undefined) return undefined;
26
+ if (typeof value === "string") return { field: value };
27
+ return plain(value, ["field", "fieldType", "scale"], label, operation);
28
+ }
29
+
30
+ export function gradientEncodingArgs(value) {
31
+ return {
32
+ ...value,
33
+ ...(typeof value.scale === "string" ? { scale: { id: value.scale } } : {})
34
+ };
35
+ }
36
+
37
+ export function resolveGradientDensity(value, previous, operation = "createGradientPlot") {
38
+ const options = plain(value, [
39
+ "bandwidth", "extent", "steps", "kernel", "normalization"
40
+ ], "density", operation);
41
+ return Object.freeze({
42
+ bandwidth: options.bandwidth ?? previous?.bandwidth ?? "auto",
43
+ extent: options.extent ?? previous?.extent ?? "auto",
44
+ steps: options.steps ?? previous?.steps ?? 64,
45
+ kernel: options.kernel ?? previous?.kernel ?? "gaussian",
46
+ normalization: options.normalization ?? previous?.normalization ?? "unit"
47
+ });
48
+ }
49
+
50
+ export function resolveGradientWidth(value, previous, operation = "createGradientPlot") {
51
+ const options = plain(value, ["band"], "width", operation);
52
+ const band = options.band ?? previous?.band ?? 0.7;
53
+ if (!Number.isFinite(band) || band <= 0 || band >= 1) {
54
+ throw new RangeError(
55
+ `${operation} width.band must be greater than 0 and less than 1.`
56
+ );
57
+ }
58
+ return Object.freeze({ band });
59
+ }
60
+
61
+ function opacityPair(value, fallback, operation) {
62
+ const opacity = value ?? fallback ?? [0, 1];
63
+ if (!Array.isArray(opacity) || opacity.length !== 2) {
64
+ throw new TypeError(`${operation} gradient.opacity must contain two values.`);
65
+ }
66
+ const normalized = opacity.map((item, index) =>
67
+ validateUnitInterval(item, `${operation} gradient.opacity[${index}]`)
68
+ );
69
+ return Object.freeze(normalized);
70
+ }
71
+
72
+ export function resolveGradientAppearance(
73
+ value,
74
+ previous,
75
+ operation = "createGradientPlot"
76
+ ) {
77
+ const options = plain(value, ["palette", "opacity"], "gradient", operation);
78
+ const palette = options.palette === undefined
79
+ ? previous?.palette ?? "blues"
80
+ : normalizePalette(options.palette);
81
+ return Object.freeze({
82
+ palette,
83
+ opacity: opacityPair(options.opacity, previous?.opacity, operation)
84
+ });
85
+ }
86
+
87
+ export function resolveGradientCenter(
88
+ value,
89
+ previous,
90
+ operation = "createGradientPlot"
91
+ ) {
92
+ if (value === false) return false;
93
+ const options = plain(
94
+ value,
95
+ ["type", "stroke", "strokeWidth"],
96
+ "center",
97
+ operation
98
+ );
99
+ const type = options.type ?? previous?.type ?? "median";
100
+ if (!["mean", "median"].includes(type)) {
101
+ throw new Error(`${operation} center.type must be mean or median.`);
102
+ }
103
+ return Object.freeze({
104
+ type,
105
+ stroke: options.stroke === undefined
106
+ ? previous?.stroke ?? "#0f172a"
107
+ : validateNonEmptyString(options.stroke, `${operation} center.stroke`),
108
+ strokeWidth: options.strokeWidth === undefined
109
+ ? previous?.strokeWidth ?? 1.5
110
+ : validateNonNegativeFinite(
111
+ options.strokeWidth,
112
+ `${operation} center.strokeWidth`
113
+ )
114
+ });
115
+ }
116
+
117
+ export function resolveGradientGuides(value, operation = "createGradientPlot") {
118
+ if (value === false) return false;
119
+ return Object.freeze(plain(value, ["axes", "grid", "legend"], "guides", operation));
120
+ }
@@ -0,0 +1,107 @@
1
+ import { resolveContinuousPalette } from "../../grammar/palettes.js";
2
+ import { mapScaleConsumerValues } from "../../materialization/scales/map.js";
3
+
4
+ function parseHex(value) {
5
+ const match = value.match(/^#([0-9a-f]{6})$/i);
6
+ if (!match) throw new Error(`Gradient plot requires a six-digit hex color, received "${value}".`);
7
+ return [0, 2, 4].map(offset =>
8
+ Number.parseInt(match[1].slice(offset, offset + 2), 16)
9
+ );
10
+ }
11
+
12
+ function interpolate(left, right, amount) {
13
+ return left.map((channel, index) =>
14
+ Math.round(channel + (right[index] - channel) * amount)
15
+ );
16
+ }
17
+
18
+ function alphaColor(rgb, opacity) {
19
+ return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${Number(opacity.toFixed(6))})`;
20
+ }
21
+
22
+ function paletteColor(colors, amount) {
23
+ const bounded = Math.max(0, Math.min(1, amount));
24
+ const scaled = bounded * (colors.length - 1);
25
+ const left = Math.floor(scaled);
26
+ const right = Math.min(colors.length - 1, left + 1);
27
+ return interpolate(parseHex(colors[left]), parseHex(colors[right]), scaled - left);
28
+ }
29
+
30
+ function categoricalRamp(base) {
31
+ const rgb = parseHex(base);
32
+ const pale = interpolate([255, 255, 255], rgb, 0.2);
33
+ return [
34
+ `#${pale.map(value => value.toString(16).padStart(2, "0")).join("")}`,
35
+ base
36
+ ];
37
+ }
38
+
39
+ export function resolveGradientPlotCategoryColors(program, layer, categories) {
40
+ if (layer.encoding?.color === undefined) return undefined;
41
+ const scale = program.resolvedScales[layer.encoding.color.scale];
42
+ if (scale === undefined) {
43
+ throw new Error(`Gradient plot "${layer.id}" requires a resolved color scale.`);
44
+ }
45
+ return mapScaleConsumerValues(categories, scale, "color");
46
+ }
47
+
48
+ export function createGradientPlotPaint(profile, {
49
+ orientation,
50
+ valueScale,
51
+ intensityDomain,
52
+ palette,
53
+ opacity,
54
+ baseColor
55
+ }) {
56
+ const values = profile.values;
57
+ const intensities = profile.intensities;
58
+ const mapped = mapScaleConsumerValues(values, valueScale, "position");
59
+ const startPosition = mapped[0];
60
+ const endPosition = mapped.at(-1);
61
+ if (!Number.isFinite(startPosition) || !Number.isFinite(endPosition) || startPosition === endPosition) {
62
+ throw new Error("Gradient plot requires distinct mapped profile endpoints.");
63
+ }
64
+ const increasing = startPosition < endPosition;
65
+ const start = orientation === "vertical"
66
+ ? { x: 0.5, y: increasing ? 0 : 1 }
67
+ : { x: increasing ? 0 : 1, y: 0.5 };
68
+ const end = orientation === "vertical"
69
+ ? { x: 0.5, y: increasing ? 1 : 0 }
70
+ : { x: increasing ? 1 : 0, y: 0.5 };
71
+ const colors = baseColor === undefined
72
+ ? resolveContinuousPalette(palette, 11)
73
+ : categoricalRamp(baseColor);
74
+ const maximum = intensityDomain[1];
75
+ const stops = values.map((_, index) => {
76
+ const fraction = intensities[index] / maximum;
77
+ const offset = Number(
78
+ ((mapped[index] - startPosition) / (endPosition - startPosition)).toFixed(12)
79
+ );
80
+ return {
81
+ offset: Object.is(offset, -0) ? 0 : offset,
82
+ color: alphaColor(
83
+ paletteColor(colors, fraction),
84
+ opacity[0] + (opacity[1] - opacity[0]) * fraction
85
+ )
86
+ };
87
+ });
88
+ return {
89
+ type: "linear-gradient",
90
+ from: start,
91
+ to: end,
92
+ stops
93
+ };
94
+ }
95
+
96
+ export function createDensityLegendPaint(opacity) {
97
+ const colors = ["#e2e8f0", "#334155"];
98
+ return {
99
+ type: "linear-gradient",
100
+ from: { x: 0.5, y: 1 },
101
+ to: { x: 0.5, y: 0 },
102
+ stops: colors.map((color, index) => ({
103
+ offset: index,
104
+ color: alphaColor(parseHex(color), opacity[index])
105
+ }))
106
+ };
107
+ }
@@ -0,0 +1,51 @@
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 { findLayer } from "../../selectors/layers.js";
6
+
7
+ const OPTIONS = Object.freeze(["id", "profile", "source"]);
8
+
9
+ export const rebindGradientPlotProfile = action(
10
+ {
11
+ op: "rebindGradientPlotProfile",
12
+ description: "Rebind one gradient plot to a replayed profile revision."
13
+ },
14
+ function (args = {}) {
15
+ validateKeys(args, OPTIONS, "rebindGradientPlotProfile");
16
+ const id = validateUserId(args.id, "Gradient-plot id");
17
+ const profileId = validateUserId(
18
+ args.profile,
19
+ "Gradient profile dataset id"
20
+ );
21
+ const source = validateUserId(args.source, "Gradient profile source id");
22
+ const layer = findLayer(this, id);
23
+ const config = this.markConfigs[id]?.gradientPlot;
24
+ const profile = findDataset(this, profileId);
25
+ const transform = profile?.transform?.[0];
26
+ if (layer?.mark?.type !== "rect" || config?.materialized !== true) {
27
+ throw new Error(`Unknown materialized gradient plot "${id}".`);
28
+ }
29
+ if (layer.data !== profileId || transform?.type !== "gradientProfile") {
30
+ throw new Error(
31
+ `Gradient plot "${id}" must use gradient profile dataset "${profileId}".`
32
+ );
33
+ }
34
+ if (profile.source !== source || findDataset(this, source) === undefined) {
35
+ throw new Error(
36
+ `Gradient profile dataset "${profileId}" must use source "${source}".`
37
+ );
38
+ }
39
+ return this
40
+ ._withMarkConfig(id, {
41
+ ...this.markConfigs[id],
42
+ gradientPlot: {
43
+ ...config,
44
+ source,
45
+ profileId,
46
+ intensityDomain: transform.resolved.intensityDomain
47
+ }
48
+ })
49
+ ._withContext({ currentMark: id, currentData: source });
50
+ }
51
+ );
@@ -0,0 +1,81 @@
1
+ import { resolveOptionalUserId, validateUserId } from "../../core/identifiers.js";
2
+ import { findLayer, hasLayer } from "../../selectors/layers.js";
3
+
4
+ const CATEGORICAL = Object.freeze(["nominal", "ordinal", "temporal"]);
5
+
6
+ export function resolveGradientOrientation(x, y) {
7
+ const xType = x?.fieldType;
8
+ const yType = y?.fieldType;
9
+ if (CATEGORICAL.includes(xType) && (yType === undefined || yType === "quantitative")) {
10
+ return "vertical";
11
+ }
12
+ if ((xType === undefined || xType === "quantitative") && CATEGORICAL.includes(yType)) {
13
+ return "horizontal";
14
+ }
15
+ return undefined;
16
+ }
17
+
18
+ export function normalizeGradientPositionTypes(x, y) {
19
+ const orientation = resolveGradientOrientation(x, y);
20
+ if (orientation === "vertical") {
21
+ return { x, y: { fieldType: "quantitative", ...y }, orientation };
22
+ }
23
+ if (orientation === "horizontal") {
24
+ return { x: { fieldType: "quantitative", ...x }, y, orientation };
25
+ }
26
+ return { x, y, orientation };
27
+ }
28
+
29
+ export function resolveGradientSourceLayer(program, target) {
30
+ if (target !== undefined) {
31
+ const id = validateUserId(target, "Gradient plot source layer id");
32
+ const layer = findLayer(program, id);
33
+ if (layer === undefined) {
34
+ throw new Error(`Unknown gradient plot source layer "${id}".`);
35
+ }
36
+ return layer;
37
+ }
38
+ const current = findLayer(program, program.context.currentMark);
39
+ if (current?.encoding?.x !== undefined || current?.encoding?.y !== undefined) {
40
+ return current;
41
+ }
42
+ const eligible = program.semanticSpec.layers.filter(layer =>
43
+ layer.encoding?.x !== undefined && layer.encoding?.y !== undefined
44
+ );
45
+ if (eligible.length === 1) return eligible[0];
46
+ if (eligible.length > 1) {
47
+ throw new Error(
48
+ "createGradientPlot target is ambiguous; provide target or explicit x and y."
49
+ );
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ export function resolveGradientPlotId(program, requested) {
55
+ return resolveOptionalUserId(requested, {
56
+ defaultId: "gradientPlot",
57
+ label: "Gradient-plot id",
58
+ operation: "createGradientPlot",
59
+ ambiguous: hasLayer(program, "gradientPlot") ||
60
+ program.graphicSpec.objects.gradientPlot !== undefined
61
+ });
62
+ }
63
+
64
+ export function resolveGradientOwner(program, requested, operation) {
65
+ const eligible = program.semanticSpec.layers.filter(
66
+ layer => program.markConfigs[layer.id]?.gradientPlot?.materialized === true
67
+ );
68
+ if (requested !== undefined) {
69
+ const id = validateUserId(requested, "Gradient-plot owner id");
70
+ const layer = findLayer(program, id);
71
+ if (layer === undefined || !eligible.includes(layer)) {
72
+ throw new Error(`Unknown gradient-plot owner "${id}".`);
73
+ }
74
+ return layer;
75
+ }
76
+ const current = findLayer(program, program.context.currentMark);
77
+ if (current !== undefined && eligible.includes(current)) return current;
78
+ if (eligible.length === 1) return eligible[0];
79
+ if (eligible.length === 0) throw new Error(`${operation} requires a gradient plot.`);
80
+ throw new Error(`${operation} target is ambiguous; provide target.`);
81
+ }