ggaction 0.0.7 → 0.0.8

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 (61) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/actions/categoryOrder/index.js +111 -0
  5. package/src/actions/data/index.js +6 -0
  6. package/src/actions/data/timeUnit.js +48 -0
  7. package/src/actions/encodings/angle.js +77 -0
  8. package/src/actions/encodings/color/index.js +17 -13
  9. package/src/actions/encodings/color/layout.js +2 -0
  10. package/src/actions/encodings/color/policy.js +3 -0
  11. package/src/actions/encodings/index.js +2 -0
  12. package/src/actions/encodings/position/policies/area.js +40 -2
  13. package/src/actions/encodings/position/policies/bar.js +6 -0
  14. package/src/actions/encodings/position/policies/index.js +2 -0
  15. package/src/actions/encodings/position/policies/tick.js +19 -0
  16. package/src/actions/encodings/remove.js +1 -1
  17. package/src/actions/guides/legends/categorical/actions.js +23 -6
  18. package/src/actions/guides/legends/categorical/index.js +8 -0
  19. package/src/actions/guides/legends/continuous/common.js +17 -5
  20. package/src/actions/guides/legends/continuous/gradient.js +12 -8
  21. package/src/actions/guides/legends/continuous/opacity.js +70 -17
  22. package/src/actions/guides/legends/edit.js +13 -8
  23. package/src/actions/guides/legends/lane.js +468 -0
  24. package/src/actions/guides/legends/remove.js +3 -7
  25. package/src/actions/index.js +2 -0
  26. package/src/actions/marks/area/actions.js +3 -1
  27. package/src/actions/marks/area/materialize.js +12 -3
  28. package/src/actions/marks/index.js +2 -0
  29. package/src/actions/marks/point/materialize.js +17 -5
  30. package/src/actions/marks/tick/actions.js +225 -0
  31. package/src/actions/marks/tick/index.js +1 -0
  32. package/src/actions/primitives/semanticValidation/layer.js +2 -0
  33. package/src/actions/scales/consumers/index.js +8 -0
  34. package/src/actions/scales/consumers/seriesLayout.js +22 -2
  35. package/src/actions/scales/materialize.js +2 -0
  36. package/src/actions/selection/actions.js +5 -2
  37. package/src/core/vocabulary.js +7 -3
  38. package/src/grammar/areaSeries.js +112 -2
  39. package/src/grammar/categoryOrder.js +138 -0
  40. package/src/grammar/direction.js +46 -0
  41. package/src/grammar/facets/index.js +1 -1
  42. package/src/grammar/pointShapes.js +34 -6
  43. package/src/grammar/positionCompatibility.js +4 -0
  44. package/src/grammar/schemas/semanticPath.js +2 -1
  45. package/src/grammar/seriesLayout.js +10 -2
  46. package/src/grammar/timeUnit.js +108 -0
  47. package/src/grammar/transforms.js +6 -0
  48. package/src/grammar/window.js +73 -7
  49. package/src/layout/legendLane.js +338 -0
  50. package/src/materialization/marks/capabilities.js +9 -0
  51. package/src/materialization/marks/index.js +2 -1
  52. package/src/materialization/marks/policies.js +13 -0
  53. package/src/materialization/scales/policies/series.js +9 -0
  54. package/src/materialization/scales/resolve.js +25 -3
  55. package/src/materialization/selection/items/index.js +1 -0
  56. package/src/materialization/selection/items/path.js +4 -1
  57. package/src/materialization/selection/items/tick.js +42 -0
  58. package/src/materialization/selection/policies/index.js +2 -0
  59. package/src/materialization/selection/policies/tick.js +10 -0
  60. package/types/index.d.ts +10 -0
  61. package/types/program.d.ts +96 -3
@@ -105,12 +105,21 @@ export function normalizeContinuousLegend(args, kind) {
105
105
  }
106
106
  const offset = args.offset ?? 30;
107
107
  validateNonNegative(offset, "Legend offset");
108
- const itemGap = args.itemGap ?? 28;
108
+ const titlePosition = args.titlePosition ?? "top";
109
+ if (!["top", "left"].includes(titlePosition)) {
110
+ throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
111
+ }
112
+ if (
113
+ titlePosition === "left" &&
114
+ (!["top", "bottom"].includes(position) || kind !== "opacity")
115
+ ) {
116
+ throw new Error(
117
+ "Only horizontal opacity legends support left titlePosition."
118
+ );
119
+ }
120
+ const itemGap = args.itemGap ?? (titlePosition === "left" ? 20 : 28);
109
121
  validatePositive(itemGap, "Legend itemGap");
110
122
  if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
111
- if (args.titlePosition !== undefined && args.titlePosition !== "top") {
112
- throw new Error("Continuous legends currently require top titlePosition.");
113
- }
114
123
  if (kind === "gradient") {
115
124
  for (const key of ["symbol", "columns", "direction", "itemGap"]) {
116
125
  if (Object.hasOwn(args, key)) {
@@ -135,7 +144,9 @@ export function normalizeContinuousLegend(args, kind) {
135
144
  labels: normalizeLegendTextOptions(
136
145
  args.labels,
137
146
  "createLegend.labels",
138
- DEFAULT_LABELS
147
+ titlePosition === "left"
148
+ ? { ...DEFAULT_LABELS, offset: 8 }
149
+ : DEFAULT_LABELS
139
150
  ),
140
151
  titleStyle: normalizeLegendTextOptions(
141
152
  args.titleStyle,
@@ -143,6 +154,7 @@ export function normalizeContinuousLegend(args, kind) {
143
154
  DEFAULT_TITLE
144
155
  ),
145
156
  itemGap,
157
+ titlePosition,
146
158
  border: normalizeBorder(args.border)
147
159
  };
148
160
  }
@@ -39,12 +39,18 @@ function resolveGradientLayout(program, config, scale) {
39
39
  : config.align === "right" ? plot.x + plot.width - length
40
40
  : plot.x + (plot.width - length) / 2;
41
41
  y = config.position === "top"
42
- ? plot.y - config.offset - thickness
43
- : plot.y + plot.height + config.offset;
42
+ ? plot.y - config.offset - thickness - config.labels.offset -
43
+ config.labels.fontSize
44
+ : plot.y + plot.height + config.offset +
45
+ config.titleStyle.fontSize + 12;
44
46
  }
45
47
  const title = vertical
46
48
  ? { x, y: plot.y + 20, align: "left" }
47
- : { x: x + length / 2, y: y - 20, align: "center" };
49
+ : {
50
+ x: x + length / 2,
51
+ y: y - 12 - config.titleStyle.fontSize / 2,
52
+ align: "center"
53
+ };
48
54
  const values = sampleContinuousValues(scale.domain, config.count);
49
55
  const fractions = values.map((_, index) => index / (values.length - 1));
50
56
  const labelOffset = config.labels.offset;
@@ -58,9 +64,7 @@ function resolveGradientLayout(program, config, scale) {
58
64
  }))
59
65
  : fractions.map(fraction => ({
60
66
  x: x + length * fraction,
61
- y: config.position === "top"
62
- ? y - labelOffset
63
- : y + thickness + labelOffset,
67
+ y: y + thickness + labelOffset + config.labels.fontSize / 2,
64
68
  align: "center"
65
69
  }));
66
70
  const ticks = vertical
@@ -72,9 +76,9 @@ function resolveGradientLayout(program, config, scale) {
72
76
  }))
73
77
  : labels.map(label => ({
74
78
  x1: label.x,
75
- y1: config.position === "top" ? y : y + thickness,
79
+ y1: y + thickness,
76
80
  x2: label.x,
77
- y2: config.position === "top" ? y - 6 : y + thickness + 6
81
+ y2: y + thickness + 6
78
82
  }));
79
83
  assertLegendInsideCanvas([
80
84
  title,
@@ -2,6 +2,7 @@ import { action } from "../../../../core/action.js";
2
2
  import { isPlainObject } from "../../../../core/immutable.js";
3
3
  import { validateKeys } from "../../../../core/validation.js";
4
4
  import { mapLinearValues } from "../../../../grammar/scales/index.js";
5
+ import { measureTextWidth } from "../../../../core/textMetrics.js";
5
6
  import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
6
7
  import {
7
8
  assertLegendInsideCanvas,
@@ -84,6 +85,7 @@ function resolveOpacityLayout(program, config, scale) {
84
85
  const { plot, canvas } = resolveContinuousBounds(program);
85
86
  const vertical = ["right", "left"].includes(config.position);
86
87
  const values = sampleContinuousValues(scale.domain, config.count);
88
+ const texts = formatContinuousValues(values, scale.domain, "quantitative");
87
89
  const radius = config.symbol.radius;
88
90
  let symbols;
89
91
  let labels;
@@ -109,29 +111,81 @@ function resolveOpacityLayout(program, config, scale) {
109
111
  y: plot.y + 20,
110
112
  align: config.position === "right" ? "left" : "right"
111
113
  };
114
+ } else if (config.titlePosition === "left") {
115
+ const titleWidth = measureTextWidth(config.title, config.titleStyle);
116
+ const labelWidths = texts.map(text =>
117
+ measureTextWidth(text, config.labels)
118
+ );
119
+ const samplesWidth = labelWidths.reduce(
120
+ (sum, width) => sum + radius * 2 + config.labels.offset + width,
121
+ 0
122
+ ) + config.itemGap * (values.length - 1);
123
+ const width = titleWidth + 20 + samplesWidth;
124
+ const startX = config.align === "left" ? plot.x
125
+ : config.align === "right" ? plot.x + plot.width - width
126
+ : plot.x + (plot.width - width) / 2;
127
+ const height = Math.max(
128
+ radius * 2,
129
+ config.labels.fontSize,
130
+ config.titleStyle.fontSize
131
+ );
132
+ const centerY = config.position === "top"
133
+ ? plot.y - config.offset - height / 2
134
+ : plot.y + plot.height + config.offset + height / 2;
135
+ title = { x: startX, y: centerY, align: "left" };
136
+ let cursor = startX + titleWidth + 20;
137
+ symbols = [];
138
+ labels = [];
139
+ for (let index = 0; index < values.length; index += 1) {
140
+ const symbolX = cursor + radius;
141
+ symbols.push({ x: symbolX, y: centerY });
142
+ const labelX = symbolX + radius + config.labels.offset;
143
+ labels.push({ x: labelX, y: centerY, align: "left" });
144
+ cursor = labelX + labelWidths[index] + config.itemGap;
145
+ }
112
146
  } else {
113
147
  const width = (values.length - 1) * Math.max(56, config.itemGap * 2);
114
148
  const startX = config.align === "left" ? plot.x
115
149
  : config.align === "right" ? plot.x + plot.width - width
116
150
  : plot.x + (plot.width - width) / 2;
151
+ const titleHalf = config.titleStyle.fontSize / 2;
152
+ const labelHalf = config.labels.fontSize / 2;
117
153
  const y = config.position === "top"
118
- ? plot.y - config.offset - radius
119
- : plot.y + plot.height + config.offset + radius;
154
+ ? plot.y - config.offset - config.labels.fontSize -
155
+ config.labels.offset - radius
156
+ : plot.y + plot.height + config.offset +
157
+ config.titleStyle.fontSize + 12 + radius;
120
158
  symbols = values.map((_, index) => ({
121
159
  x: startX + index * width / (values.length - 1),
122
160
  y
123
161
  }));
124
162
  labels = symbols.map(symbol => ({
125
163
  x: symbol.x,
126
- y: config.position === "top"
127
- ? symbol.y - radius - config.labels.offset
128
- : symbol.y + radius + config.labels.offset,
164
+ y: symbol.y + radius + config.labels.offset + labelHalf,
129
165
  align: "center"
130
166
  }));
131
- title = { x: startX + width / 2, y: y - 26, align: "center" };
167
+ title = {
168
+ x: startX + width / 2,
169
+ y: y - radius - 12 - titleHalf,
170
+ align: "center"
171
+ };
132
172
  }
173
+ const titleWidth = measureTextWidth(config.title, config.titleStyle);
174
+ const titleEdge = {
175
+ x: title.x + (title.align === "left" ? titleWidth : -titleWidth),
176
+ y: title.y
177
+ };
178
+ const labelEdges = labels.map((label, index) => {
179
+ const width = measureTextWidth(texts[index], config.labels);
180
+ return {
181
+ x: label.x + (label.align === "left"
182
+ ? width
183
+ : label.align === "right" ? -width : width / 2),
184
+ y: label.y
185
+ };
186
+ });
133
187
  assertLegendInsideCanvas(
134
- [title, ...symbols, ...labels],
188
+ [title, titleEdge, ...symbols, ...labels, ...labelEdges],
135
189
  canvas,
136
190
  "Opacity legend layout"
137
191
  );
@@ -145,14 +199,17 @@ function resolveOpacityLayout(program, config, scale) {
145
199
  x: symbol.x - radius,
146
200
  y: symbol.y - radius
147
201
  })),
148
- ...labels.map(label => ({
149
- x: label.x + (
150
- label.align === "left" ? 42 : label.align === "right" ? -42 : 0
151
- ),
202
+ ...labels.map((label, index) => ({
203
+ x: label.x + (label.align === "left"
204
+ ? measureTextWidth(
205
+ texts[index],
206
+ config.labels
207
+ )
208
+ : label.align === "right" ? -42 : 0),
152
209
  y: label.y
153
210
  }))
154
211
  ], config.border, canvas, "Opacity legend");
155
- return { values, symbols, labels, title, background };
212
+ return { values, texts, symbols, labels, title, background };
156
213
  }
157
214
 
158
215
  export const rematerializeOpacityLegend = action(
@@ -229,11 +286,7 @@ export const rematerializeOpacityLegend = action(
229
286
  .editGraphics({
230
287
  target: "opacityLegendLabels",
231
288
  property: "text",
232
- value: formatContinuousValues(
233
- layout.values,
234
- scale.domain,
235
- "quantitative"
236
- )
289
+ value: layout.texts
237
290
  });
238
291
  next = editLegendBackground(
239
292
  next,
@@ -54,7 +54,7 @@ function editContinuous(program, kind, previous, args) {
54
54
  ? ["target", "position", "align", "offset", "title", "labels",
55
55
  "titleStyle", "border", "count", "gradient"]
56
56
  : ["target", "position", "align", "offset", "title", "symbol", "labels",
57
- "titleStyle", "itemGap", "border", "count"];
57
+ "titleStyle", "titlePosition", "itemGap", "border", "count"];
58
58
  for (const key of Object.keys(args)) {
59
59
  if (!allowed.includes(key)) throw new Error(`${kind} legend does not accept ${key}.`);
60
60
  }
@@ -69,18 +69,25 @@ function editContinuous(program, kind, previous, args) {
69
69
  const title = titleMode === "auto"
70
70
  ? inferred
71
71
  : typeof titleMode === "string" ? titleMode : previous.title;
72
+ const titlePosition = args.titlePosition ?? previous.titlePosition;
73
+ const enteringInline = kind === "opacity" &&
74
+ titlePosition === "left" && previous.titlePosition !== "left";
75
+ const labels = mergeObject(previous.labels, args.labels);
72
76
  const normalized = normalizeContinuousLegend({
73
77
  target: previous.target,
74
78
  position: args.position ?? previous.position,
75
79
  align: args.align ?? previous.align,
76
80
  offset: args.offset ?? previous.offset,
77
81
  count: args.count ?? previous.count,
82
+ titlePosition,
78
83
  title,
79
- labels: mergeObject(previous.labels, args.labels),
84
+ labels: enteringInline && args.labels?.offset === undefined
85
+ ? { ...labels, offset: 8 }
86
+ : labels,
80
87
  titleStyle: mergeObject(previous.titleStyle, args.titleStyle),
81
88
  border: mergeBorder(previous.border, args.border),
82
89
  ...(kind === "opacity" ? {
83
- itemGap: args.itemGap ?? previous.itemGap,
90
+ itemGap: args.itemGap ?? (enteringInline ? 20 : previous.itemGap),
84
91
  symbol: args.symbol ?? previous.symbol
85
92
  } : {})
86
93
  }, kind);
@@ -112,9 +119,7 @@ function editContinuous(program, kind, previous, args) {
112
119
  before: kind === "gradient" ? "colorGradientStrips" : "opacityLegendSymbols"
113
120
  });
114
121
  next = reconcileGraphic(next, `${prefix}Title`, titleVisible, { type: "text" });
115
- return kind === "gradient"
116
- ? next.rematerializeGradientLegend()
117
- : next.rematerializeOpacityLegend();
122
+ return next.rematerializeLegend();
118
123
  }
119
124
 
120
125
  function editInterval(program, previous, args) {
@@ -165,7 +170,7 @@ function editInterval(program, previous, args) {
165
170
  next = reconcileGraphic(next, "colorLegendTitle", titleVisible, {
166
171
  type: "text"
167
172
  });
168
- return next.rematerializeIntervalLegend();
173
+ return next.rematerializeLegend();
169
174
  }
170
175
 
171
176
  function editStrokeWidth(program, previous, args) {
@@ -220,7 +225,7 @@ function editStrokeWidth(program, previous, args) {
220
225
  next = reconcileGraphic(next, "strokeWidthLegendTitle", titleVisible, {
221
226
  type: "text"
222
227
  });
223
- return next.rematerializeStrokeWidthLegend();
228
+ return next.rematerializeLegend();
224
229
  }
225
230
 
226
231
  function categoricalSymbolIds(config) {