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.
- package/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- 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
|
+
}
|