ggaction 0.0.1

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 (222) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/package.json +84 -0
  5. package/src/AGENTS.md +119 -0
  6. package/src/ChartProgram.js +10 -0
  7. package/src/actions/boxPlots/components.js +88 -0
  8. package/src/actions/boxPlots/create.js +84 -0
  9. package/src/actions/boxPlots/index.js +10 -0
  10. package/src/actions/boxPlots/materialize.js +183 -0
  11. package/src/actions/boxPlots/options.js +147 -0
  12. package/src/actions/boxPlots/resolve.js +38 -0
  13. package/src/actions/canvas/actions.js +144 -0
  14. package/src/actions/canvas/index.js +6 -0
  15. package/src/actions/coordinates/actions.js +85 -0
  16. package/src/actions/coordinates/index.js +5 -0
  17. package/src/actions/data/box.js +40 -0
  18. package/src/actions/data/create.js +33 -0
  19. package/src/actions/data/density.js +62 -0
  20. package/src/actions/data/derived.js +52 -0
  21. package/src/actions/data/filter.js +168 -0
  22. package/src/actions/data/index.js +32 -0
  23. package/src/actions/data/interval.js +69 -0
  24. package/src/actions/data/regression.js +71 -0
  25. package/src/actions/data/shared.js +25 -0
  26. package/src/actions/encodings/appearance.js +243 -0
  27. package/src/actions/encodings/barWidth.js +60 -0
  28. package/src/actions/encodings/color.js +329 -0
  29. package/src/actions/encodings/density.js +225 -0
  30. package/src/actions/encodings/histogram.js +50 -0
  31. package/src/actions/encodings/index.js +23 -0
  32. package/src/actions/encodings/offset.js +104 -0
  33. package/src/actions/encodings/position/policies/area.js +24 -0
  34. package/src/actions/encodings/position/policies/bar.js +110 -0
  35. package/src/actions/encodings/position/policies/common.js +17 -0
  36. package/src/actions/encodings/position/policies/index.js +25 -0
  37. package/src/actions/encodings/position/policies/line.js +74 -0
  38. package/src/actions/encodings/position/policies/point.js +19 -0
  39. package/src/actions/encodings/position/policies/rule.js +17 -0
  40. package/src/actions/encodings/position/resolve.js +170 -0
  41. package/src/actions/encodings/position.js +176 -0
  42. package/src/actions/encodings/ranged.js +276 -0
  43. package/src/actions/encodings/ruleAppearance.js +47 -0
  44. package/src/actions/encodings/shared.js +164 -0
  45. package/src/actions/encodings/strokeDash.js +145 -0
  46. package/src/actions/errorBands/components.js +81 -0
  47. package/src/actions/errorBands/create.js +159 -0
  48. package/src/actions/errorBands/index.js +7 -0
  49. package/src/actions/errorBands/resolve.js +28 -0
  50. package/src/actions/errorBars/components.js +62 -0
  51. package/src/actions/errorBars/create.js +144 -0
  52. package/src/actions/errorBars/index.js +7 -0
  53. package/src/actions/errorBars/resolve.js +25 -0
  54. package/src/actions/guides/axes/axes.js +204 -0
  55. package/src/actions/guides/axes/axis.js +131 -0
  56. package/src/actions/guides/axes/index.js +17 -0
  57. package/src/actions/guides/axes/labels.js +231 -0
  58. package/src/actions/guides/axes/lines.js +149 -0
  59. package/src/actions/guides/axes/policy.js +155 -0
  60. package/src/actions/guides/axes/tickGroups.js +149 -0
  61. package/src/actions/guides/axes/ticks.js +179 -0
  62. package/src/actions/guides/axes/titles.js +264 -0
  63. package/src/actions/guides/grids/grid.js +228 -0
  64. package/src/actions/guides/grids/index.js +1 -0
  65. package/src/actions/guides/grids/resolve.js +258 -0
  66. package/src/actions/guides/guides.js +138 -0
  67. package/src/actions/guides/index.js +11 -0
  68. package/src/actions/guides/legends/categorical/actions.js +329 -0
  69. package/src/actions/guides/legends/categorical/components.js +140 -0
  70. package/src/actions/guides/legends/categorical/index.js +49 -0
  71. package/src/actions/guides/legends/categorical/layout.js +445 -0
  72. package/src/actions/guides/legends/categorical/options.js +188 -0
  73. package/src/actions/guides/legends/categorical/recipes.js +120 -0
  74. package/src/actions/guides/legends/categorical/resolve.js +131 -0
  75. package/src/actions/guides/legends/categorical/symbols.js +280 -0
  76. package/src/actions/guides/legends/categorical/validation.js +27 -0
  77. package/src/actions/guides/legends/continuous/common.js +273 -0
  78. package/src/actions/guides/legends/continuous/gradient.js +363 -0
  79. package/src/actions/guides/legends/continuous/index.js +25 -0
  80. package/src/actions/guides/legends/continuous/interval.js +234 -0
  81. package/src/actions/guides/legends/continuous/opacity.js +382 -0
  82. package/src/actions/guides/legends/edit.js +314 -0
  83. package/src/actions/guides/legends/index.js +9 -0
  84. package/src/actions/guides/legends/size.js +220 -0
  85. package/src/actions/guides/tickValues.js +84 -0
  86. package/src/actions/index.js +31 -0
  87. package/src/actions/intervals/resolve.js +352 -0
  88. package/src/actions/marks/area.js +369 -0
  89. package/src/actions/marks/bar/create.js +46 -0
  90. package/src/actions/marks/bar/edit.js +90 -0
  91. package/src/actions/marks/bar/index.js +9 -0
  92. package/src/actions/marks/bar/materialize.js +130 -0
  93. package/src/actions/marks/index.js +13 -0
  94. package/src/actions/marks/line.js +248 -0
  95. package/src/actions/marks/point.js +420 -0
  96. package/src/actions/marks/rule.js +287 -0
  97. package/src/actions/marks/shared.js +112 -0
  98. package/src/actions/primitives/createGraphics.js +163 -0
  99. package/src/actions/primitives/editGraphics.js +208 -0
  100. package/src/actions/primitives/index.js +9 -0
  101. package/src/actions/primitives/semantic.js +168 -0
  102. package/src/actions/primitives/semanticValue.js +182 -0
  103. package/src/actions/regression/components.js +216 -0
  104. package/src/actions/regression/create.js +102 -0
  105. package/src/actions/regression/index.js +23 -0
  106. package/src/actions/regression/resolve.js +57 -0
  107. package/src/actions/scales/consumers.js +283 -0
  108. package/src/actions/scales/create.js +148 -0
  109. package/src/actions/scales/definitions.js +319 -0
  110. package/src/actions/scales/edit.js +290 -0
  111. package/src/actions/scales/index.js +11 -0
  112. package/src/actions/scales/materialize.js +466 -0
  113. package/src/actions/scales/quantitativeColor.js +45 -0
  114. package/src/actions/selection/actions.js +384 -0
  115. package/src/actions/selection/index.js +23 -0
  116. package/src/actions/titles/actions.js +235 -0
  117. package/src/actions/titles/index.js +19 -0
  118. package/src/actions/titles/resolve.js +321 -0
  119. package/src/core/ChartProgram.js +316 -0
  120. package/src/core/action.js +120 -0
  121. package/src/core/identifiers.js +28 -0
  122. package/src/core/immutable.js +94 -0
  123. package/src/core/specs.js +29 -0
  124. package/src/core/textMetrics.js +23 -0
  125. package/src/core/validation.js +54 -0
  126. package/src/core/vocabulary.js +58 -0
  127. package/src/extension.js +2 -0
  128. package/src/grammar/aggregate.js +259 -0
  129. package/src/grammar/areaSeries.js +217 -0
  130. package/src/grammar/bars/aggregate.js +98 -0
  131. package/src/grammar/bars/geometry.js +54 -0
  132. package/src/grammar/bars/policy.js +90 -0
  133. package/src/grammar/boxPlot.js +140 -0
  134. package/src/grammar/coordinates.js +25 -0
  135. package/src/grammar/curveCommands.js +356 -0
  136. package/src/grammar/density.js +296 -0
  137. package/src/grammar/filter.js +169 -0
  138. package/src/grammar/histogram.js +332 -0
  139. package/src/grammar/interval.js +343 -0
  140. package/src/grammar/lineSeries.js +197 -0
  141. package/src/grammar/markFilter.js +46 -0
  142. package/src/grammar/markSelection.js +326 -0
  143. package/src/grammar/palettes.js +208 -0
  144. package/src/grammar/pathCommands.js +86 -0
  145. package/src/grammar/pointShapes.js +164 -0
  146. package/src/grammar/positionCompatibility.js +32 -0
  147. package/src/grammar/regression.js +578 -0
  148. package/src/grammar/ruleAppearance.js +12 -0
  149. package/src/grammar/rules.js +90 -0
  150. package/src/grammar/scales/appearance.js +185 -0
  151. package/src/grammar/scales/color.js +349 -0
  152. package/src/grammar/scales/continuous.js +111 -0
  153. package/src/grammar/scales/definition.js +153 -0
  154. package/src/grammar/scales/discretized.js +159 -0
  155. package/src/grammar/scales/fields.js +133 -0
  156. package/src/grammar/scales/index.js +12 -0
  157. package/src/grammar/scales/mapping.js +27 -0
  158. package/src/grammar/scales/ordinal.js +221 -0
  159. package/src/grammar/scales/policies.js +50 -0
  160. package/src/grammar/scales/temporal.js +53 -0
  161. package/src/grammar/scales/transformed.js +236 -0
  162. package/src/grammar/scales/types.js +102 -0
  163. package/src/grammar/scales/validation.js +66 -0
  164. package/src/grammar/scales.js +1 -0
  165. package/src/grammar/schemas/concreteGraphic.js +73 -0
  166. package/src/grammar/schemas/graphic.js +131 -0
  167. package/src/grammar/schemas/graphicBounds.js +146 -0
  168. package/src/grammar/schemas/graphicTree.js +133 -0
  169. package/src/grammar/schemas/mark.js +4 -0
  170. package/src/grammar/schemas/semanticPath.js +174 -0
  171. package/src/grammar/seriesLayout.js +76 -0
  172. package/src/grammar/ticks.js +149 -0
  173. package/src/grammar/transforms.js +37 -0
  174. package/src/index.js +2 -0
  175. package/src/layout/canvas.js +114 -0
  176. package/src/layout/grid.js +17 -0
  177. package/src/layout/legend.js +51 -0
  178. package/src/layout/text.js +62 -0
  179. package/src/layout/title.js +107 -0
  180. package/src/materialization/bars/aggregate.js +184 -0
  181. package/src/materialization/bars/grouped.js +117 -0
  182. package/src/materialization/bars/histogram.js +163 -0
  183. package/src/materialization/bars/ranged.js +53 -0
  184. package/src/materialization/bars/resolve.js +56 -0
  185. package/src/materialization/dataProvenance.js +15 -0
  186. package/src/materialization/density.js +29 -0
  187. package/src/materialization/dependencies.js +134 -0
  188. package/src/materialization/encodings.js +50 -0
  189. package/src/materialization/graphicHierarchy.js +104 -0
  190. package/src/materialization/legends.js +20 -0
  191. package/src/materialization/marks.js +110 -0
  192. package/src/materialization/planner.js +38 -0
  193. package/src/materialization/selection/filter.js +40 -0
  194. package/src/materialization/selection/items.js +503 -0
  195. package/src/materialization/selection/path.js +37 -0
  196. package/src/materialization/selection/point.js +118 -0
  197. package/src/materialization/selection/policies/area.js +10 -0
  198. package/src/materialization/selection/policies/bar.js +10 -0
  199. package/src/materialization/selection/policies/index.js +40 -0
  200. package/src/materialization/selection/policies/line.js +10 -0
  201. package/src/materialization/selection/policies/point.js +10 -0
  202. package/src/materialization/selection/policies/rule.js +10 -0
  203. package/src/materialization/selection/state.js +67 -0
  204. package/src/materialization/selection/styles.js +183 -0
  205. package/src/renderers/canvas/circle.js +62 -0
  206. package/src/renderers/canvas/index.js +132 -0
  207. package/src/renderers/canvas/line.js +54 -0
  208. package/src/renderers/canvas/path.js +98 -0
  209. package/src/renderers/canvas/rect.js +57 -0
  210. package/src/renderers/canvas/text.js +96 -0
  211. package/src/renderers/canvas/validation.js +23 -0
  212. package/src/renderers/png.js +29 -0
  213. package/src/selectors/coordinates.js +13 -0
  214. package/src/selectors/datasets.js +13 -0
  215. package/src/selectors/index.js +4 -0
  216. package/src/selectors/layers.js +32 -0
  217. package/src/selectors/scales.js +21 -0
  218. package/src/theme/defaults.js +13 -0
  219. package/types/extension.d.ts +14 -0
  220. package/types/index.d.ts +81 -0
  221. package/types/png.d.ts +14 -0
  222. package/types/program.d.ts +1096 -0
@@ -0,0 +1,40 @@
1
+ import { findDataset } from "../../../selectors/datasets.js";
2
+ import { requireLayer } from "../../../selectors/layers.js";
3
+ import { areaSelectionPolicy } from "./area.js";
4
+ import { barSelectionPolicy } from "./bar.js";
5
+ import { lineSelectionPolicy } from "./line.js";
6
+ import { pointSelectionPolicy } from "./point.js";
7
+ import { ruleSelectionPolicy } from "./rule.js";
8
+
9
+ const POLICIES = Object.freeze({
10
+ area: areaSelectionPolicy,
11
+ bar: barSelectionPolicy,
12
+ line: lineSelectionPolicy,
13
+ point: pointSelectionPolicy,
14
+ rule: ruleSelectionPolicy
15
+ });
16
+
17
+ export function findSelectionPolicy(markType) {
18
+ return POLICIES[markType];
19
+ }
20
+
21
+ export function requireSelectionPolicy(markType) {
22
+ const policy = findSelectionPolicy(markType);
23
+ if (policy === undefined) {
24
+ throw new Error(`Mark type "${markType}" has no selection policy.`);
25
+ }
26
+ return policy;
27
+ }
28
+
29
+ export function resolveMarkItems(program, target, grain = "item") {
30
+ const layer = requireLayer(program, target, `Unknown mark target "${target}"`);
31
+ const dataset = findDataset(program, layer.data);
32
+ if (dataset === undefined) {
33
+ throw new Error(`Mark "${target}" requires an existing dataset for selection.`);
34
+ }
35
+ const policy = requireSelectionPolicy(layer.mark?.type);
36
+ if (!policy.supportedGrains.includes(grain)) {
37
+ throw new Error(`Mark "${target}" does not support ${grain} selection grain.`);
38
+ }
39
+ return policy.resolveItems(program, layer, dataset, grain);
40
+ }
@@ -0,0 +1,10 @@
1
+ import { resolveLineItems } from "../items.js";
2
+ import { normalizeStrokeHighlightStyle } from "../styles.js";
3
+
4
+ export const lineSelectionPolicy = Object.freeze({
5
+ supportedGrains: Object.freeze(["item"]),
6
+ resolveItems: resolveLineItems,
7
+ normalizeHighlightStyle: args => normalizeStrokeHighlightStyle(args, "Line"),
8
+ applyHighlightOp: "applyPathHighlight",
9
+ rematerializeOp: "rematerializeLineMark"
10
+ });
@@ -0,0 +1,10 @@
1
+ import { resolvePointItems } from "../items.js";
2
+ import { normalizePointHighlightStyle } from "../styles.js";
3
+
4
+ export const pointSelectionPolicy = Object.freeze({
5
+ supportedGrains: Object.freeze(["item"]),
6
+ resolveItems: resolvePointItems,
7
+ normalizeHighlightStyle: normalizePointHighlightStyle,
8
+ applyHighlightOp: "applyPointHighlight",
9
+ rematerializeOp: "rematerializePointMark"
10
+ });
@@ -0,0 +1,10 @@
1
+ import { resolveRuleItems } from "../items.js";
2
+ import { normalizeStrokeHighlightStyle } from "../styles.js";
3
+
4
+ export const ruleSelectionPolicy = Object.freeze({
5
+ supportedGrains: Object.freeze(["item"]),
6
+ resolveItems: resolveRuleItems,
7
+ normalizeHighlightStyle: args => normalizeStrokeHighlightStyle(args, "Rule"),
8
+ applyHighlightOp: "applyRuleHighlight",
9
+ rematerializeOp: "rematerializeRuleMark"
10
+ });
@@ -0,0 +1,67 @@
1
+ import { validateUserId } from "../../core/identifiers.js";
2
+ import { normalizeMarkSelector, selectMarkItemKeys } from "../../grammar/markSelection.js";
3
+ import { resolveMarkItems } from "./policies/index.js";
4
+
5
+ export function resolveMarkSelection(program, target, selector) {
6
+ const normalized = normalizeMarkSelector(selector);
7
+ const items = resolveMarkItems(program, target, normalized.grain);
8
+ if (items.length > 0) {
9
+ const source = normalized.field !== undefined
10
+ ? "fields"
11
+ : normalized.channel !== undefined
12
+ ? "channels"
13
+ : "properties";
14
+ const kind = normalized.field !== undefined
15
+ ? "field"
16
+ : normalized.channel !== undefined
17
+ ? "channel"
18
+ : "graphic property";
19
+ const key = normalized.field ?? normalized.channel ?? normalized.property;
20
+ if (!items.some(item => Object.hasOwn(item[source], key))) {
21
+ throw new Error(
22
+ `Selection ${kind} "${key}" is not uniquely defined at the target ${normalized.grain} grain.`
23
+ );
24
+ }
25
+ }
26
+ return Object.freeze({
27
+ selector: normalized,
28
+ items,
29
+ keys: selectMarkItemKeys(items, normalized)
30
+ });
31
+ }
32
+
33
+ export function resolveSelectionCreationId(program, id, target) {
34
+ const resolved = validateUserId(
35
+ id ?? `${target}Selection`,
36
+ "Selection id"
37
+ );
38
+ if (program.materializationConfigs.selections?.[resolved] !== undefined) {
39
+ throw new Error(`Selection "${resolved}" already exists.`);
40
+ }
41
+ return resolved;
42
+ }
43
+
44
+ export function resolveStoredSelection(program, id) {
45
+ const selections = program.materializationConfigs.selections ?? {};
46
+ const requested = id ?? program.context.currentSelection;
47
+ const resolvedId = requested ?? (
48
+ Object.keys(selections).length === 1 ? Object.keys(selections)[0] : undefined
49
+ );
50
+ if (resolvedId === undefined) {
51
+ if (Object.keys(selections).length === 0) {
52
+ throw new Error("Selection resolution requires an existing selection.");
53
+ }
54
+ throw new Error("Selection is ambiguous; provide selection.");
55
+ }
56
+ validateUserId(resolvedId, "Selection id");
57
+ const definition = selections[resolvedId];
58
+ if (definition === undefined) {
59
+ throw new Error(`Unknown selection "${resolvedId}".`);
60
+ }
61
+ const resolved = resolveMarkSelection(
62
+ program,
63
+ definition.target,
64
+ definition.selector
65
+ );
66
+ return Object.freeze({ id: resolvedId, definition, ...resolved });
67
+ }
@@ -0,0 +1,183 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+ import {
3
+ validateKeys,
4
+ validateNonEmptyString,
5
+ validateNonNegativeFinite,
6
+ validatePositiveFinite,
7
+ validateUnitInterval
8
+ } from "../../core/validation.js";
9
+ import { validatePointShape } from "../../grammar/pointShapes.js";
10
+ import { normalizeStrokeDashPattern } from "../../grammar/scales.js";
11
+ import { DEFAULT_COLORS } from "../../theme/defaults.js";
12
+
13
+ const DEFAULT_POINT_SIZE = 2;
14
+ const DEFAULT_DIM_OPACITY = 0.25;
15
+
16
+ export { validateUnitInterval };
17
+
18
+ function normalizeOffset(offset) {
19
+ if (offset === undefined) return { x: 0, y: 0 };
20
+ if (!isPlainObject(offset)) {
21
+ throw new TypeError("Highlight offset must be a plain object.");
22
+ }
23
+ validateKeys(offset, ["x", "y"], "highlight offset");
24
+ const x = offset.x ?? 0;
25
+ const y = offset.y ?? 0;
26
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
27
+ throw new TypeError("Highlight offset x and y must be finite numbers.");
28
+ }
29
+ return { x, y };
30
+ }
31
+
32
+ export function normalizeDimOthers(dimOthers) {
33
+ if (dimOthers === undefined || dimOthers === false) return false;
34
+ if (dimOthers === true) return { opacity: DEFAULT_DIM_OPACITY };
35
+ if (!isPlainObject(dimOthers)) {
36
+ throw new TypeError("Highlight dimOthers must be a boolean or plain object.");
37
+ }
38
+ validateKeys(dimOthers, ["opacity"], "highlight dimOthers");
39
+ return {
40
+ opacity: validateUnitInterval(
41
+ dimOthers.opacity ?? DEFAULT_DIM_OPACITY,
42
+ "Highlight dim opacity"
43
+ )
44
+ };
45
+ }
46
+
47
+ export function normalizePointHighlightStyle(args) {
48
+ if (args.color !== undefined && args.fill !== undefined) {
49
+ throw new Error("Point highlight accepts color or fill, not both.");
50
+ }
51
+ if (args.strokeDash !== undefined) {
52
+ throw new Error("Point highlight does not support strokeDash.");
53
+ }
54
+ const fill = validateNonEmptyString(
55
+ args.fill ?? args.color ?? DEFAULT_COLORS.highlight,
56
+ "Point highlight fill"
57
+ );
58
+ const opacity = args.opacity === undefined
59
+ ? undefined
60
+ : validateUnitInterval(args.opacity, "Point highlight opacity");
61
+ const stroke = args.stroke === undefined
62
+ ? undefined
63
+ : validateNonEmptyString(args.stroke, "Point highlight stroke");
64
+ const strokeWidth = args.strokeWidth === undefined
65
+ ? undefined
66
+ : validateNonNegativeFinite(args.strokeWidth, "Point highlight strokeWidth");
67
+ if (strokeWidth !== undefined && stroke === undefined) {
68
+ throw new Error("Point highlight strokeWidth requires stroke.");
69
+ }
70
+ return {
71
+ fill,
72
+ ...(opacity === undefined ? {} : { opacity }),
73
+ ...(stroke === undefined ? {} : { stroke }),
74
+ ...(strokeWidth === undefined ? {} : { strokeWidth }),
75
+ ...(args.shape === undefined ? {} : { shape: validatePointShape(args.shape) }),
76
+ size: args.size === undefined
77
+ ? DEFAULT_POINT_SIZE
78
+ : validatePositiveFinite(args.size, "Point highlight size"),
79
+ offset: normalizeOffset(args.offset)
80
+ };
81
+ }
82
+
83
+ export function normalizeBarHighlightStyle(args) {
84
+ for (const option of ["shape", "size", "offset", "strokeDash"]) {
85
+ if (args[option] !== undefined) {
86
+ throw new Error(`Bar highlight does not support ${option}.`);
87
+ }
88
+ }
89
+ if (args.color !== undefined && args.fill !== undefined) {
90
+ throw new Error("Bar highlight accepts color or fill, not both.");
91
+ }
92
+ const fill = validateNonEmptyString(
93
+ args.fill ?? args.color ?? DEFAULT_COLORS.highlight,
94
+ "Bar highlight fill"
95
+ );
96
+ const opacity = args.opacity === undefined
97
+ ? undefined
98
+ : validateUnitInterval(args.opacity, "Bar highlight opacity");
99
+ const stroke = args.stroke === undefined
100
+ ? undefined
101
+ : validateNonEmptyString(args.stroke, "Bar highlight stroke");
102
+ const strokeWidth = args.strokeWidth === undefined
103
+ ? undefined
104
+ : validateNonNegativeFinite(args.strokeWidth, "Bar highlight strokeWidth");
105
+ if (strokeWidth !== undefined && stroke === undefined) {
106
+ throw new Error("Bar highlight strokeWidth requires stroke.");
107
+ }
108
+ return {
109
+ fill,
110
+ ...(opacity === undefined ? {} : { opacity }),
111
+ ...(stroke === undefined ? {} : { stroke }),
112
+ ...(strokeWidth === undefined ? {} : { strokeWidth })
113
+ };
114
+ }
115
+
116
+ function rejectHighlightOptions(args, mark, options) {
117
+ for (const option of options) {
118
+ if (args[option] !== undefined) {
119
+ throw new Error(`${mark} highlight does not support ${option}.`);
120
+ }
121
+ }
122
+ }
123
+
124
+ export function normalizeStrokeHighlightStyle(args, mark) {
125
+ rejectHighlightOptions(args, mark, ["fill", "shape", "size"]);
126
+ if (args.color !== undefined && args.stroke !== undefined) {
127
+ throw new Error(`${mark} highlight accepts color or stroke, not both.`);
128
+ }
129
+ return {
130
+ stroke: validateNonEmptyString(
131
+ args.stroke ?? args.color ?? DEFAULT_COLORS.highlight,
132
+ `${mark} highlight stroke`
133
+ ),
134
+ ...(args.strokeWidth === undefined
135
+ ? {}
136
+ : {
137
+ strokeWidth: validateNonNegativeFinite(
138
+ args.strokeWidth,
139
+ `${mark} highlight strokeWidth`
140
+ )
141
+ }),
142
+ ...(args.strokeDash === undefined
143
+ ? {}
144
+ : { strokeDash: normalizeStrokeDashPattern(args.strokeDash) }),
145
+ ...(args.opacity === undefined
146
+ ? {}
147
+ : {
148
+ opacity: validateUnitInterval(
149
+ args.opacity,
150
+ `${mark} highlight opacity`
151
+ )
152
+ }),
153
+ offset: normalizeOffset(args.offset)
154
+ };
155
+ }
156
+
157
+ export function normalizeAreaHighlightStyle(args) {
158
+ rejectHighlightOptions(args, "Area", ["shape", "size", "strokeDash"]);
159
+ if (args.color !== undefined && args.fill !== undefined) {
160
+ throw new Error("Area highlight accepts color or fill, not both.");
161
+ }
162
+ const stroke = args.stroke === undefined
163
+ ? undefined
164
+ : validateNonEmptyString(args.stroke, "Area highlight stroke");
165
+ const strokeWidth = args.strokeWidth === undefined
166
+ ? undefined
167
+ : validateNonNegativeFinite(args.strokeWidth, "Area highlight strokeWidth");
168
+ if (strokeWidth !== undefined && stroke === undefined) {
169
+ throw new Error("Area highlight strokeWidth requires stroke.");
170
+ }
171
+ return {
172
+ fill: validateNonEmptyString(
173
+ args.fill ?? args.color ?? DEFAULT_COLORS.highlight,
174
+ "Area highlight fill"
175
+ ),
176
+ ...(args.opacity === undefined
177
+ ? {}
178
+ : { opacity: validateUnitInterval(args.opacity, "Area highlight opacity") }),
179
+ ...(stroke === undefined ? {} : { stroke }),
180
+ ...(strokeWidth === undefined ? {} : { strokeWidth }),
181
+ offset: normalizeOffset(args.offset)
182
+ };
183
+ }
@@ -0,0 +1,62 @@
1
+ import { requireFiniteProperty } from "./validation.js";
2
+ import { validateConcreteGraphicProperties } from
3
+ "../../grammar/schemas/concreteGraphic.js";
4
+
5
+ function drawCircle(context, child, collectionId) {
6
+ const properties = child.properties ?? {};
7
+ const graphicId = child.id ?? collectionId;
8
+ validateConcreteGraphicProperties("circle", properties);
9
+ const x = requireFiniteProperty(properties, "x", graphicId);
10
+ const y = requireFiniteProperty(properties, "y", graphicId);
11
+ const radius = requireFiniteProperty(properties, "radius", graphicId);
12
+
13
+ if (radius < 0) {
14
+ throw new Error(`Graphic "${graphicId}" requires a non-negative radius.`);
15
+ }
16
+
17
+ if (typeof properties.fill !== "string") {
18
+ throw new Error(`Graphic "${graphicId}" requires a string fill property.`);
19
+ }
20
+
21
+ const opacity = properties.opacity ?? 1;
22
+
23
+ if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
24
+ throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
25
+ }
26
+
27
+ context.fillStyle = properties.fill;
28
+ context.globalAlpha = opacity;
29
+ context.beginPath();
30
+ context.arc(x, y, radius, 0, Math.PI * 2);
31
+ context.fill();
32
+ if (properties.stroke !== undefined) {
33
+ if (typeof properties.stroke !== "string") {
34
+ throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
35
+ }
36
+ const strokeWidth = requireFiniteProperty(
37
+ properties,
38
+ "strokeWidth",
39
+ graphicId
40
+ );
41
+ if (strokeWidth < 0) {
42
+ throw new Error(
43
+ `Graphic "${graphicId}" requires a non-negative strokeWidth.`
44
+ );
45
+ }
46
+ context.strokeStyle = properties.stroke;
47
+ context.lineWidth = strokeWidth;
48
+ context.setLineDash([]);
49
+ context.stroke();
50
+ }
51
+ }
52
+
53
+ export function drawCircleGraphic(context, id, graphic) {
54
+ if (graphic.items) {
55
+ for (const item of graphic.items) {
56
+ drawCircle(context, item, id);
57
+ }
58
+ return;
59
+ }
60
+
61
+ drawCircle(context, { id, properties: graphic.properties }, id);
62
+ }
@@ -0,0 +1,132 @@
1
+ import { drawCircleGraphic } from "./circle.js";
2
+ import { drawLineGraphic } from "./line.js";
3
+ import { drawPathGraphic } from "./path.js";
4
+ import { drawRectGraphic } from "./rect.js";
5
+ import { drawTextGraphic } from "./text.js";
6
+ import { requireFiniteProperty } from "./validation.js";
7
+ import { walkGraphicDrawOrder } from "../../grammar/schemas/graphicTree.js";
8
+
9
+ const DRAWERS = Object.freeze({
10
+ circle: drawCircleGraphic,
11
+ rect: drawRectGraphic,
12
+ line: drawLineGraphic,
13
+ text: drawTextGraphic,
14
+ path: drawPathGraphic
15
+ });
16
+
17
+ function requireCanvasContext(context) {
18
+ const methods = [
19
+ "save",
20
+ "restore",
21
+ "clearRect",
22
+ "fillRect",
23
+ "beginPath",
24
+ "closePath",
25
+ "arc",
26
+ "fill",
27
+ "moveTo",
28
+ "lineTo",
29
+ "bezierCurveTo",
30
+ "setLineDash",
31
+ "stroke",
32
+ "translate",
33
+ "rotate",
34
+ "fillText",
35
+ "scale"
36
+ ];
37
+
38
+ if (context === null || typeof context !== "object" || !context.canvas) {
39
+ throw new TypeError("render requires a Canvas 2D context.");
40
+ }
41
+
42
+ for (const method of methods) {
43
+ if (typeof context[method] !== "function") {
44
+ throw new TypeError(`Canvas context is missing ${method}().`);
45
+ }
46
+ }
47
+ }
48
+
49
+ function findCanvas(graphicSpec) {
50
+ const canvasIds = graphicSpec.order.filter(
51
+ id => graphicSpec.objects[id]?.type === "canvas"
52
+ );
53
+
54
+ if (canvasIds.length !== 1) {
55
+ throw new Error("graphicSpec must contain exactly one ordered canvas.");
56
+ }
57
+
58
+ return { id: canvasIds[0], graphic: graphicSpec.objects[canvasIds[0]] };
59
+ }
60
+
61
+ export function render(program, context, { pixelRatio = 1 } = {}) {
62
+ const graphicSpec = program?.graphicSpec;
63
+
64
+ if (
65
+ graphicSpec === null ||
66
+ typeof graphicSpec !== "object" ||
67
+ graphicSpec.objects === null ||
68
+ typeof graphicSpec.objects !== "object" ||
69
+ !Array.isArray(graphicSpec.order)
70
+ ) {
71
+ throw new TypeError("render requires a program with a graphicSpec.");
72
+ }
73
+
74
+ requireCanvasContext(context);
75
+
76
+ if (!Number.isFinite(pixelRatio) || pixelRatio <= 0) {
77
+ throw new RangeError("render pixelRatio must be a positive finite number.");
78
+ }
79
+
80
+ const { id: canvasId, graphic: canvas } = findCanvas(graphicSpec);
81
+ const width = requireFiniteProperty(canvas.properties ?? {}, "width", canvasId);
82
+ const height = requireFiniteProperty(
83
+ canvas.properties ?? {},
84
+ "height",
85
+ canvasId
86
+ );
87
+
88
+ if (width < 0 || height < 0) {
89
+ throw new Error("Canvas width and height must not be negative.");
90
+ }
91
+
92
+ context.canvas.width = Math.round(width * pixelRatio);
93
+ context.canvas.height = Math.round(height * pixelRatio);
94
+ context.save();
95
+
96
+ try {
97
+ context.scale(pixelRatio, pixelRatio);
98
+ context.clearRect(0, 0, width, height);
99
+
100
+ if (canvas.properties.background !== undefined) {
101
+ if (typeof canvas.properties.background !== "string") {
102
+ throw new Error(
103
+ `Graphic "${canvasId}" requires a string background property.`
104
+ );
105
+ }
106
+
107
+ context.globalAlpha = 1;
108
+ context.fillStyle = canvas.properties.background;
109
+ context.fillRect(0, 0, width, height);
110
+ }
111
+
112
+ walkGraphicDrawOrder(graphicSpec, ({ id, object }) => {
113
+ if (id !== canvasId) drawConcreteGraphic(context, id, object);
114
+ });
115
+ } finally {
116
+ context.restore();
117
+ }
118
+ }
119
+
120
+ function drawConcreteGraphic(context, id, graphic) {
121
+ if (graphic.type === "collection") {
122
+ for (const item of graphic.items ?? []) {
123
+ drawConcreteGraphic(context, item.id ?? id, item);
124
+ }
125
+ return;
126
+ }
127
+ const draw = DRAWERS[graphic.type];
128
+ if (draw === undefined) {
129
+ throw new Error(`Canvas renderer does not support "${graphic.type}" yet.`);
130
+ }
131
+ draw(context, id, graphic);
132
+ }
@@ -0,0 +1,54 @@
1
+ import { requireFiniteProperty } from "./validation.js";
2
+ import { validateConcreteGraphicProperties } from
3
+ "../../grammar/schemas/concreteGraphic.js";
4
+
5
+ function drawLine(context, child, collectionId) {
6
+ const properties = child.properties ?? {};
7
+ const graphicId = child.id ?? collectionId;
8
+ validateConcreteGraphicProperties("line", properties);
9
+ const x1 = requireFiniteProperty(properties, "x1", graphicId);
10
+ const y1 = requireFiniteProperty(properties, "y1", graphicId);
11
+ const x2 = requireFiniteProperty(properties, "x2", graphicId);
12
+ const y2 = requireFiniteProperty(properties, "y2", graphicId);
13
+ const strokeWidth = requireFiniteProperty(
14
+ properties,
15
+ "strokeWidth",
16
+ graphicId
17
+ );
18
+
19
+ if (strokeWidth < 0) {
20
+ throw new Error(
21
+ `Graphic "${graphicId}" requires a non-negative strokeWidth.`
22
+ );
23
+ }
24
+
25
+ if (typeof properties.stroke !== "string") {
26
+ throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
27
+ }
28
+
29
+ const opacity = properties.opacity ?? 1;
30
+
31
+ if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
32
+ throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
33
+ }
34
+
35
+ context.strokeStyle = properties.stroke;
36
+ context.lineWidth = strokeWidth;
37
+ context.globalAlpha = opacity;
38
+ context.setLineDash(properties.strokeDash ?? []);
39
+ context.beginPath();
40
+ context.moveTo(x1, y1);
41
+ context.lineTo(x2, y2);
42
+ context.stroke();
43
+ }
44
+
45
+ export function drawLineGraphic(context, id, graphic) {
46
+ if (graphic.items) {
47
+ for (const item of graphic.items) {
48
+ drawLine(context, item, id);
49
+ }
50
+ return;
51
+ }
52
+
53
+ drawLine(context, { id, properties: graphic.properties }, id);
54
+ }
@@ -0,0 +1,98 @@
1
+ import { requireFiniteProperty } from "./validation.js";
2
+ import { validateConcreteGraphicProperties } from
3
+ "../../grammar/schemas/concreteGraphic.js";
4
+
5
+ function drawPath(context, child, collectionId) {
6
+ const properties = child.properties ?? {};
7
+ const graphicId = child.id ?? collectionId;
8
+ validateConcreteGraphicProperties("path", properties);
9
+ const commands = properties.commands;
10
+
11
+ if (!Array.isArray(commands)) {
12
+ throw new Error(
13
+ `Graphic "${graphicId}" requires concrete path commands.`
14
+ );
15
+ }
16
+
17
+ const hasFill = properties.fill !== undefined;
18
+ const hasStroke = properties.stroke !== undefined;
19
+ if (!hasFill && !hasStroke) {
20
+ throw new Error(`Graphic "${graphicId}" requires a fill or stroke property.`);
21
+ }
22
+ if (hasFill && typeof properties.fill !== "string") {
23
+ throw new Error(`Graphic "${graphicId}" requires a string fill property.`);
24
+ }
25
+ if (hasFill && commands.at(-1).op !== "Z") {
26
+ throw new Error(`Graphic "${graphicId}" requires a final Z command when filled.`);
27
+ }
28
+ if (hasStroke && typeof properties.stroke !== "string") {
29
+ throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
30
+ }
31
+
32
+ const strokeWidth = hasStroke
33
+ ? requireFiniteProperty(properties, "strokeWidth", graphicId)
34
+ : undefined;
35
+ if (hasStroke && strokeWidth < 0) {
36
+ throw new Error(
37
+ `Graphic "${graphicId}" requires a non-negative strokeWidth.`
38
+ );
39
+ }
40
+
41
+ const strokeDash = properties.strokeDash ?? [];
42
+ if (
43
+ !Array.isArray(strokeDash) ||
44
+ !strokeDash.every(value => Number.isFinite(value) && value >= 0)
45
+ ) {
46
+ throw new Error(
47
+ `Graphic "${graphicId}" requires a non-negative finite strokeDash array.`
48
+ );
49
+ }
50
+
51
+ const opacity = properties.opacity ?? 1;
52
+ if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
53
+ throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
54
+ }
55
+
56
+ context.globalAlpha = opacity;
57
+ context.beginPath();
58
+ for (const command of commands) {
59
+ if (command.op === "M") {
60
+ context.moveTo(command.x, command.y);
61
+ } else if (command.op === "L") {
62
+ context.lineTo(command.x, command.y);
63
+ } else if (command.op === "C") {
64
+ context.bezierCurveTo(
65
+ command.x1,
66
+ command.y1,
67
+ command.x2,
68
+ command.y2,
69
+ command.x,
70
+ command.y
71
+ );
72
+ } else {
73
+ context.closePath();
74
+ }
75
+ }
76
+
77
+ if (hasFill) {
78
+ context.fillStyle = properties.fill;
79
+ context.fill();
80
+ }
81
+ if (hasStroke) {
82
+ context.strokeStyle = properties.stroke;
83
+ context.lineWidth = strokeWidth;
84
+ context.setLineDash(strokeDash);
85
+ context.stroke();
86
+ }
87
+ }
88
+
89
+ export function drawPathGraphic(context, id, graphic) {
90
+ if (graphic.items) {
91
+ for (const item of graphic.items) {
92
+ drawPath(context, item, id);
93
+ }
94
+ return;
95
+ }
96
+
97
+ drawPath(context, { id, properties: graphic.properties }, id);
98
+ }