ggaction 0.0.4 → 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 (157) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  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/window.js +50 -0
  26. package/src/actions/encodings/color/index.js +17 -1
  27. package/src/actions/encodings/color/layout.js +8 -0
  28. package/src/actions/encodings/density/resolve.js +109 -0
  29. package/src/actions/encodings/density.js +193 -88
  30. package/src/actions/encodings/horizon/resolve.js +283 -0
  31. package/src/actions/encodings/horizon.js +364 -0
  32. package/src/actions/encodings/index.js +6 -0
  33. package/src/actions/encodings/parallel.js +115 -0
  34. package/src/actions/encodings/pathOrder.js +131 -0
  35. package/src/actions/encodings/position/apply.js +73 -4
  36. package/src/actions/encodings/position/index.js +15 -2
  37. package/src/actions/encodings/position/policies/arc.js +27 -4
  38. package/src/actions/encodings/position/policies/area.js +33 -2
  39. package/src/actions/encodings/position/policies/index.js +7 -2
  40. package/src/actions/encodings/position/policies/line.js +18 -2
  41. package/src/actions/encodings/position/resolve.js +24 -2
  42. package/src/actions/encodings/ranged.js +3 -1
  43. package/src/actions/encodings/ruleAppearance.js +88 -10
  44. package/src/actions/facets/actions.js +1 -2
  45. package/src/actions/facets/derive.js +33 -3
  46. package/src/actions/facets/replay.js +7 -36
  47. package/src/actions/gradientPlots/components.js +177 -0
  48. package/src/actions/gradientPlots/create.js +96 -0
  49. package/src/actions/gradientPlots/edit.js +153 -0
  50. package/src/actions/gradientPlots/index.js +24 -0
  51. package/src/actions/gradientPlots/materialize.js +287 -0
  52. package/src/actions/gradientPlots/options.js +120 -0
  53. package/src/actions/gradientPlots/paint.js +107 -0
  54. package/src/actions/gradientPlots/rebind.js +51 -0
  55. package/src/actions/gradientPlots/resolve.js +81 -0
  56. package/src/actions/guides/applicability.js +44 -17
  57. package/src/actions/guides/axes/axes.js +37 -3
  58. package/src/actions/guides/axes/index.js +2 -0
  59. package/src/actions/guides/axes/labels.js +11 -6
  60. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  61. package/src/actions/guides/axes/parallel.js +228 -0
  62. package/src/actions/guides/axes/titles.js +11 -8
  63. package/src/actions/guides/guides.js +60 -5
  64. package/src/actions/guides/legends/categorical/actions.js +48 -11
  65. package/src/actions/guides/legends/edit.js +5 -0
  66. package/src/actions/guides/legends/index.js +2 -0
  67. package/src/actions/guides/legends/remove.js +4 -11
  68. package/src/actions/guides/legends/strokeWidth.js +170 -0
  69. package/src/actions/index.js +6 -0
  70. package/src/actions/marks/area/actions.js +60 -136
  71. package/src/actions/marks/area/materialize.js +163 -0
  72. package/src/actions/marks/index.js +5 -1
  73. package/src/actions/marks/line/actions.js +99 -80
  74. package/src/actions/marks/line/materialize.js +146 -0
  75. package/src/actions/marks/point/create.js +76 -0
  76. package/src/actions/marks/point/edit.js +85 -0
  77. package/src/actions/marks/point/index.js +11 -1
  78. package/src/actions/marks/point/jitter.js +72 -0
  79. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  80. package/src/actions/marks/rect/actions.js +3 -0
  81. package/src/actions/marks/remove.js +13 -2
  82. package/src/actions/marks/rule/actions.js +25 -3
  83. package/src/actions/marks/text/actions.js +19 -4
  84. package/src/actions/marks/text/index.js +7 -1
  85. package/src/actions/marks/text/layout.js +321 -0
  86. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  87. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  88. package/src/actions/primitives/semanticValidation/index.js +23 -0
  89. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  90. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  91. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  92. package/src/actions/primitives/semanticValue.js +1 -204
  93. package/src/actions/regression/edit.js +19 -21
  94. package/src/actions/scales/consumers/common.js +18 -0
  95. package/src/actions/scales/consumers/families.js +14 -3
  96. package/src/actions/scales/definitions.js +30 -0
  97. package/src/actions/scales/edit.js +6 -204
  98. package/src/actions/scales/editPolicy.js +214 -0
  99. package/src/actions/scales/materialize.js +1 -1
  100. package/src/actions/scales/patch.js +26 -0
  101. package/src/actions/selection/actions.js +28 -3
  102. package/src/actions/violinPlots/create.js +198 -0
  103. package/src/actions/violinPlots/index.js +5 -0
  104. package/src/core/textMetrics.js +47 -0
  105. package/src/core/vocabulary.js +7 -2
  106. package/src/grammar/arcs.js +49 -12
  107. package/src/grammar/areaSeries.js +41 -5
  108. package/src/grammar/bin2d.js +333 -0
  109. package/src/grammar/categoricalDensity.js +192 -0
  110. package/src/grammar/coordinates.js +1 -1
  111. package/src/grammar/curveCommands.js +3 -1
  112. package/src/grammar/density.js +252 -26
  113. package/src/grammar/facets/dependencies.js +3 -11
  114. package/src/grammar/facets/index.js +3 -1
  115. package/src/grammar/gradientProfile.js +213 -0
  116. package/src/grammar/horizon.js +455 -0
  117. package/src/grammar/jitter.js +197 -0
  118. package/src/grammar/lineSeries.js +69 -0
  119. package/src/grammar/paint.js +88 -0
  120. package/src/grammar/parallelCoordinates.js +213 -0
  121. package/src/grammar/pathOrder.js +35 -0
  122. package/src/grammar/pointShapes.js +24 -0
  123. package/src/grammar/rules.js +1 -0
  124. package/src/grammar/scales/appearance.js +21 -0
  125. package/src/grammar/schemas/concreteGraphic.js +11 -1
  126. package/src/grammar/schemas/graphicBounds.js +19 -37
  127. package/src/grammar/schemas/semanticPath.js +17 -2
  128. package/src/grammar/transforms.js +100 -11
  129. package/src/grammar/window.js +339 -0
  130. package/src/layout/labels.js +270 -0
  131. package/src/layout/text.js +4 -1
  132. package/src/layout/title.js +10 -20
  133. package/src/materialization/dataProvenance.js +56 -2
  134. package/src/materialization/dependencies.js +9 -14
  135. package/src/materialization/guides/resources.js +74 -24
  136. package/src/materialization/horizon.js +29 -0
  137. package/src/materialization/layout.js +15 -0
  138. package/src/materialization/marks/capabilities.js +21 -1
  139. package/src/materialization/marks/index.js +3 -0
  140. package/src/materialization/marks/pathOrder.js +71 -0
  141. package/src/materialization/marks/policies.js +6 -2
  142. package/src/materialization/scaleGuideDependencies.js +8 -0
  143. package/src/materialization/scales/resolve.js +2 -0
  144. package/src/materialization/selection/items/path.js +27 -1
  145. package/src/materialization/selection/items/point.js +1 -5
  146. package/src/materialization/selection/items/rect.js +21 -0
  147. package/src/materialization/selection/styles.js +20 -9
  148. package/src/materialization/text.js +12 -4
  149. package/src/renderers/canvas/fill.js +33 -0
  150. package/src/renderers/canvas/index.js +7 -0
  151. package/src/renderers/canvas/path.js +7 -4
  152. package/src/renderers/canvas/rect.js +9 -3
  153. package/src/selectors/datasets.js +4 -0
  154. package/src/theme/defaults.js +4 -0
  155. package/types/extension.d.ts +6 -0
  156. package/types/index.d.ts +76 -0
  157. package/types/program.d.ts +632 -10
@@ -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
+ }
@@ -21,3 +21,50 @@ export function measureTextWidth(text, { fontSize } = {}) {
21
21
  0
22
22
  );
23
23
  }
24
+
25
+ function horizontalExtents(width, align) {
26
+ if (align === "center") return [-width / 2, width / 2];
27
+ if (["right", "end"].includes(align)) return [-width, 0];
28
+ return [0, width];
29
+ }
30
+
31
+ function verticalExtents(fontSize, baseline) {
32
+ if (baseline === "middle") return [-fontSize / 2, fontSize / 2];
33
+ if (["top", "hanging"].includes(baseline)) return [0, fontSize];
34
+ if (["bottom", "ideographic"].includes(baseline)) return [-fontSize, 0];
35
+ return [-fontSize * 0.8, fontSize * 0.2];
36
+ }
37
+
38
+ export function resolveTextBounds({
39
+ x,
40
+ y,
41
+ text,
42
+ fontSize,
43
+ textAlign = "left",
44
+ textBaseline = "alphabetic",
45
+ rotation = 0
46
+ } = {}) {
47
+ if (![x, y, rotation].every(Number.isFinite)) {
48
+ throw new TypeError("Text bounds require finite x, y, and rotation values.");
49
+ }
50
+ const width = measureTextWidth(text, { fontSize });
51
+ const [left, right] = horizontalExtents(width, textAlign);
52
+ const [top, bottom] = verticalExtents(fontSize, textBaseline);
53
+ const cosine = Math.cos(rotation);
54
+ const sine = Math.sin(rotation);
55
+ const corners = [
56
+ [left, top],
57
+ [right, top],
58
+ [right, bottom],
59
+ [left, bottom]
60
+ ].map(([localX, localY]) => ({
61
+ x: x + localX * cosine - localY * sine,
62
+ y: y + localX * sine + localY * cosine
63
+ }));
64
+ return Object.freeze({
65
+ left: Math.min(...corners.map(point => point.x)),
66
+ right: Math.max(...corners.map(point => point.x)),
67
+ top: Math.min(...corners.map(point => point.y)),
68
+ bottom: Math.max(...corners.map(point => point.y))
69
+ });
70
+ }
@@ -31,9 +31,11 @@ export const ENCODING_CHANNELS = Object.freeze([
31
31
  "radius",
32
32
  "color",
33
33
  "strokeDash",
34
+ "strokeWidth",
34
35
  "size",
35
36
  "shape",
36
37
  "group",
38
+ "pathOrder",
37
39
  "opacity",
38
40
  "text"
39
41
  ]);
@@ -111,7 +113,9 @@ export const POSITION_CHANNEL_DEFINITIONS = Object.freeze({
111
113
  });
112
114
 
113
115
  export const SCALED_ENCODING_CHANNELS = Object.freeze(
114
- ENCODING_CHANNELS.filter(channel => !["group", "text"].includes(channel))
116
+ ENCODING_CHANNELS.filter(channel =>
117
+ !["group", "pathOrder", "text"].includes(channel)
118
+ )
115
119
  );
116
120
 
117
121
  export const POSITION_ENCODING_CHANNELS = Object.freeze(
@@ -200,7 +204,8 @@ export const LEGEND_CONFIG_KINDS = Object.freeze([
200
204
  "size",
201
205
  "gradient",
202
206
  "interval",
203
- "opacity"
207
+ "opacity",
208
+ "strokeWidth"
204
209
  ]);
205
210
 
206
211
  export function includesVocabulary(vocabulary, value) {
@@ -36,42 +36,79 @@ function colorValues(rows, encoding) {
36
36
  : readNominalField(rows, encoding.field);
37
37
  }
38
38
 
39
- function countSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
39
+ export function readArcThetaWeights(rows, field, layerId = "arc") {
40
+ if (!Array.isArray(rows)) throw new TypeError("Arc theta weights require rows.");
41
+ if (typeof field !== "string" || field.length === 0) {
42
+ throw new TypeError("Arc theta weight must be a non-empty string.");
43
+ }
44
+ const weights = rows.map((row, index) => {
45
+ const value = row?.[field];
46
+ if (!Number.isFinite(value) || value < 0) {
47
+ throw new TypeError(
48
+ `Arc theta weight field "${field}" must contain non-negative finite numbers at row ${index}.`
49
+ );
50
+ }
51
+ return value;
52
+ });
53
+ if (weights.every(value => value === 0)) {
54
+ throw new Error(`Arc mark "${layerId}" theta weights must have a positive total.`);
55
+ }
56
+ return cloneAndFreeze(weights);
57
+ }
58
+
59
+ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
40
60
  const theta = layer.encoding.theta;
41
- if (theta.aggregate !== "count") {
42
- throw new Error(`Arc mark "${layer.id}" requires count or radial layout.`);
61
+ if (!["count", "sum"].includes(theta.aggregate)) {
62
+ throw new Error(`Arc mark "${layer.id}" requires count, sum, or radial layout.`);
43
63
  }
44
64
  const values = readNominalField(rows, theta.field);
45
65
  const colors = colorValues(rows, layer.encoding?.color);
66
+ const weights = theta.aggregate === "count"
67
+ ? rows.map(() => 1)
68
+ : readArcThetaWeights(rows, theta.weight, layer.id);
46
69
  const groups = new Map(thetaScale.domain.map(value => [value, []]));
47
70
  for (let index = 0; index < rows.length; index += 1) {
48
71
  const group = groups.get(values[index]);
49
72
  if (group === undefined) {
50
73
  throw new Error(`Arc theta value "${values[index]}" is outside the scale domain.`);
51
74
  }
52
- group.push({ row: rows[index], color: colors[index], sourceIndex: index });
75
+ group.push({
76
+ row: rows[index],
77
+ color: colors[index],
78
+ sourceIndex: index,
79
+ weight: weights[index]
80
+ });
53
81
  }
54
- const total = [...groups.values()].reduce((sum, group) => sum + group.length, 0);
55
- if (total === 0) throw new Error(`Arc mark "${layer.id}" has no rows to count.`);
82
+ const total = [...groups.values()].reduce(
83
+ (sum, group) => sum + group.reduce((groupSum, item) => groupSum + item.weight, 0),
84
+ 0
85
+ );
86
+ if (total === 0) throw new Error(`Arc mark "${layer.id}" has no positive theta weight.`);
56
87
  const start = thetaScale.range[0];
57
88
  const span = thetaScale.range[1] - thetaScale.range[0];
58
89
  const innerRadius = frame.availableRadius * innerRadiusRatio;
59
90
  let cursor = start;
60
91
  const sectors = [];
61
- for (const value of thetaScale.domain) {
92
+ const positiveValues = thetaScale.domain.filter(value =>
93
+ groups.get(value).some(item => item.weight > 0)
94
+ );
95
+ for (const [positiveIndex, value] of positiveValues.entries()) {
62
96
  const group = groups.get(value);
63
- if (group.length === 0) continue;
64
- const endTheta = cursor + group.length / total * span;
97
+ const aggregateValue = group.reduce((sum, item) => sum + item.weight, 0);
98
+ const endTheta = positiveIndex === positiveValues.length - 1
99
+ ? thetaScale.range[1]
100
+ : cursor + aggregateValue / total * span;
65
101
  const distinctColors = [...new Set(group.map(item => item.color))];
66
102
  if (distinctColors.length > 1) {
67
103
  throw new Error(
68
- `Count arc theta group "${value}" must resolve to one color value.`
104
+ `Aggregated arc theta group "${value}" must resolve to one color value.`
69
105
  );
70
106
  }
71
107
  sectors.push({
72
108
  key: value,
73
109
  theta: value,
74
110
  count: group.length,
111
+ aggregateValue,
75
112
  color: distinctColors[0],
76
113
  startTheta: cursor,
77
114
  endTheta,
@@ -147,8 +184,8 @@ export function deriveArcSectors(rows, layer, {
147
184
  ) {
148
185
  throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
149
186
  }
150
- const sectors = theta.aggregate === "count"
151
- ? countSectors(rows, layer, thetaScale, frame, innerRadiusRatio)
187
+ const sectors = ["count", "sum"].includes(theta.aggregate)
188
+ ? proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio)
152
189
  : radialSectors(rows, layer, thetaScale, radiusScale);
153
190
  return cloneAndFreeze({ sectors });
154
191
  }
@@ -8,12 +8,14 @@ import {
8
8
  layoutSeriesPartition,
9
9
  validateColorLayout
10
10
  } from "./seriesLayout.js";
11
+ import { stableOrderPathValues } from "./pathOrder.js";
12
+ import { deriveCategoricalDensitySeries } from "./categoricalDensity.js";
11
13
 
12
14
  export function deriveAreaSeries(rows, layer) {
13
15
  if (layer?.mark?.type !== "area") {
14
16
  throw new Error("Area series derivation requires a semantic area mark.");
15
17
  }
16
- const { x, y, x2, y2, group } = layer.encoding ?? {};
18
+ const { x, y, x2, y2, group, color } = layer.encoding ?? {};
17
19
  const vertical =
18
20
  ["quantitative", "temporal"].includes(x?.fieldType) &&
19
21
  y?.fieldType === "quantitative" &&
@@ -46,24 +48,44 @@ export function deriveAreaSeries(rows, layer) {
46
48
  const groupValues = group === undefined
47
49
  ? rows.map(() => undefined)
48
50
  : readNominalField(rows, group.field);
51
+ const colorValues = color === undefined
52
+ ? rows.map(() => undefined)
53
+ : readNominalField(rows, color.field);
54
+ const pathOrder = layer.encoding?.pathOrder;
55
+ const orderValues = pathOrder === undefined
56
+ ? undefined
57
+ : readQuantitativeField(rows, pathOrder.field);
49
58
  const groups = new Map();
50
59
 
51
60
  for (let index = 0; index < rows.length; index += 1) {
52
61
  const key = groupValues[index];
53
62
  const series = groups.get(key) ?? {
54
- key: group === undefined ? {} : { [group.field]: key },
63
+ key: {
64
+ ...(group === undefined ? {} : { [group.field]: key }),
65
+ ...(color === undefined ? {} : { [color.field]: colorValues[index] })
66
+ },
55
67
  values: []
56
68
  };
69
+ if (
70
+ color !== undefined &&
71
+ !Object.is(series.key[color.field], colorValues[index])
72
+ ) {
73
+ throw new Error(
74
+ `Area series "${String(key)}" must have one color value.`
75
+ );
76
+ }
57
77
  series.values.push(vertical
58
78
  ? {
59
79
  x: independentValues[index],
60
80
  y: lower[index],
61
- y2: upper[index]
81
+ y2: upper[index],
82
+ ...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
62
83
  }
63
84
  : {
64
85
  x: lower[index],
65
86
  x2: upper[index],
66
- y: independentValues[index]
87
+ y: independentValues[index],
88
+ ...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
67
89
  });
68
90
  groups.set(key, series);
69
91
  }
@@ -72,7 +94,13 @@ export function deriveAreaSeries(rows, layer) {
72
94
  }
73
95
  const series = [...groups.values()].map(item => {
74
96
  const key = vertical ? "x" : "y";
75
- const values = item.values.sort((left, right) => left[key] - right[key]);
97
+ const values = pathOrder === undefined
98
+ ? item.values.sort((left, right) => left[key] - right[key])
99
+ : stableOrderPathValues(
100
+ item.values.map(({ pathOrder: _pathOrder, ...value }) => value),
101
+ item.values.map(value => value.pathOrder),
102
+ pathOrder.order
103
+ );
76
104
  if (values.length < 2) {
77
105
  throw new Error(
78
106
  `Area series on mark "${layer.id}" requires at least two points.`
@@ -96,9 +124,17 @@ export function deriveDensityAreaSeries(rows, layer, transform) {
96
124
  if (layer?.mark?.type !== "area") {
97
125
  throw new Error("Density area derivation requires a semantic area mark.");
98
126
  }
127
+ if (layer.encoding?.pathOrder !== undefined) {
128
+ throw new Error(
129
+ `Density area mark "${layer.id}" does not support path order.`
130
+ );
131
+ }
99
132
  if (transform?.type !== "density" || !Array.isArray(transform.as)) {
100
133
  throw new Error(`Area mark "${layer.id}" requires density provenance.`);
101
134
  }
135
+ if (transform.placement?.type === "category") {
136
+ return deriveCategoricalDensitySeries(rows, layer, transform);
137
+ }
102
138
  const { x, y, group } = layer.encoding ?? {};
103
139
  if (
104
140
  x?.fieldType !== "quantitative" ||