ggaction 0.0.3 → 0.0.4

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 (108) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/README.md +95 -83
  3. package/package.json +11 -6
  4. package/src/actions/data/regression.js +1 -1
  5. package/src/actions/encodings/appearance.js +1 -1
  6. package/src/actions/encodings/color/continuous.js +137 -0
  7. package/src/actions/encodings/color/index.js +151 -0
  8. package/src/actions/encodings/color/layout.js +91 -0
  9. package/src/actions/encodings/color/policy.js +88 -0
  10. package/src/actions/encodings/index.js +2 -2
  11. package/src/actions/encodings/offset.js +1 -1
  12. package/src/actions/encodings/{position.js → position/index.js} +6 -6
  13. package/src/actions/encodings/position/resolve.js +1 -1
  14. package/src/actions/encodings/ranged.js +2 -2
  15. package/src/actions/encodings/strokeDash.js +1 -1
  16. package/src/actions/encodings/text.js +1 -1
  17. package/src/actions/errorBands/options.js +1 -1
  18. package/src/actions/errorBars/options.js +1 -1
  19. package/src/actions/facets/actions.js +1 -1
  20. package/src/actions/facets/derive.js +2 -2
  21. package/src/actions/facets/guides.js +1 -1
  22. package/src/actions/guides/axes/labels.js +1 -1
  23. package/src/actions/guides/axes/ticks.js +1 -1
  24. package/src/actions/guides/axes/titles.js +1 -1
  25. package/src/actions/guides/grids/resolve.js +1 -1
  26. package/src/actions/guides/legends/categorical/layout.js +4 -2
  27. package/src/actions/guides/legends/continuous/interval.js +1 -1
  28. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  29. package/src/actions/guides/legends/edit.js +2 -20
  30. package/src/actions/guides/legends/remove.js +2 -20
  31. package/src/actions/guides/legends/size.js +1 -1
  32. package/src/actions/guides/legends/target.js +25 -0
  33. package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
  34. package/src/actions/guides/polar/index.js +1 -1
  35. package/src/actions/guides/polar/resolve.js +1 -1
  36. package/src/actions/guides/tickValues.js +1 -1
  37. package/src/actions/intervals/resolve.js +1 -1
  38. package/src/actions/marks/arc/actions.js +2 -2
  39. package/src/actions/marks/area/actions.js +2 -2
  40. package/src/actions/marks/bar/edit.js +1 -1
  41. package/src/actions/marks/line/actions.js +3 -3
  42. package/src/actions/marks/rect/actions.js +1 -1
  43. package/src/actions/marks/rule/actions.js +1 -1
  44. package/src/actions/marks/shared.js +1 -1
  45. package/src/actions/marks/text/actions.js +1 -1
  46. package/src/actions/primitives/semanticValue.js +1 -1
  47. package/src/actions/regression/components.js +1 -1
  48. package/src/actions/regression/create.js +1 -1
  49. package/src/actions/regression/edit.js +1 -1
  50. package/src/actions/scales/consumers/common.js +91 -0
  51. package/src/actions/scales/consumers/families.js +47 -0
  52. package/src/actions/scales/consumers/index.js +41 -0
  53. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
  54. package/src/actions/scales/create.js +1 -1
  55. package/src/actions/scales/definitions.js +2 -2
  56. package/src/actions/scales/edit.js +3 -3
  57. package/src/actions/scales/materialize.js +2 -2
  58. package/src/core/ChartProgram.js +22 -236
  59. package/src/core/compositionState.js +169 -0
  60. package/src/core/materializationState.js +65 -0
  61. package/src/core/programState.js +28 -0
  62. package/src/grammar/arcs.js +1 -1
  63. package/src/grammar/areaSeries.js +1 -1
  64. package/src/grammar/bars/aggregate.js +1 -1
  65. package/src/grammar/density.js +1 -1
  66. package/src/grammar/{facets.js → facets/index.js} +5 -5
  67. package/src/grammar/interval.js +2 -96
  68. package/src/grammar/lineSeries.js +1 -1
  69. package/src/grammar/palettes.js +5 -7
  70. package/src/grammar/polarLineCommands.js +1 -1
  71. package/src/grammar/regression/derive.js +97 -0
  72. package/src/grammar/regression/index.js +22 -0
  73. package/src/grammar/regression/models.js +282 -0
  74. package/src/grammar/regression/parameters.js +106 -0
  75. package/src/grammar/rules.js +1 -1
  76. package/src/grammar/scales/color.js +4 -2
  77. package/src/grammar/statistics/studentT.js +98 -0
  78. package/src/grammar/transforms.js +1 -1
  79. package/src/materialization/bars/aggregate.js +1 -1
  80. package/src/materialization/bars/grouped.js +1 -1
  81. package/src/materialization/bars/histogram.js +1 -1
  82. package/src/materialization/bars/ranged.js +1 -1
  83. package/src/materialization/density.js +1 -1
  84. package/src/materialization/dependencies.js +6 -87
  85. package/src/materialization/encodings.js +2 -2
  86. package/src/materialization/facetGuides/index.js +2 -0
  87. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  88. package/src/materialization/facetGuides/placement.js +126 -0
  89. package/src/materialization/facetGuides/preparation.js +188 -0
  90. package/src/materialization/facets.js +1 -1
  91. package/src/materialization/marks/capabilities.js +141 -0
  92. package/src/materialization/marks/index.js +105 -0
  93. package/src/materialization/marks/policies.js +87 -0
  94. package/src/materialization/rect.js +1 -1
  95. package/src/materialization/rowEncoding.js +1 -1
  96. package/src/materialization/scaleGuideDependencies.js +94 -0
  97. package/src/materialization/scales/map.js +1 -1
  98. package/src/materialization/scales/policies/series.js +1 -1
  99. package/src/materialization/scales/resolve.js +1 -1
  100. package/src/materialization/selection/items/bar.js +1 -1
  101. package/src/materialization/selection/styles.js +1 -1
  102. package/src/renderers/canvas/text.js +14 -1
  103. package/types/extension.d.ts +4 -1
  104. package/src/actions/encodings/color.js +0 -424
  105. package/src/grammar/regression.js +0 -578
  106. package/src/grammar/scales.js +0 -1
  107. package/src/materialization/facetGuides.js +0 -426
  108. package/src/materialization/marks.js +0 -316
@@ -0,0 +1,98 @@
1
+ const LANCZOS = Object.freeze([
2
+ 676.5203681218851,
3
+ -1259.1392167224028,
4
+ 771.3234287776531,
5
+ -176.6150291621406,
6
+ 12.507343278686905,
7
+ -0.13857109526572012,
8
+ 9.984369578019572e-6,
9
+ 1.5056327351493116e-7
10
+ ]);
11
+
12
+ function logGamma(value) {
13
+ if (value < 0.5) {
14
+ return Math.log(Math.PI) - Math.log(Math.sin(Math.PI * value)) -
15
+ logGamma(1 - value);
16
+ }
17
+ const shifted = value - 1;
18
+ let sum = 0.9999999999998099;
19
+ for (const [index, coefficient] of LANCZOS.entries()) {
20
+ sum += coefficient / (shifted + index + 1);
21
+ }
22
+ const base = shifted + LANCZOS.length - 0.5;
23
+ return 0.5 * Math.log(2 * Math.PI) +
24
+ (shifted + 0.5) * Math.log(base) - base + Math.log(sum);
25
+ }
26
+
27
+ function betaContinuedFraction(a, b, value) {
28
+ const maximumIterations = 200;
29
+ const epsilon = 3e-14;
30
+ const floor = 1e-300;
31
+ const sum = a + b;
32
+ let c = 1;
33
+ let d = 1 - sum * value / (a + 1);
34
+ if (Math.abs(d) < floor) d = floor;
35
+ d = 1 / d;
36
+ let result = d;
37
+
38
+ for (let iteration = 1; iteration <= maximumIterations; iteration += 1) {
39
+ const even = 2 * iteration;
40
+ let coefficient = iteration * (b - iteration) * value /
41
+ ((a + even - 1) * (a + even));
42
+ d = 1 + coefficient * d;
43
+ if (Math.abs(d) < floor) d = floor;
44
+ c = 1 + coefficient / c;
45
+ if (Math.abs(c) < floor) c = floor;
46
+ d = 1 / d;
47
+ result *= d * c;
48
+
49
+ coefficient = -(a + iteration) * (sum + iteration) * value /
50
+ ((a + even) * (a + even + 1));
51
+ d = 1 + coefficient * d;
52
+ if (Math.abs(d) < floor) d = floor;
53
+ c = 1 + coefficient / c;
54
+ if (Math.abs(c) < floor) c = floor;
55
+ d = 1 / d;
56
+ const delta = d * c;
57
+ result *= delta;
58
+ if (Math.abs(delta - 1) <= epsilon) return result;
59
+ }
60
+ throw new Error("Student-t calculation did not converge.");
61
+ }
62
+
63
+ function regularizedIncompleteBeta(value, a, b) {
64
+ if (value === 0 || value === 1) return value;
65
+ const factor = Math.exp(
66
+ logGamma(a + b) - logGamma(a) - logGamma(b) +
67
+ a * Math.log(value) + b * Math.log1p(-value)
68
+ );
69
+ if (value < (a + 1) / (a + b + 2)) {
70
+ return factor * betaContinuedFraction(a, b, value) / a;
71
+ }
72
+ return 1 - factor * betaContinuedFraction(b, a, 1 - value) / b;
73
+ }
74
+
75
+ function studentTCdf(value, degreesOfFreedom) {
76
+ if (value === 0) return 0.5;
77
+ const ratio = degreesOfFreedom /
78
+ (degreesOfFreedom + value * value);
79
+ const tail = regularizedIncompleteBeta(
80
+ ratio,
81
+ degreesOfFreedom / 2,
82
+ 0.5
83
+ ) / 2;
84
+ return value > 0 ? 1 - tail : tail;
85
+ }
86
+
87
+ export function studentTCriticalValue(level, degreesOfFreedom) {
88
+ const probability = (1 + level) / 2;
89
+ let low = 0;
90
+ let high = 1;
91
+ while (studentTCdf(high, degreesOfFreedom) < probability) high *= 2;
92
+ for (let iteration = 0; iteration < 100; iteration += 1) {
93
+ const midpoint = (low + high) / 2;
94
+ if (studentTCdf(midpoint, degreesOfFreedom) < probability) low = midpoint;
95
+ else high = midpoint;
96
+ }
97
+ return (low + high) / 2;
98
+ }
@@ -4,7 +4,7 @@ import { validateDensityTransform } from "./density.js";
4
4
  import { validateFilterTransform } from "./filter.js";
5
5
  import { validateIntervalTransform } from "./interval.js";
6
6
  import { validateMarkFilterTransform } from "./markFilter.js";
7
- import { validateRegressionTransform } from "./regression.js";
7
+ import { validateRegressionTransform } from "./regression/index.js";
8
8
 
9
9
  const TRANSFORM_VALIDATORS = Object.freeze({
10
10
  boxOutlier: validateBoxTransform,
@@ -13,7 +13,7 @@ import {
13
13
  mapContinuousScaleValues,
14
14
  mapOrdinalPositionValues,
15
15
  mapSequentialColors
16
- } from "../../grammar/scales.js";
16
+ } from "../../grammar/scales/index.js";
17
17
  import {
18
18
  DEFAULT_BAR_FILL,
19
19
  DEFAULT_BAR_STROKE,
@@ -2,7 +2,7 @@ import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
2
2
  import {
3
3
  mapContinuousScaleValues,
4
4
  mapOrdinalPositionValues
5
- } from "../../grammar/scales.js";
5
+ } from "../../grammar/scales/index.js";
6
6
  import { sameOrderedValues } from "../../core/validation.js";
7
7
  import {
8
8
  DEFAULT_BAR_STROKE,
@@ -8,7 +8,7 @@ import {
8
8
  mapOrdinalValues,
9
9
  readNominalField,
10
10
  readQuantitativeField
11
- } from "../../grammar/scales.js";
11
+ } from "../../grammar/scales/index.js";
12
12
  import { resolveBarColorLayout } from "../../grammar/bars/policy.js";
13
13
  import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
14
14
  import {
@@ -1,4 +1,4 @@
1
- import { mapContinuousScaleValues, mapOrdinalPositionValues, mapOrdinalValues } from "../../grammar/scales.js";
1
+ import { mapContinuousScaleValues, mapOrdinalPositionValues, mapOrdinalValues } from "../../grammar/scales/index.js";
2
2
  import { resolveBarWidth } from "../../grammar/bars/geometry.js";
3
3
  import { DEFAULT_BAR_FILL, DEFAULT_BAR_STROKE, DEFAULT_BAR_STROKE_WIDTH } from "./resolve.js";
4
4
 
@@ -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,48 +3,17 @@ 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";
17
-
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
- }
14
+ needsCanvasScaleRematerialization,
15
+ planScaleGuideRematerialization
16
+ } from "./scaleGuideDependencies.js";
48
17
 
49
18
  function hasTitle(program) {
50
19
  return (
@@ -96,57 +65,6 @@ export function planCanvasRematerialization(program) {
96
65
  return buildMaterializationPlan({ scales, marks, guides, layout });
97
66
  }
98
67
 
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
68
  export function planLayerDataRematerialization(program, id) {
151
69
  const layer = requireLayer(program, id);
152
70
  const scaleIds = [...new Set(
@@ -187,3 +105,4 @@ export function applyLayerDataRematerialization(program, id) {
187
105
  }
188
106
 
189
107
  export { applyMaterializationPlan } from "./planner.js";
108
+ 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
+ }