ggaction 0.0.17 → 0.0.19

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 (315) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +3 -3
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +20 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +8 -7
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/box.js +2 -1
  34. package/src/actions/data/derived.js +11 -1
  35. package/src/actions/data/edit.js +3 -1
  36. package/src/actions/data/index.js +4 -0
  37. package/src/actions/data/regression.js +25 -16
  38. package/src/actions/data/regressionStatistics.js +73 -0
  39. package/src/actions/data/shared.js +8 -4
  40. package/src/actions/data/stack.js +3 -1
  41. package/src/actions/distributions/revision.js +24 -18
  42. package/src/actions/encodings/appearance.js +37 -29
  43. package/src/actions/encodings/barWidth.js +36 -11
  44. package/src/actions/encodings/channels.js +0 -1
  45. package/src/actions/encodings/color/continuous.js +1 -1
  46. package/src/actions/encodings/color/index.js +1 -1
  47. package/src/actions/encodings/color/policy.js +10 -3
  48. package/src/actions/encodings/density/resolve.js +1 -6
  49. package/src/actions/encodings/position/policies/arc.js +12 -10
  50. package/src/actions/encodings/position/policies/area.js +3 -3
  51. package/src/actions/encodings/position/policies/bar.js +15 -6
  52. package/src/actions/encodings/position/policies/line.js +17 -6
  53. package/src/actions/encodings/position/resolve.js +3 -2
  54. package/src/actions/encodings/ranged.js +17 -0
  55. package/src/actions/encodings/remove.js +14 -8
  56. package/src/actions/encodings/seriesLayout.js +1 -0
  57. package/src/actions/encodings/shared.js +26 -24
  58. package/src/actions/encodings/text.js +6 -3
  59. package/src/actions/errorBands/create.js +2 -2
  60. package/src/actions/errorBands/edit.js +2 -1
  61. package/src/actions/facets/actions.js +176 -247
  62. package/src/actions/facets/guides.js +2 -1
  63. package/src/actions/gradientPlots/resolve.js +2 -17
  64. package/src/actions/guides/applicability.js +1 -1
  65. package/src/actions/guides/axes/axes.js +30 -11
  66. package/src/actions/guides/axes/axis.js +72 -10
  67. package/src/actions/guides/axes/components.js +13 -13
  68. package/src/actions/guides/axes/edit.js +36 -24
  69. package/src/actions/guides/axes/labels.js +155 -71
  70. package/src/actions/guides/axes/lines.js +27 -24
  71. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  72. package/src/actions/guides/axes/parallel/policy.js +8 -7
  73. package/src/actions/guides/axes/remove.js +13 -8
  74. package/src/actions/guides/axes/tickGroups.js +11 -7
  75. package/src/actions/guides/axes/ticks.js +36 -24
  76. package/src/actions/guides/axes/titles.js +107 -69
  77. package/src/actions/guides/basic.js +2 -0
  78. package/src/actions/guides/facadeAxes.js +33 -24
  79. package/src/actions/guides/facadeLegend.js +12 -4
  80. package/src/actions/guides/grids/grid.js +8 -2
  81. package/src/actions/guides/grids/resolve.js +102 -58
  82. package/src/actions/guides/legends/blocks.js +46 -20
  83. package/src/actions/guides/legends/categorical/actions.js +95 -19
  84. package/src/actions/guides/legends/categorical/components.js +15 -18
  85. package/src/actions/guides/legends/categorical/index.js +8 -0
  86. package/src/actions/guides/legends/categorical/layout.js +45 -20
  87. package/src/actions/guides/legends/categorical/options.js +39 -22
  88. package/src/actions/guides/legends/categorical/recipes.js +7 -3
  89. package/src/actions/guides/legends/categorical/resolve.js +11 -4
  90. package/src/actions/guides/legends/categorical/symbols.js +38 -18
  91. package/src/actions/guides/legends/categorical/validation.js +1 -6
  92. package/src/actions/guides/legends/colorSize.js +110 -0
  93. package/src/actions/guides/legends/combine.js +37 -0
  94. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  95. package/src/actions/guides/legends/continuous/common.js +1 -447
  96. package/src/actions/guides/legends/continuous/gradient.js +73 -46
  97. package/src/actions/guides/legends/continuous/interval.js +4 -4
  98. package/src/actions/guides/legends/continuous/opacity.js +33 -21
  99. package/src/actions/guides/legends/creation.js +2 -0
  100. package/src/actions/guides/legends/edit.js +1 -604
  101. package/src/actions/guides/legends/focused.js +8 -7
  102. package/src/actions/guides/legends/group.js +1 -0
  103. package/src/actions/guides/legends/lane.js +1 -516
  104. package/src/actions/guides/legends/lifecycle.js +2 -1
  105. package/src/actions/guides/legends/remove.js +9 -3
  106. package/src/actions/guides/legends/sampling.js +1 -1
  107. package/src/actions/guides/legends/size.js +65 -47
  108. package/src/actions/guides/legends/strokeWidth.js +13 -29
  109. package/src/actions/guides/legends/target.js +2 -79
  110. package/src/actions/guides/legends/transition.js +44 -11
  111. package/src/actions/guides/polar/axes/labels.js +4 -2
  112. package/src/actions/guides/polar/axes/titles.js +7 -5
  113. package/src/actions/guides/polar/resolve.js +6 -14
  114. package/src/actions/guides/tickValues.js +8 -5
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +10 -4
  120. package/src/actions/marks/bar/edit.js +29 -14
  121. package/src/actions/marks/bar/materialize.js +15 -7
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/edit.js +1 -1
  125. package/src/actions/marks/point/materialize.js +5 -1
  126. package/src/actions/marks/rect/actions.js +3 -3
  127. package/src/actions/marks/remove.js +12 -10
  128. package/src/actions/marks/rule/actions.js +7 -4
  129. package/src/actions/marks/text/actions.js +17 -5
  130. package/src/actions/marks/text/layout.js +3 -0
  131. package/src/actions/primitives/createGraphics.js +3 -2
  132. package/src/actions/primitives/editGraphics.js +15 -11
  133. package/src/actions/primitives/graphicProperties.js +1 -6
  134. package/src/actions/primitives/semanticAction.js +27 -1
  135. package/src/actions/primitives/semanticValidation/guide.js +11 -1
  136. package/src/actions/primitives/semanticValidation/layer.js +8 -0
  137. package/src/actions/regression/create.js +1 -1
  138. package/src/actions/regression/edit.js +18 -6
  139. package/src/actions/regression/resolve.js +2 -6
  140. package/src/actions/scales/channels.js +9 -4
  141. package/src/actions/scales/consumers/families.js +16 -5
  142. package/src/actions/scales/create.js +10 -5
  143. package/src/actions/scales/definitions.js +1 -425
  144. package/src/actions/scales/edit.js +3 -3
  145. package/src/actions/scales/editPolicy.js +12 -3
  146. package/src/actions/scales/materialize.js +1 -1
  147. package/src/actions/scales/patch.js +1 -1
  148. package/src/actions/scales/preview.js +21 -1
  149. package/src/actions/scales/quantitativeColor.js +8 -3
  150. package/src/actions/selection/actions.js +1 -13
  151. package/src/actions/theme/reconcile.js +126 -45
  152. package/src/actions/theme/state.js +12 -0
  153. package/src/actions/titles/actions.js +5 -18
  154. package/src/actions/titles/resolve.js +41 -19
  155. package/src/actions/violinPlots/edit.js +3 -20
  156. package/src/core/compositionState.js +6 -3
  157. package/src/core/font.js +9 -2
  158. package/src/core/graphicProperties.js +1 -0
  159. package/src/core/optionRequirements.js +1 -0
  160. package/src/core/programState.js +4 -0
  161. package/src/core/resourceReferences.js +5 -2
  162. package/src/core/textMetricProfile.js +3 -2
  163. package/src/core/textMetrics.js +41 -4
  164. package/src/core/validation.js +35 -0
  165. package/src/core/vocabulary.js +7 -0
  166. package/src/grammar/aggregate.js +4 -18
  167. package/src/grammar/arcs.js +1 -386
  168. package/src/grammar/areaPaint.js +10 -0
  169. package/src/grammar/areaSeries.js +18 -10
  170. package/src/grammar/axisTextPlacement.js +1 -0
  171. package/src/grammar/bars/geometry.js +30 -3
  172. package/src/grammar/bars/policy.js +12 -1
  173. package/src/grammar/bin.js +2 -12
  174. package/src/grammar/bin2d.js +1 -6
  175. package/src/grammar/boxPlot.js +56 -9
  176. package/src/grammar/categoryOrder.js +1 -6
  177. package/src/grammar/colorSizeLegend.js +39 -0
  178. package/src/grammar/complete.js +15 -19
  179. package/src/grammar/computed.js +115 -26
  180. package/src/grammar/curveCommands.js +29 -16
  181. package/src/grammar/datasetSchema.js +13 -9
  182. package/src/grammar/displayLabels.js +36 -0
  183. package/src/grammar/ecdf.js +2 -9
  184. package/src/grammar/facets/dependencies.js +6 -2
  185. package/src/grammar/facets/guides.js +58 -30
  186. package/src/grammar/facets/index.js +2 -5
  187. package/src/grammar/facets/legendIntent.js +65 -0
  188. package/src/grammar/fold.js +2 -12
  189. package/src/grammar/gradientProfile.js +1 -6
  190. package/src/grammar/guideDash.js +1 -0
  191. package/src/grammar/impute.js +21 -48
  192. package/src/grammar/interval.js +2 -9
  193. package/src/grammar/legendBlocks.js +1 -0
  194. package/src/grammar/legendBorder.js +9 -0
  195. package/src/grammar/legendClip.js +19 -0
  196. package/src/grammar/legendCombinations.js +45 -0
  197. package/src/grammar/legendGradient.js +27 -0
  198. package/src/grammar/legendGroups.js +1 -0
  199. package/src/grammar/legendOpacity.js +6 -0
  200. package/src/grammar/legendTitleStyle.js +22 -0
  201. package/src/grammar/lineSeries.js +32 -17
  202. package/src/grammar/markLabels.js +5 -2
  203. package/src/grammar/normalize.js +4 -54
  204. package/src/grammar/numeric.js +1 -290
  205. package/src/grammar/parallelCoordinates.js +1 -6
  206. package/src/grammar/parseTime.js +41 -0
  207. package/src/grammar/pathCommands.js +11 -4
  208. package/src/grammar/pointShapes.js +16 -2
  209. package/src/grammar/polar.js +1 -184
  210. package/src/grammar/positionCompatibility.js +3 -3
  211. package/src/grammar/regression/derive.js +16 -2
  212. package/src/grammar/regression/models.js +142 -54
  213. package/src/grammar/regression/parameters.js +23 -4
  214. package/src/grammar/regression/statistics.js +64 -0
  215. package/src/grammar/roundedRect.js +42 -54
  216. package/src/grammar/scales/appearance.js +1 -206
  217. package/src/grammar/scales/color.js +1 -382
  218. package/src/grammar/scales/colorConsumers.js +5 -2
  219. package/src/grammar/scales/definition.js +1 -197
  220. package/src/grammar/scales/discretized.js +1 -195
  221. package/src/grammar/scales/size.js +1 -456
  222. package/src/grammar/scales/temporal.js +2 -13
  223. package/src/grammar/scales/transformed.js +1 -383
  224. package/src/grammar/scales/types.js +1 -1
  225. package/src/grammar/scales/validation.js +1 -66
  226. package/src/grammar/schemas/concreteGraphic.js +14 -0
  227. package/src/grammar/schemas/graphic.js +6 -0
  228. package/src/grammar/schemas/graphicBounds.js +31 -7
  229. package/src/grammar/schemas/semanticPath.js +19 -3
  230. package/src/grammar/stack.js +2 -10
  231. package/src/grammar/statisticalReference.js +1 -6
  232. package/src/grammar/summary.js +4 -27
  233. package/src/grammar/text.js +19 -0
  234. package/src/grammar/timeUnit.js +22 -11
  235. package/src/grammar/transformKeys.js +47 -0
  236. package/src/grammar/transformTopology.js +1 -0
  237. package/src/grammar/transforms.js +15 -2
  238. package/src/grammar/valueFormat.js +17 -5
  239. package/src/grammar/weightedStatistics.js +1 -6
  240. package/src/grammar/window.js +4 -58
  241. package/src/inspectionDescriptors.js +1 -1
  242. package/src/layout/canvas.js +1 -110
  243. package/src/layout/facets.js +1 -244
  244. package/src/layout/guideCollisions.js +9 -1
  245. package/src/layout/labels.js +1 -0
  246. package/src/layout/legend.js +13 -0
  247. package/src/layout/legendGroup.js +1 -0
  248. package/src/layout/legendItems.js +59 -15
  249. package/src/layout/legendLane.js +1 -441
  250. package/src/layout/legendText.js +1 -0
  251. package/src/layout/title.js +35 -13
  252. package/src/materialization/bars/aggregate.js +11 -5
  253. package/src/materialization/bars/histogram.js +4 -1
  254. package/src/materialization/bars/ranged.js +19 -6
  255. package/src/materialization/bars/resolve.js +22 -1
  256. package/src/materialization/compositionSnapshot.js +2 -7
  257. package/src/materialization/coordinateBounds.js +21 -0
  258. package/src/materialization/dependencies.js +4 -44
  259. package/src/materialization/facetGuides/edit.js +30 -0
  260. package/src/materialization/facetGuides/layout.js +1 -0
  261. package/src/materialization/facetGuides/owner.js +125 -0
  262. package/src/materialization/facetGuides/ownerLayout.js +71 -0
  263. package/src/materialization/facetGuides/placement.js +71 -14
  264. package/src/materialization/facetGuides/preparation.js +22 -7
  265. package/src/materialization/facetHeaders.js +1 -347
  266. package/src/materialization/facets.js +1 -185
  267. package/src/materialization/graphicHierarchy.js +5 -1
  268. package/src/materialization/graphicTranslation.js +73 -0
  269. package/src/materialization/guides/layout.js +1 -57
  270. package/src/materialization/guides/resources.js +1 -123
  271. package/src/materialization/layout.js +53 -0
  272. package/src/materialization/legendBorder.js +28 -0
  273. package/src/materialization/legendClip.js +76 -0
  274. package/src/materialization/legendGroups.js +1 -0
  275. package/src/materialization/legendText.js +1 -0
  276. package/src/materialization/legends.js +2 -2
  277. package/src/materialization/marks/capabilities.js +5 -6
  278. package/src/materialization/rect.js +1 -1
  279. package/src/materialization/scaleGuideDependencies.js +14 -15
  280. package/src/materialization/scales/map.js +6 -0
  281. package/src/materialization/scales/policies/arc.js +8 -7
  282. package/src/materialization/scales/policies/bar.js +19 -7
  283. package/src/materialization/scales/policies/series.js +2 -1
  284. package/src/materialization/scales/resolve.js +7 -3
  285. package/src/materialization/selection/items/arc.js +1 -1
  286. package/src/materialization/selection/items/bar.js +7 -6
  287. package/src/materialization/selection/items/path.js +7 -6
  288. package/src/materialization/selection/items/rect.js +2 -2
  289. package/src/materialization/text.js +13 -4
  290. package/src/materialization/textLines.js +15 -0
  291. package/src/materialization/typography.js +10 -2
  292. package/src/persistence/codec.js +37 -6
  293. package/src/persistence/validation.js +220 -0
  294. package/src/persistence.js +19 -6
  295. package/src/renderers/canvas/circle.js +6 -3
  296. package/src/renderers/canvas/fill.js +7 -1
  297. package/src/renderers/canvas/index.js +22 -11
  298. package/src/renderers/canvas/isolated.js +166 -0
  299. package/src/renderers/canvas/limits.js +3 -0
  300. package/src/renderers/canvas/line.js +1 -0
  301. package/src/renderers/canvas/path.js +1 -1
  302. package/src/renderers/canvas/rect.js +11 -8
  303. package/src/renderers/canvas/stroke.js +2 -1
  304. package/src/renderers/canvas/text.js +5 -3
  305. package/src/renderers/pdf.js +2 -2
  306. package/src/renderers/png.js +2 -2
  307. package/src/renderers/svg.js +14 -4
  308. package/src/selectors/axes.js +1 -0
  309. package/src/selectors/graphicCapacity.js +38 -0
  310. package/src/selectors/layers.js +19 -0
  311. package/src/theme/defaults.js +8 -2
  312. package/src/version.js +1 -1
  313. package/types/basic.d.ts +3 -0
  314. package/types/index.d.ts +12 -0
  315. package/types/program.d.ts +536 -129
@@ -202,10 +202,10 @@ function validateFacetSpec(compositionSpec) {
202
202
  !isPlainObject(guides) ||
203
203
  Object.keys(guides).some(key => !["axes", "legend"].includes(key)) ||
204
204
  !["each", "outer"].includes(guides.axes) ||
205
- ![false, "shared"].includes(guides.legend)
205
+ (![false, "shared"].includes(guides.legend) && !isPlainObject(guides.legend))
206
206
  ) {
207
207
  throw new Error(
208
- 'compositionSpec.facet.guides requires axes "each" or "outer" and legend false or "shared".'
208
+ 'compositionSpec.facet.guides requires axes "each" or "outer" and legend false, "shared", or parent legend options.'
209
209
  );
210
210
  }
211
211
  }
@@ -272,7 +272,7 @@ export function ownCompositionSpec(compositionSpec, children) {
272
272
  }
273
273
  const allowed = [
274
274
  "id", "type", "direction", "children", "columns", "gap", "align",
275
- "padding", "facet"
275
+ "padding", "facet", "spacing"
276
276
  ];
277
277
  const unknown = Object.keys(compositionSpec).find(key => !allowed.includes(key));
278
278
  if (unknown !== undefined) {
@@ -282,6 +282,9 @@ export function ownCompositionSpec(compositionSpec, children) {
282
282
  throw new TypeError("compositionSpec.id must be a non-empty string.");
283
283
  }
284
284
  const facet = compositionSpec.type === "facet";
285
+ if (compositionSpec.spacing !== undefined && (!facet || !["plot", "canvas"].includes(compositionSpec.spacing))) {
286
+ throw new Error("compositionSpec.spacing requires a facet and canvas or plot spacing.");
287
+ }
285
288
  if (compositionSpec.type !== undefined && !facet) {
286
289
  throw new Error(`Unknown compositionSpec type "${compositionSpec.type}".`);
287
290
  }
package/src/core/font.js CHANGED
@@ -5,8 +5,15 @@ export function normalizeRendererFontWeight(fontWeight) {
5
5
  return Math.min(900, Math.max(100, Math.round(fontWeight / 100) * 100));
6
6
  }
7
7
 
8
- export function textMetricKey({ text, fontFamily, fontSize, fontWeight }) {
9
- return JSON.stringify([text, fontFamily, fontSize, fontWeight]);
8
+ export function validateFontStyle(value, label = "Text") {
9
+ if (!["normal", "italic"].includes(value)) {
10
+ throw new Error(`${label} fontStyle must be normal or italic.`);
11
+ }
12
+ return value;
13
+ }
14
+
15
+ export function textMetricKey({ text, fontFamily, fontSize, fontWeight, fontStyle = "normal" }) {
16
+ return JSON.stringify([text, fontFamily, fontSize, fontWeight, fontStyle]);
10
17
  }
11
18
 
12
19
  export function textMetricFontWeight(value = "normal") {
@@ -0,0 +1 @@
1
+ function n(e,t,i){for(const[r,c]of Object.entries(i))e=e.editGraphics({target:t,property:r,value:c});return e}export{n as editGraphicProperties};
@@ -1,6 +1,7 @@
1
1
  // The method's value-producing contract is shared by runtime validation and
2
2
  // authoring knowledge; editing can inherit these values from stored state.
3
3
  export const IMPUTE_REQUIRED_OPTIONS = Object.freeze({
4
+ mean: Object.freeze([]),
4
5
  constant: Object.freeze(["value"]),
5
6
  forward: Object.freeze(["sortBy"]),
6
7
  backward: Object.freeze(["sortBy"]),
@@ -44,6 +44,10 @@ export function releaseNamedResourceOwnership(program, { kind, id, dataOwner })
44
44
  materializationConfigs,
45
45
  ["calculations", "datasets", dataOwner?.current ?? id]
46
46
  ).value;
47
+ materializationConfigs = removeMaterializationConfig(
48
+ materializationConfigs,
49
+ ["regressionFits", dataOwner?.current ?? id]
50
+ ).value;
47
51
  const context = program.context.currentData === id
48
52
  ? freezeOwned({ ...program.context, currentData: undefined })
49
53
  : program.context;
@@ -97,7 +97,7 @@ export function collectSemanticScaleReferences(program) {
97
97
  }
98
98
  }
99
99
  collectKnownKeys(program.semanticSpec.guides, [], (key, value, path) => {
100
- if (key === "scale") {
100
+ if (["scale", "colorScale", "sizeScale"].includes(key)) {
101
101
  append(references, reference(
102
102
  "scale", value, "semanticGuide", "guides", path
103
103
  ));
@@ -199,13 +199,16 @@ export function collectMarkConfigReferences(program) {
199
199
 
200
200
  export function collectGuideConfigReferences(program) {
201
201
  const references = [];
202
+ for (const target of Object.keys(program.guideConfigs?.legendGroups ?? {})) {
203
+ append(references, reference("mark", target, "legend", target, ["legendGroups", target]));
204
+ }
202
205
  for (const [family, configs] of Object.entries(program.guideConfigs ?? {})) {
203
206
  collectKnownKeys(configs, [], (key, value, path, parentPath) => {
204
207
  const ownerId = parentPath.length === 0 ? family : parentPath.join(".");
205
208
  const relativePath = parentPath.length === 0
206
209
  ? path
207
210
  : path.slice(parentPath.length);
208
- if (key === "scale") {
211
+ if (["scale", "colorScale", "sizeScale"].includes(key)) {
209
212
  append(references, reference(
210
213
  "scale", value, family, ownerId, relativePath
211
214
  ));
@@ -2,7 +2,7 @@ import { cloneAndFreeze } from "./immutable.js";
2
2
  import { validateUserId } from "./identifiers.js";
3
3
  import { validateOptionObject, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString } from "./validation.js";
4
4
 
5
- import { textMetricKey } from "./font.js";
5
+ import { textMetricKey, validateFontStyle } from "./font.js";
6
6
 
7
7
  export function normalizeTextMetricProfile(profile) {
8
8
  validateOptionObject(profile, ["schemaVersion", "id", "measurements"], "Text metrics profile");
@@ -11,7 +11,7 @@ export function normalizeTextMetricProfile(profile) {
11
11
  if (!Array.isArray(profile.measurements)) throw new TypeError("Text metrics measurements must be an array.");
12
12
  const seen = new Set();
13
13
  for (const measurement of profile.measurements) {
14
- validateOptionObject(measurement, ["text", "fontFamily", "fontSize", "fontWeight", "width"], "Text measurement");
14
+ validateOptionObject(measurement, ["text", "fontFamily", "fontSize", "fontWeight", "fontStyle", "width"], "Text measurement");
15
15
  if (typeof measurement.text !== "string") throw new TypeError("Text measurement text must be a string.");
16
16
  validateNonEmptyString(measurement.fontFamily, "Text measurement fontFamily");
17
17
  validatePositiveFinite(measurement.fontSize, "Text measurement fontSize");
@@ -20,6 +20,7 @@ export function normalizeTextMetricProfile(profile) {
20
20
  measurement.fontWeight > 900 || measurement.fontWeight % 100 !== 0) {
21
21
  throw new RangeError("Text measurement fontWeight must be 100–900 in steps of 100.");
22
22
  }
23
+ if (measurement.fontStyle !== undefined) validateFontStyle(measurement.fontStyle, "Text measurement");
23
24
  const key = textMetricKey(measurement);
24
25
  if (seen.has(key)) throw new Error("Duplicate text measurement combination.");
25
26
  seen.add(key);
@@ -1,3 +1,4 @@
1
+ import { validateGeneratedItemLimit } from "./validation.js";
1
2
  import { DEFAULT_FONT_FAMILY, textMetricFontWeight, textMetricKey } from "./font.js";
2
3
 
3
4
  import { isOwned } from "./immutable.js";
@@ -13,7 +14,7 @@ function measuredWidth(profile, text, style) {
13
14
  if (isOwned(profile)) measurementIndexes.set(profile, index);
14
15
  }
15
16
  return index.get(textMetricKey({ text, fontFamily: style.fontFamily ?? DEFAULT_FONT_FAMILY,
16
- fontSize: style.fontSize, fontWeight: textMetricFontWeight(style.fontWeight) }));
17
+ fontSize: style.fontSize, fontWeight: textMetricFontWeight(style.fontWeight), fontStyle: style.fontStyle }));
17
18
  }
18
19
 
19
20
  function codePointWidth(codePoint) {
@@ -43,7 +44,7 @@ export function textBoundsFitCanvas(bounds, canvas) {
43
44
 
44
45
  export function measureTextWidth(
45
46
  text,
46
- { fontSize, fontFamily, fontWeight } = {},
47
+ { fontSize, fontFamily, fontWeight, fontStyle } = {},
47
48
  profile
48
49
  ) {
49
50
  if (typeof text !== "string") {
@@ -52,7 +53,7 @@ export function measureTextWidth(
52
53
  if (!Number.isFinite(fontSize) || fontSize <= 0) {
53
54
  throw new RangeError("Text measurement requires a positive fontSize.");
54
55
  }
55
- const measured = measuredWidth(profile, text, { fontSize, fontFamily, fontWeight });
56
+ const measured = measuredWidth(profile, text, { fontSize, fontFamily, fontWeight, fontStyle });
56
57
  if (measured !== undefined) return measured;
57
58
  let joined = false;
58
59
  let width = 0;
@@ -91,9 +92,11 @@ export function resolveTextBounds({
91
92
  x,
92
93
  y,
93
94
  text,
95
+ lines,
94
96
  fontSize,
95
97
  fontFamily,
96
98
  fontWeight,
99
+ fontStyle,
97
100
  textAlign = "left",
98
101
  textBaseline = "alphabetic",
99
102
  rotation = 0
@@ -101,7 +104,19 @@ export function resolveTextBounds({
101
104
  if (![x, y, rotation].every(Number.isFinite)) {
102
105
  throw new TypeError("Text bounds require finite x, y, and rotation values.");
103
106
  }
104
- const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight }, profile);
107
+ if (lines !== undefined) {
108
+ const cosine = Math.cos(rotation), sine = Math.sin(rotation);
109
+ const bounds = lines.map(line => resolveTextBounds({
110
+ x: x + line.x * cosine - line.y * sine, y: y + line.x * sine + line.y * cosine,
111
+ text: line.text, fontSize, fontFamily, fontWeight, fontStyle, textAlign, textBaseline, rotation
112
+ }, profile));
113
+ const union = bounds.reduce((result, bound) => ({
114
+ left: Math.min(result.left, bound.left), right: Math.max(result.right, bound.right),
115
+ top: Math.min(result.top, bound.top), bottom: Math.max(result.bottom, bound.bottom)
116
+ }), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
117
+ return Object.freeze(requireFiniteBounds(union, "Text"));
118
+ }
119
+ const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight, fontStyle }, profile);
105
120
  const [left, right] = textAlign === "center"
106
121
  ? [-width / 2, width / 2]
107
122
  : ["right", "end"].includes(textAlign) ? [-width, 0] : [0, width];
@@ -126,3 +141,25 @@ export function resolveTextBounds({
126
141
  };
127
142
  return Object.freeze(requireFiniteBounds(bounds, "Text"));
128
143
  }
144
+
145
+ export function unionBounds(bounds) {
146
+ return bounds.reduce((union, item) => ({
147
+ left: Math.min(union.left, item.left),
148
+ right: Math.max(union.right, item.right),
149
+ top: Math.min(union.top, item.top),
150
+ bottom: Math.max(union.bottom, item.bottom)
151
+ }), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
152
+ }
153
+
154
+ // Resolve explicit hard breaks once for text marks and guide layout.
155
+ export function resolveTextLines(text, config) {
156
+ const parts = text.split(/\r\n|\r|\n/u);
157
+ if (parts.length === 1 && config.blockAlign !== "middle") return undefined;
158
+ validateGeneratedItemLimit(parts.length, "Text line count");
159
+ const lineHeight = config.lineHeight ?? config.fontSize * 1.2;
160
+ const bounds = resolveTextBounds({ x: 0, y: 0, text: "", fontSize: config.fontSize,
161
+ textBaseline: config.baseline, textAlign: config.align });
162
+ const offset = config.blockAlign === "middle"
163
+ ? -(bounds.top + bounds.bottom + (parts.length - 1) * lineHeight) / 2 : 0;
164
+ return parts.map((text, index) => ({ text, x: 0, y: offset + index * lineHeight }));
165
+ }
@@ -1,5 +1,6 @@
1
1
  import { annotateError } from "./diagnostics.js";
2
2
  import { isPlainObject } from "./immutable.js";
3
+ import { getMarkGraphicTypes } from "./vocabulary.js";
3
4
 
4
5
  export const MAX_GENERATED_ITEMS = 10_000;
5
6
  export const MAX_WORK_ITEMS = 10_000_000;
@@ -30,6 +31,14 @@ export function validateWorkLimit(value, label) {
30
31
  return validateGeneratedItemLimit(value, label, MAX_WORK_ITEMS);
31
32
  }
32
33
 
34
+ export function validateGraphicItemLimit(value, capacity) {
35
+ const point = capacity.pointOwned && getMarkGraphicTypes("point").includes(capacity.graphicType);
36
+ const label = point ? "Point mark item count" : capacity.label;
37
+ const maximum = point ? 50_000 : MAX_GENERATED_ITEMS;
38
+ if (point) validateGeneratedItemLimit(capacity.rawCount, label, maximum);
39
+ return validateGeneratedItemLimit(value, label, maximum);
40
+ }
41
+
33
42
  export function validateKeys(value, supported, label) {
34
43
  for (const key of Object.keys(value)) {
35
44
  if (!supported.includes(key)) {
@@ -98,3 +107,29 @@ export function validateNonNegativeFinite(value, label) {
98
107
  }
99
108
  return value;
100
109
  }
110
+
111
+ export function requireStringValue(value, label) {
112
+ if (typeof value !== "string" || value.length === 0) {
113
+ throw new TypeError(`${label} must be a non-empty string.`);
114
+ }
115
+ return value;
116
+ }
117
+
118
+ export function rejectUnknownProperties(value, supported, label) {
119
+ const unknown = Object.keys(value).find(key => !supported.includes(key));
120
+ if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
121
+ }
122
+
123
+ export function validateFontWeight(value, label, numberType = "number") {
124
+ if (typeof value !== "string" && !Number.isFinite(value)) {
125
+ throw new TypeError(`${label} must be a string or ${numberType}.`);
126
+ }
127
+ return value;
128
+ }
129
+
130
+ export function validateTextStyle(config, label) {
131
+ validateNonEmptyString(config.color, `${label} color`);
132
+ validatePositiveFinite(config.fontSize, `${label} fontSize`);
133
+ validateNonEmptyString(config.fontFamily, `${label} fontFamily`);
134
+ validateFontWeight(config.fontWeight, `${label} fontWeight`);
135
+ }
@@ -220,6 +220,7 @@ export const LEGEND_CHANNELS = Object.freeze([
220
220
  ]);
221
221
 
222
222
  export const LEGEND_CONFIG_KINDS = Object.freeze([
223
+ "colorSize",
223
224
  "series",
224
225
  "color",
225
226
  "stroke",
@@ -235,3 +236,9 @@ export const LEGEND_CONFIG_KINDS = Object.freeze([
235
236
  export function includesVocabulary(vocabulary, value) {
236
237
  return vocabulary.includes(value);
237
238
  }
239
+
240
+ export const PLOT_LEGEND_POSITIONS = Object.freeze(["top-left", "top-right", "bottom-left", "bottom-right"]);
241
+ export const LEGEND_POSITIONS = Object.freeze(["left", "right", "top", "bottom", ...PLOT_LEGEND_POSITIONS]);
242
+ export const LEGEND_BLOCK_LAYOUTS = Object.freeze(["row", "column", "wrap"]);
243
+
244
+ export const AXIS_RESOURCE_CHANNELS = Object.freeze(["x", "y", "theta", "radius", "parallel"]);
@@ -1,6 +1,8 @@
1
+ import { requireStringValue as nonEmptyString } from "../core/validation.js";
2
+ import { isGroupValue as isNominal } from "./transformKeys.js";
1
3
  import { isPlainObject } from "../core/immutable.js";
2
4
  import {
3
- interpolateNumber,
5
+ quantileSorted,
4
6
  numericExtent,
5
7
  requireFiniteResult,
6
8
  stableFiniteDeviation,
@@ -25,11 +27,6 @@ const PARAMETERIZED_OPERATIONS = [
25
27
  "quantile", "first", "last", "ciLower", "ciUpper"
26
28
  ];
27
29
 
28
- function nonEmptyString(value, label) {
29
- if (typeof value !== "string" || value.length === 0) {
30
- throw new TypeError(`${label} must be a non-empty string.`);
31
- }
32
- }
33
30
 
34
31
  export function isScalarAggregate(value) {
35
32
  return typeof value === "string" && SCALAR_OPERATIONS.has(value);
@@ -137,10 +134,6 @@ function isMissing(value) {
137
134
  (typeof value === "number" && Number.isNaN(value));
138
135
  }
139
136
 
140
- function isNominal(value) {
141
- return typeof value === "string" || typeof value === "boolean" ||
142
- (typeof value === "number" && Number.isFinite(value));
143
- }
144
137
 
145
138
  export function validateAggregateFieldValues(rows, field, fieldType) {
146
139
  if (!Array.isArray(rows)) {
@@ -165,14 +158,7 @@ function finiteValues(values) {
165
158
 
166
159
  function quantile(values, probability) {
167
160
  const ordered = [...values].sort((left, right) => left - right);
168
- const position = (ordered.length - 1) * probability;
169
- const lower = Math.floor(position);
170
- const upper = Math.ceil(position);
171
- return interpolateNumber(
172
- ordered[lower],
173
- ordered[upper],
174
- position - lower
175
- );
161
+ return quantileSorted(ordered, probability);
176
162
  }
177
163
 
178
164
  function comparableOrderKey(value) {