ggaction 0.0.24 → 0.0.26

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 (217) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-resolver.js +1 -1
  7. package/package.json +1 -1
  8. package/src/accessibility.js +13 -0
  9. package/src/actions/canvas/actions.js +2 -1
  10. package/src/actions/canvas/fitting.js +68 -13
  11. package/src/actions/charts/beeswarm.js +2 -1
  12. package/src/actions/charts/ecdf.js +2 -1
  13. package/src/actions/charts/endpoints.js +25 -16
  14. package/src/actions/charts/heatmap.js +5 -3
  15. package/src/actions/charts/missing.js +17 -0
  16. package/src/actions/charts/radar.js +70 -33
  17. package/src/actions/charts/raincloud.js +18 -4
  18. package/src/actions/charts/rug-strip.js +2 -2
  19. package/src/actions/charts/shared.js +17 -0
  20. package/src/actions/composition/actions.js +24 -7
  21. package/src/actions/composition/adjacent.js +90 -0
  22. package/src/actions/composition/index.js +11 -1
  23. package/src/actions/composition/layer.js +42 -0
  24. package/src/actions/composition/layerPlan.js +176 -0
  25. package/src/actions/composition/targets.js +105 -0
  26. package/src/actions/data/derived.js +3 -0
  27. package/src/actions/data/filter.js +1 -1
  28. package/src/actions/data/raincloudFilter.js +3 -1
  29. package/src/actions/encodings/appearance.js +26 -7
  30. package/src/actions/encodings/channels.js +1 -0
  31. package/src/actions/encodings/color/continuous.js +37 -34
  32. package/src/actions/encodings/color/index.js +57 -43
  33. package/src/actions/encodings/color/policy.js +15 -0
  34. package/src/actions/encodings/density/resolve.js +3 -2
  35. package/src/actions/encodings/density.js +2 -2
  36. package/src/actions/encodings/offset.js +2 -0
  37. package/src/actions/encodings/pathOrder.js +2 -0
  38. package/src/actions/encodings/position/areaAlignment.js +42 -0
  39. package/src/actions/encodings/position/index.js +37 -3
  40. package/src/actions/encodings/position/policies/line.js +1 -1
  41. package/src/actions/encodings/ranged.js +2 -0
  42. package/src/actions/encodings/remove.js +13 -10
  43. package/src/actions/encodings/ruleAppearance.js +14 -2
  44. package/src/actions/facets/actions.js +1 -1
  45. package/src/actions/guides/axes/axes.js +2 -1
  46. package/src/actions/guides/axes/edit.js +1 -1
  47. package/src/actions/guides/axes/labels.js +2 -1
  48. package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
  49. package/src/actions/guides/axes/parallel/policy.js +5 -2
  50. package/src/actions/guides/axes/parallel/resolve.js +3 -1
  51. package/src/actions/guides/axes/parallel.js +9 -4
  52. package/src/actions/guides/axes/titles.js +1 -1
  53. package/src/actions/guides/facadeLegend.js +3 -2
  54. package/src/actions/guides/grids/grid.js +3 -2
  55. package/src/actions/guides/grids/resolve.js +2 -1
  56. package/src/actions/guides/guides.js +3 -2
  57. package/src/actions/guides/legends/automatic.js +28 -0
  58. package/src/actions/guides/legends/blocks.js +15 -21
  59. package/src/actions/guides/legends/categorical/actions.js +1 -1
  60. package/src/actions/guides/legends/categorical/components.js +4 -3
  61. package/src/actions/guides/legends/categorical/layout.js +5 -2
  62. package/src/actions/guides/legends/categorical/options.js +9 -14
  63. package/src/actions/guides/legends/categorical/recipes.js +3 -0
  64. package/src/actions/guides/legends/categorical/resolve.js +7 -5
  65. package/src/actions/guides/legends/colorSize.js +51 -14
  66. package/src/actions/guides/legends/continuous/common.js +1 -1
  67. package/src/actions/guides/legends/continuous/gradient.js +1 -1
  68. package/src/actions/guides/legends/continuous/opacity.js +2 -0
  69. package/src/actions/guides/legends/edit.js +1 -1
  70. package/src/actions/guides/legends/focused.js +2 -2
  71. package/src/actions/guides/legends/group.js +1 -1
  72. package/src/actions/guides/legends/lane.js +1 -1
  73. package/src/actions/guides/legends/lifecycle.js +2 -2
  74. package/src/actions/guides/legends/remove.js +18 -4
  75. package/src/actions/guides/legends/size.js +1 -1
  76. package/src/actions/guides/legends/transition.js +15 -6
  77. package/src/actions/guides/polar/axes/facade.js +4 -2
  78. package/src/actions/guides/polar/axes/labels.js +6 -1
  79. package/src/actions/guides/polar/axes/shared.js +4 -4
  80. package/src/actions/guides/polar/axes/titles.js +13 -2
  81. package/src/actions/guides/polar/grids.js +6 -3
  82. package/src/actions/guides/tickValues.js +7 -1
  83. package/src/actions/index.js +2 -0
  84. package/src/actions/marks/area/actions.js +9 -2
  85. package/src/actions/marks/area/materialize.js +1 -1
  86. package/src/actions/marks/bar/materialize.js +4 -4
  87. package/src/actions/marks/lifecycle.js +14 -0
  88. package/src/actions/marks/line/actions.js +3 -0
  89. package/src/actions/marks/line/materialize.js +4 -2
  90. package/src/actions/marks/point/edit.js +2 -0
  91. package/src/actions/marks/point/jitter.js +2 -0
  92. package/src/actions/marks/point/materialize.js +5 -4
  93. package/src/actions/marks/point/packing.js +2 -0
  94. package/src/actions/marks/references.js +5 -3
  95. package/src/actions/marks/remove.js +16 -0
  96. package/src/actions/marks/shared.js +2 -1
  97. package/src/actions/marks/text/actions.js +5 -2
  98. package/src/actions/overlays/index.js +168 -0
  99. package/src/actions/overlays/snapshot.js +45 -0
  100. package/src/actions/primitives/editGraphics.js +21 -27
  101. package/src/actions/primitives/semanticAction.js +4 -5
  102. package/src/actions/primitives/semanticValidation/guide.js +1 -0
  103. package/src/actions/primitives/semanticValidation/layer.js +7 -0
  104. package/src/actions/scales/consumers/common.js +10 -0
  105. package/src/actions/scales/consumers/families.js +1 -1
  106. package/src/actions/scales/preview.js +1 -1
  107. package/src/actions/selection/actions.js +63 -11
  108. package/src/actions/titles/actions.js +3 -14
  109. package/src/actions/titles/resolve.js +12 -9
  110. package/src/core/action.js +41 -25
  111. package/src/core/compositionScales.js +12 -0
  112. package/src/core/compositionState.js +16 -2
  113. package/src/core/immutable.js +11 -6
  114. package/src/core/programState.js +16 -0
  115. package/src/core/resourceReferences.js +19 -3
  116. package/src/core/validation.js +7 -0
  117. package/src/core/vocabulary.js +8 -2
  118. package/src/grammar/actionTargets.js +24 -0
  119. package/src/grammar/bin2d.js +7 -0
  120. package/src/grammar/colorSizeLegend.js +56 -5
  121. package/src/grammar/displayLabels.js +10 -0
  122. package/src/grammar/facetHeaders.js +1 -1
  123. package/src/grammar/facets/dependencies.js +10 -3
  124. package/src/grammar/facets/guides.js +2 -1
  125. package/src/grammar/facets/legendIntent.js +7 -5
  126. package/src/grammar/legendBlocks.js +1 -1
  127. package/src/grammar/legendCombinations.js +1 -1
  128. package/src/grammar/legendGroups.js +1 -1
  129. package/src/grammar/legendItemsOffset.js +12 -0
  130. package/src/grammar/legendOverflow.js +25 -0
  131. package/src/grammar/lineSeries.js +1 -1
  132. package/src/grammar/markSelection.js +23 -0
  133. package/src/grammar/overlay.js +51 -0
  134. package/src/grammar/parallelCoordinates.js +1 -208
  135. package/src/grammar/pathCommands.js +8 -2
  136. package/src/grammar/pathSeries.js +1 -1
  137. package/src/grammar/plotComposition.js +152 -0
  138. package/src/grammar/pointShapes.js +18 -0
  139. package/src/grammar/scales/transformed.js +1 -1
  140. package/src/grammar/schemas/concreteGraphic.js +8 -1
  141. package/src/grammar/schemas/graphicBounds.js +8 -1
  142. package/src/grammar/schemas/graphicTree.js +18 -2
  143. package/src/grammar/schemas/semanticPath.js +3 -0
  144. package/src/grammar/strokeStyle.js +14 -8
  145. package/src/grammar/text.js +72 -0
  146. package/src/inspection.js +1 -1
  147. package/src/inspectionDescriptors.js +1 -1
  148. package/src/layout/composition.js +30 -4
  149. package/src/layout/compositionLegend.js +91 -0
  150. package/src/layout/facetHeaderTitles.js +1 -1
  151. package/src/layout/labels.js +55 -27
  152. package/src/layout/legend.js +3 -2
  153. package/src/layout/legendGroup.js +1 -1
  154. package/src/layout/legendItems.js +7 -6
  155. package/src/layout/legendLane.js +1 -1
  156. package/src/layout/legendText.js +1 -1
  157. package/src/materialization/adjacentSource.js +27 -0
  158. package/src/materialization/composition.js +165 -52
  159. package/src/materialization/compositionLegends.js +120 -0
  160. package/src/materialization/compositionScales.js +125 -0
  161. package/src/materialization/compositionTitle.js +96 -0
  162. package/src/materialization/facetGuides/edit.js +22 -2
  163. package/src/materialization/facetGuides/layout.js +1 -1
  164. package/src/materialization/facetGuides/owner.js +4 -3
  165. package/src/materialization/facetGuides/ownerLayout.js +12 -6
  166. package/src/materialization/facetGuides/placement.js +19 -22
  167. package/src/materialization/facetGuides/preparation.js +17 -5
  168. package/src/materialization/facetHeaders.js +5 -1
  169. package/src/materialization/facets.js +1 -1
  170. package/src/materialization/guides/layout.js +1 -1
  171. package/src/materialization/guides/resources.js +1 -1
  172. package/src/materialization/guides/visibility.js +105 -0
  173. package/src/materialization/legendClip.js +1 -1
  174. package/src/materialization/legendGroups.js +1 -1
  175. package/src/materialization/legendItemsOffset.js +68 -0
  176. package/src/materialization/legendSuppression.js +22 -0
  177. package/src/materialization/legends.js +3 -2
  178. package/src/materialization/marks/capabilities.js +4 -0
  179. package/src/materialization/marks/index.js +5 -1
  180. package/src/materialization/marks/pathOrder.js +1 -1
  181. package/src/materialization/marks/policies.js +2 -1
  182. package/src/materialization/overlayItems.js +154 -0
  183. package/src/materialization/scales/policies/binnedPosition.js +43 -5
  184. package/src/materialization/scales/policies/series.js +12 -4
  185. package/src/materialization/scales/resolve.js +2 -0
  186. package/src/materialization/selection/items/bar.js +21 -3
  187. package/src/materialization/selection/items/common.js +18 -5
  188. package/src/materialization/selection/items/overlay.js +15 -0
  189. package/src/materialization/selection/items/path.js +1 -0
  190. package/src/materialization/selection/items/point.js +2 -3
  191. package/src/materialization/selection/items/tick.js +3 -12
  192. package/src/materialization/selection/path.js +2 -2
  193. package/src/materialization/selection/policies/bar.js +2 -0
  194. package/src/materialization/selection/policies/index.js +35 -1
  195. package/src/materialization/selection/policies/point.js +2 -0
  196. package/src/materialization/selection/policies/text.js +17 -0
  197. package/src/materialization/selection/rows.js +103 -0
  198. package/src/materialization/selection/state.js +19 -3
  199. package/src/materialization/selection/styles.js +8 -0
  200. package/src/materialization/text.js +6 -489
  201. package/src/materialization/textGeometry.js +503 -0
  202. package/src/materialization/textLines.js +20 -0
  203. package/src/persistence/validation.js +97 -24
  204. package/src/renderers/canvas/index.js +12 -7
  205. package/src/renderers/canvas/text.js +5 -5
  206. package/src/renderers/nativeBackend.js +25 -0
  207. package/src/renderers/pdf.js +3 -3
  208. package/src/renderers/png.js +4 -3
  209. package/src/renderers/svgSerialization.js +1 -643
  210. package/src/scene.js +5 -3
  211. package/src/selectors/plotComposition.js +115 -0
  212. package/src/selectors/selectionTargets.js +19 -0
  213. package/src/version.js +1 -1
  214. package/types/basic.d.ts +2 -2
  215. package/types/index.d.ts +11 -0
  216. package/types/inspection.d.ts +34 -0
  217. package/types/program.d.ts +553 -365
@@ -0,0 +1,503 @@
1
+ import { resolveTextOverrides, textIdentity, textIdentityOccurrences } from "../grammar/text.js";
2
+ import { resolveBarChannels } from "../grammar/bars/policy.js";
3
+ import {
4
+ assertMarkLabelPlacementSupport,
5
+ normalizeMarkLabelPlacement,
6
+ resolveMarkLabelPlacement
7
+ } from "../layout/labels.js";
8
+ import { resolveMarkLabelValues } from "../grammar/markLabels.js";
9
+ import { formatTextValue, resolveTextLines, resolveTextOffsetDataset, textOffsetValue } from "../grammar/text.js";
10
+ import { normalizePositionDatum } from "../grammar/positionDatum.js";
11
+ import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
12
+ import { findDataset } from "../selectors/datasets.js";
13
+ import { findCoordinate } from "../selectors/coordinates.js";
14
+ import { findLayer } from "../selectors/layers.js";
15
+ import { unionConcreteGraphicBounds } from
16
+ "../grammar/schemas/graphicBounds.js";
17
+ import { findGraphic } from "../grammar/schemas/graphicTree.js";
18
+ import { polarDirection } from "../grammar/polar.js";
19
+ import { normalizeMarkSelector, selectMarkItemKeys } from "../grammar/markSelection.js";
20
+
21
+ import { mapScaleConsumerValues } from "./scales/map.js";
22
+ import { resolveRowEncodingValues } from "./rowEncoding.js";
23
+
24
+ export function sourceValue(item, source, field) {
25
+ const measure = source.mark.type === "bar" ? resolveBarChannels(source).measure
26
+ : source.mark.type === "arc" ? "radius" : undefined;
27
+ const encoding = source.encoding?.[measure];
28
+ if (encoding?.field === field && encoding.aggregate != null && item.channels[measure] !== undefined) {
29
+ return source.mark.type === "bar"
30
+ ? item.channels[`${measure}2`] ?? item.channels[measure]
31
+ : item.channels[measure];
32
+ }
33
+ if (Object.hasOwn(item.fields, field)) return item.fields[field];
34
+ for (const [channel, encoding] of Object.entries(source.encoding ?? {})) {
35
+ if (encoding?.field !== field || item.channels[channel] === undefined) continue;
36
+ if (source.mark.type === "bar" && item.channels[`${channel}2`] !== undefined) {
37
+ return item.channels[`${channel}2`];
38
+ }
39
+ return item.channels[channel];
40
+ }
41
+ if (item.members.length === 1 && Object.hasOwn(item.members[0], field)) {
42
+ return item.members[0][field];
43
+ }
44
+ if (source.mark.type === "line" && item.members.length > 0) {
45
+ const endpoint = item.members.at(-1);
46
+ if (Object.hasOwn(endpoint, field)) return endpoint[field];
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ function barAnchor(program, source, item) {
52
+ const channels = resolveBarChannels(source);
53
+ const measure = channels.measure;
54
+ const endpoint = item.channels[`${measure}2`];
55
+ const scale = program.resolvedScales[source.encoding[measure].scale];
56
+ const mapped = endpoint === undefined || scale === undefined
57
+ ? undefined
58
+ : mapScaleConsumerValues([endpoint], scale, measure)[0];
59
+ return measure === "x"
60
+ ? {
61
+ x: mapped ?? item.properties.x + item.properties.width,
62
+ y: item.properties.y + item.properties.height / 2
63
+ }
64
+ : {
65
+ x: item.properties.x + item.properties.width / 2,
66
+ y: mapped ?? item.properties.y
67
+ };
68
+ }
69
+
70
+ function sourceAnchor(program, source, item) {
71
+ if (source.mark.type === "bar") return barAnchor(program, source, item);
72
+ if (source.mark.type === "rect") {
73
+ return {
74
+ x: item.properties.x + item.properties.width / 2,
75
+ y: item.properties.y + item.properties.height / 2
76
+ };
77
+ }
78
+ if (source.mark.type === "rule") {
79
+ return {
80
+ x: item.properties.x2,
81
+ y: item.properties.y2
82
+ };
83
+ }
84
+ if (source.mark.type === "line") {
85
+ const graphicId = item.graphicIds?.[0];
86
+ const graphic = findGraphic(program.graphicSpec, graphicId)?.object;
87
+ const commands = graphic?.properties?.commands;
88
+ const endpoint = Array.isArray(commands)
89
+ ? commands.findLast(command => Number.isFinite(command.x) && Number.isFinite(command.y))
90
+ : undefined;
91
+ return { x: endpoint?.x, y: endpoint?.y };
92
+ }
93
+ return { x: item.properties.x, y: item.properties.y };
94
+ }
95
+
96
+ function explicitIntervalAxis(source) {
97
+ const axes = ["x", "y"].filter(axis =>
98
+ source.encoding?.[axis] !== undefined &&
99
+ source.encoding?.[`${axis}2`] !== undefined
100
+ );
101
+ return axes.length === 1 ? axes[0] : undefined;
102
+ }
103
+
104
+ function sourcePlacementContext(program, source) {
105
+ const coordinateType = findCoordinate(program, source.coordinate)?.type;
106
+ return {
107
+ markType: source.mark.type,
108
+ coordinateType,
109
+ intervalAxis: source.mark.type === "bar"
110
+ ? resolveBarChannels(source).measure
111
+ : source.mark.type === "rect"
112
+ ? explicitIntervalAxis(source)
113
+ : undefined
114
+ };
115
+ }
116
+
117
+ export function validateSourceMarkLabelPlacement(program, sourceId, value) {
118
+ const source = findLayer(program, sourceId);
119
+ if (source === undefined) {
120
+ throw new Error(`Unknown mark label placement source "${sourceId}".`);
121
+ }
122
+ const placement = normalizeMarkLabelPlacement(value);
123
+ assertMarkLabelPlacementSupport(sourcePlacementContext(program, source), placement);
124
+ return placement;
125
+ }
126
+
127
+ function rectangleBounds(item) {
128
+ const { x, y, width, height } = item.properties;
129
+ if (![x, y, width, height].every(Number.isFinite)) {
130
+ throw new TypeError("Interval mark label placement requires finite rectangle geometry.");
131
+ }
132
+ return {
133
+ left: Math.min(x, x + width),
134
+ right: Math.max(x, x + width),
135
+ top: Math.min(y, y + height),
136
+ bottom: Math.max(y, y + height)
137
+ };
138
+ }
139
+
140
+ function mappedChannel(program, source, item, channel) {
141
+ const value = item.channels[channel];
142
+ const primary = channel === "x2" ? "x" : channel === "y2" ? "y" : channel;
143
+ const scale = program.resolvedScales[
144
+ source.encoding?.[channel]?.scale ?? source.encoding?.[primary]?.scale
145
+ ];
146
+ return value === undefined || scale === undefined
147
+ ? undefined
148
+ : mapScaleConsumerValues([value], scale, channel)[0];
149
+ }
150
+
151
+ function scaleIncreaseDirection(program, source, axis) {
152
+ const scale = program.resolvedScales[source.encoding?.[axis]?.scale];
153
+ const domain = scale?.domain;
154
+ if (!Array.isArray(domain) || domain.length < 2) {
155
+ return axis === "x" ? { x: 1, y: 0 } : { x: 0, y: -1 };
156
+ }
157
+ const mapped = mapScaleConsumerValues(
158
+ [domain[0], domain.at(-1)],
159
+ scale,
160
+ axis
161
+ );
162
+ const delta = mapped[1] - mapped[0];
163
+ if (!Number.isFinite(delta) || delta === 0) {
164
+ return axis === "x" ? { x: 1, y: 0 } : { x: 0, y: -1 };
165
+ }
166
+ return axis === "x"
167
+ ? { x: Math.sign(delta), y: 0 }
168
+ : { x: 0, y: Math.sign(delta) };
169
+ }
170
+
171
+ function intervalPlacementGeometry(program, source, item, axis) {
172
+ const bounds = rectangleBounds(item);
173
+ const primary = mappedChannel(program, source, item, axis);
174
+ const secondary = mappedChannel(program, source, item, `${axis}2`);
175
+ if (![primary, secondary].every(Number.isFinite)) {
176
+ throw new TypeError(
177
+ `${source.mark.type === "bar" ? "Bar" : "Rect"} semantic label placement requires resolved ${axis}/${axis}2 endpoints.`
178
+ );
179
+ }
180
+ const cross = axis === "x"
181
+ ? (bounds.top + bounds.bottom) / 2
182
+ : (bounds.left + bounds.right) / 2;
183
+ const start = axis === "x" ? { x: primary, y: cross } : { x: cross, y: primary };
184
+ const end = axis === "x" ? { x: secondary, y: cross } : { x: cross, y: secondary };
185
+ const delta = { x: end.x - start.x, y: end.y - start.y };
186
+ const length = Math.hypot(delta.x, delta.y);
187
+ const endOutward = length > 0
188
+ ? { x: delta.x / length, y: delta.y / length }
189
+ : scaleIncreaseDirection(program, source, axis);
190
+ return {
191
+ kind: "interval",
192
+ center: {
193
+ x: (bounds.left + bounds.right) / 2,
194
+ y: (bounds.top + bounds.bottom) / 2
195
+ },
196
+ start: {
197
+ point: start,
198
+ outward: { x: -endOutward.x, y: -endOutward.y }
199
+ },
200
+ end: { point: end, outward: endOutward },
201
+ bounds
202
+ };
203
+ }
204
+
205
+ function pointPlacementGeometry(program, source, item) {
206
+ const center = { x: item.properties.x, y: item.properties.y };
207
+ const coordinateType = findCoordinate(program, source.coordinate)?.type;
208
+ if (coordinateType !== "polar") return { kind: "point", center };
209
+ const thetaScale = program.resolvedScales[source.encoding?.theta?.scale];
210
+ const theta = item.channels.theta === undefined || thetaScale === undefined
211
+ ? undefined
212
+ : mapScaleConsumerValues([item.channels.theta], thetaScale, "theta")[0];
213
+ const outward = Number.isFinite(theta) ? polarDirection(theta) : { x: 0, y: -1 };
214
+ const objectBounds = unionConcreteGraphicBounds(
215
+ program.graphicSpec,
216
+ item.graphicIds
217
+ , program.materializationConfigs.textMetrics);
218
+ const corners = objectBounds === undefined ? [] : [
219
+ { x: objectBounds.left, y: objectBounds.top },
220
+ { x: objectBounds.right, y: objectBounds.top },
221
+ { x: objectBounds.right, y: objectBounds.bottom },
222
+ { x: objectBounds.left, y: objectBounds.bottom }
223
+ ];
224
+ const support = Math.max(0, ...corners.map(point =>
225
+ (point.x - center.x) * outward.x + (point.y - center.y) * outward.y
226
+ ));
227
+ return {
228
+ kind: "point",
229
+ center,
230
+ end: {
231
+ point: {
232
+ x: center.x + outward.x * support,
233
+ y: center.y + outward.y * support
234
+ },
235
+ outward
236
+ }
237
+ };
238
+ }
239
+
240
+ function sourcePlacementGeometry(program, source, item) {
241
+ if (source.mark.type === "bar") {
242
+ return intervalPlacementGeometry(
243
+ program,
244
+ source,
245
+ item,
246
+ resolveBarChannels(source).measure
247
+ );
248
+ }
249
+ if (source.mark.type === "rect") {
250
+ const axis = explicitIntervalAxis(source);
251
+ return axis === undefined
252
+ ? { kind: "point", center: sourceAnchor(program, source, item) }
253
+ : intervalPlacementGeometry(program, source, item, axis);
254
+ }
255
+ if (source.mark.type === "arc") return item.geometry;
256
+ if (source.mark.type === "point") {
257
+ return pointPlacementGeometry(program, source, item);
258
+ }
259
+ return { kind: "point", center: sourceAnchor(program, source, item) };
260
+ }
261
+
262
+ function contentValue(encoding, rowOrItem, source) {
263
+ return Object.hasOwn(encoding, "field")
264
+ ? source === undefined
265
+ ? rowOrItem[encoding.field]
266
+ : sourceValue(rowOrItem, source, encoding.field)
267
+ : encoding.datum;
268
+ }
269
+
270
+ function concreteTextItem(config, position, text, { offsets = true } = {}) {
271
+ const x = position.x;
272
+ const y = position.y;
273
+ if (text === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
274
+ return undefined;
275
+ }
276
+ const lines = resolveTextLines(text, config);
277
+ return {
278
+ type: "text",
279
+ properties: {
280
+ x: x + (offsets ? config.dx : 0),
281
+ y: y + (offsets ? config.dy : 0),
282
+ text,
283
+ ...(lines === undefined ? {} : { lines }),
284
+ fill: config.fill,
285
+ opacity: config.opacity,
286
+ fontSize: config.fontSize,
287
+ fontFamily: config.fontFamily,
288
+ fontWeight: config.fontWeight,
289
+ textAlign: config.align,
290
+ textBaseline: config.baseline,
291
+ rotation: config.rotation
292
+ }
293
+ };
294
+ }
295
+
296
+ function relativeLuminance(color) {
297
+ if (typeof color !== "string" || !/^#[0-9a-f]{6}$/i.test(color)) {
298
+ return undefined;
299
+ }
300
+ const channels = [1, 3, 5].map(offset =>
301
+ Number.parseInt(color.slice(offset, offset + 2), 16) / 255
302
+ ).map(value => value <= 0.04045
303
+ ? value / 12.92
304
+ : ((value + 0.055) / 1.055) ** 2.4
305
+ );
306
+ return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
307
+ }
308
+
309
+ function sourceTextConfig(config, source, item) {
310
+ if (config.inheritColor && config.fillExplicit !== true) {
311
+ const fill = item.properties[config.inheritColor];
312
+ if (typeof fill !== "string") throw new Error(`Source mark "${source.id}" has no resolved ${config.inheritColor} color.`);
313
+ return { ...config, fill };
314
+ }
315
+ if (
316
+ !["arc", "rect"].includes(source.mark.type) ||
317
+ config.fillExplicit === true
318
+ ) return config;
319
+ const luminance = relativeLuminance(item.properties.fill);
320
+ if (luminance === undefined) return config;
321
+ return {
322
+ ...config,
323
+ fill: luminance >= 0.26 ? "#0f172a" : "#f8fafc"
324
+ };
325
+ }
326
+
327
+ function resolveSourceTextItems(program, layer, config, identities, members, resolveMarkItems) {
328
+ const source = findLayer(program, layer.source);
329
+ if (source === undefined) {
330
+ throw new Error(`Text mark "${layer.id}" requires source layer "${layer.source}".`);
331
+ }
332
+ const requested = program.markConfigs[layer.id]?.labelAuthoring?.selection ?? {
333
+ kind: "all"
334
+ };
335
+ const resolveMarkSelection = (program, target, selector) => {
336
+ const normalized = normalizeMarkSelector(selector);
337
+ const items = resolveMarkItems(program, target, normalized.grain);
338
+ return { items, keys: selectMarkItemKeys(items, normalized) };
339
+ };
340
+ let allItems;
341
+ let items;
342
+ if (requested.kind === "all") {
343
+ allItems = resolveMarkItems(program, source.id);
344
+ items = allItems;
345
+ } else if (requested.kind === "inline") {
346
+ const resolved = resolveMarkSelection(program, source.id, requested.selector);
347
+ allItems = resolved.items;
348
+ const selected = new Set(resolved.keys);
349
+ items = allItems.filter(item => selected.has(item.key));
350
+ } else if (requested.kind === "named") {
351
+ const definition = program.materializationConfigs.selections?.[requested.id];
352
+ if (definition === undefined) {
353
+ throw new Error(`Unknown label selection "${requested.id}".`);
354
+ }
355
+ if (definition.target !== source.id) {
356
+ throw new Error(
357
+ `Label selection "${requested.id}" must target source mark "${source.id}".`
358
+ );
359
+ }
360
+ const resolved = resolveMarkSelection(program, source.id, definition.selector);
361
+ allItems = resolved.items;
362
+ const selected = new Set(resolved.keys);
363
+ items = allItems.filter(item => selected.has(item.key));
364
+ } else {
365
+ throw new Error(`Unknown label selection kind "${requested.kind}".`);
366
+ }
367
+ const resolvedValues = layer.encoding.text.content === undefined ? undefined
368
+ : resolveMarkLabelValues(source, allItems, layer.encoding.text);
369
+ const values = resolvedValues === undefined ? undefined : new Map(
370
+ allItems.map((item, index) => [item.key, resolvedValues[index]])
371
+ );
372
+ const identityValues = identities ? textIdentityOccurrences(allItems) : undefined;
373
+ const identityRecords = identities ? new Map(allItems.map((item, index) => [item, identityValues[index]])) : undefined;
374
+ const overrides = resolveTextOverrides(allItems, config.textOverrides);
375
+ return items.flatMap(item => {
376
+ const identity = identityRecords?.get(item);
377
+ const provenance = members ? { members: item.members, fields: item.fields, channels: item.channels } : {};
378
+ let resolvedConfig = sourceTextConfig(config, source, item);
379
+ const text = overrides.has(item) ? overrides.get(item) : formatTextValue(
380
+ values === undefined
381
+ ? contentValue(layer.encoding.text, item, source)
382
+ : values.get(item.key),
383
+ layer.encoding.text.format
384
+ );
385
+ if (text === undefined) return [];
386
+ const placement = program.markConfigs[layer.id]?.labelAuthoring?.placement;
387
+ if (placement === undefined) {
388
+ const anchor = sourceAnchor(program, source, item);
389
+ const concrete = concreteTextItem(resolvedConfig, anchor, text);
390
+ return concrete === undefined ? [] : [{ graphic: concrete, anchor, ...identity, ...provenance }];
391
+ }
392
+ assertMarkLabelPlacementSupport(sourcePlacementContext(program, source), placement);
393
+ const resolved = resolveMarkLabelPlacement({
394
+ placement,
395
+ geometry: sourcePlacementGeometry(program, source, item),
396
+ text: {
397
+ text,
398
+ lines: resolveTextLines(text, resolvedConfig),
399
+ fontSize: resolvedConfig.fontSize,
400
+ fontFamily: resolvedConfig.fontFamily,
401
+ fontWeight: resolvedConfig.fontWeight,
402
+ textAlign: resolvedConfig.align,
403
+ textBaseline: resolvedConfig.baseline,
404
+ rotation: resolvedConfig.rotation,
405
+ dx: resolvedConfig.dx,
406
+ dy: resolvedConfig.dy
407
+ }
408
+ }, program.materializationConfigs.textMetrics);
409
+ if (!resolved.visible) return [];
410
+ if (
411
+ resolved.anchor.startsWith("outside") &&
412
+ config.fillExplicit !== true && !config.inheritColor
413
+ ) {
414
+ resolvedConfig = config;
415
+ }
416
+ const anchor = { x: resolved.sourceX, y: resolved.sourceY };
417
+ return [{
418
+ graphic: concreteTextItem(
419
+ resolvedConfig,
420
+ { x: resolved.x, y: resolved.y },
421
+ text,
422
+ { offsets: false }
423
+ ),
424
+ anchor,
425
+ ...identity,
426
+ ...provenance,
427
+ placement: resolved
428
+ }];
429
+ });
430
+ }
431
+
432
+ function resolveRowTextItems(program, layer, config, includeIdentities, members) {
433
+ const sourceDataset = findDataset(program, layer.data);
434
+ if (sourceDataset === undefined) {
435
+ throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
436
+ }
437
+ const dataset = resolveTextOffsetDataset(layer, sourceDataset);
438
+ const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text, layer.encoding?.color,
439
+ layer.encoding?.dx, layer.encoding?.dy];
440
+ const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
441
+ ? dataset.values.length
442
+ : 1;
443
+ const positionValues = channel => {
444
+ const encoding = layer.encoding?.[channel];
445
+ if (encoding === undefined) return undefined;
446
+ if (Object.hasOwn(encoding, "field")) {
447
+ return resolveRowEncodingValues(program, layer, dataset, channel);
448
+ }
449
+ const scale = program.resolvedScales[encoding.scale];
450
+ if (scale === undefined) {
451
+ throw new Error(
452
+ `text mark "${layer.id}" requires resolved ${channel} scale "${encoding.scale}".`
453
+ );
454
+ }
455
+ const datum = normalizePositionDatum(
456
+ encoding.datum,
457
+ encoding.fieldType,
458
+ channel,
459
+ encoding.temporalUnit,
460
+ "Text"
461
+ );
462
+ const mapped = ["nominal", "ordinal"].includes(encoding.fieldType)
463
+ ? mapOrdinalPositionValues([datum], scale)
464
+ : mapScaleConsumerValues([datum], scale, channel);
465
+ return Array.from({ length }, () => mapped[0]);
466
+ };
467
+ const x = positionValues("x");
468
+ const y = positionValues("y");
469
+ if (x === undefined || y === undefined) return [];
470
+ const rows = length === 1 && dataset.values.length === 0
471
+ ? [undefined]
472
+ : dataset.values.slice(0, length);
473
+ const fills = resolveRowEncodingValues(program, layer, dataset, "color");
474
+ const identities = !includeIdentities && !config.textOverrides?.length ? [] : rows.map(row => textIdentity(row ?? {}, Object.fromEntries(
475
+ Object.entries(layer.encoding).map(([channel, encoding]) => [channel,
476
+ encoding.field === undefined ? encoding.datum : row?.[encoding.field]]))));
477
+ const occurrences = includeIdentities ? textIdentityOccurrences(identities) : undefined;
478
+ const overrides = resolveTextOverrides(identities, config.textOverrides);
479
+ return rows.flatMap((row, index) => {
480
+ const identity = identities[index];
481
+ const text = overrides.has(identity) ? overrides.get(identity)
482
+ : formatTextValue(contentValue(layer.encoding.text, row), layer.encoding.text.format);
483
+ const concrete = concreteTextItem(
484
+ {
485
+ ...config,
486
+ ...(fills === undefined ? {} : { fill: fills[index] }),
487
+ dx: textOffsetValue(layer.encoding?.dx, row, config.dx),
488
+ dy: textOffsetValue(layer.encoding?.dy, row, config.dy)
489
+ },
490
+ { x: x[index], y: y[index] },
491
+ text
492
+ );
493
+ return concrete === undefined
494
+ ? []
495
+ : [{ graphic: concrete, ...occurrences?.[index], ...(members ? { members: sourceDataset.values[index] ? [sourceDataset.values[index]] : [], fields: row ?? {}, channels: Object.fromEntries(Object.entries(layer.encoding).map(([channel, encoding]) => [channel, encoding.field === undefined ? encoding.datum : row?.[encoding.field]])) } : {}), anchor: { x: x[index], y: y[index] } }];
496
+ });
497
+ }
498
+
499
+ export function resolveTextGraphicEntries(program, layer, config, { identities = false, members = false, resolveMarkItems } = {}) {
500
+ return layer.source === undefined
501
+ ? resolveRowTextItems(program, layer, config, identities, members)
502
+ : resolveSourceTextItems(program, layer, config, identities, members, resolveMarkItems);
503
+ }
@@ -1,3 +1,4 @@
1
+ import { resolveTextLines } from "../core/textMetrics.js";
1
2
  import { freezeOwned } from "../core/immutable.js";
2
3
 
3
4
  // Reconcile optional derived line geometry without recreating its text graphic.
@@ -13,3 +14,22 @@ export function materializeTextLines(program, id, lines) {
13
14
  })
14
15
  }) });
15
16
  }
17
+
18
+ // Labels retain one semantic item even when their text contains several lines.
19
+ export function materializeHardTextLines(program, id) {
20
+ const graphic = program.graphicSpec.objects[id];
21
+ if (!(graphic.items ?? [graphic]).some(item => /[\r\n]/u.test(item.properties.text) || item.properties.lines !== undefined)) return program;
22
+ const update = object => {
23
+ const p = object.properties;
24
+ if (!/[\r\n]/u.test(p.text) && p.lines === undefined) return object;
25
+ const { lines: ignored, ...properties } = p;
26
+ const lines = resolveTextLines(p.text, { fontSize: p.fontSize,
27
+ align: p.textAlign, baseline: p.textBaseline, blockAlign: "middle" });
28
+ if (lines.length > 1) properties.lines = lines;
29
+ return freezeOwned({ ...object, properties: freezeOwned(properties) });
30
+ };
31
+ const updated = graphic.items ? freezeOwned({ ...graphic, items: freezeOwned(graphic.items.map(update)) }) : update(graphic);
32
+ if (updated === graphic) return program;
33
+ return program._clone({ graphicSpec: freezeOwned({ ...program.graphicSpec,
34
+ objects: freezeOwned({ ...program.graphicSpec.objects, [id]: updated }) }) });
35
+ }