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,107 @@
1
+ import { measureTextWidth, wrapText } from "./text.js";
2
+
3
+ function lineCenters(lines, style, lineHeight, start) {
4
+ return lines.map((_, index) => start + style.fontSize / 2 + index * lineHeight);
5
+ }
6
+
7
+ export function buildTitleReadingBlock({ text, subtitle }, config) {
8
+ const titleLines = wrapText(text, {
9
+ maxWidth: config.maxWidth,
10
+ mode: config.wrap,
11
+ style: config.titleStyle
12
+ });
13
+ const subtitleLines = subtitle === undefined ? [] : wrapText(subtitle, {
14
+ maxWidth: config.maxWidth,
15
+ mode: config.wrap,
16
+ style: config.subtitleStyle
17
+ });
18
+ const titleLineHeight = config.lineHeight ?? config.titleStyle.fontSize * 1.2;
19
+ const subtitleLineHeight = config.lineHeight ??
20
+ config.subtitleStyle.fontSize * 1.2;
21
+ const titleCenters = lineCenters(
22
+ titleLines,
23
+ config.titleStyle,
24
+ titleLineHeight,
25
+ 0
26
+ );
27
+ const titleBottom = titleCenters.at(-1) + config.titleStyle.fontSize / 2;
28
+ const subtitleStart = titleBottom + (subtitleLines.length === 0 ? 0 : config.gap);
29
+ const subtitleCenters = lineCenters(
30
+ subtitleLines,
31
+ config.subtitleStyle,
32
+ subtitleLineHeight,
33
+ subtitleStart
34
+ );
35
+ const height = subtitleLines.length === 0
36
+ ? titleBottom
37
+ : subtitleCenters.at(-1) + config.subtitleStyle.fontSize / 2;
38
+ const widths = [
39
+ ...titleLines.map(line => measureTextWidth(line, config.titleStyle)),
40
+ ...subtitleLines.map(line => measureTextWidth(line, config.subtitleStyle))
41
+ ];
42
+ return {
43
+ titleLines,
44
+ subtitleLines,
45
+ titleCenters,
46
+ subtitleCenters,
47
+ width: Math.max(...widths),
48
+ height
49
+ };
50
+ }
51
+
52
+ export function alignedTitleAnchor(start, length, blockLength, align) {
53
+ if (align === "left") return start + blockLength / 2;
54
+ if (align === "center") return start + length / 2;
55
+ return start + length - blockLength / 2;
56
+ }
57
+
58
+ function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
59
+ const width = measureTextWidth(text, style);
60
+ const left = align === "left" ? 0 : align === "center" ? -width / 2 : -width;
61
+ const right = left + width;
62
+ const top = -style.fontSize / 2;
63
+ const bottom = style.fontSize / 2;
64
+ const cosine = Math.cos(rotation);
65
+ const sine = Math.sin(rotation);
66
+ const corners = [
67
+ [left, top], [right, top], [right, bottom], [left, bottom]
68
+ ].map(([localX, localY]) => ({
69
+ x: x + localX * cosine - localY * sine,
70
+ y: y + localX * sine + localY * cosine
71
+ }));
72
+ return {
73
+ left: Math.min(...corners.map(point => point.x)),
74
+ right: Math.max(...corners.map(point => point.x)),
75
+ top: Math.min(...corners.map(point => point.y)),
76
+ bottom: Math.max(...corners.map(point => point.y))
77
+ };
78
+ }
79
+
80
+ function unionBounds(bounds) {
81
+ return {
82
+ left: Math.min(...bounds.map(item => item.left)),
83
+ right: Math.max(...bounds.map(item => item.right)),
84
+ top: Math.min(...bounds.map(item => item.top)),
85
+ bottom: Math.max(...bounds.map(item => item.bottom))
86
+ };
87
+ }
88
+
89
+ export function resolveTitleComponentBounds(component, style) {
90
+ return unionBounds(component.lines.map((text, index) => axisAlignedTextBounds({
91
+ x: Array.isArray(component.x) ? component.x[index] : component.x,
92
+ y: Array.isArray(component.y) ? component.y[index] : component.y,
93
+ text,
94
+ style,
95
+ align: component.textAlign,
96
+ rotation: component.rotation
97
+ })));
98
+ }
99
+
100
+ export function unionTitleBounds(bounds) {
101
+ return unionBounds(bounds);
102
+ }
103
+
104
+ export function layoutBoundsIntersect(first, second) {
105
+ return first.left < second.right && first.right > second.left &&
106
+ first.top < second.bottom && first.bottom > second.top;
107
+ }
@@ -0,0 +1,184 @@
1
+ import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
2
+ import {
3
+ BAR_ORIENTATIONS,
4
+ resolveBarChannels,
5
+ resolveBarColorLayout
6
+ } from "../../grammar/bars/policy.js";
7
+ import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
8
+ import {
9
+ isDiscretePositionScaleType,
10
+ mapContinuousScaleValues,
11
+ mapOrdinalPositionValues,
12
+ mapSequentialColors
13
+ } from "../../grammar/scales.js";
14
+ import {
15
+ DEFAULT_BAR_FILL,
16
+ DEFAULT_BAR_STROKE,
17
+ DEFAULT_BAR_STROKE_WIDTH
18
+ } from "./resolve.js";
19
+ import { deriveGroupedRectangles } from "./grouped.js";
20
+ import { resolveBarWidth } from "../../grammar/bars/geometry.js";
21
+
22
+ export function deriveAggregateRectangles(required, resolved, widthConfig) {
23
+ const { dataset, layer } = required;
24
+ const layout = resolveBarColorLayout(layer);
25
+ if (layout === "group") {
26
+ return deriveGroupedRectangles(required, resolved, widthConfig);
27
+ }
28
+
29
+ const xScale = resolved.resolvedScales[required.xEncoding.scale];
30
+ const yScale = resolved.resolvedScales[required.yEncoding.scale];
31
+ const channels = resolveBarChannels(layer);
32
+ const vertical = channels.orientation === BAR_ORIENTATIONS.vertical;
33
+ const categoryScale = vertical ? xScale : yScale;
34
+ const measureScale = vertical ? yScale : xScale;
35
+ const colorEncoding = layer.encoding?.color;
36
+ const colorScale = resolved.resolvedScales[colorEncoding?.scale];
37
+ if (colorEncoding !== undefined && colorScale === undefined) {
38
+ throw new Error(
39
+ `Bar mark "${layer.id}" requires a resolved color scale.`
40
+ );
41
+ }
42
+
43
+ const seriesDomain = colorScale?.domain ?? [undefined];
44
+ const seriesIndex = new Map(
45
+ seriesDomain.map((value, index) => [value, index])
46
+ );
47
+ const colors = new Map(seriesDomain.map((value, index) => [
48
+ value,
49
+ colorScale?.range[index % colorScale.range.length] ?? DEFAULT_BAR_FILL
50
+ ]));
51
+ const cells = deriveBarAggregates(dataset.values, layer).values;
52
+ const discreteCategory = categoryScale.type === "ordinal" ||
53
+ isDiscretePositionScaleType(categoryScale.type);
54
+ const categoryDomain = discreteCategory
55
+ ? categoryScale.domain
56
+ : [...new Set(cells.map(cell => cell[channels.category]))]
57
+ .sort((left, right) => left - right);
58
+ const categoryIndex = new Map(
59
+ categoryDomain.map((value, index) => [value, index])
60
+ );
61
+ const cellMap = new Map(cells.map(cell => [
62
+ JSON.stringify([cell[channels.category], cell.color]),
63
+ cell
64
+ ]));
65
+ const thickness = resolveBarWidth(
66
+ widthConfig,
67
+ Math.abs(categoryScale.bandwidth ?? categoryScale.step)
68
+ );
69
+ const baseline = layout === "overlay" ? measureScale.domain[0] : 0;
70
+ const continuousColor = colorEncoding?.fieldType === "quantitative";
71
+ if (continuousColor) {
72
+ const mappedColors = mapSequentialColors(
73
+ cells.map(cell => cell.color),
74
+ colorScale.domain,
75
+ colorScale.range,
76
+ {
77
+ interpolation: colorScale.interpolate,
78
+ clamp: colorScale.clamp ?? false
79
+ }
80
+ );
81
+ const cellByCategory = new Map(
82
+ cells.map((cell, index) => [cell[channels.category], { cell, index }])
83
+ );
84
+ const existing = resolved.graphicSpec.objects[layer.id].items;
85
+ const config = resolved.markConfigs[layer.id] ?? {};
86
+ const appearance = config.barAppearance ?? {};
87
+ return categoryDomain.flatMap(categoryValue => {
88
+ const entry = cellByCategory.get(categoryValue);
89
+ if (entry === undefined) return [];
90
+ const { cell, index } = entry;
91
+ const categoryCenter = discreteCategory
92
+ ? mapOrdinalPositionValues([categoryValue], categoryScale)[0]
93
+ : mapContinuousScaleValues(
94
+ [categoryValue],
95
+ categoryScale
96
+ )[0];
97
+ const [start, end] = mapContinuousScaleValues(
98
+ [baseline, cell[channels.measure]],
99
+ measureScale
100
+ );
101
+ return [{
102
+ x: vertical ? categoryCenter - thickness / 2 : Math.min(start, end),
103
+ y: vertical ? Math.min(start, end) : categoryCenter - thickness / 2,
104
+ width: vertical ? thickness : Math.abs(start - end),
105
+ height: vertical ? Math.abs(start - end) : thickness,
106
+ fill: mappedColors[index],
107
+ stroke: appearance.stroke === false
108
+ ? "transparent"
109
+ : appearance.stroke ?? config.stroke ??
110
+ existing[index]?.properties.stroke ?? DEFAULT_BAR_STROKE,
111
+ strokeWidth: appearance.stroke === false
112
+ ? 0
113
+ : appearance.strokeWidth ?? config.strokeWidth ??
114
+ existing[index]?.properties.strokeWidth ?? DEFAULT_BAR_STROKE_WIDTH,
115
+ ...((appearance.opacity ?? config.opacity) === undefined
116
+ ? (existing[index]?.properties.opacity === undefined
117
+ ? {}
118
+ : { opacity: existing[index].properties.opacity })
119
+ : { opacity: appearance.opacity ?? config.opacity })
120
+ }];
121
+ });
122
+ }
123
+ const segments = [];
124
+
125
+ for (const category of categoryDomain) {
126
+ const partition = seriesDomain.map(color =>
127
+ cellMap.get(JSON.stringify([category, color]))?.[channels.measure] ?? 0
128
+ );
129
+ for (const segment of layoutSeriesPartition(partition, layout, { baseline })) {
130
+ const color = seriesDomain[segment.index];
131
+ const cell = cellMap.get(JSON.stringify([category, color]));
132
+ if (cell === undefined) continue;
133
+ segments.push({ category, color, ...segment });
134
+ }
135
+ }
136
+
137
+ const existing = resolved.graphicSpec.objects[layer.id].items;
138
+ const config = resolved.markConfigs[layer.id] ?? {};
139
+ const appearance = config.barAppearance ?? {};
140
+ return segments.map((segment, index) => {
141
+ const category = categoryIndex.get(segment.category);
142
+ const color = seriesIndex.get(segment.color);
143
+ if (category === undefined || color === undefined) {
144
+ throw new Error("Bar value is outside a resolved ordinal domain.");
145
+ }
146
+ const categoryCenter = discreteCategory
147
+ ? mapOrdinalPositionValues([segment.category], categoryScale)[0]
148
+ : mapContinuousScaleValues(
149
+ [segment.category],
150
+ categoryScale
151
+ )[0];
152
+ const [start, end] = mapContinuousScaleValues(
153
+ [segment.start, segment.end],
154
+ measureScale
155
+ );
156
+ return {
157
+ x: vertical
158
+ ? categoryCenter - thickness / 2
159
+ : Math.min(start, end),
160
+ y: vertical
161
+ ? Math.min(start, end)
162
+ : categoryCenter - thickness / 2,
163
+ width: vertical ? thickness : Math.abs(start - end),
164
+ height: vertical ? Math.abs(start - end) : thickness,
165
+ fill: segment.color === undefined
166
+ ? appearance.fill ?? config.fill ?? colors.get(segment.color)
167
+ : colors.get(segment.color),
168
+ stroke: appearance.stroke === false
169
+ ? "transparent"
170
+ : appearance.stroke ?? config.stroke ?? existing[index]?.properties.stroke ?? DEFAULT_BAR_STROKE,
171
+ strokeWidth:
172
+ appearance.stroke === false
173
+ ? 0
174
+ : appearance.strokeWidth ?? config.strokeWidth ??
175
+ existing[index]?.properties.strokeWidth ??
176
+ DEFAULT_BAR_STROKE_WIDTH,
177
+ ...((appearance.opacity ?? config.opacity) === undefined
178
+ ? (existing[index]?.properties.opacity === undefined
179
+ ? {}
180
+ : { opacity: existing[index].properties.opacity })
181
+ : { opacity: appearance.opacity ?? config.opacity })
182
+ };
183
+ });
184
+ }
@@ -0,0 +1,117 @@
1
+ import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
2
+ import { mapContinuousScaleValues } from "../../grammar/scales.js";
3
+ import { sameOrderedValues } from "../../core/validation.js";
4
+ import {
5
+ DEFAULT_BAR_STROKE,
6
+ DEFAULT_BAR_STROKE_WIDTH
7
+ } from "./resolve.js";
8
+ import { resolveBarWidth } from "../../grammar/bars/geometry.js";
9
+
10
+ export function deriveGroupedRectangles(required, resolved, widthConfig) {
11
+ const { dataset, layer } = required;
12
+ const xScale = resolved.resolvedScales[required.xEncoding.scale];
13
+ const yScale = resolved.resolvedScales[required.yEncoding.scale];
14
+ const colorEncoding = layer.encoding?.color;
15
+ const offsetEncoding = layer.encoding?.xOffset;
16
+ const colorScale = resolved.resolvedScales[colorEncoding?.scale];
17
+ const offsetScale = resolved.resolvedScales[offsetEncoding?.scale];
18
+
19
+ if (
20
+ colorEncoding?.field === undefined ||
21
+ offsetEncoding?.field !== colorEncoding.field ||
22
+ colorScale === undefined ||
23
+ offsetScale === undefined
24
+ ) {
25
+ throw new Error(
26
+ `Grouped bar mark "${layer.id}" requires matching color and xOffset scales.`
27
+ );
28
+ }
29
+ if (!sameOrderedValues(colorScale.domain, offsetScale.domain)) {
30
+ throw new Error(
31
+ `Grouped bar mark "${layer.id}" requires matching color and xOffset domains.`
32
+ );
33
+ }
34
+
35
+ const xIndex = new Map(xScale.domain.map((value, index) => [value, index]));
36
+ const offsetIndex = new Map(
37
+ offsetScale.domain.map((value, index) => [value, index])
38
+ );
39
+ const colors = new Map(
40
+ colorScale.domain.map((value, index) => [
41
+ value,
42
+ colorScale.range[index % colorScale.range.length]
43
+ ])
44
+ );
45
+ const offsetMidpoint = (offsetScale.range[0] + offsetScale.range[1]) / 2;
46
+ const direction = Math.sign(xScale.step) || 1;
47
+ const offsetDirection = Math.sign(offsetScale.step) || 1;
48
+ const width = resolveBarWidth(widthConfig, offsetScale.bandwidth);
49
+ const baseline = mapContinuousScaleValues(
50
+ [yScale.domain[0]],
51
+ yScale
52
+ )[0];
53
+ const cells = [...deriveBarAggregates(dataset.values, layer).values].sort(
54
+ (left, right) =>
55
+ (xScale.type === "time"
56
+ ? left.x - right.x
57
+ : xIndex.get(left.x) - xIndex.get(right.x)) ||
58
+ offsetIndex.get(left.color) - offsetIndex.get(right.color)
59
+ );
60
+ const existing = resolved.graphicSpec.objects[layer.id].items;
61
+ const config = resolved.markConfigs[layer.id] ?? {};
62
+ const appearance = config.barAppearance ?? {};
63
+
64
+ return cells.map((cell, index) => {
65
+ const category = xIndex.get(cell.x);
66
+ const offset = offsetIndex.get(cell.color);
67
+ if ((xScale.type !== "time" && category === undefined) || offset === undefined) {
68
+ throw new Error("Grouped bar value is outside a resolved ordinal domain.");
69
+ }
70
+
71
+ const temporal = xScale.type === "time";
72
+ const categoryStart = temporal
73
+ ? mapContinuousScaleValues(
74
+ [cell.x],
75
+ xScale
76
+ )[0] - xScale.bandwidth / 2
77
+ : (xScale.start ?? xScale.range[0]) + category * xScale.step;
78
+ const offsetCenter = offsetScale.start + offset * offsetScale.step +
79
+ offsetDirection * offsetScale.bandwidth / 2;
80
+ const center = temporal
81
+ ? categoryStart + offsetCenter
82
+ : xScale.step > 0 && offsetScale.step > 0
83
+ ? categoryStart + offsetCenter
84
+ : categoryStart + xScale.step / 2 +
85
+ direction * (offsetCenter - offsetMidpoint);
86
+ const x = temporal || (xScale.step > 0 && offsetScale.step > 0)
87
+ ? categoryStart + offsetScale.start + offset * offsetScale.step +
88
+ (offsetScale.bandwidth - width) / 2
89
+ : center - width / 2;
90
+ const valueY = mapContinuousScaleValues(
91
+ [cell.y],
92
+ yScale
93
+ )[0];
94
+
95
+ return {
96
+ x,
97
+ y: Math.min(valueY, baseline),
98
+ width,
99
+ height: Math.abs(baseline - valueY),
100
+ fill: colors.get(cell.color),
101
+ stroke: appearance.stroke === false
102
+ ? "transparent"
103
+ : appearance.stroke ?? config.stroke ?? existing[index]?.properties.stroke ?? DEFAULT_BAR_STROKE,
104
+ strokeWidth:
105
+ appearance.stroke === false
106
+ ? 0
107
+ : appearance.strokeWidth ?? config.strokeWidth ??
108
+ existing[index]?.properties.strokeWidth ??
109
+ DEFAULT_BAR_STROKE_WIDTH,
110
+ ...((appearance.opacity ?? config.opacity) === undefined
111
+ ? (existing[index]?.properties.opacity === undefined
112
+ ? {}
113
+ : { opacity: existing[index].properties.opacity })
114
+ : { opacity: appearance.opacity ?? config.opacity })
115
+ };
116
+ });
117
+ }
@@ -0,0 +1,163 @@
1
+ import {
2
+ countHistogramBins,
3
+ findHistogramBinIndex,
4
+ resolveHistogramBins
5
+ } from "../../grammar/histogram.js";
6
+ import {
7
+ mapContinuousScaleValues,
8
+ mapOrdinalValues,
9
+ readNominalField,
10
+ readQuantitativeField
11
+ } from "../../grammar/scales.js";
12
+ import { resolveBarColorLayout } from "../../grammar/bars/policy.js";
13
+ import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
14
+ import {
15
+ DEFAULT_BAR_FILL,
16
+ DEFAULT_BAR_STROKE,
17
+ DEFAULT_BAR_STROKE_WIDTH
18
+ } from "./resolve.js";
19
+
20
+ export function deriveHistogramSegments({
21
+ dataset,
22
+ layer,
23
+ xEncoding,
24
+ xScale,
25
+ resolvedScales
26
+ }) {
27
+ const xValues = readQuantitativeField(dataset.values, xEncoding.field);
28
+ const bins = resolveHistogramBins({
29
+ values: xValues,
30
+ bin: xEncoding.bin,
31
+ domain: xScale.domain,
32
+ nice: xScale.nice ?? true,
33
+ zero: xScale.zero ?? false
34
+ });
35
+ const colorEncoding = layer.encoding?.color;
36
+ const layout = resolveBarColorLayout(layer);
37
+
38
+ if (colorEncoding?.scale === undefined) {
39
+ return countHistogramBins(xValues, bins.boundaries).flatMap((count, bin) =>
40
+ layoutSeriesPartition([count], layout).map(segment => ({
41
+ bin,
42
+ start: bins.boundaries[bin],
43
+ end: bins.boundaries[bin + 1],
44
+ category: segment.index,
45
+ categoryCount: 1,
46
+ stackStart: segment.start,
47
+ stackEnd: segment.end,
48
+ members: dataset.values.filter((_, index) =>
49
+ findHistogramBinIndex(xValues[index], bins.boundaries) === bin
50
+ )
51
+ }))
52
+ );
53
+ }
54
+
55
+ const colorScale = resolvedScales[colorEncoding.scale];
56
+ if (colorScale === undefined) {
57
+ throw new Error(
58
+ `Bar mark "${layer.id}" requires a resolved color scale.`
59
+ );
60
+ }
61
+ const colorValues = readNominalField(dataset.values, colorEncoding.field);
62
+ mapOrdinalValues(colorValues, colorScale.domain, colorScale.range);
63
+ const categoryIndex = new Map(
64
+ colorScale.domain.map((value, index) => [value, index])
65
+ );
66
+ const counts = bins.boundaries.slice(0, -1).map(() =>
67
+ colorScale.domain.map(() => 0)
68
+ );
69
+
70
+ for (let index = 0; index < xValues.length; index += 1) {
71
+ const bin = findHistogramBinIndex(xValues[index], bins.boundaries);
72
+ const category = categoryIndex.get(colorValues[index]);
73
+ if (bin !== -1 && category !== undefined) counts[bin][category] += 1;
74
+ }
75
+
76
+ const segments = [];
77
+ for (let bin = 0; bin < counts.length; bin += 1) {
78
+ const partition = layoutSeriesPartition(counts[bin], layout);
79
+ for (const segment of partition) {
80
+ const category = segment.index;
81
+ segments.push({
82
+ bin,
83
+ start: bins.boundaries[bin],
84
+ end: bins.boundaries[bin + 1],
85
+ category,
86
+ categoryCount: counts[bin].length,
87
+ stackStart: segment.start,
88
+ stackEnd: segment.end,
89
+ members: dataset.values.filter((_, index) =>
90
+ findHistogramBinIndex(xValues[index], bins.boundaries) === bin &&
91
+ categoryIndex.get(colorValues[index]) === category
92
+ ),
93
+ color: mapOrdinalValues(
94
+ [colorScale.domain[category]],
95
+ colorScale.domain,
96
+ colorScale.range
97
+ )[0]
98
+ });
99
+ }
100
+ }
101
+ return segments;
102
+ }
103
+
104
+ export function deriveHistogramRectangles(required, resolved) {
105
+ const xScale = resolved.resolvedScales[required.xEncoding.scale];
106
+ const yScale = resolved.resolvedScales[required.yEncoding.scale];
107
+ const segments = deriveHistogramSegments({
108
+ ...required,
109
+ resolvedScales: resolved.resolvedScales
110
+ });
111
+ const existing = resolved.graphicSpec.objects[required.layer.id].items;
112
+ const layout = resolveBarColorLayout(required.layer);
113
+ const config = resolved.markConfigs[required.layer.id] ?? {};
114
+ const appearance = config.barAppearance ?? {};
115
+
116
+ return segments.map((segment, index) => {
117
+ const [x1, x2] = mapContinuousScaleValues(
118
+ [segment.start, segment.end],
119
+ xScale
120
+ );
121
+ const [y1, y2] = mapContinuousScaleValues(
122
+ [segment.stackStart, segment.stackEnd],
123
+ yScale
124
+ );
125
+ const binStart = Math.min(x1, x2);
126
+ const binWidth = Math.abs(x2 - x1);
127
+ const groupedWidth = layout === "group"
128
+ ? binWidth / segment.categoryCount
129
+ : binWidth;
130
+ const groupedX = layout === "group"
131
+ ? binStart + groupedWidth * segment.category
132
+ : binStart;
133
+ return {
134
+ x: groupedX,
135
+ y: Math.min(y1, y2),
136
+ width: groupedWidth,
137
+ height: Math.abs(y2 - y1),
138
+ fill:
139
+ segment.color ??
140
+ appearance.fill ??
141
+ config.fill ??
142
+ existing[index]?.properties.fill ??
143
+ DEFAULT_BAR_FILL,
144
+ stroke:
145
+ appearance.stroke === false
146
+ ? "transparent"
147
+ : appearance.stroke ?? config.stroke ??
148
+ existing[index]?.properties.stroke ??
149
+ DEFAULT_BAR_STROKE,
150
+ strokeWidth:
151
+ appearance.stroke === false
152
+ ? 0
153
+ : appearance.strokeWidth ?? config.strokeWidth ??
154
+ existing[index]?.properties.strokeWidth ??
155
+ DEFAULT_BAR_STROKE_WIDTH,
156
+ ...((appearance.opacity ?? config.opacity) === undefined
157
+ ? (existing[index]?.properties.opacity === undefined
158
+ ? {}
159
+ : { opacity: existing[index].properties.opacity })
160
+ : { opacity: appearance.opacity ?? config.opacity })
161
+ };
162
+ });
163
+ }
@@ -0,0 +1,53 @@
1
+ import { mapContinuousScaleValues, mapOrdinalPositionValues, mapOrdinalValues } from "../../grammar/scales.js";
2
+ import { resolveBarWidth } from "../../grammar/bars/geometry.js";
3
+ import { DEFAULT_BAR_FILL, DEFAULT_BAR_STROKE, DEFAULT_BAR_STROKE_WIDTH } from "./resolve.js";
4
+
5
+ export function deriveRangedRectangles(required, program, width) {
6
+ const { layer, dataset } = required;
7
+ const vertical = layer.encoding?.y2 !== undefined;
8
+ const categoryChannel = vertical ? "x" : "y";
9
+ const measureChannel = vertical ? "y" : "x";
10
+ const secondaryChannel = `${measureChannel}2`;
11
+ const category = layer.encoding[categoryChannel];
12
+ const primary = layer.encoding[measureChannel];
13
+ const secondary = layer.encoding[secondaryChannel];
14
+ const categoryScale = program.resolvedScales[category.scale];
15
+ const measureScale = program.resolvedScales[primary.scale];
16
+ const centers = mapOrdinalPositionValues(dataset.values.map(row => row[category.field]), categoryScale);
17
+ const first = mapContinuousScaleValues(
18
+ dataset.values.map(row => row[primary.field]),
19
+ measureScale
20
+ );
21
+ const second = mapContinuousScaleValues(
22
+ dataset.values.map(row => row[secondary.field]),
23
+ measureScale
24
+ );
25
+ const band = resolveBarWidth(width, Math.abs(categoryScale.bandwidth ?? categoryScale.step));
26
+ const config = program.markConfigs[layer.id] ?? {};
27
+ const appearance = config.barAppearance ?? {};
28
+ const color = layer.encoding?.color;
29
+ const fills = color === undefined
30
+ ? dataset.values.map(() => appearance.fill ?? config.fill ?? DEFAULT_BAR_FILL)
31
+ : mapOrdinalValues(dataset.values.map(row => row[color.field]), program.resolvedScales[color.scale].domain, program.resolvedScales[color.scale].range);
32
+ return dataset.values.map((_, index) => vertical ? {
33
+ x: centers[index] - band / 2, y: Math.min(first[index], second[index]), width: band,
34
+ height: Math.abs(second[index] - first[index]), fill: fills[index],
35
+ stroke: appearance.stroke === false
36
+ ? "transparent"
37
+ : appearance.stroke ?? (color === undefined ? config.stroke ?? DEFAULT_BAR_STROKE : fills[index]),
38
+ strokeWidth: appearance.stroke === false
39
+ ? 0
40
+ : appearance.strokeWidth ?? config.strokeWidth ?? DEFAULT_BAR_STROKE_WIDTH,
41
+ opacity: appearance.opacity ?? config.opacity ?? 1
42
+ } : {
43
+ x: Math.min(first[index], second[index]), y: centers[index] - band / 2,
44
+ width: Math.abs(second[index] - first[index]), height: band, fill: fills[index],
45
+ stroke: appearance.stroke === false
46
+ ? "transparent"
47
+ : appearance.stroke ?? (color === undefined ? config.stroke ?? DEFAULT_BAR_STROKE : fills[index]),
48
+ strokeWidth: appearance.stroke === false
49
+ ? 0
50
+ : appearance.strokeWidth ?? config.strokeWidth ?? DEFAULT_BAR_STROKE_WIDTH,
51
+ opacity: appearance.opacity ?? config.opacity ?? 1
52
+ });
53
+ }
@@ -0,0 +1,56 @@
1
+ import { DEFAULT_COLORS } from "../../theme/defaults.js";
2
+ import { findDataset } from "../../selectors/datasets.js";
3
+ import { findLayer } from "../../selectors/layers.js";
4
+ import { findSemanticScale } from "../../selectors/scales.js";
5
+ import { BAR_GRAINS, resolveBarGrain } from "../../grammar/bars/policy.js";
6
+
7
+ export const DEFAULT_BAR_FILL = DEFAULT_COLORS.mark;
8
+ export const DEFAULT_BAR_STROKE = "white";
9
+ export const DEFAULT_BAR_STROKE_WIDTH = 0.5;
10
+
11
+ export function requireCompleteBar(program, id) {
12
+ const layer = findLayer(program, id);
13
+
14
+ if (layer?.mark?.type !== "bar") {
15
+ throw new Error(`Unknown bar mark "${id}".`);
16
+ }
17
+ const dataset = findDataset(program, layer.data);
18
+ if (dataset === undefined) {
19
+ throw new Error(`Bar mark "${id}" requires an existing dataset.`);
20
+ }
21
+ const graphic = program.graphicSpec.objects[id];
22
+ const rectCollection = graphic?.type === "collection" &&
23
+ graphic.items?.every(child => child.type === "rect");
24
+ if (graphic?.type !== "rect" && !rectCollection) {
25
+ throw new Error(`Bar mark "${id}" requires rect graphics.`);
26
+ }
27
+
28
+ const xEncoding = layer.encoding?.x;
29
+ const yEncoding = layer.encoding?.y;
30
+ const grain = resolveBarGrain(layer);
31
+
32
+ if (
33
+ xEncoding?.scale === undefined ||
34
+ yEncoding?.scale === undefined ||
35
+ grain === undefined
36
+ ) {
37
+ throw new Error(
38
+ `Bar mark "${id}" requires binned x/count y or ordinal x/aggregate y encodings.`
39
+ );
40
+ }
41
+ const xScale = findSemanticScale(program, xEncoding.scale);
42
+ const yScale = findSemanticScale(program, yEncoding.scale);
43
+ if (xScale === undefined || yScale === undefined) {
44
+ throw new Error(`Bar mark "${id}" requires x and y scales.`);
45
+ }
46
+
47
+ return {
48
+ dataset,
49
+ layer,
50
+ xEncoding,
51
+ yEncoding,
52
+ xScale,
53
+ yScale,
54
+ materialization: grain
55
+ };
56
+ }
@@ -0,0 +1,15 @@
1
+ import { findDataset } from "../selectors/datasets.js";
2
+
3
+ export function findUpstreamTransform(program, dataset, type) {
4
+ const visited = new Set();
5
+ let current = dataset;
6
+ while (current !== undefined && !visited.has(current.id)) {
7
+ visited.add(current.id);
8
+ if (current.transform?.length !== 1) return undefined;
9
+ const transform = current.transform[0];
10
+ if (transform.type === type) return transform;
11
+ if (transform.type !== "markFilter") return undefined;
12
+ current = findDataset(program, current.source);
13
+ }
14
+ return undefined;
15
+ }