1
1
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";
3
3
import {withTip} from "../mark.js";
4
4
import {maybeApplyInterval, maybeColumn, maybeZ, maybeZero} from "../options.js";
5
5
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) {
81
81
const [Y2, setY2] = column(y);
82
82
Y1.hint = Y2.hint = lengthy;
83
83
offset = maybeOffset(offset);
84
- order = maybeOrder(order, offset, ky); // TODO shorthand -order with reverse?
84
+ order = maybeOrder(order, offset, ky);
85
85
return [
86
86
basic(options, (data, facets, plotOptions) => {
87
87
const X = x == null ? undefined : setX(maybeApplyInterval(valueof(data, x), plotOptions?.[kx]));
88
88
const Y = valueof(data, y, Float64Array);
89
89
const Z = valueof(data, z);
90
- const O = order && order(data, X, Y, Z);
90
+ const compare = order && order(data, X, Y, Z);
91
91
const n = data.length;
92
92
const Y1 = setY1(new Float64Array(n));
93
93
const Y2 = setY2(new Float64Array(n));
94
94
const facetstacks = [];
95
95
for (const facet of facets) {
96
96
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 );
98
98
for (const stack of stacks) {
99
99
let yn = 0;
100
100
let yp = 0;
@@ -228,43 +228,44 @@ function offsetCenterFacets(facetstacks, Y1, Y2) {
228
228
}
229
229
230
230
function maybeOrder(order, offset, ky) {
231
- if (order === undefined && offset === offsetWiggle) return orderInsideOut;
231
+ if (order === undefined && offset === offsetWiggle) return orderInsideOut(ascendingDefined) ;
232
232
if (order == null) return;
233
233
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()) {
235
237
case "value":
236
238
case ky:
237
- return orderY;
239
+ return orderY(compare) ;
238
240
case "z":
239
- return orderZ;
241
+ return orderZ(compare) ;
240
242
case "sum":
241
- return orderSum;
243
+ return orderSum(compare) ;
242
244
case "appearance":
243
- return orderAppearance;
245
+ return orderAppearance(compare) ;
244
246
case "inside-out":
245
- return orderInsideOut;
247
+ return orderInsideOut(compare) ;
246
248
}
247
- return orderFunction (field(order));
249
+ return orderAccessor (field(order));
248
250
}
249
- if (typeof order === "function") return orderFunction (order);
251
+ if (typeof order === "function") return (order.length === 1 ? orderAccessor : orderComparator) (order);
250
252
if (Array.isArray(order)) return orderGiven(order);
251
253
throw new Error(`invalid order: ${order}`);
252
254
}
253
255
254
256
// 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]) ;
257
259
}
258
260
259
261
// 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]) ;
262
264
}
263
265
264
266
// 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) =>
268
269
groupSort(
269
270
range(data),
270
271
(I) => sum(I, (i) => Y[i]),
@@ -274,9 +275,8 @@ function orderSum(data, X, Y, Z) {
274
275
}
275
276
276
277
// 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) =>
280
280
groupSort(
281
281
range(data),
282
282
(I) => X[greatest(I, (i) => Y[i])],
@@ -287,52 +287,57 @@ function orderAppearance(data, X, Y, Z) {
287
287
288
288
// by x = argmax of value, but rearranged inside-out by alternating series
289
289
// 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
+ }
312
314
}
313
- }
314
- return orderZDomain(Z, Kn.reverse().concat(Kp) );
315
+ return Kn.reverse().concat(Kp);
316
+ } );
315
317
}
316
318
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
+ };
319
324
}
320
325
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] );
323
328
}
324
329
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);
332
332
}
333
333
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
+ };
338
343
}
0 commit comments