ggaction 0.0.4 → 0.0.6

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 (169) hide show
  1. package/CHANGELOG.md +55 -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 +484 -36
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +11 -4
  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/composition/actions.js +13 -3
  18. package/src/actions/coordinates/parallel.js +50 -0
  19. package/src/actions/data/bin2d.js +260 -0
  20. package/src/actions/data/density.js +54 -13
  21. package/src/actions/data/derived.js +22 -0
  22. package/src/actions/data/filter.js +5 -0
  23. package/src/actions/data/gradientProfile.js +65 -0
  24. package/src/actions/data/horizon.js +71 -0
  25. package/src/actions/data/index.js +34 -2
  26. package/src/actions/data/intervalEdit.js +76 -0
  27. package/src/actions/data/window.js +50 -0
  28. package/src/actions/distributions/revision.js +326 -0
  29. package/src/actions/encodings/appearance.js +35 -1
  30. package/src/actions/encodings/color/index.js +17 -1
  31. package/src/actions/encodings/color/layout.js +8 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +330 -90
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -0
  37. package/src/actions/encodings/parallel.js +115 -0
  38. package/src/actions/encodings/pathOrder.js +131 -0
  39. package/src/actions/encodings/position/apply.js +73 -4
  40. package/src/actions/encodings/position/index.js +15 -2
  41. package/src/actions/encodings/position/policies/arc.js +27 -4
  42. package/src/actions/encodings/position/policies/area.js +33 -2
  43. package/src/actions/encodings/position/policies/index.js +7 -2
  44. package/src/actions/encodings/position/policies/line.js +18 -2
  45. package/src/actions/encodings/position/resolve.js +24 -2
  46. package/src/actions/encodings/ranged.js +3 -1
  47. package/src/actions/encodings/remove.js +268 -0
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/errorBands/edit.js +121 -13
  50. package/src/actions/errorBars/edit.js +45 -8
  51. package/src/actions/errorBars/options.js +7 -1
  52. package/src/actions/facets/actions.js +137 -3
  53. package/src/actions/facets/derive.js +33 -3
  54. package/src/actions/facets/replay.js +7 -36
  55. package/src/actions/gradientPlots/components.js +177 -0
  56. package/src/actions/gradientPlots/create.js +96 -0
  57. package/src/actions/gradientPlots/edit.js +501 -0
  58. package/src/actions/gradientPlots/index.js +24 -0
  59. package/src/actions/gradientPlots/materialize.js +287 -0
  60. package/src/actions/gradientPlots/options.js +120 -0
  61. package/src/actions/gradientPlots/paint.js +107 -0
  62. package/src/actions/gradientPlots/rebind.js +51 -0
  63. package/src/actions/gradientPlots/resolve.js +81 -0
  64. package/src/actions/guides/applicability.js +44 -17
  65. package/src/actions/guides/axes/axes.js +37 -3
  66. package/src/actions/guides/axes/edit.js +129 -47
  67. package/src/actions/guides/axes/index.js +2 -0
  68. package/src/actions/guides/axes/labels.js +11 -6
  69. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  70. package/src/actions/guides/axes/parallel.js +228 -0
  71. package/src/actions/guides/axes/titles.js +11 -8
  72. package/src/actions/guides/guides.js +60 -5
  73. package/src/actions/guides/legends/categorical/actions.js +67 -19
  74. package/src/actions/guides/legends/continuous/common.js +3 -3
  75. package/src/actions/guides/legends/edit.js +63 -0
  76. package/src/actions/guides/legends/index.js +2 -0
  77. package/src/actions/guides/legends/remove.js +124 -30
  78. package/src/actions/guides/legends/strokeWidth.js +198 -0
  79. package/src/actions/index.js +6 -0
  80. package/src/actions/marks/arc/actions.js +46 -14
  81. package/src/actions/marks/area/actions.js +60 -136
  82. package/src/actions/marks/area/materialize.js +163 -0
  83. package/src/actions/marks/index.js +5 -1
  84. package/src/actions/marks/line/actions.js +99 -80
  85. package/src/actions/marks/line/materialize.js +146 -0
  86. package/src/actions/marks/point/create.js +76 -0
  87. package/src/actions/marks/point/edit.js +97 -0
  88. package/src/actions/marks/point/index.js +11 -1
  89. package/src/actions/marks/point/jitter.js +72 -0
  90. package/src/actions/marks/point/{actions.js → materialize.js} +183 -172
  91. package/src/actions/marks/rect/actions.js +3 -0
  92. package/src/actions/marks/remove.js +13 -2
  93. package/src/actions/marks/rule/actions.js +29 -4
  94. package/src/actions/marks/text/actions.js +19 -4
  95. package/src/actions/marks/text/index.js +7 -1
  96. package/src/actions/marks/text/layout.js +321 -0
  97. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  98. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  99. package/src/actions/primitives/semanticValidation/index.js +23 -0
  100. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  101. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  102. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  103. package/src/actions/primitives/semanticValue.js +1 -204
  104. package/src/actions/regression/edit.js +247 -88
  105. package/src/actions/scales/consumers/common.js +18 -0
  106. package/src/actions/scales/consumers/families.js +14 -3
  107. package/src/actions/scales/definitions.js +30 -0
  108. package/src/actions/scales/edit.js +6 -204
  109. package/src/actions/scales/editPolicy.js +214 -0
  110. package/src/actions/scales/materialize.js +1 -1
  111. package/src/actions/scales/patch.js +26 -0
  112. package/src/actions/selection/actions.js +150 -5
  113. package/src/actions/selection/index.js +6 -0
  114. package/src/actions/violinPlots/create.js +198 -0
  115. package/src/actions/violinPlots/index.js +5 -0
  116. package/src/core/textMetrics.js +47 -0
  117. package/src/core/vocabulary.js +14 -2
  118. package/src/grammar/arcs.js +49 -12
  119. package/src/grammar/areaSeries.js +41 -5
  120. package/src/grammar/bin2d.js +333 -0
  121. package/src/grammar/categoricalDensity.js +192 -0
  122. package/src/grammar/coordinates.js +1 -1
  123. package/src/grammar/curveCommands.js +3 -1
  124. package/src/grammar/density.js +252 -26
  125. package/src/grammar/facets/dependencies.js +3 -11
  126. package/src/grammar/facets/index.js +3 -1
  127. package/src/grammar/gradientProfile.js +213 -0
  128. package/src/grammar/horizon.js +455 -0
  129. package/src/grammar/jitter.js +197 -0
  130. package/src/grammar/lineSeries.js +69 -0
  131. package/src/grammar/paint.js +88 -0
  132. package/src/grammar/parallelCoordinates.js +213 -0
  133. package/src/grammar/pathOrder.js +35 -0
  134. package/src/grammar/pointShapes.js +24 -0
  135. package/src/grammar/rules.js +1 -0
  136. package/src/grammar/scales/appearance.js +21 -0
  137. package/src/grammar/schemas/concreteGraphic.js +11 -1
  138. package/src/grammar/schemas/graphicBounds.js +19 -37
  139. package/src/grammar/schemas/semanticPath.js +17 -2
  140. package/src/grammar/transforms.js +100 -11
  141. package/src/grammar/window.js +339 -0
  142. package/src/layout/labels.js +270 -0
  143. package/src/layout/text.js +4 -1
  144. package/src/layout/title.js +10 -20
  145. package/src/materialization/dataProvenance.js +56 -2
  146. package/src/materialization/dependencies.js +9 -14
  147. package/src/materialization/guides/resources.js +74 -24
  148. package/src/materialization/horizon.js +29 -0
  149. package/src/materialization/layout.js +15 -0
  150. package/src/materialization/marks/capabilities.js +21 -1
  151. package/src/materialization/marks/index.js +3 -0
  152. package/src/materialization/marks/pathOrder.js +71 -0
  153. package/src/materialization/marks/policies.js +6 -2
  154. package/src/materialization/scaleGuideDependencies.js +8 -0
  155. package/src/materialization/scales/resolve.js +2 -0
  156. package/src/materialization/selection/items/path.js +27 -1
  157. package/src/materialization/selection/items/point.js +1 -5
  158. package/src/materialization/selection/items/rect.js +21 -0
  159. package/src/materialization/selection/styles.js +20 -9
  160. package/src/materialization/text.js +12 -4
  161. package/src/renderers/canvas/fill.js +33 -0
  162. package/src/renderers/canvas/index.js +7 -0
  163. package/src/renderers/canvas/path.js +7 -4
  164. package/src/renderers/canvas/rect.js +9 -3
  165. package/src/selectors/datasets.js +4 -0
  166. package/src/theme/defaults.js +4 -0
  167. package/types/extension.d.ts +6 -0
  168. package/types/index.d.ts +77 -0
  169. package/types/program.d.ts +703 -17
@@ -1,5 +1,5 @@
1
1
  import { action } from "../../core/action.js";
2
- import { isPlainObject } from "../../core/immutable.js";
2
+ import { freezeOwned, isPlainObject } from "../../core/immutable.js";
3
3
  import {
4
4
  validateNonEmptyString,
5
5
  validateNonNegativeFinite,
@@ -9,12 +9,13 @@ import {
9
9
  import { resolveFacetDefinition } from "../../grammar/facets/index.js";
10
10
  import { normalizeFacetScalePolicies } from
11
11
  "../../grammar/facets/scales.js";
12
+ import { FACET_SCALE_CHANNELS } from "../../grammar/facets/scales.js";
12
13
  import { resolveFacetLayout } from "../../layout/facets.js";
13
14
  import { compositionChildDescriptor } from
14
15
  "../../materialization/composition.js";
15
16
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
16
17
  import { deriveFacetChildren } from "./derive.js";
17
- import { rebindLayerData, replayDerivedData } from "./replay.js";
18
+ import { replayDerivedData } from "./replay.js";
18
19
  import { composeFacetGuides } from "./guides.js";
19
20
 
20
21
  const FACET_OPTIONS = Object.freeze([
@@ -73,6 +74,76 @@ function requireFacetProgram(program, operation) {
73
74
  }
74
75
  }
75
76
 
77
+ function currentFacetDefinition(program) {
78
+ const current = program.compositionSpec;
79
+ return resolveFacetDefinition(program.semanticSpec, {
80
+ id: current.id,
81
+ data: current.facet.data,
82
+ field: current.facet.field,
83
+ values: current.facet.values
84
+ });
85
+ }
86
+
87
+ function facetUnitTemplate(program) {
88
+ const seed = program.children[program.compositionSpec.children[0]];
89
+ if (seed === undefined) {
90
+ throw new Error(`Facet "${program.compositionSpec.id}" requires a retained child.`);
91
+ }
92
+ const { facets: _facets, ...unitConfigs } = program.materializationConfigs;
93
+ return new program.constructor({
94
+ semanticSpec: program.semanticSpec,
95
+ graphicSpec: seed.graphicSpec,
96
+ resolvedScales: program.resolvedScales,
97
+ materializationConfigs: freezeOwned({
98
+ ...unitConfigs,
99
+ canvas: seed.materializationConfigs.canvas
100
+ }),
101
+ children: {},
102
+ context: program.context,
103
+ trace: program.trace,
104
+ actionStack: program.actionStack,
105
+ actionSequence: program._actionSequence
106
+ });
107
+ }
108
+
109
+ function applicableScaleRequest(program, channels) {
110
+ return Object.fromEntries(FACET_SCALE_CHANNELS.flatMap(channel =>
111
+ program.semanticSpec.layers.some(
112
+ layer => layer.encoding?.[channel]?.scale !== undefined
113
+ )
114
+ ? [[channel, channels[channel]]]
115
+ : []
116
+ ));
117
+ }
118
+
119
+ function rederiveFacet(program, { scales, guides }) {
120
+ const current = program.compositionSpec;
121
+ const definition = currentFacetDefinition(program);
122
+ const request = applicableScaleRequest(program, scales);
123
+ const normalized = normalizeFacetScalePolicies(program.semanticSpec, request);
124
+ const derived = deriveFacetChildren(facetUnitTemplate(program), definition, {
125
+ closeInheritedAction: true,
126
+ stripTitle: true,
127
+ scales: request
128
+ });
129
+ const compositionSpec = {
130
+ ...current,
131
+ facet: {
132
+ ...current.facet,
133
+ scales: normalized.channels,
134
+ guides
135
+ }
136
+ };
137
+ let next = program._withCompositionState({
138
+ children: derived.children,
139
+ compositionSpec
140
+ });
141
+ for (const id of compositionSpec.children) {
142
+ next = next.useProgram({ id });
143
+ }
144
+ return next.materializeComposition();
145
+ }
146
+
76
147
  export const facet = action(
77
148
  {
78
149
  op: "facet",
@@ -153,10 +224,73 @@ export const editFacetHeaders = action(
153
224
  }
154
225
  );
155
226
 
227
+ export const editFacetScales = action(
228
+ {
229
+ op: "editFacetScales",
230
+ description: "Edit facet scale-resolution policies and rederive every cell.",
231
+ scope: "composition"
232
+ },
233
+ function (args = {}) {
234
+ requireFacetProgram(this, "editFacetScales");
235
+ validateOptionObject(args, FACET_SCALE_CHANNELS, "editFacetScales", {
236
+ allowEmpty: false,
237
+ emptyMessage: "editFacetScales requires at least one channel policy change."
238
+ });
239
+ for (const channel of Object.keys(args)) {
240
+ if (!this.semanticSpec.layers.some(
241
+ layer => layer.encoding?.[channel]?.scale !== undefined
242
+ )) {
243
+ throw new Error(
244
+ `Facet scale channel "${channel}" is not used by an affected layer.`
245
+ );
246
+ }
247
+ }
248
+ const current = this.compositionSpec.facet;
249
+ const scales = { ...current.scales, ...args };
250
+ if (FACET_SCALE_CHANNELS.every(
251
+ channel => scales[channel] === current.scales[channel]
252
+ )) {
253
+ throw new Error("editFacetScales requires at least one channel policy change.");
254
+ }
255
+ const request = applicableScaleRequest(this, scales);
256
+ const normalized = normalizeFacetScalePolicies(this.semanticSpec, request);
257
+ const applyEdit = program => rederiveFacet(program, {
258
+ scales: normalized.channels,
259
+ guides: current.guides
260
+ });
261
+ applyEdit(this);
262
+ return applyEdit(this);
263
+ }
264
+ );
265
+
266
+ export const editFacetGuides = action(
267
+ {
268
+ op: "editFacetGuides",
269
+ description: "Edit facet guide ownership and rederive every cell.",
270
+ scope: "composition"
271
+ },
272
+ function (args = {}) {
273
+ requireFacetProgram(this, "editFacetGuides");
274
+ validateOptionObject(args, GUIDE_OPTIONS, "editFacetGuides", {
275
+ allowEmpty: false,
276
+ emptyMessage: "editFacetGuides requires at least one guide policy."
277
+ });
278
+ const current = this.compositionSpec.facet;
279
+ const guides = normalizeGuides({ ...current.guides, ...args });
280
+ const applyEdit = program => rederiveFacet(program, {
281
+ scales: current.scales,
282
+ guides
283
+ });
284
+ applyEdit(this);
285
+ return applyEdit(this);
286
+ }
287
+ );
288
+
156
289
  export function registerFacetActions(ProgramClass) {
157
290
  ProgramClass.prototype.replayDerivedData = replayDerivedData;
158
- ProgramClass.prototype.rebindLayerData = rebindLayerData;
159
291
  ProgramClass.prototype.composeFacetGuides = composeFacetGuides;
160
292
  ProgramClass.prototype.facet = facet;
161
293
  ProgramClass.prototype.editFacetHeaders = editFacetHeaders;
294
+ ProgramClass.prototype.editFacetScales = editFacetScales;
295
+ ProgramClass.prototype.editFacetGuides = editFacetGuides;
162
296
  }
@@ -12,6 +12,7 @@ import { buildMaterializationPlan } from "../../materialization/planner.js";
12
12
  import { requireDataset } from "../../selectors/datasets.js";
13
13
  import { requireLayer } from "../../selectors/layers.js";
14
14
  import { findSemanticScale } from "../../selectors/scales.js";
15
+ import { findTransformPolicy } from "../../grammar/transforms.js";
15
16
 
16
17
  function sharedHistogramBoundaries(program) {
17
18
  const boundaries = new Map();
@@ -59,6 +60,23 @@ function replayDatasetId(cell, owner) {
59
60
  return `${cell.id}-${owner}-data`;
60
61
  }
61
62
 
63
+ function rebindReplayedGradientPlots(child, base, datasets) {
64
+ let next = child;
65
+ for (const [id, markConfig] of Object.entries(base.markConfigs)) {
66
+ const gradientPlot = markConfig.gradientPlot;
67
+ if (gradientPlot?.materialized !== true) continue;
68
+ const profile = datasets.get(gradientPlot.profileId);
69
+ const source = datasets.get(gradientPlot.source);
70
+ if (profile === undefined || source === undefined) {
71
+ throw new Error(
72
+ `Facet gradient plot "${id}" is missing replayed profile dependencies.`
73
+ );
74
+ }
75
+ next = next.rebindGradientPlotProfile({ id, profile, source });
76
+ }
77
+ return next;
78
+ }
79
+
62
80
  function cellMaterializationPlan(program) {
63
81
  const scaleIds = [...new Set(program.semanticSpec.layers.flatMap(layer =>
64
82
  Object.values(layer.encoding ?? {})
@@ -72,7 +90,13 @@ function cellMaterializationPlan(program) {
72
90
  return buildMaterializationPlan({ scales });
73
91
  }
74
92
 
75
- function deriveCellProgram(base, definition, cell, histogramBoundaries) {
93
+ function deriveCellProgram(
94
+ base,
95
+ definition,
96
+ cell,
97
+ histogramBoundaries,
98
+ scales
99
+ ) {
76
100
  let child = base.filterData({
77
101
  id: cell.data,
78
102
  source: definition.data,
@@ -88,10 +112,15 @@ function deriveCellProgram(base, definition, cell, histogramBoundaries) {
88
112
  );
89
113
  }
90
114
  const id = replayDatasetId(cell, replay.id);
115
+ const policy = findTransformPolicy(replay.transform.type);
116
+ const transform = policy?.facetReplayTransform?.(
117
+ replay.transform,
118
+ { scales }
119
+ ) ?? replay.transform;
91
120
  child = child.replayDerivedData({
92
121
  id,
93
122
  source,
94
- transform: replay.transform
123
+ transform
95
124
  });
96
125
  datasets.set(replay.id, id);
97
126
  }
@@ -109,6 +138,7 @@ function deriveCellProgram(base, definition, cell, histogramBoundaries) {
109
138
  histogramBoundaries.get(layer.id)
110
139
  );
111
140
  }
141
+ child = rebindReplayedGradientPlots(child, base, datasets);
112
142
  return applyMaterializationPlan(child, cellMaterializationPlan(child));
113
143
  }
114
144
 
@@ -163,7 +193,7 @@ export function deriveFacetChildren(
163
193
  : new Map();
164
194
  const independentlyResolved = Object.fromEntries(definition.cells.map(cell => [
165
195
  cell.id,
166
- deriveCellProgram(template, definition, cell, bins)
196
+ deriveCellProgram(template, definition, cell, bins, resolutionRequest)
167
197
  ]));
168
198
  const resolution = resolveFacetScaleDomains(
169
199
  template.semanticSpec,
@@ -2,33 +2,25 @@ import { action } from "../../core/action.js";
2
2
  import { isPlainObject } from "../../core/immutable.js";
3
3
  import { validateUserId } from "../../core/identifiers.js";
4
4
  import { validateKeys } from "../../core/validation.js";
5
+ import { findTransformPolicy } from "../../grammar/transforms.js";
5
6
  import { hasDataset } from "../../selectors/datasets.js";
6
- import { requireLayer } from "../../selectors/layers.js";
7
7
 
8
8
  const REPLAY_OPTIONS = Object.freeze(["id", "source", "transform"]);
9
- const REBIND_OPTIONS = Object.freeze(["id", "data"]);
10
- const MATERIALIZERS = Object.freeze({
11
- filter: "materializeFilteredData",
12
- regression: "materializeRegressionData",
13
- density: "materializeDensityData",
14
- interval: "materializeIntervalData",
15
- boxSummary: "materializeBoxSummaryData",
16
- boxOutlier: "materializeBoxOutlierData"
17
- });
18
9
 
19
10
  function requestedTransform(transform) {
20
11
  if (!isPlainObject(transform)) {
21
12
  throw new TypeError("replayDerivedData transform must be a plain object.");
22
13
  }
23
- const materialize = MATERIALIZERS[transform.type];
24
- if (materialize === undefined) {
14
+ const policy = findTransformPolicy(transform.type);
15
+ if (policy?.facetTopology === undefined) {
25
16
  throw new Error(
26
17
  `replayDerivedData does not support transform "${transform.type ?? "unknown"}".`
27
18
  );
28
19
  }
29
- if (transform.type !== "density") return { transform, materialize };
30
- const { resolved: _resolved, ...requested } = transform;
31
- return { transform: requested, materialize };
20
+ return {
21
+ transform: policy.replayTransform?.(transform) ?? transform,
22
+ materialize: policy.materializeOp
23
+ };
32
24
  }
33
25
 
34
26
  export const replayDerivedData = action(
@@ -46,24 +38,3 @@ export const replayDerivedData = action(
46
38
  [resolved.materialize]({ id });
47
39
  }
48
40
  );
49
-
50
- export const rebindLayerData = action(
51
- {
52
- op: "rebindLayerData",
53
- description: "Rebind one facet-cell layer to its replayed dataset."
54
- },
55
- function (args = {}) {
56
- validateKeys(args, REBIND_OPTIONS, "rebindLayerData");
57
- const id = validateUserId(args.id, "Facet layer id");
58
- const data = validateUserId(args.data, "Facet layer dataset id");
59
- requireLayer(this, id);
60
- if (!hasDataset(this, data)) {
61
- throw new Error(`Facet layer dataset "${data}" does not exist.`);
62
- }
63
- return this.editSemantic({
64
- property: `layer[${id}].data`,
65
- value: data
66
- });
67
- }
68
- );
69
-
@@ -0,0 +1,177 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateKeys } from "../../core/validation.js";
3
+ import { GRADIENT_PROFILE_FIELDS } from "../../grammar/gradientProfile.js";
4
+ import { resolveGraphicBounds } from "../../layout/canvas.js";
5
+ import { findLayer } from "../../selectors/layers.js";
6
+ import { createDensityLegendPaint } from "./paint.js";
7
+
8
+ const CENTER_OPTIONS = Object.freeze([
9
+ "id", "owner", "data", "category", "categoryType", "coordinate",
10
+ "categoryScale", "measureScale", "orientation", "size", "stroke",
11
+ "strokeWidth"
12
+ ]);
13
+ const LEGEND_OPTIONS = Object.freeze(["owner", "title", "position"]);
14
+
15
+ export const createGradientPlotCenter = action(
16
+ {
17
+ op: "createGradientPlotCenter",
18
+ description: "Create the optional center rule for one gradient plot."
19
+ },
20
+ function (args = {}) {
21
+ validateKeys(args, CENTER_OPTIONS, "createGradientPlotCenter");
22
+ const categoryAction = args.orientation === "vertical" ? "encodeX" : "encodeY";
23
+ const measureAction = args.orientation === "vertical" ? "encodeY" : "encodeX";
24
+ const spanOrientation = args.orientation === "vertical" ? "horizontal" : "vertical";
25
+ let next = this.createRuleMark({ id: args.id, data: args.data });
26
+ next = next[categoryAction]({
27
+ target: args.id,
28
+ field: args.category,
29
+ fieldType: args.categoryType,
30
+ coordinate: args.coordinate,
31
+ scale: { id: args.categoryScale }
32
+ });
33
+ next = next[measureAction]({
34
+ target: args.id,
35
+ field: GRADIENT_PROFILE_FIELDS.center,
36
+ fieldType: "quantitative",
37
+ coordinate: args.coordinate,
38
+ scale: { id: args.measureScale }
39
+ });
40
+ return next
41
+ .encodeStroke({ target: args.id, value: args.stroke })
42
+ .encodeStrokeWidth({ target: args.id, value: args.strokeWidth })
43
+ .materializeRuleSpan({
44
+ id: args.id,
45
+ orientation: spanOrientation,
46
+ size: args.size
47
+ });
48
+ }
49
+ );
50
+
51
+ function legendIds(owner) {
52
+ return {
53
+ strip: `${owner}DensityLegend`,
54
+ labels: `${owner}DensityLegendLabels`,
55
+ title: `${owner}DensityLegendTitle`
56
+ };
57
+ }
58
+
59
+ function requireLegendOwner(program, owner) {
60
+ const layer = findLayer(program, owner);
61
+ const config = program.markConfigs[owner]?.gradientPlot;
62
+ if (layer === undefined || config?.materialized !== true) {
63
+ throw new Error(`Unknown gradient-plot legend owner "${owner}".`);
64
+ }
65
+ return { layer, config };
66
+ }
67
+
68
+ export const rematerializeGradientPlotLegend = action(
69
+ {
70
+ op: "rematerializeGradientPlotLegend",
71
+ description: "Rematerialize one gradient plot density legend."
72
+ },
73
+ function ({ owner } = {}) {
74
+ const { config } = requireLegendOwner(this, owner);
75
+ const ids = legendIds(owner);
76
+ const bounds = resolveGraphicBounds(this);
77
+ const position = config.guides.legend.position;
78
+ if (position !== "right") {
79
+ throw new Error('Gradient plot density legend currently requires position "right".');
80
+ }
81
+ const x = bounds.x + bounds.width + 50;
82
+ const y = bounds.y + 50;
83
+ const width = 22;
84
+ const height = Math.min(170, Math.max(80, bounds.height - 110));
85
+ return this
86
+ .editGraphics({ target: ids.strip, property: "x", value: x })
87
+ .editGraphics({ target: ids.strip, property: "y", value: y })
88
+ .editGraphics({ target: ids.strip, property: "width", value: width })
89
+ .editGraphics({ target: ids.strip, property: "height", value: height })
90
+ .editGraphics({
91
+ target: ids.strip,
92
+ property: "fill",
93
+ value: createDensityLegendPaint(config.gradient.opacity)
94
+ })
95
+ .editGraphics({ target: ids.strip, property: "stroke", value: "#cbd5e1" })
96
+ .editGraphics({ target: ids.strip, property: "strokeWidth", value: 1 })
97
+ .editGraphics({ target: ids.labels, property: "x", value: x + width + 10 })
98
+ .editGraphics({ target: ids.labels, property: "y", value: [y + height, y] })
99
+ .editGraphics({ target: ids.labels, property: "text", value: ["Low", "High"] })
100
+ .editGraphics({ target: ids.labels, property: "fill", value: "#64748b" })
101
+ .editGraphics({ target: ids.labels, property: "fontSize", value: 11 })
102
+ .editGraphics({ target: ids.labels, property: "fontFamily", value: "sans-serif" })
103
+ .editGraphics({ target: ids.labels, property: "textAlign", value: "left" })
104
+ .editGraphics({ target: ids.labels, property: "textBaseline", value: "middle" })
105
+ .editGraphics({ target: ids.title, property: "x", value: x })
106
+ .editGraphics({ target: ids.title, property: "y", value: y - 18 })
107
+ .editGraphics({ target: ids.title, property: "text", value: config.guides.legend.title })
108
+ .editGraphics({ target: ids.title, property: "fill", value: "#334155" })
109
+ .editGraphics({ target: ids.title, property: "fontSize", value: 12 })
110
+ .editGraphics({ target: ids.title, property: "fontFamily", value: "sans-serif" })
111
+ .editGraphics({ target: ids.title, property: "fontWeight", value: 600 })
112
+ .editGraphics({ target: ids.title, property: "textAlign", value: "left" })
113
+ .editGraphics({ target: ids.title, property: "textBaseline", value: "middle" });
114
+ }
115
+ );
116
+
117
+ export const createGradientPlotLegend = action(
118
+ {
119
+ op: "createGradientPlotLegend",
120
+ description: "Create the neutral density legend owned by one gradient plot."
121
+ },
122
+ function (args = {}) {
123
+ validateKeys(args, LEGEND_OPTIONS, "createGradientPlotLegend");
124
+ requireLegendOwner(this, args.owner);
125
+ if (args.position !== undefined && args.position !== "right") {
126
+ throw new Error('Gradient plot density legend currently requires position "right".');
127
+ }
128
+ if (args.title !== undefined && (typeof args.title !== "string" || args.title.length === 0)) {
129
+ throw new TypeError("Gradient plot density legend title must be a non-empty string.");
130
+ }
131
+ const ids = legendIds(args.owner);
132
+ const densityScale = `${args.owner}Density`;
133
+ let next = this
134
+ .editSemantic({ property: `scale[${densityScale}].type`, value: "sequential" })
135
+ .editSemantic({
136
+ property: `scale[${densityScale}].domain`,
137
+ value: this.markConfigs[args.owner].gradientPlot.intensityDomain
138
+ })
139
+ .editSemantic({
140
+ property: `scale[${densityScale}].range`,
141
+ value: { palette: { name: "greys" } }
142
+ })
143
+ .editSemantic({ property: "guide.legend.color.scale", value: densityScale })
144
+ .editSemantic({
145
+ property: "guide.legend.color.title",
146
+ value: args.title ?? "Relative density"
147
+ })
148
+ .createGraphics({ id: ids.strip, type: "rect", parent: "canvas" })
149
+ .createGraphics({ id: ids.labels, type: "text", length: 2, parent: "canvas" })
150
+ .createGraphics({ id: ids.title, type: "text", parent: "canvas" });
151
+ next = next._withMarkConfig(args.owner, {
152
+ ...next.markConfigs[args.owner],
153
+ gradientPlot: {
154
+ ...next.markConfigs[args.owner].gradientPlot,
155
+ guides: {
156
+ ...next.markConfigs[args.owner].gradientPlot.guides,
157
+ legend: {
158
+ title: args.title ?? "Relative density",
159
+ position: args.position ?? "right"
160
+ }
161
+ }
162
+ }
163
+ });
164
+ return next.rematerializeGradientPlotLegend({ owner: args.owner });
165
+ }
166
+ );
167
+
168
+ export function removeGradientPlotLegend(program, owner) {
169
+ const ids = legendIds(owner);
170
+ let next = program;
171
+ for (const id of Object.values(ids)) {
172
+ if (next.graphicSpec.objects[id] !== undefined) {
173
+ next = next.editGraphics({ target: id, remove: true });
174
+ }
175
+ }
176
+ return next;
177
+ }
@@ -0,0 +1,96 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateKeys } from "../../core/validation.js";
3
+ import { findDataset } from "../../selectors/datasets.js";
4
+ import { findSemanticScale } from "../../selectors/scales.js";
5
+ import {
6
+ GRADIENT_PLOT_OPTIONS,
7
+ gradientEncodingArgs,
8
+ resolveGradientAppearance,
9
+ resolveGradientCenter,
10
+ resolveGradientDensity,
11
+ resolveGradientGuides,
12
+ resolveGradientPosition,
13
+ resolveGradientWidth
14
+ } from "./options.js";
15
+ import {
16
+ normalizeGradientPositionTypes,
17
+ resolveGradientPlotId,
18
+ resolveGradientSourceLayer
19
+ } from "./resolve.js";
20
+
21
+ export const createGradientPlot = action(
22
+ {
23
+ op: "createGradientPlot",
24
+ description: "Create a categorical density gradient plot."
25
+ },
26
+ function (args = {}) {
27
+ validateKeys(args, GRADIENT_PLOT_OPTIONS, "createGradientPlot");
28
+ const id = resolveGradientPlotId(this, args.id);
29
+ const source = resolveGradientSourceLayer(this, args.target);
30
+ const data = args.data ?? source?.data ?? this.context.currentData ??
31
+ (this.semanticSpec.datasets.length === 1
32
+ ? this.semanticSpec.datasets[0].id
33
+ : undefined);
34
+ if (findDataset(this, data) === undefined) {
35
+ throw new Error("createGradientPlot requires data or one inferable dataset.");
36
+ }
37
+ const positions = normalizeGradientPositionTypes(
38
+ resolveGradientPosition(args.x, "x") ?? source?.encoding?.x,
39
+ resolveGradientPosition(args.y, "y") ?? source?.encoding?.y
40
+ );
41
+ if (
42
+ positions.x !== undefined && positions.y !== undefined &&
43
+ positions.orientation === undefined
44
+ ) {
45
+ throw new Error(
46
+ "createGradientPlot requires one categorical axis and one quantitative axis."
47
+ );
48
+ }
49
+ const density = resolveGradientDensity(args.density);
50
+ const width = resolveGradientWidth(args.width);
51
+ const gradient = resolveGradientAppearance(args.gradient);
52
+ const center = resolveGradientCenter(args.center);
53
+ const guides = resolveGradientGuides(args.guides);
54
+ const category = positions.orientation === "vertical"
55
+ ? positions.x
56
+ : positions.orientation === "horizontal"
57
+ ? positions.y
58
+ : undefined;
59
+ const categoryScaleId = typeof category?.scale === "string"
60
+ ? category.scale
61
+ : category?.scale?.id;
62
+ const categoryScale = findSemanticScale(this, categoryScaleId);
63
+ let next = categoryScale?.type === "point"
64
+ ? this.editScale({ id: categoryScaleId, type: "band" })
65
+ : this;
66
+ next = next.createRectMark({ id, data })._withMarkConfig(id, {
67
+ ...this.markConfigs[id],
68
+ gradientPlot: {
69
+ materialized: false,
70
+ density,
71
+ width,
72
+ gradient,
73
+ center,
74
+ guides
75
+ },
76
+ itemFilterable: false,
77
+ stroke: false,
78
+ strokeWidth: 0
79
+ });
80
+ if (positions.x !== undefined) {
81
+ next = next.encodeX({
82
+ ...gradientEncodingArgs(positions.x),
83
+ target: id,
84
+ coordinate: args.coordinate ?? source?.coordinate
85
+ });
86
+ }
87
+ if (positions.y !== undefined) {
88
+ next = next.encodeY({
89
+ ...gradientEncodingArgs(positions.y),
90
+ target: id,
91
+ coordinate: args.coordinate ?? source?.coordinate
92
+ });
93
+ }
94
+ return next.materializeGradientPlot({ id });
95
+ }
96
+ );