UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

149 lines (134 loc) 3.49 kB
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 = {};