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,174 @@
1
+ import { USER_ID_SOURCE } from "../../core/identifiers.js";
2
+ import {
3
+ ENCODING_CHANNELS,
4
+ POSITION_CHANNELS,
5
+ SCALED_ENCODING_CHANNELS
6
+ } from "../../core/vocabulary.js";
7
+
8
+ const ENCODING_PATHS = Object.freeze([
9
+ ...ENCODING_CHANNELS.flatMap(channel => [
10
+ `encoding.${channel}.field`,
11
+ `encoding.${channel}.datum`,
12
+ `encoding.${channel}.fieldType`,
13
+ `encoding.${channel}.title`
14
+ ]),
15
+ ...SCALED_ENCODING_CHANNELS.map(channel =>
16
+ `encoding.${channel}.scale`
17
+ ),
18
+ "encoding.x.bin.maxBins",
19
+ "encoding.x.bin.step",
20
+ "encoding.x.bin.boundaries",
21
+ "encoding.color.layout",
22
+ "encoding.color.aggregate",
23
+ ...POSITION_CHANNELS.flatMap(channel => [
24
+ `encoding.${channel}.aggregate`,
25
+ `encoding.${channel}.stack`
26
+ ])
27
+ ]);
28
+
29
+ const ENTITY_PATHS = Object.freeze({
30
+ dataset: {
31
+ collection: "datasets",
32
+ properties: new Set(["source", "transform", "values"])
33
+ },
34
+ layer: {
35
+ collection: "layers",
36
+ removableContainers: new Set(
37
+ ENCODING_CHANNELS.map(channel => `encoding.${channel}`)
38
+ ),
39
+ properties: new Set([
40
+ "data",
41
+ "coordinate",
42
+ "transform",
43
+ "mark.type",
44
+ ...ENCODING_PATHS
45
+ ])
46
+ },
47
+ scale: {
48
+ collection: "scales",
49
+ properties: new Set([
50
+ "type", "domain", "range", "nice", "zero", "clamp", "reverse",
51
+ "unknown", "base", "exponent", "constant", "interpolate",
52
+ "paddingInner", "paddingOuter", "padding", "align"
53
+ ])
54
+ },
55
+ coordinate: {
56
+ collection: "coordinates",
57
+ properties: new Set(["type"])
58
+ }
59
+ });
60
+
61
+ const GUIDE_PATHS = new Set([
62
+ "axis.x.scale",
63
+ "axis.x.coordinate",
64
+ "axis.x.title",
65
+ "axis.y.scale",
66
+ "axis.y.coordinate",
67
+ "axis.y.title",
68
+ "legend.color.scale",
69
+ "legend.color.title",
70
+ "legend.size.scale",
71
+ "legend.size.title",
72
+ "legend.opacity.scale",
73
+ "legend.opacity.title",
74
+ "legend.series.channels",
75
+ "legend.series.scales",
76
+ "legend.series.title",
77
+ "grid.horizontal.scale",
78
+ "grid.horizontal.coordinate",
79
+ "grid.vertical.scale",
80
+ "grid.vertical.coordinate"
81
+ ]);
82
+
83
+ const GUIDE_REMOVABLE_CONTAINERS = new Set([
84
+ "legend.color",
85
+ "legend.size",
86
+ "legend.opacity",
87
+ "legend.series"
88
+ ]);
89
+
90
+ const TITLE_PATHS = new Set(["text", "subtitle"]);
91
+
92
+ function splitPropertyPath(property) {
93
+ return property.split(".");
94
+ }
95
+
96
+ export function parseSemanticPath(property, { allowContainer = false } = {}) {
97
+ if (typeof property !== "string" || property.length === 0) {
98
+ throw new TypeError("editSemantic requires a non-empty property string.");
99
+ }
100
+
101
+ if (allowContainer) {
102
+ const datasetMatch = property.match(
103
+ new RegExp(`^dataset\\[(${USER_ID_SOURCE})\\]$`)
104
+ );
105
+ if (datasetMatch) {
106
+ return Object.freeze({
107
+ kind: "dataset",
108
+ id: datasetMatch[1],
109
+ collection: "datasets",
110
+ path: Object.freeze([])
111
+ });
112
+ }
113
+ }
114
+
115
+ const entityMatch = property.match(
116
+ new RegExp(
117
+ `^(${Object.keys(ENTITY_PATHS).join("|")})\\[(${USER_ID_SOURCE})\\]\\.(.+)$`
118
+ )
119
+ );
120
+
121
+ if (entityMatch) {
122
+ const [, kind, id, propertyPath] = entityMatch;
123
+ const definition = ENTITY_PATHS[kind];
124
+
125
+ if (
126
+ !definition.properties.has(propertyPath) &&
127
+ !(allowContainer && definition.removableContainers?.has(propertyPath))
128
+ ) {
129
+ throw new Error(`Unknown semantic property "${property}".`);
130
+ }
131
+
132
+ return Object.freeze({
133
+ kind,
134
+ id,
135
+ collection: definition.collection,
136
+ path: Object.freeze(splitPropertyPath(propertyPath))
137
+ });
138
+ }
139
+
140
+ if (property.startsWith("guide.")) {
141
+ const propertyPath = property.slice("guide.".length);
142
+
143
+ if (
144
+ !GUIDE_PATHS.has(propertyPath) &&
145
+ !(allowContainer && GUIDE_REMOVABLE_CONTAINERS.has(propertyPath))
146
+ ) {
147
+ throw new Error(`Unknown semantic property "${property}".`);
148
+ }
149
+
150
+ return Object.freeze({
151
+ kind: "guide",
152
+ id: propertyPath.split(".").slice(0, 2).join("."),
153
+ collection: "guides",
154
+ path: Object.freeze(splitPropertyPath(propertyPath))
155
+ });
156
+ }
157
+
158
+ if (property.startsWith("title.")) {
159
+ const propertyPath = property.slice("title.".length);
160
+
161
+ if (!TITLE_PATHS.has(propertyPath)) {
162
+ throw new Error(`Unknown semantic property "${property}".`);
163
+ }
164
+
165
+ return Object.freeze({
166
+ kind: "title",
167
+ id: "title",
168
+ collection: "title",
169
+ path: Object.freeze(splitPropertyPath(propertyPath))
170
+ });
171
+ }
172
+
173
+ throw new Error(`Unknown semantic property "${property}".`);
174
+ }
@@ -0,0 +1,76 @@
1
+ import { cloneAndFreeze } from "../core/immutable.js";
2
+ import { COLOR_LAYOUTS } from "../core/vocabulary.js";
3
+
4
+ export function validateColorLayout(layout) {
5
+ if (!COLOR_LAYOUTS.includes(layout)) {
6
+ throw new Error(`Unsupported color layout "${layout}".`);
7
+ }
8
+ return layout;
9
+ }
10
+
11
+ function validateValues(values) {
12
+ if (!Array.isArray(values) || !values.every(Number.isFinite)) {
13
+ throw new TypeError("Series layout values must be finite numbers.");
14
+ }
15
+ }
16
+
17
+ export function layoutSeriesPartition(values, layout, { baseline = 0 } = {}) {
18
+ validateValues(values);
19
+ validateColorLayout(layout);
20
+ if (!Number.isFinite(baseline)) {
21
+ throw new TypeError("Series layout baseline must be finite.");
22
+ }
23
+
24
+ if (layout === "group" || layout === "overlay") {
25
+ return cloneAndFreeze(values.flatMap((value, index) =>
26
+ value === baseline
27
+ ? []
28
+ : [{ index, value, start: baseline, end: value }]
29
+ ));
30
+ }
31
+
32
+ if (layout === "stack" || layout === "fill") {
33
+ if (values.some(value => value < 0)) {
34
+ throw new RangeError(`${layout} layout requires non-negative values.`);
35
+ }
36
+ const total = values.reduce((sum, value) => sum + value, 0);
37
+ if (layout === "fill" && total === 0) return cloneAndFreeze([]);
38
+ let offset = 0;
39
+ return cloneAndFreeze(values.flatMap((value, index) => {
40
+ if (value === 0) return [];
41
+ const resolvedValue = layout === "fill" ? value / total : value;
42
+ const start = offset;
43
+ const end = start + resolvedValue;
44
+ offset = end;
45
+ return [{ index, value, start, end }];
46
+ }));
47
+ }
48
+
49
+ let positive = 0;
50
+ let negative = 0;
51
+ return cloneAndFreeze(values.flatMap((value, index) => {
52
+ if (value === 0) return [];
53
+ const start = value > 0 ? positive : negative;
54
+ const end = start + value;
55
+ if (value > 0) positive = end;
56
+ else negative = end;
57
+ return [{ index, value, start, end }];
58
+ }));
59
+ }
60
+
61
+ export function resolveSeriesLayoutDomainValues(partitions, layout) {
62
+ if (!Array.isArray(partitions)) {
63
+ throw new TypeError("Series layout partitions must be an array.");
64
+ }
65
+ validateColorLayout(layout);
66
+ for (const partition of partitions) validateValues(partition);
67
+
68
+ if (layout === "fill") return [0, 1];
69
+ if (layout === "group" || layout === "overlay") return partitions.flat();
70
+ return partitions.flatMap(partition =>
71
+ layoutSeriesPartition(partition, layout).flatMap(segment => [
72
+ segment.start,
73
+ segment.end
74
+ ])
75
+ );
76
+ }
@@ -0,0 +1,149 @@
1
+ import { cloneAndFreeze } from "../core/immutable.js";
2
+
3
+ const LINEAR_FACTORS = Object.freeze([1, 2, 3, 5]);
4
+
5
+ export function niceTicks(domain, count) {
6
+ if (!Number.isInteger(count) || count <= 0) throw new RangeError("Tick count must be a positive integer.");
7
+ const low = Math.min(...domain);
8
+ const high = Math.max(...domain);
9
+ if (low === high) return cloneAndFreeze([low]);
10
+ const rough = (high - low) / count;
11
+ const power = 10 ** Math.floor(Math.log10(rough));
12
+ const error = rough / power;
13
+ const factor = LINEAR_FACTORS.find(candidate => candidate >= error) ?? 10;
14
+ const step = factor * power;
15
+ const tolerance = step * 1e-10;
16
+ const start = Math.ceil((low - tolerance) / step) * step;
17
+ const end = Math.floor((high + tolerance) / step) * step;
18
+ const values = [];
19
+ for (let value = start; value <= end + step * 1e-10; value += step) values.push(+value.toPrecision(12));
20
+ return cloneAndFreeze(values);
21
+ }
22
+
23
+ const SECOND = 1000;
24
+ const MINUTE = 60 * SECOND;
25
+ const HOUR = 60 * MINUTE;
26
+ const DAY = 24 * HOUR;
27
+ const YEAR = 365.2425 * DAY;
28
+
29
+ const TIME_INTERVALS = Object.freeze([
30
+ ...[1, 2, 5, 10].map(step => ({ unit: "year", step, duration: step * YEAR })),
31
+ ...[1, 2, 3, 6].map(step => ({ unit: "month", step, duration: step * YEAR / 12 })),
32
+ ...[1, 2, 7, 14].map(step => ({ unit: "day", step, duration: step * DAY })),
33
+ ...[1, 3, 6, 12].map(step => ({ unit: "hour", step, duration: step * HOUR })),
34
+ ...[1, 5, 15, 30].map(step => ({ unit: "minute", step, duration: step * MINUTE })),
35
+ ...[1, 5, 15, 30].map(step => ({ unit: "second", step, duration: step * SECOND }))
36
+ ]);
37
+
38
+ function validateTimeDomain(domain) {
39
+ if (
40
+ !Array.isArray(domain) ||
41
+ domain.length !== 2 ||
42
+ !domain.every(Number.isFinite)
43
+ ) {
44
+ throw new TypeError("Time tick domain must contain two finite timestamps.");
45
+ }
46
+
47
+ return [Math.min(...domain), Math.max(...domain)];
48
+ }
49
+
50
+ function selectTimeInterval(span, count) {
51
+ return TIME_INTERVALS.reduce((best, candidate) => {
52
+ const error = Math.abs(span / candidate.duration - count);
53
+ return error < best.error ? { ...candidate, error } : best;
54
+ }, { ...TIME_INTERVALS[0], error: Infinity });
55
+ }
56
+
57
+ function floorCalendar(timestamp, { unit, step }) {
58
+ const date = new Date(timestamp);
59
+
60
+ if (unit === "year") {
61
+ const year = Math.floor(date.getUTCFullYear() / step) * step;
62
+ return Date.UTC(year, 0, 1);
63
+ }
64
+
65
+ if (unit === "month") {
66
+ const month = date.getUTCFullYear() * 12 + date.getUTCMonth();
67
+ const aligned = Math.floor(month / step) * step;
68
+ return Date.UTC(Math.floor(aligned / 12), aligned % 12, 1);
69
+ }
70
+
71
+ const duration = unit === "day"
72
+ ? step * DAY
73
+ : unit === "hour"
74
+ ? step * HOUR
75
+ : unit === "minute"
76
+ ? step * MINUTE
77
+ : step * SECOND;
78
+
79
+ return Math.floor(timestamp / duration) * duration;
80
+ }
81
+
82
+ function addCalendar(timestamp, { unit, step }) {
83
+ const date = new Date(timestamp);
84
+
85
+ if (unit === "year") {
86
+ return Date.UTC(date.getUTCFullYear() + step, 0, 1);
87
+ }
88
+
89
+ if (unit === "month") {
90
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + step, 1);
91
+ }
92
+
93
+ const duration = unit === "day"
94
+ ? step * DAY
95
+ : unit === "hour"
96
+ ? step * HOUR
97
+ : unit === "minute"
98
+ ? step * MINUTE
99
+ : step * SECOND;
100
+
101
+ return timestamp + duration;
102
+ }
103
+
104
+ export function timeTicks(domain, count) {
105
+ if (!Number.isInteger(count) || count <= 0) {
106
+ throw new RangeError("Time tick count must be a positive integer.");
107
+ }
108
+
109
+ const [low, high] = validateTimeDomain(domain);
110
+ if (low === high) return cloneAndFreeze([low]);
111
+ const interval = selectTimeInterval(high - low, count);
112
+ let value = floorCalendar(low, interval);
113
+ if (value < low) value = addCalendar(value, interval);
114
+ const values = [];
115
+
116
+ while (value <= high) {
117
+ values.push(value);
118
+ value = addCalendar(value, interval);
119
+ }
120
+
121
+ return cloneAndFreeze(values.length === 0 ? [low, high] : values);
122
+ }
123
+
124
+ function pad(value) {
125
+ return String(value).padStart(2, "0");
126
+ }
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;
135
+ const date = new Date(value);
136
+ const year = String(date.getUTCFullYear()).padStart(4, "0");
137
+ const month = pad(date.getUTCMonth() + 1);
138
+ const day = pad(date.getUTCDate());
139
+ const hour = pad(date.getUTCHours());
140
+ const minute = pad(date.getUTCMinutes());
141
+ const second = pad(date.getUTCSeconds());
142
+
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}`;
149
+ }
@@ -0,0 +1,37 @@
1
+ import { isPlainObject } from "../core/immutable.js";
2
+ import { validateBoxTransform } from "./boxPlot.js";
3
+ import { validateDensityTransform } from "./density.js";
4
+ import { validateFilterTransform } from "./filter.js";
5
+ import { validateIntervalTransform } from "./interval.js";
6
+ import { validateMarkFilterTransform } from "./markFilter.js";
7
+ import { validateRegressionTransform } from "./regression.js";
8
+
9
+ const TRANSFORM_VALIDATORS = Object.freeze({
10
+ boxOutlier: validateBoxTransform,
11
+ boxSummary: validateBoxTransform,
12
+ density: validateDensityTransform,
13
+ filter: validateFilterTransform,
14
+ interval: validateIntervalTransform,
15
+ markFilter: validateMarkFilterTransform,
16
+ regression: validateRegressionTransform
17
+ });
18
+
19
+ export function findTransformValidator(type) {
20
+ return TRANSFORM_VALIDATORS[type];
21
+ }
22
+
23
+ export function validateDatasetTransforms(value) {
24
+ if (!Array.isArray(value) || value.length === 0 || !value.every(isPlainObject)) {
25
+ throw new TypeError(
26
+ "Dataset transform must be a non-empty array of plain objects."
27
+ );
28
+ }
29
+ for (const transform of value) {
30
+ const validate = findTransformValidator(transform.type);
31
+ if (validate === undefined) {
32
+ throw new Error(`Unsupported dataset transform "${transform.type}".`);
33
+ }
34
+ validate(transform);
35
+ }
36
+ return value;
37
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { chart } from "./ChartProgram.js";
2
+ export { render } from "./renderers/canvas/index.js";
@@ -0,0 +1,114 @@
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import {
3
+ validateNonEmptyString,
4
+ validateNonNegativeFinite
5
+ } from "../core/validation.js";
6
+
7
+ const MARGIN_KEYS = Object.freeze(["top", "right", "bottom", "left"]);
8
+
9
+ export const DEFAULT_MARGIN = cloneAndFreeze({
10
+ top: 30,
11
+ right: 30,
12
+ bottom: 60,
13
+ left: 70
14
+ });
15
+
16
+ export const DEFAULT_CANVAS = cloneAndFreeze({
17
+ width: 640,
18
+ height: 400,
19
+ background: "white",
20
+ margin: DEFAULT_MARGIN
21
+ });
22
+
23
+ function validateMarginValue(value, key) {
24
+ validateNonNegativeFinite(value, `Canvas margin.${key}`);
25
+ }
26
+
27
+ export function normalizeMargin(margin, base = DEFAULT_MARGIN) {
28
+ if (Number.isFinite(margin)) {
29
+ if (margin < 0) {
30
+ throw new RangeError("Canvas margin must not be negative.");
31
+ }
32
+
33
+ return cloneAndFreeze({
34
+ top: margin,
35
+ right: margin,
36
+ bottom: margin,
37
+ left: margin
38
+ });
39
+ }
40
+
41
+ if (!isPlainObject(margin)) {
42
+ throw new TypeError("Canvas margin must be a number or a plain object.");
43
+ }
44
+
45
+ const keys = Object.keys(margin);
46
+
47
+ if (keys.length === 0) {
48
+ throw new TypeError("Canvas margin object must contain at least one side.");
49
+ }
50
+
51
+ for (const key of keys) {
52
+ if (!MARGIN_KEYS.includes(key)) {
53
+ throw new Error(`Unknown canvas margin option "${key}".`);
54
+ }
55
+
56
+ validateMarginValue(margin[key], key);
57
+ }
58
+
59
+ return cloneAndFreeze({
60
+ ...base,
61
+ ...margin
62
+ });
63
+ }
64
+
65
+ export function validateCanvasState({ width, height, background, margin }) {
66
+ if (!Number.isInteger(width) || width <= 0) {
67
+ throw new RangeError("Canvas width must be a positive integer.");
68
+ }
69
+
70
+ if (!Number.isInteger(height) || height <= 0) {
71
+ throw new RangeError("Canvas height must be a positive integer.");
72
+ }
73
+
74
+ validateNonEmptyString(background, "Canvas background");
75
+
76
+ for (const key of MARGIN_KEYS) {
77
+ validateMarginValue(margin[key], key);
78
+ }
79
+
80
+ if (margin.left + margin.right >= width) {
81
+ throw new RangeError("Canvas horizontal margins must be smaller than width.");
82
+ }
83
+
84
+ if (margin.top + margin.bottom >= height) {
85
+ throw new RangeError("Canvas vertical margins must be smaller than height.");
86
+ }
87
+ }
88
+
89
+ export function createGraphicBounds({ width, height, margin }) {
90
+ return cloneAndFreeze({
91
+ x: margin.left,
92
+ y: margin.top,
93
+ width: width - margin.left - margin.right,
94
+ height: height - margin.top - margin.bottom
95
+ });
96
+ }
97
+
98
+ export function resolveGraphicBounds(program) {
99
+ const canvas = program.graphicSpec.objects.canvas;
100
+ const margin = program.materializationConfigs.canvas?.margin;
101
+ if (
102
+ canvas?.type !== "canvas" ||
103
+ !Number.isFinite(canvas.properties.width) ||
104
+ !Number.isFinite(canvas.properties.height) ||
105
+ margin === undefined
106
+ ) {
107
+ throw new Error("Graphical layout requires Canvas dimensions and margin.");
108
+ }
109
+ return createGraphicBounds({
110
+ width: canvas.properties.width,
111
+ height: canvas.properties.height,
112
+ margin
113
+ });
114
+ }
@@ -0,0 +1,17 @@
1
+ export function resolveGridLineGeometry({ direction, values, positions, bounds }) {
2
+ return direction === "horizontal"
3
+ ? {
4
+ values,
5
+ x1: bounds.x,
6
+ y1: positions,
7
+ x2: bounds.x + bounds.width,
8
+ y2: positions
9
+ }
10
+ : {
11
+ values,
12
+ x1: positions,
13
+ y1: bounds.y,
14
+ x2: positions,
15
+ y2: bounds.y + bounds.height
16
+ };
17
+ }
@@ -0,0 +1,51 @@
1
+ export function measureLegendSymbolHeight(config) {
2
+ return Math.max(...config.symbol.layers.map(layer => {
3
+ if (layer.type === "swatch") return layer.height;
4
+ if (layer.type === "point") return layer.size * 2;
5
+ return layer.lineWidth;
6
+ }));
7
+ }
8
+
9
+ export function measureLegendTextWidth(value) {
10
+ return String(value).length * 7;
11
+ }
12
+
13
+ export function resolveLegendGrid(config, width, count) {
14
+ const labels = config.domain.map(String);
15
+ const itemWidths = labels.map(
16
+ label => width + config.labels.offset + label.length * 7
17
+ );
18
+ const columnCount = Math.min(config.columns ?? count, count);
19
+ const rowCount = Math.ceil(count / columnCount);
20
+ const cells = Array.from({ length: count }, (_, index) => {
21
+ if (config.direction === "horizontal") {
22
+ return { column: index % columnCount, row: Math.floor(index / columnCount) };
23
+ }
24
+ return {
25
+ column: Math.floor(index / rowCount),
26
+ row: index % rowCount
27
+ };
28
+ });
29
+ const actualColumns = Math.max(...cells.map(cell => cell.column)) + 1;
30
+ const actualRows = Math.max(...cells.map(cell => cell.row)) + 1;
31
+ const columnWidths = Array.from({ length: actualColumns }, (_, column) =>
32
+ Math.max(...cells.map((cell, index) =>
33
+ cell.column === column ? itemWidths[index] : 0
34
+ ))
35
+ );
36
+ const gridWidth = columnWidths.reduce((sum, value) => sum + value, 0) +
37
+ config.itemGap * Math.max(0, actualColumns - 1);
38
+ const rowHeight = Math.max(
39
+ config.labels.fontSize,
40
+ measureLegendSymbolHeight(config)
41
+ );
42
+ const gridHeight = rowHeight * actualRows +
43
+ config.itemGap * Math.max(0, actualRows - 1);
44
+ return { cells, columnWidths, gridWidth, gridHeight, rowHeight };
45
+ }
46
+
47
+ export function alignLegendStart(bounds, width, align) {
48
+ if (align === "left") return bounds.x;
49
+ if (align === "right") return bounds.x + bounds.width - width;
50
+ return bounds.x + (bounds.width - width) / 2;
51
+ }
@@ -0,0 +1,62 @@
1
+ import { measureTextWidth } from "../core/textMetrics.js";
2
+
3
+ export { measureTextWidth } from "../core/textMetrics.js";
4
+
5
+ function wrapCodePoints(text, maxWidth, style) {
6
+ const lines = [];
7
+ let line = "";
8
+ for (const codePoint of [...text]) {
9
+ const candidate = line + codePoint;
10
+ if (line !== "" && measureTextWidth(candidate, style) > maxWidth) {
11
+ lines.push(line);
12
+ line = codePoint;
13
+ } else {
14
+ line = candidate;
15
+ }
16
+ }
17
+ if (line !== "") lines.push(line);
18
+ return lines;
19
+ }
20
+
21
+ function wrapWords(text, maxWidth, style) {
22
+ const lines = [];
23
+ let line = "";
24
+ for (const word of text.trim().split(/\s+/u)) {
25
+ if (measureTextWidth(word, style) > maxWidth) {
26
+ if (line !== "") {
27
+ lines.push(line);
28
+ line = "";
29
+ }
30
+ const fragments = wrapCodePoints(word, maxWidth, style);
31
+ lines.push(...fragments.slice(0, -1));
32
+ line = fragments.at(-1);
33
+ continue;
34
+ }
35
+ const candidate = line === "" ? word : `${line} ${word}`;
36
+ if (line !== "" && measureTextWidth(candidate, style) > maxWidth) {
37
+ lines.push(line);
38
+ line = word;
39
+ } else {
40
+ line = candidate;
41
+ }
42
+ }
43
+ if (line !== "") lines.push(line);
44
+ return lines;
45
+ }
46
+
47
+ export function wrapText(text, {
48
+ maxWidth,
49
+ mode = "word",
50
+ style
51
+ } = {}) {
52
+ if (maxWidth === undefined) return [text];
53
+ if (!Number.isFinite(maxWidth) || maxWidth <= 0) {
54
+ throw new RangeError("Text maxWidth must be positive.");
55
+ }
56
+ if (!["word", "character"].includes(mode)) {
57
+ throw new Error(`Unsupported text wrap mode "${mode}".`);
58
+ }
59
+ return mode === "word"
60
+ ? wrapWords(text, maxWidth, style)
61
+ : wrapCodePoints(text, maxWidth, style);
62
+ }