Skip to content

facetReindex #1057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/facet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {slice} from "./options.js";

export function facetReindex(facets, n) {
// Count the number of overlapping indexes across facets.
const overlap = new Uint8Array(n);
let count = 0;
for (const facet of facets) {
for (const i of facet) {
if (overlap[i]) ++count;
overlap[i] = 1;
}
}

// For each overlapping index (duplicate number), assign a new unique index at
// the end of the existing array. For example, [[0, 1, 2], [2, 1, 3]] would
// become [[0, 1, 2], [4, 5, 3]]. Attach a plan to the facets array, to be
// able to read the values associated with the old index in unaffected
// channels.
if (count > 0) {
facets = facets.map((facet) => slice(facet, Uint32Array));
const plan = (facets.plan = new Uint32Array(n + count));
let j = 0;
for (; j < n; ++j) plan[j] = j;
overlap.fill(0);
for (const facet of facets) {
for (let k = 0; k < facet.length; ++k) {
const i = facet[k];
if (overlap[i]) {
plan[j] = i;
facet[k] = j;
j++;
}
overlap[i] = 1;
}
}
}
return facets;
}

// returns a function that reads X with the facets’ reindexing plan
export function getter({plan}, X) {
return !X ? X : !plan || X.length === plan.length ? (i) => X[i] : (i) => X[plan[i]];
}

// returns an array of X expanded along the facets’ reindexing plan
export function expander({plan}, X) {
if (!plan || !X || X.length === plan.length) return X;
const V = new X.constructor(plan.length);
for (let i = 0; i < plan.length; ++i) V[i] = X[plan[i]];
return V;
}

export function originals({plan}, I) {
return plan ? I.map((i) => plan[i]) : I;
}
20 changes: 18 additions & 2 deletions src/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -609,8 +609,24 @@ export function plot(options = {}) {
.attr("transform", facetTranslate(fx, fy))
.each(function (key) {
const j = indexByFacet.get(key);
for (const [mark, {channels, values, facets}] of stateByMark) {
const facet = facets ? mark.filter(facets[j] ?? facets[0], channels, values) : null;
for (const [mark, {channels, values: original, facets}] of stateByMark) {
// this facet is possibly reindexed
let values = original;
let F = facets?.[j];
if (F && j > 0 && facets.plan) {
const {plan} = facets;
const long = Object.keys(values).filter((key) => values[key].length === plan.length);
const V = Object.fromEntries(long.map((key) => [key, []]));
F = [];
for (const i of facets[j]) {
const k = plan[i];
F.push(k);
for (const key of long) V[key][k] = original[key][i];
values = {...original, ...V};
}
}

const facet = facets ? mark.filter(F ?? facets[0], channels, values) : null;
const node = mark.render(facet, scales, values, subdimensions, context);
if (node != null) this.appendChild(node);
}
Expand Down
48 changes: 35 additions & 13 deletions src/transforms/bin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3";
import {
bin as binner,
extent,
sum,
thresholdFreedmanDiaconis,
thresholdScott,
thresholdSturges,
utcTickInterval
} from "d3";
import {
valueof,
range,
identity,
maybeColumn,
maybeTuple,
Expand Down Expand Up @@ -29,6 +36,7 @@ import {
} from "./group.js";
import {maybeInsetX, maybeInsetY} from "./inset.js";
import {maybeInterval} from "./interval.js";
import {expander, getter, originals} from "../facet.js";

/**
* ```js
Expand Down Expand Up @@ -172,12 +180,19 @@ function binn(
const GZ = Z && setGZ([]);
const GF = F && setGF([]);
const GS = S && setGS([]);
const BX = bx ? bx(data) : [[, , (I) => I]];
const BY = by ? by(data) : [[, , (I) => I]];
const BX = bx ? bx(data, facets) : [[, , (I) => I]];
const BY = by ? by(data, facets) : [[, , (I) => I]];
const BX1 = bx && setBX1([]);
const BX2 = bx && setBX2([]);
const BY1 = by && setBY1([]);
const BY2 = by && setBY2([]);

const eG = getter(facets, G);
const eK = getter(facets, K);
const gZ = getter(facets, Z);
const gF = getter(facets, F);
const gS = getter(facets, S);

let i = 0;
for (const o of outputs) o.initialize(data);
if (sort) sort.initialize(data);
Expand All @@ -187,20 +202,20 @@ function binn(
for (const o of outputs) o.scope("facet", facet);
if (sort) sort.scope("facet", facet);
if (filter) filter.scope("facet", facet);
for (const [f, I] of maybeGroup(facet, G)) {
for (const [k, g] of maybeGroup(I, K)) {
for (const [f, I] of maybeGroup(facet, eG)) {
for (const [k, g] of maybeGroup(I, eK)) {
for (const [x1, x2, fx] of BX) {
const bb = fx(g);
for (const [y1, y2, fy] of BY) {
const extent = {x1, x2, y1, y2};
const b = fy(bb);
if (filter && !filter.reduce(b, extent)) continue;
groupFacet.push(i++);
groupData.push(reduceData.reduce(b, data, extent));
groupData.push(reduceData.reduce(originals(facets, b), data, extent));
if (K) GK.push(k);
if (Z) GZ.push(G === Z ? f : Z[b[0]]);
if (F) GF.push(G === F ? f : F[b[0]]);
if (S) GS.push(G === S ? f : S[b[0]]);
if (Z) GZ.push(G === Z ? f : gZ(b[0]));
if (F) GF.push(G === F ? f : gF(b[0]));
if (S) GS.push(G === S ? f : gS(b[0]));
if (BX1) BX1.push(x1), BX2.push(x2);
if (BY1) BY1.push(y1), BY2.push(y2);
for (const o of outputs) o.reduce(b, extent);
Expand Down Expand Up @@ -248,8 +263,8 @@ function maybeBinValueTuple(options) {
function maybeBin(options) {
if (options == null) return;
const {value, cumulative, domain = extent, thresholds} = options;
const bin = (data) => {
let V = valueof(data, value, Array); // d3.bin prefers Array input
const bin = (data, facets) => {
let V = expander(facets, valueof(data, value, Array)); // d3.bin prefers Array input
const bin = binner().value((i) => V[i]);
if (isTemporal(V) || isTimeThresholds(thresholds)) {
V = V.map(coerceDate);
Expand Down Expand Up @@ -279,7 +294,7 @@ function maybeBin(options) {
}
bin.thresholds(t).domain(d);
}
let bins = bin(range(data)).map(binset);
let bins = bin(union(facets)).map(binset);
if (cumulative) bins = (cumulative < 0 ? bins.reverse() : bins).map(bincumset);
return bins.map(binfilter);
};
Expand Down Expand Up @@ -365,3 +380,10 @@ function binfilter([{x0, x1}, set]) {
function binempty() {
return new Uint32Array(0);
}

function union(facets) {
const U = new Uint32Array(sum(facets, (d) => d.length));
let c = 0;
for (const facet of facets) for (const i of facet) U[c++] = i;
return U;
}
20 changes: 13 additions & 7 deletions src/transforms/dodge.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {finite, positive} from "../defined.js";
import {identity, maybeNamed, number, valueof} from "../options.js";
import {coerceNumbers} from "../scales.js";
import {initializer} from "./basic.js";
import {facetReindex, getter} from "../facet.js";

const anchorXLeft = ({marginLeft}) => [1, marginLeft];
const anchorXRight = ({width, marginRight}) => [-1, width - marginRight];
Expand Down Expand Up @@ -90,21 +91,26 @@ function dodge(y, x, anchor, padding, options) {
return initializer(options, function (data, facets, {[x]: X, r: R}, scales, dimensions) {
if (!X) throw new Error(`missing channel: ${x}`);
X = coerceNumbers(valueof(X.value, scales[X.scale] || identity));

// make facets exclusive
facets = facetReindex(facets, data.length);

const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? number(options.r) : 3;
if (R) R = coerceNumbers(valueof(R.value, scales[R.scale] || identity));
let [ky, ty] = anchor(dimensions);
const compare = ky ? compareAscending : compareSymmetric;
const Y = new Float64Array(X.length);
const radius = R ? (i) => R[i] : () => r;
const Y = new Float64Array((facets.plan || X).length);
const radius = R ? getter(facets, R) : () => r;
const getX = getter(facets, X);
for (let I of facets) {
const tree = IntervalTree();
I = I.filter(R ? (i) => finite(X[i]) && positive(R[i]) : (i) => finite(X[i]));
I = I.filter(R ? (i) => finite(getX(i)) && positive(radius(i)) : (i) => finite(getX(i)));
const intervals = new Float64Array(2 * I.length + 2);
for (const i of I) {
const ri = radius(i);
const y0 = ky ? ri + padding : 0; // offset baseline for varying radius
const l = X[i] - ri;
const h = X[i] + ri;
const l = getX(i) - ri;
const h = getX(i) + ri;

// The first two positions are 0 to test placing the dot on the baseline.
let k = 2;
Expand All @@ -114,8 +120,8 @@ function dodge(y, x, anchor, padding, options) {
// https://observablehq.com/@mbostock/circle-offset-along-line
tree.queryInterval(l - padding, h + padding, ([, , j]) => {
const yj = Y[j] - y0;
const dx = X[i] - X[j];
const dr = padding + (R ? R[i] + R[j] : 2 * r);
const dx = getX(i) - getX(j);
const dr = padding + (R ? radius(i) + radius(j) : 2 * r);
const dy = Math.sqrt(dr * dr - dx * dx);
intervals[k++] = yj - dy;
intervals[k++] = yj + dy;
Expand Down
29 changes: 19 additions & 10 deletions src/transforms/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
percentile
} from "../options.js";
import {basic} from "./basic.js";
import {getter, originals} from "../facet.js";

/**
* ```js
Expand Down Expand Up @@ -158,6 +159,14 @@ function groupn(
const GZ = Z && setGZ([]);
const GF = F && setGF([]);
const GS = S && setGS([]);

const eG = getter(facets, G);
const eX = getter(facets, X);
const eY = getter(facets, Y);
const gZ = getter(facets, Z);
const gF = getter(facets, F);
const gS = getter(facets, S);

let i = 0;
for (const o of outputs) o.initialize(data);
if (sort) sort.initialize(data);
Expand All @@ -167,17 +176,17 @@ function groupn(
for (const o of outputs) o.scope("facet", facet);
if (sort) sort.scope("facet", facet);
if (filter) filter.scope("facet", facet);
for (const [f, I] of maybeGroup(facet, G)) {
for (const [y, gg] of maybeGroup(I, Y)) {
for (const [x, g] of maybeGroup(gg, X)) {
for (const [f, I] of maybeGroup(facet, eG)) {
for (const [y, gg] of maybeGroup(I, eY)) {
for (const [x, g] of maybeGroup(gg, eX)) {
if (filter && !filter.reduce(g)) continue;
groupFacet.push(i++);
groupData.push(reduceData.reduce(g, data));
groupData.push(reduceData.reduce(originals(facets, g), data));
if (X) GX.push(x);
if (Y) GY.push(y);
if (Z) GZ.push(G === Z ? f : Z[g[0]]);
if (F) GF.push(G === F ? f : F[g[0]]);
if (S) GS.push(G === S ? f : S[g[0]]);
if (Z) GZ.push(G === Z ? f : gZ(g[0]));
if (F) GF.push(G === F ? f : gF(g[0]));
if (S) GS.push(G === S ? f : gS(g[0]));
for (const o of outputs) o.reduce(g);
if (sort) sort.reduce(g);
}
Expand Down Expand Up @@ -256,10 +265,10 @@ export function maybeEvaluator(name, reduce, inputs) {
};
}

export function maybeGroup(I, X) {
return X
export function maybeGroup(I, x) {
return x
? sort(
grouper(I, (i) => X[i]),
grouper(I, (i) => x(i)),
first
)
: [[, I]];
Expand Down
28 changes: 19 additions & 9 deletions src/transforms/hexbin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {sqrt3} from "../symbols.js";
import {identity, isNoneish, number, valueof} from "../options.js";
import {initializer} from "./basic.js";
import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js";
import {getter} from "../facet.js";

// We don’t want the hexagons to align with the edges of the plot frame, as that
// would cause extreme x-values (the upper bound of the default x-scale domain)
Expand Down Expand Up @@ -100,19 +101,28 @@ export function hexbin(outputs = {fill: "count"}, options = {}) {
const BX = [];
const BY = [];
let i = -1;

// Mind reindexed facets
const eG = getter(facets, G);
const gX = getter(facets, X);
const gY = getter(facets, Y);
const gZ = getter(facets, Z);
const gF = getter(facets, F);
const gS = getter(facets, S);
const gQ = getter(facets, Q);
for (const o of outputs) o.initialize(data);
for (const facet of facets) {
const binFacet = [];
for (const o of outputs) o.scope("facet", facet);
for (const [f, I] of maybeGroup(facet, G)) {
for (const bin of hbin(I, X, Y, binWidth)) {
for (const [f, I] of maybeGroup(facet, eG)) {
for (const bin of hbin(I, gX, gY, binWidth)) {
binFacet.push(++i);
BX.push(bin.x);
BY.push(bin.y);
if (Z) GZ.push(G === Z ? f : Z[bin[0]]);
if (F) GF.push(G === F ? f : F[bin[0]]);
if (S) GS.push(G === S ? f : S[bin[0]]);
if (Q) GQ.push(G === Q ? f : Q[bin[0]]);
if (Z) GZ.push(G === Z ? f : gZ(bin[0]));
if (F) GF.push(G === F ? f : gF(bin[0]));
if (S) GS.push(G === S ? f : gS(bin[0]));
if (Q) GQ.push(G === Q ? f : gQ(bin[0]));
for (const o of outputs) o.reduce(bin);
}
}
Expand All @@ -139,12 +149,12 @@ export function hexbin(outputs = {fill: "count"}, options = {}) {
});
}

function hbin(I, X, Y, dx) {
function hbin(I, x, y, dx) {
const dy = dx * (1.5 / sqrt3);
const bins = new Map();
for (const i of I) {
let px = X[i],
py = Y[i];
let px = x(i),
py = y(i);
if (isNaN(px) || isNaN(py)) continue;
let pj = Math.round((py = (py - oy) / dy)),
pi = Math.round((px = (px - ox) / dx - (pj & 1) / 2)),
Expand Down
9 changes: 6 additions & 3 deletions src/transforms/map.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {count, group, rank} from "d3";
import {maybeZ, take, valueof, maybeInput, column} from "../options.js";
import {basic} from "./basic.js";
import {facetReindex, getter, expander} from "../facet.js";

/**
* ```js
Expand Down Expand Up @@ -58,11 +59,13 @@ export function map(outputs = {}, options = {}) {
});
return {
...basic(options, (data, facets) => {
// make facets exclusive
facets = facetReindex(facets, data.length);
const Z = valueof(data, z);
const X = channels.map(({input}) => valueof(data, input));
const MX = channels.map(({setOutput}) => setOutput(new Array(data.length)));
const X = channels.map(({input}) => expander(facets, valueof(data, input)));
const MX = channels.map(({setOutput}) => setOutput(new Array((facets.plan || data).length)));
for (const facet of facets) {
for (const I of Z ? group(facet, (i) => Z[i]).values() : [facet]) {
for (const I of Z ? group(facet, getter(facets, Z)).values() : [facet]) {
channels.forEach(({map}, i) => map.map(I, X[i], MX[i]));
}
}
Expand Down
Loading