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,183 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import {
4
+ BOX_FIELDS,
5
+ deriveBoxData,
6
+ normalizeBoxTransform
7
+ } from "../../grammar/boxPlot.js";
8
+ import { findDataset } from "../../selectors/datasets.js";
9
+ import { findLayer } from "../../selectors/layers.js";
10
+ import { resolveBoxOrientation } from "./resolve.js";
11
+ import { resolvePlotGraphicPlacement } from
12
+ "../../materialization/graphicHierarchy.js";
13
+
14
+ export const materializeBoxPlot = action(
15
+ {
16
+ op: "materializeBoxPlot",
17
+ description: "Materialize a complete box-plot composite."
18
+ },
19
+ function ({ id } = {}) {
20
+ const ownerId = validateUserId(id, "Box-plot id");
21
+ const config = this.markConfigs[ownerId]?.boxPlot;
22
+ const layer = findLayer(this, ownerId);
23
+ if (config === undefined || layer?.mark?.type !== "bar") {
24
+ throw new Error(`Unknown box plot "${ownerId}".`);
25
+ }
26
+ if (config.materialized) return this;
27
+ const x = layer.encoding?.x;
28
+ const y = layer.encoding?.y;
29
+ if (x?.field === undefined || y?.field === undefined) return this;
30
+ const orientation = resolveBoxOrientation(x, y);
31
+ if (orientation === undefined) {
32
+ throw new Error(
33
+ "createBoxPlot requires one categorical axis and one quantitative axis."
34
+ );
35
+ }
36
+ const category = orientation === "vertical" ? x : y;
37
+ const measure = orientation === "vertical" ? y : x;
38
+ const measureChannel = orientation === "vertical" ? "y" : "x";
39
+ const source = layer.data;
40
+ const summaryId = `${ownerId}SummaryData`;
41
+ const outlierDataId = `${ownerId}OutlierData`;
42
+ const whiskerId = `${ownerId}Whisker`;
43
+ const medianId = `${ownerId}Median`;
44
+ const outlierId = `${ownerId}Outliers`;
45
+ let next = this._withMarkConfig(ownerId, {
46
+ ...this.markConfigs[ownerId],
47
+ boxPlot: {
48
+ ...config,
49
+ materialized: true,
50
+ source,
51
+ orientation,
52
+ category: category.field,
53
+ measure: measure.field
54
+ },
55
+ barWidth: { band: config.width },
56
+ fill: config.box.fill,
57
+ opacity: config.box.opacity,
58
+ stroke: config.box.stroke,
59
+ strokeWidth: config.box.strokeWidth
60
+ }).createBoxSummaryData({
61
+ id: summaryId,
62
+ source,
63
+ category: category.field,
64
+ field: measure.field,
65
+ whisker: config.whisker.type,
66
+ ...(config.whisker.factor === undefined
67
+ ? {}
68
+ : { factor: config.whisker.factor })
69
+ });
70
+ const sourceRows = findDataset(next, source).values;
71
+ const hasOutliers = config.outliers &&
72
+ config.whisker.type === "tukey" &&
73
+ deriveBoxData(sourceRows, normalizeBoxTransform({
74
+ type: "boxOutlier",
75
+ category: category.field,
76
+ field: measure.field,
77
+ whisker: config.whisker.type,
78
+ factor: config.whisker.factor
79
+ })).outliers.length > 0;
80
+ if (hasOutliers) {
81
+ next = next.createBoxOutlierData({
82
+ id: outlierDataId,
83
+ source,
84
+ category: category.field,
85
+ field: measure.field,
86
+ whisker: config.whisker.type,
87
+ factor: config.whisker.factor
88
+ });
89
+ }
90
+ next = next.editSemantic({
91
+ property: `layer[${ownerId}].data`,
92
+ value: summaryId
93
+ });
94
+ const rangeAction = orientation === "vertical" ? "encodeYRange" : "encodeXRange";
95
+ next = next[rangeAction]({
96
+ target: ownerId,
97
+ lower: BOX_FIELDS.q1,
98
+ upper: BOX_FIELDS.q3,
99
+ scale: { id: measure.scale }
100
+ }).editSemantic({
101
+ property: `layer[${ownerId}].encoding.${measureChannel}.title`,
102
+ value: measure.field
103
+ });
104
+ next = next.createErrorBar({
105
+ id: whiskerId,
106
+ data: summaryId,
107
+ ...(orientation === "vertical"
108
+ ? {
109
+ x: {
110
+ field: category.field,
111
+ fieldType: category.fieldType,
112
+ scale: { id: category.scale }
113
+ },
114
+ y: {
115
+ center: BOX_FIELDS.median,
116
+ lower: BOX_FIELDS.lowerWhisker,
117
+ upper: BOX_FIELDS.upperWhisker,
118
+ scale: { id: measure.scale }
119
+ }
120
+ }
121
+ : {
122
+ x: {
123
+ center: BOX_FIELDS.median,
124
+ lower: BOX_FIELDS.lowerWhisker,
125
+ upper: BOX_FIELDS.upperWhisker,
126
+ scale: { id: measure.scale }
127
+ },
128
+ y: {
129
+ field: category.field,
130
+ fieldType: category.fieldType,
131
+ scale: { id: category.scale }
132
+ }
133
+ }),
134
+ coordinate: layer.coordinate,
135
+ stroke: "#111111",
136
+ strokeWidth: 1.5
137
+ });
138
+ next = next
139
+ .editSemantic({
140
+ property: `layer[${whiskerId}].encoding.${measureChannel}.title`,
141
+ value: measure.field
142
+ })
143
+ .editGraphics({ target: ownerId, remove: true })
144
+ .createGraphics({
145
+ id: ownerId,
146
+ type: "rect",
147
+ length: 0,
148
+ ...resolvePlotGraphicPlacement(next)
149
+ })
150
+ .rematerializeBarMark({ id: ownerId })
151
+ .createBoxMedian({
152
+ id: medianId,
153
+ owner: ownerId,
154
+ data: summaryId,
155
+ category: category.field,
156
+ categoryType: category.fieldType,
157
+ measure: BOX_FIELDS.median,
158
+ orientation,
159
+ coordinate: layer.coordinate,
160
+ categoryScale: category.scale,
161
+ measureScale: measure.scale,
162
+ stroke: config.median.stroke,
163
+ strokeWidth: config.median.strokeWidth
164
+ });
165
+ if (hasOutliers) {
166
+ next = next.createBoxOutliers({
167
+ id: outlierId,
168
+ data: outlierDataId,
169
+ category: category.field,
170
+ categoryType: category.fieldType,
171
+ measure: measure.field,
172
+ orientation,
173
+ coordinate: layer.coordinate,
174
+ categoryScale: category.scale,
175
+ measureScale: measure.scale,
176
+ shape: config.outlier.shape,
177
+ radius: config.outlier.radius,
178
+ opacity: config.outlier.opacity
179
+ });
180
+ }
181
+ return next._withContext({ currentMark: ownerId, currentData: source });
182
+ }
183
+ );
@@ -0,0 +1,147 @@
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 { DEFAULT_COLORS } from "../../theme/defaults.js";
11
+
12
+ export const BOX_PLOT_OPTIONS = Object.freeze([
13
+ "id", "target", "data", "x", "y", "coordinate", "whisker",
14
+ "width", "outliers", "box", "median", "outlier"
15
+ ]);
16
+
17
+ const DEFAULT_BOX = Object.freeze({
18
+ fill: DEFAULT_COLORS.mark,
19
+ opacity: 1,
20
+ stroke: DEFAULT_COLORS.mark,
21
+ strokeWidth: 1.5
22
+ });
23
+ const DEFAULT_MEDIAN = Object.freeze({ stroke: "#1f2937", strokeWidth: 1.5 });
24
+ const DEFAULT_OUTLIER = Object.freeze({
25
+ shape: "diamond",
26
+ radius: 3,
27
+ opacity: 0.75
28
+ });
29
+
30
+ function plainOptions(value, keys, label) {
31
+ if (value === undefined) return {};
32
+ if (!isPlainObject(value)) {
33
+ throw new TypeError(`createBoxPlot ${label} must be a plain object.`);
34
+ }
35
+ validateKeys(value, keys, `createBoxPlot ${label}`);
36
+ return value;
37
+ }
38
+
39
+ export function resolveBoxPosition(value, label) {
40
+ if (value === undefined) return undefined;
41
+ if (!isPlainObject(value)) {
42
+ throw new TypeError(`createBoxPlot ${label} must be a plain object.`);
43
+ }
44
+ validateKeys(value, ["field", "fieldType", "scale"], `createBoxPlot ${label}`);
45
+ return value;
46
+ }
47
+
48
+ export function boxEncodingArgs(value) {
49
+ return {
50
+ ...value,
51
+ ...(typeof value.scale === "string" ? { scale: { id: value.scale } } : {})
52
+ };
53
+ }
54
+
55
+ export function resolveBoxWhisker(value) {
56
+ if (value === undefined) return Object.freeze({ type: "tukey", factor: 1.5 });
57
+ if (!isPlainObject(value)) {
58
+ throw new TypeError("createBoxPlot whisker must be a plain object.");
59
+ }
60
+ validateKeys(value, ["type", "factor"], "createBoxPlot whisker");
61
+ const type = value.type ?? "tukey";
62
+ if (!["tukey", "minmax"].includes(type)) {
63
+ throw new Error(`Unsupported createBoxPlot whisker type "${type}".`);
64
+ }
65
+ if (type === "minmax") {
66
+ if (value.factor !== undefined) {
67
+ throw new Error("createBoxPlot minmax whiskers do not accept factor.");
68
+ }
69
+ return Object.freeze({ type });
70
+ }
71
+ const factor = value.factor ?? 1.5;
72
+ if (!Number.isFinite(factor) || factor <= 0) {
73
+ throw new RangeError(
74
+ "createBoxPlot whisker factor must be positive and finite."
75
+ );
76
+ }
77
+ return Object.freeze({ type, factor });
78
+ }
79
+
80
+ export function resolveBoxWidth(value) {
81
+ const options = plainOptions(value, ["band"], "width");
82
+ const band = options.band ?? 0.7;
83
+ if (!Number.isFinite(band) || band <= 0 || band >= 1) {
84
+ throw new RangeError(
85
+ "createBoxPlot width.band must be greater than 0 and less than 1."
86
+ );
87
+ }
88
+ return band;
89
+ }
90
+
91
+ export function resolveBoxAppearance(value) {
92
+ const options = plainOptions(
93
+ value,
94
+ ["fill", "opacity", "stroke", "strokeWidth"],
95
+ "box"
96
+ );
97
+ return Object.freeze({
98
+ fill: options.fill === undefined
99
+ ? DEFAULT_BOX.fill
100
+ : validateNonEmptyString(options.fill, "createBoxPlot box.fill"),
101
+ opacity: options.opacity === undefined
102
+ ? DEFAULT_BOX.opacity
103
+ : validateUnitInterval(options.opacity, "createBoxPlot box.opacity"),
104
+ stroke: options.stroke === undefined
105
+ ? DEFAULT_BOX.stroke
106
+ : validateNonEmptyString(options.stroke, "createBoxPlot box.stroke"),
107
+ strokeWidth: options.strokeWidth === undefined
108
+ ? DEFAULT_BOX.strokeWidth
109
+ : validateNonNegativeFinite(
110
+ options.strokeWidth,
111
+ "createBoxPlot box.strokeWidth"
112
+ )
113
+ });
114
+ }
115
+
116
+ export function resolveBoxMedianAppearance(value) {
117
+ const options = plainOptions(value, ["stroke", "strokeWidth"], "median");
118
+ return Object.freeze({
119
+ stroke: options.stroke === undefined
120
+ ? DEFAULT_MEDIAN.stroke
121
+ : validateNonEmptyString(options.stroke, "createBoxPlot median.stroke"),
122
+ strokeWidth: options.strokeWidth === undefined
123
+ ? DEFAULT_MEDIAN.strokeWidth
124
+ : validateNonNegativeFinite(
125
+ options.strokeWidth,
126
+ "createBoxPlot median.strokeWidth"
127
+ )
128
+ });
129
+ }
130
+
131
+ export function resolveBoxOutlierAppearance(value) {
132
+ const options = plainOptions(value, ["shape", "radius", "opacity"], "outlier");
133
+ return Object.freeze({
134
+ shape: options.shape === undefined
135
+ ? DEFAULT_OUTLIER.shape
136
+ : validatePointShape(options.shape),
137
+ radius: options.radius === undefined
138
+ ? DEFAULT_OUTLIER.radius
139
+ : validatePositiveFinite(options.radius, "createBoxPlot outlier.radius"),
140
+ opacity: options.opacity === undefined
141
+ ? DEFAULT_OUTLIER.opacity
142
+ : validateUnitInterval(
143
+ options.opacity,
144
+ "createBoxPlot outlier.opacity"
145
+ )
146
+ });
147
+ }
@@ -0,0 +1,38 @@
1
+ import { resolveOptionalUserId, validateUserId } from "../../core/identifiers.js";
2
+ import { findLayer, hasLayer } from "../../selectors/layers.js";
3
+
4
+ const CATEGORY_TYPES = Object.freeze(["nominal", "ordinal", "temporal"]);
5
+
6
+ export function resolveBoxOrientation(x, y) {
7
+ const xType = x?.fieldType ?? "quantitative";
8
+ const yType = y?.fieldType ?? "quantitative";
9
+ if (CATEGORY_TYPES.includes(xType) && yType === "quantitative") return "vertical";
10
+ if (xType === "quantitative" && CATEGORY_TYPES.includes(yType)) return "horizontal";
11
+ return undefined;
12
+ }
13
+
14
+ export function resolveBoxSourceLayer(program, target) {
15
+ if (target !== undefined) {
16
+ const layer = findLayer(program, validateUserId(target, "Box source layer id"));
17
+ if (layer === undefined) throw new Error(`Unknown box source layer "${target}".`);
18
+ return layer;
19
+ }
20
+ const current = findLayer(program, program.context.currentMark);
21
+ if (current?.encoding?.x !== undefined || current?.encoding?.y !== undefined) {
22
+ return current;
23
+ }
24
+ const eligible = program.semanticSpec.layers.filter(
25
+ layer => layer.encoding?.x !== undefined && layer.encoding?.y !== undefined
26
+ );
27
+ return eligible.length === 1 ? eligible[0] : undefined;
28
+ }
29
+
30
+ export function resolveBoxPlotId(program, requested) {
31
+ return resolveOptionalUserId(requested, {
32
+ defaultId: "boxPlot",
33
+ label: "Box-plot id",
34
+ operation: "createBoxPlot",
35
+ ambiguous: hasLayer(program, "boxPlot") ||
36
+ program.graphicSpec.objects.boxPlot !== undefined
37
+ });
38
+ }
@@ -0,0 +1,144 @@
1
+ import { action } from "../../core/action.js";
2
+ import {
3
+ DEFAULT_CANVAS,
4
+ DEFAULT_MARGIN,
5
+ normalizeMargin,
6
+ validateCanvasState
7
+ } from "../../layout/canvas.js";
8
+ import { cloneAndFreeze } from "../../core/immutable.js";
9
+ import {
10
+ applyMaterializationPlan,
11
+ planCanvasRematerialization
12
+ } from "../../materialization/dependencies.js";
13
+ import {
14
+ assertCanvasHierarchyAvailable,
15
+ CANVAS_GRAPHIC_ID,
16
+ PLOT_GRAPHIC_ID
17
+ } from "../../materialization/graphicHierarchy.js";
18
+
19
+ const CANVAS_OPTIONS = Object.freeze([
20
+ "width",
21
+ "height",
22
+ "background",
23
+ "margin"
24
+ ]);
25
+
26
+ function validateOptions(args, operation, { allowEmpty = false } = {}) {
27
+ const keys = Object.keys(args);
28
+
29
+ if (!allowEmpty && keys.length === 0) {
30
+ throw new TypeError(`${operation} requires at least one option.`);
31
+ }
32
+
33
+ for (const key of keys) {
34
+ if (!CANVAS_OPTIONS.includes(key)) {
35
+ throw new Error(`Unknown ${operation} option "${key}".`);
36
+ }
37
+ }
38
+ }
39
+
40
+ function requireCanvas(program) {
41
+ const canvas = program.graphicSpec.objects.canvas;
42
+
43
+ if (canvas?.type !== "canvas") {
44
+ throw new Error("editCanvas requires an existing canvas.");
45
+ }
46
+
47
+ return canvas;
48
+ }
49
+
50
+ function resolveCanvasState(program, args) {
51
+ const canvas = requireCanvas(program);
52
+ const baseMargin =
53
+ program.materializationConfigs.canvas?.margin ?? DEFAULT_MARGIN;
54
+ const state = {
55
+ width: Object.hasOwn(args, "width")
56
+ ? args.width
57
+ : canvas.properties.width,
58
+ height: Object.hasOwn(args, "height")
59
+ ? args.height
60
+ : canvas.properties.height,
61
+ background: Object.hasOwn(args, "background")
62
+ ? args.background
63
+ : canvas.properties.background,
64
+ margin: Object.hasOwn(args, "margin")
65
+ ? normalizeMargin(args.margin, baseMargin)
66
+ : baseMargin
67
+ };
68
+
69
+ validateCanvasState(state);
70
+ return cloneAndFreeze(state);
71
+ }
72
+
73
+ export const editCanvas = action(
74
+ {
75
+ op: "editCanvas",
76
+ description: "Edit canvas properties and authoring bounds."
77
+ },
78
+ function (args = {}) {
79
+ validateOptions(args, "editCanvas");
80
+ const state = resolveCanvasState(this, args);
81
+ let next = this;
82
+
83
+ for (const property of ["width", "height", "background"]) {
84
+ if (Object.hasOwn(args, property)) {
85
+ next = next.editGraphics({
86
+ target: "canvas",
87
+ property,
88
+ value: state[property]
89
+ });
90
+ }
91
+ }
92
+
93
+ next = next._withCanvasConfig({ margin: state.margin });
94
+
95
+ if (
96
+ Object.hasOwn(args, "width") ||
97
+ Object.hasOwn(args, "height") ||
98
+ Object.hasOwn(args, "margin")
99
+ ) {
100
+ next = applyMaterializationPlan(
101
+ next,
102
+ planCanvasRematerialization(next)
103
+ );
104
+ }
105
+
106
+ return next;
107
+ }
108
+ );
109
+
110
+ export const createCanvas = action(
111
+ {
112
+ op: "createCanvas",
113
+ description: "Create and configure the chart canvas."
114
+ },
115
+ function (args = {}) {
116
+ validateOptions(args, "createCanvas", { allowEmpty: true });
117
+
118
+ assertCanvasHierarchyAvailable(this);
119
+
120
+ const options = {
121
+ width: Object.hasOwn(args, "width")
122
+ ? args.width
123
+ : DEFAULT_CANVAS.width,
124
+ height: Object.hasOwn(args, "height")
125
+ ? args.height
126
+ : DEFAULT_CANVAS.height,
127
+ background: Object.hasOwn(args, "background")
128
+ ? args.background
129
+ : DEFAULT_CANVAS.background,
130
+ margin: Object.hasOwn(args, "margin")
131
+ ? args.margin
132
+ : DEFAULT_CANVAS.margin
133
+ };
134
+
135
+ return this
136
+ .createGraphics({ id: CANVAS_GRAPHIC_ID, type: "canvas" })
137
+ .createGraphics({
138
+ id: PLOT_GRAPHIC_ID,
139
+ type: "collection",
140
+ parent: CANVAS_GRAPHIC_ID
141
+ })
142
+ .editCanvas(options);
143
+ }
144
+ );
@@ -0,0 +1,6 @@
1
+ import { createCanvas, editCanvas } from "./actions.js";
2
+
3
+ export function registerCanvasActions(ProgramClass) {
4
+ ProgramClass.prototype.editCanvas = editCanvas;
5
+ ProgramClass.prototype.createCanvas = createCanvas;
6
+ }
@@ -0,0 +1,85 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateCoordinateType } from "../../grammar/coordinates.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import { findCoordinate } from "../../selectors/coordinates.js";
5
+ import { findLayer, hasLayer } from "../../selectors/layers.js";
6
+
7
+ const COORDINATE_OPTIONS = Object.freeze(["id", "type", "layers"]);
8
+
9
+ function validateOptions(args) {
10
+ for (const key of Object.keys(args)) {
11
+ if (!COORDINATE_OPTIONS.includes(key)) {
12
+ throw new Error(`Unknown createCoordinate option "${key}".`);
13
+ }
14
+ }
15
+ }
16
+
17
+ function validateLayers(program, layers) {
18
+ if (!Array.isArray(layers)) {
19
+ throw new TypeError("createCoordinate layers must be an array.");
20
+ }
21
+
22
+ const ids = layers.map(layer => validateUserId(layer, "Layer id"));
23
+
24
+ if (new Set(ids).size !== ids.length) {
25
+ throw new Error("createCoordinate layers must not contain duplicates.");
26
+ }
27
+
28
+ for (const id of ids) {
29
+ if (!hasLayer(program, id)) {
30
+ throw new Error(`Unknown layer "${id}".`);
31
+ }
32
+ }
33
+
34
+ return ids;
35
+ }
36
+
37
+ export const createCoordinate = action(
38
+ {
39
+ op: "createCoordinate",
40
+ description: "Create a semantic coordinate and attach layers to it."
41
+ },
42
+ function (args = {}) {
43
+ validateOptions(args);
44
+ const id = validateUserId(args.id ?? "main", "Coordinate id");
45
+ const type = validateCoordinateType(args.type ?? "cartesian");
46
+ const layers = validateLayers(this, args.layers ?? []);
47
+ const existing = findCoordinate(this, id);
48
+
49
+ if (existing !== undefined && existing.type !== type) {
50
+ throw new Error(
51
+ `Coordinate "${id}" already exists with type "${existing.type}".`
52
+ );
53
+ }
54
+
55
+ for (const layerId of layers) {
56
+ const layer = findLayer(this, layerId);
57
+
58
+ if (layer.coordinate !== undefined && layer.coordinate !== id) {
59
+ throw new Error(
60
+ `Layer "${layerId}" already uses coordinate "${layer.coordinate}".`
61
+ );
62
+ }
63
+ }
64
+
65
+ let program = existing === undefined
66
+ ? this.editSemantic({
67
+ property: `coordinate[${id}].type`,
68
+ value: type
69
+ })
70
+ : this;
71
+
72
+ for (const layerId of layers) {
73
+ const layer = findLayer(program, layerId);
74
+
75
+ if (layer.coordinate === undefined) {
76
+ program = program.editSemantic({
77
+ property: `layer[${layerId}].coordinate`,
78
+ value: id
79
+ });
80
+ }
81
+ }
82
+
83
+ return program;
84
+ }
85
+ );
@@ -0,0 +1,5 @@
1
+ import { createCoordinate } from "./actions.js";
2
+
3
+ export function registerCoordinateActions(ProgramClass) {
4
+ ProgramClass.prototype.createCoordinate = createCoordinate;
5
+ }
@@ -0,0 +1,40 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import { BOX_FIELDS, deriveBoxData, normalizeBoxTransform } from "../../grammar/boxPlot.js";
5
+ import { MATERIALIZE_OPTIONS, requireDerivedDataset } from "./shared.js";
6
+
7
+ const OPTIONS = Object.freeze([
8
+ "id", "source", "category", "field", "whisker", "factor", "as"
9
+ ]);
10
+
11
+ function materializer(type, op, select) {
12
+ return action({ op, description: `Materialize ${type} box-plot data.` }, function (args = {}) {
13
+ validateKeys(args, MATERIALIZE_OPTIONS, op);
14
+ const { id, source, transform } = requireDerivedDataset(this, args.id, type);
15
+ return this.editSemantic({ property: `dataset[${id}].values`, value: select(deriveBoxData(source.values, transform)) });
16
+ });
17
+ }
18
+
19
+ export const materializeBoxSummaryData = materializer("boxSummary", "materializeBoxSummaryData", value => value.summaries);
20
+ export const materializeBoxOutlierData = materializer("boxOutlier", "materializeBoxOutlierData", value => value.outliers);
21
+
22
+ function creator(type, op, materialize) {
23
+ return action({ op, description: `Create immutable ${type} box-plot data.` }, function (args = {}) {
24
+ validateKeys(args, OPTIONS, op);
25
+ const id = validateUserId(args.id, "Box dataset id");
26
+ const source = validateUserId(args.source ?? this.context.currentData, "Box source dataset id");
27
+ const transform = normalizeBoxTransform({
28
+ type,
29
+ category: args.category,
30
+ field: args.field,
31
+ ...(args.whisker === undefined ? {} : { whisker: args.whisker }),
32
+ ...(args.factor === undefined ? {} : { factor: args.factor }),
33
+ as: args.as ?? BOX_FIELDS
34
+ });
35
+ return this.createDerivedData({ id, source, transform: [transform] })[materialize]({ id });
36
+ });
37
+ }
38
+
39
+ export const createBoxSummaryData = creator("boxSummary", "createBoxSummaryData", "materializeBoxSummaryData");
40
+ export const createBoxOutlierData = creator("boxOutlier", "createBoxOutlierData", "materializeBoxOutlierData");
@@ -0,0 +1,33 @@
1
+ import { action } from "../../core/action.js";
2
+ import { resolveOptionalUserId } from "../../core/identifiers.js";
3
+ import { isPlainObject } from "../../core/immutable.js";
4
+ import { validateKeys } from "../../core/validation.js";
5
+ import { hasDataset } from "../../selectors/index.js";
6
+
7
+ const OPTIONS = Object.freeze(["id", "values"]);
8
+
9
+ export const createData = action(
10
+ { op: "createData", description: "Create an immutable named dataset." },
11
+ function (args = {}) {
12
+ validateKeys(args, OPTIONS, "createData");
13
+ const id = resolveOptionalUserId(args.id, {
14
+ defaultId: "data",
15
+ label: "Dataset id",
16
+ operation: "createData",
17
+ ambiguous: this.semanticSpec.datasets.length > 0
18
+ });
19
+ if (!Array.isArray(args.values)) {
20
+ throw new TypeError("createData requires values to be an array.");
21
+ }
22
+ if (!args.values.every(isPlainObject)) {
23
+ throw new TypeError("createData requires every row to be a plain object.");
24
+ }
25
+ if (hasDataset(this, id)) {
26
+ throw new Error(`Dataset "${id}" already exists.`);
27
+ }
28
+ return this.editSemantic({
29
+ property: `dataset[${id}].values`,
30
+ value: args.values
31
+ });
32
+ }
33
+ );