ggaction 0.0.10 → 0.0.12

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 (224) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -3
  3. package/knowledge/action-card.schema.json +1 -0
  4. package/knowledge/action-cards.json +1 -10941
  5. package/knowledge/action-cards.schema.json +1 -0
  6. package/knowledge/intent-taxonomy.json +1 -183
  7. package/knowledge/intent-taxonomy.schema.json +1 -0
  8. package/knowledge/mcp-resources.json +1 -95
  9. package/knowledge/mcp-resources.schema.json +1 -0
  10. package/knowledge/task-packet.schema.json +1 -159
  11. package/knowledge/task-resolver.js +1 -1230
  12. package/package.json +18 -2
  13. package/src/actions/boxPlots/edit.js +45 -298
  14. package/src/actions/boxPlots/options.js +46 -77
  15. package/src/actions/boxPlots/resolve.js +1 -1
  16. package/src/actions/charts/bar.js +3 -0
  17. package/src/actions/charts/heatmap.js +73 -83
  18. package/src/actions/charts/index.js +1 -1
  19. package/src/actions/charts/parallel.js +35 -1
  20. package/src/actions/charts/shared.js +20 -0
  21. package/src/actions/composition/actions.js +46 -49
  22. package/src/actions/data/bin2d.js +105 -2
  23. package/src/actions/data/box.js +17 -17
  24. package/src/actions/data/density.js +42 -78
  25. package/src/actions/data/gradientProfile.js +22 -52
  26. package/src/actions/data/horizon.js +16 -41
  27. package/src/actions/data/index.js +10 -10
  28. package/src/actions/data/interval.js +16 -38
  29. package/src/actions/data/intervalEdit.js +55 -0
  30. package/src/actions/data/regression.js +7 -17
  31. package/src/actions/data/shared.js +47 -0
  32. package/src/actions/data/timeUnit.js +14 -38
  33. package/src/actions/data/window.js +14 -38
  34. package/src/actions/distributions/revision.js +154 -34
  35. package/src/actions/encodings/angle.js +9 -10
  36. package/src/actions/encodings/appearance.js +17 -51
  37. package/src/actions/encodings/density/resolve.js +3 -1
  38. package/src/actions/encodings/density.js +1 -15
  39. package/src/actions/encodings/horizon/resolve.js +13 -3
  40. package/src/actions/encodings/horizon.js +8 -7
  41. package/src/actions/encodings/index.js +9 -9
  42. package/src/actions/encodings/offset.js +41 -45
  43. package/src/actions/encodings/parallel.js +9 -10
  44. package/src/actions/encodings/pathOrder.js +6 -13
  45. package/src/actions/encodings/position/apply.js +30 -0
  46. package/src/actions/encodings/position/policies/arc.js +23 -2
  47. package/src/actions/encodings/position/policies/index.js +4 -4
  48. package/src/actions/encodings/position/policies/line.js +47 -17
  49. package/src/actions/encodings/position/resolve.js +36 -7
  50. package/src/actions/encodings/ranged.js +18 -23
  51. package/src/actions/encodings/remove.js +15 -25
  52. package/src/actions/encodings/ruleAppearance.js +9 -24
  53. package/src/actions/encodings/shared.js +37 -0
  54. package/src/actions/encodings/strokeDash.js +8 -23
  55. package/src/actions/encodings/text.js +9 -10
  56. package/src/actions/errorBands/create.js +105 -39
  57. package/src/actions/errorBands/edit.js +89 -93
  58. package/src/actions/errorBands/index.js +1 -2
  59. package/src/actions/errorBars/create.js +212 -30
  60. package/src/actions/errorBars/edit.js +91 -48
  61. package/src/actions/errorBars/index.js +1 -2
  62. package/src/actions/facets/actions.js +45 -69
  63. package/src/actions/facets/derive.js +38 -53
  64. package/src/actions/facets/replay.js +0 -1
  65. package/src/actions/gradientPlots/components.js +59 -48
  66. package/src/actions/gradientPlots/edit.js +35 -216
  67. package/src/actions/gradientPlots/index.js +11 -10
  68. package/src/actions/gradientPlots/materialize.js +21 -35
  69. package/src/actions/gradientPlots/options.js +5 -7
  70. package/src/actions/gradientPlots/paint.js +9 -6
  71. package/src/actions/gradientPlots/resolve.js +1 -1
  72. package/src/actions/guides/axes/index.js +3 -3
  73. package/src/actions/guides/axes/labels.js +68 -26
  74. package/src/actions/guides/axes/lines.js +22 -2
  75. package/src/actions/guides/axes/policy.js +28 -8
  76. package/src/actions/guides/axes/ticks.js +27 -9
  77. package/src/actions/guides/axes/titles.js +113 -65
  78. package/src/actions/guides/grids/resolve.js +14 -0
  79. package/src/actions/guides/guides.js +2 -4
  80. package/src/actions/guides/index.js +2 -2
  81. package/src/actions/guides/legends/categorical/actions.js +44 -68
  82. package/src/actions/guides/legends/categorical/components.js +6 -1
  83. package/src/actions/guides/legends/categorical/index.js +2 -2
  84. package/src/actions/guides/legends/categorical/layout.js +148 -175
  85. package/src/actions/guides/legends/categorical/recipes.js +5 -0
  86. package/src/actions/guides/legends/categorical/resolve.js +8 -1
  87. package/src/actions/guides/legends/continuous/common.js +117 -66
  88. package/src/actions/guides/legends/continuous/gradient.js +38 -37
  89. package/src/actions/guides/legends/continuous/interval.js +97 -76
  90. package/src/actions/guides/legends/continuous/opacity.js +28 -38
  91. package/src/actions/guides/legends/edit.js +14 -1
  92. package/src/actions/guides/legends/index.js +1 -1
  93. package/src/actions/guides/legends/lane.js +2 -1
  94. package/src/actions/guides/legends/remove.js +8 -0
  95. package/src/actions/guides/legends/size.js +36 -43
  96. package/src/actions/guides/legends/strokeWidth.js +20 -26
  97. package/src/actions/guides/polar/axes/labels.js +17 -1
  98. package/src/actions/guides/polar/axes/shared.js +7 -1
  99. package/src/actions/guides/polar/axes/titles.js +19 -2
  100. package/src/actions/guides/polar/grids.js +4 -0
  101. package/src/actions/guides/polar/resolve.js +6 -0
  102. package/src/actions/index.js +13 -13
  103. package/src/actions/intervals/resolve.js +87 -117
  104. package/src/actions/marks/arc/actions.js +31 -36
  105. package/src/actions/marks/area/actions.js +23 -31
  106. package/src/actions/marks/index.js +6 -6
  107. package/src/actions/marks/line/actions.js +33 -52
  108. package/src/actions/marks/line/materialize.js +7 -5
  109. package/src/actions/marks/point/materialize.js +38 -16
  110. package/src/actions/marks/remove.js +21 -0
  111. package/src/actions/marks/rule/actions.js +79 -54
  112. package/src/actions/marks/shared.js +8 -4
  113. package/src/actions/marks/text/actions.js +14 -6
  114. package/src/actions/marks/text/layout.js +21 -45
  115. package/src/actions/marks/tick/actions.js +22 -21
  116. package/src/actions/primitives/createGraphics.js +4 -0
  117. package/src/actions/primitives/editGraphics.js +4 -0
  118. package/src/actions/primitives/index.js +3 -3
  119. package/src/actions/primitives/semantic.js +1 -1
  120. package/src/actions/primitives/semanticValidation/index.js +2 -2
  121. package/src/actions/primitives/semanticValidation/layer.js +1 -1
  122. package/src/actions/regression/edit.js +3 -43
  123. package/src/actions/scales/consumers/families.js +24 -4
  124. package/src/actions/scales/consumers/index.js +1 -1
  125. package/src/actions/scales/consumers/seriesLayout.js +17 -0
  126. package/src/actions/scales/definitions.js +100 -103
  127. package/src/actions/scales/materialize.js +5 -0
  128. package/src/actions/selection/actions.js +79 -121
  129. package/src/actions/titles/actions.js +48 -94
  130. package/src/actions/titles/resolve.js +14 -27
  131. package/src/actions/violinPlots/create.js +37 -44
  132. package/src/core/textMetrics.js +74 -37
  133. package/src/core/validation.js +18 -0
  134. package/src/core/vocabulary.js +2 -2
  135. package/src/grammar/aggregate.js +48 -28
  136. package/src/grammar/arcs.js +170 -32
  137. package/src/grammar/areaSeries.js +14 -16
  138. package/src/grammar/bin2d.js +37 -23
  139. package/src/grammar/boxPlot.js +14 -13
  140. package/src/grammar/categoricalDensity.js +14 -14
  141. package/src/grammar/categoryOrder.js +8 -14
  142. package/src/grammar/curveCommands.js +87 -20
  143. package/src/grammar/density.js +162 -141
  144. package/src/grammar/facets/index.js +14 -0
  145. package/src/grammar/facets/scales.js +7 -4
  146. package/src/grammar/gradientProfile.js +12 -15
  147. package/src/grammar/histogram.js +145 -83
  148. package/src/grammar/horizon.js +60 -23
  149. package/src/grammar/interval.js +14 -13
  150. package/src/grammar/lineSeries.js +91 -61
  151. package/src/grammar/numeric.js +290 -0
  152. package/src/grammar/paint.js +3 -4
  153. package/src/grammar/palettes.js +21 -32
  154. package/src/grammar/pathCommands.js +6 -0
  155. package/src/grammar/polarPaths.js +5 -5
  156. package/src/grammar/positionCompatibility.js +1 -1
  157. package/src/grammar/regression/derive.js +48 -9
  158. package/src/grammar/regression/models.js +248 -69
  159. package/src/grammar/regression/parameters.js +8 -0
  160. package/src/grammar/rules.js +2 -0
  161. package/src/grammar/scales/color.js +35 -26
  162. package/src/grammar/scales/continuous.js +71 -24
  163. package/src/grammar/scales/definition.js +1 -1
  164. package/src/grammar/scales/discretized.js +42 -12
  165. package/src/grammar/scales/fields.js +21 -4
  166. package/src/grammar/scales/ordinal.js +139 -60
  167. package/src/grammar/scales/temporal.js +40 -11
  168. package/src/grammar/scales/transformed.js +199 -52
  169. package/src/grammar/schemas/graphicBounds.js +270 -79
  170. package/src/grammar/seriesLayout.js +50 -18
  171. package/src/grammar/ticks.js +123 -53
  172. package/src/grammar/timeUnit.js +18 -8
  173. package/src/grammar/transforms.js +4 -1
  174. package/src/grammar/window.js +70 -57
  175. package/src/index.js +1 -1
  176. package/src/layout/canvas.js +4 -8
  177. package/src/layout/composition.js +39 -40
  178. package/src/layout/facets.js +55 -30
  179. package/src/layout/labels.js +91 -15
  180. package/src/layout/legend.js +22 -20
  181. package/src/layout/legendLane.js +54 -40
  182. package/src/layout/title.js +7 -7
  183. package/src/materialization/bars/aggregate.js +14 -40
  184. package/src/materialization/bars/grouped.js +7 -18
  185. package/src/materialization/bars/histogram.js +2 -19
  186. package/src/materialization/bars/ranged.js +20 -15
  187. package/src/materialization/bars/resolve.js +21 -0
  188. package/src/materialization/facetGuides/legacyCategorical.js +9 -2
  189. package/src/materialization/facetGuides/placement.js +49 -19
  190. package/src/materialization/facetGuides/preparation.js +35 -6
  191. package/src/materialization/facets.js +42 -28
  192. package/src/materialization/guides/resources.js +2 -1
  193. package/src/materialization/marks/capabilities.js +15 -4
  194. package/src/materialization/marks/index.js +29 -6
  195. package/src/materialization/rowEncoding.js +57 -1
  196. package/src/materialization/scales/policies/bar.js +61 -102
  197. package/src/materialization/scales/policies/binnedPosition.js +34 -0
  198. package/src/materialization/scales/policies/offset.js +54 -0
  199. package/src/materialization/scales/policies/series.js +13 -7
  200. package/src/materialization/scales/resolve.js +6 -9
  201. package/src/materialization/selection/items/arc.js +16 -2
  202. package/src/materialization/selection/items/bar.js +4 -2
  203. package/src/materialization/selection/items/common.js +21 -1
  204. package/src/materialization/selection/items/path.js +19 -2
  205. package/src/materialization/selection/items/point.js +4 -16
  206. package/src/materialization/selection/items/tick.js +13 -21
  207. package/src/materialization/selection/point.js +11 -4
  208. package/src/materialization/selection/styles.js +19 -27
  209. package/src/materialization/text.js +4 -1
  210. package/src/mcp/adapter.js +8 -3
  211. package/src/renderers/canvas/fill.js +2 -2
  212. package/src/renderers/canvas/index.js +35 -2
  213. package/src/renderers/canvas/native.js +160 -0
  214. package/src/renderers/pdf.js +14 -0
  215. package/src/renderers/svg.js +37 -4
  216. package/types/index.d.ts +3 -0
  217. package/types/program.d.ts +523 -131
  218. package/types/svg.d.ts +1 -0
  219. package/src/actions/errorBands/components.js +0 -81
  220. package/src/actions/errorBands/options.js +0 -39
  221. package/src/actions/errorBands/resolve.js +0 -28
  222. package/src/actions/errorBars/components.js +0 -62
  223. package/src/actions/errorBars/options.js +0 -58
  224. package/src/actions/errorBars/resolve.js +0 -25
@@ -21,9 +21,9 @@ function assertTitleScope(program, operation) {
21
21
  }
22
22
 
23
23
  function hasRotation(graphic) {
24
- const properties = graphic.items?.map(child => child.properties) ??
25
- [graphic.properties];
26
- return properties.some(item => Object.hasOwn(item, "rotation"));
24
+ return (graphic.items ?? [graphic]).some(
25
+ item => Object.hasOwn(item.properties, "rotation")
26
+ );
27
27
  }
28
28
 
29
29
  function ensureTextShape(program, id, component) {
@@ -45,14 +45,7 @@ function ensureTextShape(program, id, component) {
45
45
  ...placement
46
46
  });
47
47
  }
48
- if (!collection && needsCollection) {
49
- return program.editGraphics({
50
- target: id,
51
- property: "length",
52
- value: component.lines.length
53
- });
54
- }
55
- if (collection && graphic.items.length !== component.lines.length) {
48
+ if (needsCollection && graphic.items?.length !== component.lines.length) {
56
49
  return program.editGraphics({
57
50
  target: id,
58
51
  property: "length",
@@ -68,20 +61,20 @@ function distributed(value, count) {
68
61
 
69
62
  function editTextGraphic(program, id, component, style) {
70
63
  const count = component.lines.length;
71
- let next = ensureTextShape(program, id, component)
72
- .editGraphics({ target: id, property: "x", value: distributed(component.x, count) })
73
- .editGraphics({ target: id, property: "y", value: distributed(component.y, count) })
74
- .editGraphics({
75
- target: id,
76
- property: "text",
77
- value: distributed(component.lines, count)
78
- })
79
- .editGraphics({ target: id, property: "fill", value: style.color })
80
- .editGraphics({ target: id, property: "fontSize", value: style.fontSize })
81
- .editGraphics({ target: id, property: "fontFamily", value: style.fontFamily })
82
- .editGraphics({ target: id, property: "fontWeight", value: style.fontWeight })
83
- .editGraphics({ target: id, property: "textAlign", value: component.textAlign })
84
- .editGraphics({ target: id, property: "textBaseline", value: "middle" });
64
+ let next = ensureTextShape(program, id, component);
65
+ for (const [property, value] of Object.entries({
66
+ x: distributed(component.x, count),
67
+ y: distributed(component.y, count),
68
+ text: distributed(component.lines, count),
69
+ fill: style.color,
70
+ fontSize: style.fontSize,
71
+ fontFamily: style.fontFamily,
72
+ fontWeight: style.fontWeight,
73
+ textAlign: component.textAlign,
74
+ textBaseline: "middle"
75
+ })) {
76
+ next = next.editGraphics({ target: id, property, value });
77
+ }
85
78
  if (component.explicitRotation) {
86
79
  next = next.editGraphics({
87
80
  target: id,
@@ -92,78 +85,39 @@ function editTextGraphic(program, id, component, style) {
92
85
  return next;
93
86
  }
94
87
 
95
- function createTextGraphic(program, id, component) {
96
- return program.createGraphics({
97
- id,
98
- type: "text",
99
- ...(component.lines.length > 1 ? { length: component.lines.length } : {}),
100
- ...resolveCanvasGraphicPlacement(program)
88
+ function titleTextAction(kind, create) {
89
+ const suffix = `${kind[0].toUpperCase()}${kind.slice(1)}Text`;
90
+ const op = `${create ? "create" : "edit"}${suffix}`;
91
+ const id = `chart${kind[0].toUpperCase()}${kind.slice(1)}`;
92
+ return action({
93
+ op,
94
+ description: `${create ? "Create" : "Rematerialize"} chart ${kind} text.`
95
+ }, function (args = {}) {
96
+ noOptions(args, op);
97
+ const config = requireTitleConfig(this);
98
+ if (create && this.graphicSpec.objects[id] !== undefined) {
99
+ throw new Error(`${op} requires a missing chart ${kind} graphic.`);
100
+ }
101
+ const component = resolveTitleLayout(this, config)[kind];
102
+ if (component === undefined) {
103
+ throw new Error(`${op} requires semantic subtitle text.`);
104
+ }
105
+ if (!create) {
106
+ return editTextGraphic(this, id, component, config[`${kind}Style`]);
107
+ }
108
+ return this.createGraphics({
109
+ id,
110
+ type: "text",
111
+ ...(component.lines.length > 1 ? { length: component.lines.length } : {}),
112
+ ...resolveCanvasGraphicPlacement(this)
113
+ })[`edit${suffix}`]();
101
114
  });
102
115
  }
103
116
 
104
- export const editTitleText = action(
105
- { op: "editTitleText", description: "Rematerialize chart title text." },
106
- function (args = {}) {
107
- noOptions(args, "editTitleText");
108
- const config = requireTitleConfig(this);
109
- const layout = resolveTitleLayout(this, config);
110
- return editTextGraphic(
111
- this,
112
- "chartTitle",
113
- layout.title,
114
- config.titleStyle
115
- );
116
- }
117
- );
118
-
119
- export const createTitleText = action(
120
- { op: "createTitleText", description: "Create chart title text." },
121
- function (args = {}) {
122
- noOptions(args, "createTitleText");
123
- const config = requireTitleConfig(this);
124
- if (this.graphicSpec.objects.chartTitle !== undefined) {
125
- throw new Error("createTitleText requires a missing chart title graphic.");
126
- }
127
- const layout = resolveTitleLayout(this, config);
128
- return createTextGraphic(this, "chartTitle", layout.title)
129
- .editTitleText();
130
- }
131
- );
132
-
133
- export const editSubtitleText = action(
134
- { op: "editSubtitleText", description: "Rematerialize chart subtitle text." },
135
- function (args = {}) {
136
- noOptions(args, "editSubtitleText");
137
- const config = requireTitleConfig(this);
138
- const layout = resolveTitleLayout(this, config);
139
- if (layout.subtitle === undefined) {
140
- throw new Error("editSubtitleText requires semantic subtitle text.");
141
- }
142
- return editTextGraphic(
143
- this,
144
- "chartSubtitle",
145
- layout.subtitle,
146
- config.subtitleStyle
147
- );
148
- }
149
- );
150
-
151
- export const createSubtitleText = action(
152
- { op: "createSubtitleText", description: "Create chart subtitle text." },
153
- function (args = {}) {
154
- noOptions(args, "createSubtitleText");
155
- const config = requireTitleConfig(this);
156
- if (this.graphicSpec.objects.chartSubtitle !== undefined) {
157
- throw new Error("createSubtitleText requires a missing chart subtitle graphic.");
158
- }
159
- const layout = resolveTitleLayout(this, config);
160
- if (layout.subtitle === undefined) {
161
- throw new Error("createSubtitleText requires semantic subtitle text.");
162
- }
163
- return createTextGraphic(this, "chartSubtitle", layout.subtitle)
164
- .editSubtitleText();
165
- }
166
- );
117
+ export const editTitleText = titleTextAction("title", false);
118
+ export const createTitleText = titleTextAction("title", true);
119
+ export const editSubtitleText = titleTextAction("subtitle", false);
120
+ export const createSubtitleText = titleTextAction("subtitle", true);
167
121
 
168
122
  export const rematerializeTitle = action(
169
123
  { op: "rematerializeTitle", description: "Rematerialize chart title graphics." },
@@ -252,29 +252,20 @@ export function resolveTitleLayout(program, config) {
252
252
  )
253
253
  }, config);
254
254
  const horizontal = ["top", "bottom"].includes(config.position);
255
- let title;
256
- let subtitle;
255
+ let component;
257
256
  if (horizontal) {
258
257
  const x = alignedTextAnchor(plot.x, plot.width, config.align);
259
258
  const blockTop = config.position === "top"
260
259
  ? 16 + config.offset
261
260
  : plot.y + plot.height + config.offset;
262
- title = {
263
- lines: block.titleLines,
261
+ component = (lines, centers) => ({
262
+ lines,
264
263
  x,
265
- y: block.titleCenters.map(value => blockTop + value),
264
+ y: centers.map(value => blockTop + value),
266
265
  textAlign: config.align,
267
266
  rotation: 0,
268
- explicitRotation: config.position !== "top" || block.titleLines.length > 1
269
- };
270
- subtitle = block.subtitleLines.length === 0 ? undefined : {
271
- lines: block.subtitleLines,
272
- x,
273
- y: block.subtitleCenters.map(value => blockTop + value),
274
- textAlign: config.align,
275
- rotation: 0,
276
- explicitRotation: config.position !== "top" || block.subtitleLines.length > 1
277
- };
267
+ explicitRotation: config.position !== "top" || lines.length > 1
268
+ });
278
269
  } else {
279
270
  const rotation = config.position === "left" ? -Math.PI / 2 : Math.PI / 2;
280
271
  const y = alignedTitleAnchor(plot.y, plot.height, block.width, config.align);
@@ -282,23 +273,19 @@ export function resolveTitleLayout(program, config) {
282
273
  ? 16 + config.offset
283
274
  : canvas.properties.width - 16 + config.offset;
284
275
  const mapX = value => config.position === "left" ? edge + value : edge - value;
285
- title = {
286
- lines: block.titleLines,
287
- x: block.titleCenters.map(mapX),
288
- y,
289
- textAlign: "center",
290
- rotation,
291
- explicitRotation: true
292
- };
293
- subtitle = block.subtitleLines.length === 0 ? undefined : {
294
- lines: block.subtitleLines,
295
- x: block.subtitleCenters.map(mapX),
276
+ component = (lines, centers) => ({
277
+ lines,
278
+ x: centers.map(mapX),
296
279
  y,
297
280
  textAlign: "center",
298
281
  rotation,
299
282
  explicitRotation: true
300
- };
283
+ });
301
284
  }
285
+ const title = component(block.titleLines, block.titleCenters);
286
+ const subtitle = block.subtitleLines.length === 0
287
+ ? undefined
288
+ : component(block.subtitleLines, block.subtitleCenters);
302
289
  const titleBounds = unionTitleBounds([
303
290
  resolveTitleComponentBounds(title, config.titleStyle),
304
291
  ...(subtitle === undefined
@@ -27,6 +27,8 @@ const AREA_OPTIONS = Object.freeze([
27
27
  "fill", "opacity", "stroke", "strokeWidth", "curve"
28
28
  ]);
29
29
  const CATEGORICAL_TYPES = Object.freeze(["nominal", "ordinal"]);
30
+ const OPERATION = "createViolinPlot";
31
+ const ROLE_ERROR = `${OPERATION} requires one categorical axis and one quantitative axis.`;
30
32
 
31
33
  function inferFieldType(dataset, encoding, label) {
32
34
  if (encoding.fieldType !== undefined) return encoding.fieldType;
@@ -51,29 +53,6 @@ function normalizePosition(dataset, value, label) {
51
53
  };
52
54
  }
53
55
 
54
- function normalizeDensity(value) {
55
- if (value === undefined) return { options: {}, width: undefined };
56
- validateOptionObject(value, DENSITY_OPTIONS, "createViolinPlot density");
57
- if (value.width !== undefined) {
58
- validateOptionObject(
59
- value.width,
60
- WIDTH_OPTIONS,
61
- "createViolinPlot density.width"
62
- );
63
- }
64
- const { width, ...options } = value;
65
- return { options, width };
66
- }
67
-
68
- function normalizeSplit(value) {
69
- if (value === undefined) return undefined;
70
- validateOptionObject(value, SPLIT_OPTIONS, "createViolinPlot split");
71
- if (typeof value.field !== "string" || value.field.length === 0) {
72
- throw new TypeError("createViolinPlot split.field must be a non-empty string.");
73
- }
74
- return { ...value };
75
- }
76
-
77
56
  function axisGuideOptions(value, field) {
78
57
  if (value === false) return false;
79
58
  return {
@@ -109,55 +88,69 @@ export const createViolinPlot = action(
109
88
  description: "Create a categorical kernel-density violin plot."
110
89
  },
111
90
  function (args = {}) {
112
- validateFacadeOptions(args, OPTIONS, "createViolinPlot");
91
+ validateFacadeOptions(args, OPTIONS, OPERATION);
113
92
  const id = resolveFacadeId(this, args.id, {
114
93
  defaultId: "violinPlot",
115
- operation: "createViolinPlot"
94
+ operation: OPERATION
116
95
  });
117
- const data = resolveFacadeData(this, args.data, "createViolinPlot");
96
+ const data = resolveFacadeData(this, args.data, OPERATION);
118
97
  const dataset = findDataset(this, data);
119
- const x = normalizePosition(dataset, args.x, "createViolinPlot x");
120
- const y = normalizePosition(dataset, args.y, "createViolinPlot y");
98
+ const x = normalizePosition(dataset, args.x, `${OPERATION} x`);
99
+ const y = normalizePosition(dataset, args.y, `${OPERATION} y`);
121
100
  const xCategorical = CATEGORICAL_TYPES.includes(x.fieldType);
122
101
  const yCategorical = CATEGORICAL_TYPES.includes(y.fieldType);
123
102
  if (xCategorical === yCategorical || (
124
103
  x.fieldType !== "quantitative" && y.fieldType !== "quantitative"
125
104
  )) {
126
- throw new Error(
127
- "createViolinPlot requires one categorical axis and one quantitative axis."
128
- );
105
+ throw new Error(ROLE_ERROR);
129
106
  }
130
107
  const category = xCategorical ? x : y;
131
108
  const value = xCategorical ? y : x;
132
109
  if (value.fieldType !== "quantitative") {
133
- throw new Error(
134
- "createViolinPlot requires one categorical axis and one quantitative axis."
135
- );
110
+ throw new Error(ROLE_ERROR);
111
+ }
112
+ let split = args.split;
113
+ if (split !== undefined) {
114
+ validateOptionObject(split, SPLIT_OPTIONS, `${OPERATION} split`);
115
+ if (typeof split.field !== "string" || split.field.length === 0) {
116
+ throw new TypeError(`${OPERATION} split.field must be a non-empty string.`);
117
+ }
118
+ split = { ...split };
136
119
  }
137
- const split = normalizeSplit(args.split);
138
120
  if (split?.field === category.field) {
139
- throw new Error("createViolinPlot split field must differ from its category field.");
121
+ throw new Error(`${OPERATION} split field must differ from its category field.`);
140
122
  }
141
- const color = normalizeEncoding(args.color, "createViolinPlot color");
123
+ const color = normalizeEncoding(args.color, `${OPERATION} color`);
142
124
  if (
143
125
  color !== undefined &&
144
126
  ![category.field, split?.field].includes(color.field)
145
127
  ) {
146
128
  throw new Error(
147
- "createViolinPlot color must encode its category or split field."
129
+ `${OPERATION} color must encode its category or split field.`
148
130
  );
149
131
  }
150
- const density = normalizeDensity(args.density);
132
+ const density = args.density === undefined ? {} : args.density;
133
+ if (args.density !== undefined) {
134
+ validateOptionObject(density, DENSITY_OPTIONS, `${OPERATION} density`);
135
+ if (density.width !== undefined) {
136
+ validateOptionObject(
137
+ density.width,
138
+ WIDTH_OPTIONS,
139
+ `${OPERATION} density.width`
140
+ );
141
+ }
142
+ }
143
+ const { width: densityWidth, ...densityOptions } = density;
151
144
  const area = normalizeAppearance(
152
145
  args.area,
153
146
  AREA_OPTIONS,
154
- "createViolinPlot area"
147
+ `${OPERATION} area`
155
148
  );
156
149
  if (color !== undefined && Object.hasOwn(area, "fill")) {
157
- throw new Error("createViolinPlot area.fill cannot be combined with color.");
150
+ throw new Error(`${OPERATION} area.fill cannot be combined with color.`);
158
151
  }
159
152
  const guides = guideOptions(
160
- normalizeGuides(args.guides, "createViolinPlot"),
153
+ normalizeGuides(args.guides, OPERATION),
161
154
  x,
162
155
  y,
163
156
  category,
@@ -184,10 +177,10 @@ export const createViolinPlot = action(
184
177
  densityChannel: xCategorical ? "x" : "y",
185
178
  ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
186
179
  ...(value.scale === undefined ? {} : { valueScale: value.scale }),
187
- ...density.options,
180
+ ...densityOptions,
188
181
  placement: {
189
182
  type: "category",
190
- ...(density.width === undefined ? {} : { width: density.width }),
183
+ ...(densityWidth === undefined ? {} : { width: densityWidth }),
191
184
  ...(split === undefined ? {} : { split }),
192
185
  ...(category.scale === undefined ? {} : { scale: category.scale })
193
186
  }
@@ -1,38 +1,69 @@
1
1
  function codePointWidth(codePoint) {
2
2
  if (/\s/u.test(codePoint)) return 0.28;
3
- if (/[iIl1.,:;!'|]/u.test(codePoint)) return 0.27;
3
+ if (/[iIl.,:;!'|]/u.test(codePoint)) return 0.27;
4
4
  if (/[mwMW@#%&]/u.test(codePoint)) return 0.82;
5
- if (/[A-Z]/u.test(codePoint)) return 0.61;
6
- if (/[0-9]/u.test(codePoint)) return 0.53;
5
+ if (/[A-Z0-9]/u.test(codePoint)) return 0.61;
7
6
  if (/[-_]/u.test(codePoint)) return 0.34;
8
7
  if (codePoint.codePointAt(0) > 0x7f) return 1;
9
8
  return 0.47;
10
9
  }
11
10
 
12
- export function measureTextWidth(text, { fontSize } = {}) {
11
+ export function formatVisibleText(value) {
12
+ return String(value) || "(empty)";
13
+ }
14
+
15
+ export function textBoundsIntersect(first, second) {
16
+ return first.left < second.right && first.right > second.left &&
17
+ first.top < second.bottom && first.bottom > second.top;
18
+ }
19
+
20
+ export function textBoundsFitCanvas(bounds, canvas) {
21
+ const epsilon = 1e-9;
22
+ return bounds.left >= -epsilon && bounds.right <= canvas.width + epsilon &&
23
+ bounds.top >= -epsilon && bounds.bottom <= canvas.height + epsilon;
24
+ }
25
+
26
+ export function measureTextWidth(
27
+ text,
28
+ { fontSize, fontFamily, fontWeight } = {}
29
+ ) {
13
30
  if (typeof text !== "string") {
14
31
  throw new TypeError("Text measurement requires a string.");
15
32
  }
16
33
  if (!Number.isFinite(fontSize) || fontSize <= 0) {
17
34
  throw new RangeError("Text measurement requires a positive fontSize.");
18
35
  }
19
- return [...text].reduce(
20
- (width, codePoint) => width + codePointWidth(codePoint) * fontSize,
21
- 0
22
- );
23
- }
24
-
25
- function horizontalExtents(width, align) {
26
- if (align === "center") return [-width / 2, width / 2];
27
- if (["right", "end"].includes(align)) return [-width, 0];
28
- return [0, width];
36
+ let joined = false;
37
+ let width = 0;
38
+ for (const codePoint of text) {
39
+ if (codePoint === "\u200d") {
40
+ joined = true;
41
+ } else if (!/\p{Mark}|\p{Format}|\p{Emoji_Modifier}/u.test(codePoint)) {
42
+ if (joined) joined = false;
43
+ else width += codePointWidth(codePoint) * fontSize;
44
+ }
45
+ }
46
+ const family = typeof fontFamily === "string" ? fontFamily.toLowerCase() : "";
47
+ const familyFactor = /mono/u.test(family)
48
+ ? 1.08
49
+ : /serif/u.test(family) && !/sans-serif/u.test(family) ? 1.03 : 1;
50
+ const weight = Number(fontWeight);
51
+ width *= familyFactor * ((
52
+ Number.isFinite(weight)
53
+ ? weight >= 600
54
+ : /bold|black|heavy/iu.test(fontWeight ?? "")
55
+ ) ? 1.04 : 1);
56
+ if (!Number.isFinite(width)) {
57
+ throw new RangeError("Text measurement exceeds the finite numeric range.");
58
+ }
59
+ return width;
29
60
  }
30
61
 
31
- function verticalExtents(fontSize, baseline) {
32
- if (baseline === "middle") return [-fontSize / 2, fontSize / 2];
33
- if (["top", "hanging"].includes(baseline)) return [0, fontSize];
34
- if (["bottom", "ideographic"].includes(baseline)) return [-fontSize, 0];
35
- return [-fontSize * 0.8, fontSize * 0.2];
62
+ export function requireFiniteBounds(bounds, label = "Graphic") {
63
+ if (bounds !== undefined && !Object.values(bounds).every(Number.isFinite)) {
64
+ throw new RangeError(`${label} bounds exceed the finite numeric range.`);
65
+ }
66
+ return bounds;
36
67
  }
37
68
 
38
69
  export function resolveTextBounds({
@@ -40,6 +71,8 @@ export function resolveTextBounds({
40
71
  y,
41
72
  text,
42
73
  fontSize,
74
+ fontFamily,
75
+ fontWeight,
43
76
  textAlign = "left",
44
77
  textBaseline = "alphabetic",
45
78
  rotation = 0
@@ -47,24 +80,28 @@ export function resolveTextBounds({
47
80
  if (![x, y, rotation].every(Number.isFinite)) {
48
81
  throw new TypeError("Text bounds require finite x, y, and rotation values.");
49
82
  }
50
- const width = measureTextWidth(text, { fontSize });
51
- const [left, right] = horizontalExtents(width, textAlign);
52
- const [top, bottom] = verticalExtents(fontSize, textBaseline);
83
+ const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight });
84
+ const [left, right] = textAlign === "center"
85
+ ? [-width / 2, width / 2]
86
+ : ["right", "end"].includes(textAlign) ? [-width, 0] : [0, width];
87
+ const [top, bottom] = textBaseline === "middle"
88
+ ? [-fontSize / 2, fontSize / 2]
89
+ : ["top", "hanging"].includes(textBaseline)
90
+ ? [0, fontSize]
91
+ : ["bottom", "ideographic"].includes(textBaseline)
92
+ ? [-fontSize, 0]
93
+ : [-fontSize * 0.8, fontSize * 0.2];
53
94
  const cosine = Math.cos(rotation);
54
95
  const sine = Math.sin(rotation);
55
- const corners = [
56
- [left, top],
57
- [right, top],
58
- [right, bottom],
59
- [left, bottom]
60
- ].map(([localX, localY]) => ({
61
- x: x + localX * cosine - localY * sine,
62
- y: y + localX * sine + localY * cosine
63
- }));
64
- return Object.freeze({
65
- left: Math.min(...corners.map(point => point.x)),
66
- right: Math.max(...corners.map(point => point.x)),
67
- top: Math.min(...corners.map(point => point.y)),
68
- bottom: Math.max(...corners.map(point => point.y))
69
- });
96
+ const bounds = {
97
+ left: x + Math.min(left * cosine, right * cosine) -
98
+ Math.max(top * sine, bottom * sine),
99
+ right: x + Math.max(left * cosine, right * cosine) -
100
+ Math.min(top * sine, bottom * sine),
101
+ top: y + Math.min(left * sine, right * sine) +
102
+ Math.min(top * cosine, bottom * cosine),
103
+ bottom: y + Math.max(left * sine, right * sine) +
104
+ Math.max(top * cosine, bottom * cosine)
105
+ };
106
+ return Object.freeze(requireFiniteBounds(bounds, "Text"));
70
107
  }
@@ -1,5 +1,23 @@
1
1
  import { isPlainObject } from "./immutable.js";
2
2
 
3
+ export const MAX_GENERATED_ITEMS = 10_000;
4
+ export const MAX_WORK_ITEMS = 10_000_000;
5
+
6
+ export function validateGeneratedItemLimit(
7
+ value,
8
+ label,
9
+ maximum = MAX_GENERATED_ITEMS
10
+ ) {
11
+ if (value > maximum) {
12
+ throw new RangeError(`${label} must not exceed ${maximum}.`);
13
+ }
14
+ return value;
15
+ }
16
+
17
+ export function validateWorkLimit(value, label) {
18
+ return validateGeneratedItemLimit(value, label, MAX_WORK_ITEMS);
19
+ }
20
+
3
21
  export function validateKeys(value, supported, label) {
4
22
  for (const key of Object.keys(value)) {
5
23
  if (!supported.includes(key)) {
@@ -86,14 +86,14 @@ export const POSITION_CHANNEL_DEFINITIONS = Object.freeze({
86
86
  role: "offset",
87
87
  scaleChannel: "xOffset",
88
88
  coordinate: Object.freeze({ id: "main", type: "cartesian" }),
89
- markTypes: Object.freeze(["bar"])
89
+ markTypes: Object.freeze(["bar", "point", "rule"])
90
90
  }),
91
91
  yOffset: Object.freeze({
92
92
  family: "cartesian",
93
93
  role: "offset",
94
94
  scaleChannel: "yOffset",
95
95
  coordinate: Object.freeze({ id: "main", type: "cartesian" }),
96
- markTypes: Object.freeze(["bar"])
96
+ markTypes: Object.freeze(["bar", "point", "rule"])
97
97
  }),
98
98
  theta: Object.freeze({
99
99
  family: "polar",