ggaction 0.0.17 → 0.0.18

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 (263) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +2 -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 +16 -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 +4 -4
  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/edit.js +1 -1
  35. package/src/actions/data/regression.js +2 -1
  36. package/src/actions/data/shared.js +3 -2
  37. package/src/actions/data/stack.js +3 -1
  38. package/src/actions/encodings/appearance.js +37 -29
  39. package/src/actions/encodings/barWidth.js +36 -11
  40. package/src/actions/encodings/channels.js +0 -1
  41. package/src/actions/encodings/color/continuous.js +1 -1
  42. package/src/actions/encodings/color/index.js +1 -1
  43. package/src/actions/encodings/color/policy.js +10 -3
  44. package/src/actions/encodings/density/resolve.js +1 -6
  45. package/src/actions/encodings/position/policies/arc.js +12 -10
  46. package/src/actions/encodings/position/policies/area.js +3 -3
  47. package/src/actions/encodings/position/policies/bar.js +15 -6
  48. package/src/actions/encodings/position/policies/line.js +17 -6
  49. package/src/actions/encodings/position/resolve.js +3 -2
  50. package/src/actions/encodings/ranged.js +17 -0
  51. package/src/actions/encodings/remove.js +3 -2
  52. package/src/actions/encodings/seriesLayout.js +1 -0
  53. package/src/actions/encodings/text.js +6 -3
  54. package/src/actions/errorBands/create.js +2 -2
  55. package/src/actions/errorBands/edit.js +2 -1
  56. package/src/actions/facets/actions.js +158 -240
  57. package/src/actions/gradientPlots/resolve.js +2 -17
  58. package/src/actions/guides/applicability.js +1 -1
  59. package/src/actions/guides/axes/axis.js +16 -4
  60. package/src/actions/guides/axes/edit.js +4 -4
  61. package/src/actions/guides/axes/labels.js +120 -46
  62. package/src/actions/guides/axes/lines.js +6 -5
  63. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  64. package/src/actions/guides/axes/parallel/policy.js +8 -7
  65. package/src/actions/guides/axes/remove.js +3 -2
  66. package/src/actions/guides/axes/tickGroups.js +5 -2
  67. package/src/actions/guides/axes/ticks.js +6 -3
  68. package/src/actions/guides/axes/titles.js +71 -26
  69. package/src/actions/guides/basic.js +2 -0
  70. package/src/actions/guides/facadeLegend.js +10 -2
  71. package/src/actions/guides/grids/grid.js +7 -1
  72. package/src/actions/guides/grids/resolve.js +59 -12
  73. package/src/actions/guides/legends/blocks.js +3 -1
  74. package/src/actions/guides/legends/categorical/actions.js +70 -17
  75. package/src/actions/guides/legends/categorical/components.js +12 -8
  76. package/src/actions/guides/legends/categorical/index.js +6 -0
  77. package/src/actions/guides/legends/categorical/layout.js +37 -8
  78. package/src/actions/guides/legends/categorical/options.js +31 -20
  79. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  80. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  81. package/src/actions/guides/legends/categorical/validation.js +1 -6
  82. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  83. package/src/actions/guides/legends/continuous/common.js +1 -447
  84. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  85. package/src/actions/guides/legends/continuous/interval.js +4 -4
  86. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  87. package/src/actions/guides/legends/edit.js +1 -604
  88. package/src/actions/guides/legends/focused.js +2 -2
  89. package/src/actions/guides/legends/group.js +1 -0
  90. package/src/actions/guides/legends/lane.js +1 -516
  91. package/src/actions/guides/legends/remove.js +3 -0
  92. package/src/actions/guides/legends/sampling.js +1 -1
  93. package/src/actions/guides/legends/size.js +51 -43
  94. package/src/actions/guides/legends/strokeWidth.js +11 -29
  95. package/src/actions/guides/legends/target.js +3 -3
  96. package/src/actions/guides/legends/transition.js +21 -7
  97. package/src/actions/guides/polar/axes/labels.js +4 -2
  98. package/src/actions/guides/polar/axes/titles.js +7 -5
  99. package/src/actions/guides/polar/resolve.js +6 -14
  100. package/src/actions/index.js +16 -16
  101. package/src/actions/marks/arc/actions.js +3 -10
  102. package/src/actions/marks/area/actions.js +8 -5
  103. package/src/actions/marks/area/materialize.js +8 -9
  104. package/src/actions/marks/bar/create.js +10 -4
  105. package/src/actions/marks/bar/edit.js +8 -4
  106. package/src/actions/marks/bar/materialize.js +9 -6
  107. package/src/actions/marks/line/actions.js +11 -4
  108. package/src/actions/marks/line/materialize.js +9 -5
  109. package/src/actions/marks/point/edit.js +1 -1
  110. package/src/actions/marks/rect/actions.js +3 -3
  111. package/src/actions/marks/remove.js +2 -0
  112. package/src/actions/marks/rule/actions.js +7 -4
  113. package/src/actions/marks/text/actions.js +17 -5
  114. package/src/actions/marks/text/layout.js +3 -0
  115. package/src/actions/primitives/semanticValidation/layer.js +4 -0
  116. package/src/actions/regression/create.js +1 -1
  117. package/src/actions/regression/edit.js +3 -3
  118. package/src/actions/regression/resolve.js +2 -6
  119. package/src/actions/scales/channels.js +9 -4
  120. package/src/actions/scales/consumers/families.js +16 -5
  121. package/src/actions/scales/create.js +8 -3
  122. package/src/actions/scales/definitions.js +1 -425
  123. package/src/actions/scales/edit.js +2 -2
  124. package/src/actions/scales/editPolicy.js +9 -3
  125. package/src/actions/scales/materialize.js +1 -1
  126. package/src/actions/scales/preview.js +21 -1
  127. package/src/actions/scales/quantitativeColor.js +3 -3
  128. package/src/actions/selection/actions.js +1 -13
  129. package/src/actions/theme/reconcile.js +81 -27
  130. package/src/actions/theme/state.js +12 -0
  131. package/src/actions/titles/actions.js +5 -18
  132. package/src/actions/titles/resolve.js +41 -19
  133. package/src/actions/violinPlots/edit.js +3 -20
  134. package/src/core/compositionState.js +4 -1
  135. package/src/core/font.js +9 -2
  136. package/src/core/optionRequirements.js +1 -0
  137. package/src/core/resourceReferences.js +3 -0
  138. package/src/core/textMetricProfile.js +3 -2
  139. package/src/core/textMetrics.js +41 -4
  140. package/src/core/validation.js +26 -0
  141. package/src/core/vocabulary.js +4 -0
  142. package/src/grammar/aggregate.js +4 -18
  143. package/src/grammar/arcs.js +1 -386
  144. package/src/grammar/areaPaint.js +10 -0
  145. package/src/grammar/areaSeries.js +18 -10
  146. package/src/grammar/bars/geometry.js +30 -3
  147. package/src/grammar/bars/policy.js +12 -1
  148. package/src/grammar/bin.js +2 -12
  149. package/src/grammar/bin2d.js +1 -6
  150. package/src/grammar/boxPlot.js +56 -9
  151. package/src/grammar/categoryOrder.js +1 -6
  152. package/src/grammar/complete.js +15 -19
  153. package/src/grammar/computed.js +92 -25
  154. package/src/grammar/curveCommands.js +29 -16
  155. package/src/grammar/datasetSchema.js +3 -7
  156. package/src/grammar/displayLabels.js +28 -0
  157. package/src/grammar/ecdf.js +2 -9
  158. package/src/grammar/facets/dependencies.js +6 -2
  159. package/src/grammar/facets/guides.js +5 -0
  160. package/src/grammar/facets/index.js +2 -5
  161. package/src/grammar/fold.js +2 -12
  162. package/src/grammar/gradientProfile.js +1 -6
  163. package/src/grammar/impute.js +21 -48
  164. package/src/grammar/interval.js +2 -9
  165. package/src/grammar/legendGradient.js +18 -0
  166. package/src/grammar/legendGroups.js +1 -0
  167. package/src/grammar/legendTitleStyle.js +22 -0
  168. package/src/grammar/lineSeries.js +32 -17
  169. package/src/grammar/markLabels.js +5 -2
  170. package/src/grammar/normalize.js +4 -54
  171. package/src/grammar/numeric.js +1 -290
  172. package/src/grammar/parallelCoordinates.js +1 -6
  173. package/src/grammar/parseTime.js +41 -0
  174. package/src/grammar/pathCommands.js +11 -4
  175. package/src/grammar/polar.js +1 -184
  176. package/src/grammar/positionCompatibility.js +3 -3
  177. package/src/grammar/regression/derive.js +7 -1
  178. package/src/grammar/regression/models.js +83 -55
  179. package/src/grammar/regression/parameters.js +14 -3
  180. package/src/grammar/roundedRect.js +42 -54
  181. package/src/grammar/scales/appearance.js +1 -206
  182. package/src/grammar/scales/color.js +1 -382
  183. package/src/grammar/scales/colorConsumers.js +5 -2
  184. package/src/grammar/scales/definition.js +1 -197
  185. package/src/grammar/scales/discretized.js +1 -195
  186. package/src/grammar/scales/size.js +1 -456
  187. package/src/grammar/scales/temporal.js +2 -13
  188. package/src/grammar/scales/transformed.js +1 -383
  189. package/src/grammar/scales/types.js +1 -1
  190. package/src/grammar/scales/validation.js +1 -66
  191. package/src/grammar/schemas/concreteGraphic.js +11 -0
  192. package/src/grammar/schemas/graphic.js +3 -0
  193. package/src/grammar/schemas/semanticPath.js +2 -1
  194. package/src/grammar/stack.js +2 -10
  195. package/src/grammar/statisticalReference.js +1 -6
  196. package/src/grammar/summary.js +3 -26
  197. package/src/grammar/text.js +19 -0
  198. package/src/grammar/timeUnit.js +16 -7
  199. package/src/grammar/transformKeys.js +47 -0
  200. package/src/grammar/transforms.js +4 -1
  201. package/src/grammar/valueFormat.js +17 -5
  202. package/src/grammar/weightedStatistics.js +1 -6
  203. package/src/grammar/window.js +4 -58
  204. package/src/inspectionDescriptors.js +1 -1
  205. package/src/layout/canvas.js +1 -110
  206. package/src/layout/facets.js +1 -244
  207. package/src/layout/labels.js +1 -0
  208. package/src/layout/legend.js +13 -0
  209. package/src/layout/legendGroup.js +1 -0
  210. package/src/layout/legendItems.js +59 -15
  211. package/src/layout/legendLane.js +1 -441
  212. package/src/layout/legendText.js +1 -0
  213. package/src/layout/title.js +35 -13
  214. package/src/materialization/bars/aggregate.js +11 -5
  215. package/src/materialization/bars/histogram.js +4 -1
  216. package/src/materialization/bars/ranged.js +19 -6
  217. package/src/materialization/bars/resolve.js +22 -1
  218. package/src/materialization/coordinateBounds.js +8 -0
  219. package/src/materialization/dependencies.js +4 -44
  220. package/src/materialization/facetGuides/layout.js +1 -0
  221. package/src/materialization/facetGuides/placement.js +19 -8
  222. package/src/materialization/facetGuides/preparation.js +15 -7
  223. package/src/materialization/facetHeaders.js +1 -347
  224. package/src/materialization/facets.js +1 -185
  225. package/src/materialization/graphicHierarchy.js +1 -1
  226. package/src/materialization/guides/layout.js +1 -57
  227. package/src/materialization/guides/resources.js +1 -123
  228. package/src/materialization/layout.js +53 -0
  229. package/src/materialization/legendGroups.js +1 -0
  230. package/src/materialization/legendText.js +1 -0
  231. package/src/materialization/legends.js +1 -1
  232. package/src/materialization/marks/capabilities.js +5 -6
  233. package/src/materialization/rect.js +1 -1
  234. package/src/materialization/scaleGuideDependencies.js +1 -1
  235. package/src/materialization/scales/map.js +3 -0
  236. package/src/materialization/scales/policies/arc.js +8 -7
  237. package/src/materialization/scales/policies/bar.js +19 -7
  238. package/src/materialization/scales/policies/series.js +2 -1
  239. package/src/materialization/scales/resolve.js +5 -3
  240. package/src/materialization/selection/items/arc.js +1 -1
  241. package/src/materialization/selection/items/bar.js +7 -6
  242. package/src/materialization/selection/items/path.js +7 -6
  243. package/src/materialization/selection/items/rect.js +2 -2
  244. package/src/materialization/text.js +13 -4
  245. package/src/materialization/textLines.js +15 -0
  246. package/src/persistence/codec.js +37 -6
  247. package/src/persistence/validation.js +47 -0
  248. package/src/persistence.js +18 -5
  249. package/src/renderers/canvas/circle.js +6 -3
  250. package/src/renderers/canvas/fill.js +7 -1
  251. package/src/renderers/canvas/index.js +2 -1
  252. package/src/renderers/canvas/line.js +1 -0
  253. package/src/renderers/canvas/path.js +1 -1
  254. package/src/renderers/canvas/rect.js +11 -8
  255. package/src/renderers/canvas/stroke.js +2 -1
  256. package/src/renderers/canvas/text.js +5 -3
  257. package/src/renderers/svg.js +4 -1
  258. package/src/selectors/layers.js +19 -0
  259. package/src/theme/defaults.js +8 -2
  260. package/src/version.js +1 -1
  261. package/types/basic.d.ts +1 -0
  262. package/types/index.d.ts +7 -0
  263. package/types/program.d.ts +347 -85
@@ -27,11 +27,13 @@ export function resolveSeriesLayoutDomain({
27
27
  seriesLayouts[index] === undefined &&
28
28
  !isSourceOwnedText(consumer.layer)
29
29
  );
30
+ const layout = activeLayouts[0].layout;
30
31
  const compatibleDirect = directConsumers.every(({ consumer }) =>
31
32
  layoutConsumers.every(({ consumer: layoutConsumer }) => {
32
33
  const aggregate = layoutConsumer.encoding.aggregate;
33
34
  const directEncoding = consumer.encoding;
34
35
  return directEncoding.fieldType === "quantitative" && (
36
+ layout === "overlay" ||
35
37
  Object.hasOwn(directEncoding, "datum") ||
36
38
  (
37
39
  aggregate !== "count" &&
@@ -45,7 +47,6 @@ export function resolveSeriesLayoutDomain({
45
47
  `Series layout scale "${id}" cannot be shared with another policy.`
46
48
  );
47
49
  }
48
- const layout = activeLayouts[0].layout;
49
50
  const values = [
50
51
  ...activeLayouts.flatMap(item => item.values),
51
52
  ...directConsumers.flatMap(item => item.values)
@@ -160,7 +160,7 @@ function resolveContinuousScale({
160
160
  }) {
161
161
  return {
162
162
  type: isSequentialColor
163
- ? "sequential"
163
+ ? scale.type
164
164
  : isOrdinalAppearance
165
165
  ? validateOrdinalScaleType(scale.type)
166
166
  : scale.type === "time"
@@ -377,7 +377,7 @@ export function resolveScaleMaterialization(options) {
377
377
  validateMeasuredRadiusConsumers({ scale, domain, range, consumers, markConfigs, thetaScales });
378
378
  if (channel === "shape" && domain.length > range.length) {
379
379
  throw new Error(
380
- `Shape scale "${id}" requires at least one distinct shape per domain value.`
380
+ `Shape scale "${id}" requires at least one shape entry per domain value.`
381
381
  );
382
382
  }
383
383
 
@@ -440,7 +440,9 @@ export function resolveScaleMaterialization(options) {
440
440
  const temporalBarBand = resolveTemporalBarBand(
441
441
  valuesByConsumer,
442
442
  domain,
443
- range
443
+ range,
444
+ markConfigs,
445
+ Array.isArray(scale.range)
444
446
  );
445
447
  if (temporalBarBand !== undefined) {
446
448
  resolvedScale = {
@@ -16,7 +16,7 @@ export function resolveArcItems(program, layer, dataset) {
16
16
  thetaScale,
17
17
  ...(radiusScale === undefined ? {} : { radiusScale }),
18
18
  frame,
19
- innerRadiusRatio: program.markConfigs[layer.id]?.innerRadius ?? 0
19
+ innerRadius: program.markConfigs[layer.id]?.innerRadius ?? 0
20
20
  });
21
21
  const graphic = program.graphicSpec.objects[layer.id];
22
22
  const stroke = layer.encoding?.stroke;
@@ -1,3 +1,4 @@
1
+ import { hasRectRadius } from "../../../grammar/roundedRect.js";
1
2
  import { deriveSeriesBarCells } from "../../../grammar/bars/aggregate.js";
2
3
  import { readSeriesIdentity } from "../../../grammar/pathSeries.js";
3
4
  import {
@@ -128,9 +129,12 @@ function histogramDefinitions(program, layer, dataset) {
128
129
  }
129
130
 
130
131
  function rangedDefinitions(layer, dataset) {
132
+ const measure = resolveBarGrain(layer) === BAR_GRAINS.centered ? resolveBarChannels(layer).measure : undefined;
131
133
  return dataset.values.map(row => ({
132
134
  fields: ownFields(row),
133
- channels: channelMapFromRow(row, layer),
135
+ channels: { ...channelMapFromRow(row, layer),
136
+ ...(measure !== undefined && layer.encoding[`${measure}2`] === undefined
137
+ ? { [measure]: 0, [`${measure}2`]: row[layer.encoding[measure].field] } : {}) },
134
138
  members: [row]
135
139
  }));
136
140
  }
@@ -205,16 +209,13 @@ export function resolveBarItems(program, layer, dataset, selectionGrain) {
205
209
  ? histogramDefinitions(program, layer, dataset)
206
210
  : grain === BAR_GRAINS.aggregate
207
211
  ? aggregateCellDefinitions(program, layer, dataset)
208
- : grain === BAR_GRAINS.ranged
212
+ : [BAR_GRAINS.ranged, BAR_GRAINS.centered].includes(grain)
209
213
  ? rangedDefinitions(layer, dataset)
210
214
  : undefined;
211
215
  if (definitions === undefined) {
212
216
  throw new Error(`Bar mark "${layer.id}" is incomplete for selection.`);
213
217
  }
214
- const graphicTypes = Object.hasOwn(
215
- program.markConfigs[layer.id]?.barAppearance ?? {},
216
- "cornerRadius"
217
- ) ? ["rect", "path"] : "rect";
218
+ const graphicTypes = hasRectRadius(program.markConfigs[layer.id]?.barAppearance) ? ["rect", "path"] : "rect";
218
219
  if (selectionGrain === "stack") {
219
220
  return finalizeItems(
220
221
  program,
@@ -59,15 +59,16 @@ export function resolveLineItems(program, layer, dataset) {
59
59
  const derived = deriveLineSeries(
60
60
  dataset.values,
61
61
  layer,
62
- x?.bin === undefined
63
- ? undefined
64
- : {
62
+ {
63
+ xDomain: program.resolvedScales[x?.scale]?.domain,
64
+ ...(x?.bin === undefined ? {} : {
65
65
  xBinBoundaries: resolveLineBins(
66
66
  dataset.values,
67
67
  layer,
68
68
  requireSemanticScale(program, x.scale)
69
69
  ).boundaries
70
- }
70
+ })
71
+ }
71
72
  );
72
73
  return finalizeItems(
73
74
  program,
@@ -82,8 +83,8 @@ export function resolveAreaItems(program, layer, dataset) {
82
83
  const transform = findUpstreamTransform(program, dataset, "density");
83
84
  const derived = transform === undefined
84
85
  ? layer.encoding?.y?.stack === "center"
85
- ? deriveCenteredAreaSeries(dataset.values, layer)
86
- : deriveAreaSeries(dataset.values, layer)
86
+ ? deriveCenteredAreaSeries(dataset.values, layer, { xDomain: program.resolvedScales[layer.encoding.x?.scale]?.domain, yDomain: program.resolvedScales[layer.encoding.y?.scale]?.domain })
87
+ : deriveAreaSeries(dataset.values, layer, { xDomain: program.resolvedScales[layer.encoding.x?.scale]?.domain, yDomain: program.resolvedScales[layer.encoding.y?.scale]?.domain })
87
88
  : deriveDensityAreaSeries(dataset.values, layer, transform);
88
89
  return finalizeItems(
89
90
  program,
@@ -1,5 +1,5 @@
1
1
  import { rectUsesFields } from "../../../grammar/rects.js";
2
- import { readRectItemGeometry } from "../../../grammar/roundedRect.js";
2
+ import { hasRectRadius, readRectItemGeometry } from "../../../grammar/roundedRect.js";
3
3
  import { resolveRectRows } from "../../rect.js";
4
4
  import {
5
5
  channelMapFromRow,
@@ -40,7 +40,7 @@ function resolveGradientPlotItems(program, layer, dataset) {
40
40
 
41
41
  export function resolveRectItems(program, layer, dataset) {
42
42
  const graphic = program.graphicSpec.objects[layer.id];
43
- const rounded = Object.hasOwn(program.markConfigs[layer.id] ?? {}, "cornerRadius");
43
+ const rounded = hasRectRadius(program.markConfigs[layer.id]);
44
44
  const compatibleCollection = rounded && graphic?.type === "collection" &&
45
45
  graphic.items?.every(item => ["rect", "path"].includes(item.type));
46
46
  if (
@@ -5,7 +5,7 @@ import {
5
5
  resolveMarkLabelPlacement
6
6
  } from "../layout/labels.js";
7
7
  import { resolveMarkLabelValues } from "../grammar/markLabels.js";
8
- import { formatTextValue } from "../grammar/text.js";
8
+ import { formatTextValue, resolveTextLines } from "../grammar/text.js";
9
9
  import { normalizePositionDatum } from "../grammar/positionDatum.js";
10
10
  import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
11
11
  import { findDataset } from "../selectors/datasets.js";
@@ -274,12 +274,14 @@ function concreteTextItem(config, position, text, { offsets = true } = {}) {
274
274
  if (text === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
275
275
  return undefined;
276
276
  }
277
+ const lines = resolveTextLines(text, config);
277
278
  return {
278
279
  type: "text",
279
280
  properties: {
280
281
  x: x + (offsets ? config.dx : 0),
281
282
  y: y + (offsets ? config.dy : 0),
282
283
  text,
284
+ ...(lines === undefined ? {} : { lines }),
283
285
  fill: config.fill,
284
286
  opacity: config.opacity,
285
287
  fontSize: config.fontSize,
@@ -310,6 +312,11 @@ function relativeLuminance(color) {
310
312
  }
311
313
 
312
314
  function sourceTextConfig(config, source, item) {
315
+ if (config.inheritColor && config.fillExplicit !== true) {
316
+ const fill = item.properties[config.inheritColor];
317
+ if (typeof fill !== "string") throw new Error(`Source mark "${source.id}" has no resolved ${config.inheritColor} color.`);
318
+ return { ...config, fill };
319
+ }
313
320
  if (
314
321
  !["arc", "rect"].includes(source.mark.type) ||
315
322
  config.fillExplicit === true
@@ -383,6 +390,7 @@ function resolveSourceTextItems(program, layer, config) {
383
390
  geometry: sourcePlacementGeometry(program, source, item),
384
391
  text: {
385
392
  text,
393
+ lines: resolveTextLines(text, resolvedConfig),
386
394
  fontSize: resolvedConfig.fontSize,
387
395
  fontFamily: resolvedConfig.fontFamily,
388
396
  fontWeight: resolvedConfig.fontWeight,
@@ -396,7 +404,7 @@ function resolveSourceTextItems(program, layer, config) {
396
404
  if (!resolved.visible) return [];
397
405
  if (
398
406
  resolved.anchor.startsWith("outside") &&
399
- config.fillExplicit !== true
407
+ config.fillExplicit !== true && !config.inheritColor
400
408
  ) {
401
409
  resolvedConfig = config;
402
410
  }
@@ -420,7 +428,7 @@ function resolveRowTextItems(program, layer, config) {
420
428
  throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
421
429
  }
422
430
  const dataset = applyItemMissingPolicy(layer, sourceDataset);
423
- const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text];
431
+ const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color];
424
432
  const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
425
433
  ? dataset.values.length
426
434
  : 1;
@@ -454,9 +462,10 @@ function resolveRowTextItems(program, layer, config) {
454
462
  const rows = length === 1 && dataset.values.length === 0
455
463
  ? [undefined]
456
464
  : dataset.values.slice(0, length);
465
+ const fills = resolveRowEncodingValues(program, layer, dataset, "color");
457
466
  return rows.flatMap((row, index) => {
458
467
  const concrete = concreteItem(
459
- config,
468
+ fills === undefined ? config : { ...config, fill: fills[index] },
460
469
  { x: x[index], y: y[index] },
461
470
  contentValue(layer.encoding.text, row),
462
471
  layer.encoding.text.format
@@ -0,0 +1,15 @@
1
+ import { freezeOwned } from "../core/immutable.js";
2
+
3
+ // Reconcile optional derived line geometry without recreating its text graphic.
4
+ // The owning wrapped domain materializer records the transition.
5
+ export function materializeTextLines(program, id, lines) {
6
+ if (lines !== undefined) return program.editGraphics({ target: id, property: "lines", value: lines });
7
+ const graphic = program.graphicSpec.objects[id];
8
+ if (!Object.hasOwn(graphic.properties, "lines")) return program;
9
+ const { lines: _lines, ...properties } = graphic.properties;
10
+ return program._clone({ graphicSpec: freezeOwned({ ...program.graphicSpec,
11
+ objects: freezeOwned({ ...program.graphicSpec.objects,
12
+ [id]: freezeOwned({ ...graphic, properties: freezeOwned(properties) })
13
+ })
14
+ }) });
15
+ }
@@ -5,7 +5,7 @@ export function invalidSnapshot(message, optionPath) {
5
5
  return annotateError(new TypeError(message), { code: "unsupported-format", optionPath });
6
6
  }
7
7
 
8
- export function encodeValue(value, path = "payload", ancestors = new WeakSet()) {
8
+ export function encodeValue(value, path = "payload", ancestors = new WeakSet(), shared) {
9
9
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
10
10
  if (value === undefined) return ["undefined"];
11
11
  if (typeof value === "bigint") return ["bigint", value.toString()];
@@ -17,6 +17,7 @@ export function encodeValue(value, path = "payload", ancestors = new WeakSet())
17
17
  if (ancestors.has(value)) throw invalidSnapshot(`Cannot serialize a cycle at ${path}.`, path);
18
18
  if (!Array.isArray(value) && !isPlainObject(value)) throw invalidSnapshot(`Cannot serialize a class instance at ${path}.`, path);
19
19
  if (Object.getOwnPropertySymbols(value).length) throw invalidSnapshot(`Cannot serialize symbol keys at ${path}.`, path);
20
+ if (shared?.seen.has(value)) return shared.seen.get(value);
20
21
  const keys = Object.getOwnPropertyNames(value);
21
22
  if (Array.isArray(value) ? keys.some(key => key !== "length" && !(/^(0|[1-9]\d*)$/.test(key) && Number(key) < value.length)) :
22
23
  keys.some(key => !Object.getOwnPropertyDescriptor(value, key).enumerable)) {
@@ -25,18 +26,48 @@ export function encodeValue(value, path = "payload", ancestors = new WeakSet())
25
26
  ancestors.add(value);
26
27
  const result = Array.isArray(value)
27
28
  ? ["array", Array.from({ length: value.length }, (_, index) => Object.hasOwn(value, index)
28
- ? encodeValue(value[index], `${path}[${index}]`, ancestors) : ["hole"])]
29
- : ["object", Object.entries(value).map(([key, child]) => [key, encodeValue(child, `${path}.${key}`, ancestors)])];
29
+ ? encodeValue(value[index], `${path}[${index}]`, ancestors, shared) : ["hole"])]
30
+ : ["object", Object.entries(value).map(([key, child]) => [key, encodeValue(child, `${path}.${key}`, ancestors, shared)])];
30
31
  ancestors.delete(value);
32
+ if (shared !== undefined && Array.isArray(value) && value.length >= 32) {
33
+ const key = JSON.stringify(result);
34
+ let index = shared.keys.get(key);
35
+ if (index === undefined) {
36
+ index = shared.arrays.length;
37
+ shared.keys.set(key, index);
38
+ shared.arrays.push(result);
39
+ }
40
+ const reference = ["reference", index];
41
+ shared.seen.set(value, reference);
42
+ return reference;
43
+ }
31
44
  return result;
32
45
  }
33
46
 
34
- export function decodeValue(value, path = "payload") {
47
+ export function encodeSharedValue(value) {
48
+ const shared = { arrays: [], keys: new Map(), seen: new WeakMap() };
49
+ const root = encodeValue(value, "payload", new WeakSet(), shared);
50
+ return ["shared", shared.arrays, root];
51
+ }
52
+
53
+ export function decodeValue(value, path = "payload", references) {
35
54
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
36
55
  if (typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return value;
37
56
  const invalid = () => { throw invalidSnapshot(`Invalid encoded value at ${path}.`, path); };
38
57
  if (!Array.isArray(value)) return invalid();
39
58
  const [tag, payload] = value;
59
+ if (tag === "shared" && references === undefined && value.length === 3 && Array.isArray(payload)) {
60
+ const arrays = [];
61
+ for (const [index, encoded] of payload.entries()) {
62
+ if (!Array.isArray(encoded) || encoded[0] !== "array") return invalid();
63
+ arrays.push(decodeValue(encoded, `${path}.arrays[${index}]`, arrays));
64
+ }
65
+ return decodeValue(value[2], path, arrays);
66
+ }
67
+ if (tag === "reference" && references !== undefined && value.length === 2 &&
68
+ Number.isInteger(payload) && payload >= 0 && payload < references.length) {
69
+ return references[payload];
70
+ }
40
71
  if (tag === "undefined" && value.length === 1) return undefined;
41
72
  if (value.length !== 2) return invalid();
42
73
  if (tag === "bigint" && typeof payload === "string" && /^(?:0|-?[1-9]\d*)$/.test(payload)) return BigInt(payload);
@@ -47,7 +78,7 @@ export function decodeValue(value, path = "payload") {
47
78
  const result = new Array(payload.length);
48
79
  payload.forEach((child, index) => {
49
80
  if (Array.isArray(child) && child.length === 1 && child[0] === "hole") return;
50
- result[index] = decodeValue(child, `${path}[${index}]`);
81
+ result[index] = decodeValue(child, `${path}[${index}]`, references ?? []);
51
82
  });
52
83
  return result;
53
84
  }
@@ -56,7 +87,7 @@ export function decodeValue(value, path = "payload") {
56
87
  for (const entry of payload) {
57
88
  if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || Object.hasOwn(result, entry[0])) return invalid();
58
89
  Object.defineProperty(result, entry[0], {
59
- value: decodeValue(entry[1], `${path}.${entry[0]}`), enumerable: true, writable: true, configurable: true
90
+ value: decodeValue(entry[1], `${path}.${entry[0]}`, references ?? []), enumerable: true, writable: true, configurable: true
60
91
  });
61
92
  }
62
93
  return result;
@@ -1,3 +1,9 @@
1
+ import { LEGEND_TITLE_STYLE_KEYS, validateLegendTitleAlignment } from "../grammar/legendTitleStyle.js";
2
+ import { validateAreaPaintConfig } from "../grammar/areaPaint.js";
3
+ import { validateOptionObject, validateKeys, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString, validateGeneratedItemLimit } from "../core/validation.js";
4
+ import { isContinuousColorScaleType } from "../grammar/scales/types.js";
5
+ import { validateLegendGroupState } from "../grammar/legendGroups.js";
6
+ import { validateLegendGradient } from "../grammar/legendGradient.js";
1
7
  import { normalizeTextMetricProfile } from "../core/textMetricProfile.js";
2
8
  import { isPlainObject } from "../core/immutable.js";
3
9
  import { validateUserId } from "../core/identifiers.js";
@@ -142,6 +148,44 @@ export function validateProgramState(program, path) {
142
148
  for (const key of ["resolvedScales", "materializationConfigs", "context"]) requireObject(program[key], `${path}.${key}`);
143
149
  for (const key of ["marks", "guides"]) requireObject(program.materializationConfigs[key], `${path}.materializationConfigs.${key}`);
144
150
  if (program.materializationConfigs.textMetrics !== undefined) normalizeTextMetricProfile(program.materializationConfigs.textMetrics);
151
+ if (program.guideConfigs.legendGroups !== undefined) validateLegendGroupState(program.guideConfigs.legendGroups);
152
+ for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
153
+ if (config.titleStyle !== undefined) {
154
+ validateOptionObject(config.titleStyle, LEGEND_TITLE_STYLE_KEYS, "Stored legend title style");
155
+ validateLegendTitleAlignment(config.titleStyle, config.titlePosition);
156
+ }
157
+ if (config.inferredOffset !== undefined && typeof config.inferredOffset !== "boolean") throw invalidSnapshot("Invalid legend offset inference.", path);
158
+ if (["gradient", "strokeGradient"].includes(kind) && config.representation !== "symbols") {
159
+ validateLegendGradient(config.gradient, "Stored legend gradient");
160
+ }
161
+ if (config.representation === undefined) continue;
162
+ if (kind !== "gradient" || !["symbols", "gradient"].includes(config.representation)) throw invalidSnapshot("Invalid continuous color legend representation.", path);
163
+ if (config.representation === "symbols") {
164
+ if (config.gradient !== undefined) throw invalidSnapshot("Symbol color legend cannot store gradient dimensions.", path);
165
+ requireObject(config.sampling, path);
166
+ const sample = config.sampling;
167
+ exactKeys(sample, sample.mode === "values" ? ["mode", "values", "count"] : ["mode", "count"], path);
168
+ if (!["auto", "values"].includes(sample.mode)) throw invalidSnapshot("Invalid color legend sample mode.", path);
169
+ if (!Number.isInteger(sample.count) || sample.count < 2) throw invalidSnapshot("Invalid color legend sample count.", path);
170
+ validateGeneratedItemLimit(sample.count, "Color legend sample count");
171
+ const scale = program.resolvedScales[config.scale];
172
+ if (!isContinuousColorScaleType(scale?.type)) throw invalidSnapshot("Sampled color legend requires a continuous color scale.", path);
173
+ if (sample.mode === "values") {
174
+ requireArray(sample.values, path);
175
+ if (!sample.values.length || sample.values.length > 100 || sample.values.some((value, index) => !Number.isFinite(value) || index > 0 && value <= sample.values[index - 1] || value < Math.min(...scale.domain) || value > Math.max(...scale.domain) || scale.type === "log" && value <= 0)) throw invalidSnapshot("Invalid exact color legend samples.", path);
176
+ }
177
+ requireObject(config.symbol, path);
178
+ validateKeys(config.symbol, ["type", "radius", "stroke", "strokeWidth"], "Stored sampled color symbol");
179
+ if (config.symbol.type !== "point") throw invalidSnapshot("Invalid sampled color symbol type.", path);
180
+ validatePositiveFinite(config.symbol.radius, "Stored sampled color radius");
181
+ validateNonNegativeFinite(config.symbol.strokeWidth, "Stored sampled color strokeWidth");
182
+ if (config.symbol.stroke !== undefined) validateNonEmptyString(config.symbol.stroke, "Stored sampled color stroke");
183
+ if (!["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"].includes(config.position) || !["horizontal", "vertical"].includes(config.direction) || config.layout !== "edge" || !["left", "center", "right"].includes(config.align) || !["top", "left"].includes(config.titlePosition) || config.columns !== undefined && (!Number.isInteger(config.columns) || config.columns < 1)) throw invalidSnapshot("Invalid sampled color item layout.", path);
184
+ validatePositiveFinite(config.itemGap, "Stored sampled color itemGap");
185
+ validateNonNegativeFinite(config.offset, "Stored sampled color offset");
186
+ } else if (["sampling", "symbol", "columns", "direction", "layout"].some(key => config[key] !== undefined)) throw invalidSnapshot("Gradient color legend cannot store symbol representation settings.", path);
187
+ }
188
+
145
189
  const selections = program.materializationConfigs.selections ?? {};
146
190
  requireObject(selections, `${path}.materializationConfigs.selections`);
147
191
  for (const [id, selection] of Object.entries(selections)) {
@@ -167,6 +211,9 @@ export function validateProgramState(program, path) {
167
211
  }
168
212
  for (const id of Object.keys(program.markConfigs)) {
169
213
  if (!resources.get("mark").has(id)) throw invalidSnapshot(`Mark config "${id}" has no layer owner.`, path);
214
+ if (resources.get("mark").get(id).mark.type === "area") {
215
+ validateAreaPaintConfig(program.markConfigs[id]);
216
+ }
170
217
  }
171
218
  for (const kind of ["data", "mark"]) {
172
219
  const visited = new Set();
@@ -1,9 +1,9 @@
1
1
  import { ChartProgram } from "./ChartProgram.js";
2
2
  import { BasicChartProgram } from "./BasicChartProgram.js";
3
- import { cloneAndFreeze } from "./core/immutable.js";
3
+ import { cloneAndFreeze, freezeOwned } from "./core/immutable.js";
4
4
  import { hasRegisteredExtension, isBuiltinProgramClass } from "./core/extensionRegistry.js";
5
5
  import { packageVersion } from "./version.js";
6
- import { decodeValue, encodeValue, invalidSnapshot } from "./persistence/codec.js";
6
+ import { decodeValue, encodeValue, encodeSharedValue, invalidSnapshot } from "./persistence/codec.js";
7
7
  import { exactKeys, requireObject, STATE_KEYS, validateGraphic, validateProgramState, validateTrace } from "./persistence/validation.js";
8
8
  import {
9
9
  deriveTransformSchema,
@@ -25,7 +25,7 @@ function canonicalState(program, extensions, path = "payload") {
25
25
  }
26
26
 
27
27
  function envelope(kind, payload, extensions = []) {
28
- return JSON.stringify({ schemaVersion: kind === "editable" ? 2 : 1, kind, packageVersion, extensions, payload: encodeValue(payload) });
28
+ return JSON.stringify({ schemaVersion: kind === "editable" ? 3 : 1, kind, packageVersion, extensions, payload: kind === "editable" ? encodeSharedValue(payload) : encodeValue(payload) });
29
29
  }
30
30
 
31
31
  function readEnvelope(text, kind) {
@@ -33,7 +33,7 @@ function readEnvelope(text, kind) {
33
33
  let value;
34
34
  try { value = JSON.parse(text); } catch { throw invalidSnapshot("Snapshot must be valid JSON.", "snapshot"); }
35
35
  exactKeys(value, ["schemaVersion", "kind", "packageVersion", "extensions", "payload"], "snapshot");
36
- const versions = kind === "editable" ? [1, 2] : [1];
36
+ const versions = kind === "editable" ? [1, 2, 3] : [1];
37
37
  if (!versions.includes(value.schemaVersion) || value.kind !== kind) {
38
38
  throw invalidSnapshot(
39
39
  `Unsupported snapshot schema or kind; expected schema ${versions.join(" or ")} ${kind}.`,
@@ -45,6 +45,9 @@ function readEnvelope(text, kind) {
45
45
  value.extensions.some(name => typeof name !== "string" || !hasRegisteredExtension(name)) || (kind === "graphic" && value.extensions.length)) {
46
46
  throw invalidSnapshot("Snapshot requires unknown, duplicate, or inapplicable extensions.", "snapshot.extensions");
47
47
  }
48
+ if (value.schemaVersion < 3 && value.payload?.[0] === "shared") {
49
+ throw invalidSnapshot("Shared array payloads require editable snapshot version 3.", "snapshot.payload");
50
+ }
48
51
  return { ...value, payload: decodeValue(value.payload) };
49
52
  }
50
53
 
@@ -64,7 +67,7 @@ function restoreState(state, extensions, path = "payload", { requireSchemas = fa
64
67
  if (requireSchemas) {
65
68
  for (const dataset of program.semanticSpec.datasets) {
66
69
  if (dataset.schema === undefined) {
67
- throw invalidSnapshot(`Dataset "${dataset.id}" requires schema in editable snapshot version 2.`, `${path}.semanticSpec.datasets`);
70
+ throw invalidSnapshot(`Dataset "${dataset.id}" requires schema in editable snapshot version 2 or later.`, `${path}.semanticSpec.datasets`);
68
71
  }
69
72
  validateDatasetSchema(dataset.schema);
70
73
  }
@@ -118,6 +121,15 @@ export function serializeProgram(program) {
118
121
  return envelope("editable", canonicalState(program, extensions), [...extensions].sort());
119
122
  }
120
123
 
124
+ // Decoding creates fresh plain containers. Own them once across the whole graph
125
+ // so child constructors retain shared immutable data instead of copying each pool.
126
+ function ownDecodedState(value, seen = new WeakSet()) {
127
+ if (value === null || typeof value !== "object" || seen.has(value)) return;
128
+ seen.add(value);
129
+ for (const child of Object.values(value)) ownDecodedState(child, seen);
130
+ freezeOwned(value);
131
+ }
132
+
121
133
  export function deserializeProgram(text) {
122
134
  const stored = readEnvelope(text, "editable");
123
135
  const extensions = new Set();
@@ -128,6 +140,7 @@ export function deserializeProgram(text) {
128
140
  restoreState(payload, new Set());
129
141
  payload = migrateStateSchemas(payload);
130
142
  }
143
+ ownDecodedState(payload);
131
144
  const program = restoreState(payload, extensions, "payload", { requireSchemas: true });
132
145
  if (extensions.size !== stored.extensions.length || stored.extensions.some(name => !extensions.has(name))) {
133
146
  throw invalidSnapshot("Snapshot extensions must exactly match its action traces.", "snapshot.extensions");
@@ -1,3 +1,4 @@
1
+ import { resolveCanvasSolidPaint } from "./fill.js";
1
2
  import { requireFiniteProperty } from "./validation.js";
2
3
  import { validateConcreteGraphicProperties } from
3
4
  "../../grammar/schemas/concreteGraphic.js";
@@ -25,7 +26,7 @@ function drawCircle(context, child, collectionId) {
25
26
  throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
26
27
  }
27
28
 
28
- context.fillStyle = properties.fill;
29
+ context.fillStyle = resolveCanvasSolidPaint(properties.fill);
29
30
  context.globalAlpha = opacity;
30
31
  context.beginPath();
31
32
  context.arc(x, y, radius, 0, Math.PI * 2);
@@ -44,8 +45,10 @@ function drawCircle(context, child, collectionId) {
44
45
  `Graphic "${graphicId}" requires a non-negative strokeWidth.`
45
46
  );
46
47
  }
47
- applyCanvasStroke(context, properties);
48
- context.stroke();
48
+ if (strokeWidth > 0) {
49
+ applyCanvasStroke(context, properties);
50
+ context.stroke();
51
+ }
49
52
  }
50
53
  }
51
54
 
@@ -4,10 +4,16 @@ import {
4
4
  } from "../../grammar/paint.js";
5
5
  import { resolveCanvasGradientCoordinates } from "./native.js";
6
6
 
7
+ // SVG's no-paint keyword is not a valid Canvas color assignment. Assign a real
8
+ // transparent color so the backend cannot retain a previous opaque paint.
9
+ export function resolveCanvasSolidPaint(paint) {
10
+ return paint === "none" ? "rgba(0,0,0,0)" : paint;
11
+ }
12
+
7
13
  export function applyCanvasFill(context, fill, bounds, graphicId) {
8
14
  validateFillPaint(fill, `${graphicId}.fill`);
9
15
  if (!isLinearGradientPaint(fill)) {
10
- context.fillStyle = fill;
16
+ context.fillStyle = resolveCanvasSolidPaint(fill);
11
17
  return fill;
12
18
  }
13
19
  if (typeof context.createLinearGradient !== "function") {
@@ -1,3 +1,4 @@
1
+ import { resolveCanvasSolidPaint } from "./fill.js";
1
2
  import { annotateError } from "../../core/diagnostics.js";
2
3
  import { drawCircleGraphic } from "./circle.js";
3
4
  import { drawLineGraphic } from "./line.js";
@@ -81,7 +82,7 @@ function fillCanvasBackground(context, id, canvas, width, height) {
81
82
  );
82
83
  }
83
84
  context.globalAlpha = 1;
84
- context.fillStyle = canvas.properties.background;
85
+ context.fillStyle = resolveCanvasSolidPaint(canvas.properties.background);
85
86
  context.fillRect(0, 0, width, height);
86
87
  }
87
88
 
@@ -33,6 +33,7 @@ function drawLine(context, child, collectionId) {
33
33
  throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
34
34
  }
35
35
 
36
+ if (strokeWidth === 0) return;
36
37
  context.globalAlpha = opacity;
37
38
  applyCanvasStroke(context, properties, properties.strokeDash ?? []);
38
39
  context.beginPath();
@@ -82,7 +82,7 @@ function drawPath(context, child, collectionId) {
82
82
  applyCanvasFill(context, properties.fill, bounds, graphicId);
83
83
  context.fill();
84
84
  }
85
- if (hasStroke) {
85
+ if (hasStroke && strokeWidth > 0) {
86
86
  applyCanvasStroke(context, properties, strokeDash);
87
87
  context.stroke();
88
88
  }
@@ -43,14 +43,17 @@ function drawRect(context, child, collectionId) {
43
43
  bottom: y + height
44
44
  }, graphicId);
45
45
  context.fillRect(x, y, width, height);
46
- applyCanvasStroke(context, properties);
47
- context.beginPath();
48
- context.moveTo(x, y);
49
- context.lineTo(x + width, y);
50
- context.lineTo(x + width, y + height);
51
- context.lineTo(x, y + height);
52
- context.lineTo(x, y);
53
- context.stroke();
46
+ // Canvas ignores lineWidth = 0 and would retain a previous nonzero width.
47
+ if (strokeWidth > 0) {
48
+ applyCanvasStroke(context, properties);
49
+ context.beginPath();
50
+ context.moveTo(x, y);
51
+ context.lineTo(x + width, y);
52
+ context.lineTo(x + width, y + height);
53
+ context.lineTo(x, y + height);
54
+ context.lineTo(x, y);
55
+ context.stroke();
56
+ }
54
57
  }
55
58
 
56
59
  export function drawRectGraphic(context, id, graphic) {
@@ -1,8 +1,9 @@
1
+ import { resolveCanvasSolidPaint } from "./fill.js";
1
2
  import { resolveStrokeDetails } from "../../grammar/strokeStyle.js";
2
3
 
3
4
  export function applyCanvasStroke(context, properties, strokeDash = []) {
4
5
  const details = resolveStrokeDetails(properties);
5
- context.strokeStyle = properties.stroke;
6
+ context.strokeStyle = resolveCanvasSolidPaint(properties.stroke);
6
7
  context.lineWidth = properties.strokeWidth;
7
8
  context.lineCap = details.lineCap;
8
9
  context.lineJoin = details.lineJoin;
@@ -1,3 +1,4 @@
1
+ import { resolveCanvasSolidPaint } from "./fill.js";
1
2
  import { requireFiniteProperty, requireStringProperty } from "./validation.js";
2
3
  import { validateConcreteGraphicProperties } from
3
4
  "../../grammar/schemas/concreteGraphic.js";
@@ -72,15 +73,16 @@ function drawText(context, child, collectionId) {
72
73
  context.save();
73
74
 
74
75
  try {
75
- context.fillStyle = fill;
76
+ context.fillStyle = resolveCanvasSolidPaint(fill);
76
77
  context.globalAlpha = opacity;
77
78
  const rendererFontWeight = normalizeRendererFontWeight(fontWeight);
78
- context.font = `${rendererFontWeight} ${fontSize}px ${fontFamily}`;
79
+ context.font = `${properties.fontStyle === "italic" ? "italic " : ""}${rendererFontWeight} ${fontSize}px ${fontFamily}`;
79
80
  context.textAlign = textAlign;
80
81
  context.textBaseline = textBaseline;
81
82
  context.translate(x, y);
82
83
  context.rotate(rotation);
83
- context.fillText(text, 0, 0);
84
+ if (properties.lines === undefined) context.fillText(text, 0, 0);
85
+ else for (const line of properties.lines) context.fillText(line.text, line.x, line.y);
84
86
  } finally {
85
87
  context.restore();
86
88
  }
@@ -419,13 +419,16 @@ function serializeText(state, graphicId, properties) {
419
419
  ["font-family", fontFamily],
420
420
  ["font-size", formatNumber(fontSize)],
421
421
  ["font-weight", normalizeRendererFontWeight(fontWeight)],
422
+ ["font-style", properties.fontStyle],
422
423
  ["text-anchor", TEXT_ANCHORS[textAlign]],
423
424
  ["dominant-baseline", DOMINANT_BASELINES[textBaseline]],
424
425
  ["opacity", formatNumber(opacity)],
425
426
  ["transform", rotation === 0
426
427
  ? undefined
427
428
  : `rotate(${formatNumber(rotationDegrees)} ${formatNumber(x)} ${formatNumber(y)})`]
428
- ], escapeText(text));
429
+ ], properties.lines === undefined ? escapeText(text) : properties.lines.map(line =>
430
+ element("tspan", [["x", formatNumber(x + line.x)], ["y", formatNumber(y + line.y)]], escapeText(line.text))
431
+ ).join(""));
429
432
  }
430
433
 
431
434
  function serializeConcreteGraphic(state, id, graphic) {