ggaction 0.0.6 → 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 (96) hide show
  1. package/CHANGELOG.md +43 -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/canvas/actions.js +42 -0
  7. package/src/actions/canvas/index.js +5 -1
  8. package/src/actions/categoryOrder/index.js +111 -0
  9. package/src/actions/charts/basic.js +13 -0
  10. package/src/actions/data/basic.js +11 -0
  11. package/src/actions/data/basicBin2d.js +43 -0
  12. package/src/actions/data/bin2d.js +1 -26
  13. package/src/actions/data/bin2dMaterialize.js +29 -0
  14. package/src/actions/data/index.js +6 -0
  15. package/src/actions/data/timeUnit.js +48 -0
  16. package/src/actions/encodings/angle.js +77 -0
  17. package/src/actions/encodings/appearance.js +6 -2
  18. package/src/actions/encodings/basic.js +21 -0
  19. package/src/actions/encodings/color/index.js +17 -13
  20. package/src/actions/encodings/color/layout.js +2 -0
  21. package/src/actions/encodings/color/policy.js +3 -0
  22. package/src/actions/encodings/index.js +2 -0
  23. package/src/actions/encodings/position/index.js +5 -1
  24. package/src/actions/encodings/position/policies/area.js +40 -2
  25. package/src/actions/encodings/position/policies/bar.js +6 -0
  26. package/src/actions/encodings/position/policies/index.js +2 -0
  27. package/src/actions/encodings/position/policies/tick.js +19 -0
  28. package/src/actions/encodings/ranged.js +6 -2
  29. package/src/actions/encodings/remove.js +1 -1
  30. package/src/actions/guides/axes/index.js +10 -2
  31. package/src/actions/guides/basic.js +19 -0
  32. package/src/actions/guides/grids/grid.js +7 -3
  33. package/src/actions/guides/grids/index.js +1 -1
  34. package/src/actions/guides/legends/categorical/actions.js +23 -6
  35. package/src/actions/guides/legends/categorical/index.js +14 -2
  36. package/src/actions/guides/legends/continuous/common.js +17 -5
  37. package/src/actions/guides/legends/continuous/gradient.js +12 -8
  38. package/src/actions/guides/legends/continuous/index.js +6 -2
  39. package/src/actions/guides/legends/continuous/opacity.js +70 -17
  40. package/src/actions/guides/legends/edit.js +13 -8
  41. package/src/actions/guides/legends/lane.js +468 -0
  42. package/src/actions/guides/legends/remove.js +3 -7
  43. package/src/actions/index.js +2 -0
  44. package/src/actions/marks/area/actions.js +3 -1
  45. package/src/actions/marks/area/materialize.js +12 -3
  46. package/src/actions/marks/bar/index.js +5 -1
  47. package/src/actions/marks/basic.js +11 -0
  48. package/src/actions/marks/index.js +2 -0
  49. package/src/actions/marks/line/actions.js +5 -1
  50. package/src/actions/marks/line/index.js +4 -1
  51. package/src/actions/marks/point/index.js +5 -1
  52. package/src/actions/marks/point/materialize.js +17 -5
  53. package/src/actions/marks/rect/actions.js +5 -1
  54. package/src/actions/marks/rect/index.js +4 -1
  55. package/src/actions/marks/tick/actions.js +225 -0
  56. package/src/actions/marks/tick/index.js +1 -0
  57. package/src/actions/primitives/semanticValidation/layer.js +2 -0
  58. package/src/actions/scales/consumers/index.js +8 -0
  59. package/src/actions/scales/consumers/seriesLayout.js +22 -2
  60. package/src/actions/scales/index.js +5 -1
  61. package/src/actions/scales/materialize.js +2 -0
  62. package/src/actions/selection/actions.js +5 -2
  63. package/src/basic.js +2 -0
  64. package/src/core/vocabulary.js +7 -3
  65. package/src/grammar/areaSeries.js +112 -2
  66. package/src/grammar/categoryOrder.js +138 -0
  67. package/src/grammar/direction.js +46 -0
  68. package/src/grammar/facets/index.js +1 -1
  69. package/src/grammar/pointShapes.js +34 -6
  70. package/src/grammar/positionCompatibility.js +4 -0
  71. package/src/grammar/schemas/semanticPath.js +2 -1
  72. package/src/grammar/seriesLayout.js +10 -2
  73. package/src/grammar/timeUnit.js +108 -0
  74. package/src/grammar/transforms.js +6 -0
  75. package/src/grammar/window.js +73 -7
  76. package/src/layout/legendLane.js +338 -0
  77. package/src/materialization/marks/capabilities.js +9 -0
  78. package/src/materialization/marks/index.js +2 -1
  79. package/src/materialization/marks/policies.js +13 -0
  80. package/src/materialization/scales/policies/series.js +9 -0
  81. package/src/materialization/scales/resolve.js +25 -3
  82. package/src/materialization/selection/items/index.js +1 -0
  83. package/src/materialization/selection/items/path.js +4 -1
  84. package/src/materialization/selection/items/tick.js +42 -0
  85. package/src/materialization/selection/policies/index.js +2 -0
  86. package/src/materialization/selection/policies/tick.js +10 -0
  87. package/src/renderers/canvas/index.js +96 -60
  88. package/src/renderers/canvas/text.js +3 -14
  89. package/src/renderers/pdf.js +112 -0
  90. package/src/renderers/svg.js +559 -0
  91. package/src/renderers/text.js +11 -0
  92. package/types/basic.d.ts +107 -0
  93. package/types/index.d.ts +10 -0
  94. package/types/pdf.d.ts +26 -0
  95. package/types/program.d.ts +96 -3
  96. package/types/svg.d.ts +11 -0
@@ -12,6 +12,7 @@ import {
12
12
  resolvePointJitter
13
13
  } from "../../../grammar/jitter.js";
14
14
  import { polarToCartesian, resolvePolarFrame } from "../../../grammar/polar.js";
15
+ import { resolveDirectionValues } from "../../../grammar/direction.js";
15
16
  import { resolveGraphicBounds } from "../../../layout/canvas.js";
16
17
  import { validateMarkOptions } from "../shared.js";
17
18
  import {
@@ -60,8 +61,10 @@ function compactProperties(properties) {
60
61
  );
61
62
  }
62
63
 
63
- function incompleteShapeGraphic(shape, properties) {
64
- const type = getPointGraphicType(shape);
64
+ function incompleteShapeGraphic(shape, properties, angle) {
65
+ const type = angle !== undefined && shape === "square"
66
+ ? "path"
67
+ : getPointGraphicType(shape);
65
68
  const incomplete = type === "path"
66
69
  ? { fill: properties.fill, opacity: properties.opacity }
67
70
  : properties;
@@ -99,6 +102,7 @@ function resolveJitterEntries({
99
102
  y,
100
103
  area,
101
104
  shapes,
105
+ angles,
102
106
  config,
103
107
  graphic,
104
108
  existingChildren
@@ -131,7 +135,8 @@ function resolveJitterEntries({
131
135
  area: resolvedArea,
132
136
  strokeWidth: typeof config.stroke === "string"
133
137
  ? config.strokeWidth ?? 1
134
- : 0
138
+ : 0,
139
+ angle: angles?.[index]
135
140
  });
136
141
  return [{
137
142
  index,
@@ -150,6 +155,7 @@ function applyPointJitter(program, {
150
155
  y,
151
156
  area,
152
157
  shapes,
158
+ angles,
153
159
  config,
154
160
  graphic,
155
161
  existingChildren
@@ -172,6 +178,7 @@ function applyPointJitter(program, {
172
178
  y,
173
179
  area,
174
180
  shapes,
181
+ angles,
175
182
  config,
176
183
  graphic,
177
184
  existingChildren
@@ -234,6 +241,7 @@ export const rematerializePointMark = action(
234
241
  const area = resolveRowEncodingValues(this, layer, dataset, "size");
235
242
  const encodedShape = resolveRowEncodingValues(this, layer, dataset, "shape");
236
243
  const encodedOpacity = resolveRowEncodingValues(this, layer, dataset, "opacity");
244
+ const angles = resolveDirectionValues(dataset.values, layer.encoding?.angle);
237
245
  const config = this.markConfigs[id] ?? {};
238
246
  const fill = mappedFill ?? config.fill ?? DEFAULT_POINT_FILL;
239
247
  const shapes = encodedShape ?? dataset.values.map(() => config.shape ?? "circle");
@@ -241,6 +249,7 @@ export const rematerializePointMark = action(
241
249
  const constantShape = validatePointShape(config.shape ?? "circle");
242
250
  const requiresMixedCollection =
243
251
  encodedShape !== undefined ||
252
+ angles !== undefined ||
244
253
  graphic.type === "collection" ||
245
254
  getPointGraphicType(constantShape) !== graphic.type;
246
255
  const jittered = applyPointJitter(this, {
@@ -251,6 +260,7 @@ export const rematerializePointMark = action(
251
260
  y: positions.y,
252
261
  area,
253
262
  shapes,
263
+ angles,
254
264
  config,
255
265
  graphic,
256
266
  existingChildren
@@ -273,6 +283,7 @@ export const rematerializePointMark = action(
273
283
  );
274
284
  const centerX = x?.[index] ?? existing.x;
275
285
  const centerY = y?.[index] ?? existing.y;
286
+ const angle = angles?.[index];
276
287
  if (
277
288
  resolvedArea === undefined ||
278
289
  !Number.isFinite(centerX) ||
@@ -283,7 +294,7 @@ export const rematerializePointMark = action(
283
294
  y: centerY,
284
295
  fill: color,
285
296
  opacity
286
- });
297
+ }, angle);
287
298
  }
288
299
  return createPointShapeGraphic({
289
300
  shape,
@@ -295,7 +306,8 @@ export const rematerializePointMark = action(
295
306
  ...(config.strokeWidth === undefined
296
307
  ? {}
297
308
  : { strokeWidth: config.strokeWidth }),
298
- opacity
309
+ opacity,
310
+ angle
299
311
  });
300
312
  });
301
313
  return jittered.program.editGraphics({
@@ -162,7 +162,11 @@ const editRectMark = action(
162
162
  );
163
163
 
164
164
  export function registerRectMarkActions(ProgramClass) {
165
- ProgramClass.prototype.createRectMark = createRectMark;
166
165
  ProgramClass.prototype.editRectMark = editRectMark;
166
+ registerBasicRectMarkActions(ProgramClass);
167
+ }
168
+
169
+ export function registerBasicRectMarkActions(ProgramClass) {
170
+ ProgramClass.prototype.createRectMark = createRectMark;
167
171
  ProgramClass.prototype.rematerializeRectMark = rematerializeRectMark;
168
172
  }
@@ -1 +1,4 @@
1
- export { registerRectMarkActions } from "./actions.js";
1
+ export {
2
+ registerBasicRectMarkActions,
3
+ registerRectMarkActions
4
+ } from "./actions.js";
@@ -0,0 +1,225 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateUserId } from "../../../core/identifiers.js";
3
+ import {
4
+ validateNonEmptyString,
5
+ validateNonNegativeFinite,
6
+ validatePositiveFinite,
7
+ validateUnitInterval
8
+ } from "../../../core/validation.js";
9
+ import {
10
+ centeredDirectionalSegment,
11
+ resolveDirectionValues
12
+ } from "../../../grammar/direction.js";
13
+ import { resolveMarkGraphicPlacement } from
14
+ "../../../materialization/graphicHierarchy.js";
15
+ import { resolveRowEncodingValues } from
16
+ "../../../materialization/rowEncoding.js";
17
+ import { findDataset } from "../../../selectors/datasets.js";
18
+ import { findLayer } from "../../../selectors/layers.js";
19
+ import { DEFAULT_COLORS } from "../../../theme/defaults.js";
20
+ import { rematerializeHighlightBaseline } from "../lifecycle.js";
21
+ import {
22
+ applyLayeredMarkInheritance,
23
+ assertMarkAvailable,
24
+ materializeInheritedMark,
25
+ resolveLayeredMarkInheritance,
26
+ resolveMarkData,
27
+ resolveMarkId,
28
+ validateMarkOptions
29
+ } from "../shared.js";
30
+
31
+ const CREATE_OPTIONS = Object.freeze([
32
+ "id", "data", "length", "stroke", "strokeWidth", "opacity"
33
+ ]);
34
+ const EDIT_OPTIONS = Object.freeze([
35
+ "target", "length", "stroke", "strokeWidth", "opacity"
36
+ ]);
37
+ const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
38
+
39
+ export const DEFAULT_TICK_CONFIG = Object.freeze({
40
+ length: 14,
41
+ stroke: DEFAULT_COLORS.mark,
42
+ strokeWidth: 2,
43
+ opacity: 1
44
+ });
45
+
46
+ function resolveTick(program, requested, operation) {
47
+ const candidates = program.semanticSpec.layers.filter(
48
+ layer => layer.mark?.type === "tick"
49
+ );
50
+ if (requested !== undefined) {
51
+ const id = validateUserId(requested, "Tick mark id");
52
+ const layer = findLayer(program, id);
53
+ if (layer?.mark?.type !== "tick") {
54
+ throw new Error(`Unknown tick mark "${id}".`);
55
+ }
56
+ return layer;
57
+ }
58
+ const current = findLayer(program, program.context.currentMark);
59
+ if (current?.mark?.type === "tick") return current;
60
+ if (candidates.length === 1) return candidates[0];
61
+ if (candidates.length === 0) {
62
+ throw new Error(`${operation} requires an existing tick mark.`);
63
+ }
64
+ throw new Error(`${operation} target is ambiguous; provide target.`);
65
+ }
66
+
67
+ function validateTickConfig(args, current = DEFAULT_TICK_CONFIG) {
68
+ return {
69
+ length: Object.hasOwn(args, "length")
70
+ ? validatePositiveFinite(args.length, "Tick length")
71
+ : current.length,
72
+ stroke: Object.hasOwn(args, "stroke")
73
+ ? validateNonEmptyString(args.stroke, "Tick stroke")
74
+ : current.stroke,
75
+ strokeWidth: Object.hasOwn(args, "strokeWidth")
76
+ ? validateNonNegativeFinite(args.strokeWidth, "Tick strokeWidth")
77
+ : current.strokeWidth,
78
+ opacity: Object.hasOwn(args, "opacity")
79
+ ? validateUnitInterval(args.opacity, "Tick opacity")
80
+ : current.opacity
81
+ };
82
+ }
83
+
84
+ export const createTickMark = action(
85
+ {
86
+ op: "createTickMark",
87
+ description: "Create a centered fixed-length Tick mark."
88
+ },
89
+ function (args = {}) {
90
+ validateMarkOptions(args, CREATE_OPTIONS, "createTickMark");
91
+ const id = resolveMarkId(this, args.id, {
92
+ defaultId: "tick",
93
+ label: "Tick mark id",
94
+ markType: "tick",
95
+ operation: "createTickMark"
96
+ });
97
+ const inherited = resolveLayeredMarkInheritance(this, args, "tick");
98
+ const { data } = resolveMarkData(this, {
99
+ ...args,
100
+ ...(args.data === undefined && this.context.currentData === undefined &&
101
+ inherited?.data !== undefined ? { data: inherited.data } : {})
102
+ });
103
+ const config = validateTickConfig(args);
104
+ assertMarkAvailable(this, id);
105
+
106
+ let created = this
107
+ .editSemantic({ property: `layer[${id}].mark.type`, value: "tick" })
108
+ .editSemantic({ property: `layer[${id}].data`, value: data });
109
+ created = applyLayeredMarkInheritance(created, id, inherited)
110
+ .createGraphics({
111
+ id,
112
+ type: "line",
113
+ length: 0,
114
+ ...resolveMarkGraphicPlacement(created, { data, markType: "tick" })
115
+ })
116
+ ._withMarkConfig(id, DEFAULT_TICK_CONFIG);
117
+ const materialized = materializeInheritedMark(created, id);
118
+ const appearance = Object.fromEntries(
119
+ ["length", "stroke", "strokeWidth", "opacity"]
120
+ .filter(property => Object.hasOwn(args, property))
121
+ .map(property => [property, config[property]])
122
+ );
123
+ return Object.keys(appearance).length === 0
124
+ ? materialized
125
+ : materialized.editTickMark({ target: id, ...appearance });
126
+ }
127
+ );
128
+
129
+ export const editTickMark = action(
130
+ {
131
+ op: "editTickMark",
132
+ description: "Edit Tick length and constant line appearance."
133
+ },
134
+ function (args = {}) {
135
+ validateMarkOptions(args, EDIT_OPTIONS, "editTickMark");
136
+ const editable = ["length", "stroke", "strokeWidth", "opacity"];
137
+ if (!editable.some(property => Object.hasOwn(args, property))) {
138
+ throw new Error(
139
+ "editTickMark requires length, stroke, strokeWidth, or opacity."
140
+ );
141
+ }
142
+ const layer = resolveTick(this, args.target, "editTickMark");
143
+ const config = validateTickConfig(args, {
144
+ ...DEFAULT_TICK_CONFIG,
145
+ ...this.markConfigs[layer.id]
146
+ });
147
+ return this
148
+ ._withMarkConfig(layer.id, config)
149
+ .rematerializeTickMark({ id: layer.id });
150
+ }
151
+ );
152
+
153
+ export const rematerializeTickMark = action(
154
+ {
155
+ op: "rematerializeTickMark",
156
+ description: "Recompute concrete centered Tick endpoints and appearance."
157
+ },
158
+ function (args = {}) {
159
+ validateMarkOptions(
160
+ args,
161
+ REMATERIALIZE_OPTIONS,
162
+ "rematerializeTickMark"
163
+ );
164
+ const id = validateUserId(args.id, "Tick mark id");
165
+ const highlighted = rematerializeHighlightBaseline(this, {
166
+ target: id,
167
+ operation: "rematerializeTickMark",
168
+ resetProperty: "length",
169
+ resetValue: 0
170
+ });
171
+ if (highlighted !== undefined) return highlighted;
172
+ const layer = findLayer(this, id);
173
+ if (layer?.mark?.type !== "tick") {
174
+ throw new Error(`Unknown tick mark "${id}".`);
175
+ }
176
+ const graphic = this.graphicSpec.objects[id];
177
+ if (graphic?.type !== "line" || !Array.isArray(graphic.items)) {
178
+ throw new Error(`Tick mark "${id}" requires line collection graphics.`);
179
+ }
180
+ const dataset = findDataset(this, layer.data);
181
+ if (dataset === undefined) {
182
+ throw new Error(`Tick mark "${id}" requires an existing dataset.`);
183
+ }
184
+ if (
185
+ layer.encoding?.x?.scale === undefined ||
186
+ layer.encoding?.y?.scale === undefined
187
+ ) {
188
+ return graphic.items.length === 0
189
+ ? this
190
+ : this.editGraphics({ target: id, property: "length", value: 0 });
191
+ }
192
+
193
+ const x = resolveRowEncodingValues(this, layer, dataset, "x");
194
+ const y = resolveRowEncodingValues(this, layer, dataset, "y");
195
+ const angles = resolveDirectionValues(dataset.values, layer.encoding?.angle);
196
+ const config = validateTickConfig({}, {
197
+ ...DEFAULT_TICK_CONFIG,
198
+ ...this.markConfigs[id]
199
+ });
200
+ const segments = dataset.values.map((_, index) =>
201
+ centeredDirectionalSegment({
202
+ x: x[index],
203
+ y: y[index],
204
+ degrees: angles?.[index] ?? 0,
205
+ length: config.length
206
+ })
207
+ );
208
+
209
+ return this
210
+ .editGraphics({ target: id, property: "length", value: segments.length })
211
+ .editGraphics({ target: id, property: "x1", value: segments.map(item => item.x1) })
212
+ .editGraphics({ target: id, property: "y1", value: segments.map(item => item.y1) })
213
+ .editGraphics({ target: id, property: "x2", value: segments.map(item => item.x2) })
214
+ .editGraphics({ target: id, property: "y2", value: segments.map(item => item.y2) })
215
+ .editGraphics({ target: id, property: "stroke", value: config.stroke })
216
+ .editGraphics({ target: id, property: "strokeWidth", value: config.strokeWidth })
217
+ .editGraphics({ target: id, property: "opacity", value: config.opacity });
218
+ }
219
+ );
220
+
221
+ export function registerTickMarkActions(ProgramClass) {
222
+ ProgramClass.prototype.createTickMark = createTickMark;
223
+ ProgramClass.prototype.editTickMark = editTickMark;
224
+ ProgramClass.prototype.rematerializeTickMark = rematerializeTickMark;
225
+ }
@@ -0,0 +1 @@
1
+ export { registerTickMarkActions } from "./actions.js";
@@ -16,6 +16,7 @@ import {
16
16
  validateParallelMissingPolicy
17
17
  } from "../../../grammar/parallelCoordinates.js";
18
18
  import { validatePathOrderDirection } from "../../../grammar/pathOrder.js";
19
+ import { normalizeCategoryOrder } from "../../../grammar/categoryOrder.js";
19
20
  import { validateSemanticFieldType } from "../../../grammar/scales/index.js";
20
21
  import { findLayer } from "../../../selectors/layers.js";
21
22
  import { validateNonEmptySemanticString } from "./shared.js";
@@ -50,6 +51,7 @@ export function validateLayerSemanticValue(program, parsed, value) {
50
51
  }
51
52
  if (property.endsWith(".fieldType")) validateSemanticFieldType(value);
52
53
  if (property === "encoding.pathOrder.order") validatePathOrderDirection(value);
54
+ if (property.endsWith(".categoryOrder")) normalizeCategoryOrder(value);
53
55
  if (property === "encoding.parallel.dimensions") {
54
56
  validateParallelDimensions(value, { normalized: true });
55
57
  }
@@ -6,6 +6,7 @@ import {
6
6
  requireConsumerDataset
7
7
  } from "./common.js";
8
8
  import { resolveMarkFamilyConsumerValues } from "./families.js";
9
+ import { resolveCategoryOrder } from "../../../grammar/categoryOrder.js";
9
10
 
10
11
  export { findScale, findScaleConsumers } from "./common.js";
11
12
  export {
@@ -39,3 +40,10 @@ export function resolveConsumerValues(program, consumer) {
39
40
  ? family.values
40
41
  : readConsumerFieldValues(program, consumer, dataset, scale);
41
42
  }
43
+
44
+ export function resolveConsumerCategoryOrder(program, consumer) {
45
+ const order = consumer.encoding.categoryOrder;
46
+ if (order === undefined) return undefined;
47
+ const dataset = requireConsumerDataset(program, consumer);
48
+ return resolveCategoryOrder(dataset.values, consumer.encoding.field, order);
49
+ }
@@ -10,7 +10,10 @@ import {
10
10
  findHistogramBinIndex,
11
11
  resolveHistogramBins
12
12
  } from "../../../grammar/histogram.js";
13
- import { deriveDensityAreaSeries } from "../../../grammar/areaSeries.js";
13
+ import {
14
+ deriveCenteredAreaSeries,
15
+ deriveDensityAreaSeries
16
+ } from "../../../grammar/areaSeries.js";
14
17
  import {
15
18
  resolveSeriesLayoutDomainValues
16
19
  } from "../../../grammar/seriesLayout.js";
@@ -122,6 +125,16 @@ function resolveAreaPartitions(program, consumer) {
122
125
  dataset.transform[0].type === "density"
123
126
  ? dataset.transform[0]
124
127
  : undefined;
128
+ if (
129
+ transform === undefined &&
130
+ layer.encoding?.y?.stack === "center" &&
131
+ layer.encoding?.x !== undefined
132
+ ) {
133
+ const derived = deriveCenteredAreaSeries(dataset.values, layer);
134
+ return Array.from({ length: derived.series[0].values.length }, (_, index) =>
135
+ derived.series.map(series => series.values[index].y)
136
+ );
137
+ }
125
138
  if (transform === undefined) return undefined;
126
139
  const derived = deriveDensityAreaSeries(dataset.values, layer, transform);
127
140
  if (derived.mode !== "y-density") return undefined;
@@ -153,10 +166,17 @@ export function resolveSeriesLayoutScaleValues(program, consumer) {
153
166
  }
154
167
  if (consumer.channel !== "y") return undefined;
155
168
  if (consumer.layer.mark?.type === "area") {
156
- const layout = consumer.layer.encoding?.color?.layout;
169
+ const layout = consumer.layer.encoding?.y?.stack === "center"
170
+ ? "center"
171
+ : consumer.layer.encoding?.color?.layout;
157
172
  if (layout === undefined || layout === "overlay") return undefined;
158
173
  const partitions = resolveAreaPartitions(program, consumer);
159
174
  if (partitions === undefined) {
175
+ if (
176
+ layout === "center" &&
177
+ consumer.layer.encoding?.y?.stack === "center" &&
178
+ consumer.layer.encoding?.x === undefined
179
+ ) return undefined;
160
180
  throw new Error(
161
181
  `Area layout "${layout}" currently requires vertical density series.`
162
182
  );
@@ -4,8 +4,12 @@ import { rematerializeScale } from "./materialize.js";
4
4
  import { setQuantitativeColorScale } from "./quantitativeColor.js";
5
5
 
6
6
  export function registerScaleActions(ProgramClass) {
7
- ProgramClass.prototype.createScale = createScale;
8
7
  ProgramClass.prototype.editScale = editScale;
8
+ registerBasicScaleActions(ProgramClass);
9
+ }
10
+
11
+ export function registerBasicScaleActions(ProgramClass) {
12
+ ProgramClass.prototype.createScale = createScale;
9
13
  ProgramClass.prototype.rematerializeScale = rematerializeScale;
10
14
  ProgramClass.prototype.setQuantitativeColorScale = setQuantitativeColorScale;
11
15
  }
@@ -19,6 +19,7 @@ import { resolveScaleMaterialization } from
19
19
  import {
20
20
  findScale,
21
21
  findScaleConsumers,
22
+ resolveConsumerCategoryOrder,
22
23
  resolveConsumerValues,
23
24
  resolveSeriesLayoutScaleValues
24
25
  } from "./consumers/index.js";
@@ -62,6 +63,7 @@ export const rematerializeScale = action(
62
63
  const valuesByConsumer = consumers.map(consumer => ({
63
64
  consumer,
64
65
  values: resolveConsumerValues(this, consumer),
66
+ categoryOrder: resolveConsumerCategoryOrder(this, consumer),
65
67
  seriesLayout: resolveSeriesLayoutScaleValues(this, consumer)
66
68
  }));
67
69
  const resolvedScale = resolveScaleMaterialization({
@@ -340,8 +340,11 @@ export const applyRuleHighlight = action(
340
340
  validateKeys(args, INTERNAL_SELECTION_OPTIONS, "applyRuleHighlight");
341
341
  const resolved = resolveStoredSelection(this, args.selection);
342
342
  const keys = selectedKeys(args, resolved);
343
- if (resolved.items[0]?.markType !== "rule" && resolved.items.length > 0) {
344
- throw new Error("applyRuleHighlight requires a rule selection.");
343
+ if (
344
+ resolved.items.length > 0 &&
345
+ !["rule", "tick"].includes(resolved.items[0]?.markType)
346
+ ) {
347
+ throw new Error("applyRuleHighlight requires a rule or Tick selection.");
345
348
  }
346
349
  if (keys.length === 0) return this;
347
350
  const selected = new Set(keys);
package/src/basic.js ADDED
@@ -0,0 +1,2 @@
1
+ export { chart } from "./BasicChartProgram.js";
2
+ export { render } from "./renderers/canvas/index.js";
@@ -5,6 +5,7 @@ export const MARK_TYPES = Object.freeze([
5
5
  "area",
6
6
  "arc",
7
7
  "rule",
8
+ "tick",
8
9
  "text",
9
10
  "rect"
10
11
  ]);
@@ -16,6 +17,7 @@ export const MARK_GRAPHIC_TYPES = Object.freeze({
16
17
  area: Object.freeze(["path"]),
17
18
  arc: Object.freeze(["path"]),
18
19
  rule: Object.freeze(["line"]),
20
+ tick: Object.freeze(["line"]),
19
21
  text: Object.freeze(["text"]),
20
22
  rect: Object.freeze(["rect"])
21
23
  });
@@ -34,6 +36,7 @@ export const ENCODING_CHANNELS = Object.freeze([
34
36
  "strokeWidth",
35
37
  "size",
36
38
  "shape",
39
+ "angle",
37
40
  "group",
38
41
  "pathOrder",
39
42
  "opacity",
@@ -41,7 +44,7 @@ export const ENCODING_CHANNELS = Object.freeze([
41
44
  ]);
42
45
 
43
46
  const CARTESIAN_MARK_TYPES = Object.freeze([
44
- "point", "line", "bar", "area", "rule", "text", "rect"
47
+ "point", "line", "bar", "area", "rule", "tick", "text", "rect"
45
48
  ]);
46
49
  const POLAR_MARK_TYPES = Object.freeze(["point", "line", "arc"]);
47
50
 
@@ -114,7 +117,7 @@ export const POSITION_CHANNEL_DEFINITIONS = Object.freeze({
114
117
 
115
118
  export const SCALED_ENCODING_CHANNELS = Object.freeze(
116
119
  ENCODING_CHANNELS.filter(channel =>
117
- !["group", "pathOrder", "text"].includes(channel)
120
+ !["angle", "group", "pathOrder", "text"].includes(channel)
118
121
  )
119
122
  );
120
123
 
@@ -185,12 +188,13 @@ export function getMarkGraphicTypes(markType) {
185
188
  export const COLOR_LAYOUTS = Object.freeze([
186
189
  "stack",
187
190
  "fill",
191
+ "center",
188
192
  "group",
189
193
  "overlay",
190
194
  "diverging"
191
195
  ]);
192
196
 
193
- export const STACK_MODES = Object.freeze(["zero", "normalize"]);
197
+ export const STACK_MODES = Object.freeze(["zero", "normalize", "center"]);
194
198
 
195
199
  export const CATEGORICAL_LEGEND_CHANNELS = Object.freeze([
196
200
  "color",
@@ -120,6 +120,110 @@ export function deriveAreaSeries(rows, layer) {
120
120
  });
121
121
  }
122
122
 
123
+ export function deriveCenteredAreaSeries(rows, layer) {
124
+ if (layer?.mark?.type !== "area") {
125
+ throw new Error("Centered area series derivation requires an area mark.");
126
+ }
127
+ const { x, y, x2, y2, group, color } = layer.encoding ?? {};
128
+ if (
129
+ !["quantitative", "temporal"].includes(x?.fieldType) ||
130
+ y?.fieldType !== "quantitative" ||
131
+ y?.stack !== "center" ||
132
+ x2 !== undefined ||
133
+ y2 !== undefined
134
+ ) {
135
+ throw new Error(
136
+ `Centered area mark "${layer.id}" requires one x field and one quantitative y field without ranged endpoints.`
137
+ );
138
+ }
139
+ if (group?.fieldType !== "nominal") {
140
+ throw new Error(`Centered area mark "${layer.id}" requires a nominal group encoding.`);
141
+ }
142
+ if (color !== undefined && color.field !== group.field) {
143
+ throw new Error(
144
+ `Centered area color on mark "${layer.id}" must match its group field.`
145
+ );
146
+ }
147
+ const xValues = x.fieldType === "temporal"
148
+ ? readTemporalField(rows, x.field)
149
+ : readQuantitativeField(rows, x.field);
150
+ const yValues = readQuantitativeField(rows, y.field);
151
+ const groups = readNominalField(rows, group.field);
152
+ const groupOrder = [];
153
+ const positions = new Set();
154
+ const byGroup = new Map();
155
+ for (let index = 0; index < rows.length; index += 1) {
156
+ const key = groups[index];
157
+ if (!byGroup.has(key)) {
158
+ groupOrder.push(key);
159
+ byGroup.set(key, new Map());
160
+ }
161
+ const values = byGroup.get(key);
162
+ if (values.has(xValues[index])) {
163
+ throw new Error(
164
+ `Centered area mark "${layer.id}" has duplicate ${group.field}/${x.field} rows.`
165
+ );
166
+ }
167
+ values.set(xValues[index], yValues[index]);
168
+ positions.add(xValues[index]);
169
+ }
170
+ const orderedPositions = [...positions].sort((left, right) => left - right);
171
+ if (groupOrder.length === 0 || orderedPositions.length < 2) {
172
+ throw new Error(
173
+ `Centered area mark "${layer.id}" requires at least two aligned positions.`
174
+ );
175
+ }
176
+ for (const [key, values] of byGroup) {
177
+ if (
178
+ values.size !== orderedPositions.length ||
179
+ orderedPositions.some(position => !values.has(position))
180
+ ) {
181
+ throw new Error(
182
+ `Centered area series "${String(key)}" requires one aligned value at every x position.`
183
+ );
184
+ }
185
+ }
186
+
187
+ const valuesBySeries = groupOrder.map(() => []);
188
+ for (const position of orderedPositions) {
189
+ const partition = groupOrder.map(key => byGroup.get(key).get(position));
190
+ const segments = new Map(
191
+ layoutSeriesPartition(partition, "center").map(segment => [
192
+ segment.index,
193
+ segment
194
+ ])
195
+ );
196
+ let endpoint = -partition.reduce((sum, value) => sum + value, 0) / 2;
197
+ for (let index = 0; index < groupOrder.length; index += 1) {
198
+ const segment = segments.get(index);
199
+ valuesBySeries[index].push({
200
+ x: position,
201
+ y: partition[index],
202
+ lower: segment?.start ?? endpoint,
203
+ upper: segment?.end ?? endpoint
204
+ });
205
+ if (segment !== undefined) endpoint = segment.end;
206
+ }
207
+ }
208
+ const series = groupOrder.map((key, index) => ({
209
+ key: {
210
+ [group.field]: key,
211
+ ...(color === undefined ? {} : { [color.field]: key })
212
+ },
213
+ values: valuesBySeries[index]
214
+ }));
215
+ return cloneAndFreeze({
216
+ mode: "y-center",
217
+ orientation: "vertical",
218
+ xValues: orderedPositions,
219
+ yValues: series.flatMap(item => item.values.flatMap(value => [
220
+ value.lower,
221
+ value.upper
222
+ ])),
223
+ series
224
+ });
225
+ }
226
+
123
227
  export function deriveDensityAreaSeries(rows, layer, transform) {
124
228
  if (layer?.mark?.type !== "area") {
125
229
  throw new Error("Density area derivation requires a semantic area mark.");
@@ -233,13 +337,19 @@ export function layoutDensityAreaSeries(derived, layout = "overlay") {
233
337
  segment
234
338
  ])
235
339
  );
340
+ let zeroThicknessEndpoint = layout === "center"
341
+ ? -densities.reduce((sum, value) => sum + value, 0) / 2
342
+ : 0;
236
343
  for (let index = 0; index < derived.series.length; index += 1) {
237
344
  const segment = segments.get(index);
238
345
  valuesBySeries[index].push({
239
346
  x,
240
- lower: segment?.start ?? 0,
241
- upper: segment?.end ?? 0
347
+ lower: segment?.start ?? zeroThicknessEndpoint,
348
+ upper: segment?.end ?? zeroThicknessEndpoint
242
349
  });
350
+ if (segment !== undefined && ["stack", "fill", "center"].includes(layout)) {
351
+ zeroThicknessEndpoint = segment.end;
352
+ }
243
353
  }
244
354
  }
245
355