ggaction 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +58 -3
  2. package/README.md +117 -83
  3. package/package.json +27 -11
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +31 -35
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +8 -1
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/coordinates/parallel.js +50 -0
  18. package/src/actions/data/bin2d.js +137 -0
  19. package/src/actions/data/density.js +54 -13
  20. package/src/actions/data/derived.js +22 -0
  21. package/src/actions/data/filter.js +5 -0
  22. package/src/actions/data/gradientProfile.js +65 -0
  23. package/src/actions/data/horizon.js +71 -0
  24. package/src/actions/data/index.js +29 -2
  25. package/src/actions/data/regression.js +1 -1
  26. package/src/actions/data/window.js +50 -0
  27. package/src/actions/encodings/appearance.js +1 -1
  28. package/src/actions/encodings/color/continuous.js +137 -0
  29. package/src/actions/encodings/color/index.js +167 -0
  30. package/src/actions/encodings/color/layout.js +99 -0
  31. package/src/actions/encodings/color/policy.js +88 -0
  32. package/src/actions/encodings/density/resolve.js +109 -0
  33. package/src/actions/encodings/density.js +193 -88
  34. package/src/actions/encodings/horizon/resolve.js +283 -0
  35. package/src/actions/encodings/horizon.js +364 -0
  36. package/src/actions/encodings/index.js +8 -2
  37. package/src/actions/encodings/offset.js +1 -1
  38. package/src/actions/encodings/parallel.js +115 -0
  39. package/src/actions/encodings/pathOrder.js +131 -0
  40. package/src/actions/encodings/position/apply.js +73 -4
  41. package/src/actions/encodings/{position.js → position/index.js} +21 -8
  42. package/src/actions/encodings/position/policies/arc.js +27 -4
  43. package/src/actions/encodings/position/policies/area.js +33 -2
  44. package/src/actions/encodings/position/policies/index.js +7 -2
  45. package/src/actions/encodings/position/policies/line.js +18 -2
  46. package/src/actions/encodings/position/resolve.js +25 -3
  47. package/src/actions/encodings/ranged.js +5 -3
  48. package/src/actions/encodings/ruleAppearance.js +88 -10
  49. package/src/actions/encodings/strokeDash.js +1 -1
  50. package/src/actions/encodings/text.js +1 -1
  51. package/src/actions/errorBands/options.js +1 -1
  52. package/src/actions/errorBars/options.js +1 -1
  53. package/src/actions/facets/actions.js +2 -3
  54. package/src/actions/facets/derive.js +35 -5
  55. package/src/actions/facets/guides.js +1 -1
  56. package/src/actions/facets/replay.js +7 -36
  57. package/src/actions/gradientPlots/components.js +177 -0
  58. package/src/actions/gradientPlots/create.js +96 -0
  59. package/src/actions/gradientPlots/edit.js +153 -0
  60. package/src/actions/gradientPlots/index.js +24 -0
  61. package/src/actions/gradientPlots/materialize.js +287 -0
  62. package/src/actions/gradientPlots/options.js +120 -0
  63. package/src/actions/gradientPlots/paint.js +107 -0
  64. package/src/actions/gradientPlots/rebind.js +51 -0
  65. package/src/actions/gradientPlots/resolve.js +81 -0
  66. package/src/actions/guides/applicability.js +44 -17
  67. package/src/actions/guides/axes/axes.js +37 -3
  68. package/src/actions/guides/axes/index.js +2 -0
  69. package/src/actions/guides/axes/labels.js +12 -7
  70. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  71. package/src/actions/guides/axes/parallel.js +228 -0
  72. package/src/actions/guides/axes/ticks.js +1 -1
  73. package/src/actions/guides/axes/titles.js +12 -9
  74. package/src/actions/guides/grids/resolve.js +1 -1
  75. package/src/actions/guides/guides.js +60 -5
  76. package/src/actions/guides/legends/categorical/actions.js +48 -11
  77. package/src/actions/guides/legends/categorical/layout.js +4 -2
  78. package/src/actions/guides/legends/continuous/interval.js +1 -1
  79. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  80. package/src/actions/guides/legends/edit.js +7 -20
  81. package/src/actions/guides/legends/index.js +2 -0
  82. package/src/actions/guides/legends/remove.js +6 -31
  83. package/src/actions/guides/legends/size.js +1 -1
  84. package/src/actions/guides/legends/strokeWidth.js +170 -0
  85. package/src/actions/guides/legends/target.js +25 -0
  86. package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
  87. package/src/actions/guides/polar/index.js +1 -1
  88. package/src/actions/guides/polar/resolve.js +1 -1
  89. package/src/actions/guides/tickValues.js +1 -1
  90. package/src/actions/index.js +6 -0
  91. package/src/actions/intervals/resolve.js +1 -1
  92. package/src/actions/marks/arc/actions.js +2 -2
  93. package/src/actions/marks/area/actions.js +61 -137
  94. package/src/actions/marks/area/materialize.js +163 -0
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/index.js +5 -1
  97. package/src/actions/marks/line/actions.js +100 -81
  98. package/src/actions/marks/line/materialize.js +146 -0
  99. package/src/actions/marks/point/create.js +76 -0
  100. package/src/actions/marks/point/edit.js +85 -0
  101. package/src/actions/marks/point/index.js +11 -1
  102. package/src/actions/marks/point/jitter.js +72 -0
  103. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  104. package/src/actions/marks/rect/actions.js +4 -1
  105. package/src/actions/marks/remove.js +13 -2
  106. package/src/actions/marks/rule/actions.js +26 -4
  107. package/src/actions/marks/shared.js +1 -1
  108. package/src/actions/marks/text/actions.js +20 -5
  109. package/src/actions/marks/text/index.js +7 -1
  110. package/src/actions/marks/text/layout.js +321 -0
  111. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  112. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  113. package/src/actions/primitives/semanticValidation/index.js +23 -0
  114. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  115. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  116. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  117. package/src/actions/primitives/semanticValue.js +1 -204
  118. package/src/actions/regression/components.js +1 -1
  119. package/src/actions/regression/create.js +1 -1
  120. package/src/actions/regression/edit.js +20 -22
  121. package/src/actions/scales/consumers/common.js +109 -0
  122. package/src/actions/scales/consumers/families.js +58 -0
  123. package/src/actions/scales/consumers/index.js +41 -0
  124. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
  125. package/src/actions/scales/create.js +1 -1
  126. package/src/actions/scales/definitions.js +32 -2
  127. package/src/actions/scales/edit.js +8 -206
  128. package/src/actions/scales/editPolicy.js +214 -0
  129. package/src/actions/scales/materialize.js +3 -3
  130. package/src/actions/scales/patch.js +26 -0
  131. package/src/actions/selection/actions.js +28 -3
  132. package/src/actions/violinPlots/create.js +198 -0
  133. package/src/actions/violinPlots/index.js +5 -0
  134. package/src/core/ChartProgram.js +22 -236
  135. package/src/core/compositionState.js +169 -0
  136. package/src/core/materializationState.js +65 -0
  137. package/src/core/programState.js +28 -0
  138. package/src/core/textMetrics.js +47 -0
  139. package/src/core/vocabulary.js +7 -2
  140. package/src/grammar/arcs.js +50 -13
  141. package/src/grammar/areaSeries.js +42 -6
  142. package/src/grammar/bars/aggregate.js +1 -1
  143. package/src/grammar/bin2d.js +333 -0
  144. package/src/grammar/categoricalDensity.js +192 -0
  145. package/src/grammar/coordinates.js +1 -1
  146. package/src/grammar/curveCommands.js +3 -1
  147. package/src/grammar/density.js +253 -27
  148. package/src/grammar/facets/dependencies.js +3 -11
  149. package/src/grammar/{facets.js → facets/index.js} +8 -6
  150. package/src/grammar/gradientProfile.js +213 -0
  151. package/src/grammar/horizon.js +455 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/jitter.js +197 -0
  154. package/src/grammar/lineSeries.js +70 -1
  155. package/src/grammar/paint.js +88 -0
  156. package/src/grammar/palettes.js +5 -7
  157. package/src/grammar/parallelCoordinates.js +213 -0
  158. package/src/grammar/pathOrder.js +35 -0
  159. package/src/grammar/pointShapes.js +24 -0
  160. package/src/grammar/polarLineCommands.js +1 -1
  161. package/src/grammar/regression/derive.js +97 -0
  162. package/src/grammar/regression/index.js +22 -0
  163. package/src/grammar/regression/models.js +282 -0
  164. package/src/grammar/regression/parameters.js +106 -0
  165. package/src/grammar/rules.js +2 -1
  166. package/src/grammar/scales/appearance.js +21 -0
  167. package/src/grammar/scales/color.js +4 -2
  168. package/src/grammar/schemas/concreteGraphic.js +11 -1
  169. package/src/grammar/schemas/graphicBounds.js +19 -37
  170. package/src/grammar/schemas/semanticPath.js +17 -2
  171. package/src/grammar/statistics/studentT.js +98 -0
  172. package/src/grammar/transforms.js +101 -12
  173. package/src/grammar/window.js +339 -0
  174. package/src/layout/labels.js +270 -0
  175. package/src/layout/text.js +4 -1
  176. package/src/layout/title.js +10 -20
  177. package/src/materialization/bars/aggregate.js +1 -1
  178. package/src/materialization/bars/grouped.js +1 -1
  179. package/src/materialization/bars/histogram.js +1 -1
  180. package/src/materialization/bars/ranged.js +1 -1
  181. package/src/materialization/dataProvenance.js +56 -2
  182. package/src/materialization/density.js +1 -1
  183. package/src/materialization/dependencies.js +15 -101
  184. package/src/materialization/encodings.js +2 -2
  185. package/src/materialization/facetGuides/index.js +2 -0
  186. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  187. package/src/materialization/facetGuides/placement.js +126 -0
  188. package/src/materialization/facetGuides/preparation.js +188 -0
  189. package/src/materialization/facets.js +1 -1
  190. package/src/materialization/guides/resources.js +74 -24
  191. package/src/materialization/horizon.js +29 -0
  192. package/src/materialization/layout.js +15 -0
  193. package/src/materialization/marks/capabilities.js +161 -0
  194. package/src/materialization/marks/index.js +108 -0
  195. package/src/materialization/marks/pathOrder.js +71 -0
  196. package/src/materialization/marks/policies.js +91 -0
  197. package/src/materialization/rect.js +1 -1
  198. package/src/materialization/rowEncoding.js +1 -1
  199. package/src/materialization/scaleGuideDependencies.js +102 -0
  200. package/src/materialization/scales/map.js +1 -1
  201. package/src/materialization/scales/policies/series.js +1 -1
  202. package/src/materialization/scales/resolve.js +3 -1
  203. package/src/materialization/selection/items/bar.js +1 -1
  204. package/src/materialization/selection/items/path.js +27 -1
  205. package/src/materialization/selection/items/point.js +1 -5
  206. package/src/materialization/selection/items/rect.js +21 -0
  207. package/src/materialization/selection/styles.js +21 -10
  208. package/src/materialization/text.js +12 -4
  209. package/src/renderers/canvas/fill.js +33 -0
  210. package/src/renderers/canvas/index.js +7 -0
  211. package/src/renderers/canvas/path.js +7 -4
  212. package/src/renderers/canvas/rect.js +9 -3
  213. package/src/renderers/canvas/text.js +14 -1
  214. package/src/selectors/datasets.js +4 -0
  215. package/src/theme/defaults.js +4 -0
  216. package/types/extension.d.ts +10 -1
  217. package/types/index.d.ts +76 -0
  218. package/types/program.d.ts +632 -10
  219. package/src/actions/encodings/color.js +0 -424
  220. package/src/grammar/regression.js +0 -578
  221. package/src/grammar/scales.js +0 -1
  222. package/src/materialization/facetGuides.js +0 -426
  223. package/src/materialization/marks.js +0 -316
@@ -0,0 +1,214 @@
1
+ import {
2
+ hasOrdinalDomain,
3
+ isDiscretizedColorScaleType,
4
+ isDiscretePositionScaleType,
5
+ isTransformedScaleType,
6
+ normalizeScaleDefinition,
7
+ SCALE_ROLES,
8
+ validateColorRange,
9
+ validateDiscretizedColorDomain,
10
+ validateDiscretizedColorRange,
11
+ validateOrdinalDomain,
12
+ validateScaleDomain,
13
+ validateScaleRange,
14
+ validateScaleType,
15
+ validateScaleTypeForRole,
16
+ validateScaleUnknown,
17
+ validateSequentialColorRange,
18
+ validateShapeRange,
19
+ validateSizeRange,
20
+ validateStrokeDashRange,
21
+ validateStrokeWidthRange
22
+ } from "../../grammar/scales/index.js";
23
+ import {
24
+ validateRadialRange,
25
+ validateThetaRange
26
+ } from "../../grammar/polar.js";
27
+ import { normalizePositionScaleChannel } from "../../core/vocabulary.js";
28
+
29
+ export function resolveScaleConsumerChannel(consumers, id) {
30
+ const channels = new Set(
31
+ consumers.map(consumer => normalizePositionScaleChannel(consumer.channel))
32
+ );
33
+ if (channels.size > 1) {
34
+ throw new Error(`Scale "${id}" cannot be shared across channels.`);
35
+ }
36
+ return channels.values().next().value;
37
+ }
38
+
39
+ function validateRangeForChannel(scale, channel, value) {
40
+ if (value === "auto") return value;
41
+ if (channel === "theta") return validateThetaRange(value);
42
+ if (channel === "radius") return validateRadialRange(value);
43
+ if (scale.type === "sequential") {
44
+ return validateSequentialColorRange(value);
45
+ }
46
+ if (isDiscretizedColorScaleType(scale.type)) {
47
+ return validateDiscretizedColorRange(value);
48
+ }
49
+ if (scale.type !== "ordinal") return validateScaleRange(value);
50
+ if (channel === "color") return validateColorRange(value);
51
+ if (channel === "shape") return validateShapeRange(value);
52
+ if (channel === "strokeDash") return validateStrokeDashRange(value);
53
+ if (channel === "size") return validateSizeRange(value);
54
+ if (channel === "strokeWidth") return validateStrokeWidthRange(value);
55
+ return validateScaleRange(value);
56
+ }
57
+
58
+ function validateTypeTransition(scale, nextType, channel, consumers) {
59
+ if (nextType === scale.type) return;
60
+ validateScaleType(nextType);
61
+ if (consumers.length === 0) return;
62
+ if (nextType === "sequential" || isDiscretizedColorScaleType(nextType)) {
63
+ const discretized = isDiscretizedColorScaleType(nextType);
64
+ if (channel !== "color" || consumers.some(consumer =>
65
+ ["nominal", "ordinal"].includes(consumer.encoding.fieldType) ||
66
+ (discretized && (
67
+ consumer.encoding.fieldType !== "quantitative" ||
68
+ consumer.layer.mark?.type !== "point"
69
+ )) ||
70
+ (!discretized && !["point", "bar"].includes(consumer.layer.mark?.type))
71
+ )) {
72
+ throw new Error(
73
+ `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
74
+ );
75
+ }
76
+ return;
77
+ }
78
+ if (nextType === "time") {
79
+ if (
80
+ !["x", "y", "theta"].includes(channel) ||
81
+ consumers.some(consumer => consumer.encoding.fieldType !== "temporal")
82
+ ) {
83
+ throw new Error(
84
+ `Scale "${scale.id}" has a consumer incompatible with type "time".`
85
+ );
86
+ }
87
+ return;
88
+ }
89
+ if (isDiscretePositionScaleType(nextType)) {
90
+ if (
91
+ !["x", "y", "theta"].includes(channel) || consumers.some(consumer =>
92
+ !["nominal", "ordinal"].includes(consumer.encoding.fieldType)
93
+ )
94
+ ) {
95
+ throw new Error(
96
+ `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
97
+ );
98
+ }
99
+ if (
100
+ nextType === "point" &&
101
+ consumers.some(consumer => consumer.layer.mark?.type === "bar")
102
+ ) {
103
+ throw new Error("Point scales cannot provide bar bandwidth.");
104
+ }
105
+ return;
106
+ }
107
+ const quantitative = nextType === "linear" || isTransformedScaleType(nextType);
108
+ if (
109
+ !quantitative ||
110
+ !["x", "y", "theta", "radius", "strokeWidth"].includes(channel)
111
+ ) {
112
+ throw new Error(
113
+ "editScale type transition currently requires a quantitative position scale."
114
+ );
115
+ }
116
+ validateScaleTypeForRole(nextType, SCALE_ROLES.quantitativePosition);
117
+ if (channel === "theta" && nextType !== "linear") {
118
+ throw new Error("Theta quantitative position currently requires a linear scale.");
119
+ }
120
+ if (
121
+ channel === "strokeWidth" &&
122
+ consumers.some(consumer => !["line", "rule"].includes(consumer.layer.mark?.type))
123
+ ) {
124
+ throw new Error(
125
+ `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
126
+ );
127
+ }
128
+ if (consumers.some(consumer => consumer.encoding.fieldType !== "quantitative")) {
129
+ throw new Error(
130
+ `Scale "${scale.id}" has a consumer incompatible with type "${nextType}".`
131
+ );
132
+ }
133
+ }
134
+
135
+ function validateLegendTypeTransition(program, scale, nextType) {
136
+ if (nextType === scale.type) return;
137
+ const legends = program.guideConfigs.legend ?? {};
138
+ if (legends.gradient?.scale === scale.id && nextType !== "sequential") {
139
+ throw new Error(
140
+ `Scale "${scale.id}" cannot change type while its gradient legend is active.`
141
+ );
142
+ }
143
+ if (
144
+ legends.interval?.scale === scale.id &&
145
+ !isDiscretizedColorScaleType(nextType)
146
+ ) {
147
+ throw new Error(
148
+ `Scale "${scale.id}" cannot change type while its interval legend is active.`
149
+ );
150
+ }
151
+ }
152
+
153
+ function normalizeDefinition(scale, channel, consumers, patch) {
154
+ const type = patch.type ?? scale.type;
155
+ validateTypeTransition(scale, type, channel, consumers);
156
+ const definition = normalizeScaleDefinition({
157
+ type,
158
+ previous: scale,
159
+ patch,
160
+ retainCoreOnTypeChange: true,
161
+ retainCompatibleOnTypeChange: true,
162
+ validateDomain: (scaleType, value) =>
163
+ isDiscretizedColorScaleType(scaleType)
164
+ ? validateDiscretizedColorDomain(scaleType, value)
165
+ : hasOrdinalDomain(scaleType)
166
+ ? validateOrdinalDomain(value)
167
+ : validateScaleDomain(value),
168
+ validateRange: (scaleType, value) =>
169
+ validateRangeForChannel({ type: scaleType }, channel, value)
170
+ });
171
+ if (
172
+ channel === "strokeWidth" &&
173
+ definition.domain !== "auto" &&
174
+ definition.domain.some(value => value < 0)
175
+ ) {
176
+ throw new RangeError("StrokeWidth scale domain cannot contain negative values.");
177
+ }
178
+ const typeChanged = type !== scale.type;
179
+ const unknown = Object.hasOwn(patch, "unknown")
180
+ ? patch.unknown
181
+ : typeChanged ? undefined : scale.unknown;
182
+ if (unknown !== undefined) {
183
+ if (consumers.some(consumer => consumer.layer.mark?.type !== "point")) {
184
+ throw new Error("Scale unknown currently requires row-owned point consumers.");
185
+ }
186
+ definition.unknown = consumers.length === 0
187
+ ? unknown
188
+ : validateScaleUnknown(channel, unknown);
189
+ }
190
+ return definition;
191
+ }
192
+
193
+ export function prepareScaleEdit(program, scale, channel, consumers, args) {
194
+ const hasPalette = Object.hasOwn(args, "palette");
195
+ if (hasPalette && Object.hasOwn(args, "range")) {
196
+ throw new Error("editScale cannot specify both palette and range.");
197
+ }
198
+ if (
199
+ hasPalette &&
200
+ channel !== "color" &&
201
+ scale.type !== "sequential" &&
202
+ !isDiscretizedColorScaleType(scale.type)
203
+ ) {
204
+ throw new Error("editScale palette requires a color scale.");
205
+ }
206
+ const patch = !hasPalette
207
+ ? args
208
+ : Object.fromEntries([
209
+ ...Object.entries(args).filter(([key]) => key !== "palette"),
210
+ ["range", { palette: args.palette }]
211
+ ]);
212
+ validateLegendTypeTransition(program, scale, args.type ?? scale.type);
213
+ return normalizeDefinition(scale, channel, consumers, patch);
214
+ }
@@ -11,7 +11,7 @@ import {
11
11
  canDeferScaleConsumerApplication,
12
12
  getMarkRematerializationStep,
13
13
  getScaleConsumerMaterializationMode
14
- } from "../../materialization/marks.js";
14
+ } from "../../materialization/marks/index.js";
15
15
  import { mapScaleConsumerValues } from
16
16
  "../../materialization/scales/map.js";
17
17
  import { resolveScaleMaterialization } from
@@ -21,7 +21,7 @@ import {
21
21
  findScaleConsumers,
22
22
  resolveConsumerValues,
23
23
  resolveSeriesLayoutScaleValues
24
- } from "./consumers.js";
24
+ } from "./consumers/index.js";
25
25
 
26
26
  const OPTIONS = Object.freeze(["id", "guides", "marks"]);
27
27
 
@@ -70,7 +70,7 @@ export const rematerializeScale = action(
70
70
  channel,
71
71
  consumers,
72
72
  valuesByConsumer,
73
- bounds: ["color", "strokeDash", "shape", "size", "opacity", "xOffset", "yOffset"]
73
+ bounds: ["color", "strokeDash", "strokeWidth", "shape", "size", "opacity", "xOffset", "yOffset"]
74
74
  .includes(channel)
75
75
  ? undefined
76
76
  : resolveGraphicBounds(this),
@@ -0,0 +1,26 @@
1
+ const SCALE_EDIT_PROPERTIES = Object.freeze([
2
+ "type", "domain", "range", "nice", "zero", "clamp", "reverse",
3
+ "base", "exponent", "constant", "paddingInner", "paddingOuter",
4
+ "padding", "align", "interpolate", "unknown"
5
+ ]);
6
+
7
+ function sameValue(left, right) {
8
+ return JSON.stringify(left) === JSON.stringify(right);
9
+ }
10
+
11
+ export function scaleEditPatch(id, definition) {
12
+ return {
13
+ id,
14
+ ...Object.fromEntries(SCALE_EDIT_PROPERTIES
15
+ .filter(property => Object.hasOwn(definition, property))
16
+ .map(property => [property, definition[property]]))
17
+ };
18
+ }
19
+
20
+ export function changedScaleEditPatch(current, definition) {
21
+ const patch = scaleEditPatch(current.id, definition);
22
+ return Object.entries(patch).some(
23
+ ([property, value]) => property !== "id" &&
24
+ !sameValue(current[property], value)
25
+ ) ? patch : undefined;
26
+ }
@@ -117,7 +117,26 @@ export const applyPointHighlight = action(
117
117
  }
118
118
  );
119
119
 
120
- function applyRectangularHighlight(program, args, operation, markType) {
120
+ function transformRectangularProperties(properties, style, offset = false) {
121
+ if (!offset || style.offset === undefined) {
122
+ return { ...properties, ...style };
123
+ }
124
+ const { offset: translation, ...appearance } = style;
125
+ return {
126
+ ...properties,
127
+ ...appearance,
128
+ x: properties.x + translation.x,
129
+ y: properties.y + translation.y
130
+ };
131
+ }
132
+
133
+ function applyRectangularHighlight(
134
+ program,
135
+ args,
136
+ operation,
137
+ markType,
138
+ { offset = false } = {}
139
+ ) {
121
140
  validateKeys(args, INTERNAL_SELECTION_OPTIONS, operation);
122
141
  const resolved = resolveStoredSelection(program, args.selection);
123
142
  const keys = selectedKeys(args, resolved);
@@ -136,7 +155,7 @@ function applyRectangularHighlight(program, args, operation, markType) {
136
155
  value: graphic.items.map(child => ({
137
156
  type: child.type ?? graphic.type,
138
157
  properties: selected.has(keyByGraphic.get(child.id))
139
- ? { ...child.properties, ...args.style }
158
+ ? transformRectangularProperties(child.properties, args.style, offset)
140
159
  : child.properties
141
160
  }))
142
161
  });
@@ -152,7 +171,13 @@ export const applyBarHighlight = action(
152
171
  export const applyRectHighlight = action(
153
172
  { op: "applyRectHighlight", description: "Apply selected rect appearance." },
154
173
  function (args = {}) {
155
- return applyRectangularHighlight(this, args, "applyRectHighlight", "rect");
174
+ return applyRectangularHighlight(
175
+ this,
176
+ args,
177
+ "applyRectHighlight",
178
+ "rect",
179
+ { offset: true }
180
+ );
156
181
  }
157
182
  );
158
183
 
@@ -0,0 +1,198 @@
1
+ import { action } from "../../core/action.js";
2
+ import { validateOptionObject } from "../../core/validation.js";
3
+ import { findDataset } from "../../selectors/datasets.js";
4
+ import {
5
+ applyFacadeGuides,
6
+ normalizeAppearance,
7
+ normalizeEncoding,
8
+ normalizeFieldEncoding,
9
+ normalizeGuides,
10
+ resolveFacadeData,
11
+ resolveFacadeId,
12
+ targetArgs,
13
+ validateFacadeOptions
14
+ } from "../charts/shared.js";
15
+
16
+ const OPTIONS = Object.freeze([
17
+ "id", "data", "coordinate", "x", "y", "split", "color", "density",
18
+ "area", "guides"
19
+ ]);
20
+ const POSITION_OPTIONS = Object.freeze(["field", "fieldType", "scale"]);
21
+ const DENSITY_OPTIONS = Object.freeze([
22
+ "bandwidth", "extent", "steps", "kernel", "normalization", "width"
23
+ ]);
24
+ const WIDTH_OPTIONS = Object.freeze(["band", "resolve"]);
25
+ const SPLIT_OPTIONS = Object.freeze(["field", "domain"]);
26
+ const AREA_OPTIONS = Object.freeze([
27
+ "fill", "opacity", "stroke", "strokeWidth", "curve"
28
+ ]);
29
+ const CATEGORICAL_TYPES = Object.freeze(["nominal", "ordinal"]);
30
+
31
+ function inferFieldType(dataset, encoding, label) {
32
+ if (encoding.fieldType !== undefined) return encoding.fieldType;
33
+ if (typeof encoding.field !== "string" || encoding.field.length === 0) {
34
+ throw new TypeError(`${label} field must be a non-empty string.`);
35
+ }
36
+ const values = dataset.values
37
+ .map(row => row?.[encoding.field])
38
+ .filter(value => value !== undefined && value !== null && value !== "");
39
+ if (values.length === 0) {
40
+ throw new Error(`${label} field "${encoding.field}" has no values.`);
41
+ }
42
+ return values.every(Number.isFinite) ? "quantitative" : "nominal";
43
+ }
44
+
45
+ function normalizePosition(dataset, value, label) {
46
+ const encoding = normalizeFieldEncoding(value, label);
47
+ validateOptionObject(encoding, POSITION_OPTIONS, label);
48
+ return {
49
+ ...encoding,
50
+ fieldType: inferFieldType(dataset, encoding, label)
51
+ };
52
+ }
53
+
54
+ function normalizeDensity(value) {
55
+ if (value === undefined) return { options: {}, width: undefined };
56
+ validateOptionObject(value, DENSITY_OPTIONS, "createViolinPlot density");
57
+ if (value.width !== undefined) {
58
+ validateOptionObject(
59
+ value.width,
60
+ WIDTH_OPTIONS,
61
+ "createViolinPlot density.width"
62
+ );
63
+ }
64
+ const { width, ...options } = value;
65
+ return { options, width };
66
+ }
67
+
68
+ function normalizeSplit(value) {
69
+ if (value === undefined) return undefined;
70
+ validateOptionObject(value, SPLIT_OPTIONS, "createViolinPlot split");
71
+ if (typeof value.field !== "string" || value.field.length === 0) {
72
+ throw new TypeError("createViolinPlot split.field must be a non-empty string.");
73
+ }
74
+ return { ...value };
75
+ }
76
+
77
+ function axisGuideOptions(value, field) {
78
+ if (value === false) return false;
79
+ return {
80
+ ...(value ?? {}),
81
+ title: {
82
+ text: field,
83
+ ...(value?.title ?? {})
84
+ }
85
+ };
86
+ }
87
+
88
+ function guideOptions(guides, x, y, category, color) {
89
+ if (guides === false) return false;
90
+ const axes = guides.axes === false
91
+ ? false
92
+ : {
93
+ ...(guides.axes ?? {}),
94
+ x: axisGuideOptions(guides.axes?.x, x.field),
95
+ y: axisGuideOptions(guides.axes?.y, y.field)
96
+ };
97
+ const redundantLegend = color?.field === category.field &&
98
+ !Object.hasOwn(guides, "legend");
99
+ return {
100
+ ...guides,
101
+ axes,
102
+ ...(redundantLegend ? { legend: false } : {})
103
+ };
104
+ }
105
+
106
+ export const createViolinPlot = action(
107
+ {
108
+ op: "createViolinPlot",
109
+ description: "Create a categorical kernel-density violin plot."
110
+ },
111
+ function (args = {}) {
112
+ validateFacadeOptions(args, OPTIONS, "createViolinPlot");
113
+ const id = resolveFacadeId(this, args.id, {
114
+ defaultId: "violinPlot",
115
+ operation: "createViolinPlot"
116
+ });
117
+ const data = resolveFacadeData(this, args.data, "createViolinPlot");
118
+ const dataset = findDataset(this, data);
119
+ const x = normalizePosition(dataset, args.x, "createViolinPlot x");
120
+ const y = normalizePosition(dataset, args.y, "createViolinPlot y");
121
+ const xCategorical = CATEGORICAL_TYPES.includes(x.fieldType);
122
+ const yCategorical = CATEGORICAL_TYPES.includes(y.fieldType);
123
+ if (xCategorical === yCategorical || (
124
+ x.fieldType !== "quantitative" && y.fieldType !== "quantitative"
125
+ )) {
126
+ throw new Error(
127
+ "createViolinPlot requires one categorical axis and one quantitative axis."
128
+ );
129
+ }
130
+ const category = xCategorical ? x : y;
131
+ const value = xCategorical ? y : x;
132
+ if (value.fieldType !== "quantitative") {
133
+ throw new Error(
134
+ "createViolinPlot requires one categorical axis and one quantitative axis."
135
+ );
136
+ }
137
+ const split = normalizeSplit(args.split);
138
+ if (split?.field === category.field) {
139
+ throw new Error("createViolinPlot split field must differ from its category field.");
140
+ }
141
+ const color = normalizeEncoding(args.color, "createViolinPlot color");
142
+ if (
143
+ color !== undefined &&
144
+ ![category.field, split?.field].includes(color.field)
145
+ ) {
146
+ throw new Error(
147
+ "createViolinPlot color must encode its category or split field."
148
+ );
149
+ }
150
+ const density = normalizeDensity(args.density);
151
+ const area = normalizeAppearance(
152
+ args.area,
153
+ AREA_OPTIONS,
154
+ "createViolinPlot area"
155
+ );
156
+ if (color !== undefined && Object.hasOwn(area, "fill")) {
157
+ throw new Error("createViolinPlot area.fill cannot be combined with color.");
158
+ }
159
+ const guides = guideOptions(
160
+ normalizeGuides(args.guides, "createViolinPlot"),
161
+ x,
162
+ y,
163
+ category,
164
+ color
165
+ );
166
+ const { strokeWidth, ...areaCreate } = area;
167
+ const hasFillStroke = strokeWidth !== undefined && area.stroke === undefined;
168
+ let next = this.createAreaMark({
169
+ id,
170
+ data,
171
+ ...areaCreate,
172
+ ...(area.stroke === undefined || strokeWidth === undefined
173
+ ? {}
174
+ : { strokeWidth })
175
+ });
176
+ if (hasFillStroke) {
177
+ next = next.configureAreaStrokeFromFill({ id, strokeWidth });
178
+ }
179
+ next = next.encodeDensity({
180
+ target: id,
181
+ source: data,
182
+ field: value.field,
183
+ groupBy: category.field,
184
+ densityChannel: xCategorical ? "x" : "y",
185
+ ...(args.coordinate === undefined ? {} : { coordinate: args.coordinate }),
186
+ ...(value.scale === undefined ? {} : { valueScale: value.scale }),
187
+ ...density.options,
188
+ placement: {
189
+ type: "category",
190
+ ...(density.width === undefined ? {} : { width: density.width }),
191
+ ...(split === undefined ? {} : { split }),
192
+ ...(category.scale === undefined ? {} : { scale: category.scale })
193
+ }
194
+ });
195
+ if (color !== undefined) next = next.encodeColor(targetArgs(color, id));
196
+ return applyFacadeGuides(next, guides);
197
+ }
198
+ );
@@ -0,0 +1,5 @@
1
+ import { createViolinPlot } from "./create.js";
2
+
3
+ export function registerViolinPlotActions(ProgramClass) {
4
+ ProgramClass.prototype.createViolinPlot = createViolinPlot;
5
+ }