ggaction 0.0.3 → 0.0.5

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 (223) hide show
  1. package/CHANGELOG.md +58 -3
  2. package/README.md +117 -83
  3. package/package.json +27 -11
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +31 -35
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +8 -1
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/coordinates/parallel.js +50 -0
  18. package/src/actions/data/bin2d.js +137 -0
  19. package/src/actions/data/density.js +54 -13
  20. package/src/actions/data/derived.js +22 -0
  21. package/src/actions/data/filter.js +5 -0
  22. package/src/actions/data/gradientProfile.js +65 -0
  23. package/src/actions/data/horizon.js +71 -0
  24. package/src/actions/data/index.js +29 -2
  25. package/src/actions/data/regression.js +1 -1
  26. package/src/actions/data/window.js +50 -0
  27. package/src/actions/encodings/appearance.js +1 -1
  28. package/src/actions/encodings/color/continuous.js +137 -0
  29. package/src/actions/encodings/color/index.js +167 -0
  30. package/src/actions/encodings/color/layout.js +99 -0
  31. package/src/actions/encodings/color/policy.js +88 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +193 -88
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -2
  37. package/src/actions/encodings/offset.js +1 -1
  38. package/src/actions/encodings/parallel.js +115 -0
  39. package/src/actions/encodings/pathOrder.js +131 -0
  40. package/src/actions/encodings/position/apply.js +73 -4
  41. package/src/actions/encodings/{position.js → position/index.js} +21 -8
  42. package/src/actions/encodings/position/policies/arc.js +27 -4
  43. package/src/actions/encodings/position/policies/area.js +33 -2
  44. package/src/actions/encodings/position/policies/index.js +7 -2
  45. package/src/actions/encodings/position/policies/line.js +18 -2
  46. package/src/actions/encodings/position/resolve.js +25 -3
  47. package/src/actions/encodings/ranged.js +5 -3
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/encodings/strokeDash.js +1 -1
  50. package/src/actions/encodings/text.js +1 -1
  51. package/src/actions/errorBands/options.js +1 -1
  52. package/src/actions/errorBars/options.js +1 -1
  53. package/src/actions/facets/actions.js +2 -3
  54. package/src/actions/facets/derive.js +35 -5
  55. package/src/actions/facets/guides.js +1 -1
  56. package/src/actions/facets/replay.js +7 -36
  57. package/src/actions/gradientPlots/components.js +177 -0
  58. package/src/actions/gradientPlots/create.js +96 -0
  59. package/src/actions/gradientPlots/edit.js +153 -0
  60. package/src/actions/gradientPlots/index.js +24 -0
  61. package/src/actions/gradientPlots/materialize.js +287 -0
  62. package/src/actions/gradientPlots/options.js +120 -0
  63. package/src/actions/gradientPlots/paint.js +107 -0
  64. package/src/actions/gradientPlots/rebind.js +51 -0
  65. package/src/actions/gradientPlots/resolve.js +81 -0
  66. package/src/actions/guides/applicability.js +44 -17
  67. package/src/actions/guides/axes/axes.js +37 -3
  68. package/src/actions/guides/axes/index.js +2 -0
  69. package/src/actions/guides/axes/labels.js +12 -7
  70. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  71. package/src/actions/guides/axes/parallel.js +228 -0
  72. package/src/actions/guides/axes/ticks.js +1 -1
  73. package/src/actions/guides/axes/titles.js +12 -9
  74. package/src/actions/guides/grids/resolve.js +1 -1
  75. package/src/actions/guides/guides.js +60 -5
  76. package/src/actions/guides/legends/categorical/actions.js +48 -11
  77. package/src/actions/guides/legends/categorical/layout.js +4 -2
  78. package/src/actions/guides/legends/continuous/interval.js +1 -1
  79. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  80. package/src/actions/guides/legends/edit.js +7 -20
  81. package/src/actions/guides/legends/index.js +2 -0
  82. package/src/actions/guides/legends/remove.js +6 -31
  83. package/src/actions/guides/legends/size.js +1 -1
  84. package/src/actions/guides/legends/strokeWidth.js +170 -0
  85. package/src/actions/guides/legends/target.js +25 -0
  86. package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
  87. package/src/actions/guides/polar/index.js +1 -1
  88. package/src/actions/guides/polar/resolve.js +1 -1
  89. package/src/actions/guides/tickValues.js +1 -1
  90. package/src/actions/index.js +6 -0
  91. package/src/actions/intervals/resolve.js +1 -1
  92. package/src/actions/marks/arc/actions.js +2 -2
  93. package/src/actions/marks/area/actions.js +61 -137
  94. package/src/actions/marks/area/materialize.js +163 -0
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/index.js +5 -1
  97. package/src/actions/marks/line/actions.js +100 -81
  98. package/src/actions/marks/line/materialize.js +146 -0
  99. package/src/actions/marks/point/create.js +76 -0
  100. package/src/actions/marks/point/edit.js +85 -0
  101. package/src/actions/marks/point/index.js +11 -1
  102. package/src/actions/marks/point/jitter.js +72 -0
  103. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  104. package/src/actions/marks/rect/actions.js +4 -1
  105. package/src/actions/marks/remove.js +13 -2
  106. package/src/actions/marks/rule/actions.js +26 -4
  107. package/src/actions/marks/shared.js +1 -1
  108. package/src/actions/marks/text/actions.js +20 -5
  109. package/src/actions/marks/text/index.js +7 -1
  110. package/src/actions/marks/text/layout.js +321 -0
  111. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  112. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  113. package/src/actions/primitives/semanticValidation/index.js +23 -0
  114. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  115. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  116. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  117. package/src/actions/primitives/semanticValue.js +1 -204
  118. package/src/actions/regression/components.js +1 -1
  119. package/src/actions/regression/create.js +1 -1
  120. package/src/actions/regression/edit.js +20 -22
  121. package/src/actions/scales/consumers/common.js +109 -0
  122. package/src/actions/scales/consumers/families.js +58 -0
  123. package/src/actions/scales/consumers/index.js +41 -0
  124. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
  125. package/src/actions/scales/create.js +1 -1
  126. package/src/actions/scales/definitions.js +32 -2
  127. package/src/actions/scales/edit.js +8 -206
  128. package/src/actions/scales/editPolicy.js +214 -0
  129. package/src/actions/scales/materialize.js +3 -3
  130. package/src/actions/scales/patch.js +26 -0
  131. package/src/actions/selection/actions.js +28 -3
  132. package/src/actions/violinPlots/create.js +198 -0
  133. package/src/actions/violinPlots/index.js +5 -0
  134. package/src/core/ChartProgram.js +22 -236
  135. package/src/core/compositionState.js +169 -0
  136. package/src/core/materializationState.js +65 -0
  137. package/src/core/programState.js +28 -0
  138. package/src/core/textMetrics.js +47 -0
  139. package/src/core/vocabulary.js +7 -2
  140. package/src/grammar/arcs.js +50 -13
  141. package/src/grammar/areaSeries.js +42 -6
  142. package/src/grammar/bars/aggregate.js +1 -1
  143. package/src/grammar/bin2d.js +333 -0
  144. package/src/grammar/categoricalDensity.js +192 -0
  145. package/src/grammar/coordinates.js +1 -1
  146. package/src/grammar/curveCommands.js +3 -1
  147. package/src/grammar/density.js +253 -27
  148. package/src/grammar/facets/dependencies.js +3 -11
  149. package/src/grammar/{facets.js → facets/index.js} +8 -6
  150. package/src/grammar/gradientProfile.js +213 -0
  151. package/src/grammar/horizon.js +455 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/jitter.js +197 -0
  154. package/src/grammar/lineSeries.js +70 -1
  155. package/src/grammar/paint.js +88 -0
  156. package/src/grammar/palettes.js +5 -7
  157. package/src/grammar/parallelCoordinates.js +213 -0
  158. package/src/grammar/pathOrder.js +35 -0
  159. package/src/grammar/pointShapes.js +24 -0
  160. package/src/grammar/polarLineCommands.js +1 -1
  161. package/src/grammar/regression/derive.js +97 -0
  162. package/src/grammar/regression/index.js +22 -0
  163. package/src/grammar/regression/models.js +282 -0
  164. package/src/grammar/regression/parameters.js +106 -0
  165. package/src/grammar/rules.js +2 -1
  166. package/src/grammar/scales/appearance.js +21 -0
  167. package/src/grammar/scales/color.js +4 -2
  168. package/src/grammar/schemas/concreteGraphic.js +11 -1
  169. package/src/grammar/schemas/graphicBounds.js +19 -37
  170. package/src/grammar/schemas/semanticPath.js +17 -2
  171. package/src/grammar/statistics/studentT.js +98 -0
  172. package/src/grammar/transforms.js +101 -12
  173. package/src/grammar/window.js +339 -0
  174. package/src/layout/labels.js +270 -0
  175. package/src/layout/text.js +4 -1
  176. package/src/layout/title.js +10 -20
  177. package/src/materialization/bars/aggregate.js +1 -1
  178. package/src/materialization/bars/grouped.js +1 -1
  179. package/src/materialization/bars/histogram.js +1 -1
  180. package/src/materialization/bars/ranged.js +1 -1
  181. package/src/materialization/dataProvenance.js +56 -2
  182. package/src/materialization/density.js +1 -1
  183. package/src/materialization/dependencies.js +15 -101
  184. package/src/materialization/encodings.js +2 -2
  185. package/src/materialization/facetGuides/index.js +2 -0
  186. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  187. package/src/materialization/facetGuides/placement.js +126 -0
  188. package/src/materialization/facetGuides/preparation.js +188 -0
  189. package/src/materialization/facets.js +1 -1
  190. package/src/materialization/guides/resources.js +74 -24
  191. package/src/materialization/horizon.js +29 -0
  192. package/src/materialization/layout.js +15 -0
  193. package/src/materialization/marks/capabilities.js +161 -0
  194. package/src/materialization/marks/index.js +108 -0
  195. package/src/materialization/marks/pathOrder.js +71 -0
  196. package/src/materialization/marks/policies.js +91 -0
  197. package/src/materialization/rect.js +1 -1
  198. package/src/materialization/rowEncoding.js +1 -1
  199. package/src/materialization/scaleGuideDependencies.js +102 -0
  200. package/src/materialization/scales/map.js +1 -1
  201. package/src/materialization/scales/policies/series.js +1 -1
  202. package/src/materialization/scales/resolve.js +3 -1
  203. package/src/materialization/selection/items/bar.js +1 -1
  204. package/src/materialization/selection/items/path.js +27 -1
  205. package/src/materialization/selection/items/point.js +1 -5
  206. package/src/materialization/selection/items/rect.js +21 -0
  207. package/src/materialization/selection/styles.js +21 -10
  208. package/src/materialization/text.js +12 -4
  209. package/src/renderers/canvas/fill.js +33 -0
  210. package/src/renderers/canvas/index.js +7 -0
  211. package/src/renderers/canvas/path.js +7 -4
  212. package/src/renderers/canvas/rect.js +9 -3
  213. package/src/renderers/canvas/text.js +14 -1
  214. package/src/selectors/datasets.js +4 -0
  215. package/src/theme/defaults.js +4 -0
  216. package/types/extension.d.ts +10 -1
  217. package/types/index.d.ts +76 -0
  218. package/types/program.d.ts +632 -10
  219. package/src/actions/encodings/color.js +0 -424
  220. package/src/grammar/regression.js +0 -578
  221. package/src/grammar/scales.js +0 -1
  222. package/src/materialization/facetGuides.js +0 -426
  223. package/src/materialization/marks.js +0 -316
@@ -1,4 +1,56 @@
1
- import { findDataset } from "../selectors/datasets.js";
1
+ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
+ import { validateUserId } from "../core/identifiers.js";
3
+ import { findTransformPolicy } from "../grammar/transforms.js";
4
+ import { findDataset, hasDataset } from "../selectors/datasets.js";
5
+ import { requireLayer } from "../selectors/layers.js";
6
+
7
+ function validateRevisionRole(value) {
8
+ if (typeof value !== "string" || !/^[A-Za-z][A-Za-z0-9]*$/.test(value)) {
9
+ throw new TypeError(
10
+ "Derived dataset revision role must be an alphanumeric identifier."
11
+ );
12
+ }
13
+ return value;
14
+ }
15
+
16
+ function nextRevisionId(program, owner, role) {
17
+ let revision = 1;
18
+ const prefix = `${owner}${role}Revision`;
19
+ while (hasDataset(program, `${prefix}${revision}`)) revision += 1;
20
+ return `${prefix}${revision}`;
21
+ }
22
+
23
+ export function planDerivedDataRevision(program, options = {}) {
24
+ if (!isPlainObject(options)) {
25
+ throw new TypeError("Derived dataset revision options must be a plain object.");
26
+ }
27
+ const owner = validateUserId(options.owner, "Derived dataset revision owner");
28
+ const role = validateRevisionRole(options.role);
29
+ const previous = options.previous === undefined
30
+ ? undefined
31
+ : validateUserId(options.previous, "Previous derived dataset id");
32
+ if (previous !== undefined && !hasDataset(program, previous)) {
33
+ throw new Error(`Unknown previous derived dataset "${previous}".`);
34
+ }
35
+ if (!Array.isArray(options.consumers)) {
36
+ throw new TypeError("Derived dataset revision consumers must be an array.");
37
+ }
38
+ const consumers = options.consumers.map(id => {
39
+ const validated = validateUserId(id, "Derived dataset consumer id");
40
+ requireLayer(program, validated);
41
+ return validated;
42
+ });
43
+ if (new Set(consumers).size !== consumers.length) {
44
+ throw new Error("Derived dataset revision consumers must be unique.");
45
+ }
46
+ const id = nextRevisionId(program, owner, role);
47
+ return cloneAndFreeze({
48
+ id,
49
+ ...(previous === undefined ? {} : { previous }),
50
+ rebinds: consumers.map(consumer => ({ id: consumer, data: id })),
51
+ ...(previous === undefined ? {} : { release: { id: previous } })
52
+ });
53
+ }
2
54
 
3
55
  export function findUpstreamTransform(program, dataset, type) {
4
56
  const visited = new Set();
@@ -8,7 +60,9 @@ export function findUpstreamTransform(program, dataset, type) {
8
60
  if (current.transform?.length !== 1) return undefined;
9
61
  const transform = current.transform[0];
10
62
  if (transform.type === type) return transform;
11
- if (transform.type !== "markFilter") return undefined;
63
+ if (findTransformPolicy(transform.type)?.provenanceTransparent !== true) {
64
+ return undefined;
65
+ }
12
66
  current = findDataset(program, current.source);
13
67
  }
14
68
  return undefined;
@@ -1,4 +1,4 @@
1
- import { getMarkMaterializationStep } from "./marks.js";
1
+ import { getMarkMaterializationStep } from "./marks/index.js";
2
2
  import { requireLayer } from "../selectors/layers.js";
3
3
  import { buildMaterializationPlan } from "./planner.js";
4
4
 
@@ -3,54 +3,24 @@ import {
3
3
  getExistingMarkRematerializationStep,
4
4
  getMarkMaterializationStep,
5
5
  getSourceDependentMarkSteps
6
- } from "./marks.js";
6
+ } from "./marks/index.js";
7
7
  import { requireLayer } from "../selectors/layers.js";
8
8
  import {
9
9
  applyMaterializationPlan,
10
10
  buildMaterializationPlan
11
11
  } from "./planner.js";
12
+ import { hasMaterializedLegend } from "./legends.js";
12
13
  import {
13
- hasMaterializedLegend,
14
- materializedLegendUsesScale
15
- } from "./legends.js";
16
- import { POSITION_ENCODING_CHANNELS } from "../core/vocabulary.js";
14
+ needsCanvasScaleRematerialization,
15
+ planScaleGuideRematerialization
16
+ } from "./scaleGuideDependencies.js";
17
+ import { planLayoutRematerialization } from "./layout.js";
17
18
 
18
- function usesPositionalScale(program, id) {
19
- return program.semanticSpec.layers.some(layer =>
20
- POSITION_ENCODING_CHANNELS.some(
21
- channel => layer.encoding?.[channel]?.scale === id
22
- )
23
- );
24
- }
25
-
26
- function usesRadialScale(program, id) {
27
- return program.semanticSpec.layers.some(layer =>
28
- layer.encoding?.radius?.scale === id
29
- );
30
- }
31
-
32
- function needsCanvasScaleRematerialization(program, scale) {
33
- return (
34
- (scale.range === "auto" ||
35
- usesRadialScale(program, scale.id) ||
36
- program.semanticSpec.guides.axis?.x?.scale === scale.id ||
37
- program.semanticSpec.guides.axis?.y?.scale === scale.id ||
38
- program.semanticSpec.guides.axis?.theta?.scale === scale.id ||
39
- program.semanticSpec.guides.axis?.radius?.scale === scale.id ||
40
- program.semanticSpec.guides.grid?.horizontal?.scale === scale.id ||
41
- program.semanticSpec.guides.grid?.vertical?.scale === scale.id ||
42
- program.semanticSpec.guides.grid?.theta?.scale === scale.id ||
43
- program.semanticSpec.guides.grid?.radial?.scale === scale.id) &&
44
- program.resolvedScales[scale.id] !== undefined &&
45
- usesPositionalScale(program, scale.id)
46
- );
47
- }
48
-
49
- function hasTitle(program) {
50
- return (
51
- program.semanticSpec.title.text !== undefined &&
52
- program.titleConfig !== undefined
53
- );
19
+ function layerScaleIds(layer) {
20
+ return [
21
+ ...Object.values(layer.encoding ?? {}).map(encoding => encoding?.scale),
22
+ ...(layer.encoding?.parallel?.dimensions ?? []).map(dimension => dimension.scale)
23
+ ].filter(scale => scale !== undefined);
54
24
  }
55
25
 
56
26
  export function planCanvasRematerialization(program) {
@@ -68,9 +38,7 @@ export function planCanvasRematerialization(program) {
68
38
  if (needsCanvasScaleRematerialization(program, scale)) {
69
39
  const deferredConsumers = program.semanticSpec.layers.filter(layer =>
70
40
  canDeferScaleConsumerApplication(layer) &&
71
- Object.values(layer.encoding ?? {}).some(
72
- encoding => encoding?.scale === scale.id
73
- )
41
+ layerScaleIds(layer).includes(scale.id)
74
42
  );
75
43
  const canDeferMarks = deferredConsumers.length === 0 ||
76
44
  deferredConsumers.every(layer => deferredMarkIds.has(layer.id));
@@ -92,68 +60,13 @@ export function planCanvasRematerialization(program) {
92
60
  if (hasMaterializedLegend(program)) {
93
61
  guides.push({ op: "rematerializeLegend" });
94
62
  }
95
- const layout = hasTitle(program) ? [{ op: "rematerializeTitle" }] : [];
63
+ const layout = planLayoutRematerialization(program);
96
64
  return buildMaterializationPlan({ scales, marks, guides, layout });
97
65
  }
98
66
 
99
- export function planScaleGuideRematerialization(program, id) {
100
- const guides = [];
101
- const objects = program.graphicSpec?.objects ?? {};
102
- const guideConfigs = program.guideConfigs ?? {};
103
- if (
104
- objects.xAxisLine &&
105
- program.semanticSpec.guides.axis?.x?.scale === id
106
- ) {
107
- guides.push({ op: "editXAxisLine" });
108
- }
109
- if (
110
- objects.yAxisLine &&
111
- program.semanticSpec.guides.axis?.y?.scale === id
112
- ) {
113
- guides.push({ op: "editYAxisLine" });
114
- }
115
- for (const channel of ["x", "y"]) {
116
- for (const component of ["ticks", "labels", "title"]) {
117
- if (guideConfigs.axis?.[channel]?.[component]?.scale === id) {
118
- const suffix = component[0].toUpperCase() + component.slice(1);
119
- guides.push({ op: `edit${channel.toUpperCase()}Axis${suffix}` });
120
- }
121
- }
122
- }
123
- for (const channel of ["theta", "radius"]) {
124
- const prefix = channel === "theta" ? "Theta" : "Radial";
125
- for (const component of ["line", "ticks", "labels", "title"]) {
126
- if (guideConfigs.axis?.[channel]?.[component]?.scale === id) {
127
- const suffix = component[0].toUpperCase() + component.slice(1);
128
- guides.push({ op: `edit${prefix}Axis${suffix}` });
129
- }
130
- }
131
- }
132
- if (guideConfigs.grid?.horizontal?.scale === id) {
133
- guides.push({ op: "rematerializeHorizontalGrid" });
134
- }
135
- if (guideConfigs.grid?.vertical?.scale === id) {
136
- guides.push({ op: "rematerializeVerticalGrid" });
137
- }
138
- if (guideConfigs.grid?.theta?.scale === id) {
139
- guides.push({ op: "rematerializeThetaGrid" });
140
- }
141
- if (guideConfigs.grid?.radial?.scale === id) {
142
- guides.push({ op: "rematerializeRadialGrid" });
143
- }
144
- if (materializedLegendUsesScale(program, id)) {
145
- guides.push({ op: "rematerializeLegend" });
146
- }
147
- return buildMaterializationPlan({ guides });
148
- }
149
-
150
67
  export function planLayerDataRematerialization(program, id) {
151
68
  const layer = requireLayer(program, id);
152
- const scaleIds = [...new Set(
153
- Object.values(layer.encoding ?? {})
154
- .map(encoding => encoding?.scale)
155
- .filter(scale => scale !== undefined)
156
- )];
69
+ const scaleIds = [...new Set(layerScaleIds(layer))];
157
70
  const markStep = getMarkMaterializationStep(program, layer);
158
71
  const scales = scaleIds.map(scale => ({
159
72
  op: "rematerializeScale",
@@ -187,3 +100,4 @@ export function applyLayerDataRematerialization(program, id) {
187
100
  }
188
101
 
189
102
  export { applyMaterializationPlan } from "./planner.js";
103
+ export { planScaleGuideRematerialization };
@@ -2,9 +2,9 @@ import { hasMaterializedLegend } from "./legends.js";
2
2
  import { requireLayer } from "../selectors/layers.js";
3
3
  import {
4
4
  getEncodingMaterializationStages
5
- } from "./marks.js";
5
+ } from "./marks/index.js";
6
6
  import { buildMaterializationPlan } from "./planner.js";
7
- import { getSourceDependentMarkSteps } from "./marks.js";
7
+ import { getSourceDependentMarkSteps } from "./marks/index.js";
8
8
 
9
9
  export function planEncodingRematerialization(program, {
10
10
  target,
@@ -0,0 +1,2 @@
1
+ export { prepareSharedFacetLegend } from "./preparation.js";
2
+ export { applyFacetGuideComposition } from "./placement.js";
@@ -0,0 +1,125 @@
1
+ import {
2
+ resolveSharedFacetLegends
3
+ } from "../../grammar/facets/guides.js";
4
+ import { mapOrdinalValues } from "../../grammar/scales/index.js";
5
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
6
+
7
+ function textItem(text, x, y, options = {}) {
8
+ return {
9
+ type: "text",
10
+ properties: {
11
+ x,
12
+ y,
13
+ text: String(text),
14
+ fill: options.color ?? DEFAULT_COLORS.strongText,
15
+ fontSize: options.fontSize ?? 12,
16
+ fontFamily: options.fontFamily ?? DEFAULT_FONT_FAMILY,
17
+ fontWeight: options.fontWeight ?? "normal",
18
+ textAlign: options.textAlign ?? "left",
19
+ textBaseline: options.textBaseline ?? "middle"
20
+ }
21
+ };
22
+ }
23
+
24
+ function collection(program, id, items) {
25
+ return program
26
+ .createGraphics({ id, type: "collection", parent: "canvas" })
27
+ .editGraphics({ target: id, property: "items", value: items });
28
+ }
29
+
30
+ export function resolveLegacyCategoricalLegend(program) {
31
+ const encodings = program.semanticSpec.layers.flatMap(layer =>
32
+ layer.encoding?.color === undefined
33
+ ? []
34
+ : [{ layer, encoding: layer.encoding.color }]
35
+ );
36
+ if (encodings.length === 0) return undefined;
37
+ const fields = new Set(encodings.map(entry => entry.encoding.field));
38
+ const scales = new Set(encodings.map(entry => entry.encoding.scale));
39
+ const marks = new Set(encodings.map(entry => entry.layer.mark?.type));
40
+ if (fields.size !== 1 || scales.size !== 1 || marks.size !== 1) {
41
+ throw new Error(
42
+ "Shared facet legend requires one compatible color field, scale, and mark recipe."
43
+ );
44
+ }
45
+ if (encodings.some(entry =>
46
+ !["nominal", "ordinal"].includes(entry.encoding.fieldType)
47
+ )) {
48
+ return undefined;
49
+ }
50
+ const firstId = program.compositionSpec.children[0];
51
+ const scaleId = encodings[0].encoding.scale;
52
+ const scale = program.children[firstId].resolvedScales[scaleId];
53
+ if (scale?.type !== "ordinal") return undefined;
54
+ const config = {
55
+ kind: "color",
56
+ target: encodings[0].layer.id,
57
+ field: encodings[0].encoding.field,
58
+ scale: scaleId,
59
+ title: encodings[0].encoding.field
60
+ };
61
+ const compatibility = resolveSharedFacetLegends(
62
+ program.compositionSpec.children.map(id => ({
63
+ id,
64
+ guideConfigs: { legend: { color: config } },
65
+ resolvedScales: program.children[id].resolvedScales
66
+ }))
67
+ );
68
+ return {
69
+ mode: "legacyCategorical",
70
+ compatibility,
71
+ reservation: { gap: 18, width: 132 },
72
+ legend: {
73
+ field: config.field,
74
+ mark: encodings[0].layer.mark.type,
75
+ domain: scale.domain,
76
+ colors: mapOrdinalValues(scale.domain, scale.domain, scale.range)
77
+ }
78
+ };
79
+ }
80
+
81
+ export function materializeLegacyCategoricalLegend(program, prepared, layout) {
82
+ const legend = prepared.legend;
83
+ const items = [textItem(legend.field, layout.legend.x, layout.legend.y, {
84
+ fontSize: 12,
85
+ fontWeight: 700,
86
+ textBaseline: "top"
87
+ })];
88
+ legend.domain.forEach((value, index) => {
89
+ const y = layout.legend.y + 29 + index * 26;
90
+ items.push(legend.mark === "point"
91
+ ? {
92
+ type: "circle",
93
+ properties: {
94
+ x: layout.legend.x + 7,
95
+ y,
96
+ radius: 5.5,
97
+ fill: legend.colors[index],
98
+ stroke: "#ffffff",
99
+ strokeWidth: 0.35,
100
+ opacity: 1
101
+ }
102
+ }
103
+ : {
104
+ type: "rect",
105
+ properties: {
106
+ x: layout.legend.x + 1,
107
+ y: y - 6,
108
+ width: 12,
109
+ height: 12,
110
+ fill: legend.colors[index],
111
+ stroke: "#ffffff",
112
+ strokeWidth: 0.6,
113
+ opacity: 1
114
+ }
115
+ });
116
+ items.push(textItem(value, layout.legend.x + 22, y, {
117
+ fontSize: 10.5
118
+ }));
119
+ });
120
+ return collection(
121
+ program,
122
+ `${program.compositionSpec.id}-legend`,
123
+ items
124
+ );
125
+ }
@@ -0,0 +1,126 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+ import { planFacetGuideOwnership } from "../../grammar/facets/guides.js";
3
+ import {
4
+ axisGraphicIds,
5
+ allLegendGraphicIds
6
+ } from "../guides/resources.js";
7
+ import { attachSnapshotObject } from "../composition.js";
8
+ import { namespaceGraphicId } from "../compositionSnapshot.js";
9
+ import {
10
+ materializeLegacyCategoricalLegend
11
+ } from "./legacyCategorical.js";
12
+ import { legendKinds, prepareSharedFacetLegend } from "./preparation.js";
13
+
14
+ function copyLegendConfig(program, source) {
15
+ let next = program;
16
+ for (const [kind, config] of Object.entries(source.guideConfigs.legend ?? {})) {
17
+ next = next._withLegendConfig(kind, config);
18
+ }
19
+ return next;
20
+ }
21
+
22
+ function removeNamespacedGraphics(program, namespace, ids) {
23
+ let next = program;
24
+ for (const id of ids) {
25
+ const target = namespaceGraphicId(namespace, id);
26
+ if (next.graphicSpec.objects[target] !== undefined) {
27
+ next = next.editGraphics({ target, remove: true });
28
+ }
29
+ }
30
+ return next;
31
+ }
32
+
33
+ function gradientAnchor(prepared, plot, layout) {
34
+ const strips = prepared.source.graphicSpec.objects.colorGradientStrips;
35
+ if (strips?.items?.[0] === undefined) return undefined;
36
+ const properties = strips.items[0].properties;
37
+ const length = prepared.source.guideConfigs.legend.gradient.gradient.length;
38
+ return {
39
+ sourceX: properties.x,
40
+ sourceY: properties.y,
41
+ targetX: layout.legend.x,
42
+ targetY: plot.y + (plot.height - length) / 2
43
+ };
44
+ }
45
+
46
+ function legendTranslation(prepared, plot, layout) {
47
+ const gradient = gradientAnchor(prepared, plot, layout);
48
+ if (gradient !== undefined) {
49
+ return {
50
+ x: gradient.targetX - gradient.sourceX,
51
+ y: gradient.targetY - gradient.sourceY
52
+ };
53
+ }
54
+ const height = prepared.bounds.bottom - prepared.bounds.top;
55
+ return {
56
+ x: layout.legend.x - prepared.bounds.left,
57
+ y: plot.y + Math.max(0, (plot.height - height) / 2) - prepared.bounds.top
58
+ };
59
+ }
60
+
61
+ function attachParentLegend(program, prepared, plot, layout) {
62
+ const translation = legendTranslation(prepared, plot, layout);
63
+ const canvas = prepared.source.graphicSpec.objects.canvas;
64
+ const id = `${program.compositionSpec.id}-shared-legend`;
65
+ let next = copyLegendConfig(program, prepared.source)
66
+ .createGraphics({ id, type: "canvas", parent: "canvas" });
67
+ for (const [property, value] of Object.entries({
68
+ x: translation.x,
69
+ y: translation.y,
70
+ width: canvas.properties.width,
71
+ height: canvas.properties.height,
72
+ background: "transparent"
73
+ })) {
74
+ next = next.editGraphics({ target: id, property, value });
75
+ }
76
+ for (const root of prepared.roots) {
77
+ next = attachSnapshotObject(next, prepared.source.graphicSpec, root, id);
78
+ }
79
+ return next;
80
+ }
81
+
82
+ export function applyFacetGuideComposition(program, { layout, plot } = {}) {
83
+ if (!isPlainObject(layout) || !Array.isArray(layout.children)) {
84
+ throw new TypeError("Facet guide composition requires a resolved layout.");
85
+ }
86
+ if (!isPlainObject(plot)) {
87
+ throw new TypeError("Facet guide composition requires resolved plot bounds.");
88
+ }
89
+ const prepared = prepareSharedFacetLegend(program);
90
+ const children = layout.children.map(cell => {
91
+ const child = program.children[cell.id];
92
+ return {
93
+ id: cell.id,
94
+ axes: Object.keys(child.guideConfigs.axis ?? {})
95
+ .filter(channel => ["x", "y"].includes(channel)),
96
+ legendKinds: legendKinds(child)
97
+ };
98
+ });
99
+ const ownership = planFacetGuideOwnership({
100
+ placements: layout.children,
101
+ children,
102
+ axes: program.compositionSpec.facet.guides.axes,
103
+ legend: program.compositionSpec.facet.guides.legend,
104
+ ...(prepared === undefined ? {} : { sharedLegends: prepared.compatibility })
105
+ });
106
+ let next = program;
107
+ for (const cell of layout.children) {
108
+ const namespace = `${program.compositionSpec.id}-${cell.id}`;
109
+ const childPlan = ownership.children[cell.id];
110
+ for (const channel of childPlan.removeAxes) {
111
+ next = removeNamespacedGraphics(next, namespace, axisGraphicIds(channel));
112
+ }
113
+ if (childPlan.removeLegends.length > 0) {
114
+ next = removeNamespacedGraphics(
115
+ next,
116
+ namespace,
117
+ allLegendGraphicIds(childPlan.removeLegends)
118
+ );
119
+ }
120
+ }
121
+ if (prepared === undefined) return next;
122
+ if (prepared.mode === "legacyCategorical") {
123
+ return materializeLegacyCategoricalLegend(next, prepared, layout);
124
+ }
125
+ return attachParentLegend(next, prepared, plot, layout);
126
+ }
@@ -0,0 +1,188 @@
1
+ import {
2
+ unionConcreteGraphicBounds
3
+ } from "../../grammar/schemas/graphicBounds.js";
4
+ import {
5
+ resolveSharedFacetLegends
6
+ } from "../../grammar/facets/guides.js";
7
+ import { findLayer } from "../../selectors/layers.js";
8
+ import { findSemanticScale } from "../../selectors/scales.js";
9
+ import { allLegendGraphicIds } from "../guides/resources.js";
10
+ import { DEFAULT_COLORS } from "../../theme/defaults.js";
11
+ import { resolveLegacyCategoricalLegend } from "./legacyCategorical.js";
12
+
13
+ const LEGEND_SOURCE_CANVAS = Object.freeze({
14
+ width: 640,
15
+ height: 400,
16
+ margin: Object.freeze({ top: 40, right: 170, bottom: 60, left: 70 })
17
+ });
18
+
19
+ export function legendKinds(program) {
20
+ return Object.keys(program.guideConfigs.legend ?? {});
21
+ }
22
+
23
+ function legendCandidates(program) {
24
+ const layers = program.semanticSpec.layers;
25
+ const color = layers.filter(layer => {
26
+ const encoding = layer.encoding?.color;
27
+ return encoding?.scale !== undefined &&
28
+ findSemanticScale(program, encoding.scale) !== undefined;
29
+ });
30
+ if (color.length === 1) {
31
+ const layer = color[0];
32
+ return {
33
+ target: layer.id,
34
+ channels: ["color", "shape", "strokeDash"].filter(
35
+ channel => layer.encoding?.[channel]?.scale !== undefined
36
+ )
37
+ };
38
+ }
39
+ if (color.length > 1) {
40
+ throw new Error(
41
+ "Shared facet legend requires one unambiguous color legend target."
42
+ );
43
+ }
44
+ for (const channel of ["shape", "strokeDash", "size", "opacity"]) {
45
+ const candidates = layers.filter(
46
+ layer => layer.encoding?.[channel]?.scale !== undefined
47
+ );
48
+ if (candidates.length === 1) {
49
+ return { target: candidates[0].id, channels: [channel] };
50
+ }
51
+ if (candidates.length > 1) {
52
+ throw new Error(
53
+ `Shared facet legend requires one unambiguous ${channel} legend target.`
54
+ );
55
+ }
56
+ }
57
+ throw new Error("Shared facet legend requires one eligible encoded scale.");
58
+ }
59
+
60
+ function compactNumber(value) {
61
+ if (Math.abs(value) >= 1_000_000_000) {
62
+ return `${+(value / 1_000_000_000).toFixed(1)}B`;
63
+ }
64
+ if (Math.abs(value) >= 1_000_000) {
65
+ return `${Math.round(value / 1_000_000)}M`;
66
+ }
67
+ if (Math.abs(value) >= 1_000) {
68
+ return `${Math.round(value / 1_000)}K`;
69
+ }
70
+ return String(+value.toPrecision(3));
71
+ }
72
+
73
+ function continuousValues(domain, count) {
74
+ return Array.from(
75
+ { length: count },
76
+ (_, index) => domain[0] + index / (count - 1) * (domain[1] - domain[0])
77
+ );
78
+ }
79
+
80
+ function prepareAutoLegendSource(child) {
81
+ const request = legendCandidates(child);
82
+ const canvas = child.graphicSpec.objects.canvas;
83
+ let source = child
84
+ ._withCanvasConfig({
85
+ margin: LEGEND_SOURCE_CANVAS.margin,
86
+ size: { width: "explicit", height: "explicit" }
87
+ })
88
+ .editGraphics({
89
+ target: "canvas",
90
+ property: "width",
91
+ value: LEGEND_SOURCE_CANVAS.width
92
+ })
93
+ .editGraphics({
94
+ target: "canvas",
95
+ property: "height",
96
+ value: LEGEND_SOURCE_CANVAS.height
97
+ });
98
+ if (canvas?.properties.background !== undefined) {
99
+ source = source.editGraphics({
100
+ target: "canvas",
101
+ property: "background",
102
+ value: canvas.properties.background
103
+ });
104
+ }
105
+ const scale = findSemanticScale(
106
+ source,
107
+ findLayer(source, request.target)?.encoding?.color?.scale
108
+ );
109
+ const gradient = scale?.type === "sequential";
110
+ source = source.createLegend({
111
+ target: request.target,
112
+ channels: request.channels,
113
+ ...(gradient ? {
114
+ count: 5,
115
+ gradient: { length: 180, thickness: 12 },
116
+ labels: { offset: 9, fontSize: 10 },
117
+ titleStyle: { color: DEFAULT_COLORS.strongText, fontSize: 11 }
118
+ } : {})
119
+ });
120
+ if (gradient) {
121
+ const resolved = source.resolvedScales[scale.id];
122
+ source = source
123
+ .editGraphics({
124
+ target: "colorGradientLabels",
125
+ property: "text",
126
+ value: continuousValues(resolved.domain, 5).map(compactNumber)
127
+ })
128
+ .editGraphics({
129
+ target: "colorGradientTitle",
130
+ property: "y",
131
+ value: source.graphicSpec.objects.colorGradientTitle.properties.y + 8
132
+ });
133
+ }
134
+ return source;
135
+ }
136
+
137
+ function compatibleLegendChildren(program, source, inferred) {
138
+ return program.compositionSpec.children.map(id => {
139
+ const child = program.children[id];
140
+ return {
141
+ id,
142
+ guideConfigs: inferred ? source.guideConfigs : child.guideConfigs,
143
+ resolvedScales: child.resolvedScales
144
+ };
145
+ });
146
+ }
147
+
148
+ export function prepareSharedFacetLegend(program) {
149
+ if (program.compositionSpec.facet.guides.legend !== "shared") {
150
+ return undefined;
151
+ }
152
+ const firstId = program.compositionSpec.children[0];
153
+ const first = program.children[firstId];
154
+ const inferred = legendKinds(first).length === 0;
155
+ if (inferred) {
156
+ const legacy = resolveLegacyCategoricalLegend(program);
157
+ if (legacy !== undefined) return legacy;
158
+ }
159
+ const source = inferred ? prepareAutoLegendSource(first) : first;
160
+ const kinds = legendKinds(source);
161
+ const compatibility = resolveSharedFacetLegends(
162
+ compatibleLegendChildren(program, source, inferred)
163
+ );
164
+ const roots = allLegendGraphicIds(kinds).filter(
165
+ id => source.graphicSpec.objects[id] !== undefined
166
+ );
167
+ if (roots.length === 0) {
168
+ throw new Error("Shared facet legend has no concrete guide graphics.");
169
+ }
170
+ const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots);
171
+ if (bounds === undefined) {
172
+ throw new Error("Shared facet legend has no measurable concrete bounds.");
173
+ }
174
+ const gradient = kinds.includes("gradient");
175
+ return {
176
+ mode: "promoted",
177
+ source,
178
+ inferred,
179
+ kinds,
180
+ roots,
181
+ bounds,
182
+ compatibility,
183
+ reservation: {
184
+ gap: gradient ? 24 : 18,
185
+ width: gradient ? 112 : Math.max(132, Math.ceil(bounds.right - bounds.left))
186
+ }
187
+ };
188
+ }
@@ -6,7 +6,7 @@ import {
6
6
  } from "../layout/title.js";
7
7
  import { resolveFacetLayout } from "../layout/facets.js";
8
8
  import { namespaceGraphicSnapshot } from "./compositionSnapshot.js";
9
- import { prepareSharedFacetLegend } from "./facetGuides.js";
9
+ import { prepareSharedFacetLegend } from "./facetGuides/index.js";
10
10
  import {
11
11
  attachSnapshotObject,
12
12
  clearCompositionChildren,