Skip to content

Commit 301de0d

Browse files
committed
facet expansion
1 parent 171e97b commit 301de0d

25 files changed

+15881
-89
lines changed

src/facet.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import {slice} from "./options.js";
1+
import {column, maybeColorChannel, maybeNumberChannel, slice, valueof} from "./options.js";
2+
import {maybeSymbolChannel} from "./symbols.js";
3+
import {maybeFontSizeChannel} from "./marks/text.js";
4+
import {maybePathChannel} from "./marks/image.js";
25

36
export function facetReindex(facets, n) {
47
const overlap = new Uint8Array(n);
@@ -8,6 +11,7 @@ export function facetReindex(facets, n) {
811
// Count the number of overlapping indexes across facets.
912
for (const facet of facets) {
1013
for (const i of facet) {
14+
if (i >= n) return {facets}; // already dedup'ed!
1115
if (overlap[i]) ++count;
1216
overlap[i] = 1;
1317
}
@@ -46,3 +50,47 @@ export function maybeExpand(X, plan) {
4650
for (let i = 0; i < plan.length; ++i) V[i] = X[plan[i]];
4751
return V;
4852
}
53+
54+
// Iterate over the options and pull out any that represent columns of values.
55+
const knownChannels = [
56+
["x"],
57+
["x1"],
58+
["x2"],
59+
["y"],
60+
["y1"],
61+
["y2"],
62+
["z"],
63+
["ariaLabel"],
64+
["href"],
65+
["title"],
66+
["fill", (value) => maybeColorChannel(value)[0]],
67+
["stroke", (value) => maybeColorChannel(value)[0]],
68+
["fillOpacity", (value) => maybeNumberChannel(value)[0]],
69+
["strokeOpacity", (value) => maybeNumberChannel(value)[0]],
70+
["opacity", (value) => maybeNumberChannel(value)[0]],
71+
["strokeWidth", (value) => maybeNumberChannel(value)[0]],
72+
["symbol", (value) => maybeSymbolChannel(value)[0]], // dot
73+
["r", (value) => maybeNumberChannel(value)[0]], // dot
74+
["rotate", (value) => maybeNumberChannel(value)[0]], // dot, text
75+
["fontSize", (value) => maybeFontSizeChannel(value)[0]], // text
76+
["text"], // text
77+
["length", (value) => maybeNumberChannel(value)[0]], // vector
78+
["width", (value) => maybeNumberChannel(value)[0]], // image
79+
["height", (value) => maybeNumberChannel(value)[0]], // image
80+
["src", (value) => maybePathChannel(value)[0]], // image
81+
["weight", (value) => maybeNumberChannel(value)[0]] // density
82+
];
83+
84+
export function maybeExpandOutputs(options) {
85+
const other = {};
86+
const outputs = [];
87+
for (const [name, test = (value) => value] of knownChannels) {
88+
const value = test(options[name]);
89+
if (value != null) {
90+
const [V, setV] = column(value);
91+
other[name] = V;
92+
outputs.push((data, plan) => setV(maybeExpand(valueof(data, value), plan)));
93+
}
94+
}
95+
return [other, outputs];
96+
}

src/marks/image.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function isUrl(string) {
3434

3535
// Disambiguates a constant src definition from a channel. A path or URL string
3636
// is assumed to be a constant; any other string is assumed to be a field name.
37-
function maybePathChannel(value) {
37+
export function maybePathChannel(value) {
3838
return typeof value === "string" && (isPath(value) || isUrl(value)) ? [undefined, value] : [value, undefined];
3939
}
4040

src/marks/text.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ const fontSizes = new Set([
241241
// - string <length>: e.g., "12px"
242242
// - string <percentage>: e.g., "80%"
243243
// Anything else is assumed to be a channel definition.
244-
function maybeFontSizeChannel(fontSize) {
244+
export function maybeFontSizeChannel(fontSize) {
245245
if (fontSize == null || typeof fontSize === "number") return [undefined, fontSize];
246246
if (typeof fontSize !== "string") return [fontSize, undefined];
247247
fontSize = fontSize.trim().toLowerCase();

src/transforms/bin.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3";
22
import {
33
valueof,
4-
range,
54
identity,
65
maybeColumn,
76
maybeTuple,
@@ -161,6 +160,7 @@ function binn(
161160
...("fill" in inputs && {fill: GF || fill}),
162161
...("stroke" in inputs && {stroke: GS || stroke}),
163162
...basic(options, (data, facets) => {
163+
const cover = (bx || by) && union(facets);
164164
const K = valueof(data, k);
165165
const Z = valueof(data, z);
166166
const F = valueof(data, vfill);
@@ -172,8 +172,8 @@ function binn(
172172
const GZ = Z && setGZ([]);
173173
const GF = F && setGF([]);
174174
const GS = S && setGS([]);
175-
const BX = bx ? bx(data) : [[, , (I) => I]];
176-
const BY = by ? by(data) : [[, , (I) => I]];
175+
const BX = bx ? bx(data, cover) : [[, , (I) => I]];
176+
const BY = by ? by(data, cover) : [[, , (I) => I]];
177177
const BX1 = bx && setBX1([]);
178178
const BX2 = bx && setBX2([]);
179179
const BY1 = by && setBY1([]);
@@ -248,7 +248,7 @@ function maybeBinValueTuple(options) {
248248
function maybeBin(options) {
249249
if (options == null) return;
250250
const {value, cumulative, domain = extent, thresholds} = options;
251-
const bin = (data) => {
251+
const bin = (data, cover) => {
252252
let V = valueof(data, value, Array); // d3.bin prefers Array input
253253
const bin = binner().value((i) => V[i]);
254254
if (isTemporal(V) || isTimeThresholds(thresholds)) {
@@ -279,7 +279,7 @@ function maybeBin(options) {
279279
}
280280
bin.thresholds(t).domain(d);
281281
}
282-
let bins = bin(range(data)).map(binset);
282+
let bins = bin(cover).map(binset);
283283
if (cumulative) bins = (cumulative < 0 ? bins.reverse() : bins).map(bincumset);
284284
return bins.map(binfilter);
285285
};
@@ -365,3 +365,9 @@ function binfilter([{x0, x1}, set]) {
365365
function binempty() {
366366
return new Uint32Array(0);
367367
}
368+
369+
function union(facets) {
370+
const U = new Set();
371+
for (const f of facets) for (const i of f) U.add(i);
372+
return U;
373+
}

src/transforms/dodge.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {finite, positive} from "../defined.js";
33
import {identity, maybeNamed, number, valueof} from "../options.js";
44
import {coerceNumbers} from "../scales.js";
55
import {initializer} from "./basic.js";
6+
import {maybeExpand, facetReindex} from "../facet.js";
67

78
const anchorXLeft = ({marginLeft}) => [1, marginLeft];
89
const anchorXRight = ({width, marginRight}) => [-1, width - marginRight];
@@ -87,11 +88,15 @@ function dodge(y, x, anchor, padding, options) {
8788
options = {...options, channels: {r: {value: r, scale: "r"}, ...maybeNamed(channels)}};
8889
if (sort === undefined && reverse === undefined) options.sort = {channel: "r", order: "descending"};
8990
}
90-
return initializer(options, function (data, facets, {[x]: X, r: R}, scales, dimensions) {
91+
return initializer(options, function (data, facets, {[x]: X, r: R, ...channels}, scales, dimensions) {
9192
if (!X) throw new Error(`missing channel: ${x}`);
92-
X = coerceNumbers(valueof(X.value, scales[X.scale] || identity));
93+
94+
let plan;
95+
({facets, plan} = facetReindex(facets, data.length)); // make facets exclusive
96+
X = maybeExpand(coerceNumbers(valueof(X.value, scales[X.scale] || identity)), plan);
97+
9398
const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? number(options.r) : 3;
94-
if (R) R = coerceNumbers(valueof(R.value, scales[R.scale] || identity));
99+
if (R) R = maybeExpand(coerceNumbers(valueof(R.value, scales[R.scale] || identity)));
95100
let [ky, ty] = anchor(dimensions);
96101
const compare = ky ? compareAscending : compareSymmetric;
97102
const Y = new Float64Array(X.length);
@@ -144,13 +149,17 @@ function dodge(y, x, anchor, padding, options) {
144149
Y[i] = Y[i] * ky + ty;
145150
}
146151
}
152+
for (const key in channels) {
153+
channels[key].value = maybeExpand(channels[key].value, plan);
154+
}
147155
return {
148156
data,
149157
facets,
150158
channels: {
151159
[x]: {value: X},
152160
[y]: {value: Y},
153-
...(R && {r: {value: R}})
161+
...(R && {r: {value: R}}),
162+
...channels
154163
}
155164
};
156165
});

src/transforms/map.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {count, group, rank} from "d3";
22
import {maybeZ, take, valueof, maybeInput, column} from "../options.js";
33
import {basic} from "./basic.js";
4+
import {maybeExpand, facetReindex, maybeExpandOutputs} from "../facet.js";
45

56
/**
67
* ```js
@@ -56,18 +57,23 @@ export function map(outputs = {}, options = {}) {
5657
const [output, setOutput] = column(input);
5758
return {key, input, output, setOutput, map: maybeMap(map)};
5859
});
60+
const [other, facetOutputs] = maybeExpandOutputs(options); // TODO wrap outputs in facetReindex
5961
return {
6062
...basic(options, (data, facets) => {
61-
const Z = valueof(data, z);
62-
const X = channels.map(({input}) => valueof(data, input));
63-
const MX = channels.map(({setOutput}) => setOutput(new Array(data.length)));
63+
let plan;
64+
({facets, plan} = facetReindex(facets, data.length)); // make facets exclusive
65+
const Z = maybeExpand(valueof(data, z), plan);
66+
const X = channels.map(({input}) => maybeExpand(valueof(data, input), plan));
67+
const MX = channels.map(({setOutput}) => setOutput(new Array(plan ? plan.length : data.length)));
68+
for (const o of facetOutputs) o(data, plan); // expand any extra channels
6469
for (const facet of facets) {
6570
for (const I of Z ? group(facet, (i) => Z[i]).values() : [facet]) {
6671
channels.forEach(({map}, i) => map.map(I, X[i], MX[i]));
6772
}
6873
}
6974
return {data, facets};
7075
}),
76+
...other,
7177
...Object.fromEntries(channels.map(({key, output}) => [key, output]))
7278
};
7379
}

src/transforms/stack.js

Lines changed: 10 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {InternMap, cumsum, group, groupSort, greatest, max, min, rollup, sum} from "d3";
22
import {ascendingDefined} from "../defined.js";
3-
import {maybeExpand, facetReindex} from "../facet.js";
3+
import {maybeExpand, facetReindex, maybeExpandOutputs} from "../facet.js";
44
import {field, column, mid, range, valueof, one} from "../options.js";
5-
import {maybeColumn, maybeColorChannel, maybeZ, maybeZero} from "../options.js";
5+
import {maybeColumn, maybeZ, maybeZero} from "../options.js";
66
import {basic} from "./basic.js";
77

88
/**
@@ -19,7 +19,7 @@ export function stackX(stackOptions = {}, options = {}) {
1919
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
2020
const {y1, y = y1, x, ...rest} = options; // note: consumes x!
2121
const [transform, Y, x1, x2, other] = stack(y, x, "x", stackOptions, rest);
22-
return {...transform, y1, y: Y, x1, x2, ...other, x: mid(x1, x2)};
22+
return {...transform, ...other, y1, y: Y, x1, x2, x: mid(x1, x2)};
2323
}
2424

2525
/**
@@ -38,7 +38,7 @@ export function stackX1(stackOptions = {}, options = {}) {
3838
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
3939
const {y1, y = y1, x} = options;
4040
const [transform, Y, X, , other] = stack(y, x, "x", stackOptions, options);
41-
return {...transform, y1, y: Y, x: X, ...other};
41+
return {...transform, ...other, y1, y: Y, x: X};
4242
}
4343

4444
/**
@@ -57,7 +57,7 @@ export function stackX2(stackOptions = {}, options = {}) {
5757
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
5858
const {y1, y = y1, x} = options;
5959
const [transform, Y, , X, other] = stack(y, x, "x", stackOptions, options);
60-
return {...transform, y1, y: Y, x: X, ...other};
60+
return {...transform, ...other, y1, y: Y, x: X};
6161
}
6262

6363
/**
@@ -79,7 +79,7 @@ export function stackY(stackOptions = {}, options = {}) {
7979
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
8080
const {x1, x = x1, y, ...rest} = options; // note: consumes y!
8181
const [transform, X, y1, y2, other] = stack(x, y, "y", stackOptions, rest);
82-
return {...transform, x1, x: X, y1, y2, y: mid(y1, y2), ...other};
82+
return {...transform, ...other, x1, x: X, y1, y2, y: mid(y1, y2)};
8383
}
8484

8585
/**
@@ -98,7 +98,7 @@ export function stackY1(stackOptions = {}, options = {}) {
9898
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
9999
const {x1, x = x1, y} = options;
100100
const [transform, X, Y, , other] = stack(x, y, "y", stackOptions, options);
101-
return {...transform, x1, x: X, y: Y, ...other};
101+
return {...transform, ...other, x1, x: X, y: Y};
102102
}
103103

104104
/**
@@ -117,7 +117,7 @@ export function stackY2(stackOptions = {}, options = {}) {
117117
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
118118
const {x1, x = x1, y} = options;
119119
const [transform, X, , Y, other] = stack(x, y, "y", stackOptions, options);
120-
return {...transform, x1, x: X, y: Y, ...other};
120+
return {...transform, ...other, x1, x: X, y: Y};
121121
}
122122

123123
export function maybeStackX({x, x1, x2, ...options} = {}) {
@@ -147,54 +147,11 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) {
147147
const [Y2, setY2] = column(y);
148148
offset = maybeOffset(offset);
149149
order = maybeOrder(order, offset, ky);
150-
151-
// TODO Here we will need to iterate over the options and pull out any that
152-
// represent channels (columns of values). The list of possible channels for
153-
// all Marks:
154-
// - fill (color)
155-
// - fillOpacity (opacity/number)
156-
// - stroke (color)
157-
// - strokeOpacity (opacity/number)
158-
// - strokeWidth (number)
159-
// - opacity (opacity/number)
160-
// - title
161-
// - href
162-
// - ariaLabel
163-
// - z?
164-
// TODO
165-
// - text (text)
166-
// - rotate (text, dot)
167-
// - fontSize (text)
168-
// - symbol (dot)
169-
// - r (dot)
170-
// - length (vector)
171-
// - width (image)
172-
// - height (image)
173-
// - src (image)
174-
// - weight (density)
175-
176-
const knownChannels = [
177-
["fill", (value) => maybeColorChannel(value)[0]],
178-
["stroke", (value) => maybeColorChannel(value)[0]],
179-
["text"]
180-
];
181-
182-
const other = {};
183-
const outputs = [];
184-
for (const [name, test = (value) => value] of knownChannels) {
185-
const value = test(options[name]);
186-
if (value) {
187-
const [V, setV] = column(value);
188-
other[name] = V;
189-
outputs.push((data, plan) => setV(maybeExpand(valueof(data, value), plan)));
190-
}
191-
}
192-
150+
const [other, outputs] = maybeExpandOutputs(options); // TODO wrap outputs in facetReindex
193151
return [
194152
basic(options, (data, facets) => {
195153
let plan;
196154
({facets, plan} = facetReindex(facets, data.length)); // make facets exclusive
197-
for (const o of outputs) o(data, plan); // expand any extra channels
198155
const XS = x == null ? undefined : valueof(data, x);
199156
const YS = valueof(data, y, Float64Array);
200157
const ZS = valueof(data, z);
@@ -203,6 +160,7 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) {
203160
const Z = maybeExpand(ZS, plan);
204161
const O = order && maybeExpand(order(data, XS, YS, ZS), plan);
205162
const n = plan ? plan.length : data.length;
163+
for (const o of outputs) o(data, plan); // expand any extra channels
206164
const Y1 = setY1(new Float64Array(n));
207165
const Y2 = setY2(new Float64Array(n));
208166
const facetstacks = [];

0 commit comments

Comments
 (0)