Skip to content

Commit caa785d

Browse files
committed
stacks, rebased on observablehq/plot#176
1 parent 8194ba3 commit caa785d

22 files changed

+9524
-31
lines changed

src/transforms/stack.js

Lines changed: 157 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,114 @@
1-
import {InternMap} from "d3-array";
2-
import {valueof} from "../mark.js";
1+
import {InternMap, ascending, cumsum, descending, group, groupSort, maxIndex, range, rollup, sum} from "d3-array";
2+
import {field, maybeSort, take, valueof} from "../mark.js";
33

44
export function stackX({x, y, ...options}) {
5-
const [transform, Y, x1, x2] = stack(y, x);
5+
const [transform, Y, x1, x2] = stack(y, x, options);
66
return {...options, transform, y: Y, x1, x2};
77
}
88

99
export function stackY({x, y, ...options}) {
10-
const [transform, X, y1, y2] = stack(x, y);
10+
const [transform, X, y1, y2] = stack(x, y, options);
1111
return {...options, transform, x: X, y1, y2};
1212
}
1313

14-
// TODO configurable series order
15-
function stack(x, y) {
14+
function stack(x, y = () => 1, {
15+
z, fill, stroke, title,
16+
rank,
17+
reverse = ["descending", "reverse"].includes(rank),
18+
offset,
19+
sort
20+
}) {
1621
const [X, setX] = lazyChannel(x);
1722
const [Y1, setY1] = lazyChannel(y);
1823
const [Y2, setY2] = lazyChannel(y);
1924
return [
20-
data => {
25+
(data, facets) => {
2126
const X = setX(valueof(data, x));
2227
const Y = valueof(data, y);
23-
const n = X.length;
24-
const Y0 = new InternMap();
28+
const Z = valueof(data, z || fill || stroke || title);
29+
const R = maybeRank(rank, data, X, Y, Z);
30+
const n = data.length;
31+
const I = range(n);
2532
const Y1 = setY1(new Float64Array(n));
2633
const Y2 = setY2(new Float64Array(n));
27-
for (let i = 0; i < n; ++i) {
28-
const k = X[i];
29-
const y1 = Y1[i] = Y0.has(k) ? Y0.get(k) : 0;
30-
const y2 = Y2[i] = y1 + +Y[i];
31-
Y0.set(k, isNaN(y2) ? y1 : y2);
34+
sort = maybeSort(sort);
35+
36+
for (const index of (facets === undefined ? [I] : facets)) {
37+
38+
if (sort) {
39+
const facet = take(data, index);
40+
const index0 = index.slice();
41+
const sorted = sort(facet);
42+
for (let k = 0; k < index.length; k++) index[k] = index0[facet.indexOf(sorted[k])];
43+
}
44+
45+
const Yp = new InternMap();
46+
const Yn = new InternMap();
47+
48+
const stacks = group(index, i => X[i]);
49+
50+
// rank sort
51+
if (R) {
52+
const a = reverse ? descending : ascending;
53+
for (const [, stack] of stacks) stack.sort((i, j) => a(R[i], R[j]));
54+
}
55+
56+
// stack
57+
for (const [x, stack] of stacks) {
58+
for (const i of stack) {
59+
const v = +Y[i];
60+
const [Y0, ceil, floor] = v < 0 ? [Yn, Y1, Y2] : [Yp, Y2, Y1];
61+
const y1 = floor[i] = Y0.has(x) ? Y0.get(x) : 0;
62+
const y2 = ceil[i] = y1 + +Y[i];
63+
Y0.set(x, isNaN(y2) ? y1 : y2);
64+
}
65+
}
66+
67+
// offset
68+
if (offset === "expand") {
69+
for (const i of index) {
70+
const x = X[i];
71+
const floor = Yn.has(x) ? Yn.get(x) : 0;
72+
const ceil = Yp.has(x) ? Yp.get(x) : 0;
73+
const m = 1 / (ceil - floor || 1);
74+
Y1[i] = m * (-floor + Y1[i]);
75+
Y2[i] = m * (-floor + Y2[i]);
76+
}
77+
}
78+
if (offset === "silhouette") {
79+
for (const i of index) {
80+
const x = X[i];
81+
const floor = Yn.has(x) ? Yn.get(x) : 0;
82+
const ceil = Yp.has(x) ? Yp.get(x) : 0;
83+
const m = (ceil + floor) / 2;
84+
Y1[i] -= m;
85+
Y2[i] -= m;
86+
}
87+
}
88+
if (offset === "wiggle") {
89+
const prev = new InternMap();
90+
let y = 0;
91+
for (const [, stack] of stacks) {
92+
let j = -1;
93+
const Fi = stack.map(i => Math.abs(Y2[i] - Y1[i]));
94+
const Df = stack.map(i => {
95+
j = z ? Z[i] : ++j;
96+
const value = Y2[i] - Y1[i];
97+
const diff = prev.has(j) ? value - prev.get(j) : 0;
98+
prev.set(j, value);
99+
return diff;
100+
});
101+
const Cf1 = [0, ...cumsum(Df)];
102+
for (const i of stack) {
103+
Y1[i] += y;
104+
Y2[i] += y;
105+
}
106+
const s1 = sum(Fi);
107+
if (s1) y -= sum(Fi, (d, i) => (Df[i] / 2 + Cf1[i]) * d) / s1;
108+
}
109+
}
32110
}
33-
return data;
111+
return {index: facets === undefined ? I : facets, data};
34112
},
35113
X,
36114
Y1,
@@ -45,11 +123,74 @@ function lazyChannel(source) {
45123
let value;
46124
return [
47125
{
48-
transform() { return value; },
126+
transform: () => value,
49127
label: typeof source === "string" ? source
50128
: source ? source.label
51129
: undefined
52130
},
53131
v => value = v
54132
];
55133
}
134+
135+
// well-known ranking strategies by series
136+
function maybeRank(rank, data, X, Y, Z) {
137+
if (rank == null) return [null];
138+
// d3.stackOrderNone, sorts series by key, ascending
139+
// d3.stackOrderReverse, sorts series by key, descending
140+
if (rank === "key" || rank === "none" || rank === "reverse") {
141+
return Z;
142+
}
143+
// d3.stackOrderAscending, sorts series by sum of value, ascending
144+
if (rank === "sum" || rank === "ascending" || rank === "descending") {
145+
const S = groupSort(range(data.length), g => sum(g, i => Y[i]), i => Z[i]);
146+
return Z.map(z => S.indexOf(z));
147+
}
148+
// ranks items by value
149+
if (rank === "value") {
150+
return Y;
151+
}
152+
// d3.stackOrderAppearance, sorts series by x = argmax of value
153+
if (rank === "appearance") {
154+
const K = groupSort(
155+
range(data.length),
156+
v => X[v[maxIndex(v, i => Y[i])]],
157+
i => Z[i]
158+
);
159+
return Z.map(z => K.indexOf(z));
160+
}
161+
// d3.stackOrderInsideOut, sorts series by x = argmax of value, then rearranges them
162+
// inside out by alternating series according to the sign of a running divergence
163+
// of their sums
164+
if (rank === "insideOut") {
165+
const K = groupSort(
166+
range(data.length),
167+
v => X[v[maxIndex(v, i => Y[i])]],
168+
i => Z[i]
169+
);
170+
const sums = rollup(range(data.length), v => sum(v, i => Y[i]), i => Z[i]);
171+
const order = [];
172+
let diff = 0;
173+
for (const k of K) {
174+
if (diff < 0) {
175+
diff += sums.get(k);
176+
order.push(k);
177+
} else {
178+
diff -= sums.get(k);
179+
order.unshift(k);
180+
}
181+
}
182+
return Z.map(z => order.indexOf(z));
183+
}
184+
// any other string is a datum accessor
185+
if (typeof rank === "string") {
186+
return valueof(data, field(rank));
187+
}
188+
// rank can be an array of z (particularly useful with groupSort)
189+
if (rank.indexOf) {
190+
return Z.map(z => rank.indexOf(z));
191+
}
192+
// final case, a generic function
193+
if (typeof rank === "function") {
194+
return valueof(data, rank);
195+
}
196+
}

test/data/caltrain.csv

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
station,time,line,type,orientation,hours,minutes
2+
Palo Alto,5:01am,101N,N,N,5,01
3+
Palo Alto,8:01pm,191N,N,N,20,01
4+
Palo Alto,9:01pm,193N,N,N,21,01
5+
Palo Alto,10:01pm,195N,N,N,22,01
6+
Palo Alto,11:01pm,197N,N,N,23,01
7+
Palo Alto,8:01am,216L,L,S,8,01
8+
Palo Alto,9:01am,226L,L,S,9,01
9+
Palo Alto,5:01pm,264L,L,S,17,01
10+
Palo Alto,6:02pm,274L,L,S,18,02
11+
Palo Alto,10:03am,134N,N,S,10,03
12+
Palo Alto,11:03am,138N,N,S,11,03
13+
Palo Alto,12:03pm,142N,N,S,12,03
14+
Palo Alto,1:03pm,146N,N,S,13,03
15+
Palo Alto,2:03pm,150N,N,S,14,03
16+
Palo Alto,3:03pm,154N,N,S,15,03
17+
Palo Alto,4:03pm,158N,N,S,16,03
18+
Palo Alto,6:05am,305B,B,N,6,05
19+
Palo Alto,7:05am,313B,B,N,7,05
20+
Palo Alto,8:05am,323B,B,N,8,05
21+
Palo Alto,5:06pm,369B,B,N,17,06
22+
Palo Alto,6:06pm,379B,B,N,18,06
23+
Palo Alto,7:10pm,287L,L,N,19,10
24+
Palo Alto,9:11am,233L,L,N,9,11
25+
Palo Alto,10:11am,237L,L,N,10,11
26+
Palo Alto,3:11pm,257L,L,N,15,11
27+
Palo Alto,5:12pm,368B,B,S,17,12
28+
Palo Alto,6:12pm,378B,B,S,18,12
29+
Palo Alto,7:12pm,386B,B,S,19,12
30+
Palo Alto,7:16am,215L,L,N,7,16
31+
Palo Alto,8:16am,225L,L,N,8,16
32+
Palo Alto,4:16pm,261L,L,N,16,16
33+
Palo Alto,5:16pm,267L,L,N,17,16
34+
Palo Alto,6:16pm,277L,L,N,18,16
35+
Palo Alto,7:18am,208L,L,S,7,18
36+
Palo Alto,8:18am,218L,L,S,8,18
37+
Palo Alto,9:18am,228L,L,S,9,18
38+
Palo Alto,7:21pm,189N,N,N,19,21
39+
Palo Alto,6:21am,104N,N,S,6,21
40+
Palo Alto,6:23am,309B,B,N,6,23
41+
Palo Alto,7:23am,319B,B,N,7,23
42+
Palo Alto,8:23am,329B,B,N,8,23
43+
Palo Alto,4:24pm,263L,L,N,16,24
44+
Palo Alto,5:24pm,271L,L,N,17,24
45+
Palo Alto,6:24pm,281L,L,N,18,24
46+
Palo Alto,10:25am,236L,L,S,10,25
47+
Palo Alto,3:25pm,256L,L,S,15,25
48+
Palo Alto,4:25pm,260L,L,S,16,25
49+
Palo Alto,7:26am,210L,L,S,7,26
50+
Palo Alto,8:26am,220L,L,S,8,26
51+
Palo Alto,9:26am,230L,L,S,9,26
52+
Palo Alto,8:26pm,190N,N,S,20,26
53+
Palo Alto,5:36am,103N,N,N,5,36
54+
Palo Alto,6:36am,207L,L,N,6,36
55+
Palo Alto,7:36am,217L,L,N,7,36
56+
Palo Alto,8:36am,227L,L,N,8,36
57+
Palo Alto,9:36pm,192N,N,S,21,36
58+
Palo Alto,10:36pm,194N,N,S,22,36
59+
Palo Alto,11:36pm,196N,N,S,23,36
60+
Palo Alto,3:38pm,159N,N,N,15,38
61+
Palo Alto,5:38pm,270L,L,S,17,38
62+
Palo Alto,6:38pm,280L,L,S,18,38
63+
Palo Alto,7:38pm,288L,L,S,19,38
64+
Palo Alto,9:41am,135N,N,N,9,41
65+
Palo Alto,10:41am,139N,N,N,10,41
66+
Palo Alto,11:41am,143N,N,N,11,41
67+
Palo Alto,12:41pm,147N,N,N,12,41
68+
Palo Alto,1:41pm,151N,N,N,13,41
69+
Palo Alto,2:41pm,155N,N,N,14,41
70+
Palo Alto,4:44pm,362B,B,S,16,44
71+
Palo Alto,5:49pm,372B,B,S,17,49
72+
Palo Alto,6:49pm,382B,B,S,18,49
73+
Palo Alto,5:51am,102N,N,S,5,51
74+
Palo Alto,7:51am,314B,B,S,7,51
75+
Palo Alto,8:51am,324B,B,S,8,51
76+
Palo Alto,5:54pm,275L,L,N,17,54
77+
Palo Alto,6:54pm,285L,L,N,18,54
78+
Palo Alto,6:57am,206L,L,S,6,57
79+
Palo Alto,12:57am,198N,N,S,24,57

test/data/learning-poverty.csv

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
Country Name,Out-of-School (OoS),Below Minimum Proficiency (in School),Learning Poverty,Assessment Year,Assessment,population,density
2+
Afghanistan,49.6,87,93.4,2013,NLA,38928346,60
3+
Argentina,0.6,53.6,53.9,2013,LLECE,45195774,17
4+
Armenia,7.2,30,35,2015,TIMSS,2963243,104
5+
Australia,3.2,5.5,8.6,2016,PIRLS,25499884,3
6+
Austria,0,2.4,2.4,2016,PIRLS,9006398,109
7+
Azerbaijan,5,19.2,23.3,2016,PIRLS,10139177,123
8+
Bahrain,2.1,30.6,32.1,2016,PIRLS,1701575,2239
9+
Bangladesh,4.9,56,58.1,2017,NLA,164689383,1265
10+
Belgium,1.3,5.1,6.4,2016,PIRLS,11589623,383
11+
Benin,3.6,77.3,78.2,2014,PASEC,12123200,108
12+
Botswana,7.2,44.3,48.3,2011,PIRLS,2351627,4
13+
Brazil,2.7,46.9,48.4,2013,LLECE,212559417,25
14+
Bulgaria,6.8,5.2,11.7,2016,PIRLS,6948445,64
15+
Burkina Faso,31.7,78.6,85.4,2014,PASEC,20903273,76
16+
Burundi,2.7,92.7,92.9,2014,PASEC,11890784,463
17+
Cambodia,2.6,49.8,51.1,2013,NLA,16718965,95
18+
Cameroon,5.2,75.9,77.2,2014,PASEC,26545863,56
19+
Canada,0,4.3,4.3,2016,PIRLS,37742154,4
20+
Chad,21.1,97,97.7,2014,PASEC,16425864,13
21+
Chile,9.3,30.3,36.8,2013,LLECE,19116201,26
22+
China,0,18.2,18.2,2016,NLA,1439323776,153
23+
Colombia,6.9,44.7,48.6,2013,LLECE,50882891,46
24+
"Congo, Dem Rep",63.2,62,86,2011,NLA,89561403,40
25+
"Congo, Rep",12.8,82.9,85.1,2014,PASEC,5518087,16
26+
Costa Rica,1.1,31.7,32.5,2013,LLECE,5094118,100
27+
Cote d’Ivoire,21.1,77.6,82.3,2014,PASEC,26378274,83
28+
Croatia,3,1,4,2011,PIRLS,4105267,73
29+
Cyprus,2.2,14.3,16.2,2015,TIMSS,1207359,131
30+
Czech Republic,0,3,3,2016,PIRLS,10708981,139
31+
Denmark,1,2.6,3.6,2016,PIRLS,5792202,137
32+
Dominican Republic,6.6,79.4,80.7,2013,LLECE,10847910,225
33+
Ecuador,1.9,62.1,62.8,2013,LLECE,17643054,71
34+
"Egypt, Arab Rep",1.4,69.2,69.6,2016,PIRLS,102334404,103
35+
Ethiopia,14,88.7,90.3,2015,NLA,114963588,115
36+
Finland,0.9,1.7,2.6,2016,PIRLS,5540720,18
37+
France,0.9,6.3,7.1,2016,PIRLS,65273511,119
38+
Georgia,0.4,13.5,13.8,2016,PIRLS,3989167,57
39+
Germany,0.2,5.5,5.7,2016,PIRLS,83783942,240
40+
Guatemala,10.1,63.6,67.3,2013,LLECE,17915568,167
41+
Honduras,17.1,69.4,74.7,2013,LLECE,9904607,89
42+
"Hong Kong SAR, China",1.9,1.4,3.2,2016,PIRLS,7496981,7140
43+
Hungary,3.1,2.9,5.9,2016,PIRLS,9660351,107
44+
India,2.3,53.7,54.8,2017,NLA,1380004385,464
45+
Indonesia,2.4,33.8,35.4,2011,PIRLS,273523615,151
46+
"Iran, Islamic Rep",0.9,35.1,35.7,2016,PIRLS,83992949,52
47+
Ireland,0,2.3,2.3,2016,PIRLS,4937786,72
48+
Israel,2.9,9,11.7,2016,PIRLS,8655535,400
49+
Italy,1.4,2.1,3.5,2016,PIRLS,60461826,206
50+
Japan,1.2,1,2.2,2015,TIMSS,126476461,347
51+
Jordan,4,50,52,2015,TIMSS,10203134,115
52+
Kazakhstan,0.3,1.9,2.2,2016,PIRLS,18776707,7
53+
"Korea, Rep",2.7,0.3,3,2015,TIMSS,51269185,527
54+
Kuwait,3.3,49.4,51,2016,PIRLS,4270571,240
55+
Kyrgyz Republic,1.9,63.8,64.5,2014,NLA,6524195,34
56+
Latvia,3.2,0.8,4,2016,PIRLS,1886198,30
57+
Lithuania,0.3,2.7,3,2016,PIRLS,2722289,43
58+
"Macao SAR, China",1.3,2.4,3.7,2016,PIRLS,649335,21645
59+
Madagascar,21.9,95.8,96.7,2015,NLA,27691018,48
60+
Malaysia,1.4,11.7,12.9,2017,NLA,32365999,99
61+
Mali,33,86.6,91,2012,NLA,20250833,17
62+
Malta,2.4,26.8,28.6,2016,PIRLS,441543,1380
63+
Mexico,1.2,42.5,43.2,2013,LLECE,128932753,66
64+
Morocco,5.4,63.8,65.8,2016,PIRLS,36910560,83
65+
Netherlands,0.3,1.3,1.6,2016,PIRLS,17134872,508
66+
New Zealand,1.5,10,11.4,2016,PIRLS,4822233,18
67+
Nicaragua,1.6,69.3,69.8,2013,LLECE,6624554,55
68+
Niger,38.9,97.9,98.7,2014,PASEC,24206644,19
69+
Norway,0.2,5.8,6,2016,PIRLS,5421241,15
70+
Oman,1.5,40.9,41.8,2016,PIRLS,5106626,16
71+
Pakistan,27.3,65,74.5,2014,NLA,220892340,287
72+
Panama,7.1,64.1,66.6,2013,LLECE,4314767,58
73+
Paraguay,10.8,71.3,74.4,2013,LLECE,7132538,18
74+
Peru,4.2,53.7,55.7,2013,LLECE,32971854,26
75+
Poland,4.4,2,6.3,2016,PIRLS,37846611,124
76+
Portugal,3.6,3,6.5,2016,PIRLS,10196709,111
77+
Qatar,2.2,33.8,35.3,2016,PIRLS,2881053,248
78+
Romania,6.9,14.1,20,2011,PIRLS,19237691,84
79+
Russian Federation,2.4,0.9,3.3,2016,PIRLS,145934462,9
80+
Saudi Arabia,2.5,36.7,38.3,2016,PIRLS,34813871,16
81+
Senegal,25.7,65.2,74.1,2014,PASEC,16743927,87
82+
Serbia,0.8,7.4,8.1,2015,TIMSS,8737371,100
83+
Singapore,0.1,2.7,2.8,2016,PIRLS,5850342,8358
84+
Slovak Republic,2.1,6.6,8.5,2016,PIRLS,5459642,114
85+
Slovenia,2.2,3.7,5.8,2016,PIRLS,2078938,103
86+
South Africa,8.4,77.9,79.8,2016,PIRLS,59308690,49
87+
Spain,1.5,3.4,4.9,2016,PIRLS,46754778,94
88+
Sri Lanka,0.9,14,14.8,2015,NLA,21413249,341
89+
Sweden,0.4,1.9,2.3,2016,PIRLS,10099265,25
90+
Thailand,2,21.9,23.5,2011,TIMSS,69799978,137
91+
Togo,8.5,84.2,85.6,2014,PASEC,8278724,152
92+
Trinidad and Tobago,1.3,19.7,20.7,2016,PIRLS,1399488,273
93+
Tunisia,0.4,65.1,65.3,2011,TIMSS,11818619,76
94+
Turkey,5,17.6,21.7,2015,TIMSS,84339067,110
95+
Uganda,9,81.1,82.8,2014,NLA,45741007,229
96+
United Arab Emirates,2.8,32.4,34.3,2016,PIRLS,9890402,118
97+
United Kingdom,0.2,3.2,3.4,2016,PIRLS,67886011,281
98+
United States,4.1,3.9,7.9,2016,PIRLS,331002651,36
99+
Uruguay,0.5,41.4,41.7,2013,LLECE,3473730,20
100+
Vietnam,0.6,1.1,1.7,2011,NLA,97338579,314
101+
"Yemen, Rep",18.9,93.5,94.7,2011,TIMSS,29825964,56

0 commit comments

Comments
 (0)