ggaction 0.0.13 → 0.0.15

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 (270) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -11
  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/package.json +8 -3
  7. package/src/actions/basic.js +2 -2
  8. package/src/actions/boxPlots/components.js +14 -4
  9. package/src/actions/boxPlots/edit.js +31 -5
  10. package/src/actions/boxPlots/materialize.js +11 -3
  11. package/src/actions/boxPlots/options.js +48 -16
  12. package/src/actions/categoryOrder/index.js +15 -4
  13. package/src/actions/charts/area.js +5 -1
  14. package/src/actions/charts/bar.js +2 -1
  15. package/src/actions/charts/density.js +6 -2
  16. package/src/actions/charts/ecdf.js +4 -1
  17. package/src/actions/charts/endpoints.js +25 -3
  18. package/src/actions/charts/heatmap.js +2 -1
  19. package/src/actions/charts/histogram.js +4 -3
  20. package/src/actions/charts/horizon.js +3 -1
  21. package/src/actions/charts/interval-regression.js +5 -2
  22. package/src/actions/charts/line.js +3 -1
  23. package/src/actions/charts/parallel.js +8 -2
  24. package/src/actions/charts/pie.js +10 -2
  25. package/src/actions/charts/polar.js +15 -4
  26. package/src/actions/charts/radar.js +8 -2
  27. package/src/actions/charts/radial.js +10 -2
  28. package/src/actions/charts/rug-strip.js +6 -2
  29. package/src/actions/charts/scatter.js +3 -1
  30. package/src/actions/composition/actions.js +13 -4
  31. package/src/actions/coordinates/actions.js +1 -1
  32. package/src/actions/coordinates/edit.js +76 -0
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/bin2d.js +57 -185
  35. package/src/actions/data/complete.js +26 -0
  36. package/src/actions/data/computed.js +2 -2
  37. package/src/actions/data/create.js +2 -2
  38. package/src/actions/data/density.js +12 -20
  39. package/src/actions/data/derived.js +47 -17
  40. package/src/actions/data/edit.js +539 -0
  41. package/src/actions/data/filter.js +14 -2
  42. package/src/actions/data/impute.js +26 -0
  43. package/src/actions/data/index.js +14 -0
  44. package/src/actions/data/normalize.js +27 -0
  45. package/src/actions/data/regression.js +16 -21
  46. package/src/actions/data/revisionBindings.js +145 -0
  47. package/src/actions/data/shared.js +20 -4
  48. package/src/actions/data/summary.js +1 -1
  49. package/src/actions/data/timeUnit.js +4 -1
  50. package/src/actions/data/window.js +1 -1
  51. package/src/actions/encodings/channels.js +415 -0
  52. package/src/actions/encodings/color/index.js +2 -1
  53. package/src/actions/encodings/density.js +5 -2
  54. package/src/actions/encodings/histogram.js +3 -2
  55. package/src/actions/encodings/index.js +4 -0
  56. package/src/actions/encodings/offset.js +20 -13
  57. package/src/actions/encodings/position/apply.js +53 -7
  58. package/src/actions/encodings/position/index.js +10 -2
  59. package/src/actions/encodings/position/policies/bar.js +9 -0
  60. package/src/actions/encodings/position/resolve.js +24 -7
  61. package/src/actions/encodings/remove.js +9 -4
  62. package/src/actions/encodings/ruleAppearance.js +1 -23
  63. package/src/actions/encodings/seriesLayout.js +1 -0
  64. package/src/actions/encodings/shared.js +82 -14
  65. package/src/actions/encodings/stroke.js +226 -0
  66. package/src/actions/errorBands/create.js +23 -3
  67. package/src/actions/errorBands/edit.js +21 -6
  68. package/src/actions/errorBars/create.js +35 -7
  69. package/src/actions/errorBars/edit.js +19 -4
  70. package/src/actions/facets/actions.js +237 -57
  71. package/src/actions/facets/derive.js +91 -26
  72. package/src/actions/gradientPlots/components.js +10 -2
  73. package/src/actions/gradientPlots/edit.js +14 -4
  74. package/src/actions/gradientPlots/materialize.js +3 -1
  75. package/src/actions/gradientPlots/options.js +10 -2
  76. package/src/actions/guides/applicability.js +8 -2
  77. package/src/actions/guides/axes/labels.js +58 -12
  78. package/src/actions/guides/axes/lines.js +6 -2
  79. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  80. package/src/actions/guides/axes/tickGroups.js +2 -1
  81. package/src/actions/guides/axes/ticks.js +6 -2
  82. package/src/actions/guides/axes/titles.js +11 -3
  83. package/src/actions/guides/grids/resolve.js +7 -2
  84. package/src/actions/guides/guides.js +19 -3
  85. package/src/actions/guides/legends/blocks.js +426 -0
  86. package/src/actions/guides/legends/categorical/actions.js +134 -48
  87. package/src/actions/guides/legends/categorical/components.js +17 -12
  88. package/src/actions/guides/legends/categorical/index.js +37 -33
  89. package/src/actions/guides/legends/categorical/layout.js +97 -15
  90. package/src/actions/guides/legends/categorical/options.js +2 -2
  91. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  92. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  93. package/src/actions/guides/legends/categorical/symbols.js +85 -30
  94. package/src/actions/guides/legends/continuous/common.js +30 -12
  95. package/src/actions/guides/legends/continuous/gradient.js +10 -7
  96. package/src/actions/guides/legends/continuous/index.js +2 -0
  97. package/src/actions/guides/legends/continuous/interval.js +8 -3
  98. package/src/actions/guides/legends/continuous/opacity.js +8 -5
  99. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  100. package/src/actions/guides/legends/creation.js +14 -1
  101. package/src/actions/guides/legends/edit.js +158 -59
  102. package/src/actions/guides/legends/index.js +2 -0
  103. package/src/actions/guides/legends/lane.js +54 -19
  104. package/src/actions/guides/legends/lifecycle.js +19 -8
  105. package/src/actions/guides/legends/remove.js +13 -3
  106. package/src/actions/guides/legends/sampling.js +140 -0
  107. package/src/actions/guides/legends/size.js +93 -40
  108. package/src/actions/guides/legends/strokeWidth.js +34 -28
  109. package/src/actions/guides/legends/target.js +112 -1
  110. package/src/actions/guides/legends/transition.js +165 -16
  111. package/src/actions/guides/polar/axes/facade.js +4 -4
  112. package/src/actions/guides/polar/axes/labels.js +24 -5
  113. package/src/actions/guides/polar/axes/lines.js +10 -4
  114. package/src/actions/guides/polar/axes/shared.js +16 -0
  115. package/src/actions/guides/polar/axes/ticks.js +1 -1
  116. package/src/actions/guides/polar/axes/titles.js +1 -1
  117. package/src/actions/guides/polar/grids.js +1 -1
  118. package/src/actions/guides/polar/resolve.js +12 -8
  119. package/src/actions/index.js +14 -3
  120. package/src/actions/marks/arc/actions.js +77 -19
  121. package/src/actions/marks/area/actions.js +34 -9
  122. package/src/actions/marks/area/materialize.js +17 -1
  123. package/src/actions/marks/bar/create.js +11 -2
  124. package/src/actions/marks/bar/edit.js +20 -3
  125. package/src/actions/marks/bar/materialize.js +22 -2
  126. package/src/actions/marks/index.js +10 -1
  127. package/src/actions/marks/line/actions.js +37 -14
  128. package/src/actions/marks/line/materialize.js +29 -10
  129. package/src/actions/marks/point/create.js +8 -2
  130. package/src/actions/marks/point/edit.js +18 -6
  131. package/src/actions/marks/point/materialize.js +49 -15
  132. package/src/actions/marks/rect/actions.js +33 -12
  133. package/src/actions/marks/references.js +601 -30
  134. package/src/actions/marks/remove.js +71 -1
  135. package/src/actions/marks/rule/actions.js +47 -10
  136. package/src/actions/marks/shared.js +1 -1
  137. package/src/actions/marks/text/actions.js +74 -9
  138. package/src/actions/marks/text/index.js +4 -0
  139. package/src/actions/marks/text/layout.js +154 -6
  140. package/src/actions/marks/text/removal.js +173 -0
  141. package/src/actions/marks/text/selection.js +73 -0
  142. package/src/actions/marks/tick/actions.js +22 -5
  143. package/src/actions/primitives/editGraphics.js +26 -0
  144. package/src/actions/primitives/semantic.js +2 -1
  145. package/src/actions/primitives/semanticAction.js +83 -10
  146. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  147. package/src/actions/primitives/semanticValidation/index.js +9 -1
  148. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  149. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  150. package/src/actions/regression/components.js +26 -11
  151. package/src/actions/regression/create.js +10 -2
  152. package/src/actions/regression/edit.js +21 -7
  153. package/src/actions/resources/index.js +7 -0
  154. package/src/actions/resources/remove.js +172 -0
  155. package/src/actions/scales/channels.js +212 -0
  156. package/src/actions/scales/consumers/common.js +1 -1
  157. package/src/actions/scales/consumers/families.js +3 -2
  158. package/src/actions/scales/consumers/index.js +19 -2
  159. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  160. package/src/actions/scales/create.js +54 -22
  161. package/src/actions/scales/definitions.js +94 -40
  162. package/src/actions/scales/edit.js +66 -11
  163. package/src/actions/scales/editPolicy.js +81 -11
  164. package/src/actions/scales/index.js +6 -0
  165. package/src/actions/scales/materialize.js +4 -3
  166. package/src/actions/scales/offset.js +48 -0
  167. package/src/actions/scales/parallel.js +71 -0
  168. package/src/actions/scales/preview.js +111 -10
  169. package/src/actions/selection/actions.js +154 -30
  170. package/src/actions/selection/index.js +3 -0
  171. package/src/actions/theme/actions.js +68 -13
  172. package/src/actions/theme/composition.js +238 -0
  173. package/src/actions/theme/index.js +9 -2
  174. package/src/actions/theme/reconcile.js +315 -67
  175. package/src/actions/theme/state.js +160 -0
  176. package/src/actions/violinPlots/create.js +5 -2
  177. package/src/actions/violinPlots/edit.js +1 -0
  178. package/src/core/action.js +10 -0
  179. package/src/core/compositionState.js +6 -1
  180. package/src/core/programState.js +20 -0
  181. package/src/core/resourceReferences.js +332 -0
  182. package/src/core/vocabulary.js +16 -3
  183. package/src/grammar/bars/aggregate.js +4 -1
  184. package/src/grammar/bars/geometry.js +44 -0
  185. package/src/grammar/bin.js +54 -9
  186. package/src/grammar/complete.js +279 -0
  187. package/src/grammar/computed.js +265 -64
  188. package/src/grammar/coordinates.js +37 -0
  189. package/src/grammar/density.js +239 -37
  190. package/src/grammar/displayLabels.js +57 -0
  191. package/src/grammar/facets/dependencies.js +133 -1
  192. package/src/grammar/facets/guides.js +3 -0
  193. package/src/grammar/facets/index.js +7 -47
  194. package/src/grammar/facets/scales.js +15 -18
  195. package/src/grammar/impute.js +340 -0
  196. package/src/grammar/markLabelSelection.js +57 -0
  197. package/src/grammar/markLabels.js +18 -1
  198. package/src/grammar/normalize.js +366 -0
  199. package/src/grammar/pathSeries.js +26 -8
  200. package/src/grammar/pointShapes.js +13 -4
  201. package/src/grammar/polar.js +90 -8
  202. package/src/grammar/regression/index.js +1 -0
  203. package/src/grammar/regression/parameters.js +23 -0
  204. package/src/grammar/roundedRect.js +180 -0
  205. package/src/grammar/rules.js +1 -0
  206. package/src/grammar/scales/colorConsumers.js +20 -1
  207. package/src/grammar/scales/continuous.js +2 -2
  208. package/src/grammar/scales/definition.js +27 -4
  209. package/src/grammar/scales/index.js +1 -0
  210. package/src/grammar/scales/ordinal.js +12 -3
  211. package/src/grammar/scales/policies.js +2 -2
  212. package/src/grammar/scales/size.js +456 -0
  213. package/src/grammar/schemas/concreteGraphic.js +14 -2
  214. package/src/grammar/schemas/graphic.js +15 -0
  215. package/src/grammar/schemas/graphicBounds.js +179 -30
  216. package/src/grammar/schemas/semanticPath.js +7 -2
  217. package/src/grammar/statisticalReference.js +103 -0
  218. package/src/grammar/strokeStyle.js +77 -0
  219. package/src/grammar/summary.js +78 -16
  220. package/src/grammar/timeUnit.js +216 -5
  221. package/src/grammar/transformTopology.js +4 -0
  222. package/src/grammar/transforms.js +394 -28
  223. package/src/grammar/weightedStatistics.js +374 -0
  224. package/src/grammar/window.js +221 -16
  225. package/src/layout/aspect.js +80 -0
  226. package/src/layout/facets.js +59 -5
  227. package/src/layout/labels.js +343 -0
  228. package/src/materialization/bars/aggregate.js +25 -2
  229. package/src/materialization/bars/histogram.js +84 -24
  230. package/src/materialization/bars/ranged.js +19 -3
  231. package/src/materialization/bars/resolve.js +7 -4
  232. package/src/materialization/composition.js +3 -1
  233. package/src/materialization/coordinateBounds.js +204 -0
  234. package/src/materialization/dependencies.js +36 -0
  235. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  236. package/src/materialization/facetGuides/placement.js +8 -2
  237. package/src/materialization/facetGuides/preparation.js +12 -7
  238. package/src/materialization/facetHeaders.js +346 -0
  239. package/src/materialization/facets.js +15 -65
  240. package/src/materialization/guides/resources.js +26 -0
  241. package/src/materialization/legends.js +1 -1
  242. package/src/materialization/marks/index.js +11 -5
  243. package/src/materialization/marks/policies.js +2 -1
  244. package/src/materialization/rect.js +25 -12
  245. package/src/materialization/rectConfig.js +5 -1
  246. package/src/materialization/scales/map.js +5 -3
  247. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  248. package/src/materialization/scales/policies/offset.js +50 -8
  249. package/src/materialization/scales/policies/series.js +4 -1
  250. package/src/materialization/scales/resolve.js +22 -6
  251. package/src/materialization/selection/items/arc.js +28 -4
  252. package/src/materialization/selection/items/bar.js +30 -3
  253. package/src/materialization/selection/items/common.js +19 -4
  254. package/src/materialization/selection/items/rect.js +28 -4
  255. package/src/materialization/selection/path.js +1 -1
  256. package/src/materialization/selection/point.js +3 -1
  257. package/src/materialization/text.js +265 -14
  258. package/src/renderers/canvas/circle.js +2 -3
  259. package/src/renderers/canvas/line.js +2 -3
  260. package/src/renderers/canvas/native.js +4 -4
  261. package/src/renderers/canvas/path.js +2 -3
  262. package/src/renderers/canvas/rect.js +2 -3
  263. package/src/renderers/canvas/stroke.js +11 -0
  264. package/src/renderers/svg.js +19 -0
  265. package/src/selectors/datasets.js +32 -1
  266. package/src/selectors/layers.js +4 -0
  267. package/src/theme/defaults.js +101 -0
  268. package/types/basic.d.ts +6 -0
  269. package/types/index.d.ts +85 -0
  270. package/types/program.d.ts +910 -117
@@ -1,5 +1,9 @@
1
1
  import { validateUserId } from "../../../core/identifiers.js";
2
- import { LEGEND_CHANNELS } from "../../../core/vocabulary.js";
2
+ import {
3
+ LEGEND_CHANNELS,
4
+ LEGEND_CONFIG_KINDS
5
+ } from "../../../core/vocabulary.js";
6
+ import { isDiscreteSizeScaleType } from "../../../grammar/scales/index.js";
3
7
 
4
8
  export function validateLegendChannels(channels, operation) {
5
9
  if (!Array.isArray(channels)) throw new TypeError(`${operation} channels must be an array.`);
@@ -35,3 +39,110 @@ export function resolveLegendTarget(program, requested, operation) {
35
39
  }
36
40
  return targets[0];
37
41
  }
42
+
43
+ const CATEGORICAL_KINDS = Object.freeze(["series", "color", "stroke"]);
44
+ const SINGLE_CHANNELS = Object.freeze({
45
+ size: "size",
46
+ opacity: "opacity",
47
+ strokeWidth: "strokeWidth",
48
+ gradient: "color",
49
+ interval: "color",
50
+ strokeGradient: "stroke",
51
+ strokeInterval: "stroke"
52
+ });
53
+
54
+ const asciiCompare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
55
+
56
+ export function legendBlockChannels(kind, config) {
57
+ const channels = CATEGORICAL_KINDS.includes(kind)
58
+ ? config?.channels
59
+ : SINGLE_CHANNELS[kind] === undefined
60
+ ? undefined
61
+ : [SINGLE_CHANNELS[kind]];
62
+ if (!Array.isArray(channels) || channels.length === 0) {
63
+ throw new Error(`Legend block ${kind} requires at least one channel.`);
64
+ }
65
+ validateLegendChannels(channels, `Legend block ${kind}`);
66
+ return Object.freeze([...channels].sort(asciiCompare));
67
+ }
68
+
69
+ export function legendBlockKey(channels) {
70
+ if (!Array.isArray(channels) || channels.length === 0) {
71
+ throw new TypeError("Legend block key requires a non-empty channel array.");
72
+ }
73
+ return JSON.stringify([...channels].sort(asciiCompare));
74
+ }
75
+
76
+ export function describeLegendBlock(program, kind, config) {
77
+ if (!LEGEND_CONFIG_KINDS.includes(kind)) {
78
+ throw new Error(`Unknown legend block kind "${kind}".`);
79
+ }
80
+ const channels = legendBlockChannels(kind, config);
81
+ const scaleIds = CATEGORICAL_KINDS.includes(kind)
82
+ ? config?.scales
83
+ : [config?.scale];
84
+ if (!Array.isArray(scaleIds) || scaleIds.length === 0 ||
85
+ scaleIds.some(id => typeof id !== "string" || program.resolvedScales[id] === undefined)) {
86
+ throw new Error(`Legend block ${kind} requires resolved scales.`);
87
+ }
88
+ const family = CATEGORICAL_KINDS.includes(kind)
89
+ ? "categorical"
90
+ : kind === "size"
91
+ ? isDiscreteSizeScaleType(program.resolvedScales[scaleIds[0]].type)
92
+ ? "discrete-size"
93
+ : "sampled"
94
+ : ["opacity", "strokeWidth"].includes(kind)
95
+ ? "sampled"
96
+ : ["gradient", "strokeGradient"].includes(kind)
97
+ ? "gradient"
98
+ : "interval";
99
+ return Object.freeze({
100
+ target: config.target,
101
+ kind,
102
+ family,
103
+ key: legendBlockKey(channels),
104
+ channels,
105
+ scaleIds: Object.freeze([...scaleIds]),
106
+ config
107
+ });
108
+ }
109
+
110
+ export function legendBlockDescriptors(program, target) {
111
+ const requested = validateUserId(target, "Legend target id");
112
+ const descriptors = LEGEND_CONFIG_KINDS.flatMap(kind => {
113
+ const config = program.guideConfigs.legend?.[kind];
114
+ return config?.target === requested
115
+ ? [describeLegendBlock(program, kind, config)]
116
+ : [];
117
+ });
118
+ const seen = new Set();
119
+ for (const descriptor of descriptors) {
120
+ if (seen.has(descriptor.key)) {
121
+ throw new Error(
122
+ `Legend target "${requested}" has ambiguous block key ${descriptor.key}.`
123
+ );
124
+ }
125
+ seen.add(descriptor.key);
126
+ }
127
+ return Object.freeze(descriptors);
128
+ }
129
+
130
+ export function resolveLegendBlock(program, { target, channel }, operation = "editLegendBlock") {
131
+ const requested = resolveLegendTarget(program, target, operation);
132
+ if (!LEGEND_CHANNELS.includes(channel)) {
133
+ throw new Error(`${operation} has unsupported legend channel "${channel}".`);
134
+ }
135
+ const matches = legendBlockDescriptors(program, requested)
136
+ .filter(descriptor => descriptor.channels.includes(channel));
137
+ if (matches.length === 0) {
138
+ throw new Error(
139
+ `${operation} target "${requested}" has no legend block for channel "${channel}".`
140
+ );
141
+ }
142
+ if (matches.length !== 1) {
143
+ throw new Error(
144
+ `${operation} target "${requested}" has ambiguous channel "${channel}".`
145
+ );
146
+ }
147
+ return matches[0];
148
+ }
@@ -2,42 +2,191 @@ import { isDiscretizedColorScaleType } from "../../../grammar/scales/types.js";
2
2
  import { normalizeContinuousLegend } from "./continuous/common.js";
3
3
  import { DEFAULT_GRADIENT_SIZE } from "./continuous/gradient.js";
4
4
  import { normalizeIntervalLegend } from "./continuous/interval.js";
5
+ import {
6
+ describeLegendBlock,
7
+ legendBlockDescriptors,
8
+ resolveLegendBlock
9
+ } from "./target.js";
10
+ import { validateLegendBlockOverride } from "./blocks.js";
5
11
 
6
- const family = type => type === "sequential" ? "gradient"
7
- : isDiscretizedColorScaleType(type) ? "interval" : undefined;
12
+ const family = (channel, type) => type === "sequential"
13
+ ? channel === "stroke" ? "strokeGradient" : "gradient"
14
+ : isDiscretizedColorScaleType(type)
15
+ ? channel === "stroke" ? "strokeInterval" : "interval"
16
+ : type === "ordinal"
17
+ ? channel === "stroke" ? "stroke" : "color"
18
+ : undefined;
8
19
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
9
20
 
10
- export function planColorLegendTransition(program, scale, nextType) {
11
- const from = family(scale.type), to = family(nextType);
21
+ function categoricalFamilyCustom(program, kind, stored) {
22
+ const side = ["left", "right"].includes(stored.position);
23
+ const direction = side ? "vertical" : "horizontal";
24
+ const itemGap = side ? 28 : stored.position === "top" ? 24 : 20;
25
+ const order = program.semanticSpec.guides.legend?.[kind]?.order;
26
+ return stored.inferredSymbol !== true ||
27
+ stored.direction !== direction ||
28
+ (stored.columns !== undefined && !(side && stored.columns === 1)) ||
29
+ stored.itemGap !== itemGap || stored.layout !== "edge" ||
30
+ order !== undefined;
31
+ }
32
+
33
+ function planChannelLegendTransition(program, scale, nextType, channel) {
34
+ const from = family(channel, scale.type), to = family(channel, nextType);
12
35
  if (from === undefined || to === undefined || from === to) return undefined;
13
36
  const stored = program.guideConfigs.legend?.[from];
14
- if (stored?.scale !== scale.id) return undefined;
37
+ if ((stored?.scale ?? stored?.scales?.[0]) !== scale.id) return undefined;
38
+ const oldDescriptor = describeLegendBlock(program, from, stored);
39
+ const overrideKeys = Object.keys(stored.blockOverrides ?? {});
40
+ if (overrideKeys.some(key => key !== oldDescriptor.key)) {
41
+ throw new Error(`${channel} legend transition found stale legend block overrides.`);
42
+ }
43
+ const blockOverride = stored.blockOverrides?.[oldDescriptor.key];
15
44
  const side = ["left", "right"].includes(stored.position);
16
- const defaults = from === "gradient" ? normalizeContinuousLegend({ position: stored.position }, "gradient")
45
+ const categorical = ["color", "stroke"].includes(from);
46
+ const gradientFamily = from.toLowerCase().endsWith("gradient");
47
+ const defaults = gradientFamily ? normalizeContinuousLegend({ position: stored.position }, "gradient")
17
48
  : normalizeIntervalLegend({ position: stored.position });
18
- const custom = from === "gradient"
49
+ const comparableSymbol = channel === "stroke" && stored.inferredStrokeWidth === true
50
+ ? { ...stored.symbol, strokeWidth: defaults.symbol?.strokeWidth }
51
+ : stored.symbol;
52
+ const custom = categorical
53
+ ? categoricalFamilyCustom(program, from, stored)
54
+ : gradientFamily
19
55
  ? stored.count !== defaults.count || !same(stored.gradient, DEFAULT_GRADIENT_SIZE) || stored.itemGap !== defaults.itemGap
20
- : !same(stored.symbol, defaults.symbol) || stored.itemGap !== defaults.itemGap ||
56
+ : !same(comparableSymbol, defaults.symbol) || stored.itemGap !== defaults.itemGap ||
21
57
  stored.direction !== defaults.direction || (stored.columns !== undefined && !(side && stored.columns === 1));
22
58
  if (custom) {
23
- throw new Error("Color legend transition cannot discard custom family settings; remove and recreate the legend explicitly.");
59
+ throw new Error(`${channel} legend transition cannot discard custom family settings; remove and recreate the legend explicitly.`);
60
+ }
61
+ if (blockOverride?.title !== undefined) {
62
+ throw new Error(`${channel} legend transition cannot distribute a legend block title; edit the destination block explicitly.`);
63
+ }
64
+ if (blockOverride?.labelMap !== undefined) {
65
+ throw new Error(`${channel} legend transition cannot distribute a legend block labelMap; edit the destination block explicitly.`);
66
+ }
67
+ if (blockOverride?.symbol !== undefined && ["gradient", "strokeGradient"].includes(to)) {
68
+ throw new Error(`${channel} legend transition cannot apply symbol overrides to a gradient block.`);
24
69
  }
25
70
  const args = {
26
- target: stored.target, channels: ["color"], position: stored.position,
71
+ target: stored.target, channels: [channel], position: stored.position,
27
72
  labels: stored.labels, titleStyle: stored.titleStyle, border: stored.border,
28
73
  align: stored.align, offset: stored.offset, titlePosition: stored.titlePosition,
29
74
  ...(stored.inferredTitle ? {} : { title: stored.title })
30
75
  };
31
76
  try {
32
- if (to === "interval") normalizeIntervalLegend(args);
33
- else normalizeContinuousLegend(args, "gradient");
77
+ if (["interval", "strokeInterval"].includes(to)) {
78
+ normalizeIntervalLegend({ ...args, channels: undefined });
79
+ }
80
+ else if (["gradient", "strokeGradient"].includes(to)) {
81
+ normalizeContinuousLegend(args, "gradient");
82
+ }
34
83
  } catch (error) {
35
- throw new Error(`Color legend transition has incompatible layout or style: ${error.message}`);
84
+ throw new Error(`${channel} legend transition has incompatible layout or style: ${error.message}`);
36
85
  }
37
- return { args, titleVisible: stored.titleVisible !== false };
86
+ return { args, channel, titleVisible: stored.titleVisible !== false, blockOverride };
87
+ }
88
+
89
+ export function planColorLegendTransitions(program, scale, nextType) {
90
+ return ["color", "stroke"].flatMap(channel => {
91
+ const plan = planChannelLegendTransition(program, scale, nextType, channel);
92
+ return plan === undefined ? [] : [plan];
93
+ });
38
94
  }
39
95
 
40
96
  export function applyColorLegendTransition(program, plan) {
41
- const next = program.createLegend(plan.args);
42
- return plan.titleVisible ? next : next.editLegend({ target: plan.args.target, title: false });
97
+ let next = program.createLegend(plan.args);
98
+ if (!plan.titleVisible) {
99
+ next = next.editLegend({ target: plan.args.target, title: false });
100
+ }
101
+ if (plan.blockOverride === undefined) return next;
102
+ const descriptor = resolveLegendBlock(next, {
103
+ target: plan.args.target,
104
+ channel: plan.channel
105
+ }, `${plan.channel} legend transition`);
106
+ validateLegendBlockOverride(descriptor, plan.blockOverride);
107
+ next = next._withLegendConfig(descriptor.kind, {
108
+ ...descriptor.config,
109
+ blockOverrides: { [descriptor.key]: plan.blockOverride }
110
+ });
111
+ return next.rematerializeLegend();
112
+ }
113
+
114
+ function intersects(left, right) {
115
+ return left.channels.some(channel => right.channels.includes(channel));
116
+ }
117
+
118
+ function explicitOrder(program, descriptor) {
119
+ if (descriptor.family !== "categorical") return undefined;
120
+ return program.semanticSpec.guides.legend?.[descriptor.kind]?.order;
121
+ }
122
+
123
+ function sameBlockValue(left, right) {
124
+ if (Object.is(left, right)) return true;
125
+ if (Array.isArray(left) && Array.isArray(right)) {
126
+ return left.length === right.length &&
127
+ left.every((value, index) => sameBlockValue(value, right[index]));
128
+ }
129
+ if (left !== null && right !== null &&
130
+ typeof left === "object" && typeof right === "object" &&
131
+ !Array.isArray(left) && !Array.isArray(right)) {
132
+ const leftKeys = Object.keys(left).sort();
133
+ const rightKeys = Object.keys(right).sort();
134
+ return leftKeys.length === rightKeys.length &&
135
+ leftKeys.every((key, index) => key === rightKeys[index] &&
136
+ sameBlockValue(left[key], right[key]));
137
+ }
138
+ return false;
139
+ }
140
+
141
+ function transitionError(target, oldDescriptors, nextDescriptor, field) {
142
+ const oldKeys = oldDescriptors.map(item => item.key).join(", ");
143
+ throw new Error(
144
+ `Legend block transition for target "${target}" from ${oldKeys} to ${nextDescriptor.key} conflicts on ${field}.`
145
+ );
146
+ }
147
+
148
+ function withoutBlockOverrides(config) {
149
+ const { blockOverrides: _removed, ...rest } = config;
150
+ return rest;
151
+ }
152
+
153
+ export function planLegendBlockTransitions(program, target, plans) {
154
+ const oldDescriptors = legendBlockDescriptors(program, target);
155
+ const nextDescriptors = plans.map(plan =>
156
+ describeLegendBlock(program, plan.kind, plan.config));
157
+ return plans.map((plan, index) => {
158
+ const descriptor = nextDescriptors[index];
159
+ const incoming = oldDescriptors.filter(old => intersects(old, descriptor));
160
+ let override;
161
+ if (incoming.length === 1 && incoming[0].key === descriptor.key) {
162
+ override = incoming[0].config.blockOverrides?.[incoming[0].key];
163
+ } else if (incoming.length === 1) {
164
+ const old = incoming[0];
165
+ override = old.config.blockOverrides?.[old.key];
166
+ if (override?.title !== undefined) {
167
+ transitionError(target, incoming, descriptor, "title");
168
+ }
169
+ if (override?.labelMap !== undefined) {
170
+ transitionError(target, incoming, descriptor, "labelMap");
171
+ }
172
+ } else if (incoming.length > 1) {
173
+ const overrides = incoming.map(old => old.config.blockOverrides?.[old.key]);
174
+ if (!overrides.every(value => sameBlockValue(value, overrides[0]))) {
175
+ transitionError(target, incoming, descriptor, "block override");
176
+ }
177
+ const orders = incoming.map(old => explicitOrder(program, old));
178
+ if (!orders.every(value => sameBlockValue(value, orders[0]))) {
179
+ transitionError(target, incoming, descriptor, "order");
180
+ }
181
+ override = overrides[0];
182
+ }
183
+ if (override !== undefined) validateLegendBlockOverride(descriptor, override);
184
+ const config = withoutBlockOverrides(plan.config);
185
+ return {
186
+ ...plan,
187
+ config: override === undefined
188
+ ? config
189
+ : { ...config, blockOverrides: { [descriptor.key]: override } }
190
+ };
191
+ });
43
192
  }
@@ -8,7 +8,7 @@ import {
8
8
  validateEnabledAxisComponents
9
9
  } from "../../axes/components.js";
10
10
  import {
11
- LABEL_EDIT_OPTIONS,
11
+ labelEditOptions,
12
12
  LINE_EDIT_OPTIONS,
13
13
  prefix,
14
14
  resolveAngle,
@@ -54,7 +54,7 @@ export function validatePolarAxisArgs(kind, args, operation) {
54
54
  if (Object.hasOwn(args.ticksAndLabels, "labels")) {
55
55
  validateObject(
56
56
  args.ticksAndLabels.labels,
57
- LABEL_EDIT_OPTIONS.filter(key => !["count", "values"].includes(key)),
57
+ labelEditOptions(kind).filter(key => !["count", "values"].includes(key)),
58
58
  `${operation}.ticksAndLabels.labels`
59
59
  );
60
60
  }
@@ -131,7 +131,7 @@ function validateAxisEditArgs(kind, args, operation) {
131
131
  validateModeOptions(args.ticks, `${operation}.ticks`);
132
132
  }
133
133
  if (Object.hasOwn(args, "labels") && args.labels !== false) {
134
- validateObject(args.labels, LABEL_EDIT_OPTIONS, `${operation}.labels`);
134
+ validateObject(args.labels, labelEditOptions(kind), `${operation}.labels`);
135
135
  validateModeOptions(args.labels, `${operation}.labels`);
136
136
  }
137
137
  if (args.ticksAndLabels !== undefined &&
@@ -155,7 +155,7 @@ function validateAxisEditArgs(kind, args, operation) {
155
155
  if (args.ticksAndLabels.labels !== undefined) {
156
156
  validateObject(
157
157
  args.ticksAndLabels.labels,
158
- LABEL_EDIT_OPTIONS.filter(key => !["count", "values"].includes(key)),
158
+ labelEditOptions(kind).filter(key => !["count", "values"].includes(key)),
159
159
  `${operation}.ticksAndLabels.labels`
160
160
  );
161
161
  }
@@ -20,8 +20,8 @@ import {
20
20
  } from "../resolve.js";
21
21
  import {
22
22
  componentResources,
23
- LABEL_CREATE_OPTIONS,
24
- LABEL_EDIT_OPTIONS,
23
+ labelCreateOptions,
24
+ labelEditOptions,
25
25
  operations,
26
26
  prefix,
27
27
  resolveAngle,
@@ -30,9 +30,11 @@ import {
30
30
  validateObject,
31
31
  withAxisSemantics
32
32
  } from "./shared.js";
33
+ import { normalizeDisplayLabelMap } from
34
+ "../../../../grammar/displayLabels.js";
33
35
 
34
36
  function labelGeometry(program, kind, config, angle = resolveAngle(program, kind, {})) {
35
- const frame = resolvePolarFrameForProgram(program);
37
+ const frame = resolvePolarFrameForProgram(program, config.coordinate);
36
38
  const mapped = mapPolarGuideValues(program, config);
37
39
  const text = formatPolarGuideValues(program, config, mapped.values);
38
40
  const geometry = {
@@ -115,6 +117,23 @@ function resolveLabelConfig(program, kind, args, resources, previous) {
115
117
  fontWeight: args.fontWeight ?? previous?.fontWeight ??
116
118
  POLAR_AXIS_DEFAULTS.labels.fontWeight
117
119
  };
120
+ const labelScale = program.resolvedScales[resources.scale];
121
+ if (Object.hasOwn(args, "labelMap")) {
122
+ if (kind !== "theta" || !["ordinal", "band", "point"].includes(labelScale?.type)) {
123
+ throw new Error(`${prefix(kind)} axis labelMap requires a categorical theta scale.`);
124
+ }
125
+ if (args.labelMap === "auto") delete config.labelMap;
126
+ else {
127
+ config.labelMap = normalizeDisplayLabelMap(
128
+ args.labelMap,
129
+ `${prefix(kind)} axis labelMap`
130
+ );
131
+ }
132
+ }
133
+ if (config.labelMap !== undefined &&
134
+ (kind !== "theta" || !["ordinal", "band", "point"].includes(labelScale?.type))) {
135
+ throw new Error(`${prefix(kind)} axis labelMap requires a categorical theta scale.`);
136
+ }
118
137
  validatePolarTickConfig(config, `${kind}-axis labels`);
119
138
  validateNonNegativeFinite(config.offset, "Polar axis label offset");
120
139
  validatePolarLabelFormat(config.format);
@@ -128,7 +147,7 @@ function makeEditLabels(kind) {
128
147
  op: operation.edit,
129
148
  description: `Edit the Polar ${kind}-axis labels.`
130
149
  }, function (args = {}) {
131
- validateObject(args, LABEL_EDIT_OPTIONS, operation.edit);
150
+ validateObject(args, labelEditOptions(kind), operation.edit);
132
151
  validateModeOptions(args, operation.edit);
133
152
  const names = polarGuideNames(kind);
134
153
  const previous = this.guideConfigs.axis?.[kind]?.labels;
@@ -172,7 +191,7 @@ function makeCreateLabels(kind) {
172
191
  op: operation.create,
173
192
  description: `Create the Polar ${kind}-axis labels.`
174
193
  }, function (args = {}) {
175
- validateComponentCreateArgs(kind, args, LABEL_CREATE_OPTIONS, operation.create);
194
+ validateComponentCreateArgs(kind, args, labelCreateOptions(kind), operation.create);
176
195
  validateModeOptions(args, operation.create);
177
196
  const names = polarGuideNames(kind);
178
197
  if (this.graphicSpec.objects[names.labels] !== undefined) {
@@ -23,8 +23,8 @@ import {
23
23
  withAxisSemantics
24
24
  } from "./shared.js";
25
25
 
26
- function lineGeometry(program, kind, angle, scale) {
27
- const frame = resolvePolarFrameForProgram(program);
26
+ function lineGeometry(program, kind, angle, scale, coordinate) {
27
+ const frame = resolvePolarFrameForProgram(program, coordinate);
28
28
  if (kind === "theta") {
29
29
  return { commands: buildPolarCircleCommands(frame, frame.availableRadius) };
30
30
  }
@@ -52,7 +52,13 @@ function makeEditLine(kind) {
52
52
  const config = { ...previous, ...args };
53
53
  validatePolarLineStyle(config, `${prefix(kind)} axis line`);
54
54
  const angle = resolveAngle(this, kind, {});
55
- const geometry = lineGeometry(this, kind, angle, previous.scale);
55
+ const geometry = lineGeometry(
56
+ this,
57
+ kind,
58
+ angle,
59
+ previous.scale,
60
+ previous.coordinate
61
+ );
56
62
  let next = this._withGuideConfig(kind, "line", config);
57
63
  for (const [property, value] of Object.entries(geometry)) {
58
64
  next = next.editGraphics({ target: names.line, property, value });
@@ -91,7 +97,7 @@ function makeCreateLine(kind) {
91
97
  lineWidth: args.lineWidth ?? POLAR_AXIS_DEFAULTS.line.lineWidth
92
98
  };
93
99
  validatePolarLineStyle(config, `${prefix(kind)} axis line`);
94
- lineGeometry(this, kind, angle, resources.scale);
100
+ lineGeometry(this, kind, angle, resources.scale, resources.coordinate);
95
101
  let next = withAxisSemantics(this, kind, resources);
96
102
  if (kind === "radius" &&
97
103
  next.guideConfigs.axis?.radius?.layout === undefined) {
@@ -26,6 +26,22 @@ export const LABEL_EDIT_OPTIONS = Object.freeze([
26
26
  "count", "values", "offset", "format", "color", "fontSize",
27
27
  "fontFamily", "fontWeight"
28
28
  ]);
29
+ const THETA_LABEL_CREATE_OPTIONS = Object.freeze([
30
+ ...LABEL_CREATE_OPTIONS,
31
+ "labelMap"
32
+ ]);
33
+ const THETA_LABEL_EDIT_OPTIONS = Object.freeze([
34
+ ...LABEL_EDIT_OPTIONS,
35
+ "labelMap"
36
+ ]);
37
+
38
+ export function labelCreateOptions(kind) {
39
+ return kind === "theta" ? THETA_LABEL_CREATE_OPTIONS : LABEL_CREATE_OPTIONS;
40
+ }
41
+
42
+ export function labelEditOptions(kind) {
43
+ return kind === "theta" ? THETA_LABEL_EDIT_OPTIONS : LABEL_EDIT_OPTIONS;
44
+ }
29
45
  export const TITLE_CREATE_OPTIONS = Object.freeze([
30
46
  "scale", "coordinate", "angle", "text", "offset", "color", "fontSize",
31
47
  "fontFamily", "fontWeight", "position"
@@ -29,7 +29,7 @@ import {
29
29
  } from "./shared.js";
30
30
 
31
31
  function tickGeometry(program, kind, config, angle = resolveAngle(program, kind, {})) {
32
- const frame = resolvePolarFrameForProgram(program);
32
+ const frame = resolvePolarFrameForProgram(program, config.coordinate);
33
33
  const mapped = mapPolarGuideValues(program, config);
34
34
  return {
35
35
  values: mapped.values,
@@ -30,7 +30,7 @@ import {
30
30
  } from "./shared.js";
31
31
 
32
32
  function titleGeometry(program, kind, config, angle = resolveAngle(program, kind, {})) {
33
- const frame = resolvePolarFrameForProgram(program);
33
+ const frame = resolvePolarFrameForProgram(program, config.coordinate);
34
34
  return kind === "theta"
35
35
  ? resolveThetaAxisTitle({ frame, offset: config.offset })
36
36
  : resolveRadialAxisTitle({
@@ -103,7 +103,7 @@ function resolveConfig(program, kind, args, resources, previous) {
103
103
  }
104
104
 
105
105
  function resolveGeometry(program, kind, config) {
106
- const frame = resolvePolarFrameForProgram(program);
106
+ const frame = resolvePolarFrameForProgram(program, config.coordinate);
107
107
  const mapped = mapPolarGuideValues(program, config);
108
108
  if (kind === "theta") {
109
109
  return { values: mapped.values, ...resolveThetaSpokes({
@@ -5,7 +5,6 @@ import {
5
5
  validateNonNegativeFinite,
6
6
  validatePositiveFinite
7
7
  } from "../../../core/validation.js";
8
- import { resolvePolarFrame } from "../../../grammar/polar.js";
9
8
  import {
10
9
  formatTransformedTick,
11
10
  isTransformedScaleType,
@@ -13,7 +12,8 @@ import {
13
12
  mapOrdinalPositionValues
14
13
  } from "../../../grammar/scales/index.js";
15
14
  import { formatTimeTick, formatTimeTicks } from "../../../grammar/ticks.js";
16
- import { resolveGraphicBounds } from "../../../layout/canvas.js";
15
+ import { resolveCoordinatePolarFrame } from
16
+ "../../../materialization/coordinateBounds.js";
17
17
  import { findCoordinate } from "../../../selectors/coordinates.js";
18
18
  import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
19
19
  "../../../theme/defaults.js";
@@ -23,6 +23,7 @@ import {
23
23
  } from "../../../grammar/schemas/graphicTree.js";
24
24
  import { valuesFromTickConfig } from "../tickValues.js";
25
25
  import { formatAxisValue, validateAxisFormat } from "../axes/policy.js";
26
+ import { resolveDisplayLabel } from "../../../grammar/displayLabels.js";
26
27
 
27
28
  export const POLAR_AXIS_DEFAULTS = Object.freeze({
28
29
  angle: 90,
@@ -160,12 +161,8 @@ export function resolvePolarGuideResources(program, kind, args, operation) {
160
161
  });
161
162
  }
162
163
 
163
- export function resolvePolarFrameForProgram(program) {
164
- const bounds = resolveGraphicBounds(program);
165
- if (bounds === undefined) {
166
- throw new Error("Polar guides require graphical Canvas bounds.");
167
- }
168
- return resolvePolarFrame(bounds);
164
+ export function resolvePolarFrameForProgram(program, coordinate) {
165
+ return resolveCoordinatePolarFrame(program, coordinate);
169
166
  }
170
167
 
171
168
  export function resolvePolarTickValues(program, config) {
@@ -203,6 +200,13 @@ export function mapPolarGuideValues(program, config) {
203
200
 
204
201
  export function formatPolarGuideValues(program, config, values) {
205
202
  const scale = program.resolvedScales[config.scale];
203
+ if (["ordinal", "band", "point"].includes(scale.type)) {
204
+ return values.map(value => resolveDisplayLabel(
205
+ value,
206
+ config.labelMap,
207
+ String
208
+ ));
209
+ }
206
210
  if (scale.type === "time" && config.format === "auto") {
207
211
  return formatTimeTicks(values, scale.domain);
208
212
  }
@@ -5,7 +5,8 @@ import { registerCategoryOrderActions } from "./categoryOrder/index.js";
5
5
  import { registerEncodingActions } from "./encodings/index.js";
6
6
  import { registerScaleActions } from "./scales/index.js";
7
7
  import { registerMarkActions } from "./marks/index.js";
8
- import { registerCoordinateActions } from "./coordinates/actions.js";
8
+ import { registerBasicCoordinateActions } from "./coordinates/actions.js";
9
+ import { registerCoordinateEditActions } from "./coordinates/edit.js";
9
10
  import { registerFacetActions } from "./facets/index.js";
10
11
  import { registerCompositionActions } from "./composition/index.js";
11
12
  import { registerTitleActions } from "./titles/index.js";
@@ -19,9 +20,17 @@ import { registerViolinPlotActions } from "./violinPlots/index.js";
19
20
  import { registerErrorBandActions } from "./errorBands/index.js";
20
21
  import { registerChartActions } from "./charts/index.js";
21
22
  import { registerThemeActions } from "./theme/index.js";
23
+ import { registerResourceActions } from "./resources/index.js";
24
+ import {
25
+ applyCompositionTheme,
26
+ removeCompositionTheme
27
+ } from "./theme/composition.js";
22
28
 
23
29
  export function registerActions(ProgramClass) {
24
- registerThemeActions(ProgramClass);
30
+ registerThemeActions(ProgramClass, {
31
+ apply: applyCompositionTheme,
32
+ remove: removeCompositionTheme
33
+ });
25
34
  registerPrimitiveActions(ProgramClass);
26
35
  registerCompositionActions(ProgramClass);
27
36
  registerFacetActions(ProgramClass);
@@ -30,7 +39,8 @@ export function registerActions(ProgramClass) {
30
39
  registerMarkActions(ProgramClass);
31
40
  registerScaleActions(ProgramClass);
32
41
  registerEncodingActions(ProgramClass);
33
- registerCoordinateActions(ProgramClass);
42
+ registerBasicCoordinateActions(ProgramClass);
43
+ registerCoordinateEditActions(ProgramClass);
34
44
  registerGuideActions(ProgramClass);
35
45
  registerTitleActions(ProgramClass);
36
46
  registerRegressionActions(ProgramClass);
@@ -42,4 +52,5 @@ export function registerActions(ProgramClass) {
42
52
  registerChartActions(ProgramClass);
43
53
  registerCategoryOrderActions(ProgramClass);
44
54
  registerSelectionActions(ProgramClass);
55
+ registerResourceActions(ProgramClass);
45
56
  }