parent
f25839702b
commit
2496e34798
@ -0,0 +1,5 @@ |
||||
.boxplot:hover { |
||||
fill: var(--primary-accent); |
||||
fill-opacity: 0.9; |
||||
filter: drop-shadow(0 0 calc(4rem / 16) var(--primary-accent)); |
||||
} |
@ -0,0 +1,332 @@ |
||||
import { AxisLeft, AxisBottom } from "@visx/axis"; |
||||
import { GridRows, GridColumns } from "@visx/grid"; |
||||
import { Group } from "@visx/group"; |
||||
import { Stats } from "@visx/mock-data/lib/generators/genStats"; |
||||
import { Point } from "@visx/point"; |
||||
import { scaleBand, scaleLinear } from "@visx/scale"; |
||||
import { Line } from "@visx/shape"; |
||||
import { BoxPlot } from "@visx/stats"; |
||||
import { |
||||
withTooltip, |
||||
Tooltip, |
||||
defaultStyles as defaultTooltipStyles, |
||||
} from "@visx/tooltip"; |
||||
import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip"; |
||||
import React from "react"; |
||||
import { Color } from "utils/Color"; |
||||
|
||||
import styles from "./Boxplot.module.css"; |
||||
|
||||
const DEFAULT_LABEL_SIZE = 16; |
||||
|
||||
interface BoxPlotData { |
||||
x: string; |
||||
min: number; |
||||
median: number; |
||||
max: number; |
||||
firstQuartile: number; |
||||
thirdQuartile: number; |
||||
outliers: number[]; |
||||
} |
||||
|
||||
interface TooltipData { |
||||
name?: string; |
||||
min?: number; |
||||
median?: number; |
||||
max?: number; |
||||
firstQuartile?: number; |
||||
thirdQuartile?: number; |
||||
} |
||||
|
||||
export type StatsPlotProps = { |
||||
width: number; |
||||
height: number; |
||||
data: BoxPlotData[]; |
||||
margin: { |
||||
top: number; |
||||
left: number; |
||||
}; |
||||
strokeWidth: number; |
||||
strokeDashArray: string; |
||||
valueAxisLeftOffset?: number; |
||||
valueAxisLabelTopOffset?: number; |
||||
valueAxisLabelLeftOffset?: number; |
||||
categoryAxisLabelSize?: number; |
||||
categoryAxisLabelLeftOffset?: number; |
||||
valueAxisLabelSize?: number; |
||||
gridColumnTopOffset?: number; |
||||
toolTipFontSize?: number; |
||||
}; |
||||
|
||||
export default withTooltip<StatsPlotProps, TooltipData>( |
||||
({ |
||||
width, |
||||
height, |
||||
data, |
||||
margin, |
||||
strokeWidth, |
||||
strokeDashArray, |
||||
tooltipOpen, |
||||
tooltipLeft, |
||||
tooltipTop, |
||||
tooltipData, |
||||
showTooltip, |
||||
hideTooltip, |
||||
valueAxisLeftOffset = 40, |
||||
gridColumnTopOffset = -20, |
||||
valueAxisLabelTopOffset = 15, |
||||
valueAxisLabelLeftOffset = 10, |
||||
categoryAxisLabelLeftOffset = 30, |
||||
categoryAxisLabelSize = DEFAULT_LABEL_SIZE, |
||||
valueAxisLabelSize = DEFAULT_LABEL_SIZE, |
||||
toolTipFontSize = DEFAULT_LABEL_SIZE, |
||||
}: StatsPlotProps & WithTooltipProvidedProps<TooltipData>) => { |
||||
// bounds
|
||||
const xMax = width; |
||||
const yMax = height - 120; |
||||
// formatting data
|
||||
const d: Stats[] = []; |
||||
for (let i = 0; i < data.length; i++) { |
||||
d.push({ |
||||
boxPlot: data[i], |
||||
binData: [], |
||||
}); |
||||
} |
||||
|
||||
// accessors
|
||||
const x = (d: Stats) => d.boxPlot.x; |
||||
const min = (d: Stats) => d.boxPlot.min; |
||||
const max = (d: Stats) => d.boxPlot.max; |
||||
const median = (d: Stats) => d.boxPlot.median; |
||||
const firstQuartile = (d: Stats) => d.boxPlot.firstQuartile; |
||||
const thirdQuartile = (d: Stats) => d.boxPlot.thirdQuartile; |
||||
|
||||
// scales
|
||||
const xScale = scaleBand<string>({ |
||||
range: [18, xMax - 80], // scaling is needed due to the left offset
|
||||
round: true, |
||||
domain: d.map(x), |
||||
padding: 0.4, |
||||
}); |
||||
|
||||
const values = d.reduce((allValues, { boxPlot }) => { |
||||
allValues.push(boxPlot.min, boxPlot.max); |
||||
return allValues; |
||||
}, [] as number[]); |
||||
const minYValue = Math.min(...values); |
||||
const maxYValue = Math.max(...values); |
||||
|
||||
const yScale = scaleLinear<number>({ |
||||
range: [yMax, 0], |
||||
round: true, |
||||
domain: [minYValue, maxYValue], |
||||
}); |
||||
|
||||
const boxWidth = xScale.bandwidth(); |
||||
const constrainedWidth = Math.min(200, boxWidth); |
||||
|
||||
return width < 10 ? null : ( |
||||
<div style={{ position: "relative" }}> |
||||
<svg width={width} height={height}> |
||||
<Group top={margin.top} left={margin.left}> |
||||
<GridRows |
||||
top={10} |
||||
left={valueAxisLeftOffset} |
||||
scale={yScale} |
||||
width={xMax} |
||||
height={yMax} |
||||
numTicks={6} |
||||
stroke={Color.tertiaryBackground} |
||||
strokeWidth={strokeWidth} |
||||
strokeDasharray={strokeDashArray} |
||||
/> |
||||
<GridColumns |
||||
scale={xScale} |
||||
width={xMax} |
||||
height={yMax + 32} |
||||
top={gridColumnTopOffset} |
||||
left={valueAxisLeftOffset} |
||||
stroke={Color.tertiaryBackground} |
||||
strokeWidth={strokeWidth} |
||||
strokeDasharray={strokeDashArray} |
||||
/> |
||||
<Line |
||||
fill={Color.tertiaryBackground} |
||||
to={new Point({ x: valueAxisLeftOffset, y: gridColumnTopOffset })} |
||||
from={new Point({ x: valueAxisLeftOffset, y: yMax + 10 })} |
||||
stroke={Color.tertiaryBackground} |
||||
strokeWidth={strokeWidth} |
||||
strokeDasharray={strokeDashArray} |
||||
/> |
||||
<Line |
||||
fill={Color.tertiaryBackground} |
||||
to={ |
||||
new Point({ |
||||
x: xMax - 20 - strokeWidth, |
||||
y: gridColumnTopOffset, |
||||
}) |
||||
} |
||||
from={new Point({ x: xMax - 20 - strokeWidth, y: yMax + 10 })} |
||||
stroke={Color.tertiaryBackground} |
||||
strokeWidth={strokeWidth} |
||||
strokeDasharray={strokeDashArray} |
||||
/> |
||||
<AxisBottom |
||||
top={yMax + 30} |
||||
left={categoryAxisLabelLeftOffset} |
||||
scale={xScale} |
||||
hideAxisLine |
||||
hideTicks |
||||
labelProps={{ |
||||
fontSize: `${categoryAxisLabelSize / 16}rem`, |
||||
}} |
||||
tickLabelProps={() => { |
||||
return { |
||||
fill: Color.label, |
||||
fontWeight: 800, |
||||
}; |
||||
}} |
||||
/> |
||||
<AxisLeft |
||||
scale={yScale} |
||||
top={valueAxisLabelTopOffset} |
||||
left={valueAxisLabelLeftOffset} |
||||
numTicks={6} |
||||
hideAxisLine |
||||
labelProps={{ |
||||
fontSize: `${valueAxisLabelSize / 16}rem`, |
||||
}} |
||||
tickLabelProps={() => { |
||||
return { |
||||
fill: Color.label, |
||||
fontWeight: 800, |
||||
}; |
||||
}} |
||||
/> |
||||
<Group top={6}> |
||||
{d.map((d: Stats, i) => ( |
||||
<g key={i}> |
||||
<BoxPlot |
||||
className={styles.boxplot} |
||||
min={min(d)} |
||||
max={max(d)} |
||||
left={ |
||||
xScale(x(d))! + |
||||
0.3 * constrainedWidth + |
||||
valueAxisLeftOffset |
||||
} |
||||
firstQuartile={firstQuartile(d)} |
||||
thirdQuartile={thirdQuartile(d)} |
||||
median={median(d)} |
||||
boxWidth={constrainedWidth * 0.4} |
||||
fill={Color.primaryAccentLight} |
||||
fillOpacity={0.8} |
||||
stroke={Color.label} |
||||
strokeWidth={strokeWidth} |
||||
valueScale={yScale} |
||||
minProps={{ |
||||
onMouseOver: () => { |
||||
showTooltip({ |
||||
tooltipTop: yScale(min(d)) ?? 0 + 40, |
||||
tooltipLeft: xScale(x(d))! + constrainedWidth + 5, |
||||
tooltipData: { |
||||
min: min(d), |
||||
name: x(d), |
||||
}, |
||||
}); |
||||
}, |
||||
onMouseLeave: () => { |
||||
hideTooltip(); |
||||
}, |
||||
}} |
||||
maxProps={{ |
||||
onMouseOver: () => { |
||||
showTooltip({ |
||||
tooltipTop: yScale(max(d)) ?? 0 + 40, |
||||
tooltipLeft: xScale(x(d))! + constrainedWidth + 5, |
||||
tooltipData: { |
||||
max: max(d), |
||||
name: x(d), |
||||
}, |
||||
}); |
||||
}, |
||||
onMouseLeave: () => { |
||||
hideTooltip(); |
||||
}, |
||||
}} |
||||
boxProps={{ |
||||
onMouseOver: () => { |
||||
showTooltip({ |
||||
tooltipTop: yScale(median(d)) ?? 0 + 20, |
||||
tooltipLeft: xScale(x(d))! + constrainedWidth + 5, |
||||
tooltipData: { |
||||
...d.boxPlot, |
||||
name: x(d), |
||||
}, |
||||
}); |
||||
}, |
||||
onMouseLeave: () => { |
||||
hideTooltip(); |
||||
}, |
||||
}} |
||||
medianProps={{ |
||||
style: { |
||||
stroke: "white", |
||||
}, |
||||
onMouseOver: () => { |
||||
showTooltip({ |
||||
tooltipTop: yScale(median(d)) ?? 0 + 40, |
||||
tooltipLeft: xScale(x(d))! + constrainedWidth + 5, |
||||
tooltipData: { |
||||
median: median(d), |
||||
name: x(d), |
||||
}, |
||||
}); |
||||
}, |
||||
onMouseLeave: () => { |
||||
hideTooltip(); |
||||
}, |
||||
}} |
||||
/> |
||||
</g> |
||||
))} |
||||
</Group> |
||||
</Group> |
||||
</svg> |
||||
|
||||
{tooltipOpen && tooltipData && ( |
||||
<Tooltip |
||||
top={tooltipTop} |
||||
left={tooltipLeft} |
||||
style={{ |
||||
...defaultTooltipStyles, |
||||
backgroundColor: Color.secondaryBackground, |
||||
color: Color.primaryText, |
||||
padding: "10px", |
||||
}} |
||||
> |
||||
<div> |
||||
<strong>{tooltipData.name}</strong> |
||||
</div> |
||||
<div |
||||
style={{ |
||||
marginTop: "5px", |
||||
fontSize: `${toolTipFontSize / 16}rem`, |
||||
}} |
||||
> |
||||
{tooltipData.max && <div>max: {tooltipData.max}</div>} |
||||
{tooltipData.thirdQuartile && ( |
||||
<div>third quartile: {tooltipData.thirdQuartile}</div> |
||||
)} |
||||
{tooltipData.median && <div>median: {tooltipData.median}</div>} |
||||
{tooltipData.firstQuartile && ( |
||||
<div>first quartile: {tooltipData.firstQuartile}</div> |
||||
)} |
||||
{tooltipData.min && <div>min: {tooltipData.min}</div>} |
||||
</div> |
||||
</Tooltip> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
); |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue