Skip to content

Commit 20eeb56

Browse files
authored
stack order comparator (#1642)
* stack order comparator * stack descending shorthand * transpose orderZDomain * more inlining * inline options * better comparator type
1 parent 2911d55 commit 20eeb56

File tree

4 files changed

+241
-70
lines changed

4 files changed

+241
-70
lines changed

src/transforms/stack.d.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {ChannelValue} from "../channel.js";
2-
import type {Transformed} from "./basic.js";
2+
import type {CompareFunction, Transformed} from "./basic.js";
33

44
/**
55
* A built-in stack offset method; one of:
@@ -62,10 +62,17 @@ export type StackOrderName = "value" | "x" | "y" | "z" | "sum" | "appearance" |
6262
*
6363
* - a named stack order method such as *inside-out* or *sum*
6464
* - a field name, for natural order of the corresponding values
65-
* - a function of data, for natural order of the corresponding values
65+
* - an accessor function, for natural order of the corresponding values
66+
* - a comparator function for ordering data
6667
* - an array of explicit **z** values in the desired order
6768
*/
68-
export type StackOrder = StackOrderName | (string & Record<never, never>) | ((d: any, i: number) => any) | any[];
69+
export type StackOrder =
70+
| StackOrderName
71+
| `-${StackOrderName}`
72+
| (string & Record<never, never>)
73+
| ((d: any) => any) // accessor
74+
| CompareFunction
75+
| any[];
6976

7077
/** Options for the stack transform. */
7178
export interface StackOptions {

src/transforms/stack.js

+67-62
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {InternMap, cumsum, greatest, group, groupSort, max, min, rollup, sum} from "d3";
2-
import {ascendingDefined} from "../defined.js";
2+
import {ascendingDefined, descendingDefined} from "../defined.js";
33
import {withTip} from "../mark.js";
44
import {maybeApplyInterval, maybeColumn, maybeZ, maybeZero} from "../options.js";
55
import {column, field, mid, one, range, valueof} from "../options.js";
@@ -81,20 +81,20 @@ function stack(x, y = one, kx, ky, {offset, order, reverse}, options) {
8181
const [Y2, setY2] = column(y);
8282
Y1.hint = Y2.hint = lengthy;
8383
offset = maybeOffset(offset);
84-
order = maybeOrder(order, offset, ky); // TODO shorthand -order with reverse?
84+
order = maybeOrder(order, offset, ky);
8585
return [
8686
basic(options, (data, facets, plotOptions) => {
8787
const X = x == null ? undefined : setX(maybeApplyInterval(valueof(data, x), plotOptions?.[kx]));
8888
const Y = valueof(data, y, Float64Array);
8989
const Z = valueof(data, z);
90-
const O = order && order(data, X, Y, Z);
90+
const compare = order && order(data, X, Y, Z);
9191
const n = data.length;
9292
const Y1 = setY1(new Float64Array(n));
9393
const Y2 = setY2(new Float64Array(n));
9494
const facetstacks = [];
9595
for (const facet of facets) {
9696
const stacks = X ? Array.from(group(facet, (i) => X[i]).values()) : [facet];
97-
if (O) applyOrder(stacks, O);
97+
if (compare) for (const stack of stacks) stack.sort(compare);
9898
for (const stack of stacks) {
9999
let yn = 0;
100100
let yp = 0;
@@ -228,43 +228,44 @@ function offsetCenterFacets(facetstacks, Y1, Y2) {
228228
}
229229

230230
function maybeOrder(order, offset, ky) {
231-
if (order === undefined && offset === offsetWiggle) return orderInsideOut;
231+
if (order === undefined && offset === offsetWiggle) return orderInsideOut(ascendingDefined);
232232
if (order == null) return;
233233
if (typeof order === "string") {
234-
switch (order.toLowerCase()) {
234+
const negate = order.startsWith("-");
235+
const compare = negate ? descendingDefined : ascendingDefined;
236+
switch ((negate ? order.slice(1) : order).toLowerCase()) {
235237
case "value":
236238
case ky:
237-
return orderY;
239+
return orderY(compare);
238240
case "z":
239-
return orderZ;
241+
return orderZ(compare);
240242
case "sum":
241-
return orderSum;
243+
return orderSum(compare);
242244
case "appearance":
243-
return orderAppearance;
245+
return orderAppearance(compare);
244246
case "inside-out":
245-
return orderInsideOut;
247+
return orderInsideOut(compare);
246248
}
247-
return orderFunction(field(order));
249+
return orderAccessor(field(order));
248250
}
249-
if (typeof order === "function") return orderFunction(order);
251+
if (typeof order === "function") return (order.length === 1 ? orderAccessor : orderComparator)(order);
250252
if (Array.isArray(order)) return orderGiven(order);
251253
throw new Error(`invalid order: ${order}`);
252254
}
253255

254256
// by value
255-
function orderY(data, X, Y) {
256-
return Y;
257+
function orderY(compare) {
258+
return (data, X, Y) => (i, j) => compare(Y[i], Y[j]);
257259
}
258260

259261
// by location
260-
function orderZ(order, X, Y, Z) {
261-
return Z;
262+
function orderZ(compare) {
263+
return (data, X, Y, Z) => (i, j) => compare(Z[i], Z[j]);
262264
}
263265

264266
// by sum of value (a.k.a. “ascending”)
265-
function orderSum(data, X, Y, Z) {
266-
return orderZDomain(
267-
Z,
267+
function orderSum(compare) {
268+
return orderZDomain(compare, (data, X, Y, Z) =>
268269
groupSort(
269270
range(data),
270271
(I) => sum(I, (i) => Y[i]),
@@ -274,9 +275,8 @@ function orderSum(data, X, Y, Z) {
274275
}
275276

276277
// by x = argmax of value
277-
function orderAppearance(data, X, Y, Z) {
278-
return orderZDomain(
279-
Z,
278+
function orderAppearance(compare) {
279+
return orderZDomain(compare, (data, X, Y, Z) =>
280280
groupSort(
281281
range(data),
282282
(I) => X[greatest(I, (i) => Y[i])],
@@ -287,52 +287,57 @@ function orderAppearance(data, X, Y, Z) {
287287

288288
// by x = argmax of value, but rearranged inside-out by alternating series
289289
// according to the sign of a running divergence of sums
290-
function orderInsideOut(data, X, Y, Z) {
291-
const I = range(data);
292-
const K = groupSort(
293-
I,
294-
(I) => X[greatest(I, (i) => Y[i])],
295-
(i) => Z[i]
296-
);
297-
const sums = rollup(
298-
I,
299-
(I) => sum(I, (i) => Y[i]),
300-
(i) => Z[i]
301-
);
302-
const Kp = [],
303-
Kn = [];
304-
let s = 0;
305-
for (const k of K) {
306-
if (s < 0) {
307-
s += sums.get(k);
308-
Kp.push(k);
309-
} else {
310-
s -= sums.get(k);
311-
Kn.push(k);
290+
function orderInsideOut(compare) {
291+
return orderZDomain(compare, (data, X, Y, Z) => {
292+
const I = range(data);
293+
const K = groupSort(
294+
I,
295+
(I) => X[greatest(I, (i) => Y[i])],
296+
(i) => Z[i]
297+
);
298+
const sums = rollup(
299+
I,
300+
(I) => sum(I, (i) => Y[i]),
301+
(i) => Z[i]
302+
);
303+
const Kp = [],
304+
Kn = [];
305+
let s = 0;
306+
for (const k of K) {
307+
if (s < 0) {
308+
s += sums.get(k);
309+
Kp.push(k);
310+
} else {
311+
s -= sums.get(k);
312+
Kn.push(k);
313+
}
312314
}
313-
}
314-
return orderZDomain(Z, Kn.reverse().concat(Kp));
315+
return Kn.reverse().concat(Kp);
316+
});
315317
}
316318

317-
function orderFunction(f) {
318-
return (data) => valueof(data, f);
319+
function orderAccessor(f) {
320+
return (data) => {
321+
const O = valueof(data, f);
322+
return (i, j) => ascendingDefined(O[i], O[j]);
323+
};
319324
}
320325

321-
function orderGiven(domain) {
322-
return (data, X, Y, Z) => orderZDomain(Z, domain);
326+
function orderComparator(f) {
327+
return (data) => (i, j) => f(data[i], data[j]);
323328
}
324329

325-
// Given an explicit ordering of distinct values in z, returns a parallel column
326-
// O that can be used with applyOrder to sort stacks. Note that this is a series
327-
// order: it will be consistent across stacks.
328-
function orderZDomain(Z, domain) {
329-
if (!Z) throw new Error("missing channel: z");
330-
domain = new InternMap(domain.map((d, i) => [d, i]));
331-
return Z.map((z) => domain.get(z));
330+
function orderGiven(domain) {
331+
return orderZDomain(ascendingDefined, () => domain);
332332
}
333333

334-
function applyOrder(stacks, O) {
335-
for (const stack of stacks) {
336-
stack.sort((i, j) => ascendingDefined(O[i], O[j]));
337-
}
334+
// Given an ordering (domain) of distinct values in z that can be derived from
335+
// the data, returns a comparator that can be used to sort stacks. Note that
336+
// this is a series order: it will be consistent across stacks.
337+
function orderZDomain(compare, domain) {
338+
return (data, X, Y, Z) => {
339+
if (!Z) throw new Error("missing channel: z");
340+
const map = new InternMap(domain(data, X, Y, Z).map((d, i) => [d, i]));
341+
return (i, j) => compare(map.get(Z[i]), map.get(Z[j]));
342+
};
338343
}

0 commit comments

Comments
 (0)