ggaction 0.0.5 → 0.0.7

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 (69) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +77 -14
  3. package/package.json +19 -2
  4. package/src/BasicChartProgram.js +10 -0
  5. package/src/actions/basic.js +21 -0
  6. package/src/actions/boxPlots/edit.js +455 -3
  7. package/src/actions/boxPlots/options.js +3 -3
  8. package/src/actions/canvas/actions.js +42 -0
  9. package/src/actions/canvas/index.js +5 -1
  10. package/src/actions/charts/basic.js +13 -0
  11. package/src/actions/composition/actions.js +13 -3
  12. package/src/actions/data/basic.js +11 -0
  13. package/src/actions/data/basicBin2d.js +43 -0
  14. package/src/actions/data/bin2d.js +138 -40
  15. package/src/actions/data/bin2dMaterialize.js +29 -0
  16. package/src/actions/data/index.js +6 -1
  17. package/src/actions/data/intervalEdit.js +76 -0
  18. package/src/actions/distributions/revision.js +326 -0
  19. package/src/actions/encodings/appearance.js +41 -3
  20. package/src/actions/encodings/basic.js +21 -0
  21. package/src/actions/encodings/density.js +215 -80
  22. package/src/actions/encodings/index.js +2 -0
  23. package/src/actions/encodings/position/index.js +5 -1
  24. package/src/actions/encodings/ranged.js +6 -2
  25. package/src/actions/encodings/remove.js +268 -0
  26. package/src/actions/errorBands/edit.js +121 -13
  27. package/src/actions/errorBars/edit.js +45 -8
  28. package/src/actions/errorBars/options.js +7 -1
  29. package/src/actions/facets/actions.js +136 -1
  30. package/src/actions/gradientPlots/edit.js +352 -4
  31. package/src/actions/guides/axes/edit.js +129 -47
  32. package/src/actions/guides/axes/index.js +10 -2
  33. package/src/actions/guides/basic.js +19 -0
  34. package/src/actions/guides/grids/grid.js +7 -3
  35. package/src/actions/guides/grids/index.js +1 -1
  36. package/src/actions/guides/legends/categorical/actions.js +19 -8
  37. package/src/actions/guides/legends/categorical/index.js +6 -2
  38. package/src/actions/guides/legends/continuous/common.js +3 -3
  39. package/src/actions/guides/legends/continuous/index.js +6 -2
  40. package/src/actions/guides/legends/edit.js +61 -3
  41. package/src/actions/guides/legends/remove.js +124 -23
  42. package/src/actions/guides/legends/strokeWidth.js +42 -14
  43. package/src/actions/marks/arc/actions.js +46 -14
  44. package/src/actions/marks/bar/index.js +5 -1
  45. package/src/actions/marks/basic.js +11 -0
  46. package/src/actions/marks/line/actions.js +5 -1
  47. package/src/actions/marks/line/index.js +4 -1
  48. package/src/actions/marks/point/edit.js +15 -3
  49. package/src/actions/marks/point/index.js +5 -1
  50. package/src/actions/marks/point/materialize.js +9 -3
  51. package/src/actions/marks/rect/actions.js +5 -1
  52. package/src/actions/marks/rect/index.js +4 -1
  53. package/src/actions/marks/rule/actions.js +4 -1
  54. package/src/actions/regression/edit.js +235 -74
  55. package/src/actions/scales/index.js +5 -1
  56. package/src/actions/selection/actions.js +122 -2
  57. package/src/actions/selection/index.js +6 -0
  58. package/src/basic.js +2 -0
  59. package/src/core/vocabulary.js +7 -0
  60. package/src/renderers/canvas/index.js +96 -60
  61. package/src/renderers/canvas/text.js +3 -14
  62. package/src/renderers/pdf.js +112 -0
  63. package/src/renderers/svg.js +559 -0
  64. package/src/renderers/text.js +11 -0
  65. package/types/basic.d.ts +107 -0
  66. package/types/index.d.ts +1 -0
  67. package/types/pdf.d.ts +26 -0
  68. package/types/program.d.ts +71 -7
  69. package/types/svg.d.ts +11 -0
@@ -360,14 +360,25 @@ export const createLegend = action(
360
360
  symbol
361
361
  });
362
362
  if (requestedPoint.encoding?.size?.scale !== undefined) {
363
- next = next.createSizeLegend({
364
- target: requestedPoint.id,
365
- ...(count === undefined ? {} : { count }),
366
- inheritAppearance:
367
- categoricalArgs.position === "left" ||
368
- categoricalArgs.labels !== undefined ||
369
- categoricalArgs.titleStyle !== undefined
370
- });
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
+ }
371
382
  next = next.rematerializeLegend();
372
383
  }
373
384
  return next;
@@ -25,9 +25,8 @@ import {
25
25
  } from "./symbols.js";
26
26
  import { editLegend } from "../edit.js";
27
27
 
28
- export function registerCategoricalLegendActions(ProgramClass) {
28
+ export function registerBasicCategoricalLegendActions(ProgramClass) {
29
29
  ProgramClass.prototype.createLegend = createLegend;
30
- ProgramClass.prototype.editLegend = editLegend;
31
30
  ProgramClass.prototype.createCategoricalLegend = createCategoricalLegend;
32
31
  ProgramClass.prototype.removeCategoricalLegend = removeCategoricalLegend;
33
32
  ProgramClass.prototype.createLegendBackground = createLegendBackground;
@@ -47,3 +46,8 @@ export function registerCategoricalLegendActions(ProgramClass) {
47
46
  ProgramClass.prototype.rematerializeLegendTitle = rematerializeLegendTitle;
48
47
  ProgramClass.prototype.rematerializeLegend = rematerializeLegend;
49
48
  }
49
+
50
+ export function registerCategoricalLegendActions(ProgramClass) {
51
+ registerBasicCategoricalLegendActions(ProgramClass);
52
+ ProgramClass.prototype.editLegend = editLegend;
53
+ }
@@ -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
@@ -15,11 +15,15 @@ export {
15
15
  export { createIntervalLegend, rematerializeIntervalLegend } from "./interval.js";
16
16
 
17
17
  export function registerContinuousLegendActions(ProgramClass) {
18
- ProgramClass.prototype.createGradientLegend = createGradientLegend;
19
- ProgramClass.prototype.rematerializeGradientLegend = rematerializeGradientLegend;
18
+ registerGradientLegendActions(ProgramClass);
20
19
  ProgramClass.prototype.createOpacityLegend = createOpacityLegend;
21
20
  ProgramClass.prototype.rematerializeOpacityLegend = rematerializeOpacityLegend;
22
21
  ProgramClass.prototype.removeOpacityLegend = removeOpacityLegend;
23
22
  ProgramClass.prototype.createIntervalLegend = createIntervalLegend;
24
23
  ProgramClass.prototype.rematerializeIntervalLegend = rematerializeIntervalLegend;
25
24
  }
25
+
26
+ export function registerGradientLegendActions(ProgramClass) {
27
+ ProgramClass.prototype.createGradientLegend = createGradientLegend;
28
+ ProgramClass.prototype.rematerializeGradientLegend = rematerializeGradientLegend;
29
+ }
@@ -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
  }
@@ -285,9 +345,7 @@ export const editLegend = action(
285
345
  return editInterval(this, configs.interval, args);
286
346
  }
287
347
  if (configs.strokeWidth?.target === target) {
288
- throw new Error(
289
- "editLegend does not yet support stroke-width legends; edit the owning scale instead."
290
- );
348
+ return editStrokeWidth(this, configs.strokeWidth, args);
291
349
  }
292
350
  const continuousKind = ["gradient", "opacity"].find(
293
351
  kind => configs[kind]?.target === target
@@ -2,38 +2,139 @@ import { action } from "../../../core/action.js";
2
2
  import { validateKeys } from "../../../core/validation.js";
3
3
  import { legendGraphicIds, legendResourcePolicy } from
4
4
  "../../../materialization/guides/resources.js";
5
+ import { LEGEND_CHANNELS } from "../../../core/vocabulary.js";
5
6
  import { resolveLegendTarget } from "./target.js";
6
7
 
7
- const OPTIONS = Object.freeze(["target"]);
8
+ const OPTIONS = Object.freeze(["target", "channels"]);
9
+
10
+ function legendKindChannels(kind, config) {
11
+ if (["series", "color"].includes(kind)) return config.channels;
12
+ return {
13
+ size: ["size"],
14
+ gradient: ["color"],
15
+ interval: ["color"],
16
+ opacity: ["opacity"],
17
+ strokeWidth: ["strokeWidth"]
18
+ }[kind];
19
+ }
20
+
21
+ function validateRequestedChannels(channels) {
22
+ if (!Array.isArray(channels)) {
23
+ throw new TypeError("removeLegend channels must be an array.");
24
+ }
25
+ if (channels.length === 0) {
26
+ throw new Error("removeLegend channels must select at least one channel.");
27
+ }
28
+ const seen = new Set();
29
+ for (const channel of channels) {
30
+ if (!LEGEND_CHANNELS.includes(channel)) {
31
+ throw new Error(`Unsupported legend channel "${channel}".`);
32
+ }
33
+ if (seen.has(channel)) {
34
+ throw new Error(`removeLegend channels contains duplicate "${channel}".`);
35
+ }
36
+ seen.add(channel);
37
+ }
38
+ }
39
+
40
+ function resolveRequestedKinds(program, target, channels) {
41
+ validateRequestedChannels(channels);
42
+ const requested = new Set(channels);
43
+ const matched = new Set();
44
+ const kinds = [];
45
+ for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
46
+ if (config?.target !== target) continue;
47
+ const owned = legendKindChannels(kind, config);
48
+ const overlap = owned.filter(channel => requested.has(channel));
49
+ if (overlap.length > 0 && overlap.length !== owned.length) {
50
+ throw new Error(
51
+ `Legend channels ${owned.map(channel => `"${channel}"`).join(", ")} ` +
52
+ "belong to one combined block and must be removed together."
53
+ );
54
+ }
55
+ if (overlap.length === owned.length) {
56
+ kinds.push(kind);
57
+ for (const channel of owned) matched.add(channel);
58
+ }
59
+ }
60
+ const missing = channels.filter(channel => !matched.has(channel));
61
+ if (missing.length > 0) {
62
+ throw new Error(
63
+ `Legend target "${target}" has no complete block for channel "${missing[0]}".`
64
+ );
65
+ }
66
+ return kinds;
67
+ }
68
+
69
+ export function removeLegendKinds(program, kinds) {
70
+ const removed = new Set(kinds);
71
+ const retainedSemanticKinds = new Set(
72
+ Object.entries(program.guideConfigs.legend ?? {})
73
+ .filter(([kind]) => !removed.has(kind))
74
+ .map(([kind]) => legendResourcePolicy(kind).semanticKind)
75
+ );
76
+ const semanticKinds = new Set(
77
+ kinds.map(kind => legendResourcePolicy(kind).semanticKind)
78
+ );
79
+ let next = program;
80
+ for (const kind of semanticKinds) {
81
+ if (
82
+ !retainedSemanticKinds.has(kind) &&
83
+ next.semanticSpec.guides.legend?.[kind] !== undefined
84
+ ) {
85
+ next = next.editSemantic({
86
+ property: `guide.legend.${kind}`,
87
+ remove: true
88
+ });
89
+ }
90
+ }
91
+ for (const id of new Set(kinds.flatMap(legendGraphicIds))) {
92
+ if (next.graphicSpec.objects[id] !== undefined) {
93
+ next = next.editGraphics({ target: id, remove: true });
94
+ }
95
+ }
96
+ for (const kind of kinds) {
97
+ next = next._withoutMaterializationConfig(["guides", "legend", kind]);
98
+ }
99
+ return next;
100
+ }
8
101
 
9
102
  export const removeLegend = action(
10
103
  { op: "removeLegend", description: "Remove every legend block owned by one mark." },
11
104
  function (args = {}) {
12
105
  validateKeys(args, OPTIONS, "removeLegend");
13
106
  const target = resolveLegendTarget(this, args.target, "removeLegend");
14
- const kinds = Object.entries(this.guideConfigs.legend ?? {})
107
+ const targetKinds = Object.entries(this.guideConfigs.legend ?? {})
15
108
  .filter(([, config]) => config?.target === target)
16
109
  .map(([kind]) => kind);
17
- const semanticKinds = new Set(
18
- kinds.map(kind => legendResourcePolicy(kind).semanticKind)
19
- );
20
- let next = this;
21
- for (const kind of semanticKinds) {
22
- if (next.semanticSpec.guides.legend?.[kind] !== undefined) {
23
- next = next.editSemantic({
24
- property: `guide.legend.${kind}`,
25
- remove: true
26
- });
27
- }
28
- }
29
- for (const id of new Set(kinds.flatMap(legendGraphicIds))) {
30
- if (next.graphicSpec.objects[id] !== undefined) {
31
- next = next.editGraphics({ target: id, remove: true });
32
- }
33
- }
34
- for (const kind of kinds) {
35
- next = next._withoutMaterializationConfig(["guides", "legend", kind]);
36
- }
37
- return next;
110
+ const kinds = args.channels === undefined
111
+ ? targetKinds
112
+ : resolveRequestedKinds(this, target, args.channels);
113
+ let next = removeLegendKinds(this, kinds);
114
+ const retainedKinds = Object.keys(next.guideConfigs.legend ?? {});
115
+ if (args.channels === undefined) {
116
+ const removedSemanticKinds = new Set(
117
+ kinds.map(kind => legendResourcePolicy(kind).semanticKind)
118
+ );
119
+ const hasSharedSemanticKind = retainedKinds.some(kind =>
120
+ removedSemanticKinds.has(legendResourcePolicy(kind).semanticKind)
121
+ );
122
+ return hasSharedSemanticKind ? next.rematerializeLegend() : next;
123
+ }
124
+ const remainingTargetKinds = targetKinds.filter(kind => !kinds.includes(kind));
125
+ const removedCategorical = kinds.some(kind => ["series", "color"].includes(kind));
126
+ if (
127
+ removedCategorical &&
128
+ remainingTargetKinds.includes("size") &&
129
+ next.guideConfigs.legend.size.inheritAppearance === true
130
+ ) {
131
+ next = next._withLegendConfig("size", {
132
+ ...next.guideConfigs.legend.size,
133
+ inheritAppearance: false
134
+ });
135
+ }
136
+ return retainedKinds.length === 0
137
+ ? next
138
+ : next.rematerializeLegend();
38
139
  }
39
140
  );
@@ -10,6 +10,21 @@ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
10
10
 
11
11
  const OPTIONS = Object.freeze(["target", "count"]);
12
12
 
13
+ export const STROKE_WIDTH_LEGEND_LABELS = Object.freeze({
14
+ offset: 12,
15
+ color: DEFAULT_COLORS.text,
16
+ fontSize: 12,
17
+ fontFamily: DEFAULT_FONT_FAMILY,
18
+ fontWeight: "normal"
19
+ });
20
+
21
+ export const STROKE_WIDTH_LEGEND_TITLE_STYLE = Object.freeze({
22
+ color: DEFAULT_COLORS.strongText,
23
+ fontSize: 13,
24
+ fontFamily: DEFAULT_FONT_FAMILY,
25
+ fontWeight: 600
26
+ });
27
+
13
28
  export function isStrokeWidthLegendLayer(layer) {
14
29
  return ["line", "rule"].includes(layer?.mark?.type) &&
15
30
  layer.encoding?.strokeWidth?.scale !== undefined;
@@ -41,14 +56,12 @@ function requireScale(program, id) {
41
56
  return scale;
42
57
  }
43
58
 
44
- function styleText(program, id, { title = false } = {}) {
59
+ function styleText(program, id, style) {
45
60
  return program
46
- .editGraphics({ target: id, property: "fill", value: title
47
- ? DEFAULT_COLORS.strongText
48
- : DEFAULT_COLORS.text })
49
- .editGraphics({ target: id, property: "fontSize", value: title ? 13 : 12 })
50
- .editGraphics({ target: id, property: "fontFamily", value: DEFAULT_FONT_FAMILY })
51
- .editGraphics({ target: id, property: "fontWeight", value: title ? 600 : "normal" })
61
+ .editGraphics({ target: id, property: "fill", value: style.color })
62
+ .editGraphics({ target: id, property: "fontSize", value: style.fontSize })
63
+ .editGraphics({ target: id, property: "fontFamily", value: style.fontFamily })
64
+ .editGraphics({ target: id, property: "fontWeight", value: style.fontWeight })
52
65
  .editGraphics({ target: id, property: "textAlign", value: "left" })
53
66
  .editGraphics({ target: id, property: "textBaseline", value: "middle" });
54
67
  }
@@ -60,10 +73,16 @@ export const rematerializeStrokeWidthLegend = action(
60
73
  },
61
74
  function (args = {}) {
62
75
  validateKeys(args, [], "rematerializeStrokeWidthLegend");
63
- const config = this.guideConfigs.legend?.strokeWidth;
64
- if (config === undefined) {
76
+ const stored = this.guideConfigs.legend?.strokeWidth;
77
+ if (stored === undefined) {
65
78
  throw new Error("Stroke-width legend requires stored configuration.");
66
79
  }
80
+ const config = {
81
+ ...stored,
82
+ labels: stored.labels ?? { ...STROKE_WIDTH_LEGEND_LABELS },
83
+ titleStyle: stored.titleStyle ?? { ...STROKE_WIDTH_LEGEND_TITLE_STYLE },
84
+ titleVisible: stored.titleVisible !== false
85
+ };
67
86
  const layer = findLayer(this, config.target);
68
87
  const encoding = layer?.encoding?.strokeWidth;
69
88
  if (encoding?.scale === undefined) {
@@ -101,18 +120,24 @@ export const rematerializeStrokeWidthLegend = action(
101
120
  .editGraphics({ target: "strokeWidthLegendSymbols", property: "stroke", value: DEFAULT_COLORS.mark })
102
121
  .editGraphics({ target: "strokeWidthLegendSymbols", property: "strokeWidth", value: widths })
103
122
  .editGraphics({ target: "strokeWidthLegendLabels", property: "length", value: values.length })
104
- .editGraphics({ target: "strokeWidthLegendLabels", property: "x", value: values.map(() => originX + 44) })
123
+ .editGraphics({
124
+ target: "strokeWidthLegendLabels",
125
+ property: "x",
126
+ value: values.map(() => originX + 32 + config.labels.offset)
127
+ })
105
128
  .editGraphics({ target: "strokeWidthLegendLabels", property: "y", value: y })
106
129
  .editGraphics({
107
130
  target: "strokeWidthLegendLabels",
108
131
  property: "text",
109
132
  value: values.map(value => String(+value.toPrecision(3)))
110
- })
133
+ });
134
+ next = styleText(next, "strokeWidthLegendLabels", config.labels);
135
+ if (config.titleVisible === false) return next;
136
+ next = next
111
137
  .editGraphics({ target: "strokeWidthLegendTitle", property: "x", value: originX })
112
138
  .editGraphics({ target: "strokeWidthLegendTitle", property: "y", value: titleY })
113
139
  .editGraphics({ target: "strokeWidthLegendTitle", property: "text", value: title });
114
- next = styleText(next, "strokeWidthLegendLabels");
115
- return styleText(next, "strokeWidthLegendTitle", { title: true });
140
+ return styleText(next, "strokeWidthLegendTitle", config.titleStyle);
116
141
  }
117
142
  );
118
143
 
@@ -140,7 +165,10 @@ export const createStrokeWidthLegend = action(
140
165
  scale: encoding.scale,
141
166
  title: encoding.field,
142
167
  inferredTitle: true,
143
- count
168
+ count,
169
+ labels: { ...STROKE_WIDTH_LEGEND_LABELS },
170
+ titleStyle: { ...STROKE_WIDTH_LEGEND_TITLE_STYLE },
171
+ titleVisible: true
144
172
  })
145
173
  .createGraphics({
146
174
  id: "strokeWidthLegendSymbols",
@@ -46,8 +46,8 @@ function validateInnerRadius(value) {
46
46
  return value;
47
47
  }
48
48
 
49
- function normalizeConfig(args, previous = {}) {
50
- return {
49
+ function normalizeConfig(args, previous = {}, { allowStrokeRemoval = false } = {}) {
50
+ let config = {
51
51
  ...previous,
52
52
  ...(Object.hasOwn(args, "innerRadius")
53
53
  ? { innerRadius: validateInnerRadius(args.innerRadius) }
@@ -61,18 +61,28 @@ function normalizeConfig(args, previous = {}) {
61
61
  ...(Object.hasOwn(args, "opacity")
62
62
  ? { opacity: validateUnitInterval(args.opacity, "Arc opacity") }
63
63
  : {}),
64
- ...(Object.hasOwn(args, "stroke")
64
+ ...(Object.hasOwn(args, "stroke") && args.stroke !== false
65
65
  ? { stroke: validateNonEmptyString(args.stroke, "Arc stroke") }
66
- : {}),
67
- ...(Object.hasOwn(args, "strokeWidth")
68
- ? {
69
- strokeWidth: validateNonNegativeFinite(
70
- args.strokeWidth,
71
- "Arc strokeWidth"
72
- )
73
- }
74
66
  : {})
75
67
  };
68
+ if (args.stroke === false) {
69
+ if (!allowStrokeRemoval) {
70
+ throw new TypeError("Arc stroke must be a non-empty string.");
71
+ }
72
+ config.stroke = false;
73
+ delete config.strokeWidth;
74
+ } else if (Object.hasOwn(args, "stroke")) {
75
+ if (previous.stroke === false && !Object.hasOwn(args, "strokeWidth")) {
76
+ config.strokeWidth = 1;
77
+ }
78
+ }
79
+ if (Object.hasOwn(args, "strokeWidth")) {
80
+ config.strokeWidth = validateNonNegativeFinite(
81
+ args.strokeWidth,
82
+ "Arc strokeWidth"
83
+ );
84
+ }
85
+ return config;
76
86
  }
77
87
 
78
88
  const createArcMark = action(
@@ -189,8 +199,16 @@ const rematerializeArcMark = action(
189
199
  .editGraphics({ target: id, property: "commands", value: commands })
190
200
  .editGraphics({ target: id, property: "fill", value: fills })
191
201
  .editGraphics({ target: id, property: "opacity", value: config.opacity ?? 1 })
192
- .editGraphics({ target: id, property: "stroke", value: config.stroke ?? "#ffffff" })
193
- .editGraphics({ target: id, property: "strokeWidth", value: config.strokeWidth ?? 1 })
202
+ .editGraphics({
203
+ target: id,
204
+ property: "stroke",
205
+ value: config.stroke === false ? "transparent" : config.stroke ?? "#ffffff"
206
+ })
207
+ .editGraphics({
208
+ target: id,
209
+ property: "strokeWidth",
210
+ value: config.stroke === false ? 0 : config.strokeWidth ?? 1
211
+ })
194
212
  .editGraphics({
195
213
  target: id,
196
214
  property: "strokeDash",
@@ -222,9 +240,23 @@ const editArcMark = action(
222
240
  if (Object.hasOwn(args, "fill") && layer.encoding?.color !== undefined) {
223
241
  throw new Error("editArcMark fill cannot be combined with a color encoding.");
224
242
  }
243
+ if (args.stroke === false && Object.hasOwn(args, "strokeWidth")) {
244
+ throw new Error(
245
+ "editArcMark cannot set strokeWidth while removing stroke."
246
+ );
247
+ }
248
+ if (
249
+ Object.hasOwn(args, "strokeWidth") &&
250
+ !Object.hasOwn(args, "stroke") &&
251
+ this.markConfigs[layer.id]?.stroke === false
252
+ ) {
253
+ throw new Error("editArcMark strokeWidth requires an active stroke.");
254
+ }
225
255
  const next = this._withMarkConfig(
226
256
  layer.id,
227
- normalizeConfig(args, this.markConfigs[layer.id])
257
+ normalizeConfig(args, this.markConfigs[layer.id], {
258
+ allowStrokeRemoval: true
259
+ })
228
260
  );
229
261
  return canMaterializeArc(next, layer)
230
262
  ? next.rematerializeArcMark({ id: layer.id })
@@ -3,7 +3,11 @@ import { editBarMark } from "./edit.js";
3
3
  import { rematerializeBarMark } from "./materialize.js";
4
4
 
5
5
  export function registerBarMarkActions(ProgramClass) {
6
- ProgramClass.prototype.createBarMark = createBarMark;
7
6
  ProgramClass.prototype.editBarMark = editBarMark;
7
+ registerBasicBarMarkActions(ProgramClass);
8
+ }
9
+
10
+ export function registerBasicBarMarkActions(ProgramClass) {
11
+ ProgramClass.prototype.createBarMark = createBarMark;
8
12
  ProgramClass.prototype.rematerializeBarMark = rematerializeBarMark;
9
13
  }
@@ -0,0 +1,11 @@
1
+ import { registerBarMarkActions } from "./bar/index.js";
2
+ import { registerLineMarkActions } from "./line/index.js";
3
+ import { registerPointMarkActions } from "./point/index.js";
4
+ import { registerRectMarkActions } from "./rect/index.js";
5
+
6
+ export function registerBasicMarkActions(ProgramClass) {
7
+ registerPointMarkActions(ProgramClass);
8
+ registerLineMarkActions(ProgramClass);
9
+ registerBarMarkActions(ProgramClass);
10
+ registerRectMarkActions(ProgramClass);
11
+ }
@@ -386,7 +386,11 @@ const editLineMark = action(
386
386
  );
387
387
 
388
388
  export function registerLineMarkActions(ProgramClass) {
389
- ProgramClass.prototype.createLineMark = createLineMark;
390
389
  ProgramClass.prototype.editLineMark = editLineMark;
390
+ registerBasicLineMarkActions(ProgramClass);
391
+ }
392
+
393
+ export function registerBasicLineMarkActions(ProgramClass) {
394
+ ProgramClass.prototype.createLineMark = createLineMark;
391
395
  ProgramClass.prototype.rematerializeLineMark = rematerializeLineMark;
392
396
  }
@@ -1 +1,4 @@
1
- export { registerLineMarkActions } from "./actions.js";
1
+ export {
2
+ registerBasicLineMarkActions,
3
+ registerLineMarkActions
4
+ } from "./actions.js";
@@ -61,12 +61,24 @@ export const editPointMark = action(
61
61
  if (Object.hasOwn(args, "opacity")) {
62
62
  config.opacity = validateUnitInterval(args.opacity, "Point opacity");
63
63
  }
64
+ if (args.stroke === false && Object.hasOwn(args, "strokeWidth")) {
65
+ throw new Error(
66
+ "editPointMark cannot set strokeWidth while removing stroke."
67
+ );
68
+ }
64
69
  if (Object.hasOwn(args, "stroke")) {
65
- config.stroke = validateNonEmptyString(args.stroke, "Point stroke");
66
- config.strokeWidth ??= 1;
70
+ if (args.stroke === false) {
71
+ config.stroke = false;
72
+ delete config.strokeWidth;
73
+ } else {
74
+ const restoresStroke = config.stroke === false;
75
+ config.stroke = validateNonEmptyString(args.stroke, "Point stroke");
76
+ if (restoresStroke) config.strokeWidth = 1;
77
+ else config.strokeWidth ??= 1;
78
+ }
67
79
  }
68
80
  if (Object.hasOwn(args, "strokeWidth")) {
69
- if (config.stroke === undefined) {
81
+ if (typeof config.stroke !== "string") {
70
82
  throw new Error("Point strokeWidth requires an active stroke.");
71
83
  }
72
84
  config.strokeWidth = validateNonNegativeFinite(
@@ -5,7 +5,11 @@ import { rematerializePointMark } from "./materialize.js";
5
5
  export { registerPointJitterActions } from "./jitter.js";
6
6
 
7
7
  export function registerPointMarkActions(ProgramClass) {
8
- ProgramClass.prototype.createPointMark = createPointMark;
9
8
  ProgramClass.prototype.editPointMark = editPointMark;
9
+ registerBasicPointMarkActions(ProgramClass);
10
+ }
11
+
12
+ export function registerBasicPointMarkActions(ProgramClass) {
13
+ ProgramClass.prototype.createPointMark = createPointMark;
10
14
  ProgramClass.prototype.rematerializePointMark = rematerializePointMark;
11
15
  }
@@ -129,7 +129,9 @@ function resolveJitterEntries({
129
129
  const extent = resolvePointShapeExtent({
130
130
  shape: shapes[index],
131
131
  area: resolvedArea,
132
- strokeWidth: config.stroke === undefined ? 0 : config.strokeWidth ?? 1
132
+ strokeWidth: typeof config.stroke === "string"
133
+ ? config.strokeWidth ?? 1
134
+ : 0
133
135
  });
134
136
  return [{
135
137
  index,
@@ -289,7 +291,7 @@ export const rematerializePointMark = action(
289
291
  y: centerY,
290
292
  area: resolvedArea,
291
293
  fill: color,
292
- ...(config.stroke === undefined ? {} : { stroke: config.stroke }),
294
+ ...(typeof config.stroke === "string" ? { stroke: config.stroke } : {}),
293
295
  ...(config.strokeWidth === undefined
294
296
  ? {}
295
297
  : { strokeWidth: config.strokeWidth }),
@@ -371,7 +373,11 @@ export const rematerializePointMark = action(
371
373
  } else if (encodedOpacity !== undefined) {
372
374
  next = next.editGraphics({ target: id, property: "opacity", value: encodedOpacity });
373
375
  }
374
- if (config.stroke !== undefined) {
376
+ if (config.stroke === false) {
377
+ next = next
378
+ .editGraphics({ target: id, property: "stroke", value: "transparent" })
379
+ .editGraphics({ target: id, property: "strokeWidth", value: 0 });
380
+ } else if (config.stroke !== undefined) {
375
381
  next = next.editGraphics({ target: id, property: "stroke", value: config.stroke });
376
382
  next = next.editGraphics({
377
383
  target: id,