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,321 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+ import {
3
+ validateKeys,
4
+ validateNonEmptyString,
5
+ validateNonNegativeFinite,
6
+ validatePositiveFinite
7
+ } from "../../core/validation.js";
8
+ import { resolveGraphicBounds } from "../../layout/canvas.js";
9
+ import { resolveConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
10
+ import {
11
+ alignedTitleAnchor,
12
+ buildTitleReadingBlock,
13
+ layoutBoundsIntersect,
14
+ resolveTitleComponentBounds,
15
+ unionTitleBounds
16
+ } from "../../layout/title.js";
17
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
18
+
19
+ const OPTIONS = Object.freeze([
20
+ "text", "subtitle", "position", "align", "offset", "gap", "titleStyle",
21
+ "subtitleStyle", "maxWidth", "wrap", "lineHeight"
22
+ ]);
23
+ const STYLE_OPTIONS = Object.freeze([
24
+ "color", "fontSize", "fontFamily", "fontWeight"
25
+ ]);
26
+ const POSITIONS = Object.freeze(["top", "bottom", "left", "right"]);
27
+ const ALIGNS = Object.freeze(["left", "center", "right"]);
28
+ const WRAPS = Object.freeze(["word", "character"]);
29
+ const DEFAULTS = Object.freeze({
30
+ position: "top",
31
+ align: "left",
32
+ offset: 0,
33
+ gap: 8,
34
+ titleStyle: Object.freeze({
35
+ color: DEFAULT_COLORS.strongText,
36
+ fontSize: 22,
37
+ fontFamily: DEFAULT_FONT_FAMILY,
38
+ fontWeight: 600
39
+ }),
40
+ subtitleStyle: Object.freeze({
41
+ color: DEFAULT_COLORS.mutedText,
42
+ fontSize: 14,
43
+ fontFamily: DEFAULT_FONT_FAMILY,
44
+ fontWeight: "normal"
45
+ })
46
+ });
47
+
48
+ export function validateTitleString(value, label) {
49
+ validateNonEmptyString(value, label);
50
+ if (/\r|\n/u.test(value)) {
51
+ throw new Error(`${label} does not accept explicit newlines.`);
52
+ }
53
+ return value;
54
+ }
55
+
56
+ function normalizeStyle(value, defaults, label) {
57
+ if (value !== undefined && !isPlainObject(value)) {
58
+ throw new TypeError(`${label} must be a plain object.`);
59
+ }
60
+ const style = { ...defaults, ...(value ?? {}) };
61
+ validateKeys(style, STYLE_OPTIONS, label);
62
+ validateTitleString(style.color, `${label} color`);
63
+ validateTitleString(style.fontFamily, `${label} fontFamily`);
64
+ validatePositiveFinite(style.fontSize, `${label} fontSize`);
65
+ if (!(
66
+ (typeof style.fontWeight === "string" && style.fontWeight.length > 0) ||
67
+ Number.isFinite(style.fontWeight)
68
+ )) {
69
+ throw new TypeError(`${label} fontWeight must be a non-empty string or number.`);
70
+ }
71
+ return style;
72
+ }
73
+
74
+ function normalizeConfig(args, defaults, { hasSubtitle }) {
75
+ const position = args.position ?? defaults.position;
76
+ const align = args.align ?? defaults.align;
77
+ const offset = args.offset ?? defaults.offset;
78
+ const gap = args.gap ?? defaults.gap;
79
+ const titleStyle = normalizeStyle(
80
+ args.titleStyle,
81
+ defaults.titleStyle,
82
+ "createTitle.titleStyle"
83
+ );
84
+ const subtitleStyle = normalizeStyle(
85
+ args.subtitleStyle,
86
+ defaults.subtitleStyle,
87
+ "createTitle.subtitleStyle"
88
+ );
89
+ const maxWidth = args.maxWidth ?? defaults.maxWidth;
90
+ const wrap = args.wrap ?? defaults.wrap ?? (maxWidth === undefined ? undefined : "word");
91
+ const lineHeight = args.lineHeight ?? defaults.lineHeight;
92
+ if (!POSITIONS.includes(position)) {
93
+ throw new Error(`Unsupported title position "${position}".`);
94
+ }
95
+ if (!ALIGNS.includes(align)) {
96
+ throw new Error(`Unsupported title align "${align}".`);
97
+ }
98
+ if (!Number.isFinite(offset)) {
99
+ throw new TypeError("Chart title offset must be a finite number.");
100
+ }
101
+ validateNonNegativeFinite(gap, "Chart title gap");
102
+ if (maxWidth !== undefined) {
103
+ validatePositiveFinite(maxWidth, "Chart title maxWidth");
104
+ }
105
+ if (wrap !== undefined && !WRAPS.includes(wrap)) {
106
+ throw new Error(`Unsupported title wrap "${wrap}".`);
107
+ }
108
+ if (maxWidth === undefined && (wrap !== undefined || lineHeight !== undefined)) {
109
+ throw new Error("Chart title wrap and lineHeight require maxWidth.");
110
+ }
111
+ if (lineHeight !== undefined && (
112
+ !Number.isFinite(lineHeight) ||
113
+ lineHeight < titleStyle.fontSize ||
114
+ (hasSubtitle && lineHeight < subtitleStyle.fontSize)
115
+ )) {
116
+ throw new RangeError("Chart title lineHeight must cover every visible fontSize.");
117
+ }
118
+ return {
119
+ position,
120
+ align,
121
+ offset,
122
+ gap,
123
+ titleStyle,
124
+ subtitleStyle,
125
+ ...(maxWidth === undefined ? {} : { maxWidth, wrap, ...(lineHeight === undefined
126
+ ? {}
127
+ : { lineHeight }) })
128
+ };
129
+ }
130
+
131
+ export function normalizeTitleOptions(args) {
132
+ if (!isPlainObject(args)) {
133
+ throw new TypeError("createTitle options must be a plain object.");
134
+ }
135
+ validateKeys(args, OPTIONS, "createTitle");
136
+ const text = validateTitleString(args.text, "Chart title text");
137
+ const subtitle = args.subtitle === undefined
138
+ ? undefined
139
+ : validateTitleString(args.subtitle, "Chart subtitle");
140
+ return {
141
+ text,
142
+ subtitle,
143
+ ...normalizeConfig(args, DEFAULTS, { hasSubtitle: subtitle !== undefined })
144
+ };
145
+ }
146
+
147
+ export function normalizeTitleEditOptions(args, previous, semanticTitle) {
148
+ if (!isPlainObject(args)) {
149
+ throw new TypeError("editTitle options must be a plain object.");
150
+ }
151
+ validateKeys(args, OPTIONS, "editTitle");
152
+ if (Object.keys(args).length === 0) {
153
+ throw new Error("editTitle requires at least one change.");
154
+ }
155
+ const text = args.text === undefined
156
+ ? semanticTitle.text
157
+ : validateTitleString(args.text, "Chart title text");
158
+ const subtitle = args.subtitle === false
159
+ ? undefined
160
+ : args.subtitle === undefined
161
+ ? semanticTitle.subtitle
162
+ : validateTitleString(args.subtitle, "Chart subtitle");
163
+ if (args.subtitle !== undefined && args.subtitle !== false && typeof args.subtitle !== "string") {
164
+ throw new TypeError("Chart subtitle must be a non-empty string or false.");
165
+ }
166
+ return {
167
+ text,
168
+ subtitle,
169
+ config: normalizeConfig(args, previous, { hasSubtitle: subtitle !== undefined })
170
+ };
171
+ }
172
+
173
+ export function requireTitleConfig(program) {
174
+ if (program.titleConfig === undefined) {
175
+ throw new Error("Title component requires chart title configuration.");
176
+ }
177
+ return program.titleConfig;
178
+ }
179
+
180
+ function reservedGraphicIds(program, position) {
181
+ const ids = [];
182
+ const axisChannel = ["top", "bottom"].includes(position) ? "x" : "y";
183
+ const axis = program.guideConfigs.axis?.[axisChannel];
184
+ if (axis && Object.values(axis).some(config => config?.position === position)) {
185
+ const prefix = axisChannel === "x" ? "xAxis" : "yAxis";
186
+ ids.push(`${prefix}Line`, `${prefix}Ticks`, `${prefix}Labels`, `${prefix}Title`);
187
+ }
188
+ const prefixes = {
189
+ series: "seriesLegend",
190
+ color: "colorLegend",
191
+ gradient: "colorGradient",
192
+ opacity: "opacityLegend"
193
+ };
194
+ for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
195
+ if (config?.position !== position || prefixes[kind] === undefined) continue;
196
+ ids.push(...Object.keys(program.graphicSpec.objects).filter(
197
+ id => id.startsWith(prefixes[kind])
198
+ ));
199
+ if (kind === "series" && program.guideConfigs.legend?.size !== undefined) {
200
+ ids.push(...Object.keys(program.graphicSpec.objects).filter(
201
+ id => id.startsWith("sizeLegend")
202
+ ));
203
+ }
204
+ }
205
+ return [...new Set(ids)];
206
+ }
207
+
208
+ function validateLayout(program, position, titleBounds, plot, canvas) {
209
+ if (
210
+ titleBounds.left < 0 || titleBounds.top < 0 ||
211
+ titleBounds.right > canvas.width || titleBounds.bottom > canvas.height
212
+ ) {
213
+ throw new Error(`Chart title requires more ${position}-margin space.`);
214
+ }
215
+ const outsidePlot = {
216
+ top: titleBounds.bottom <= plot.y,
217
+ bottom: titleBounds.top >= plot.y + plot.height,
218
+ left: titleBounds.right <= plot.x,
219
+ right: titleBounds.left >= plot.x + plot.width
220
+ }[position];
221
+ if (!outsidePlot) {
222
+ throw new Error(`Chart title requires more ${position}-margin space.`);
223
+ }
224
+ for (const id of reservedGraphicIds(program, position)) {
225
+ const bounds = program.graphicSpec.objects[id] === undefined
226
+ ? undefined
227
+ : resolveConcreteGraphicBounds(program.graphicSpec, id);
228
+ if (bounds !== undefined && layoutBoundsIntersect(titleBounds, bounds)) {
229
+ throw new Error(`Chart title and ${position} guides require more margin space.`);
230
+ }
231
+ }
232
+ }
233
+
234
+ export function resolveTitleLayout(program, config) {
235
+ const plot = resolveGraphicBounds(program);
236
+ const canvas = program.graphicSpec.objects.canvas;
237
+ if (
238
+ plot === undefined ||
239
+ ![plot.x, plot.y, plot.width, plot.height].every(Number.isFinite) ||
240
+ canvas?.type !== "canvas" ||
241
+ !Number.isFinite(canvas.properties.width) ||
242
+ !Number.isFinite(canvas.properties.height)
243
+ ) {
244
+ throw new Error("Chart title layout requires Canvas bounds and dimensions.");
245
+ }
246
+ const block = buildTitleReadingBlock({
247
+ text: validateTitleString(program.semanticSpec.title.text, "Chart title text"),
248
+ subtitle: program.semanticSpec.title.subtitle === undefined
249
+ ? undefined
250
+ : validateTitleString(
251
+ program.semanticSpec.title.subtitle,
252
+ "Chart subtitle"
253
+ )
254
+ }, config);
255
+ const horizontal = ["top", "bottom"].includes(config.position);
256
+ let title;
257
+ let subtitle;
258
+ if (horizontal) {
259
+ const x = config.align === "left"
260
+ ? plot.x
261
+ : config.align === "center"
262
+ ? plot.x + plot.width / 2
263
+ : plot.x + plot.width;
264
+ const blockTop = config.position === "top"
265
+ ? 16 + config.offset
266
+ : plot.y + plot.height + config.offset;
267
+ title = {
268
+ lines: block.titleLines,
269
+ x,
270
+ y: block.titleCenters.map(value => blockTop + value),
271
+ textAlign: config.align,
272
+ rotation: 0,
273
+ explicitRotation: config.position !== "top" || block.titleLines.length > 1
274
+ };
275
+ subtitle = block.subtitleLines.length === 0 ? undefined : {
276
+ lines: block.subtitleLines,
277
+ x,
278
+ y: block.subtitleCenters.map(value => blockTop + value),
279
+ textAlign: config.align,
280
+ rotation: 0,
281
+ explicitRotation: config.position !== "top" || block.subtitleLines.length > 1
282
+ };
283
+ } else {
284
+ const rotation = config.position === "left" ? -Math.PI / 2 : Math.PI / 2;
285
+ const y = alignedTitleAnchor(plot.y, plot.height, block.width, config.align);
286
+ const edge = config.position === "left"
287
+ ? 16 + config.offset
288
+ : canvas.properties.width - 16 + config.offset;
289
+ const mapX = value => config.position === "left" ? edge + value : edge - value;
290
+ title = {
291
+ lines: block.titleLines,
292
+ x: block.titleCenters.map(mapX),
293
+ y,
294
+ textAlign: "center",
295
+ rotation,
296
+ explicitRotation: true
297
+ };
298
+ subtitle = block.subtitleLines.length === 0 ? undefined : {
299
+ lines: block.subtitleLines,
300
+ x: block.subtitleCenters.map(mapX),
301
+ y,
302
+ textAlign: "center",
303
+ rotation,
304
+ explicitRotation: true
305
+ };
306
+ }
307
+ const titleBounds = unionTitleBounds([
308
+ resolveTitleComponentBounds(title, config.titleStyle),
309
+ ...(subtitle === undefined
310
+ ? []
311
+ : [resolveTitleComponentBounds(subtitle, config.subtitleStyle)])
312
+ ]);
313
+ validateLayout(
314
+ program,
315
+ config.position,
316
+ titleBounds,
317
+ plot,
318
+ canvas.properties
319
+ );
320
+ return { title, subtitle, bounds: titleBounds };
321
+ }
@@ -0,0 +1,316 @@
1
+ import {
2
+ appendActionNodeAtPath,
3
+ countActionNodes,
4
+ createActionNode
5
+ } from "./action.js";
6
+ import {
7
+ cloneAndFreeze,
8
+ freezeOwned,
9
+ isOwned,
10
+ isPlainObject,
11
+ removeOwnedPath
12
+ } from "./immutable.js";
13
+ import {
14
+ createEmptyGraphicSpec,
15
+ createEmptySemanticSpec,
16
+ createTraceRoot
17
+ } from "./specs.js";
18
+ import { LEGEND_CONFIG_KINDS } from "./vocabulary.js";
19
+
20
+ function ownState(value) {
21
+ return isOwned(value) ? value : cloneAndFreeze(value);
22
+ }
23
+
24
+ function createMaterializationConfigs(
25
+ markConfigs,
26
+ guideConfigs,
27
+ titleConfig,
28
+ canvasConfig
29
+ ) {
30
+ return cloneAndFreeze({
31
+ marks: markConfigs,
32
+ guides: guideConfigs,
33
+ ...(canvasConfig === undefined ? {} : { canvas: canvasConfig }),
34
+ ...(titleConfig === undefined ? {} : { title: titleConfig })
35
+ });
36
+ }
37
+
38
+ function updateConfigPath(value, path, config) {
39
+ if (path.length === 0) return cloneAndFreeze(config);
40
+ const [key, ...rest] = path;
41
+ return freezeOwned({
42
+ ...value,
43
+ [key]: updateConfigPath(value?.[key] ?? {}, rest, config)
44
+ });
45
+ }
46
+
47
+ export class ChartProgram {
48
+ constructor({
49
+ semanticSpec = createEmptySemanticSpec(),
50
+ graphicSpec = createEmptyGraphicSpec(),
51
+ resolvedScales = {},
52
+ materializationConfigs,
53
+ markConfigs = {},
54
+ guideConfigs = {},
55
+ titleConfig,
56
+ canvasConfig,
57
+ context = {},
58
+ trace = createTraceRoot(),
59
+ actionStack = [],
60
+ actionSequence
61
+ } = {}) {
62
+ this.semanticSpec = ownState(semanticSpec);
63
+ this.graphicSpec = ownState(graphicSpec);
64
+ this.resolvedScales = ownState(resolvedScales);
65
+ this.materializationConfigs = materializationConfigs === undefined
66
+ ? createMaterializationConfigs(
67
+ markConfigs,
68
+ guideConfigs,
69
+ titleConfig,
70
+ canvasConfig
71
+ )
72
+ : ownState(materializationConfigs);
73
+ this.context = ownState(context);
74
+ this.trace = ownState(trace);
75
+ this.actionStack = ownState(actionStack);
76
+ Object.defineProperty(this, "_actionSequence", {
77
+ value: actionSequence ?? countActionNodes(this.trace),
78
+ enumerable: false
79
+ });
80
+
81
+ Object.freeze(this);
82
+ }
83
+
84
+ get markConfigs() {
85
+ return this.materializationConfigs.marks;
86
+ }
87
+
88
+ get guideConfigs() {
89
+ return this.materializationConfigs.guides;
90
+ }
91
+
92
+ get titleConfig() {
93
+ return this.materializationConfigs.title;
94
+ }
95
+
96
+ _withSelectionConfig(id, config) {
97
+ if (typeof id !== "string" || id.length === 0) {
98
+ throw new TypeError("Selection config id must be a non-empty string.");
99
+ }
100
+ if (!isPlainObject(config)) {
101
+ throw new TypeError("Selection config must be a plain object.");
102
+ }
103
+ return this._withMaterializationConfig(["selections", id], config);
104
+ }
105
+
106
+ _withHighlightConfig(id, config) {
107
+ if (typeof id !== "string" || id.length === 0) {
108
+ throw new TypeError("Highlight config id must be a non-empty string.");
109
+ }
110
+ if (!isPlainObject(config)) {
111
+ throw new TypeError("Highlight config must be a plain object.");
112
+ }
113
+ return this._withMaterializationConfig(["highlights", id], config);
114
+ }
115
+
116
+ _clone({
117
+ semanticSpec = this.semanticSpec,
118
+ graphicSpec = this.graphicSpec,
119
+ resolvedScales = this.resolvedScales,
120
+ materializationConfigs = this.materializationConfigs,
121
+ context = this.context,
122
+ trace = this.trace,
123
+ actionStack = this.actionStack,
124
+ actionSequence = this._actionSequence
125
+ } = {}) {
126
+ return new this.constructor({
127
+ semanticSpec,
128
+ graphicSpec,
129
+ resolvedScales,
130
+ materializationConfigs,
131
+ context,
132
+ trace,
133
+ actionStack,
134
+ actionSequence
135
+ });
136
+ }
137
+
138
+ _withContext(patch = {}) {
139
+ if (!isPlainObject(patch)) {
140
+ throw new TypeError("Context patch must be a plain object.");
141
+ }
142
+
143
+ const ownedPatch = cloneAndFreeze(patch);
144
+
145
+ return this._clone({
146
+ context: freezeOwned({
147
+ ...this.context,
148
+ ...ownedPatch
149
+ })
150
+ });
151
+ }
152
+
153
+ _withResolvedScale(id, scale) {
154
+ if (typeof id !== "string" || id.length === 0) {
155
+ throw new TypeError("Resolved scale id must be a non-empty string.");
156
+ }
157
+
158
+ if (!isPlainObject(scale)) {
159
+ throw new TypeError("Resolved scale must be a plain object.");
160
+ }
161
+
162
+ const ownedScale = cloneAndFreeze(scale);
163
+
164
+ return this._clone({
165
+ resolvedScales: freezeOwned({
166
+ ...this.resolvedScales,
167
+ [id]: ownedScale
168
+ })
169
+ });
170
+ }
171
+
172
+ _withMarkConfig(id, config) {
173
+ if (typeof id !== "string" || id.length === 0) {
174
+ throw new TypeError("Mark config id must be a non-empty string.");
175
+ }
176
+ if (!isPlainObject(config)) {
177
+ throw new TypeError("Mark config must be a plain object.");
178
+ }
179
+
180
+ return this._withMaterializationConfig(["marks", id], config);
181
+ }
182
+
183
+ _withCanvasConfig(config) {
184
+ if (!isPlainObject(config)) {
185
+ throw new TypeError("Canvas config must be a plain object.");
186
+ }
187
+ return this._withMaterializationConfig(["canvas"], config);
188
+ }
189
+
190
+ _withMaterializationConfig(path, config) {
191
+ if (
192
+ !Array.isArray(path) ||
193
+ path.length === 0 ||
194
+ !path.every(key => typeof key === "string" && key.length > 0)
195
+ ) {
196
+ throw new TypeError("Materialization config path must contain names.");
197
+ }
198
+ if (!isPlainObject(config)) {
199
+ throw new TypeError("Materialization config must be a plain object.");
200
+ }
201
+ return this._clone({
202
+ materializationConfigs: updateConfigPath(
203
+ this.materializationConfigs,
204
+ path,
205
+ config
206
+ )
207
+ });
208
+ }
209
+
210
+ _withoutMaterializationConfig(path) {
211
+ if (
212
+ !Array.isArray(path) ||
213
+ path.length === 0 ||
214
+ !path.every(key => typeof key === "string" && key.length > 0)
215
+ ) {
216
+ throw new TypeError("Materialization config path must contain names.");
217
+ }
218
+ const removed = removeOwnedPath(this.materializationConfigs, path);
219
+ if (!removed.removed) return this;
220
+ return this._clone({
221
+ materializationConfigs: freezeOwned({
222
+ ...removed.value,
223
+ marks: removed.value.marks ?? freezeOwned({}),
224
+ guides: removed.value.guides ?? freezeOwned({})
225
+ })
226
+ });
227
+ }
228
+
229
+ _withGuideConfig(channel, component, config) {
230
+ if (config === undefined) {
231
+ config = component;
232
+ component = "ticks";
233
+ }
234
+
235
+ return this._withMaterializationConfig(
236
+ ["guides", "axis", channel, component],
237
+ config
238
+ );
239
+ }
240
+
241
+ _withLegendConfig(kind, config) {
242
+ if (config === undefined) {
243
+ config = kind;
244
+ kind = "series";
245
+ }
246
+ if (!LEGEND_CONFIG_KINDS.includes(kind)) {
247
+ throw new Error(`Unknown legend kind "${kind}".`);
248
+ }
249
+ if (!isPlainObject(config)) {
250
+ throw new TypeError("Legend config must be a plain object.");
251
+ }
252
+
253
+ return this._withMaterializationConfig(
254
+ ["guides", "legend", kind],
255
+ config
256
+ );
257
+ }
258
+
259
+ _withGridConfig(direction, config) {
260
+ if (!["horizontal", "vertical"].includes(direction)) {
261
+ throw new Error(`Unknown grid direction "${direction}".`);
262
+ }
263
+ if (!isPlainObject(config)) {
264
+ throw new TypeError("Grid config must be a plain object.");
265
+ }
266
+
267
+ return this._withMaterializationConfig(
268
+ ["guides", "grid", direction],
269
+ config
270
+ );
271
+ }
272
+
273
+ _withTitleConfig(config) {
274
+ if (!isPlainObject(config)) {
275
+ throw new TypeError("Title config must be a plain object.");
276
+ }
277
+
278
+ return this._withMaterializationConfig(["title"], config);
279
+ }
280
+
281
+ _enterAction({ op, description, args }) {
282
+ const actionSequence = this._actionSequence + 1;
283
+ const id = `a${actionSequence}`;
284
+ const parentPath = this.actionStack.at(-1)?.path ?? [];
285
+ const actionNode = createActionNode({ id, op, description, args });
286
+ const appended = appendActionNodeAtPath(
287
+ this.trace,
288
+ parentPath,
289
+ actionNode
290
+ );
291
+ const actionStack = cloneAndFreeze([
292
+ ...this.actionStack,
293
+ { id, path: appended.path }
294
+ ]);
295
+
296
+ return this._clone({
297
+ trace: appended.root,
298
+ actionStack,
299
+ actionSequence
300
+ });
301
+ }
302
+
303
+ _exitAction() {
304
+ if (this.actionStack.length === 0) {
305
+ throw new Error("Cannot exit an action when the action stack is empty.");
306
+ }
307
+
308
+ return this._clone({
309
+ actionStack: cloneAndFreeze(this.actionStack.slice(0, -1))
310
+ });
311
+ }
312
+ }
313
+
314
+ export function chart() {
315
+ return new ChartProgram();
316
+ }