ggaction 0.0.1

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 (222) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/package.json +84 -0
  5. package/src/AGENTS.md +119 -0
  6. package/src/ChartProgram.js +10 -0
  7. package/src/actions/boxPlots/components.js +88 -0
  8. package/src/actions/boxPlots/create.js +84 -0
  9. package/src/actions/boxPlots/index.js +10 -0
  10. package/src/actions/boxPlots/materialize.js +183 -0
  11. package/src/actions/boxPlots/options.js +147 -0
  12. package/src/actions/boxPlots/resolve.js +38 -0
  13. package/src/actions/canvas/actions.js +144 -0
  14. package/src/actions/canvas/index.js +6 -0
  15. package/src/actions/coordinates/actions.js +85 -0
  16. package/src/actions/coordinates/index.js +5 -0
  17. package/src/actions/data/box.js +40 -0
  18. package/src/actions/data/create.js +33 -0
  19. package/src/actions/data/density.js +62 -0
  20. package/src/actions/data/derived.js +52 -0
  21. package/src/actions/data/filter.js +168 -0
  22. package/src/actions/data/index.js +32 -0
  23. package/src/actions/data/interval.js +69 -0
  24. package/src/actions/data/regression.js +71 -0
  25. package/src/actions/data/shared.js +25 -0
  26. package/src/actions/encodings/appearance.js +243 -0
  27. package/src/actions/encodings/barWidth.js +60 -0
  28. package/src/actions/encodings/color.js +329 -0
  29. package/src/actions/encodings/density.js +225 -0
  30. package/src/actions/encodings/histogram.js +50 -0
  31. package/src/actions/encodings/index.js +23 -0
  32. package/src/actions/encodings/offset.js +104 -0
  33. package/src/actions/encodings/position/policies/area.js +24 -0
  34. package/src/actions/encodings/position/policies/bar.js +110 -0
  35. package/src/actions/encodings/position/policies/common.js +17 -0
  36. package/src/actions/encodings/position/policies/index.js +25 -0
  37. package/src/actions/encodings/position/policies/line.js +74 -0
  38. package/src/actions/encodings/position/policies/point.js +19 -0
  39. package/src/actions/encodings/position/policies/rule.js +17 -0
  40. package/src/actions/encodings/position/resolve.js +170 -0
  41. package/src/actions/encodings/position.js +176 -0
  42. package/src/actions/encodings/ranged.js +276 -0
  43. package/src/actions/encodings/ruleAppearance.js +47 -0
  44. package/src/actions/encodings/shared.js +164 -0
  45. package/src/actions/encodings/strokeDash.js +145 -0
  46. package/src/actions/errorBands/components.js +81 -0
  47. package/src/actions/errorBands/create.js +159 -0
  48. package/src/actions/errorBands/index.js +7 -0
  49. package/src/actions/errorBands/resolve.js +28 -0
  50. package/src/actions/errorBars/components.js +62 -0
  51. package/src/actions/errorBars/create.js +144 -0
  52. package/src/actions/errorBars/index.js +7 -0
  53. package/src/actions/errorBars/resolve.js +25 -0
  54. package/src/actions/guides/axes/axes.js +204 -0
  55. package/src/actions/guides/axes/axis.js +131 -0
  56. package/src/actions/guides/axes/index.js +17 -0
  57. package/src/actions/guides/axes/labels.js +231 -0
  58. package/src/actions/guides/axes/lines.js +149 -0
  59. package/src/actions/guides/axes/policy.js +155 -0
  60. package/src/actions/guides/axes/tickGroups.js +149 -0
  61. package/src/actions/guides/axes/ticks.js +179 -0
  62. package/src/actions/guides/axes/titles.js +264 -0
  63. package/src/actions/guides/grids/grid.js +228 -0
  64. package/src/actions/guides/grids/index.js +1 -0
  65. package/src/actions/guides/grids/resolve.js +258 -0
  66. package/src/actions/guides/guides.js +138 -0
  67. package/src/actions/guides/index.js +11 -0
  68. package/src/actions/guides/legends/categorical/actions.js +329 -0
  69. package/src/actions/guides/legends/categorical/components.js +140 -0
  70. package/src/actions/guides/legends/categorical/index.js +49 -0
  71. package/src/actions/guides/legends/categorical/layout.js +445 -0
  72. package/src/actions/guides/legends/categorical/options.js +188 -0
  73. package/src/actions/guides/legends/categorical/recipes.js +120 -0
  74. package/src/actions/guides/legends/categorical/resolve.js +131 -0
  75. package/src/actions/guides/legends/categorical/symbols.js +280 -0
  76. package/src/actions/guides/legends/categorical/validation.js +27 -0
  77. package/src/actions/guides/legends/continuous/common.js +273 -0
  78. package/src/actions/guides/legends/continuous/gradient.js +363 -0
  79. package/src/actions/guides/legends/continuous/index.js +25 -0
  80. package/src/actions/guides/legends/continuous/interval.js +234 -0
  81. package/src/actions/guides/legends/continuous/opacity.js +382 -0
  82. package/src/actions/guides/legends/edit.js +314 -0
  83. package/src/actions/guides/legends/index.js +9 -0
  84. package/src/actions/guides/legends/size.js +220 -0
  85. package/src/actions/guides/tickValues.js +84 -0
  86. package/src/actions/index.js +31 -0
  87. package/src/actions/intervals/resolve.js +352 -0
  88. package/src/actions/marks/area.js +369 -0
  89. package/src/actions/marks/bar/create.js +46 -0
  90. package/src/actions/marks/bar/edit.js +90 -0
  91. package/src/actions/marks/bar/index.js +9 -0
  92. package/src/actions/marks/bar/materialize.js +130 -0
  93. package/src/actions/marks/index.js +13 -0
  94. package/src/actions/marks/line.js +248 -0
  95. package/src/actions/marks/point.js +420 -0
  96. package/src/actions/marks/rule.js +287 -0
  97. package/src/actions/marks/shared.js +112 -0
  98. package/src/actions/primitives/createGraphics.js +163 -0
  99. package/src/actions/primitives/editGraphics.js +208 -0
  100. package/src/actions/primitives/index.js +9 -0
  101. package/src/actions/primitives/semantic.js +168 -0
  102. package/src/actions/primitives/semanticValue.js +182 -0
  103. package/src/actions/regression/components.js +216 -0
  104. package/src/actions/regression/create.js +102 -0
  105. package/src/actions/regression/index.js +23 -0
  106. package/src/actions/regression/resolve.js +57 -0
  107. package/src/actions/scales/consumers.js +283 -0
  108. package/src/actions/scales/create.js +148 -0
  109. package/src/actions/scales/definitions.js +319 -0
  110. package/src/actions/scales/edit.js +290 -0
  111. package/src/actions/scales/index.js +11 -0
  112. package/src/actions/scales/materialize.js +466 -0
  113. package/src/actions/scales/quantitativeColor.js +45 -0
  114. package/src/actions/selection/actions.js +384 -0
  115. package/src/actions/selection/index.js +23 -0
  116. package/src/actions/titles/actions.js +235 -0
  117. package/src/actions/titles/index.js +19 -0
  118. package/src/actions/titles/resolve.js +321 -0
  119. package/src/core/ChartProgram.js +316 -0
  120. package/src/core/action.js +120 -0
  121. package/src/core/identifiers.js +28 -0
  122. package/src/core/immutable.js +94 -0
  123. package/src/core/specs.js +29 -0
  124. package/src/core/textMetrics.js +23 -0
  125. package/src/core/validation.js +54 -0
  126. package/src/core/vocabulary.js +58 -0
  127. package/src/extension.js +2 -0
  128. package/src/grammar/aggregate.js +259 -0
  129. package/src/grammar/areaSeries.js +217 -0
  130. package/src/grammar/bars/aggregate.js +98 -0
  131. package/src/grammar/bars/geometry.js +54 -0
  132. package/src/grammar/bars/policy.js +90 -0
  133. package/src/grammar/boxPlot.js +140 -0
  134. package/src/grammar/coordinates.js +25 -0
  135. package/src/grammar/curveCommands.js +356 -0
  136. package/src/grammar/density.js +296 -0
  137. package/src/grammar/filter.js +169 -0
  138. package/src/grammar/histogram.js +332 -0
  139. package/src/grammar/interval.js +343 -0
  140. package/src/grammar/lineSeries.js +197 -0
  141. package/src/grammar/markFilter.js +46 -0
  142. package/src/grammar/markSelection.js +326 -0
  143. package/src/grammar/palettes.js +208 -0
  144. package/src/grammar/pathCommands.js +86 -0
  145. package/src/grammar/pointShapes.js +164 -0
  146. package/src/grammar/positionCompatibility.js +32 -0
  147. package/src/grammar/regression.js +578 -0
  148. package/src/grammar/ruleAppearance.js +12 -0
  149. package/src/grammar/rules.js +90 -0
  150. package/src/grammar/scales/appearance.js +185 -0
  151. package/src/grammar/scales/color.js +349 -0
  152. package/src/grammar/scales/continuous.js +111 -0
  153. package/src/grammar/scales/definition.js +153 -0
  154. package/src/grammar/scales/discretized.js +159 -0
  155. package/src/grammar/scales/fields.js +133 -0
  156. package/src/grammar/scales/index.js +12 -0
  157. package/src/grammar/scales/mapping.js +27 -0
  158. package/src/grammar/scales/ordinal.js +221 -0
  159. package/src/grammar/scales/policies.js +50 -0
  160. package/src/grammar/scales/temporal.js +53 -0
  161. package/src/grammar/scales/transformed.js +236 -0
  162. package/src/grammar/scales/types.js +102 -0
  163. package/src/grammar/scales/validation.js +66 -0
  164. package/src/grammar/scales.js +1 -0
  165. package/src/grammar/schemas/concreteGraphic.js +73 -0
  166. package/src/grammar/schemas/graphic.js +131 -0
  167. package/src/grammar/schemas/graphicBounds.js +146 -0
  168. package/src/grammar/schemas/graphicTree.js +133 -0
  169. package/src/grammar/schemas/mark.js +4 -0
  170. package/src/grammar/schemas/semanticPath.js +174 -0
  171. package/src/grammar/seriesLayout.js +76 -0
  172. package/src/grammar/ticks.js +149 -0
  173. package/src/grammar/transforms.js +37 -0
  174. package/src/index.js +2 -0
  175. package/src/layout/canvas.js +114 -0
  176. package/src/layout/grid.js +17 -0
  177. package/src/layout/legend.js +51 -0
  178. package/src/layout/text.js +62 -0
  179. package/src/layout/title.js +107 -0
  180. package/src/materialization/bars/aggregate.js +184 -0
  181. package/src/materialization/bars/grouped.js +117 -0
  182. package/src/materialization/bars/histogram.js +163 -0
  183. package/src/materialization/bars/ranged.js +53 -0
  184. package/src/materialization/bars/resolve.js +56 -0
  185. package/src/materialization/dataProvenance.js +15 -0
  186. package/src/materialization/density.js +29 -0
  187. package/src/materialization/dependencies.js +134 -0
  188. package/src/materialization/encodings.js +50 -0
  189. package/src/materialization/graphicHierarchy.js +104 -0
  190. package/src/materialization/legends.js +20 -0
  191. package/src/materialization/marks.js +110 -0
  192. package/src/materialization/planner.js +38 -0
  193. package/src/materialization/selection/filter.js +40 -0
  194. package/src/materialization/selection/items.js +503 -0
  195. package/src/materialization/selection/path.js +37 -0
  196. package/src/materialization/selection/point.js +118 -0
  197. package/src/materialization/selection/policies/area.js +10 -0
  198. package/src/materialization/selection/policies/bar.js +10 -0
  199. package/src/materialization/selection/policies/index.js +40 -0
  200. package/src/materialization/selection/policies/line.js +10 -0
  201. package/src/materialization/selection/policies/point.js +10 -0
  202. package/src/materialization/selection/policies/rule.js +10 -0
  203. package/src/materialization/selection/state.js +67 -0
  204. package/src/materialization/selection/styles.js +183 -0
  205. package/src/renderers/canvas/circle.js +62 -0
  206. package/src/renderers/canvas/index.js +132 -0
  207. package/src/renderers/canvas/line.js +54 -0
  208. package/src/renderers/canvas/path.js +98 -0
  209. package/src/renderers/canvas/rect.js +57 -0
  210. package/src/renderers/canvas/text.js +96 -0
  211. package/src/renderers/canvas/validation.js +23 -0
  212. package/src/renderers/png.js +29 -0
  213. package/src/selectors/coordinates.js +13 -0
  214. package/src/selectors/datasets.js +13 -0
  215. package/src/selectors/index.js +4 -0
  216. package/src/selectors/layers.js +32 -0
  217. package/src/selectors/scales.js +21 -0
  218. package/src/theme/defaults.js +13 -0
  219. package/types/extension.d.ts +14 -0
  220. package/types/index.d.ts +81 -0
  221. package/types/png.d.ts +14 -0
  222. package/types/program.d.ts +1096 -0
@@ -0,0 +1,164 @@
1
+ import { validateUserId } from "../../core/identifiers.js";
2
+ import { findDataset } from "../../selectors/datasets.js";
3
+ import { resolveEligibleLayer } from "../../selectors/layers.js";
4
+ import { hasMaterializedLegend } from "../../materialization/legends.js";
5
+ import { findSemanticScale } from "../../selectors/scales.js";
6
+ import { validateKeys } from "../../core/validation.js";
7
+
8
+ export function validateOptions(args, supported, operation) {
9
+ validateKeys(args, supported, operation);
10
+ }
11
+
12
+ export function validateLineSeriesCompatibility(layer, channel, field) {
13
+ if (layer.mark?.type !== "line") return;
14
+ for (const companion of ["group", "color", "strokeDash"]) {
15
+ if (companion === channel) continue;
16
+ const companionField = layer.encoding?.[companion]?.field;
17
+ if (companionField !== undefined && companionField !== field) {
18
+ throw new Error(
19
+ `Line ${channel} field "${field}" must match ${companion} field "${companionField}".`
20
+ );
21
+ }
22
+ }
23
+ }
24
+
25
+ export function resolveReassignmentScaleOptions(encoding, options) {
26
+ if (encoding?.scale === undefined || options?.id !== undefined) return options;
27
+ return { ...options, id: encoding.scale };
28
+ }
29
+
30
+ export function applyEncodingScale(
31
+ program,
32
+ definition,
33
+ options = {},
34
+ { reassignment = false } = {}
35
+ ) {
36
+ const existing = findSemanticScale(program, definition.id);
37
+ if (existing === undefined) {
38
+ return program.createScale(definition);
39
+ }
40
+ if (existing.type !== definition.type) {
41
+ throw new Error(
42
+ `Scale "${definition.id}" cannot change type from "${existing.type}" to "${definition.type}".`
43
+ );
44
+ }
45
+
46
+ const patch = { id: definition.id };
47
+ for (const property of [
48
+ "domain", "range", "nice", "zero", "clamp", "reverse",
49
+ "base", "exponent", "constant", "paddingInner", "paddingOuter",
50
+ "padding", "align", "unknown", "interpolate"
51
+ ]) {
52
+ if (Object.hasOwn(options, property)) patch[property] = definition[property];
53
+ }
54
+ if (Object.hasOwn(options, "palette")) patch.range = definition.range;
55
+
56
+ return Object.keys(patch).length === 1 || !reassignment
57
+ ? program.createScale(definition)
58
+ : program.editScale(patch);
59
+ }
60
+
61
+ export function rebindPositionGuides(
62
+ program,
63
+ channel,
64
+ previousScale,
65
+ nextScale,
66
+ target
67
+ ) {
68
+ if (previousScale === undefined || previousScale === nextScale) return program;
69
+
70
+ const axis = program.semanticSpec.guides.axis?.[channel];
71
+ const direction = channel === "x" ? "vertical" : "horizontal";
72
+ const grid = program.semanticSpec.guides.grid?.[direction];
73
+ const ownsAxis = axis?.scale === previousScale;
74
+ const ownsGrid = grid?.scale === previousScale;
75
+ if (!ownsAxis && !ownsGrid) return program;
76
+
77
+ const remaining = program.semanticSpec.layers.some(layer =>
78
+ layer.id !== target && layer.encoding?.[channel]?.scale === previousScale
79
+ );
80
+ if (remaining) {
81
+ throw new Error(
82
+ `Cannot rebind ${channel} guides from shared scale "${previousScale}" while it has other consumers.`
83
+ );
84
+ }
85
+
86
+ let next = program;
87
+ if (ownsAxis) {
88
+ next = next.editSemantic({
89
+ property: `guide.axis.${channel}.scale`,
90
+ value: nextScale
91
+ });
92
+ for (const component of ["ticks", "labels", "title"]) {
93
+ const config = next.guideConfigs.axis?.[channel]?.[component];
94
+ if (config?.scale === previousScale) {
95
+ next = next._withGuideConfig(channel, component, {
96
+ ...config,
97
+ scale: nextScale
98
+ });
99
+ }
100
+ }
101
+ }
102
+ if (ownsGrid) {
103
+ next = next
104
+ .editSemantic({
105
+ property: `guide.grid.${direction}.scale`,
106
+ value: nextScale
107
+ })
108
+ ._withGridConfig(direction, {
109
+ ...next.guideConfigs.grid[direction],
110
+ scale: nextScale
111
+ });
112
+ }
113
+ return next;
114
+ }
115
+
116
+ export function rematerializeExistingLegend(program) {
117
+ return hasMaterializedLegend(program)
118
+ ? program.rematerializeLegend()
119
+ : program;
120
+ }
121
+
122
+ export function resolveTarget(
123
+ program,
124
+ target,
125
+ supportedTypes = ["point", "line"],
126
+ label = "position mark"
127
+ ) {
128
+ const requested = target === undefined
129
+ ? undefined
130
+ : validateUserId(target, "Mark id");
131
+ const layer = resolveEligibleLayer(program, {
132
+ target: requested,
133
+ predicate: candidate => supportedTypes.includes(candidate.mark?.type),
134
+ label
135
+ });
136
+ const id = layer.id;
137
+
138
+ const dataset = findDataset(program, layer.data);
139
+
140
+ if (dataset === undefined) {
141
+ throw new Error(`Mark "${id}" requires an existing dataset.`);
142
+ }
143
+
144
+ const expectedGraphic = {
145
+ point: ["circle", "rect", "collection"],
146
+ line: "path",
147
+ bar: "rect",
148
+ area: "path",
149
+ rule: "line"
150
+ }[layer.mark.type];
151
+
152
+ const graphicType = program.graphicSpec.objects[id]?.type;
153
+ const matches = Array.isArray(expectedGraphic)
154
+ ? expectedGraphic.includes(graphicType)
155
+ : graphicType === expectedGraphic;
156
+ if (!matches) {
157
+ const label = Array.isArray(expectedGraphic)
158
+ ? expectedGraphic.join(" or ")
159
+ : expectedGraphic;
160
+ throw new Error(`Mark "${id}" requires ${label} graphics.`);
161
+ }
162
+
163
+ return { id, dataset, layer };
164
+ }
@@ -0,0 +1,145 @@
1
+ import { action } from "../../core/action.js";
2
+ import {
3
+ normalizeStrokeDashPattern,
4
+ readNominalField,
5
+ validateNominalFieldType
6
+ } from "../../grammar/scales.js";
7
+ import { resolveStrokeDashScaleDefinition } from "../scales/definitions.js";
8
+ import { applyMaterializationPlan } from "../../materialization/dependencies.js";
9
+ import { planEncodingRematerialization } from "../../materialization/encodings.js";
10
+ import {
11
+ applyEncodingScale,
12
+ resolveReassignmentScaleOptions,
13
+ resolveTarget,
14
+ validateLineSeriesCompatibility,
15
+ validateOptions
16
+ } from "./shared.js";
17
+ import { findLayer } from "../../selectors/layers.js";
18
+
19
+ const STROKE_DASH_ENCODING_OPTIONS = Object.freeze([
20
+ "field", "value", "target", "fieldType", "scale"
21
+ ]);
22
+
23
+ const clearStrokeDashEncoding = action(
24
+ {
25
+ op: "clearStrokeDashEncoding",
26
+ description: "Remove the current semantic stroke-dash assignment."
27
+ },
28
+ function ({ target } = {}) {
29
+ const layer = findLayer(this, target);
30
+ if (layer?.encoding?.strokeDash === undefined) return this;
31
+ return this.editSemantic({
32
+ property: `layer[${target}].encoding.strokeDash`,
33
+ remove: true
34
+ });
35
+ }
36
+ );
37
+
38
+ function reconcileLegendAfterDashRemoval(program, target) {
39
+ const config = program.guideConfigs.legend?.series;
40
+ if (
41
+ config?.target !== target ||
42
+ !config.channels.includes("strokeDash")
43
+ ) {
44
+ return program;
45
+ }
46
+ const layer = findLayer(program, target);
47
+ const channels = config.channels.filter(
48
+ channel =>
49
+ channel !== "strokeDash" &&
50
+ layer.encoding?.[channel]?.scale !== undefined
51
+ );
52
+ if (channels.length === 0) return program.removeCategoricalLegend();
53
+ return program
54
+ .editSemantic({
55
+ property: "guide.legend.series.channels",
56
+ value: channels
57
+ })
58
+ .rematerializeLegend();
59
+ }
60
+
61
+ const encodeStrokeDash = action(
62
+ {
63
+ op: "encodeStrokeDash",
64
+ description: "Encode a nominal field as line stroke dash."
65
+ },
66
+ function (args = {}) {
67
+ validateOptions(args, STROKE_DASH_ENCODING_OPTIONS, "encodeStrokeDash");
68
+ const hasField = Object.hasOwn(args, "field");
69
+ const hasValue = Object.hasOwn(args, "value");
70
+ if (hasField === hasValue) {
71
+ throw new Error("encodeStrokeDash requires exactly one of field or value.");
72
+ }
73
+ if (hasValue && (args.fieldType !== undefined || args.scale !== undefined)) {
74
+ throw new Error("Constant stroke dash does not accept fieldType or scale.");
75
+ }
76
+ const { id: target, dataset, layer } = resolveTarget(
77
+ this,
78
+ args.target,
79
+ ["line", "rule"],
80
+ "line or rule mark"
81
+ );
82
+ if (hasValue) {
83
+ normalizeStrokeDashPattern(args.value);
84
+ let next = layer.encoding?.strokeDash === undefined
85
+ ? this
86
+ : this.clearStrokeDashEncoding({ target });
87
+ next = next.editSemantic({
88
+ property: `layer[${target}].encoding.strokeDash.datum`,
89
+ value: args.value
90
+ });
91
+ next = reconcileLegendAfterDashRemoval(next, target);
92
+ return layer.mark.type === "rule"
93
+ ? next.rematerializeRuleMark({ id: target })
94
+ : next.rematerializeLineMark({ id: target });
95
+ }
96
+
97
+ const fieldType = validateNominalFieldType(args.fieldType ?? "nominal");
98
+ validateLineSeriesCompatibility(layer, "strokeDash", args.field);
99
+ const previous = layer.encoding?.strokeDash;
100
+ const requestedScale = previous?.field === args.field
101
+ ? resolveReassignmentScaleOptions(previous, args.scale ?? {})
102
+ : args.scale ?? {};
103
+ const scale = resolveStrokeDashScaleDefinition(this, requestedScale);
104
+ if (Object.hasOwn(scale, "unknown")) {
105
+ throw new Error(
106
+ "StrokeDash scale unknown is not supported for grouped line or rule paths."
107
+ );
108
+ }
109
+ readNominalField(dataset.values, args.field);
110
+
111
+ let next = previous === undefined
112
+ ? this
113
+ : this.clearStrokeDashEncoding({ target });
114
+ next = next
115
+ .editSemantic({
116
+ property: `layer[${target}].encoding.strokeDash.field`,
117
+ value: args.field
118
+ })
119
+ .editSemantic({
120
+ property: `layer[${target}].encoding.strokeDash.fieldType`,
121
+ value: fieldType
122
+ })
123
+ .editSemantic({
124
+ property: `layer[${target}].encoding.strokeDash.scale`,
125
+ value: scale.id
126
+ });
127
+ next = applyEncodingScale(next, scale, requestedScale, {
128
+ reassignment: previous?.scale === scale.id
129
+ });
130
+
131
+ return applyMaterializationPlan(
132
+ next,
133
+ planEncodingRematerialization(next, {
134
+ target,
135
+ channel: "strokeDash",
136
+ scale: scale.id
137
+ })
138
+ );
139
+ }
140
+ );
141
+
142
+ export function registerStrokeDashEncodingActions(ProgramClass) {
143
+ ProgramClass.prototype.encodeStrokeDash = encodeStrokeDash;
144
+ ProgramClass.prototype.clearStrokeDashEncoding = clearStrokeDashEncoding;
145
+ }
@@ -0,0 +1,81 @@
1
+ import { action } from "../../core/action.js";
2
+
3
+ function positionOptions({ target, field, fieldType, coordinate, scale }) {
4
+ return {
5
+ target,
6
+ field,
7
+ fieldType,
8
+ coordinate,
9
+ scale: { id: scale }
10
+ };
11
+ }
12
+
13
+ export const createErrorBandBoundary = action(
14
+ {
15
+ op: "createErrorBandBoundary",
16
+ description: "Create one lower or upper error-band boundary line."
17
+ },
18
+ function ({
19
+ id,
20
+ data,
21
+ orientation,
22
+ bound,
23
+ position,
24
+ coordinate,
25
+ intervalScale,
26
+ positionScale,
27
+ groupBy,
28
+ stroke,
29
+ strokeWidth,
30
+ strokeDash,
31
+ opacity,
32
+ curve
33
+ } = {}) {
34
+ let next = this.createLineMark({ id, data, strokeWidth, curve });
35
+ if (orientation === "vertical") {
36
+ next = next
37
+ .encodeY(positionOptions({
38
+ target: id,
39
+ field: bound,
40
+ fieldType: "quantitative",
41
+ coordinate,
42
+ scale: intervalScale
43
+ }))
44
+ .encodeX(positionOptions({
45
+ target: id,
46
+ field: position.field,
47
+ fieldType: position.fieldType,
48
+ coordinate,
49
+ scale: positionScale
50
+ }));
51
+ } else {
52
+ next = next
53
+ .encodeY(positionOptions({
54
+ target: id,
55
+ field: position.field,
56
+ fieldType: position.fieldType,
57
+ coordinate,
58
+ scale: positionScale
59
+ }))
60
+ .encodeX(positionOptions({
61
+ target: id,
62
+ field: bound,
63
+ fieldType: "quantitative",
64
+ coordinate,
65
+ scale: intervalScale
66
+ }));
67
+ }
68
+ if (groupBy !== undefined) {
69
+ next = next.encodeGroup({ target: id, field: groupBy });
70
+ }
71
+ next = next
72
+ .editGraphics({ target: id, property: "stroke", value: stroke })
73
+ .editGraphics({
74
+ target: id,
75
+ property: "strokeDash",
76
+ value: next.graphicSpec.objects[id].items.map(() => strokeDash)
77
+ })
78
+ .editGraphics({ target: id, property: "opacity", value: opacity });
79
+ return next;
80
+ }
81
+ );
@@ -0,0 +1,159 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import {
4
+ validateKeys,
5
+ validateNonEmptyString,
6
+ validateNonNegativeFinite,
7
+ validateUnitInterval
8
+ } from "../../core/validation.js";
9
+ import { DEFAULT_COLORS } from "../../theme/defaults.js";
10
+ import { validateCurveInterpolation } from "../../grammar/curveCommands.js";
11
+ import { normalizeStrokeDashPattern } from "../../grammar/scales.js";
12
+ import { resolveErrorBand } from "./resolve.js";
13
+
14
+ const OPTIONS = Object.freeze([
15
+ "id",
16
+ "target",
17
+ "data",
18
+ "x",
19
+ "y",
20
+ "groupBy",
21
+ "coordinate",
22
+ "fill",
23
+ "opacity",
24
+ "curve",
25
+ "boundaries"
26
+ ]);
27
+ const BOUNDARY_OPTIONS = Object.freeze([
28
+ "stroke", "strokeWidth", "strokeDash", "opacity", "curve"
29
+ ]);
30
+
31
+ function resolveBoundaries(value, areaCurve) {
32
+ if (value === undefined || value === false) return undefined;
33
+ if (!isPlainObject(value)) {
34
+ throw new TypeError("createErrorBand boundaries must be false or a plain object.");
35
+ }
36
+ validateKeys(value, BOUNDARY_OPTIONS, "createErrorBand boundaries");
37
+ const stroke = value.stroke ?? DEFAULT_COLORS.mark;
38
+ const strokeWidth = value.strokeWidth ?? 1;
39
+ const strokeDash = value.strokeDash ?? "solid";
40
+ const opacity = value.opacity ?? 1;
41
+ const curve = validateCurveInterpolation(value.curve ?? areaCurve);
42
+ validateNonEmptyString(stroke, "Error-band boundary stroke");
43
+ validateNonNegativeFinite(
44
+ strokeWidth,
45
+ "Error-band boundary strokeWidth"
46
+ );
47
+ const resolvedStrokeDash = normalizeStrokeDashPattern(strokeDash);
48
+ validateUnitInterval(opacity, "Error-band boundary opacity");
49
+ return {
50
+ stroke,
51
+ strokeWidth,
52
+ strokeDash: resolvedStrokeDash,
53
+ opacity,
54
+ curve
55
+ };
56
+ }
57
+
58
+ function positionArgs(resolved) {
59
+ return {
60
+ target: resolved.id,
61
+ field: resolved.position.field,
62
+ fieldType: resolved.position.fieldType,
63
+ coordinate: resolved.coordinate,
64
+ scale: resolved.position.scale
65
+ };
66
+ }
67
+
68
+ function rangeArgs(resolved) {
69
+ return {
70
+ target: resolved.id,
71
+ lower: resolved.fields.lower,
72
+ upper: resolved.fields.upper,
73
+ fieldType: "quantitative",
74
+ coordinate: resolved.coordinate,
75
+ scale: resolved.interval.scale
76
+ };
77
+ }
78
+
79
+ export const createErrorBand = action(
80
+ {
81
+ op: "createErrorBand",
82
+ description: "Create a statistical or explicit interval band."
83
+ },
84
+ function (args = {}) {
85
+ validateKeys(args, OPTIONS, "createErrorBand");
86
+ const resolved = resolveErrorBand(this, args);
87
+ const curve = validateCurveInterpolation(args.curve ?? "linear");
88
+ const boundaries = resolveBoundaries(args.boundaries, curve);
89
+ let next = this;
90
+
91
+ if (resolved.interval.mode === "statistical") {
92
+ next = next.createIntervalData({
93
+ id: resolved.dataId,
94
+ source: resolved.source,
95
+ field: resolved.interval.field,
96
+ groupBy: resolved.groupBy,
97
+ center: resolved.interval.center,
98
+ extent: resolved.interval.extent,
99
+ level: resolved.interval.level,
100
+ as: resolved.fields
101
+ });
102
+ }
103
+
104
+ next = next.createAreaMark({
105
+ id: resolved.id,
106
+ data: resolved.dataId,
107
+ ...(args.fill === undefined ? {} : { fill: args.fill }),
108
+ ...(args.opacity === undefined ? {} : { opacity: args.opacity }),
109
+ ...(Object.hasOwn(args, "curve") ? { curve } : {})
110
+ });
111
+ next = resolved.orientation === "vertical"
112
+ ? next
113
+ .encodeX(positionArgs(resolved))
114
+ .encodeYRange(rangeArgs(resolved))
115
+ : next
116
+ .encodeY(positionArgs(resolved))
117
+ .encodeXRange(rangeArgs(resolved));
118
+
119
+ if (resolved.interval.mode === "explicit") {
120
+ next = next.editSemantic({
121
+ property: `layer[${resolved.id}].encoding.${resolved.interval.channel}.title`,
122
+ value: resolved.interval.title
123
+ });
124
+ }
125
+ if (resolved.groupField !== undefined) {
126
+ next = next.encodeGroup({
127
+ target: resolved.id,
128
+ field: resolved.groupField,
129
+ fieldType: "nominal"
130
+ });
131
+ }
132
+ if (boundaries !== undefined) {
133
+ const shared = {
134
+ data: resolved.dataId,
135
+ orientation: resolved.orientation,
136
+ position: resolved.position,
137
+ coordinate: resolved.coordinate,
138
+ intervalScale:
139
+ resolved.interval.scale.id ?? resolved.interval.channel,
140
+ positionScale:
141
+ resolved.position.scale.id ?? resolved.position.channel,
142
+ groupBy: resolved.groupField,
143
+ ...boundaries
144
+ };
145
+ next = next
146
+ .createErrorBandBoundary({
147
+ ...shared,
148
+ id: `${resolved.id}LowerBoundary`,
149
+ bound: resolved.fields.lower
150
+ })
151
+ .createErrorBandBoundary({
152
+ ...shared,
153
+ id: `${resolved.id}UpperBoundary`,
154
+ bound: resolved.fields.upper
155
+ });
156
+ }
157
+ return next;
158
+ }
159
+ );
@@ -0,0 +1,7 @@
1
+ import { createErrorBand } from "./create.js";
2
+ import { createErrorBandBoundary } from "./components.js";
3
+
4
+ export function registerErrorBandActions(ProgramClass) {
5
+ ProgramClass.prototype.createErrorBand = createErrorBand;
6
+ ProgramClass.prototype.createErrorBandBoundary = createErrorBandBoundary;
7
+ }
@@ -0,0 +1,28 @@
1
+ import { resolveIntervalComposite } from "../intervals/resolve.js";
2
+
3
+ const ERROR_BAND_POLICY = Object.freeze({
4
+ operation: "createErrorBand",
5
+ resourceLabel: "error-band",
6
+ defaultId: "errorBand",
7
+ ownerLabel: "Error-band id",
8
+ positionTypes: Object.freeze(["quantitative", "temporal"]),
9
+ defaultPositionType: "quantitative",
10
+ defaultIntervalChannel: "y",
11
+ scaleDefaults: fieldType => fieldType === "temporal"
12
+ ? { nice: true }
13
+ : { nice: true, zero: false },
14
+ intervalScaleDefaults: Object.freeze({ nice: true, zero: false }),
15
+ allowExplicitGrouping: true,
16
+ ambiguousMessage:
17
+ "createErrorBand cannot infer the interval axis when both positions are quantitative; provide an interval option."
18
+ });
19
+
20
+ export function resolveErrorBand(program, args) {
21
+ const resolved = resolveIntervalComposite(program, args, ERROR_BAND_POLICY);
22
+ if (resolved.groupField === resolved.position.field) {
23
+ throw new Error(
24
+ "createErrorBand groupBy must differ from the independent position field."
25
+ );
26
+ }
27
+ return resolved;
28
+ }
@@ -0,0 +1,62 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateKeys } from "../../core/validation.js";
3
+
4
+ const OPTIONS = Object.freeze([
5
+ "id",
6
+ "data",
7
+ "orientation",
8
+ "positionField",
9
+ "positionFieldType",
10
+ "intervalField",
11
+ "coordinate",
12
+ "positionScale",
13
+ "intervalScale",
14
+ "capSize",
15
+ "stroke",
16
+ "strokeWidth",
17
+ "strokeDash",
18
+ "opacity"
19
+ ]);
20
+
21
+ export const createErrorBarCap = action(
22
+ {
23
+ op: "createErrorBarCap",
24
+ description: "Create one fixed-pixel error-bar cap."
25
+ },
26
+ function (args = {}) {
27
+ validateKeys(args, OPTIONS, "createErrorBarCap");
28
+ if (!["vertical", "horizontal"].includes(args.orientation)) {
29
+ throw new Error(`Unsupported error-bar orientation "${args.orientation}".`);
30
+ }
31
+ const positionChannel = args.orientation === "vertical" ? "x" : "y";
32
+ const intervalChannel = args.orientation === "vertical" ? "y" : "x";
33
+ const positionAction = positionChannel === "x" ? "encodeX" : "encodeY";
34
+ const intervalAction = intervalChannel === "x" ? "encodeX" : "encodeY";
35
+
36
+ return this
37
+ .createRuleMark({ id: args.id, data: args.data })
38
+ [positionAction]({
39
+ target: args.id,
40
+ field: args.positionField,
41
+ fieldType: args.positionFieldType,
42
+ coordinate: args.coordinate,
43
+ scale: { id: args.positionScale }
44
+ })
45
+ [intervalAction]({
46
+ target: args.id,
47
+ field: args.intervalField,
48
+ fieldType: "quantitative",
49
+ coordinate: args.coordinate,
50
+ scale: { id: args.intervalScale }
51
+ })
52
+ .encodeStroke({ target: args.id, value: args.stroke })
53
+ .encodeStrokeWidth({ target: args.id, value: args.strokeWidth })
54
+ .encodeStrokeDash({ target: args.id, value: args.strokeDash })
55
+ .encodeOpacity({ target: args.id, value: args.opacity })
56
+ .materializeRuleSpan({
57
+ id: args.id,
58
+ orientation: args.orientation === "vertical" ? "horizontal" : "vertical",
59
+ size: args.capSize
60
+ });
61
+ }
62
+ );