ggaction 0.0.2 → 0.0.3

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 (199) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +12 -1
  3. package/package.json +6 -1
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/encodings/appearance.js +11 -0
  16. package/src/actions/encodings/barWidth.js +5 -3
  17. package/src/actions/encodings/color.js +120 -25
  18. package/src/actions/encodings/index.js +2 -0
  19. package/src/actions/encodings/offset.js +90 -72
  20. package/src/actions/encodings/position/apply.js +66 -0
  21. package/src/actions/encodings/position/policies/arc.js +26 -0
  22. package/src/actions/encodings/position/policies/bar.js +9 -5
  23. package/src/actions/encodings/position/policies/index.js +5 -1
  24. package/src/actions/encodings/position/policies/line.js +20 -3
  25. package/src/actions/encodings/position/resolve.js +57 -8
  26. package/src/actions/encodings/position.js +44 -90
  27. package/src/actions/encodings/ranged.js +28 -6
  28. package/src/actions/encodings/shared.js +17 -21
  29. package/src/actions/encodings/text.js +73 -0
  30. package/src/actions/errorBands/create.js +37 -29
  31. package/src/actions/errorBands/edit.js +206 -0
  32. package/src/actions/errorBands/index.js +9 -0
  33. package/src/actions/errorBands/options.js +39 -0
  34. package/src/actions/errorBars/create.js +50 -43
  35. package/src/actions/errorBars/edit.js +130 -0
  36. package/src/actions/errorBars/index.js +3 -0
  37. package/src/actions/errorBars/options.js +52 -0
  38. package/src/actions/facets/actions.js +162 -0
  39. package/src/actions/facets/derive.js +200 -0
  40. package/src/actions/facets/guides.js +16 -0
  41. package/src/actions/facets/index.js +1 -0
  42. package/src/actions/facets/replay.js +69 -0
  43. package/src/actions/guides/applicability.js +113 -0
  44. package/src/actions/guides/axes/axes.js +70 -16
  45. package/src/actions/guides/axes/edit.js +126 -0
  46. package/src/actions/guides/axes/index.js +12 -0
  47. package/src/actions/guides/axes/labels.js +21 -18
  48. package/src/actions/guides/axes/remove.js +60 -0
  49. package/src/actions/guides/axes/tickGroups.js +7 -5
  50. package/src/actions/guides/axes/ticks.js +9 -4
  51. package/src/actions/guides/axes/titles.js +7 -5
  52. package/src/actions/guides/grids/grid.js +144 -15
  53. package/src/actions/guides/grids/resolve.js +5 -13
  54. package/src/actions/guides/guides.js +17 -75
  55. package/src/actions/guides/index.js +6 -0
  56. package/src/actions/guides/legends/categorical/actions.js +48 -9
  57. package/src/actions/guides/legends/categorical/components.js +16 -16
  58. package/src/actions/guides/legends/categorical/index.js +14 -14
  59. package/src/actions/guides/legends/categorical/layout.js +3 -1
  60. package/src/actions/guides/legends/categorical/options.js +2 -5
  61. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  62. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  63. package/src/actions/guides/legends/continuous/common.js +7 -7
  64. package/src/actions/guides/legends/continuous/interval.js +5 -5
  65. package/src/actions/guides/legends/edit.js +2 -6
  66. package/src/actions/guides/legends/focused.js +63 -0
  67. package/src/actions/guides/legends/index.js +4 -0
  68. package/src/actions/guides/legends/remove.js +64 -0
  69. package/src/actions/guides/legends/size.js +12 -14
  70. package/src/actions/guides/polar/axes/facade.js +219 -0
  71. package/src/actions/guides/polar/axes/labels.js +188 -0
  72. package/src/actions/guides/polar/axes/lines.js +113 -0
  73. package/src/actions/guides/polar/axes/shared.js +112 -0
  74. package/src/actions/guides/polar/axes/ticks.js +164 -0
  75. package/src/actions/guides/polar/axes/titles.js +165 -0
  76. package/src/actions/guides/polar/axes.js +55 -0
  77. package/src/actions/guides/polar/grids.js +252 -0
  78. package/src/actions/guides/polar/index.js +2 -0
  79. package/src/actions/guides/polar/resolve.js +269 -0
  80. package/src/actions/index.js +4 -0
  81. package/src/actions/marks/arc/actions.js +239 -0
  82. package/src/actions/marks/arc/index.js +1 -0
  83. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  84. package/src/actions/marks/area/index.js +4 -0
  85. package/src/actions/marks/bar/create.js +26 -5
  86. package/src/actions/marks/bar/materialize.js +25 -31
  87. package/src/actions/marks/index.js +12 -4
  88. package/src/actions/marks/lifecycle.js +27 -0
  89. package/src/actions/marks/line/actions.js +373 -0
  90. package/src/actions/marks/line/index.js +1 -0
  91. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  92. package/src/actions/marks/point/index.js +1 -0
  93. package/src/actions/marks/rect/actions.js +165 -0
  94. package/src/actions/marks/rect/index.js +1 -0
  95. package/src/actions/marks/remove.js +214 -0
  96. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  97. package/src/actions/marks/rule/index.js +1 -0
  98. package/src/actions/marks/shared.js +156 -25
  99. package/src/actions/marks/text/actions.js +192 -0
  100. package/src/actions/marks/text/index.js +1 -0
  101. package/src/actions/primitives/createGraphics.js +13 -6
  102. package/src/actions/primitives/editGraphics.js +10 -3
  103. package/src/actions/primitives/semantic.js +26 -3
  104. package/src/actions/primitives/semanticValue.js +25 -3
  105. package/src/actions/regression/components.js +1 -1
  106. package/src/actions/regression/create.js +18 -1
  107. package/src/actions/regression/edit.js +247 -0
  108. package/src/actions/regression/index.js +3 -0
  109. package/src/actions/regression/resolve.js +3 -1
  110. package/src/actions/scales/consumers.js +15 -6
  111. package/src/actions/scales/definitions.js +13 -4
  112. package/src/actions/scales/edit.js +42 -19
  113. package/src/actions/scales/materialize.js +57 -408
  114. package/src/actions/selection/actions.js +38 -24
  115. package/src/actions/selection/index.js +2 -0
  116. package/src/actions/titles/actions.js +63 -2
  117. package/src/actions/titles/index.js +2 -0
  118. package/src/actions/titles/resolve.js +12 -17
  119. package/src/composition.js +10 -0
  120. package/src/core/ChartProgram.js +200 -2
  121. package/src/core/action.js +10 -0
  122. package/src/core/validation.js +16 -0
  123. package/src/core/vocabulary.js +154 -4
  124. package/src/grammar/arcs.js +154 -0
  125. package/src/grammar/bars/aggregate.js +2 -2
  126. package/src/grammar/bars/geometry.js +3 -3
  127. package/src/grammar/bars/policy.js +7 -1
  128. package/src/grammar/coordinates.js +3 -7
  129. package/src/grammar/density.js +20 -1
  130. package/src/grammar/facets/dependencies.js +185 -0
  131. package/src/grammar/facets/guides.js +257 -0
  132. package/src/grammar/facets/scales.js +254 -0
  133. package/src/grammar/facets.js +122 -0
  134. package/src/grammar/lineSeries.js +111 -3
  135. package/src/grammar/polar.js +102 -0
  136. package/src/grammar/polarGuides.js +243 -0
  137. package/src/grammar/polarLineCommands.js +43 -0
  138. package/src/grammar/polarPaths.js +161 -0
  139. package/src/grammar/positionCompatibility.js +18 -2
  140. package/src/grammar/rects.js +26 -0
  141. package/src/grammar/scales/continuous.js +6 -1
  142. package/src/grammar/scales/fields.js +7 -0
  143. package/src/grammar/scales/ordinal.js +5 -2
  144. package/src/grammar/scales/policies.js +1 -1
  145. package/src/grammar/schemas/graphic.js +1 -1
  146. package/src/grammar/schemas/graphicBounds.js +64 -9
  147. package/src/grammar/schemas/graphicTree.js +44 -10
  148. package/src/grammar/schemas/semanticPath.js +31 -9
  149. package/src/grammar/seriesLayout.js +13 -2
  150. package/src/grammar/text.js +71 -0
  151. package/src/grammar/ticks.js +50 -13
  152. package/src/index.js +1 -0
  153. package/src/layout/composition.js +310 -0
  154. package/src/layout/facets.js +131 -0
  155. package/src/layout/title.js +6 -0
  156. package/src/materialization/bars/aggregate.js +5 -2
  157. package/src/materialization/bars/grouped.js +75 -37
  158. package/src/materialization/composition.js +152 -0
  159. package/src/materialization/compositionSnapshot.js +114 -0
  160. package/src/materialization/dependencies.js +71 -16
  161. package/src/materialization/encodings.js +14 -28
  162. package/src/materialization/facetGuides.js +426 -0
  163. package/src/materialization/facets.js +214 -0
  164. package/src/materialization/graphicHierarchy.js +15 -5
  165. package/src/materialization/guides/resources.js +46 -0
  166. package/src/materialization/marks.js +220 -14
  167. package/src/materialization/planner.js +105 -12
  168. package/src/materialization/rect.js +138 -0
  169. package/src/materialization/rectConfig.js +38 -0
  170. package/src/materialization/rowEncoding.js +33 -0
  171. package/src/materialization/scales/map.js +40 -0
  172. package/src/materialization/scales/policies/arc.js +37 -0
  173. package/src/materialization/scales/policies/bar.js +132 -0
  174. package/src/materialization/scales/policies/series.js +76 -0
  175. package/src/materialization/scales/resolve.js +326 -0
  176. package/src/materialization/selection/items/arc.js +29 -0
  177. package/src/materialization/selection/items/bar.js +255 -0
  178. package/src/materialization/selection/items/common.js +131 -0
  179. package/src/materialization/selection/items/index.js +6 -0
  180. package/src/materialization/selection/items/path.js +59 -0
  181. package/src/materialization/selection/items/point.js +55 -0
  182. package/src/materialization/selection/items/rect.js +23 -0
  183. package/src/materialization/selection/items/rule.js +25 -0
  184. package/src/materialization/selection/policies/arc.js +10 -0
  185. package/src/materialization/selection/policies/area.js +1 -1
  186. package/src/materialization/selection/policies/bar.js +1 -1
  187. package/src/materialization/selection/policies/index.js +5 -1
  188. package/src/materialization/selection/policies/line.js +1 -1
  189. package/src/materialization/selection/policies/point.js +1 -1
  190. package/src/materialization/selection/policies/rect.js +10 -0
  191. package/src/materialization/selection/policies/rule.js +1 -1
  192. package/src/materialization/selection/styles.js +24 -16
  193. package/src/materialization/text.js +156 -0
  194. package/src/renderers/canvas/index.js +68 -22
  195. package/types/extension.d.ts +1 -0
  196. package/types/index.d.ts +56 -1
  197. package/types/program.d.ts +664 -27
  198. package/src/actions/marks/line.js +0 -248
  199. package/src/materialization/selection/items.js +0 -503
@@ -1,16 +1,24 @@
1
1
  export const POSITION_FIELD_COMPATIBILITY = Object.freeze({
2
2
  point: Object.freeze({
3
3
  x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
4
- y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
4
+ y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
5
+ theta: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
6
+ radius: Object.freeze(["quantitative"])
5
7
  }),
6
8
  line: Object.freeze({
7
9
  x: Object.freeze(["quantitative", "temporal"]),
8
- y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
10
+ y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
11
+ theta: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
12
+ radius: Object.freeze(["quantitative"])
9
13
  }),
10
14
  area: Object.freeze({
11
15
  x: Object.freeze(["quantitative", "temporal"]),
12
16
  y: Object.freeze(["quantitative", "temporal"])
13
17
  }),
18
+ arc: Object.freeze({
19
+ theta: Object.freeze(["ordinal", "nominal"]),
20
+ radius: Object.freeze(["quantitative"])
21
+ }),
14
22
  rule: Object.freeze({
15
23
  x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
16
24
  y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
@@ -18,6 +26,14 @@ export const POSITION_FIELD_COMPATIBILITY = Object.freeze({
18
26
  bar: Object.freeze({
19
27
  x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
20
28
  y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
29
+ }),
30
+ text: Object.freeze({
31
+ x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
32
+ y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
33
+ }),
34
+ rect: Object.freeze({
35
+ x: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"]),
36
+ y: Object.freeze(["quantitative", "temporal", "ordinal", "nominal"])
21
37
  })
22
38
  });
23
39
 
@@ -0,0 +1,26 @@
1
+ export const RECT_MODES = Object.freeze({
2
+ discrete: "discrete",
3
+ ranged: "ranged"
4
+ });
5
+
6
+ function categorical(encoding) {
7
+ return ["nominal", "ordinal"].includes(encoding?.fieldType) &&
8
+ encoding.scale !== undefined;
9
+ }
10
+
11
+ function ranged(primary, secondary) {
12
+ return primary?.scale !== undefined &&
13
+ secondary?.scale === primary.scale &&
14
+ ["quantitative", "temporal"].includes(primary.fieldType) &&
15
+ secondary.fieldType === primary.fieldType;
16
+ }
17
+
18
+ export function resolveRectMode(layer) {
19
+ if (layer?.mark?.type !== "rect") return undefined;
20
+ const { x, y, x2, y2 } = layer.encoding ?? {};
21
+ if (x2 === undefined && y2 === undefined && categorical(x) && categorical(y)) {
22
+ return RECT_MODES.discrete;
23
+ }
24
+ if (ranged(x, x2) && ranged(y, y2)) return RECT_MODES.ranged;
25
+ return undefined;
26
+ }
@@ -1,4 +1,6 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
2
+ import { POLAR_POSITION_CHANNELS } from "../../core/vocabulary.js";
3
+ import { resolvePolarScaleRange } from "../polar.js";
2
4
  import { niceTimeDomain } from "./temporal.js";
3
5
  import {
4
6
  validatePair,
@@ -66,8 +68,11 @@ export function resolveContinuousDomain({ domain, values, type, nice, zero }) {
66
68
  }
67
69
 
68
70
  export function resolveScaleRange(range, channel, bounds) {
69
- const validated = validateScaleRange(range);
70
71
  validatePositionChannel(channel);
72
+ if (POLAR_POSITION_CHANNELS.includes(channel)) {
73
+ return resolvePolarScaleRange(range, channel, bounds);
74
+ }
75
+ const validated = validateScaleRange(range);
71
76
  if (validated !== "auto") return validated;
72
77
  if (
73
78
  !isPlainObject(bounds) ||
@@ -22,6 +22,13 @@ export function validateNominalFieldType(fieldType) {
22
22
  return fieldType;
23
23
  }
24
24
 
25
+ export function validateCategoricalFieldType(fieldType) {
26
+ if (!["nominal", "ordinal"].includes(fieldType)) {
27
+ throw new Error(`Unsupported categorical field type "${fieldType}".`);
28
+ }
29
+ return fieldType;
30
+ }
31
+
25
32
  export function validateSemanticFieldType(fieldType) {
26
33
  if (!["quantitative", "nominal", "ordinal", "temporal"].includes(fieldType)) {
27
34
  throw new Error(`Unsupported semantic field type "${fieldType}".`);
@@ -151,11 +151,14 @@ export function resolveOrdinalOffsetScale({
151
151
  range,
152
152
  parentBandwidth,
153
153
  paddingInner = 0,
154
- paddingOuter = 0
154
+ paddingOuter = 0,
155
+ channel = "xOffset"
155
156
  }) {
156
157
  const resolvedDomain = resolveOrdinalDomain(domain, values);
157
158
  if (!Number.isFinite(parentBandwidth) || parentBandwidth <= 0) {
158
- throw new Error("Automatic xOffset range requires a positive x bandwidth.");
159
+ throw new Error(
160
+ `Automatic ${channel} range requires a positive ${channel[0]} bandwidth.`
161
+ );
159
162
  }
160
163
  const resolvedRange = range === "auto"
161
164
  ? validatePair([0, parentBandwidth], "Offset scale range")
@@ -3,7 +3,7 @@ import { POINT_SHAPES } from "../pointShapes.js";
3
3
  import { normalizeStrokeDashPattern } from "./appearance.js";
4
4
 
5
5
  export function validateScaleUnknown(channel, value) {
6
- if (["x", "y", "xOffset"].includes(channel)) {
6
+ if (["x", "y", "xOffset", "yOffset"].includes(channel)) {
7
7
  if (!Number.isFinite(value)) {
8
8
  throw new TypeError(`Scale unknown for ${channel} must be a finite number.`);
9
9
  }
@@ -11,7 +11,7 @@ const DRAWABLE_TYPES = new Set([
11
11
  ]);
12
12
  const STRUCTURAL_TYPES = new Set(["canvas"]);
13
13
  const GRAPHIC_PROPERTIES = Object.freeze({
14
- canvas: new Set(["width", "height", "background"]),
14
+ canvas: new Set(["x", "y", "width", "height", "background"]),
15
15
  collection: new Set([
16
16
  "items",
17
17
  "x",
@@ -11,6 +11,69 @@ function unionBounds(bounds) {
11
11
  };
12
12
  }
13
13
 
14
+ function cubicValue(start, control1, control2, end, t) {
15
+ const remaining = 1 - t;
16
+ return remaining ** 3 * start +
17
+ 3 * remaining ** 2 * t * control1 +
18
+ 3 * remaining * t ** 2 * control2 +
19
+ t ** 3 * end;
20
+ }
21
+
22
+ function cubicExtrema(start, control1, control2, end) {
23
+ const quadratic = 3 * (-start + 3 * control1 - 3 * control2 + end);
24
+ const linear = 2 * (3 * start - 6 * control1 + 3 * control2);
25
+ const constant = 3 * (control1 - start);
26
+ const epsilon = Number.EPSILON * 64 * Math.max(
27
+ 1,
28
+ Math.abs(quadratic),
29
+ Math.abs(linear),
30
+ Math.abs(constant)
31
+ );
32
+ if (Math.abs(quadratic) <= epsilon) {
33
+ if (Math.abs(linear) <= epsilon) return [];
34
+ const root = -constant / linear;
35
+ return root > 0 && root < 1 ? [root] : [];
36
+ }
37
+ const discriminant = linear ** 2 - 4 * quadratic * constant;
38
+ if (discriminant < -epsilon) return [];
39
+ const root = Math.sqrt(Math.max(0, discriminant));
40
+ return [
41
+ (-linear - root) / (2 * quadratic),
42
+ (-linear + root) / (2 * quadratic)
43
+ ].filter(value => value > 0 && value < 1);
44
+ }
45
+
46
+ function pathPoints(commands) {
47
+ const points = [];
48
+ let current;
49
+ for (const command of commands) {
50
+ if (command.op === "M" || command.op === "L") {
51
+ if ([command.x, command.y].every(Number.isFinite)) {
52
+ current = { x: command.x, y: command.y };
53
+ points.push(current);
54
+ }
55
+ continue;
56
+ }
57
+ if (command.op !== "C" || current === undefined) continue;
58
+ if (![command.x1, command.y1, command.x2, command.y2, command.x, command.y]
59
+ .every(Number.isFinite)) continue;
60
+ const roots = new Set([
61
+ 0,
62
+ 1,
63
+ ...cubicExtrema(current.x, command.x1, command.x2, command.x),
64
+ ...cubicExtrema(current.y, command.y1, command.y2, command.y)
65
+ ]);
66
+ for (const t of roots) {
67
+ points.push({
68
+ x: cubicValue(current.x, command.x1, command.x2, command.x, t),
69
+ y: cubicValue(current.y, command.y1, command.y2, command.y, t)
70
+ });
71
+ }
72
+ current = { x: command.x, y: command.y };
73
+ }
74
+ return points;
75
+ }
76
+
14
77
  function primitiveBounds(type, properties = {}) {
15
78
  const strokeExtent = (properties.strokeWidth ?? 0) / 2;
16
79
  if (type === "circle") {
@@ -86,15 +149,7 @@ function primitiveBounds(type, properties = {}) {
86
149
  if (type === "path") {
87
150
  const commands = properties.commands;
88
151
  if (!Array.isArray(commands)) return undefined;
89
- const points = commands.flatMap(command => {
90
- if (command.op === "M" || command.op === "L") return [{ x: command.x, y: command.y }];
91
- if (command.op === "C") return [
92
- { x: command.x1, y: command.y1 },
93
- { x: command.x2, y: command.y2 },
94
- { x: command.x, y: command.y }
95
- ];
96
- return [];
97
- }).filter(point => Number.isFinite(point.x) && Number.isFinite(point.y));
152
+ const points = pathPoints(commands);
98
153
  if (points.length === 0) return undefined;
99
154
  return {
100
155
  left: Math.min(...points.map(point => point.x)) - strokeExtent,
@@ -70,7 +70,7 @@ export function graphicSiblings(graphicSpec, target) {
70
70
  return graphicSpec.order;
71
71
  }
72
72
 
73
- function visitNamed(graphicSpec, id, visitor, path, visited) {
73
+ function visitNamed(graphicSpec, id, visitors, path, visited) {
74
74
  if (path.includes(id)) {
75
75
  throw new Error(`Graphic attachment cycle includes "${id}".`);
76
76
  }
@@ -82,29 +82,37 @@ function visitNamed(graphicSpec, id, visitor, path, visited) {
82
82
  throw new Error(`Graphic "${id}" is attached more than once.`);
83
83
  }
84
84
  visited.add(id);
85
- visitor({ id, object, kind: "object", depth: path.length });
85
+ const entry = { id, object, kind: "object", depth: path.length };
86
+ visitors.enter?.(entry);
86
87
  for (const item of object.items ?? []) {
87
- visitor({
88
+ visitors.item?.({
88
89
  id: item.id,
89
90
  object: item,
90
91
  kind: "item",
91
92
  ownerId: id,
93
+ owner: object,
92
94
  depth: path.length + 1
93
95
  });
94
96
  }
95
97
  for (const childId of object.children ?? []) {
96
- visitNamed(graphicSpec, childId, visitor, [...path, id], visited);
98
+ visitNamed(graphicSpec, childId, visitors, [...path, id], visited);
97
99
  }
100
+ visitors.exit?.(entry);
98
101
  }
99
102
 
100
- export function walkGraphicTree(graphicSpec, visitor) {
103
+ export function walkGraphicTreeEvents(graphicSpec, visitors) {
101
104
  requireGraphicSpec(graphicSpec);
102
- if (typeof visitor !== "function") {
103
- throw new TypeError("walkGraphicTree requires a visitor function.");
105
+ if (!isPlainObject(visitors)) {
106
+ throw new TypeError("walkGraphicTreeEvents requires visitor callbacks.");
107
+ }
108
+ for (const name of ["enter", "item", "exit"]) {
109
+ if (visitors[name] !== undefined && typeof visitors[name] !== "function") {
110
+ throw new TypeError(`walkGraphicTreeEvents ${name} must be a function.`);
111
+ }
104
112
  }
105
113
  const visited = new Set();
106
114
  for (const id of graphicSpec.order) {
107
- visitNamed(graphicSpec, id, visitor, [], visited);
115
+ visitNamed(graphicSpec, id, visitors, [], visited);
108
116
  }
109
117
  const orphan = Object.keys(graphicSpec.objects).find(id => !visited.has(id));
110
118
  if (orphan !== undefined) {
@@ -112,12 +120,38 @@ export function walkGraphicTree(graphicSpec, visitor) {
112
120
  }
113
121
  }
114
122
 
123
+ export function walkGraphicTree(graphicSpec, visitor) {
124
+ if (typeof visitor !== "function") {
125
+ throw new TypeError("walkGraphicTree requires a visitor function.");
126
+ }
127
+ walkGraphicTreeEvents(graphicSpec, {
128
+ enter: visitor,
129
+ item: visitor
130
+ });
131
+ }
132
+
115
133
  export function walkGraphicDrawOrder(graphicSpec, visitor) {
116
- walkGraphicTree(graphicSpec, entry => {
117
- if (entry.kind === "object") visitor(entry);
134
+ walkGraphicTreeEvents(graphicSpec, { enter: visitor });
135
+ }
136
+
137
+ export function findOrderedGraphicsByType(graphicSpec, type) {
138
+ requireGraphicSpec(graphicSpec);
139
+ return graphicSpec.order.flatMap(id => {
140
+ const object = graphicSpec.objects[id];
141
+ return object?.type === type ? [{ id, object }] : [];
118
142
  });
119
143
  }
120
144
 
145
+ export function requireSingleOrderedGraphicByType(graphicSpec, type) {
146
+ const matches = findOrderedGraphicsByType(graphicSpec, type);
147
+ if (matches.length !== 1) {
148
+ throw new Error(
149
+ `graphicSpec must contain exactly one ordered ${type}.`
150
+ );
151
+ }
152
+ return matches[0];
153
+ }
154
+
121
155
  export function collectGraphicSubtreeIds(graphicSpec, target) {
122
156
  const found = requireGraphic(graphicSpec, target);
123
157
  if (found.kind === "item") {
@@ -1,7 +1,7 @@
1
1
  import { USER_ID_SOURCE } from "../../core/identifiers.js";
2
2
  import {
3
+ CARTESIAN_POSITION_CHANNELS,
3
4
  ENCODING_CHANNELS,
4
- POSITION_CHANNELS,
5
5
  SCALED_ENCODING_CHANNELS
6
6
  } from "../../core/vocabulary.js";
7
7
 
@@ -20,7 +20,9 @@ const ENCODING_PATHS = Object.freeze([
20
20
  "encoding.x.bin.boundaries",
21
21
  "encoding.color.layout",
22
22
  "encoding.color.aggregate",
23
- ...POSITION_CHANNELS.flatMap(channel => [
23
+ "encoding.theta.aggregate",
24
+ "encoding.text.format",
25
+ ...CARTESIAN_POSITION_CHANNELS.flatMap(channel => [
24
26
  `encoding.${channel}.aggregate`,
25
27
  `encoding.${channel}.stack`
26
28
  ])
@@ -38,6 +40,7 @@ const ENTITY_PATHS = Object.freeze({
38
40
  ),
39
41
  properties: new Set([
40
42
  "data",
43
+ "source",
41
44
  "coordinate",
42
45
  "transform",
43
46
  "mark.type",
@@ -65,6 +68,12 @@ const GUIDE_PATHS = new Set([
65
68
  "axis.y.scale",
66
69
  "axis.y.coordinate",
67
70
  "axis.y.title",
71
+ "axis.theta.scale",
72
+ "axis.theta.coordinate",
73
+ "axis.theta.title",
74
+ "axis.radius.scale",
75
+ "axis.radius.coordinate",
76
+ "axis.radius.title",
68
77
  "legend.color.scale",
69
78
  "legend.color.title",
70
79
  "legend.size.scale",
@@ -77,10 +86,22 @@ const GUIDE_PATHS = new Set([
77
86
  "grid.horizontal.scale",
78
87
  "grid.horizontal.coordinate",
79
88
  "grid.vertical.scale",
80
- "grid.vertical.coordinate"
89
+ "grid.vertical.coordinate",
90
+ "grid.theta.scale",
91
+ "grid.theta.coordinate",
92
+ "grid.radial.scale",
93
+ "grid.radial.coordinate"
81
94
  ]);
82
95
 
83
96
  const GUIDE_REMOVABLE_CONTAINERS = new Set([
97
+ "axis.x",
98
+ "axis.y",
99
+ "axis.theta",
100
+ "axis.radius",
101
+ "grid.horizontal",
102
+ "grid.vertical",
103
+ "grid.theta",
104
+ "grid.radial",
84
105
  "legend.color",
85
106
  "legend.size",
86
107
  "legend.opacity",
@@ -99,14 +120,15 @@ export function parseSemanticPath(property, { allowContainer = false } = {}) {
99
120
  }
100
121
 
101
122
  if (allowContainer) {
102
- const datasetMatch = property.match(
103
- new RegExp(`^dataset\\[(${USER_ID_SOURCE})\\]$`)
123
+ const entityMatch = property.match(
124
+ new RegExp(`^(dataset|layer)\\[(${USER_ID_SOURCE})\\]$`)
104
125
  );
105
- if (datasetMatch) {
126
+ if (entityMatch) {
127
+ const [, kind, id] = entityMatch;
106
128
  return Object.freeze({
107
- kind: "dataset",
108
- id: datasetMatch[1],
109
- collection: "datasets",
129
+ kind,
130
+ id,
131
+ collection: ENTITY_PATHS[kind].collection,
110
132
  path: Object.freeze([])
111
133
  });
112
134
  }
@@ -1,6 +1,8 @@
1
1
  import { cloneAndFreeze } from "../core/immutable.js";
2
2
  import { COLOR_LAYOUTS } from "../core/vocabulary.js";
3
3
 
4
+ export const DEFAULT_SERIES_BASELINE = 0;
5
+
4
6
  export function validateColorLayout(layout) {
5
7
  if (!COLOR_LAYOUTS.includes(layout)) {
6
8
  throw new Error(`Unsupported color layout "${layout}".`);
@@ -14,7 +16,11 @@ function validateValues(values) {
14
16
  }
15
17
  }
16
18
 
17
- export function layoutSeriesPartition(values, layout, { baseline = 0 } = {}) {
19
+ export function layoutSeriesPartition(
20
+ values,
21
+ layout,
22
+ { baseline = DEFAULT_SERIES_BASELINE } = {}
23
+ ) {
18
24
  validateValues(values);
19
25
  validateColorLayout(layout);
20
26
  if (!Number.isFinite(baseline)) {
@@ -66,7 +72,12 @@ export function resolveSeriesLayoutDomainValues(partitions, layout) {
66
72
  for (const partition of partitions) validateValues(partition);
67
73
 
68
74
  if (layout === "fill") return [0, 1];
69
- if (layout === "group" || layout === "overlay") return partitions.flat();
75
+ if (layout === "group" || layout === "overlay") {
76
+ return partitions.flatMap(partition => [
77
+ DEFAULT_SERIES_BASELINE,
78
+ ...partition
79
+ ]);
80
+ }
70
81
  return partitions.flatMap(partition =>
71
82
  layoutSeriesPartition(partition, layout).flatMap(segment => [
72
83
  segment.start,
@@ -0,0 +1,71 @@
1
+ import { cloneAndFreeze } from "../core/immutable.js";
2
+ import { validateConcreteGraphicValue } from "./schemas/concreteGraphic.js";
3
+
4
+ export const DEFAULT_TEXT_MARK = cloneAndFreeze({
5
+ fill: "#334155",
6
+ opacity: 1,
7
+ fontSize: 12,
8
+ fontFamily: "sans-serif",
9
+ fontWeight: "normal",
10
+ align: "left",
11
+ baseline: "alphabetic",
12
+ rotation: 0,
13
+ dx: 0,
14
+ dy: 0
15
+ });
16
+
17
+ const FIXED_FORMAT = /^\.(\d{1,2})f$/;
18
+
19
+ export function validateTextFormat(format) {
20
+ if (format === undefined || format === "auto") return format ?? "auto";
21
+ if (typeof format !== "string" || !FIXED_FORMAT.test(format)) {
22
+ throw new Error(
23
+ 'Text format must be "auto" or a fixed-decimal token such as ".1f".'
24
+ );
25
+ }
26
+ const decimals = Number(format.match(FIXED_FORMAT)[1]);
27
+ if (decimals > 12) {
28
+ throw new RangeError("Text fixed-decimal format supports at most 12 decimals.");
29
+ }
30
+ return format;
31
+ }
32
+
33
+ export function formatTextValue(value, format = "auto") {
34
+ const resolved = validateTextFormat(format);
35
+ if (value === undefined || value === null) return undefined;
36
+ if (resolved === "auto") {
37
+ const text = String(value);
38
+ return text.length === 0 ? undefined : text;
39
+ }
40
+ if (!Number.isFinite(value)) {
41
+ throw new TypeError(`Text format "${resolved}" requires a finite number.`);
42
+ }
43
+ return value.toFixed(Number(resolved.match(FIXED_FORMAT)[1]));
44
+ }
45
+
46
+ export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
47
+ const config = { ...base };
48
+ const mapping = {
49
+ fill: "fill",
50
+ opacity: "opacity",
51
+ fontSize: "fontSize",
52
+ fontFamily: "fontFamily",
53
+ fontWeight: "fontWeight",
54
+ align: "textAlign",
55
+ baseline: "textBaseline",
56
+ rotation: "rotation"
57
+ };
58
+ for (const [option, property] of Object.entries(mapping)) {
59
+ if (!Object.hasOwn(options, option)) continue;
60
+ validateConcreteGraphicValue("text", property, options[option]);
61
+ config[option] = options[option];
62
+ }
63
+ for (const property of ["dx", "dy"]) {
64
+ if (!Object.hasOwn(options, property)) continue;
65
+ if (!Number.isFinite(options[property])) {
66
+ throw new TypeError(`Text ${property} must be a finite number.`);
67
+ }
68
+ config[property] = options[property];
69
+ }
70
+ return cloneAndFreeze(config);
71
+ }
@@ -125,13 +125,7 @@ function pad(value) {
125
125
  return String(value).padStart(2, "0");
126
126
  }
127
127
 
128
- export function formatTimeTick(value, domain) {
129
- if (!Number.isFinite(value)) {
130
- throw new TypeError("Time tick value must be a finite timestamp.");
131
- }
132
-
133
- const [low, high] = validateTimeDomain(domain);
134
- const span = high - low;
128
+ function formatTimePrecision(value, precision) {
135
129
  const date = new Date(value);
136
130
  const year = String(date.getUTCFullYear()).padStart(4, "0");
137
131
  const month = pad(date.getUTCMonth() + 1);
@@ -139,11 +133,54 @@ export function formatTimeTick(value, domain) {
139
133
  const hour = pad(date.getUTCHours());
140
134
  const minute = pad(date.getUTCMinutes());
141
135
  const second = pad(date.getUTCSeconds());
136
+ const millisecond = String(date.getUTCMilliseconds()).padStart(3, "0");
137
+
138
+ if (precision === 0) return year;
139
+ if (precision === 1) return `${year}-${month}`;
140
+ if (precision === 2) return `${year}-${month}-${day}`;
141
+ if (precision === 3) return `${year}-${month}-${day} ${hour}:${minute}`;
142
+ if (precision === 4) return `${hour}:${minute}`;
143
+ if (precision === 5) return `${hour}:${minute}:${second}`;
144
+ return `${hour}:${minute}:${second}.${millisecond}`;
145
+ }
146
+
147
+ function timePrecision(domain) {
148
+ const [low, high] = validateTimeDomain(domain);
149
+ const span = high - low;
150
+
151
+ if (span >= 2 * YEAR) return 0;
152
+ if (span >= 60 * DAY) return 1;
153
+ if (span >= 2 * DAY) return 2;
154
+ if (span >= 2 * HOUR) return 3;
155
+ if (span >= 2 * MINUTE) return 4;
156
+ return 5;
157
+ }
158
+
159
+ export function formatTimeTick(value, domain) {
160
+ if (!Number.isFinite(value)) {
161
+ throw new TypeError("Time tick value must be a finite timestamp.");
162
+ }
163
+ return formatTimePrecision(value, timePrecision(domain));
164
+ }
165
+
166
+ function distinguishesTicks(values, labels) {
167
+ const timestamps = new Map();
168
+ for (let index = 0; index < values.length; index += 1) {
169
+ const previous = timestamps.get(labels[index]);
170
+ if (previous !== undefined && previous !== values[index]) return false;
171
+ timestamps.set(labels[index], values[index]);
172
+ }
173
+ return true;
174
+ }
142
175
 
143
- if (span >= 2 * YEAR) return year;
144
- if (span >= 60 * DAY) return `${year}-${month}`;
145
- if (span >= 2 * DAY) return `${year}-${month}-${day}`;
146
- if (span >= 2 * HOUR) return `${year}-${month}-${day} ${hour}:00`;
147
- if (span >= 2 * MINUTE) return `${hour}:${minute}`;
148
- return `${hour}:${minute}:${second}`;
176
+ export function formatTimeTicks(values, domain) {
177
+ if (!Array.isArray(values) || !values.every(Number.isFinite)) {
178
+ throw new TypeError("Time tick values must be finite timestamps.");
179
+ }
180
+ const initial = timePrecision(domain);
181
+ for (let precision = initial; precision <= 6; precision += 1) {
182
+ const labels = values.map(value => formatTimePrecision(value, precision));
183
+ if (distinguishesTicks(values, labels)) return cloneAndFreeze(labels);
184
+ }
185
+ return cloneAndFreeze(values.map(value => String(value)));
149
186
  }
package/src/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { chart } from "./ChartProgram.js";
2
+ export { hconcat, vconcat } from "./composition.js";
2
3
  export { render } from "./renderers/canvas/index.js";