-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathstackY.ts
129 lines (117 loc) · 3.64 KB
/
stackY.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { deepMix } from '@antv/util';
import { TransformComponent as TC } from '../runtime';
import { StackYTransform } from '../spec';
import {
column,
columnOf,
inferredColumn,
maybeColumnOf,
} from './utils/helper';
import { normalizeComparator, createGroups, applyOrder } from './utils/order';
export type StackYOptions = Omit<StackYTransform, 'type'>;
/**
* The stack transform group marks into series by color channel,
* and then produce new y channel for each series by specified order,
* say to form vertical "stacks" by specified channels.
*/
export const StackY: TC<StackYOptions> = (options = {}) => {
const {
groupBy = 'x',
orderBy = null,
reverse = false,
y: fromY = 'y',
y1: fromY1 = 'y1',
series = true,
} = options;
return (I, mark) => {
const { data, encode, style = {} } = mark;
const [Y, fy] = columnOf(encode, 'y');
const [Y1, fy1] = columnOf(encode, 'y1');
const [S] = series
? maybeColumnOf(encode, 'series', 'color')
: columnOf(encode, 'color');
// Create groups and apply specified order for each group.
const groups = createGroups(groupBy, I, mark);
const createComparator = normalizeComparator(orderBy) ?? (() => null);
const comparator = createComparator(data, Y, S);
if (comparator) applyOrder(groups, comparator);
// Stack y channels to produce new y and y1 channel.
const newY = new Array(I.length);
const newY1 = new Array(I.length);
const TY = new Array(I.length);
const F = [];
const L = [];
for (const G of groups) {
if (reverse) G.reverse();
// For range interval with specified y and y1.
const start = Y1 ? +Y1[G[0]] : 0;
// Split positive indices of Y and negative Y.
const PG = [];
const NG = [];
for (const i of G) {
const y = (TY[i] = +Y[i] - start);
if (y < 0) NG.push(i);
else if (y >= 0) PG.push(i);
}
// Store the first and last layer.
const FG = PG.length > 0 ? PG : NG;
const LG = NG.length > 0 ? NG : PG;
let i = PG.length - 1;
let j = 0;
// Find the last non-zero index.
while (i > 0 && Y[FG[i]] === 0) i--;
// Find the first non-zero index.
while (j < LG.length - 1 && Y[LG[j]] === 0) j++;
F.push(FG[i]);
L.push(LG[j]);
// Stack negative y in reverse order.
let ny = start;
for (const i of NG.reverse()) {
const y = TY[i];
ny = newY[i] = (newY1[i] = ny) + y;
}
// Stack positive y in input order.
let py = start;
for (const i of PG) {
const y = TY[i];
if (y > 0) py = newY[i] = (newY1[i] = py) + y;
else newY[i] = newY1[i] = py;
}
}
// Only set top radius for the first layer,
// and set bottom radius for the last layer.
const FS = new Set(F);
const LS = new Set(L);
// Choose new y or y1 channel as the new y channel.
const V = fromY === 'y' ? newY : newY1;
const V1 = fromY1 === 'y' ? newY : newY1;
let newEncode: Record<string, any>;
// mark point will compute the actural Y = (y + y1) / 2 if y1 exists
if (mark.type === 'point') {
newEncode = {
y0: inferredColumn(Y, fy), // Store original Y.
y: column(V, fy),
};
} else {
newEncode = {
y0: inferredColumn(Y, fy), // Store original Y.
y: column(V, fy),
y1: column(V1, fy1),
};
}
return [
I,
deepMix({}, mark, {
encode: {
...newEncode,
},
style: {
first: (_, i) => FS.has(i),
last: (_, i) => LS.has(i),
...style,
},
}),
];
};
};
StackY.props = {};