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,503 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { deriveAreaSeries, deriveDensityAreaSeries } from "../../grammar/areaSeries.js";
|
|
3
|
+
import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
|
|
4
|
+
import {
|
|
5
|
+
BAR_GRAINS,
|
|
6
|
+
resolveBarChannels,
|
|
7
|
+
resolveBarColorLayout,
|
|
8
|
+
resolveBarGrain
|
|
9
|
+
} from "../../grammar/bars/policy.js";
|
|
10
|
+
import { deriveLineSeries } from "../../grammar/lineSeries.js";
|
|
11
|
+
import { deriveRuleValues } from "../../grammar/rules.js";
|
|
12
|
+
import {
|
|
13
|
+
MARK_GRAPHIC_PROPERTIES,
|
|
14
|
+
selectMarkItemKeys
|
|
15
|
+
} from "../../grammar/markSelection.js";
|
|
16
|
+
import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
|
|
17
|
+
import {
|
|
18
|
+
readNominalField,
|
|
19
|
+
readTemporalField
|
|
20
|
+
} from "../../grammar/scales.js";
|
|
21
|
+
import { deriveHistogramSegments } from "../bars/histogram.js";
|
|
22
|
+
import { findUpstreamTransform } from "../dataProvenance.js";
|
|
23
|
+
import { unionConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
|
|
24
|
+
import { findSemanticScale } from "../../selectors/scales.js";
|
|
25
|
+
|
|
26
|
+
function itemKey(layer, grain, index) {
|
|
27
|
+
return `${layer.id}/${grain}/${index}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function graphicId(layer, index) {
|
|
31
|
+
return `${layer.id}:${index}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function encodingValue(row, encoding) {
|
|
35
|
+
if (encoding === undefined) return undefined;
|
|
36
|
+
if (Object.hasOwn(encoding, "field")) return row?.[encoding.field];
|
|
37
|
+
return encoding.datum;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function channelMapFromRow(row, layer) {
|
|
41
|
+
return Object.fromEntries(
|
|
42
|
+
Object.entries(layer.encoding ?? {})
|
|
43
|
+
.map(([channel, encoding]) => [channel, encodingValue(row, encoding)])
|
|
44
|
+
.filter(([, value]) => value !== undefined)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ownFields(row) {
|
|
49
|
+
return isPlainObject(row) ? { ...row } : {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function uniqueFields(rows) {
|
|
53
|
+
if (rows.length === 0) return {};
|
|
54
|
+
const fields = new Set(rows.flatMap(row =>
|
|
55
|
+
isPlainObject(row) ? Object.keys(row) : []
|
|
56
|
+
));
|
|
57
|
+
return Object.fromEntries([...fields].flatMap(field => {
|
|
58
|
+
if (!rows.every(row => isPlainObject(row) && Object.hasOwn(row, field))) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const value = rows[0][field];
|
|
62
|
+
return rows.every(row => row[field] === value) ? [[field, value]] : [];
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function requireResolvedGraphic(program, layer, type) {
|
|
67
|
+
const graphic = program.graphicSpec.objects[layer.id];
|
|
68
|
+
const compatibleCollection = graphic?.type === "collection" &&
|
|
69
|
+
graphic.items?.every(child => child.type === type);
|
|
70
|
+
if (
|
|
71
|
+
(graphic?.type !== type && !compatibleCollection) ||
|
|
72
|
+
!Array.isArray(graphic.items)
|
|
73
|
+
) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Mark "${layer.id}" requires a materialized ${type} collection for selection.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return graphic;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const SELECTABLE_GRAPHIC_PROPERTIES = new Set(MARK_GRAPHIC_PROPERTIES);
|
|
82
|
+
|
|
83
|
+
function concreteProperties(properties) {
|
|
84
|
+
return Object.fromEntries(
|
|
85
|
+
Object.entries(properties ?? {}).filter(([key, value]) =>
|
|
86
|
+
SELECTABLE_GRAPHIC_PROPERTIES.has(key) &&
|
|
87
|
+
(Number.isFinite(value) || typeof value === "string")
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sharedConcreteProperties(items) {
|
|
93
|
+
if (items.length === 0) return {};
|
|
94
|
+
const shared = concreteProperties(items[0].properties);
|
|
95
|
+
for (const key of Object.keys(shared)) {
|
|
96
|
+
if (!items.every(child => child.properties?.[key] === shared[key])) {
|
|
97
|
+
delete shared[key];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return shared;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function collectionBounds(program, items) {
|
|
104
|
+
const bounds = unionConcreteGraphicBounds(
|
|
105
|
+
program.graphicSpec,
|
|
106
|
+
items.map(item => item.id)
|
|
107
|
+
);
|
|
108
|
+
if (bounds === undefined) return {};
|
|
109
|
+
const { left, top, right, bottom } = bounds;
|
|
110
|
+
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function finalizeItems(program, layer, grain, definitions, graphicType) {
|
|
114
|
+
const graphic = requireResolvedGraphic(program, layer, graphicType);
|
|
115
|
+
const referenced = definitions.flatMap((definition, index) =>
|
|
116
|
+
definition.graphicIndices ?? [index]
|
|
117
|
+
);
|
|
118
|
+
if (
|
|
119
|
+
referenced.some(index => graphic.items[index] === undefined) ||
|
|
120
|
+
(definitions.every(definition => definition.graphicIndices === undefined) &&
|
|
121
|
+
graphic.items.length !== definitions.length)
|
|
122
|
+
) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Mark "${layer.id}" item count does not match its materialized graphics.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return Object.freeze(definitions.map((definition, index) => {
|
|
128
|
+
const indices = definition.graphicIndices ?? [index];
|
|
129
|
+
const items = indices.map(childIndex => graphic.items[childIndex]);
|
|
130
|
+
const properties = definition.properties ?? (
|
|
131
|
+
items.length === 1
|
|
132
|
+
? concreteProperties(items[0].properties)
|
|
133
|
+
: {
|
|
134
|
+
...sharedConcreteProperties(items),
|
|
135
|
+
...collectionBounds(program, items)
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
key: definition.key ?? itemKey(layer, grain, index),
|
|
140
|
+
layer: layer.id,
|
|
141
|
+
markType: layer.mark.type,
|
|
142
|
+
fields: cloneAndFreeze(definition.fields),
|
|
143
|
+
channels: cloneAndFreeze(definition.channels),
|
|
144
|
+
properties: cloneAndFreeze(properties),
|
|
145
|
+
members: Object.freeze([...definition.members]),
|
|
146
|
+
graphicIds: Object.freeze(items.map((child, childOffset) =>
|
|
147
|
+
child.id ?? graphicId(layer, indices[childOffset])
|
|
148
|
+
))
|
|
149
|
+
});
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function resolvePointItems(program, layer, dataset) {
|
|
154
|
+
const graphic = program.graphicSpec.objects[layer.id];
|
|
155
|
+
if (
|
|
156
|
+
!Array.isArray(graphic?.items) ||
|
|
157
|
+
layer.encoding?.x?.scale === undefined ||
|
|
158
|
+
layer.encoding?.y?.scale === undefined ||
|
|
159
|
+
(
|
|
160
|
+
layer.encoding?.size?.scale === undefined &&
|
|
161
|
+
!Number.isFinite(program.markConfigs[layer.id]?.radius)
|
|
162
|
+
)
|
|
163
|
+
) {
|
|
164
|
+
throw new Error(`Point mark "${layer.id}" is incomplete for selection.`);
|
|
165
|
+
}
|
|
166
|
+
let definitions = dataset.values.map((row, index) => ({
|
|
167
|
+
key: itemKey(layer, "point", index),
|
|
168
|
+
fields: ownFields(row),
|
|
169
|
+
channels: channelMapFromRow(row, layer),
|
|
170
|
+
properties: concreteProperties(graphic.items[index]?.properties),
|
|
171
|
+
members: [row]
|
|
172
|
+
}));
|
|
173
|
+
for (const config of Object.values(
|
|
174
|
+
program.materializationConfigs.highlights ?? {}
|
|
175
|
+
)) {
|
|
176
|
+
if (config.target !== layer.id || config.bringToFront !== true) continue;
|
|
177
|
+
const selection = program.materializationConfigs.selections?.[config.selection];
|
|
178
|
+
if (selection?.target !== layer.id) continue;
|
|
179
|
+
const selected = new Set(selectMarkItemKeys(definitions, selection.selector));
|
|
180
|
+
definitions = [
|
|
181
|
+
...definitions.filter(definition => !selected.has(definition.key)),
|
|
182
|
+
...definitions.filter(definition => selected.has(definition.key))
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
return finalizeItems(
|
|
186
|
+
program,
|
|
187
|
+
layer,
|
|
188
|
+
"point",
|
|
189
|
+
definitions,
|
|
190
|
+
program.graphicSpec.objects[layer.id]?.type
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function categoryValues(rows, encoding) {
|
|
195
|
+
return encoding.fieldType === "temporal"
|
|
196
|
+
? readTemporalField(rows, encoding.field)
|
|
197
|
+
: readNominalField(rows, encoding.field);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function aggregateMembers(rows, layer, cell, channels) {
|
|
201
|
+
const category = layer.encoding[channels.category];
|
|
202
|
+
const categories = categoryValues(rows, category);
|
|
203
|
+
const categoryValue = cell[channels.category];
|
|
204
|
+
const color = layer.encoding?.color;
|
|
205
|
+
const colors = color?.fieldType !== "nominal"
|
|
206
|
+
? undefined
|
|
207
|
+
: readNominalField(rows, color.field);
|
|
208
|
+
return rows.filter((_, index) =>
|
|
209
|
+
categories[index] === categoryValue &&
|
|
210
|
+
(colors === undefined || colors[index] === cell.color)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function aggregateCellDefinitions(program, layer, dataset) {
|
|
215
|
+
const channels = resolveBarChannels(layer);
|
|
216
|
+
const derived = deriveBarAggregates(dataset.values, layer).values;
|
|
217
|
+
const categoryEncoding = layer.encoding[channels.category];
|
|
218
|
+
const categoryScale = program.resolvedScales[categoryEncoding.scale];
|
|
219
|
+
const measureScale = program.resolvedScales[
|
|
220
|
+
layer.encoding[channels.measure].scale
|
|
221
|
+
];
|
|
222
|
+
const colorEncoding = layer.encoding?.color;
|
|
223
|
+
const colorScale = program.resolvedScales[colorEncoding?.scale];
|
|
224
|
+
const offsetScale = program.resolvedScales[layer.encoding?.xOffset?.scale];
|
|
225
|
+
const layout = resolveBarColorLayout(layer);
|
|
226
|
+
|
|
227
|
+
function definition(cell, start, end) {
|
|
228
|
+
const members = aggregateMembers(dataset.values, layer, cell, channels);
|
|
229
|
+
return {
|
|
230
|
+
fields: uniqueFields(members),
|
|
231
|
+
channels: {
|
|
232
|
+
[channels.category]: cell[channels.category],
|
|
233
|
+
[channels.measure]: start,
|
|
234
|
+
[`${channels.measure}2`]: end,
|
|
235
|
+
...(cell.color === undefined ? {} : { color: cell.color }),
|
|
236
|
+
...(layer.encoding?.xOffset === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: { xOffset: cell.color })
|
|
239
|
+
},
|
|
240
|
+
members
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (colorEncoding?.fieldType === "quantitative") {
|
|
245
|
+
const cells = new Map(derived.map(cell => [
|
|
246
|
+
cell[channels.category],
|
|
247
|
+
cell
|
|
248
|
+
]));
|
|
249
|
+
const categories = ["ordinal", "band", "point"].includes(categoryScale.type)
|
|
250
|
+
? categoryScale.domain
|
|
251
|
+
: [...new Set(derived.map(cell => cell[channels.category]))]
|
|
252
|
+
.sort((left, right) => left - right);
|
|
253
|
+
return categories.flatMap(category => {
|
|
254
|
+
const cell = cells.get(category);
|
|
255
|
+
return cell === undefined
|
|
256
|
+
? []
|
|
257
|
+
: [definition(
|
|
258
|
+
cell,
|
|
259
|
+
measureScale.domain[0],
|
|
260
|
+
cell[channels.measure]
|
|
261
|
+
)];
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (layout === "group" && offsetScale !== undefined) {
|
|
266
|
+
const categoryIndex = new Map(
|
|
267
|
+
categoryScale.domain.map((value, index) => [value, index])
|
|
268
|
+
);
|
|
269
|
+
const offsetIndex = new Map(
|
|
270
|
+
offsetScale.domain.map((value, index) => [value, index])
|
|
271
|
+
);
|
|
272
|
+
const cells = [...derived].sort((left, right) =>
|
|
273
|
+
categoryIndex.get(left[channels.category]) -
|
|
274
|
+
categoryIndex.get(right[channels.category]) ||
|
|
275
|
+
offsetIndex.get(left.color) - offsetIndex.get(right.color)
|
|
276
|
+
);
|
|
277
|
+
return cells.map(cell => definition(
|
|
278
|
+
cell,
|
|
279
|
+
measureScale.domain[0],
|
|
280
|
+
cell[channels.measure]
|
|
281
|
+
));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const categories = ["ordinal", "band", "point"].includes(categoryScale.type)
|
|
285
|
+
? categoryScale.domain
|
|
286
|
+
: [...new Set(derived.map(cell => cell[channels.category]))]
|
|
287
|
+
.sort((left, right) => left - right);
|
|
288
|
+
const series = colorScale?.domain ?? [undefined];
|
|
289
|
+
const lookup = new Map(derived.map(cell => [
|
|
290
|
+
JSON.stringify([cell[channels.category], cell.color]),
|
|
291
|
+
cell
|
|
292
|
+
]));
|
|
293
|
+
const baseline = layout === "overlay" ? measureScale.domain[0] : 0;
|
|
294
|
+
return categories.flatMap(category => {
|
|
295
|
+
const cells = series.map(color =>
|
|
296
|
+
lookup.get(JSON.stringify([category, color]))
|
|
297
|
+
);
|
|
298
|
+
const values = cells.map(cell => cell?.[channels.measure] ?? 0);
|
|
299
|
+
return layoutSeriesPartition(values, layout, { baseline }).flatMap(segment => {
|
|
300
|
+
const cell = cells[segment.index];
|
|
301
|
+
return cell === undefined
|
|
302
|
+
? []
|
|
303
|
+
: [definition(cell, segment.start, segment.end)];
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function histogramDefinitions(program, layer, dataset) {
|
|
309
|
+
const xEncoding = layer.encoding.x;
|
|
310
|
+
const colorEncoding = layer.encoding?.color;
|
|
311
|
+
const segments = deriveHistogramSegments({
|
|
312
|
+
dataset,
|
|
313
|
+
layer,
|
|
314
|
+
xEncoding,
|
|
315
|
+
xScale: findSemanticScale(program, xEncoding.scale),
|
|
316
|
+
resolvedScales: program.resolvedScales
|
|
317
|
+
});
|
|
318
|
+
const colorScale = program.resolvedScales[colorEncoding?.scale];
|
|
319
|
+
return segments.map(segment => {
|
|
320
|
+
const colorValue = colorScale?.domain[segment.category];
|
|
321
|
+
return {
|
|
322
|
+
fields: uniqueFields(segment.members),
|
|
323
|
+
channels: {
|
|
324
|
+
x: segment.start,
|
|
325
|
+
x2: segment.end,
|
|
326
|
+
y: segment.stackStart,
|
|
327
|
+
y2: segment.stackEnd,
|
|
328
|
+
...(colorValue === undefined ? {} : { color: colorValue })
|
|
329
|
+
},
|
|
330
|
+
members: segment.members
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function rangedDefinitions(layer, dataset) {
|
|
336
|
+
return dataset.values.map(row => ({
|
|
337
|
+
fields: ownFields(row),
|
|
338
|
+
channels: channelMapFromRow(row, layer),
|
|
339
|
+
members: [row]
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function sharedChannels(definitions) {
|
|
344
|
+
if (definitions.length === 0) return {};
|
|
345
|
+
const shared = { ...definitions[0].channels };
|
|
346
|
+
for (const channel of Object.keys(shared)) {
|
|
347
|
+
if (!definitions.every(definition =>
|
|
348
|
+
definition.channels[channel] === shared[channel]
|
|
349
|
+
)) delete shared[channel];
|
|
350
|
+
}
|
|
351
|
+
return shared;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function barStackDefinitions(layer, barGrain, definitions) {
|
|
355
|
+
const layout = resolveBarColorLayout(layer);
|
|
356
|
+
if (!["stack", "fill", "diverging"].includes(layout)) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`Bar mark "${layer.id}" does not define stacked items at its current "${layout}" layout.`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
if (barGrain === BAR_GRAINS.ranged) {
|
|
362
|
+
throw new Error(`Ranged bar mark "${layer.id}" has no stack grain.`);
|
|
363
|
+
}
|
|
364
|
+
const channels = resolveBarChannels(layer);
|
|
365
|
+
const categoryChannels = barGrain === BAR_GRAINS.histogram
|
|
366
|
+
? ["x", "x2"]
|
|
367
|
+
: [channels.category];
|
|
368
|
+
const measure = channels.measure;
|
|
369
|
+
const secondary = `${measure}2`;
|
|
370
|
+
const groups = new Map();
|
|
371
|
+
definitions.forEach((definition, index) => {
|
|
372
|
+
const key = JSON.stringify(categoryChannels.map(channel =>
|
|
373
|
+
definition.channels[channel]
|
|
374
|
+
));
|
|
375
|
+
const group = groups.get(key) ?? [];
|
|
376
|
+
group.push({ definition, index });
|
|
377
|
+
groups.set(key, group);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return [...groups.values()].map((entries, index) => {
|
|
381
|
+
const grouped = entries.map(entry => entry.definition);
|
|
382
|
+
const endpoints = grouped.flatMap(definition => [
|
|
383
|
+
definition.channels[measure],
|
|
384
|
+
definition.channels[secondary]
|
|
385
|
+
]).filter(Number.isFinite);
|
|
386
|
+
if (endpoints.length === 0) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Bar mark "${layer.id}" stack ${index} has no resolved semantic endpoints.`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const members = grouped.flatMap(definition => definition.members);
|
|
392
|
+
return {
|
|
393
|
+
key: itemKey(layer, "stack", index),
|
|
394
|
+
fields: uniqueFields(members),
|
|
395
|
+
channels: {
|
|
396
|
+
...sharedChannels(grouped),
|
|
397
|
+
[measure]: Math.min(...endpoints),
|
|
398
|
+
[secondary]: Math.max(...endpoints)
|
|
399
|
+
},
|
|
400
|
+
members,
|
|
401
|
+
graphicIndices: entries.map(entry => entry.index)
|
|
402
|
+
};
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function resolveBarItems(program, layer, dataset, selectionGrain) {
|
|
407
|
+
const grain = resolveBarGrain(layer);
|
|
408
|
+
const definitions = grain === BAR_GRAINS.histogram
|
|
409
|
+
? histogramDefinitions(program, layer, dataset)
|
|
410
|
+
: grain === BAR_GRAINS.aggregate
|
|
411
|
+
? aggregateCellDefinitions(program, layer, dataset)
|
|
412
|
+
: grain === BAR_GRAINS.ranged
|
|
413
|
+
? rangedDefinitions(layer, dataset)
|
|
414
|
+
: undefined;
|
|
415
|
+
if (definitions === undefined) {
|
|
416
|
+
throw new Error(`Bar mark "${layer.id}" is incomplete for selection.`);
|
|
417
|
+
}
|
|
418
|
+
if (selectionGrain === "stack") {
|
|
419
|
+
return finalizeItems(
|
|
420
|
+
program,
|
|
421
|
+
layer,
|
|
422
|
+
"stack",
|
|
423
|
+
barStackDefinitions(layer, grain, definitions),
|
|
424
|
+
"rect"
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return finalizeItems(program, layer, grain, definitions, "rect");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function rowsForSeries(rows, key) {
|
|
431
|
+
const entries = Object.entries(key);
|
|
432
|
+
return entries.length === 0
|
|
433
|
+
? rows
|
|
434
|
+
: rows.filter(row => entries.every(([field, value]) => row[field] === value));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function seriesDefinitions(layer, rows, series) {
|
|
438
|
+
return series.map(item => {
|
|
439
|
+
const members = rowsForSeries(rows, item.key);
|
|
440
|
+
const fields = { ...uniqueFields(members), ...item.key };
|
|
441
|
+
const channels = Object.fromEntries(
|
|
442
|
+
Object.entries(layer.encoding ?? {}).flatMap(([channel, encoding]) => {
|
|
443
|
+
if (encoding.field === undefined) {
|
|
444
|
+
return Object.hasOwn(encoding, "datum")
|
|
445
|
+
? [[channel, encoding.datum]]
|
|
446
|
+
: [];
|
|
447
|
+
}
|
|
448
|
+
return Object.hasOwn(fields, encoding.field)
|
|
449
|
+
? [[channel, fields[encoding.field]]]
|
|
450
|
+
: [];
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
return { fields, channels, members };
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function resolveLineItems(program, layer, dataset) {
|
|
458
|
+
const derived = deriveLineSeries(dataset.values, layer);
|
|
459
|
+
return finalizeItems(
|
|
460
|
+
program,
|
|
461
|
+
layer,
|
|
462
|
+
"series",
|
|
463
|
+
seriesDefinitions(layer, dataset.values, derived.series),
|
|
464
|
+
"path"
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function resolveAreaItems(program, layer, dataset) {
|
|
469
|
+
const transform = findUpstreamTransform(program, dataset, "density");
|
|
470
|
+
const derived = transform === undefined
|
|
471
|
+
? deriveAreaSeries(dataset.values, layer)
|
|
472
|
+
: deriveDensityAreaSeries(dataset.values, layer, transform);
|
|
473
|
+
return finalizeItems(
|
|
474
|
+
program,
|
|
475
|
+
layer,
|
|
476
|
+
"series",
|
|
477
|
+
seriesDefinitions(layer, dataset.values, derived.series),
|
|
478
|
+
"path"
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function resolveRuleItems(program, layer, dataset) {
|
|
483
|
+
const derived = deriveRuleValues(dataset.values, layer);
|
|
484
|
+
const hasField = Object.values(layer.encoding ?? {}).some(encoding =>
|
|
485
|
+
Object.hasOwn(encoding, "field")
|
|
486
|
+
);
|
|
487
|
+
const definitions = Array.from({ length: derived.length }, (_, index) => {
|
|
488
|
+
const members = hasField ? [dataset.values[index]] : dataset.values;
|
|
489
|
+
return {
|
|
490
|
+
fields: hasField
|
|
491
|
+
? ownFields(dataset.values[index])
|
|
492
|
+
: uniqueFields(dataset.values),
|
|
493
|
+
channels: Object.fromEntries(
|
|
494
|
+
Object.entries(derived.values).map(([channel, values]) => [
|
|
495
|
+
channel,
|
|
496
|
+
values[index]
|
|
497
|
+
])
|
|
498
|
+
),
|
|
499
|
+
members
|
|
500
|
+
};
|
|
501
|
+
});
|
|
502
|
+
return finalizeItems(program, layer, "rule", definitions, "line");
|
|
503
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function translateCommand(command, offset) {
|
|
2
|
+
const translated = { ...command };
|
|
3
|
+
for (const key of ["x", "x1", "x2"]) {
|
|
4
|
+
if (Number.isFinite(translated[key])) translated[key] += offset.x;
|
|
5
|
+
}
|
|
6
|
+
for (const key of ["y", "y1", "y2"]) {
|
|
7
|
+
if (Number.isFinite(translated[key])) translated[key] += offset.y;
|
|
8
|
+
}
|
|
9
|
+
return translated;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function transformPathHighlightProperties(properties, style) {
|
|
13
|
+
const { offset, ...appearance } = style;
|
|
14
|
+
return {
|
|
15
|
+
...properties,
|
|
16
|
+
...appearance,
|
|
17
|
+
...(offset.x === 0 && offset.y === 0
|
|
18
|
+
? {}
|
|
19
|
+
: {
|
|
20
|
+
commands: properties.commands.map(command =>
|
|
21
|
+
translateCommand(command, offset)
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function transformRuleHighlightProperties(properties, style) {
|
|
28
|
+
const { offset, ...appearance } = style;
|
|
29
|
+
return {
|
|
30
|
+
...properties,
|
|
31
|
+
...appearance,
|
|
32
|
+
x1: properties.x1 + offset.x,
|
|
33
|
+
x2: properties.x2 + offset.x,
|
|
34
|
+
y1: properties.y1 + offset.y,
|
|
35
|
+
y2: properties.y2 + offset.y
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createPointShapeGraphic } from "../../grammar/pointShapes.js";
|
|
2
|
+
|
|
3
|
+
function pointCenterAndArea(child) {
|
|
4
|
+
const properties = child.properties;
|
|
5
|
+
if (child.type === "circle") {
|
|
6
|
+
return {
|
|
7
|
+
x: properties.x,
|
|
8
|
+
y: properties.y,
|
|
9
|
+
area: Math.PI * properties.radius ** 2
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (child.type === "rect") {
|
|
13
|
+
return {
|
|
14
|
+
x: properties.x + properties.width / 2,
|
|
15
|
+
y: properties.y + properties.height / 2,
|
|
16
|
+
area: properties.width * properties.height
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const points = properties.commands.filter(command =>
|
|
20
|
+
command.op === "M" || command.op === "L"
|
|
21
|
+
);
|
|
22
|
+
const xs = points.map(point => point.x);
|
|
23
|
+
const ys = points.map(point => point.y);
|
|
24
|
+
let twiceArea = 0;
|
|
25
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
26
|
+
const current = points[index];
|
|
27
|
+
const next = points[(index + 1) % points.length];
|
|
28
|
+
twiceArea += current.x * next.y - next.x * current.y;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
x: (Math.min(...xs) + Math.max(...xs)) / 2,
|
|
32
|
+
y: (Math.min(...ys) + Math.max(...ys)) / 2,
|
|
33
|
+
area: Math.abs(twiceArea) / 2
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function transformCommand(command, geometry, center, scale) {
|
|
38
|
+
if (command.op === "Z") return command;
|
|
39
|
+
const transform = (x, y) => ({
|
|
40
|
+
x: center.x + (x - geometry.x) * scale,
|
|
41
|
+
y: center.y + (y - geometry.y) * scale
|
|
42
|
+
});
|
|
43
|
+
if (command.op !== "C") {
|
|
44
|
+
return { op: command.op, ...transform(command.x, command.y) };
|
|
45
|
+
}
|
|
46
|
+
const control1 = transform(command.x1, command.y1);
|
|
47
|
+
const control2 = transform(command.x2, command.y2);
|
|
48
|
+
return {
|
|
49
|
+
op: "C",
|
|
50
|
+
x1: control1.x,
|
|
51
|
+
y1: control1.y,
|
|
52
|
+
x2: control2.x,
|
|
53
|
+
y2: control2.y,
|
|
54
|
+
...transform(command.x, command.y)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function transformPointHighlightChild(child, style) {
|
|
59
|
+
const geometry = pointCenterAndArea(child);
|
|
60
|
+
const center = {
|
|
61
|
+
x: geometry.x + style.offset.x,
|
|
62
|
+
y: geometry.y + style.offset.y
|
|
63
|
+
};
|
|
64
|
+
const area = geometry.area * style.size;
|
|
65
|
+
const shared = {
|
|
66
|
+
fill: style.fill ?? child.properties.fill,
|
|
67
|
+
...(style.opacity === undefined
|
|
68
|
+
? (child.properties.opacity === undefined ? {} : { opacity: child.properties.opacity })
|
|
69
|
+
: { opacity: style.opacity }),
|
|
70
|
+
...(style.stroke === undefined ? {} : { stroke: style.stroke }),
|
|
71
|
+
...(style.strokeWidth === undefined ? {} : { strokeWidth: style.strokeWidth })
|
|
72
|
+
};
|
|
73
|
+
if (style.shape !== undefined) {
|
|
74
|
+
return createPointShapeGraphic({
|
|
75
|
+
shape: style.shape,
|
|
76
|
+
...center,
|
|
77
|
+
area,
|
|
78
|
+
...shared
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const scale = Math.sqrt(style.size);
|
|
82
|
+
if (child.type === "circle") {
|
|
83
|
+
return {
|
|
84
|
+
type: "circle",
|
|
85
|
+
properties: {
|
|
86
|
+
...child.properties,
|
|
87
|
+
...center,
|
|
88
|
+
radius: child.properties.radius * scale,
|
|
89
|
+
...shared
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (child.type === "rect") {
|
|
94
|
+
const width = child.properties.width * scale;
|
|
95
|
+
const height = child.properties.height * scale;
|
|
96
|
+
return {
|
|
97
|
+
type: "rect",
|
|
98
|
+
properties: {
|
|
99
|
+
...child.properties,
|
|
100
|
+
x: center.x - width / 2,
|
|
101
|
+
y: center.y - height / 2,
|
|
102
|
+
width,
|
|
103
|
+
height,
|
|
104
|
+
...shared
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
type: "path",
|
|
110
|
+
properties: {
|
|
111
|
+
...child.properties,
|
|
112
|
+
commands: child.properties.commands.map(command =>
|
|
113
|
+
transformCommand(command, geometry, center, scale)
|
|
114
|
+
),
|
|
115
|
+
...shared
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolveAreaItems } from "../items.js";
|
|
2
|
+
import { normalizeAreaHighlightStyle } from "../styles.js";
|
|
3
|
+
|
|
4
|
+
export const areaSelectionPolicy = Object.freeze({
|
|
5
|
+
supportedGrains: Object.freeze(["item"]),
|
|
6
|
+
resolveItems: resolveAreaItems,
|
|
7
|
+
normalizeHighlightStyle: normalizeAreaHighlightStyle,
|
|
8
|
+
applyHighlightOp: "applyPathHighlight",
|
|
9
|
+
rematerializeOp: "rematerializeAreaMark"
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolveBarItems } from "../items.js";
|
|
2
|
+
import { normalizeBarHighlightStyle } from "../styles.js";
|
|
3
|
+
|
|
4
|
+
export const barSelectionPolicy = Object.freeze({
|
|
5
|
+
supportedGrains: Object.freeze(["item", "stack"]),
|
|
6
|
+
resolveItems: resolveBarItems,
|
|
7
|
+
normalizeHighlightStyle: normalizeBarHighlightStyle,
|
|
8
|
+
applyHighlightOp: "applyBarHighlight",
|
|
9
|
+
rematerializeOp: "rematerializeBarMark"
|
|
10
|
+
});
|