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
@@ -0,0 +1,228 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateOptionObject } from "../../../core/validation.js";
3
+ import { resolvePlotGraphicPlacement } from
4
+ "../../../materialization/graphicHierarchy.js";
5
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
6
+ "../../../theme/defaults.js";
7
+ import {
8
+ requireParallelAxisLayer,
9
+ resolveParallelAxisTarget,
10
+ resolveParallelAxisValues
11
+ } from "./parallel/resolve.js";
12
+
13
+ const CREATE_OPTIONS = Object.freeze(["target", "coordinate"]);
14
+ const REMATERIALIZE_OPTIONS = Object.freeze(["target"]);
15
+
16
+ export const rematerializeParallelAxes = action(
17
+ {
18
+ op: "rematerializeParallelAxes",
19
+ description: "Recompute concrete Parallel dimension axes."
20
+ },
21
+ function (args = {}) {
22
+ validateOptionObject(
23
+ args,
24
+ REMATERIALIZE_OPTIONS,
25
+ "rematerializeParallelAxes"
26
+ );
27
+ const target = resolveParallelAxisTarget(
28
+ this,
29
+ args.target ?? this.guideConfigs.axis?.parallel?.axes?.target
30
+ );
31
+ const { dimensions } = requireParallelAxisLayer(this, target);
32
+ const { axes, bounds } = resolveParallelAxisValues(this, dimensions);
33
+ const ticks = axes.flatMap(axis => axis.values.map((value, index) => ({
34
+ x: axis.x,
35
+ y: axis.y[index],
36
+ value,
37
+ text: axis.labels[index]
38
+ })));
39
+ let next = this
40
+ .editGraphics({
41
+ target: "parallelAxisLines",
42
+ property: "length",
43
+ value: axes.length
44
+ })
45
+ .editGraphics({
46
+ target: "parallelAxisLines",
47
+ property: "x1",
48
+ value: axes.map(axis => axis.x)
49
+ })
50
+ .editGraphics({ target: "parallelAxisLines", property: "y1", value: bounds.y })
51
+ .editGraphics({
52
+ target: "parallelAxisLines",
53
+ property: "x2",
54
+ value: axes.map(axis => axis.x)
55
+ })
56
+ .editGraphics({
57
+ target: "parallelAxisLines",
58
+ property: "y2",
59
+ value: bounds.y + bounds.height
60
+ })
61
+ .editGraphics({
62
+ target: "parallelAxisLines",
63
+ property: "stroke",
64
+ value: DEFAULT_COLORS.axis
65
+ })
66
+ .editGraphics({
67
+ target: "parallelAxisLines",
68
+ property: "strokeWidth",
69
+ value: 1.25
70
+ })
71
+ .editGraphics({
72
+ target: "parallelAxisTicks",
73
+ property: "length",
74
+ value: ticks.length
75
+ })
76
+ .editGraphics({
77
+ target: "parallelAxisTicks",
78
+ property: "x1",
79
+ value: ticks.map(tick => tick.x - 4)
80
+ })
81
+ .editGraphics({
82
+ target: "parallelAxisTicks",
83
+ property: "y1",
84
+ value: ticks.map(tick => tick.y)
85
+ })
86
+ .editGraphics({
87
+ target: "parallelAxisTicks",
88
+ property: "x2",
89
+ value: ticks.map(tick => tick.x + 4)
90
+ })
91
+ .editGraphics({
92
+ target: "parallelAxisTicks",
93
+ property: "y2",
94
+ value: ticks.map(tick => tick.y)
95
+ })
96
+ .editGraphics({
97
+ target: "parallelAxisTicks",
98
+ property: "stroke",
99
+ value: DEFAULT_COLORS.mutedText
100
+ })
101
+ .editGraphics({
102
+ target: "parallelAxisTicks",
103
+ property: "strokeWidth",
104
+ value: 1
105
+ })
106
+ .editGraphics({
107
+ target: "parallelAxisLabels",
108
+ property: "length",
109
+ value: ticks.length
110
+ })
111
+ .editGraphics({
112
+ target: "parallelAxisLabels",
113
+ property: "x",
114
+ value: ticks.map(tick => tick.x - 9)
115
+ })
116
+ .editGraphics({
117
+ target: "parallelAxisLabels",
118
+ property: "y",
119
+ value: ticks.map(tick => tick.y)
120
+ })
121
+ .editGraphics({
122
+ target: "parallelAxisLabels",
123
+ property: "text",
124
+ value: ticks.map(tick => tick.text)
125
+ })
126
+ .editGraphics({ target: "parallelAxisLabels", property: "fill", value: DEFAULT_COLORS.axis })
127
+ .editGraphics({ target: "parallelAxisLabels", property: "fontSize", value: 11 })
128
+ .editGraphics({ target: "parallelAxisLabels", property: "fontFamily", value: DEFAULT_FONT_FAMILY })
129
+ .editGraphics({ target: "parallelAxisLabels", property: "fontWeight", value: "normal" })
130
+ .editGraphics({ target: "parallelAxisLabels", property: "textAlign", value: "right" })
131
+ .editGraphics({ target: "parallelAxisLabels", property: "textBaseline", value: "middle" })
132
+ .editGraphics({
133
+ target: "parallelAxisTitles",
134
+ property: "length",
135
+ value: axes.length
136
+ })
137
+ .editGraphics({
138
+ target: "parallelAxisTitles",
139
+ property: "x",
140
+ value: axes.map(axis => axis.x)
141
+ })
142
+ .editGraphics({
143
+ target: "parallelAxisTitles",
144
+ property: "y",
145
+ value: bounds.y - 20
146
+ })
147
+ .editGraphics({
148
+ target: "parallelAxisTitles",
149
+ property: "text",
150
+ value: axes.map(axis => axis.title)
151
+ })
152
+ .editGraphics({ target: "parallelAxisTitles", property: "fill", value: DEFAULT_COLORS.axisTitle })
153
+ .editGraphics({ target: "parallelAxisTitles", property: "fontSize", value: 13 })
154
+ .editGraphics({ target: "parallelAxisTitles", property: "fontFamily", value: DEFAULT_FONT_FAMILY })
155
+ .editGraphics({ target: "parallelAxisTitles", property: "fontWeight", value: 600 })
156
+ .editGraphics({ target: "parallelAxisTitles", property: "textAlign", value: "center" })
157
+ .editGraphics({ target: "parallelAxisTitles", property: "textBaseline", value: "middle" });
158
+ return next._withGuideConfig("parallel", "axes", {
159
+ target,
160
+ scales: dimensions.map(dimension => dimension.scale)
161
+ });
162
+ }
163
+ );
164
+
165
+ export const createParallelAxes = action(
166
+ {
167
+ op: "createParallelAxes",
168
+ description: "Create ordinary line and text graphics for Parallel dimensions."
169
+ },
170
+ function (args = {}) {
171
+ validateOptionObject(args, CREATE_OPTIONS, "createParallelAxes");
172
+ const target = resolveParallelAxisTarget(this, args.target);
173
+ const { coordinate, dimensions } = requireParallelAxisLayer(this, target);
174
+ if (args.coordinate !== undefined && args.coordinate !== coordinate.id) {
175
+ throw new Error(
176
+ `Parallel layer "${target}" uses coordinate "${coordinate.id}".`
177
+ );
178
+ }
179
+ for (const id of [
180
+ "parallelAxisLines", "parallelAxisTicks", "parallelAxisLabels",
181
+ "parallelAxisTitles"
182
+ ]) {
183
+ if (this.graphicSpec.objects[id] !== undefined) {
184
+ throw new Error("createParallelAxes requires missing Parallel axes.");
185
+ }
186
+ }
187
+ const placement = resolvePlotGraphicPlacement(this);
188
+ let next = this
189
+ .editSemantic({
190
+ property: "guide.axis.parallel.target",
191
+ value: target
192
+ })
193
+ .editSemantic({
194
+ property: "guide.axis.parallel.coordinate",
195
+ value: coordinate.id
196
+ })
197
+ .editSemantic({
198
+ property: "guide.axis.parallel.scales",
199
+ value: dimensions.map(dimension => dimension.scale)
200
+ })
201
+ .createGraphics({ id: "parallelAxisLines", type: "line", length: 0, ...placement })
202
+ .editGraphics({ target: "parallelAxisLines", property: "stroke", value: DEFAULT_COLORS.axis })
203
+ .editGraphics({ target: "parallelAxisLines", property: "strokeWidth", value: 1.25 })
204
+ .createGraphics({ id: "parallelAxisTicks", type: "line", length: 0, ...placement })
205
+ .editGraphics({ target: "parallelAxisTicks", property: "stroke", value: DEFAULT_COLORS.mutedText })
206
+ .editGraphics({ target: "parallelAxisTicks", property: "strokeWidth", value: 1 })
207
+ .createGraphics({ id: "parallelAxisLabels", type: "text", length: 0, ...placement })
208
+ .editGraphics({ target: "parallelAxisLabels", property: "fill", value: DEFAULT_COLORS.axis })
209
+ .editGraphics({ target: "parallelAxisLabels", property: "fontSize", value: 11 })
210
+ .editGraphics({ target: "parallelAxisLabels", property: "fontFamily", value: DEFAULT_FONT_FAMILY })
211
+ .editGraphics({ target: "parallelAxisLabels", property: "fontWeight", value: "normal" })
212
+ .editGraphics({ target: "parallelAxisLabels", property: "textAlign", value: "right" })
213
+ .editGraphics({ target: "parallelAxisLabels", property: "textBaseline", value: "middle" })
214
+ .createGraphics({ id: "parallelAxisTitles", type: "text", length: 0, ...placement })
215
+ .editGraphics({ target: "parallelAxisTitles", property: "fill", value: DEFAULT_COLORS.axisTitle })
216
+ .editGraphics({ target: "parallelAxisTitles", property: "fontSize", value: 13 })
217
+ .editGraphics({ target: "parallelAxisTitles", property: "fontFamily", value: DEFAULT_FONT_FAMILY })
218
+ .editGraphics({ target: "parallelAxisTitles", property: "fontWeight", value: 600 })
219
+ .editGraphics({ target: "parallelAxisTitles", property: "textAlign", value: "center" })
220
+ .editGraphics({ target: "parallelAxisTitles", property: "textBaseline", value: "middle" });
221
+ return next.rematerializeParallelAxes({ target });
222
+ }
223
+ );
224
+
225
+ export function registerParallelAxisActions(ProgramClass) {
226
+ ProgramClass.prototype.createParallelAxes = createParallelAxes;
227
+ ProgramClass.prototype.rematerializeParallelAxes = rematerializeParallelAxes;
228
+ }
@@ -24,6 +24,7 @@ import {
24
24
  } from "./policy.js";
25
25
  import { findCanvasGraphic, resolvePlotGraphicPlacement } from
26
26
  "../../../materialization/graphicHierarchy.js";
27
+ import { resolveTextBounds } from "../../../core/textMetrics.js";
27
28
 
28
29
  const CREATE_OPTIONS = Object.freeze([
29
30
  "text", "scale", "position", "at", "offset", "rotation", "color",
@@ -132,17 +133,19 @@ function resolveGeometry(program, channel, config) {
132
133
  offset: config.offset
133
134
  });
134
135
  const text = program.semanticSpec.guides.axis?.[channel]?.title ?? "";
135
- const width = [...text].length * config.fontSize * 0.6;
136
- const height = config.fontSize;
137
- const cosine = Math.cos(config.rotation);
138
- const sine = Math.sin(config.rotation);
139
- const extentX = Math.abs(cosine) * width / 2 + Math.abs(sine) * height / 2;
140
- const extentY = Math.abs(sine) * width / 2 + Math.abs(cosine) * height / 2;
136
+ const titleBounds = resolveTextBounds({
137
+ ...geometry,
138
+ text,
139
+ fontSize: config.fontSize,
140
+ textAlign: "center",
141
+ textBaseline: "middle",
142
+ rotation: config.rotation
143
+ });
141
144
  const canvas = findCanvasGraphic(program)?.properties;
142
145
  const newEdgeFits = config.position === "top"
143
- ? geometry.y - extentY >= 0
146
+ ? titleBounds.top >= 0
144
147
  : config.position === "right"
145
- ? geometry.x + extentX <= canvas?.width
148
+ ? titleBounds.right <= canvas?.width
146
149
  : true;
147
150
  if (!canvas || !newEdgeFits) {
148
151
  throw new Error(`The ${channel}-axis title does not fit the Canvas margin.`);
@@ -5,6 +5,9 @@ import {
5
5
  resolveAutomaticGridOptions,
6
6
  resolveGuideApplicability
7
7
  } from "./applicability.js";
8
+ import { findDataset } from "../../selectors/datasets.js";
9
+ import { findUpstreamTransform } from
10
+ "../../materialization/dataProvenance.js";
8
11
 
9
12
  const OPTIONS = Object.freeze(["axes", "grid", "legend"]);
10
13
 
@@ -21,16 +24,51 @@ function validateOptions(args) {
21
24
  validateGuideOption(args.legend, "createGuides legend");
22
25
  }
23
26
 
24
- function inferAxesOptions(program) {
27
+ function inferAxesOptions(program, applicability) {
25
28
  const horizontalInterval = program.semanticSpec.layers.some(layer =>
26
29
  layer.mark?.type === "rule" &&
27
30
  layer.encoding?.x?.fieldType === "quantitative" &&
28
31
  layer.encoding?.x2?.fieldType === "quantitative" &&
29
32
  ["nominal", "ordinal", "temporal"].includes(layer.encoding?.y?.fieldType)
30
33
  );
31
- return horizontalInterval
34
+ const inferred = horizontalInterval
32
35
  ? { x: { ticksAndLabels: { count: 7 } } }
33
36
  : {};
37
+ const directions = applicability.axes.directions;
38
+ const hasStoredY = program.semanticSpec.layers.some(
39
+ layer => layer.encoding?.y !== undefined
40
+ );
41
+ if (!directions.y && hasStoredY) inferred.y = false;
42
+ const horizonLayers = program.semanticSpec.layers.filter(layer =>
43
+ findUpstreamTransform(
44
+ program,
45
+ findDataset(program, layer.data),
46
+ "horizon"
47
+ ) !== undefined
48
+ );
49
+ if (horizonLayers.length === 1 && directions.x && !directions.y) {
50
+ const scaleId = horizonLayers[0].encoding?.x?.scale;
51
+ const domain = program.resolvedScales?.[scaleId]?.domain;
52
+ if (
53
+ Array.isArray(domain) &&
54
+ domain.length === 2 &&
55
+ domain.every(Number.isFinite) &&
56
+ domain[0] !== domain[1]
57
+ ) {
58
+ const low = Math.min(...domain);
59
+ const high = Math.max(...domain);
60
+ inferred.x = {
61
+ ...(inferred.x ?? {}),
62
+ ticksAndLabels: {
63
+ values: Array.from(
64
+ { length: 6 },
65
+ (_, index) => low + (high - low) * index / 5
66
+ )
67
+ }
68
+ };
69
+ }
70
+ }
71
+ return inferred;
34
72
  }
35
73
 
36
74
  function selectOption(explicit, applicable) {
@@ -39,6 +77,17 @@ function selectOption(explicit, applicable) {
39
77
  return applicable ? {} : undefined;
40
78
  }
41
79
 
80
+ function mergeInferredOptions(inferred, explicit) {
81
+ if (!isPlainObject(inferred) || !isPlainObject(explicit)) return explicit;
82
+ const merged = { ...inferred };
83
+ for (const [key, value] of Object.entries(explicit)) {
84
+ merged[key] = isPlainObject(value) && isPlainObject(inferred[key])
85
+ ? mergeInferredOptions(inferred[key], value)
86
+ : value;
87
+ }
88
+ return merged;
89
+ }
90
+
42
91
  const createGuides = action(
43
92
  {
44
93
  op: "createGuides",
@@ -47,10 +96,16 @@ const createGuides = action(
47
96
  function (args = {}) {
48
97
  validateOptions(args);
49
98
  const applicability = resolveGuideApplicability(this);
50
- const hasAxes = applicability.axes.cartesian || applicability.axes.polar;
99
+ const hasAxes = applicability.axes.cartesian || applicability.axes.polar ||
100
+ applicability.axes.parallel;
101
+ const inferredAxes = applicability.axes.cartesian
102
+ ? inferAxesOptions(this, applicability)
103
+ : {};
51
104
  const axes = args.axes === undefined && applicability.axes.cartesian
52
- ? inferAxesOptions(this)
53
- : selectOption(args.axes, hasAxes);
105
+ ? inferredAxes
106
+ : args.axes !== undefined && args.axes !== false && hasAxes
107
+ ? mergeInferredOptions(inferredAxes, args.axes)
108
+ : selectOption(args.axes, hasAxes);
54
109
  const grid = args.grid === undefined &&
55
110
  (applicability.grid.cartesian || applicability.grid.polar)
56
111
  ? resolveAutomaticGridOptions(this)
@@ -5,6 +5,9 @@ import { normalizeOptions } from "./options.js";
5
5
  import { findLayer } from "../../../../selectors/layers.js";
6
6
  import { findSemanticScale } from "../../../../selectors/scales.js";
7
7
  import { isSizeLegendPoint } from "../size.js";
8
+ import { isStrokeWidthLegendLayer } from "../strokeWidth.js";
9
+ import { legendResourcePolicies } from
10
+ "../../../../materialization/guides/resources.js";
8
11
  import {
9
12
  resolveCurrentDefinition,
10
13
  resolveDefinition,
@@ -80,17 +83,13 @@ export const rematerializeLegend = action(
80
83
  ).some(highlight => highlight.target === config.target);
81
84
  if (hasHighlight) next = next.rematerializeLegendHighlights();
82
85
  }
83
- if (this.guideConfigs.legend?.size !== undefined) {
84
- next = next.rematerializeSizeLegend();
85
- }
86
- if (this.guideConfigs.legend?.gradient !== undefined) {
87
- next = next.rematerializeGradientLegend();
88
- }
89
- if (this.guideConfigs.legend?.interval !== undefined) {
90
- next = next.rematerializeIntervalLegend();
91
- }
92
- if (this.guideConfigs.legend?.opacity !== undefined) {
93
- next = next.rematerializeOpacityLegend();
86
+ for (const policy of legendResourcePolicies()) {
87
+ if (
88
+ policy.rematerializeOp !== undefined &&
89
+ this.guideConfigs.legend?.[policy.kind] !== undefined
90
+ ) {
91
+ next = next[policy.rematerializeOp]();
92
+ }
94
93
  }
95
94
  return next;
96
95
  }
@@ -177,6 +176,8 @@ export const createLegend = action(
177
176
  throw new TypeError("createLegend channels must be an array.");
178
177
  }
179
178
  const explicitSize = channels?.length === 1 && channels[0] === "size";
179
+ const explicitStrokeWidth = channels?.length === 1 &&
180
+ channels[0] === "strokeWidth";
180
181
  const requestedSizeLayer = args.target === undefined
181
182
  ? undefined
182
183
  : findLayer(this, args.target);
@@ -216,6 +217,42 @@ export const createLegend = action(
216
217
  ...(count === undefined ? {} : { count })
217
218
  });
218
219
  }
220
+ const strokeWidthCandidates = this.semanticSpec.layers.filter(
221
+ isStrokeWidthLegendLayer
222
+ );
223
+ const requestedStrokeWidthLayer = args.target === undefined
224
+ ? undefined
225
+ : findLayer(this, args.target);
226
+ const hasOtherStrokeWidthLegendCandidate = this.semanticSpec.layers.some(
227
+ layer => ["color", "shape", "strokeDash", "size", "opacity"].some(
228
+ channel => layer.encoding?.[channel]?.scale !== undefined
229
+ )
230
+ );
231
+ const inferredStrokeWidth = channels === undefined && (
232
+ (args.target !== undefined &&
233
+ isStrokeWidthLegendLayer(requestedStrokeWidthLayer) &&
234
+ !hasNonSizeLegendEncoding(requestedStrokeWidthLayer)) ||
235
+ (args.target === undefined && strokeWidthCandidates.length === 1 &&
236
+ !hasOtherStrokeWidthLegendCandidate)
237
+ );
238
+ if (explicitStrokeWidth || inferredStrokeWidth) {
239
+ const { target, count, position, channels: _channels, ...unsupported } = args;
240
+ const unsupportedKeys = Object.keys(unsupported);
241
+ if (unsupportedKeys.length > 0) {
242
+ throw new Error(
243
+ `Standalone stroke-width legend does not support option "${unsupportedKeys[0]}".`
244
+ );
245
+ }
246
+ if (position !== undefined && position !== "right") {
247
+ throw new Error(
248
+ 'Standalone stroke-width legends currently require position "right".'
249
+ );
250
+ }
251
+ return this.createStrokeWidthLegend({
252
+ ...(target === undefined ? {} : { target }),
253
+ ...(count === undefined ? {} : { count })
254
+ });
255
+ }
219
256
  const opacityCandidates = this.semanticSpec.layers.filter(layer =>
220
257
  layer.mark?.type === "point" && layer.encoding?.opacity?.scale !== undefined
221
258
  );
@@ -323,14 +360,25 @@ export const createLegend = action(
323
360
  symbol
324
361
  });
325
362
  if (requestedPoint.encoding?.size?.scale !== undefined) {
326
- next = next.createSizeLegend({
327
- target: requestedPoint.id,
328
- ...(count === undefined ? {} : { count }),
329
- inheritAppearance:
330
- categoricalArgs.position === "left" ||
331
- categoricalArgs.labels !== undefined ||
332
- categoricalArgs.titleStyle !== undefined
333
- });
363
+ const existingSize = next.guideConfigs.legend?.size;
364
+ if (existingSize === undefined) {
365
+ next = next.createSizeLegend({
366
+ target: requestedPoint.id,
367
+ ...(count === undefined ? {} : { count }),
368
+ inheritAppearance:
369
+ categoricalArgs.position === "left" ||
370
+ categoricalArgs.labels !== undefined ||
371
+ categoricalArgs.titleStyle !== undefined
372
+ });
373
+ } else if (existingSize.target !== requestedPoint.id) {
374
+ throw new Error(
375
+ "Combined point series legend requires the active size legend to share its target."
376
+ );
377
+ } else if (count !== undefined && count !== existingSize.count) {
378
+ throw new Error(
379
+ "Existing size legend count must be edited before recreating the categorical block."
380
+ );
381
+ }
334
382
  next = next.rematerializeLegend();
335
383
  }
336
384
  return next;
@@ -49,7 +49,7 @@ const DEFAULT_BORDER = Object.freeze({
49
49
  export const validatePositive = validatePositiveFinite;
50
50
  export const validateNonNegative = validateNonNegativeFinite;
51
51
 
52
- function validateTextOptions(value, label, defaults) {
52
+ export function normalizeLegendTextOptions(value, label, defaults) {
53
53
  if (value === undefined) return { ...defaults };
54
54
  if (!isPlainObject(value)) {
55
55
  throw new TypeError(`${label} must be a plain object.`);
@@ -132,12 +132,12 @@ export function normalizeContinuousLegend(args, kind) {
132
132
  count,
133
133
  title: args.title,
134
134
  inferredTitle: args.title === undefined,
135
- labels: validateTextOptions(
135
+ labels: normalizeLegendTextOptions(
136
136
  args.labels,
137
137
  "createLegend.labels",
138
138
  DEFAULT_LABELS
139
139
  ),
140
- titleStyle: validateTextOptions(
140
+ titleStyle: normalizeLegendTextOptions(
141
141
  args.titleStyle,
142
142
  "createLegend.titleStyle",
143
143
  DEFAULT_TITLE
@@ -3,6 +3,7 @@ import { validateOptionObject } from "../../../core/validation.js";
3
3
  import { normalizeOptions } from "./categorical/options.js";
4
4
  import { symbolGraphic } from "./categorical/layout.js";
5
5
  import {
6
+ normalizeLegendTextOptions,
6
7
  normalizeContinuousLegend,
7
8
  validatePositive
8
9
  } from "./continuous/common.js";
@@ -12,6 +13,10 @@ import { findLayer } from "../../../selectors/layers.js";
12
13
  import { resolveLegendGraphicPlacement } from
13
14
  "../../../materialization/graphicHierarchy.js";
14
15
  import { resolveLegendTarget } from "./target.js";
16
+ import {
17
+ STROKE_WIDTH_LEGEND_LABELS,
18
+ STROKE_WIDTH_LEGEND_TITLE_STYLE
19
+ } from "./strokeWidth.js";
15
20
 
16
21
  const OPTIONS = Object.freeze([
17
22
  "target", "position", "align", "direction", "columns", "offset",
@@ -163,6 +168,61 @@ function editInterval(program, previous, args) {
163
168
  return next.rematerializeIntervalLegend();
164
169
  }
165
170
 
171
+ function editStrokeWidth(program, previous, args) {
172
+ const allowed = ["target", "title", "count", "labels", "titleStyle"];
173
+ for (const key of Object.keys(args)) {
174
+ if (!allowed.includes(key)) {
175
+ throw new Error(`stroke-width legend does not accept ${key}.`);
176
+ }
177
+ }
178
+ const count = args.count ?? previous.count;
179
+ if (!Number.isInteger(count) || count < 2) {
180
+ throw new RangeError(
181
+ "Stroke-width legend count must be an integer of at least 2."
182
+ );
183
+ }
184
+ const layer = findLayer(program, previous.target);
185
+ const titleMode = args.title;
186
+ const inferredTitle = titleMode === "auto"
187
+ ? true
188
+ : typeof titleMode === "string" ? false : previous.inferredTitle;
189
+ const titleVisible = titleMode === false
190
+ ? false
191
+ : titleMode === undefined ? previous.titleVisible !== false : true;
192
+ const title = titleMode === "auto"
193
+ ? layer?.encoding?.strokeWidth?.field
194
+ : typeof titleMode === "string" ? titleMode : previous.title;
195
+ const config = {
196
+ ...previous,
197
+ title,
198
+ inferredTitle,
199
+ titleVisible,
200
+ count,
201
+ labels: normalizeLegendTextOptions(
202
+ args.labels,
203
+ "editLegend.labels",
204
+ previous.labels ?? STROKE_WIDTH_LEGEND_LABELS
205
+ ),
206
+ titleStyle: normalizeLegendTextOptions(
207
+ args.titleStyle,
208
+ "editLegend.titleStyle",
209
+ previous.titleStyle ?? STROKE_WIDTH_LEGEND_TITLE_STYLE
210
+ )
211
+ };
212
+ let next = program;
213
+ if (titleMode === "auto" || typeof titleMode === "string") {
214
+ next = next.editSemantic({
215
+ property: "guide.legend.strokeWidth.title",
216
+ value: title
217
+ });
218
+ }
219
+ next = next._withLegendConfig("strokeWidth", config);
220
+ next = reconcileGraphic(next, "strokeWidthLegendTitle", titleVisible, {
221
+ type: "text"
222
+ });
223
+ return next.rematerializeStrokeWidthLegend();
224
+ }
225
+
166
226
  function categoricalSymbolIds(config) {
167
227
  return new Set(config.symbol.layers.map(layer => symbolGraphic(config, layer.type)));
168
228
  }
@@ -284,6 +344,9 @@ export const editLegend = action(
284
344
  if (configs.interval?.target === target) {
285
345
  return editInterval(this, configs.interval, args);
286
346
  }
347
+ if (configs.strokeWidth?.target === target) {
348
+ return editStrokeWidth(this, configs.strokeWidth, args);
349
+ }
287
350
  const continuousKind = ["gradient", "opacity"].find(
288
351
  kind => configs[kind]?.target === target
289
352
  );
@@ -1,6 +1,7 @@
1
1
  import { registerCategoricalLegendActions } from "./categorical/index.js";
2
2
  import { registerContinuousLegendActions } from "./continuous/index.js";
3
3
  import { registerSizeLegendActions } from "./size.js";
4
+ import { registerStrokeWidthLegendActions } from "./strokeWidth.js";
4
5
  import { registerFocusedLegendActions } from "./focused.js";
5
6
  import { removeLegend } from "./remove.js";
6
7
 
@@ -8,6 +9,7 @@ export function registerLegendActions(ProgramClass) {
8
9
  registerCategoricalLegendActions(ProgramClass);
9
10
  registerContinuousLegendActions(ProgramClass);
10
11
  registerSizeLegendActions(ProgramClass);
12
+ registerStrokeWidthLegendActions(ProgramClass);
11
13
  registerFocusedLegendActions(ProgramClass);
12
14
  ProgramClass.prototype.removeLegend = removeLegend;
13
15
  }