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,248 @@
1
+ import { action } from "../../core/action.js";
2
+ import { deriveLineSeries } from "../../grammar/lineSeries.js";
3
+ import { mapContinuousScaleValues, mapOrdinalValues } from "../../grammar/scales.js";
4
+ import { validateUserId } from "../../core/identifiers.js";
5
+ import { validateNonNegativeFinite } from "../../core/validation.js";
6
+ import {
7
+ assertMarkAvailable,
8
+ resolveMarkId,
9
+ resolveMarkData,
10
+ validateMarkOptions
11
+ } from "./shared.js";
12
+ import { DEFAULT_COLORS } from "../../theme/defaults.js";
13
+ import { findDataset } from "../../selectors/datasets.js";
14
+ import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
15
+ import {
16
+ buildCurvePathCommands,
17
+ validateCurveInterpolation
18
+ } from "../../grammar/curveCommands.js";
19
+ import { normalizeStrokeDashPattern } from "../../grammar/scales.js";
20
+ import { canMaterializeLine } from "../../materialization/marks.js";
21
+ import { resolveMarkGraphicPlacement } from
22
+ "../../materialization/graphicHierarchy.js";
23
+
24
+ const DEFAULT_LINE_STROKE = DEFAULT_COLORS.mark;
25
+ const DEFAULT_LINE_WIDTH = 2;
26
+ const CREATE_OPTIONS = Object.freeze(["id", "data", "strokeWidth", "curve"]);
27
+ const EDIT_OPTIONS = Object.freeze(["target", "strokeWidth", "curve"]);
28
+ const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
29
+
30
+ const createLineMark = action(
31
+ {
32
+ op: "createLineMark",
33
+ description: "Create a semantic line mark and empty path collection."
34
+ },
35
+ function (args = {}) {
36
+ validateMarkOptions(args, CREATE_OPTIONS, "createLineMark");
37
+ const id = resolveMarkId(this, args.id, {
38
+ defaultId: "line",
39
+ label: "Line mark id",
40
+ markType: "line",
41
+ operation: "createLineMark"
42
+ });
43
+ const { data } = resolveMarkData(this, args);
44
+ const strokeWidth = validateNonNegativeFinite(
45
+ args.strokeWidth ?? DEFAULT_LINE_WIDTH,
46
+ "Line strokeWidth"
47
+ );
48
+ const curve = validateCurveInterpolation(args.curve ?? "linear");
49
+ assertMarkAvailable(this, id);
50
+
51
+ return this
52
+ .editSemantic({
53
+ property: `layer[${id}].mark.type`,
54
+ value: "line"
55
+ })
56
+ .editSemantic({
57
+ property: `layer[${id}].data`,
58
+ value: data
59
+ })
60
+ .createGraphics({
61
+ id,
62
+ type: "path",
63
+ length: 0,
64
+ ...resolveMarkGraphicPlacement(this, { data, markType: "line" })
65
+ })
66
+ ._withMarkConfig(
67
+ id,
68
+ {
69
+ ...(Object.hasOwn(args, "strokeWidth") ? { strokeWidth } : {}),
70
+ ...(Object.hasOwn(args, "curve") ? { curve } : {})
71
+ }
72
+ );
73
+ }
74
+ );
75
+
76
+ function requireLine(program, id) {
77
+ const layer = findLayer(program, id);
78
+
79
+ if (layer?.mark?.type !== "line") {
80
+ throw new Error(`Unknown line mark "${id}".`);
81
+ }
82
+
83
+ const dataset = findDataset(program, layer.data);
84
+
85
+ if (dataset === undefined) {
86
+ throw new Error(`Line mark "${id}" requires an existing dataset.`);
87
+ }
88
+
89
+ if (program.graphicSpec.objects[id]?.type !== "path") {
90
+ throw new Error(`Line mark "${id}" requires path graphics.`);
91
+ }
92
+
93
+ return { dataset, layer };
94
+ }
95
+
96
+ const rematerializeLineMark = action(
97
+ {
98
+ op: "rematerializeLineMark",
99
+ description: "Recompute aggregate series and concrete line paths."
100
+ },
101
+ function (args = {}) {
102
+ validateMarkOptions(
103
+ args,
104
+ REMATERIALIZE_OPTIONS,
105
+ "rematerializeLineMark"
106
+ );
107
+ const id = validateUserId(args.id, "Line mark id");
108
+ const highlights = Object.entries(
109
+ this.materializationConfigs.highlights ?? {}
110
+ ).filter(([, config]) => config.target === id);
111
+ if (highlights.length > 0) {
112
+ let baseline = this;
113
+ for (const [highlightId] of highlights) {
114
+ baseline = baseline._withoutMaterializationConfig(["highlights", highlightId]);
115
+ }
116
+ return baseline
117
+ .editGraphics({ target: id, property: "length", value: 0 })
118
+ .rematerializeLineMark({ id })
119
+ .rematerializeMarkHighlights({ target: id, highlights });
120
+ }
121
+ const { dataset, layer } = requireLine(this, id);
122
+ const existingChildren = this.graphicSpec.objects[id].items;
123
+ const xScaleId = layer.encoding?.x?.scale;
124
+ const yScaleId = layer.encoding?.y?.scale;
125
+ const derived = deriveLineSeries(dataset.values, layer);
126
+
127
+ if (xScaleId === undefined || yScaleId === undefined) {
128
+ throw new Error(`Line mark "${id}" requires x and y scales.`);
129
+ }
130
+
131
+ let resolved = this
132
+ .rematerializeScale({ id: xScaleId })
133
+ .rematerializeScale({ id: yScaleId });
134
+
135
+ for (const channel of ["color", "strokeDash"]) {
136
+ const scaleId = layer.encoding?.[channel]?.scale;
137
+ if (scaleId !== undefined) {
138
+ resolved = resolved.rematerializeScale({ id: scaleId });
139
+ }
140
+ }
141
+
142
+ const xScale = resolved.resolvedScales[xScaleId];
143
+ const yScale = resolved.resolvedScales[yScaleId];
144
+ const commands = derived.series.map(series => {
145
+ const x = mapContinuousScaleValues(
146
+ series.values.map(value => value.x),
147
+ xScale
148
+ );
149
+ const y = mapContinuousScaleValues(
150
+ series.values.map(value => value.y),
151
+ yScale
152
+ );
153
+
154
+ return buildCurvePathCommands(
155
+ series.values.map((_, index) => ({ x: x[index], y: y[index] })),
156
+ this.markConfigs[id]?.curve ?? "linear"
157
+ );
158
+ });
159
+ const colorEncoding = layer.encoding?.color;
160
+ const dashEncoding = layer.encoding?.strokeDash;
161
+ const strokes = colorEncoding?.scale === undefined
162
+ ? commands.map(
163
+ (_, index) =>
164
+ existingChildren[index]?.properties.stroke ?? DEFAULT_LINE_STROKE
165
+ )
166
+ : mapOrdinalValues(
167
+ derived.series.map(series => series.key[colorEncoding.field]),
168
+ resolved.resolvedScales[colorEncoding.scale].domain,
169
+ resolved.resolvedScales[colorEncoding.scale].range
170
+ );
171
+ const strokeWidths = commands.map(
172
+ (_, index) =>
173
+ this.markConfigs[id]?.strokeWidth ??
174
+ existingChildren[index]?.properties.strokeWidth ??
175
+ DEFAULT_LINE_WIDTH
176
+ );
177
+ const strokeDashes = dashEncoding?.datum !== undefined
178
+ ? commands.map(() => normalizeStrokeDashPattern(dashEncoding.datum))
179
+ : dashEncoding?.scale === undefined
180
+ ? commands.map(
181
+ (_, index) => existingChildren[index]?.properties.strokeDash ?? []
182
+ )
183
+ : mapOrdinalValues(
184
+ derived.series.map(series => series.key[dashEncoding.field]),
185
+ resolved.resolvedScales[dashEncoding.scale].domain,
186
+ resolved.resolvedScales[dashEncoding.scale].range
187
+ );
188
+
189
+ return resolved
190
+ .editGraphics({ target: id, property: "length", value: commands.length })
191
+ .editGraphics({ target: id, property: "commands", value: commands })
192
+ .editGraphics({ target: id, property: "stroke", value: strokes })
193
+ .editGraphics({ target: id, property: "strokeWidth", value: strokeWidths })
194
+ .editGraphics({
195
+ target: id,
196
+ property: "strokeDash",
197
+ value: strokeDashes
198
+ });
199
+ }
200
+ );
201
+
202
+ const editLineMark = action(
203
+ {
204
+ op: "editLineMark",
205
+ description: "Edit line-mark curve and stroke width."
206
+ },
207
+ function (args = {}) {
208
+ validateMarkOptions(args, EDIT_OPTIONS, "editLineMark");
209
+ if (
210
+ !Object.hasOwn(args, "strokeWidth") &&
211
+ !Object.hasOwn(args, "curve")
212
+ ) {
213
+ throw new Error("editLineMark requires strokeWidth or curve.");
214
+ }
215
+ const target = Object.hasOwn(args, "target")
216
+ ? validateUserId(args.target, "Line mark id")
217
+ : undefined;
218
+ const layer = resolveEligibleLayer(this, {
219
+ target,
220
+ predicate: candidate => candidate.mark?.type === "line",
221
+ label: "line mark"
222
+ });
223
+ const config = {
224
+ ...this.markConfigs[layer.id],
225
+ ...(Object.hasOwn(args, "strokeWidth")
226
+ ? {
227
+ strokeWidth: validateNonNegativeFinite(
228
+ args.strokeWidth,
229
+ "Line strokeWidth"
230
+ )
231
+ }
232
+ : {}),
233
+ ...(Object.hasOwn(args, "curve")
234
+ ? { curve: validateCurveInterpolation(args.curve) }
235
+ : {})
236
+ };
237
+ const next = this._withMarkConfig(layer.id, config);
238
+ return canMaterializeLine(next, layer)
239
+ ? next.rematerializeLineMark({ id: layer.id })
240
+ : next;
241
+ }
242
+ );
243
+
244
+ export function registerLineMarkActions(ProgramClass) {
245
+ ProgramClass.prototype.createLineMark = createLineMark;
246
+ ProgramClass.prototype.editLineMark = editLineMark;
247
+ ProgramClass.prototype.rematerializeLineMark = rematerializeLineMark;
248
+ }
@@ -0,0 +1,420 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import {
4
+ validateNonEmptyString,
5
+ validateNonNegativeFinite,
6
+ validateUnitInterval
7
+ } from "../../core/validation.js";
8
+ import { getPointGraphicType } from "../../grammar/schemas/mark.js";
9
+ import {
10
+ createPointShapeGraphic,
11
+ validatePointShape
12
+ } from "../../grammar/pointShapes.js";
13
+ import {
14
+ mapContinuousScaleValues,
15
+ mapDiscretizedColors,
16
+ mapOrdinalPositionValues,
17
+ mapOrdinalValues,
18
+ mapSequentialColors,
19
+ readScaleField,
20
+ readNominalField,
21
+ readQuantitativeField,
22
+ readTemporalField
23
+ } from "../../grammar/scales.js";
24
+ import {
25
+ assertMarkAvailable,
26
+ resolveLayeredMarkInheritance,
27
+ resolveMarkId,
28
+ resolveMarkData,
29
+ validateMarkOptions
30
+ } from "./shared.js";
31
+ import { DEFAULT_COLORS } from "../../theme/defaults.js";
32
+ import { findDataset } from "../../selectors/datasets.js";
33
+ import { findLayer } from "../../selectors/layers.js";
34
+ import { rematerializeExistingLegend } from "../encodings/shared.js";
35
+ import { resolveMarkGraphicPlacement } from
36
+ "../../materialization/graphicHierarchy.js";
37
+
38
+ const POINT_MARK_OPTIONS = Object.freeze(["id", "data", "shape"]);
39
+ const EDIT_POINT_OPTIONS = Object.freeze([
40
+ "target", "shape", "fill", "opacity", "stroke", "strokeWidth"
41
+ ]);
42
+ const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
43
+ const DEFAULT_POINT_FILL = DEFAULT_COLORS.mark;
44
+
45
+ const createPointMark = action(
46
+ {
47
+ op: "createPointMark",
48
+ description: "Create a semantic point mark and concrete point graphics."
49
+ },
50
+ function (args = {}) {
51
+ validateMarkOptions(args, POINT_MARK_OPTIONS, "createPointMark");
52
+ const id = resolveMarkId(this, args.id, {
53
+ defaultId: "point",
54
+ label: "Point mark id",
55
+ markType: "point",
56
+ operation: "createPointMark"
57
+ });
58
+ const inherited = resolveLayeredMarkInheritance(this, args);
59
+ const { data, dataset } = resolveMarkData(this, {
60
+ ...args,
61
+ ...(args.data === undefined &&
62
+ this.context.currentData === undefined &&
63
+ inherited?.data !== undefined
64
+ ? { data: inherited.data }
65
+ : {})
66
+ });
67
+ const shape = Object.hasOwn(args, "shape") ? args.shape : "circle";
68
+ validatePointShape(shape);
69
+ const resolvedType = getPointGraphicType(shape);
70
+ const graphicType = resolvedType === "path" ? "circle" : resolvedType;
71
+
72
+ assertMarkAvailable(this, id);
73
+
74
+ let next = this
75
+ .editSemantic({
76
+ property: `layer[${id}].mark.type`,
77
+ value: "point"
78
+ })
79
+ .editSemantic({
80
+ property: `layer[${id}].data`,
81
+ value: data
82
+ });
83
+ if (inherited?.coordinate !== undefined) {
84
+ next = next.editSemantic({
85
+ property: `layer[${id}].coordinate`,
86
+ value: inherited.coordinate
87
+ });
88
+ }
89
+ for (const channel of ["x", "y"]) {
90
+ for (const [property, value] of Object.entries(
91
+ inherited?.encoding[channel] ?? {}
92
+ )) {
93
+ next = next.editSemantic({
94
+ property: `layer[${id}].encoding.${channel}.${property}`,
95
+ value
96
+ });
97
+ }
98
+ }
99
+ next = next
100
+ .createGraphics({
101
+ id,
102
+ type: graphicType,
103
+ length: dataset.values.length,
104
+ ...resolveMarkGraphicPlacement(next, { data, markType: "point" })
105
+ })
106
+ ._withMarkConfig(id, { shape });
107
+ const inheritedEncodings = Object.values(inherited?.encoding ?? {});
108
+ const canMaterializeInherited = inheritedEncodings.length > 0 &&
109
+ inheritedEncodings.every(encoding =>
110
+ encoding.scale !== undefined && next.resolvedScales[encoding.scale] !== undefined
111
+ );
112
+ return !canMaterializeInherited
113
+ ? next
114
+ : next.rematerializePointMark({ id });
115
+ }
116
+ );
117
+
118
+ function resolveMappedValues(program, layer, dataset, channel) {
119
+ const encoding = layer.encoding?.[channel];
120
+ if (encoding === undefined) return undefined;
121
+ const scale = program.resolvedScales[encoding.scale];
122
+ if (scale === undefined) {
123
+ throw new Error(
124
+ `Point mark "${layer.id}" requires resolved ${channel} scale "${encoding.scale}".`
125
+ );
126
+ }
127
+ const ordinal = ["nominal", "ordinal"].includes(encoding.fieldType);
128
+ const values = Object.hasOwn(scale, "unknown")
129
+ ? readScaleField(dataset.values, encoding.field, encoding.fieldType, {
130
+ allowUnknown: true
131
+ })
132
+ : ordinal
133
+ ? readNominalField(dataset.values, encoding.field)
134
+ : encoding.fieldType === "temporal"
135
+ ? readTemporalField(dataset.values, encoding.field)
136
+ : readQuantitativeField(dataset.values, encoding.field);
137
+ if (channel === "color" && scale.type === "sequential") {
138
+ return mapSequentialColors(values, scale.domain, scale.range, {
139
+ interpolation: scale.interpolate,
140
+ clamp: scale.clamp ?? false,
141
+ ...(Object.hasOwn(scale, "unknown") ? { unknown: scale.unknown } : {})
142
+ });
143
+ }
144
+ if (
145
+ channel === "color" &&
146
+ ["quantize", "quantile", "threshold"].includes(scale.type)
147
+ ) {
148
+ return mapDiscretizedColors(values, scale);
149
+ }
150
+ return ordinal
151
+ ? ["x", "y"].includes(channel)
152
+ ? mapOrdinalPositionValues(values, scale)
153
+ : mapOrdinalValues(values, scale.domain, scale.range, {
154
+ ...(Object.hasOwn(scale, "unknown") ? { unknown: scale.unknown } : {})
155
+ })
156
+ : mapContinuousScaleValues(values, scale);
157
+ }
158
+
159
+ function compactProperties(properties) {
160
+ return Object.fromEntries(
161
+ Object.entries(properties).filter(([, value]) => value !== undefined)
162
+ );
163
+ }
164
+
165
+ function incompleteShapeGraphic(shape, properties) {
166
+ const type = getPointGraphicType(shape);
167
+ const incomplete = type === "path"
168
+ ? { fill: properties.fill, opacity: properties.opacity }
169
+ : properties;
170
+ return { type, properties: compactProperties(incomplete) };
171
+ }
172
+
173
+ function existingArea(child, parentType) {
174
+ const type = child?.type ?? parentType;
175
+ if (type === "circle" && Number.isFinite(child?.properties?.radius)) {
176
+ return Math.PI * child.properties.radius ** 2;
177
+ }
178
+ if (
179
+ type === "rect" &&
180
+ Number.isFinite(child.properties?.width) &&
181
+ Number.isFinite(child.properties?.height)
182
+ ) {
183
+ return child.properties.width * child.properties.height;
184
+ }
185
+ return undefined;
186
+ }
187
+
188
+ const rematerializePointMark = action(
189
+ {
190
+ op: "rematerializePointMark",
191
+ description: "Recompute concrete point geometry and appearance."
192
+ },
193
+ function (args = {}) {
194
+ validateMarkOptions(args, REMATERIALIZE_OPTIONS, "rematerializePointMark");
195
+ const id = validateUserId(args.id, "Point mark id");
196
+ const highlights = Object.entries(
197
+ this.materializationConfigs.highlights ?? {}
198
+ ).filter(([, config]) => config.target === id);
199
+ if (highlights.length > 0) {
200
+ let baseline = this;
201
+ for (const [highlightId] of highlights) {
202
+ baseline = baseline._withoutMaterializationConfig([
203
+ "highlights",
204
+ highlightId
205
+ ]);
206
+ }
207
+ return baseline
208
+ .editGraphics({ target: id, property: "items", value: [] })
209
+ .rematerializePointMark({ id })
210
+ .rematerializeMarkHighlights({ target: id, highlights });
211
+ }
212
+ const layer = findLayer(this, id);
213
+ const graphic = this.graphicSpec.objects[id];
214
+
215
+ if (layer?.mark?.type !== "point") {
216
+ throw new Error(`Unknown point mark "${id}".`);
217
+ }
218
+ if (!["circle", "rect", "path", "collection"].includes(graphic?.type)) {
219
+ throw new Error(`Point mark "${id}" requires point graphics.`);
220
+ }
221
+ const dataset = findDataset(this, layer.data);
222
+ if (dataset === undefined) {
223
+ throw new Error(`Point mark "${id}" requires an existing dataset.`);
224
+ }
225
+
226
+ const x = resolveMappedValues(this, layer, dataset, "x");
227
+ const y = resolveMappedValues(this, layer, dataset, "y");
228
+ const mappedFill = resolveMappedValues(this, layer, dataset, "color");
229
+ const area = resolveMappedValues(this, layer, dataset, "size");
230
+ const encodedShape = resolveMappedValues(this, layer, dataset, "shape");
231
+ const encodedOpacity = resolveMappedValues(this, layer, dataset, "opacity");
232
+ const config = this.markConfigs[id] ?? {};
233
+ const fill = mappedFill ?? config.fill ?? DEFAULT_POINT_FILL;
234
+ const shapes = encodedShape ?? dataset.values.map(() => config.shape ?? "circle");
235
+ const existingChildren = graphic.items ?? [];
236
+ const constantShape = validatePointShape(config.shape ?? "circle");
237
+ const requiresMixedCollection =
238
+ encodedShape !== undefined ||
239
+ graphic.type === "collection" ||
240
+ getPointGraphicType(constantShape) !== graphic.type;
241
+
242
+ if (requiresMixedCollection) {
243
+ const items = dataset.values.map((_, index) => {
244
+ const shape = shapes[index];
245
+ const existing = existingChildren[index]?.properties ?? {};
246
+ const color = mappedFill?.[index] ?? config.fill ??
247
+ existing.fill ?? DEFAULT_POINT_FILL;
248
+ const opacity = encodedOpacity?.[index] ?? config.opacity ?? existing.opacity;
249
+ const resolvedArea = area?.[index] ??
250
+ (config.radius === undefined
251
+ ? existingArea(existingChildren[index], graphic.type)
252
+ :
253
+ Math.PI * config.radius ** 2);
254
+ const centerX = x?.[index] ?? existing.x;
255
+ const centerY = y?.[index] ?? existing.y;
256
+ if (
257
+ resolvedArea === undefined ||
258
+ !Number.isFinite(centerX) ||
259
+ !Number.isFinite(centerY)
260
+ ) {
261
+ return incompleteShapeGraphic(shape, {
262
+ x: centerX,
263
+ y: centerY,
264
+ fill: color,
265
+ opacity
266
+ });
267
+ }
268
+ return createPointShapeGraphic({
269
+ shape,
270
+ x: centerX,
271
+ y: centerY,
272
+ area: resolvedArea,
273
+ fill: color,
274
+ ...(config.stroke === undefined ? {} : { stroke: config.stroke }),
275
+ ...(config.strokeWidth === undefined
276
+ ? {}
277
+ : { strokeWidth: config.strokeWidth }),
278
+ opacity
279
+ });
280
+ });
281
+ return this.editGraphics({ target: id, property: "items", value: items });
282
+ }
283
+
284
+ let next = graphic.items.length === dataset.values.length
285
+ ? this
286
+ : this.editGraphics({
287
+ target: id,
288
+ property: "length",
289
+ value: dataset.values.length
290
+ });
291
+ if (x !== undefined) next = next.editGraphics({ target: id, property: "x", value: x });
292
+ if (y !== undefined) next = next.editGraphics({ target: id, property: "y", value: y });
293
+ if (fill !== undefined) {
294
+ next = next.editGraphics({ target: id, property: "fill", value: fill });
295
+ }
296
+ if (graphic.type === "circle") {
297
+ const radii = area === undefined
298
+ ? config.radius
299
+ : area.map(value => Math.sqrt(value / Math.PI));
300
+ if (radii !== undefined) {
301
+ next = next.editGraphics({ target: id, property: "radius", value: radii });
302
+ }
303
+ } else if (area !== undefined || config.radius !== undefined) {
304
+ const sides = area === undefined
305
+ ? config.radius * Math.sqrt(Math.PI)
306
+ : area.map(value => Math.sqrt(value));
307
+ const centersX = x ?? existingChildren.map(child => child.properties.x);
308
+ const centersY = y ?? existingChildren.map(child => child.properties.y);
309
+ if (centersX.every(Number.isFinite)) {
310
+ next = next.editGraphics({
311
+ target: id,
312
+ property: "x",
313
+ value: centersX.map((value, index) => value - (Array.isArray(sides) ? sides[index] : sides) / 2)
314
+ });
315
+ }
316
+ if (centersY.every(Number.isFinite)) {
317
+ next = next.editGraphics({
318
+ target: id,
319
+ property: "y",
320
+ value: centersY.map((value, index) => value - (Array.isArray(sides) ? sides[index] : sides) / 2)
321
+ });
322
+ }
323
+ next = next
324
+ .editGraphics({ target: id, property: "width", value: sides })
325
+ .editGraphics({ target: id, property: "height", value: sides })
326
+ .editGraphics({ target: id, property: "stroke", value: fill ?? DEFAULT_POINT_FILL })
327
+ .editGraphics({ target: id, property: "strokeWidth", value: 0 });
328
+ }
329
+ if (config.opacity !== undefined) {
330
+ next = next.editGraphics({ target: id, property: "opacity", value: config.opacity });
331
+ } else if (encodedOpacity !== undefined) {
332
+ next = next.editGraphics({ target: id, property: "opacity", value: encodedOpacity });
333
+ }
334
+ if (config.stroke !== undefined) {
335
+ next = next.editGraphics({ target: id, property: "stroke", value: config.stroke });
336
+ next = next.editGraphics({
337
+ target: id,
338
+ property: "strokeWidth",
339
+ value: config.strokeWidth
340
+ });
341
+ }
342
+ return next;
343
+ }
344
+ );
345
+
346
+ const editPointMark = action(
347
+ {
348
+ op: "editPointMark",
349
+ description: "Edit constant point-mark appearance."
350
+ },
351
+ function (args = {}) {
352
+ validateMarkOptions(args, EDIT_POINT_OPTIONS, "editPointMark");
353
+ const editable = ["shape", "fill", "opacity", "stroke", "strokeWidth"];
354
+ if (!editable.some(property => Object.hasOwn(args, property))) {
355
+ throw new Error(
356
+ "editPointMark requires shape, fill, opacity, stroke, or strokeWidth."
357
+ );
358
+ }
359
+ const candidates = this.semanticSpec.layers.filter(
360
+ layer => layer.mark?.type === "point"
361
+ );
362
+ const current = findLayer(this, this.context.currentMark);
363
+ const requested = args.target ??
364
+ (current?.mark?.type === "point" ? current.id : undefined);
365
+ const inferred = requested ?? (candidates.length === 1
366
+ ? candidates[0].id
367
+ : undefined);
368
+ const id = validateUserId(inferred, "Point mark id");
369
+ const layer = findLayer(this, id);
370
+ if (layer?.mark?.type !== "point") {
371
+ throw new Error(`Unknown point mark "${id}".`);
372
+ }
373
+ if (Object.hasOwn(args, "shape") && layer.encoding?.shape !== undefined) {
374
+ throw new Error(
375
+ "editPointMark shape cannot be combined with a shape encoding."
376
+ );
377
+ }
378
+ if (Object.hasOwn(args, "fill") && layer.encoding?.color !== undefined) {
379
+ throw new Error(
380
+ "editPointMark fill cannot be combined with a color encoding."
381
+ );
382
+ }
383
+ const config = { ...this.markConfigs[id] };
384
+ if (Object.hasOwn(args, "shape")) {
385
+ config.shape = validatePointShape(args.shape);
386
+ }
387
+ if (Object.hasOwn(args, "fill")) {
388
+ config.fill = validateNonEmptyString(args.fill, "Point fill");
389
+ }
390
+ if (Object.hasOwn(args, "opacity")) {
391
+ config.opacity = validateUnitInterval(args.opacity, "Point opacity");
392
+ }
393
+ if (Object.hasOwn(args, "stroke")) {
394
+ config.stroke = validateNonEmptyString(args.stroke, "Point stroke");
395
+ config.strokeWidth ??= 1;
396
+ }
397
+ if (Object.hasOwn(args, "strokeWidth")) {
398
+ if (config.stroke === undefined) {
399
+ throw new Error("Point strokeWidth requires an active stroke.");
400
+ }
401
+ config.strokeWidth = validateNonNegativeFinite(
402
+ args.strokeWidth,
403
+ "Point strokeWidth"
404
+ );
405
+ }
406
+ const next = this
407
+ ._withMarkConfig(id, config)
408
+ .rematerializePointMark({ id });
409
+ const legend = next.guideConfigs.legend?.series;
410
+ return legend?.target === id && legend.channels.includes("shape")
411
+ ? rematerializeExistingLegend(next)
412
+ : next;
413
+ }
414
+ );
415
+
416
+ export function registerPointMarkActions(ProgramClass) {
417
+ ProgramClass.prototype.createPointMark = createPointMark;
418
+ ProgramClass.prototype.editPointMark = editPointMark;
419
+ ProgramClass.prototype.rematerializePointMark = rematerializePointMark;
420
+ }