-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathboxplot.ts
148 lines (134 loc) · 3.41 KB
/
boxplot.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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { min as d3Min, max as d3Max, quantile, group } from 'd3-array';
import { CompositeMarkComponent as CC } from '../runtime';
import { BoxPlotMark } from '../spec';
import { subObject } from '../utils/helper';
import { maybeAnimation, subTooltip } from '../utils/mark';
export type BoxPlotOptions = Omit<BoxPlotMark, 'type'>;
function min(I: number[], V: number[]): number {
return d3Min(I, (i) => V[i]);
}
function max(I: number[], V: number[]): number {
return d3Max(I, (i) => V[i]);
}
function lower(I: number[], V: number[]): number {
const lo = q1(I, V) * 2.5 - q3(I, V) * 1.5;
return d3Min(I, (i) => (V[i] >= lo ? V[i] : NaN));
}
function q1(I: number[], V: number[]): number {
return quantile(I, 0.25, (i) => V[i]);
}
function q2(I: number[], V: number[]): number {
return quantile(I, 0.5, (i) => V[i]);
}
function q3(I: number[], V: number[]): number {
return quantile(I, 0.75, (i) => V[i]);
}
function upper(I: number[], V: number[]): number {
const hi = q3(I, V) * 2.5 - q1(I, V) * 1.5;
return d3Max(I, (i) => (V[i] <= hi ? V[i] : NaN));
}
/**
* Group marks by x and reserve outlier indexes.
*/
function OutlierY() {
return (I: number[], mark) => {
const { encode } = mark;
const { y, x } = encode;
const { value: V } = y;
const { value: X } = x;
const GI = Array.from(group(I, (i) => X[+i]).values());
const FI = GI.flatMap((I) => {
const lo = lower(I, V);
const hi = upper(I, V);
return I.filter((i) => V[i] < lo || V[i] > hi);
});
return [FI, mark];
};
}
export const Boxplot: CC<BoxPlotOptions> = (options) => {
const {
data,
encode,
style = {},
tooltip = {},
transform,
animate,
...rest
} = options;
const { point = true, ...restStyle } = style;
const { y } = encode;
const encodeY = { y, y1: y, y2: y, y3: y, y4: y };
const qy = { y1: q1, y2: q2, y3: q3 };
// Tooltips.
const boxTooltip = subTooltip(
tooltip,
'box',
{
items: [
{ channel: 'y', name: 'min' },
{ channel: 'y1', name: 'q1' },
{ channel: 'y2', name: 'q2' },
{ channel: 'y3', name: 'q3' },
{ channel: 'y4', name: 'max' },
],
},
true,
);
const pointTooltip = subTooltip(tooltip, 'point', {
title: { channel: 'x' },
items: [{ name: 'outlier', channel: 'y' }],
});
// Only show min and max instead of lower and upper.
// Only draw a box.
if (!point) {
return {
type: 'box',
data: data,
transform: [
{
type: 'groupX',
y: min,
...qy,
y4: max,
},
],
encode: { ...encode, ...encodeY },
style: restStyle,
tooltip: boxTooltip,
...rest,
};
}
const boxStyle = subObject(restStyle, 'box');
const pointStyle = subObject(restStyle, 'point');
return [
// Draw five-number box.
{
type: 'box',
data: data,
transform: [
{
type: 'groupX',
y: lower,
...qy,
y4: upper,
},
],
encode: { ...encode, ...encodeY },
style: boxStyle,
tooltip: boxTooltip,
animate: maybeAnimation(animate, 'box'),
...rest,
},
// Draw outliers.
{
type: 'point',
data: data,
transform: [{ type: OutlierY }],
encode,
style: { ...pointStyle },
tooltip: pointTooltip,
animate: maybeAnimation(animate, 'point'),
},
];
};
Boxplot.props = {};