ggaction 0.0.2 → 0.0.3

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 (199) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +12 -1
  3. package/package.json +6 -1
  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/encodings/appearance.js +11 -0
  16. package/src/actions/encodings/barWidth.js +5 -3
  17. package/src/actions/encodings/color.js +120 -25
  18. package/src/actions/encodings/index.js +2 -0
  19. package/src/actions/encodings/offset.js +90 -72
  20. package/src/actions/encodings/position/apply.js +66 -0
  21. package/src/actions/encodings/position/policies/arc.js +26 -0
  22. package/src/actions/encodings/position/policies/bar.js +9 -5
  23. package/src/actions/encodings/position/policies/index.js +5 -1
  24. package/src/actions/encodings/position/policies/line.js +20 -3
  25. package/src/actions/encodings/position/resolve.js +57 -8
  26. package/src/actions/encodings/position.js +44 -90
  27. package/src/actions/encodings/ranged.js +28 -6
  28. package/src/actions/encodings/shared.js +17 -21
  29. package/src/actions/encodings/text.js +73 -0
  30. package/src/actions/errorBands/create.js +37 -29
  31. package/src/actions/errorBands/edit.js +206 -0
  32. package/src/actions/errorBands/index.js +9 -0
  33. package/src/actions/errorBands/options.js +39 -0
  34. package/src/actions/errorBars/create.js +50 -43
  35. package/src/actions/errorBars/edit.js +130 -0
  36. package/src/actions/errorBars/index.js +3 -0
  37. package/src/actions/errorBars/options.js +52 -0
  38. package/src/actions/facets/actions.js +162 -0
  39. package/src/actions/facets/derive.js +200 -0
  40. package/src/actions/facets/guides.js +16 -0
  41. package/src/actions/facets/index.js +1 -0
  42. package/src/actions/facets/replay.js +69 -0
  43. package/src/actions/guides/applicability.js +113 -0
  44. package/src/actions/guides/axes/axes.js +70 -16
  45. package/src/actions/guides/axes/edit.js +126 -0
  46. package/src/actions/guides/axes/index.js +12 -0
  47. package/src/actions/guides/axes/labels.js +21 -18
  48. package/src/actions/guides/axes/remove.js +60 -0
  49. package/src/actions/guides/axes/tickGroups.js +7 -5
  50. package/src/actions/guides/axes/ticks.js +9 -4
  51. package/src/actions/guides/axes/titles.js +7 -5
  52. package/src/actions/guides/grids/grid.js +144 -15
  53. package/src/actions/guides/grids/resolve.js +5 -13
  54. package/src/actions/guides/guides.js +17 -75
  55. package/src/actions/guides/index.js +6 -0
  56. package/src/actions/guides/legends/categorical/actions.js +48 -9
  57. package/src/actions/guides/legends/categorical/components.js +16 -16
  58. package/src/actions/guides/legends/categorical/index.js +14 -14
  59. package/src/actions/guides/legends/categorical/layout.js +3 -1
  60. package/src/actions/guides/legends/categorical/options.js +2 -5
  61. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  62. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  63. package/src/actions/guides/legends/continuous/common.js +7 -7
  64. package/src/actions/guides/legends/continuous/interval.js +5 -5
  65. package/src/actions/guides/legends/edit.js +2 -6
  66. package/src/actions/guides/legends/focused.js +63 -0
  67. package/src/actions/guides/legends/index.js +4 -0
  68. package/src/actions/guides/legends/remove.js +64 -0
  69. package/src/actions/guides/legends/size.js +12 -14
  70. package/src/actions/guides/polar/axes/facade.js +219 -0
  71. package/src/actions/guides/polar/axes/labels.js +188 -0
  72. package/src/actions/guides/polar/axes/lines.js +113 -0
  73. package/src/actions/guides/polar/axes/shared.js +112 -0
  74. package/src/actions/guides/polar/axes/ticks.js +164 -0
  75. package/src/actions/guides/polar/axes/titles.js +165 -0
  76. package/src/actions/guides/polar/axes.js +55 -0
  77. package/src/actions/guides/polar/grids.js +252 -0
  78. package/src/actions/guides/polar/index.js +2 -0
  79. package/src/actions/guides/polar/resolve.js +269 -0
  80. package/src/actions/index.js +4 -0
  81. package/src/actions/marks/arc/actions.js +239 -0
  82. package/src/actions/marks/arc/index.js +1 -0
  83. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  84. package/src/actions/marks/area/index.js +4 -0
  85. package/src/actions/marks/bar/create.js +26 -5
  86. package/src/actions/marks/bar/materialize.js +25 -31
  87. package/src/actions/marks/index.js +12 -4
  88. package/src/actions/marks/lifecycle.js +27 -0
  89. package/src/actions/marks/line/actions.js +373 -0
  90. package/src/actions/marks/line/index.js +1 -0
  91. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  92. package/src/actions/marks/point/index.js +1 -0
  93. package/src/actions/marks/rect/actions.js +165 -0
  94. package/src/actions/marks/rect/index.js +1 -0
  95. package/src/actions/marks/remove.js +214 -0
  96. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  97. package/src/actions/marks/rule/index.js +1 -0
  98. package/src/actions/marks/shared.js +156 -25
  99. package/src/actions/marks/text/actions.js +192 -0
  100. package/src/actions/marks/text/index.js +1 -0
  101. package/src/actions/primitives/createGraphics.js +13 -6
  102. package/src/actions/primitives/editGraphics.js +10 -3
  103. package/src/actions/primitives/semantic.js +26 -3
  104. package/src/actions/primitives/semanticValue.js +25 -3
  105. package/src/actions/regression/components.js +1 -1
  106. package/src/actions/regression/create.js +18 -1
  107. package/src/actions/regression/edit.js +247 -0
  108. package/src/actions/regression/index.js +3 -0
  109. package/src/actions/regression/resolve.js +3 -1
  110. package/src/actions/scales/consumers.js +15 -6
  111. package/src/actions/scales/definitions.js +13 -4
  112. package/src/actions/scales/edit.js +42 -19
  113. package/src/actions/scales/materialize.js +57 -408
  114. package/src/actions/selection/actions.js +38 -24
  115. package/src/actions/selection/index.js +2 -0
  116. package/src/actions/titles/actions.js +63 -2
  117. package/src/actions/titles/index.js +2 -0
  118. package/src/actions/titles/resolve.js +12 -17
  119. package/src/composition.js +10 -0
  120. package/src/core/ChartProgram.js +200 -2
  121. package/src/core/action.js +10 -0
  122. package/src/core/validation.js +16 -0
  123. package/src/core/vocabulary.js +154 -4
  124. package/src/grammar/arcs.js +154 -0
  125. package/src/grammar/bars/aggregate.js +2 -2
  126. package/src/grammar/bars/geometry.js +3 -3
  127. package/src/grammar/bars/policy.js +7 -1
  128. package/src/grammar/coordinates.js +3 -7
  129. package/src/grammar/density.js +20 -1
  130. package/src/grammar/facets/dependencies.js +185 -0
  131. package/src/grammar/facets/guides.js +257 -0
  132. package/src/grammar/facets/scales.js +254 -0
  133. package/src/grammar/facets.js +122 -0
  134. package/src/grammar/lineSeries.js +111 -3
  135. package/src/grammar/polar.js +102 -0
  136. package/src/grammar/polarGuides.js +243 -0
  137. package/src/grammar/polarLineCommands.js +43 -0
  138. package/src/grammar/polarPaths.js +161 -0
  139. package/src/grammar/positionCompatibility.js +18 -2
  140. package/src/grammar/rects.js +26 -0
  141. package/src/grammar/scales/continuous.js +6 -1
  142. package/src/grammar/scales/fields.js +7 -0
  143. package/src/grammar/scales/ordinal.js +5 -2
  144. package/src/grammar/scales/policies.js +1 -1
  145. package/src/grammar/schemas/graphic.js +1 -1
  146. package/src/grammar/schemas/graphicBounds.js +64 -9
  147. package/src/grammar/schemas/graphicTree.js +44 -10
  148. package/src/grammar/schemas/semanticPath.js +31 -9
  149. package/src/grammar/seriesLayout.js +13 -2
  150. package/src/grammar/text.js +71 -0
  151. package/src/grammar/ticks.js +50 -13
  152. package/src/index.js +1 -0
  153. package/src/layout/composition.js +310 -0
  154. package/src/layout/facets.js +131 -0
  155. package/src/layout/title.js +6 -0
  156. package/src/materialization/bars/aggregate.js +5 -2
  157. package/src/materialization/bars/grouped.js +75 -37
  158. package/src/materialization/composition.js +152 -0
  159. package/src/materialization/compositionSnapshot.js +114 -0
  160. package/src/materialization/dependencies.js +71 -16
  161. package/src/materialization/encodings.js +14 -28
  162. package/src/materialization/facetGuides.js +426 -0
  163. package/src/materialization/facets.js +214 -0
  164. package/src/materialization/graphicHierarchy.js +15 -5
  165. package/src/materialization/guides/resources.js +46 -0
  166. package/src/materialization/marks.js +220 -14
  167. package/src/materialization/planner.js +105 -12
  168. package/src/materialization/rect.js +138 -0
  169. package/src/materialization/rectConfig.js +38 -0
  170. package/src/materialization/rowEncoding.js +33 -0
  171. package/src/materialization/scales/map.js +40 -0
  172. package/src/materialization/scales/policies/arc.js +37 -0
  173. package/src/materialization/scales/policies/bar.js +132 -0
  174. package/src/materialization/scales/policies/series.js +76 -0
  175. package/src/materialization/scales/resolve.js +326 -0
  176. package/src/materialization/selection/items/arc.js +29 -0
  177. package/src/materialization/selection/items/bar.js +255 -0
  178. package/src/materialization/selection/items/common.js +131 -0
  179. package/src/materialization/selection/items/index.js +6 -0
  180. package/src/materialization/selection/items/path.js +59 -0
  181. package/src/materialization/selection/items/point.js +55 -0
  182. package/src/materialization/selection/items/rect.js +23 -0
  183. package/src/materialization/selection/items/rule.js +25 -0
  184. package/src/materialization/selection/policies/arc.js +10 -0
  185. package/src/materialization/selection/policies/area.js +1 -1
  186. package/src/materialization/selection/policies/bar.js +1 -1
  187. package/src/materialization/selection/policies/index.js +5 -1
  188. package/src/materialization/selection/policies/line.js +1 -1
  189. package/src/materialization/selection/policies/point.js +1 -1
  190. package/src/materialization/selection/policies/rect.js +10 -0
  191. package/src/materialization/selection/policies/rule.js +1 -1
  192. package/src/materialization/selection/styles.js +24 -16
  193. package/src/materialization/text.js +156 -0
  194. package/src/renderers/canvas/index.js +68 -22
  195. package/types/extension.d.ts +1 -0
  196. package/types/index.d.ts +56 -1
  197. package/types/program.d.ts +664 -27
  198. package/src/actions/marks/line.js +0 -248
  199. package/src/materialization/selection/items.js +0 -503
@@ -1,15 +1,8 @@
1
1
  import { action } from "../../core/action.js";
2
- import { validateKeys, validatePositiveFinite } from "../../core/validation.js";
3
- import {
4
- normalizeStrokeDashPattern,
5
- validateOpacityValue
6
- } from "../../grammar/scales.js";
7
- import {
8
- validateRuleStroke,
9
- validateRuleStrokeWidth
10
- } from "../../grammar/ruleAppearance.js";
2
+ import { validateKeys } from "../../core/validation.js";
11
3
  import { findLayer } from "../../selectors/layers.js";
12
4
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
5
+ import { resolveErrorBarAppearance } from "./options.js";
13
6
  import { resolveErrorBar } from "./resolve.js";
14
7
 
15
8
  const OPTIONS = Object.freeze([
@@ -29,24 +22,22 @@ const OPTIONS = Object.freeze([
29
22
  ]);
30
23
 
31
24
  function resolveAppearance(args) {
32
- const caps = args.caps ?? true;
33
- if (typeof caps !== "boolean") {
34
- throw new TypeError("createErrorBar caps must be a boolean.");
35
- }
36
- const capSize = args.capSize ?? 8;
37
- validatePositiveFinite(capSize, "createErrorBar capSize");
38
- const stroke = validateRuleStroke(
39
- args.stroke ?? DEFAULT_COLORS.mark,
40
- "createErrorBar stroke"
41
- );
42
- const strokeWidth = validateRuleStrokeWidth(
43
- args.strokeWidth ?? 1.5,
44
- "createErrorBar strokeWidth"
25
+ const appearanceArgs = Object.fromEntries(
26
+ ["caps", "capSize", "stroke", "strokeWidth", "strokeDash", "opacity"]
27
+ .filter(key => Object.hasOwn(args, key))
28
+ .map(key => [key, args[key]])
45
29
  );
46
- const strokeDash = args.strokeDash ?? "solid";
47
- normalizeStrokeDashPattern(strokeDash);
48
- const opacity = validateOpacityValue(args.opacity ?? 1, "createErrorBar opacity");
49
- return { caps, capSize, stroke, strokeWidth, strokeDash, opacity };
30
+ return resolveErrorBarAppearance(appearanceArgs, {
31
+ defaults: {
32
+ caps: true,
33
+ capSize: 8,
34
+ stroke: DEFAULT_COLORS.mark,
35
+ strokeWidth: 1.5,
36
+ strokeDash: "solid",
37
+ opacity: 1
38
+ },
39
+ operation: "createErrorBar"
40
+ });
50
41
  }
51
42
 
52
43
  function positionArgs(resolved) {
@@ -115,30 +106,46 @@ export const createErrorBar = action(
115
106
  .encodeStrokeDash({ target: resolved.id, value: appearance.strokeDash })
116
107
  .encodeOpacity({ target: resolved.id, value: appearance.opacity });
117
108
 
118
- if (!appearance.caps) return next;
119
-
120
109
  const intervalLayer = findLayer(next, resolved.id);
121
- for (const [id, field] of [
122
- [resolved.lowerCapId, resolved.fields.lower],
123
- [resolved.upperCapId, resolved.fields.upper]
124
- ]) {
125
- next = next.createErrorBarCap({
126
- id,
110
+ if (appearance.caps) {
111
+ for (const [id, field] of [
112
+ [resolved.lowerCapId, resolved.fields.lower],
113
+ [resolved.upperCapId, resolved.fields.upper]
114
+ ]) {
115
+ next = next.createErrorBarCap({
116
+ id,
117
+ data: resolved.dataId,
118
+ orientation: resolved.orientation,
119
+ positionField: resolved.position.field,
120
+ positionFieldType: resolved.position.fieldType,
121
+ intervalField: field,
122
+ coordinate: resolved.coordinate,
123
+ positionScale: intervalLayer.encoding[resolved.position.channel].scale,
124
+ intervalScale: intervalLayer.encoding[resolved.interval.channel].scale,
125
+ capSize: appearance.capSize,
126
+ stroke: appearance.stroke,
127
+ strokeWidth: appearance.strokeWidth,
128
+ strokeDash: appearance.strokeDash,
129
+ opacity: appearance.opacity
130
+ });
131
+ }
132
+ }
133
+ return next._withMarkConfig(resolved.id, {
134
+ ...next.markConfigs[resolved.id],
135
+ errorBar: {
127
136
  data: resolved.dataId,
128
137
  orientation: resolved.orientation,
129
138
  positionField: resolved.position.field,
130
139
  positionFieldType: resolved.position.fieldType,
131
- intervalField: field,
140
+ lowerField: resolved.fields.lower,
141
+ upperField: resolved.fields.upper,
132
142
  coordinate: resolved.coordinate,
133
143
  positionScale: intervalLayer.encoding[resolved.position.channel].scale,
134
144
  intervalScale: intervalLayer.encoding[resolved.interval.channel].scale,
135
- capSize: appearance.capSize,
136
- stroke: appearance.stroke,
137
- strokeWidth: appearance.strokeWidth,
138
- strokeDash: appearance.strokeDash,
139
- opacity: appearance.opacity
140
- });
141
- }
142
- return next;
145
+ lowerCapId: resolved.lowerCapId,
146
+ upperCapId: resolved.upperCapId,
147
+ ...appearance
148
+ }
149
+ });
143
150
  }
144
151
  );
@@ -0,0 +1,130 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateUserId } from "../../core/identifiers.js";
3
+ import { validateKeys } from "../../core/validation.js";
4
+ import { findLayer, resolveEligibleLayer } from "../../selectors/layers.js";
5
+ import {
6
+ ERROR_BAR_EDIT_OPTIONS,
7
+ resolveErrorBarAppearance
8
+ } from "./options.js";
9
+
10
+ const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
11
+
12
+ function resolveOwner(program, requested) {
13
+ const target = requested === undefined
14
+ ? undefined
15
+ : validateUserId(requested, "Error-bar id");
16
+ return resolveEligibleLayer(program, {
17
+ target,
18
+ label: "error bar",
19
+ predicate: layer =>
20
+ layer.mark?.type === "rule" &&
21
+ program.markConfigs[layer.id]?.errorBar !== undefined
22
+ });
23
+ }
24
+
25
+ function removeCap(program, id) {
26
+ if (findLayer(program, id) === undefined) return program;
27
+ return program
28
+ .editSemantic({ property: `layer[${id}]`, remove: true })
29
+ .editGraphics({ target: id, remove: true })
30
+ ._withoutMaterializationConfig(["marks", id]);
31
+ }
32
+
33
+ function capArgs(config, id, intervalField) {
34
+ return {
35
+ id,
36
+ data: config.data,
37
+ orientation: config.orientation,
38
+ positionField: config.positionField,
39
+ positionFieldType: config.positionFieldType,
40
+ intervalField,
41
+ coordinate: config.coordinate,
42
+ positionScale: config.positionScale,
43
+ intervalScale: config.intervalScale,
44
+ capSize: config.capSize,
45
+ stroke: config.stroke,
46
+ strokeWidth: config.strokeWidth,
47
+ strokeDash: config.strokeDash,
48
+ opacity: config.opacity
49
+ };
50
+ }
51
+
52
+ function rematerializeRuleAppearance(program, id, config, fixedSpan) {
53
+ return program
54
+ .editSemantic({
55
+ property: `layer[${id}].encoding.strokeDash.datum`,
56
+ value: config.strokeDash
57
+ })
58
+ ._withMarkConfig(id, {
59
+ ...program.markConfigs[id],
60
+ stroke: config.stroke,
61
+ strokeWidth: config.strokeWidth,
62
+ strokeDash: config.strokeDash,
63
+ opacity: config.opacity,
64
+ ...(fixedSpan === undefined ? {} : { fixedSpan })
65
+ })
66
+ .rematerializeRuleMark({ id });
67
+ }
68
+
69
+ export const rematerializeErrorBar = action(
70
+ {
71
+ op: "rematerializeErrorBar",
72
+ description: "Reconcile one error bar and its owned caps."
73
+ },
74
+ function (args = {}) {
75
+ validateKeys(args, REMATERIALIZE_OPTIONS, "rematerializeErrorBar");
76
+ const id = validateUserId(args.id, "Error-bar id");
77
+ const layer = findLayer(this, id);
78
+ const config = this.markConfigs[id]?.errorBar;
79
+ if (layer?.mark?.type !== "rule" || config === undefined) {
80
+ throw new Error(`Unknown error bar "${id}".`);
81
+ }
82
+ let next = rematerializeRuleAppearance(this, id, config);
83
+ const capDefinitions = [
84
+ [config.lowerCapId, config.lowerField],
85
+ [config.upperCapId, config.upperField]
86
+ ];
87
+ if (!config.caps) {
88
+ for (const [capId] of capDefinitions) next = removeCap(next, capId);
89
+ return next;
90
+ }
91
+ const span = {
92
+ orientation: config.orientation === "vertical" ? "horizontal" : "vertical",
93
+ size: config.capSize
94
+ };
95
+ for (const [capId, field] of capDefinitions) {
96
+ if (findLayer(next, capId) === undefined) {
97
+ next = next.createErrorBarCap(capArgs(config, capId, field));
98
+ } else {
99
+ next = rematerializeRuleAppearance(next, capId, config, span);
100
+ }
101
+ }
102
+ return next;
103
+ }
104
+ );
105
+
106
+ export const editErrorBar = action(
107
+ {
108
+ op: "editErrorBar",
109
+ description: "Edit one error bar and its owned cap appearance."
110
+ },
111
+ function (args = {}) {
112
+ validateKeys(args, ERROR_BAR_EDIT_OPTIONS, "editErrorBar");
113
+ const editable = ERROR_BAR_EDIT_OPTIONS.filter(option => option !== "target");
114
+ if (!editable.some(option => Object.hasOwn(args, option))) {
115
+ throw new Error("editErrorBar requires at least one appearance change.");
116
+ }
117
+ const owner = resolveOwner(this, args.target);
118
+ const current = this.markConfigs[owner.id].errorBar;
119
+ const appearance = resolveErrorBarAppearance(args, {
120
+ defaults: current,
121
+ operation: "editErrorBar"
122
+ });
123
+ return this
124
+ ._withMarkConfig(owner.id, {
125
+ ...this.markConfigs[owner.id],
126
+ errorBar: { ...current, ...appearance }
127
+ })
128
+ .rematerializeErrorBar({ id: owner.id });
129
+ }
130
+ );
@@ -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.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.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.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.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.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
+ );