ggaction 0.0.2 → 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 (242) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/README.md +95 -72
  3. package/package.json +15 -5
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/data/regression.js +1 -1
  16. package/src/actions/encodings/appearance.js +12 -1
  17. package/src/actions/encodings/barWidth.js +5 -3
  18. package/src/actions/encodings/color/continuous.js +137 -0
  19. package/src/actions/encodings/color/index.js +151 -0
  20. package/src/actions/encodings/color/layout.js +91 -0
  21. package/src/actions/encodings/color/policy.js +88 -0
  22. package/src/actions/encodings/index.js +4 -2
  23. package/src/actions/encodings/offset.js +91 -73
  24. package/src/actions/encodings/position/apply.js +66 -0
  25. package/src/actions/encodings/position/index.js +130 -0
  26. package/src/actions/encodings/position/policies/arc.js +26 -0
  27. package/src/actions/encodings/position/policies/bar.js +9 -5
  28. package/src/actions/encodings/position/policies/index.js +5 -1
  29. package/src/actions/encodings/position/policies/line.js +20 -3
  30. package/src/actions/encodings/position/resolve.js +58 -9
  31. package/src/actions/encodings/ranged.js +30 -8
  32. package/src/actions/encodings/shared.js +17 -21
  33. package/src/actions/encodings/strokeDash.js +1 -1
  34. package/src/actions/encodings/text.js +73 -0
  35. package/src/actions/errorBands/create.js +37 -29
  36. package/src/actions/errorBands/edit.js +206 -0
  37. package/src/actions/errorBands/index.js +9 -0
  38. package/src/actions/errorBands/options.js +39 -0
  39. package/src/actions/errorBars/create.js +50 -43
  40. package/src/actions/errorBars/edit.js +130 -0
  41. package/src/actions/errorBars/index.js +3 -0
  42. package/src/actions/errorBars/options.js +52 -0
  43. package/src/actions/facets/actions.js +162 -0
  44. package/src/actions/facets/derive.js +200 -0
  45. package/src/actions/facets/guides.js +16 -0
  46. package/src/actions/facets/index.js +1 -0
  47. package/src/actions/facets/replay.js +69 -0
  48. package/src/actions/guides/applicability.js +113 -0
  49. package/src/actions/guides/axes/axes.js +70 -16
  50. package/src/actions/guides/axes/edit.js +126 -0
  51. package/src/actions/guides/axes/index.js +12 -0
  52. package/src/actions/guides/axes/labels.js +22 -19
  53. package/src/actions/guides/axes/remove.js +60 -0
  54. package/src/actions/guides/axes/tickGroups.js +7 -5
  55. package/src/actions/guides/axes/ticks.js +10 -5
  56. package/src/actions/guides/axes/titles.js +8 -6
  57. package/src/actions/guides/grids/grid.js +144 -15
  58. package/src/actions/guides/grids/resolve.js +6 -14
  59. package/src/actions/guides/guides.js +17 -75
  60. package/src/actions/guides/index.js +6 -0
  61. package/src/actions/guides/legends/categorical/actions.js +48 -9
  62. package/src/actions/guides/legends/categorical/components.js +16 -16
  63. package/src/actions/guides/legends/categorical/index.js +14 -14
  64. package/src/actions/guides/legends/categorical/layout.js +7 -3
  65. package/src/actions/guides/legends/categorical/options.js +2 -5
  66. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  67. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  68. package/src/actions/guides/legends/continuous/common.js +7 -7
  69. package/src/actions/guides/legends/continuous/interval.js +6 -6
  70. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  71. package/src/actions/guides/legends/edit.js +4 -26
  72. package/src/actions/guides/legends/focused.js +63 -0
  73. package/src/actions/guides/legends/index.js +4 -0
  74. package/src/actions/guides/legends/remove.js +46 -0
  75. package/src/actions/guides/legends/size.js +13 -15
  76. package/src/actions/guides/legends/target.js +25 -0
  77. package/src/actions/guides/polar/axes/facade.js +219 -0
  78. package/src/actions/guides/polar/axes/index.js +55 -0
  79. package/src/actions/guides/polar/axes/labels.js +188 -0
  80. package/src/actions/guides/polar/axes/lines.js +113 -0
  81. package/src/actions/guides/polar/axes/shared.js +112 -0
  82. package/src/actions/guides/polar/axes/ticks.js +164 -0
  83. package/src/actions/guides/polar/axes/titles.js +165 -0
  84. package/src/actions/guides/polar/grids.js +252 -0
  85. package/src/actions/guides/polar/index.js +2 -0
  86. package/src/actions/guides/polar/resolve.js +269 -0
  87. package/src/actions/guides/tickValues.js +1 -1
  88. package/src/actions/index.js +4 -0
  89. package/src/actions/intervals/resolve.js +1 -1
  90. package/src/actions/marks/arc/actions.js +239 -0
  91. package/src/actions/marks/arc/index.js +1 -0
  92. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  93. package/src/actions/marks/area/index.js +4 -0
  94. package/src/actions/marks/bar/create.js +26 -5
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/bar/materialize.js +25 -31
  97. package/src/actions/marks/index.js +12 -4
  98. package/src/actions/marks/lifecycle.js +27 -0
  99. package/src/actions/marks/line/actions.js +373 -0
  100. package/src/actions/marks/line/index.js +1 -0
  101. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  102. package/src/actions/marks/point/index.js +1 -0
  103. package/src/actions/marks/rect/actions.js +165 -0
  104. package/src/actions/marks/rect/index.js +1 -0
  105. package/src/actions/marks/remove.js +214 -0
  106. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  107. package/src/actions/marks/rule/index.js +1 -0
  108. package/src/actions/marks/shared.js +156 -25
  109. package/src/actions/marks/text/actions.js +192 -0
  110. package/src/actions/marks/text/index.js +1 -0
  111. package/src/actions/primitives/createGraphics.js +13 -6
  112. package/src/actions/primitives/editGraphics.js +10 -3
  113. package/src/actions/primitives/semantic.js +26 -3
  114. package/src/actions/primitives/semanticValue.js +26 -4
  115. package/src/actions/regression/components.js +2 -2
  116. package/src/actions/regression/create.js +19 -2
  117. package/src/actions/regression/edit.js +247 -0
  118. package/src/actions/regression/index.js +3 -0
  119. package/src/actions/regression/resolve.js +3 -1
  120. package/src/actions/scales/consumers/common.js +91 -0
  121. package/src/actions/scales/consumers/families.js +47 -0
  122. package/src/actions/scales/consumers/index.js +41 -0
  123. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +14 -127
  124. package/src/actions/scales/create.js +1 -1
  125. package/src/actions/scales/definitions.js +15 -6
  126. package/src/actions/scales/edit.js +45 -22
  127. package/src/actions/scales/materialize.js +58 -409
  128. package/src/actions/selection/actions.js +38 -24
  129. package/src/actions/selection/index.js +2 -0
  130. package/src/actions/titles/actions.js +63 -2
  131. package/src/actions/titles/index.js +2 -0
  132. package/src/actions/titles/resolve.js +12 -17
  133. package/src/composition.js +10 -0
  134. package/src/core/ChartProgram.js +49 -65
  135. package/src/core/action.js +10 -0
  136. package/src/core/compositionState.js +169 -0
  137. package/src/core/materializationState.js +65 -0
  138. package/src/core/programState.js +28 -0
  139. package/src/core/validation.js +16 -0
  140. package/src/core/vocabulary.js +154 -4
  141. package/src/grammar/arcs.js +154 -0
  142. package/src/grammar/areaSeries.js +1 -1
  143. package/src/grammar/bars/aggregate.js +3 -3
  144. package/src/grammar/bars/geometry.js +3 -3
  145. package/src/grammar/bars/policy.js +7 -1
  146. package/src/grammar/coordinates.js +3 -7
  147. package/src/grammar/density.js +21 -2
  148. package/src/grammar/facets/dependencies.js +185 -0
  149. package/src/grammar/facets/guides.js +257 -0
  150. package/src/grammar/facets/index.js +122 -0
  151. package/src/grammar/facets/scales.js +254 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/lineSeries.js +112 -4
  154. package/src/grammar/palettes.js +5 -7
  155. package/src/grammar/polar.js +102 -0
  156. package/src/grammar/polarGuides.js +243 -0
  157. package/src/grammar/polarLineCommands.js +43 -0
  158. package/src/grammar/polarPaths.js +161 -0
  159. package/src/grammar/positionCompatibility.js +18 -2
  160. package/src/grammar/rects.js +26 -0
  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 +1 -1
  166. package/src/grammar/scales/color.js +4 -2
  167. package/src/grammar/scales/continuous.js +6 -1
  168. package/src/grammar/scales/fields.js +7 -0
  169. package/src/grammar/scales/ordinal.js +5 -2
  170. package/src/grammar/scales/policies.js +1 -1
  171. package/src/grammar/schemas/graphic.js +1 -1
  172. package/src/grammar/schemas/graphicBounds.js +64 -9
  173. package/src/grammar/schemas/graphicTree.js +44 -10
  174. package/src/grammar/schemas/semanticPath.js +31 -9
  175. package/src/grammar/seriesLayout.js +13 -2
  176. package/src/grammar/statistics/studentT.js +98 -0
  177. package/src/grammar/text.js +71 -0
  178. package/src/grammar/ticks.js +50 -13
  179. package/src/grammar/transforms.js +1 -1
  180. package/src/index.js +1 -0
  181. package/src/layout/composition.js +310 -0
  182. package/src/layout/facets.js +131 -0
  183. package/src/layout/title.js +6 -0
  184. package/src/materialization/bars/aggregate.js +6 -3
  185. package/src/materialization/bars/grouped.js +75 -37
  186. package/src/materialization/bars/histogram.js +1 -1
  187. package/src/materialization/bars/ranged.js +1 -1
  188. package/src/materialization/composition.js +152 -0
  189. package/src/materialization/compositionSnapshot.js +114 -0
  190. package/src/materialization/density.js +1 -1
  191. package/src/materialization/dependencies.js +47 -73
  192. package/src/materialization/encodings.js +14 -28
  193. package/src/materialization/facetGuides/index.js +2 -0
  194. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  195. package/src/materialization/facetGuides/placement.js +126 -0
  196. package/src/materialization/facetGuides/preparation.js +188 -0
  197. package/src/materialization/facets.js +214 -0
  198. package/src/materialization/graphicHierarchy.js +15 -5
  199. package/src/materialization/guides/resources.js +46 -0
  200. package/src/materialization/marks/capabilities.js +141 -0
  201. package/src/materialization/marks/index.js +105 -0
  202. package/src/materialization/marks/policies.js +87 -0
  203. package/src/materialization/planner.js +105 -12
  204. package/src/materialization/rect.js +138 -0
  205. package/src/materialization/rectConfig.js +38 -0
  206. package/src/materialization/rowEncoding.js +33 -0
  207. package/src/materialization/scaleGuideDependencies.js +94 -0
  208. package/src/materialization/scales/map.js +40 -0
  209. package/src/materialization/scales/policies/arc.js +37 -0
  210. package/src/materialization/scales/policies/bar.js +132 -0
  211. package/src/materialization/scales/policies/series.js +76 -0
  212. package/src/materialization/scales/resolve.js +326 -0
  213. package/src/materialization/selection/items/arc.js +29 -0
  214. package/src/materialization/selection/items/bar.js +255 -0
  215. package/src/materialization/selection/items/common.js +131 -0
  216. package/src/materialization/selection/items/index.js +6 -0
  217. package/src/materialization/selection/items/path.js +59 -0
  218. package/src/materialization/selection/items/point.js +55 -0
  219. package/src/materialization/selection/items/rect.js +23 -0
  220. package/src/materialization/selection/items/rule.js +25 -0
  221. package/src/materialization/selection/policies/arc.js +10 -0
  222. package/src/materialization/selection/policies/area.js +1 -1
  223. package/src/materialization/selection/policies/bar.js +1 -1
  224. package/src/materialization/selection/policies/index.js +5 -1
  225. package/src/materialization/selection/policies/line.js +1 -1
  226. package/src/materialization/selection/policies/point.js +1 -1
  227. package/src/materialization/selection/policies/rect.js +10 -0
  228. package/src/materialization/selection/policies/rule.js +1 -1
  229. package/src/materialization/selection/styles.js +25 -17
  230. package/src/materialization/text.js +156 -0
  231. package/src/renderers/canvas/index.js +68 -22
  232. package/src/renderers/canvas/text.js +14 -1
  233. package/types/extension.d.ts +5 -1
  234. package/types/index.d.ts +56 -1
  235. package/types/program.d.ts +664 -27
  236. package/src/actions/encodings/color.js +0 -329
  237. package/src/actions/encodings/position.js +0 -176
  238. package/src/actions/marks/line.js +0 -248
  239. package/src/grammar/regression.js +0 -578
  240. package/src/grammar/scales.js +0 -1
  241. package/src/materialization/marks.js +0 -110
  242. package/src/materialization/selection/items.js +0 -503
@@ -1,7 +1,10 @@
1
1
  import { createErrorBarCap } from "./components.js";
2
2
  import { createErrorBar } from "./create.js";
3
+ import { editErrorBar, rematerializeErrorBar } from "./edit.js";
3
4
 
4
5
  export function registerErrorBarActions(ProgramClass) {
5
6
  ProgramClass.prototype.createErrorBarCap = createErrorBarCap;
6
7
  ProgramClass.prototype.createErrorBar = createErrorBar;
8
+ ProgramClass.prototype.editErrorBar = editErrorBar;
9
+ ProgramClass.prototype.rematerializeErrorBar = rematerializeErrorBar;
7
10
  }
@@ -0,0 +1,52 @@
1
+ import {
2
+ validateKeys,
3
+ validatePositiveFinite
4
+ } from "../../core/validation.js";
5
+ import {
6
+ normalizeStrokeDashPattern,
7
+ validateOpacityValue
8
+ } from "../../grammar/scales/index.js";
9
+ import {
10
+ validateRuleStroke,
11
+ validateRuleStrokeWidth
12
+ } from "../../grammar/ruleAppearance.js";
13
+
14
+ export const ERROR_BAR_EDIT_OPTIONS = Object.freeze([
15
+ "target", "caps", "capSize", "stroke", "strokeWidth",
16
+ "strokeDash", "opacity"
17
+ ]);
18
+
19
+ export function resolveErrorBarAppearance(args, {
20
+ defaults,
21
+ operation
22
+ }) {
23
+ validateKeys(args, ERROR_BAR_EDIT_OPTIONS, operation);
24
+ const caps = args.caps ?? defaults.caps;
25
+ if (typeof caps !== "boolean") {
26
+ throw new TypeError(`${operation} caps must be a boolean.`);
27
+ }
28
+ const capSize = args.capSize ?? defaults.capSize;
29
+ validatePositiveFinite(capSize, `${operation} capSize`);
30
+ const stroke = validateRuleStroke(
31
+ args.stroke ?? defaults.stroke,
32
+ `${operation} stroke`
33
+ );
34
+ const strokeWidth = validateRuleStrokeWidth(
35
+ args.strokeWidth ?? defaults.strokeWidth,
36
+ `${operation} strokeWidth`
37
+ );
38
+ const strokeDash = args.strokeDash ?? defaults.strokeDash;
39
+ normalizeStrokeDashPattern(strokeDash);
40
+ const opacity = validateOpacityValue(
41
+ args.opacity ?? defaults.opacity,
42
+ `${operation} opacity`
43
+ );
44
+ return {
45
+ caps,
46
+ capSize,
47
+ stroke,
48
+ strokeWidth,
49
+ strokeDash,
50
+ opacity
51
+ };
52
+ }
@@ -0,0 +1,162 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import {
4
+ validateNonEmptyString,
5
+ validateNonNegativeFinite,
6
+ validateOptionObject,
7
+ validatePositiveFinite
8
+ } from "../../core/validation.js";
9
+ import { resolveFacetDefinition } from "../../grammar/facets/index.js";
10
+ import { normalizeFacetScalePolicies } from
11
+ "../../grammar/facets/scales.js";
12
+ import { resolveFacetLayout } from "../../layout/facets.js";
13
+ import { compositionChildDescriptor } from
14
+ "../../materialization/composition.js";
15
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
16
+ import { deriveFacetChildren } from "./derive.js";
17
+ import { rebindLayerData, replayDerivedData } from "./replay.js";
18
+ import { composeFacetGuides } from "./guides.js";
19
+
20
+ const FACET_OPTIONS = Object.freeze([
21
+ "id", "field", "data", "columns", "gap", "align", "padding", "scales",
22
+ "guides"
23
+ ]);
24
+ const GUIDE_OPTIONS = Object.freeze(["axes", "legend"]);
25
+ const HEADER_OPTIONS = Object.freeze([
26
+ "fontSize", "fontFamily", "fontWeight", "color", "offset"
27
+ ]);
28
+ const DEFAULT_HEADERS = Object.freeze({
29
+ fontSize: 12,
30
+ fontFamily: DEFAULT_FONT_FAMILY,
31
+ fontWeight: 600,
32
+ color: DEFAULT_COLORS.strongText,
33
+ offset: 10
34
+ });
35
+
36
+ function normalizeGuides(guides) {
37
+ if (guides === undefined) return { axes: "each", legend: false };
38
+ validateOptionObject(guides, GUIDE_OPTIONS, "facet.guides");
39
+ const axes = guides.axes ?? "each";
40
+ if (!["each", "outer"].includes(axes)) {
41
+ throw new Error('facet guides.axes must be "each" or "outer".');
42
+ }
43
+ const legend = guides.legend ?? false;
44
+ if (legend !== false && legend !== "shared") {
45
+ throw new Error('facet guides.legend must be false or "shared".');
46
+ }
47
+ return { axes, legend };
48
+ }
49
+
50
+ function normalizeHeaderPatch(args, previous) {
51
+ validateOptionObject(args, HEADER_OPTIONS, "editFacetHeaders", {
52
+ allowEmpty: false,
53
+ emptyMessage: "editFacetHeaders requires at least one change."
54
+ });
55
+ const next = { ...previous, ...args };
56
+ validatePositiveFinite(next.fontSize, "Facet header fontSize");
57
+ validateNonEmptyString(next.fontFamily, "Facet header fontFamily");
58
+ validateNonEmptyString(next.color, "Facet header color");
59
+ validateNonNegativeFinite(next.offset, "Facet header offset");
60
+ if (!(
61
+ (typeof next.fontWeight === "string" && next.fontWeight.length > 0) ||
62
+ Number.isFinite(next.fontWeight)
63
+ )) {
64
+ throw new TypeError("Facet header fontWeight must be a non-empty string or number.");
65
+ }
66
+ return next;
67
+ }
68
+
69
+ function requireFacetProgram(program, operation) {
70
+ program._assertCompositionProgram(operation);
71
+ if (program.compositionSpec.type !== "facet") {
72
+ throw new Error(`${operation} requires a facet composition.`);
73
+ }
74
+ }
75
+
76
+ export const facet = action(
77
+ {
78
+ op: "facet",
79
+ description: "Repeat one direct-source chart by field value."
80
+ },
81
+ function (args = {}) {
82
+ validateOptionObject(args, FACET_OPTIONS, "facet");
83
+ const guides = normalizeGuides(args.guides);
84
+ const definition = resolveFacetDefinition(this.semanticSpec, args);
85
+ const scalePolicies = normalizeFacetScalePolicies(
86
+ this.semanticSpec,
87
+ args.scales ?? {}
88
+ );
89
+ const derived = deriveFacetChildren(this, definition, {
90
+ closeInheritedAction: true,
91
+ stripTitle: true,
92
+ scales: args.scales ?? {}
93
+ });
94
+ const preflight = resolveFacetLayout({
95
+ children: definition.cells.map(cell => ({
96
+ ...compositionChildDescriptor(cell.id, derived.children[cell.id]),
97
+ value: cell.value
98
+ })),
99
+ ...(Object.hasOwn(args, "columns") ? { columns: args.columns } : {}),
100
+ ...(Object.hasOwn(args, "gap") ? { gap: args.gap } : {}),
101
+ ...(Object.hasOwn(args, "align") ? { align: args.align } : {}),
102
+ ...(Object.hasOwn(args, "padding") ? { padding: args.padding } : {}),
103
+ sharedLegend: guides.legend === "shared"
104
+ });
105
+ const compositionSpec = {
106
+ id: definition.id,
107
+ type: "facet",
108
+ children: definition.cells.map(cell => cell.id),
109
+ columns: preflight.columns,
110
+ gap: preflight.gap,
111
+ align: preflight.align,
112
+ padding: preflight.padding,
113
+ facet: {
114
+ data: definition.data,
115
+ field: definition.field,
116
+ values: definition.values,
117
+ scales: scalePolicies.channels,
118
+ guides
119
+ }
120
+ };
121
+ let next = this
122
+ ._withCompositionState({
123
+ children: derived.children,
124
+ compositionSpec
125
+ })
126
+ ._withMaterializationConfig(["facets", definition.id], {
127
+ headers: DEFAULT_HEADERS
128
+ });
129
+ for (const id of compositionSpec.children) {
130
+ next = next.useProgram({ id });
131
+ }
132
+ return next.materializeComposition();
133
+ }
134
+ );
135
+
136
+ export const editFacetHeaders = action(
137
+ {
138
+ op: "editFacetHeaders",
139
+ description: "Edit parent-owned facet header appearance.",
140
+ scope: "composition"
141
+ },
142
+ function (args = {}) {
143
+ requireFacetProgram(this, "editFacetHeaders");
144
+ const id = this.compositionSpec.id;
145
+ const config = this.materializationConfigs.facets?.[id];
146
+ if (!isPlainObject(config?.headers)) {
147
+ throw new Error(`Facet "${id}" requires header configuration.`);
148
+ }
149
+ const headers = normalizeHeaderPatch(args, config.headers);
150
+ return this
151
+ ._withMaterializationConfig(["facets", id], { ...config, headers })
152
+ .materializeComposition();
153
+ }
154
+ );
155
+
156
+ export function registerFacetActions(ProgramClass) {
157
+ ProgramClass.prototype.replayDerivedData = replayDerivedData;
158
+ ProgramClass.prototype.rebindLayerData = rebindLayerData;
159
+ ProgramClass.prototype.composeFacetGuides = composeFacetGuides;
160
+ ProgramClass.prototype.facet = facet;
161
+ ProgramClass.prototype.editFacetHeaders = editFacetHeaders;
162
+ }
@@ -0,0 +1,200 @@
1
+ import { freezeOwned } from "../../core/immutable.js";
2
+ import { resolveHistogramBins } from "../../grammar/histogram.js";
3
+ import { readQuantitativeField } from "../../grammar/scales/index.js";
4
+ import { BAR_GRAINS, resolveBarGrain } from "../../grammar/bars/policy.js";
5
+ import { resolveFacetScaleDomains } from "../../grammar/facets/scales.js";
6
+ import {
7
+ applyMaterializationPlan,
8
+ planScaleGuideRematerialization
9
+ } from "../../materialization/dependencies.js";
10
+ import { getMarkMaterializationStep } from "../../materialization/marks/index.js";
11
+ import { buildMaterializationPlan } from "../../materialization/planner.js";
12
+ import { requireDataset } from "../../selectors/datasets.js";
13
+ import { requireLayer } from "../../selectors/layers.js";
14
+ import { findSemanticScale } from "../../selectors/scales.js";
15
+
16
+ function sharedHistogramBoundaries(program) {
17
+ const boundaries = new Map();
18
+ for (const layer of program.semanticSpec.layers) {
19
+ if (
20
+ layer.mark?.type !== "bar" ||
21
+ resolveBarGrain(layer) !== BAR_GRAINS.histogram
22
+ ) {
23
+ continue;
24
+ }
25
+ const x = layer.encoding.x;
26
+ const scale = findSemanticScale(program, x.scale);
27
+ const dataset = requireDataset(program, layer.data);
28
+ const values = readQuantitativeField(dataset.values, x.field);
29
+ boundaries.set(layer.id, resolveHistogramBins({
30
+ values,
31
+ bin: x.bin,
32
+ domain: scale.domain,
33
+ nice: scale.nice ?? true,
34
+ zero: scale.zero ?? false
35
+ }).boundaries);
36
+ }
37
+ return boundaries;
38
+ }
39
+
40
+ function applySharedHistogramBoundaries(program, layerId, boundaries) {
41
+ if (boundaries === undefined) return program;
42
+ const layer = requireLayer(program, layerId);
43
+ let next = program;
44
+ for (const property of ["maxBins", "step"]) {
45
+ if (Object.hasOwn(layer.encoding.x.bin, property)) {
46
+ next = next.editSemantic({
47
+ property: `layer[${layerId}].encoding.x.bin.${property}`,
48
+ remove: true
49
+ });
50
+ }
51
+ }
52
+ return next.editSemantic({
53
+ property: `layer[${layerId}].encoding.x.bin.boundaries`,
54
+ value: boundaries
55
+ });
56
+ }
57
+
58
+ function replayDatasetId(cell, owner) {
59
+ return `${cell.id}-${owner}-data`;
60
+ }
61
+
62
+ function cellMaterializationPlan(program) {
63
+ const scaleIds = [...new Set(program.semanticSpec.layers.flatMap(layer =>
64
+ Object.values(layer.encoding ?? {})
65
+ .map(encoding => encoding?.scale)
66
+ .filter(id => id !== undefined)
67
+ ))];
68
+ const scales = scaleIds.map(id => ({
69
+ op: "rematerializeScale",
70
+ args: { id, guides: false, marks: false }
71
+ }));
72
+ return buildMaterializationPlan({ scales });
73
+ }
74
+
75
+ function deriveCellProgram(base, definition, cell, histogramBoundaries) {
76
+ let child = base.filterData({
77
+ id: cell.data,
78
+ source: definition.data,
79
+ field: definition.field,
80
+ oneOf: [cell.value]
81
+ });
82
+ const datasets = new Map([[definition.data, cell.data]]);
83
+ for (const replay of definition.dependencies.replay) {
84
+ const source = datasets.get(replay.source);
85
+ if (source === undefined) {
86
+ throw new Error(
87
+ `Facet replay source "${replay.source}" is not available in cell "${cell.id}".`
88
+ );
89
+ }
90
+ const id = replayDatasetId(cell, replay.id);
91
+ child = child.replayDerivedData({
92
+ id,
93
+ source,
94
+ transform: replay.transform
95
+ });
96
+ datasets.set(replay.id, id);
97
+ }
98
+ for (const layer of definition.dependencies.layers) {
99
+ const data = datasets.get(layer.data);
100
+ if (data === undefined) {
101
+ throw new Error(
102
+ `Facet layer "${layer.id}" has no replayed dataset in cell "${cell.id}".`
103
+ );
104
+ }
105
+ child = child.rebindLayerData({ id: layer.id, data });
106
+ child = applySharedHistogramBoundaries(
107
+ child,
108
+ layer.id,
109
+ histogramBoundaries.get(layer.id)
110
+ );
111
+ }
112
+ return applyMaterializationPlan(child, cellMaterializationPlan(child));
113
+ }
114
+
115
+ function applyResolvedDomains(program, childId, resolution, baseResolved) {
116
+ let next = program;
117
+ for (const [id, scaleResolution] of Object.entries(resolution.scales)) {
118
+ const current = next.resolvedScales[id];
119
+ if (current === undefined) {
120
+ throw new Error(`Facet child "${childId}" is missing resolved scale "${id}".`);
121
+ }
122
+ const shared = scaleResolution.policy === "shared"
123
+ ? baseResolved?.[id]
124
+ : undefined;
125
+ next = next._withResolvedScale(id, {
126
+ ...current,
127
+ ...shared,
128
+ domain: scaleResolution.childDomains[childId]
129
+ });
130
+ }
131
+ const marks = next.semanticSpec.layers.flatMap(layer => {
132
+ const step = getMarkMaterializationStep(next, layer);
133
+ if (step === undefined) return [];
134
+ return ["bar", "line", "area"].includes(layer.mark?.type)
135
+ ? [{ ...step, args: { ...step.args, scales: false } }]
136
+ : [step];
137
+ });
138
+ const guides = Object.keys(resolution.scales).flatMap(id =>
139
+ planScaleGuideRematerialization(next, id)
140
+ );
141
+ return applyMaterializationPlan(
142
+ next,
143
+ buildMaterializationPlan({ marks, guides })
144
+ );
145
+ }
146
+
147
+ export function deriveFacetChildren(
148
+ base,
149
+ definition,
150
+ {
151
+ closeInheritedAction = false,
152
+ stripTitle = false,
153
+ scales = {}
154
+ } = {}
155
+ ) {
156
+ const template = stripTitle && base.semanticSpec.title.text !== undefined
157
+ ? base.removeTitle()
158
+ : base;
159
+ const resolutionRequest = scales ?? {};
160
+ const xPolicy = resolutionRequest.x ?? "shared";
161
+ const bins = xPolicy === "shared"
162
+ ? sharedHistogramBoundaries(template)
163
+ : new Map();
164
+ const independentlyResolved = Object.fromEntries(definition.cells.map(cell => [
165
+ cell.id,
166
+ deriveCellProgram(template, definition, cell, bins)
167
+ ]));
168
+ const resolution = resolveFacetScaleDomains(
169
+ template.semanticSpec,
170
+ Object.fromEntries(Object.entries(independentlyResolved).map(([id, child]) => [
171
+ id,
172
+ child.resolvedScales
173
+ ])),
174
+ resolutionRequest,
175
+ template.resolvedScales
176
+ );
177
+ const resolvedChildren = Object.fromEntries(
178
+ Object.entries(independentlyResolved).map(([id, child]) => [
179
+ id,
180
+ applyResolvedDomains(child, id, resolution, template.resolvedScales)
181
+ ])
182
+ );
183
+ const sharedScales = Object.fromEntries(
184
+ Object.entries(resolution.scales)
185
+ .filter(([, value]) => value.policy === "shared")
186
+ .map(([id]) => [id, resolvedChildren[definition.cells[0].id].resolvedScales[id]])
187
+ );
188
+ return freezeOwned({
189
+ children: freezeOwned(Object.fromEntries(
190
+ Object.entries(resolvedChildren).map(([id, child]) => [
191
+ id,
192
+ closeInheritedAction
193
+ ? child._exitAction()
194
+ : child
195
+ ])
196
+ )),
197
+ sharedScales: freezeOwned(sharedScales),
198
+ resolution
199
+ });
200
+ }
@@ -0,0 +1,16 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateKeys } from "../../core/validation.js";
3
+ import { applyFacetGuideComposition } from
4
+ "../../materialization/facetGuides/index.js";
5
+
6
+ export const composeFacetGuides = action(
7
+ {
8
+ op: "composeFacetGuides",
9
+ description: "Apply outer-axis ownership and promote shared facet legends.",
10
+ scope: "composition"
11
+ },
12
+ function ({ layout, plot } = {}) {
13
+ validateKeys({ layout, plot }, ["layout", "plot"], "composeFacetGuides");
14
+ return applyFacetGuideComposition(this, { layout, plot });
15
+ }
16
+ );
@@ -0,0 +1 @@
1
+ export { registerFacetActions } from "./actions.js";
@@ -0,0 +1,69 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import { validateKeys } from "../../core/validation.js";
5
+ import { hasDataset } from "../../selectors/datasets.js";
6
+ import { requireLayer } from "../../selectors/layers.js";
7
+
8
+ const REPLAY_OPTIONS = Object.freeze(["id", "source", "transform"]);
9
+ const REBIND_OPTIONS = Object.freeze(["id", "data"]);
10
+ const MATERIALIZERS = Object.freeze({
11
+ filter: "materializeFilteredData",
12
+ regression: "materializeRegressionData",
13
+ density: "materializeDensityData",
14
+ interval: "materializeIntervalData",
15
+ boxSummary: "materializeBoxSummaryData",
16
+ boxOutlier: "materializeBoxOutlierData"
17
+ });
18
+
19
+ function requestedTransform(transform) {
20
+ if (!isPlainObject(transform)) {
21
+ throw new TypeError("replayDerivedData transform must be a plain object.");
22
+ }
23
+ const materialize = MATERIALIZERS[transform.type];
24
+ if (materialize === undefined) {
25
+ throw new Error(
26
+ `replayDerivedData does not support transform "${transform.type ?? "unknown"}".`
27
+ );
28
+ }
29
+ if (transform.type !== "density") return { transform, materialize };
30
+ const { resolved: _resolved, ...requested } = transform;
31
+ return { transform: requested, materialize };
32
+ }
33
+
34
+ export const replayDerivedData = action(
35
+ {
36
+ op: "replayDerivedData",
37
+ description: "Replay one stored derived-data transform for a facet cell."
38
+ },
39
+ function (args = {}) {
40
+ validateKeys(args, REPLAY_OPTIONS, "replayDerivedData");
41
+ const id = validateUserId(args.id, "Facet replay dataset id");
42
+ const source = validateUserId(args.source, "Facet replay source id");
43
+ const resolved = requestedTransform(args.transform);
44
+ return this
45
+ .createDerivedData({ id, source, transform: [resolved.transform] })
46
+ [resolved.materialize]({ id });
47
+ }
48
+ );
49
+
50
+ export const rebindLayerData = action(
51
+ {
52
+ op: "rebindLayerData",
53
+ description: "Rebind one facet-cell layer to its replayed dataset."
54
+ },
55
+ function (args = {}) {
56
+ validateKeys(args, REBIND_OPTIONS, "rebindLayerData");
57
+ const id = validateUserId(args.id, "Facet layer id");
58
+ const data = validateUserId(args.data, "Facet layer dataset id");
59
+ requireLayer(this, id);
60
+ if (!hasDataset(this, data)) {
61
+ throw new Error(`Facet layer dataset "${data}" does not exist.`);
62
+ }
63
+ return this.editSemantic({
64
+ property: `layer[${id}].data`,
65
+ value: data
66
+ });
67
+ }
68
+ );
69
+
@@ -0,0 +1,113 @@
1
+ import {
2
+ BAR_ORIENTATIONS,
3
+ resolveBarOrientation
4
+ } from "../../grammar/bars/policy.js";
5
+ import {
6
+ CARTESIAN_POSITION_CHANNELS,
7
+ POLAR_POSITION_CHANNELS
8
+ } from "../../core/vocabulary.js";
9
+ import { findSemanticScale } from "../../selectors/scales.js";
10
+
11
+ function hasEncoding(program, channel, { scaled = false, grid = false } = {}) {
12
+ return program.semanticSpec.layers.some(layer => {
13
+ const encoding = layer.encoding?.[channel];
14
+ if (!scaled && !grid) return encoding !== undefined;
15
+ if (encoding?.scale === undefined) return false;
16
+ if (!grid || POLAR_POSITION_CHANNELS.includes(channel)) return true;
17
+ const type = findSemanticScale(program, encoding.scale)?.type;
18
+ return type !== undefined && !["ordinal", "band", "point"].includes(type);
19
+ });
20
+ }
21
+
22
+ function positionalApplicability(program, channels) {
23
+ return Object.freeze(Object.fromEntries(
24
+ channels.map(channel => [
25
+ channel,
26
+ Object.freeze({
27
+ axis: hasEncoding(program, channel),
28
+ grid: hasEncoding(program, channel, { grid: true })
29
+ })
30
+ ])
31
+ ));
32
+ }
33
+
34
+ export function hasInferableLegend(program) {
35
+ return program.semanticSpec.layers.some(layer =>
36
+ (layer.mark?.type === "point" &&
37
+ layer.encoding?.opacity?.scale !== undefined) ||
38
+ (layer.mark?.type === "point" &&
39
+ layer.encoding?.size?.scale !== undefined) ||
40
+ (layer.mark?.type === "point" &&
41
+ layer.encoding?.color?.scale !== undefined &&
42
+ (layer.encoding?.shape?.scale !== undefined ||
43
+ ["sequential", "quantize", "quantile", "threshold"].includes(
44
+ findSemanticScale(program, layer.encoding.color.scale)?.type
45
+ ))) ||
46
+ (layer.mark?.type === "line" &&
47
+ ["color", "strokeDash"].some(
48
+ channel => layer.encoding?.[channel]?.scale !== undefined
49
+ )) ||
50
+ (["bar", "area", "arc", "rect"].includes(layer.mark?.type) &&
51
+ layer.encoding?.color?.scale !== undefined)
52
+ );
53
+ }
54
+
55
+ export function resolveGuideApplicability(program) {
56
+ const cartesian = positionalApplicability(
57
+ program,
58
+ CARTESIAN_POSITION_CHANNELS
59
+ );
60
+ const polar = positionalApplicability(program, POLAR_POSITION_CHANNELS);
61
+ return Object.freeze({
62
+ axes: Object.freeze({
63
+ cartesian: CARTESIAN_POSITION_CHANNELS.some(
64
+ channel => cartesian[channel].axis
65
+ ),
66
+ polar: POLAR_POSITION_CHANNELS.some(channel => polar[channel].axis)
67
+ }),
68
+ grid: Object.freeze({
69
+ cartesian: CARTESIAN_POSITION_CHANNELS.some(
70
+ channel => cartesian[channel].grid
71
+ ),
72
+ polar: POLAR_POSITION_CHANNELS.some(channel => polar[channel].grid),
73
+ directions: Object.freeze({
74
+ horizontal: cartesian.y.grid,
75
+ vertical: cartesian.x.grid,
76
+ theta: polar.theta.grid,
77
+ radial: polar.radius.grid
78
+ })
79
+ }),
80
+ legend: hasInferableLegend(program)
81
+ });
82
+ }
83
+
84
+ export function resolveAutomaticGridOptions(program) {
85
+ const applicability = resolveGuideApplicability(program);
86
+ const directions = applicability.grid.directions;
87
+ if (applicability.grid.polar && !applicability.grid.cartesian) {
88
+ return Object.freeze({
89
+ ...(directions.theta ? { theta: {} } : {}),
90
+ ...(directions.radial ? { radial: {} } : {})
91
+ });
92
+ }
93
+
94
+ const barOrientations = program.semanticSpec.layers
95
+ .map(resolveBarOrientation)
96
+ .filter(Boolean);
97
+ const positionedLayers = program.semanticSpec.layers.filter(
98
+ layer => layer.encoding?.x !== undefined && layer.encoding?.y !== undefined
99
+ );
100
+ const horizontalQuantitative = positionedLayers.length > 0 &&
101
+ positionedLayers.every(layer =>
102
+ layer.encoding.x.fieldType === "quantitative" &&
103
+ ["nominal", "ordinal", "temporal"].includes(layer.encoding.y.fieldType)
104
+ );
105
+ if (
106
+ horizontalQuantitative ||
107
+ (barOrientations.length > 0 &&
108
+ barOrientations.every(value => value === BAR_ORIENTATIONS.horizontal))
109
+ ) {
110
+ return Object.freeze({ horizontal: false, vertical: {} });
111
+ }
112
+ return Object.freeze({ horizontal: {}, vertical: false });
113
+ }