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 as VisxBoxPlot } from "@visx/stats"; import { withTooltip, Tooltip } 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; const TICK_LABEL_FONT_WEIGHT = 800; interface BoxPlotData { category: string; min: number; median: number; max: number; firstQuartile: number; thirdQuartile: number; outliers?: number[]; } type TooltipData = Omit; export type StatsPlotProps = { data: BoxPlotData[]; /** Width of the entire graph, in pixels, greater than 10. */ width: number; /** Height of the entire graph, in pixels. */ height: number; /** Distance between the edge of the graph and the area where the bars are drawn, in pixels. */ margin: { top: number; left: number; }; /** Width of the lines in the graph, in px. */ strokeWidth?: number; /** Length of the dashes and the gaps in the graph, in px. */ strokeDashArray?: string; /** Number of ticks for the value (y-)axis */ numTicksLeftAxis?: number; /** Distance between the boxplot and the top of the grid, in px. */ plotTopOffset?: number; /** Distance between the left axis labels and the start of the lines of the graph, in px. */ valueAxisLeftOffset?: number; /** Distance between the top and the first label of the y axis, in px. */ valueAxisLabelTopOffset?: number; /** Distance between the left and the labels of the y axis, in px. */ valueAxisLabelLeftOffset?: number; /** Distance between the left and the start of the first label of the x axis, in px. */ categoryAxisLabelLeftOffset?: number; /** Distance between the top and the column lines of the grid of the graph, in px. */ gridColumnTopOffset?: number; /** Distance between the top of the point in the boxplot and the start of the tooltip box, in px. */ toolTipTopOffset?: number; /** Distance between the left of the point in the boxplot and the start of the tooltip box, in px. */ toolTipLeftOffset?: number; /** Font size of the category (x-)axis labels */ categoryAxisLabelSize?: number; /** Font size of the value (y-)axis labels */ valueAxisLabelSize?: number; /** Font size of the text in the tool tip box */ toolTipFontSize?: number; /** Factor multiplied with the compressed width to determine the box width, in px. */ boxPlotWidthFactor?: number; /** Factor multiplied with the compressed width to determine the distance between boxes, in px. */ boxPlotLeftOffset?: number; }; export const BoxPlot = withTooltip( ({ width, height, data, margin, tooltipOpen, tooltipLeft, tooltipTop, tooltipData, showTooltip, hideTooltip, strokeWidth = 2.5, strokeDashArray = "10,4", numTicksLeftAxis = 6, plotTopOffset = 10, valueAxisLeftOffset = 40, gridColumnTopOffset = -20, valueAxisLabelTopOffset = 5, valueAxisLabelLeftOffset = 10, categoryAxisLabelLeftOffset = 30, toolTipTopOffset = 20, toolTipLeftOffset = 5, categoryAxisLabelSize = DEFAULT_LABEL_SIZE, valueAxisLabelSize = DEFAULT_LABEL_SIZE, boxPlotWidthFactor = 0.4, boxPlotLeftOffset = 0.3, }: StatsPlotProps & WithTooltipProvidedProps) => { // bounds const xMax = width; const yMax = height - 120; // formatting data const plotData: Stats[] = data.map((d) => { return { boxPlot: { ...d, x: d.category, outliers: [], }, binData: [], }; }); // accessors const getX = (d: Stats) => d.boxPlot.x; const getMin = (d: Stats) => d.boxPlot.min; const getMax = (d: Stats) => d.boxPlot.max; const getMedian = (d: Stats) => d.boxPlot.median; const getFirstQuartile = (d: Stats) => d.boxPlot.firstQuartile; const getThirdQuartile = (d: Stats) => d.boxPlot.thirdQuartile; // scales const xScale = scaleBand({ range: [18, xMax - 80], // scaling is needed due to the left offset round: true, domain: plotData.map(getX), padding: 0.3, }); const values = plotData.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({ range: [yMax, 0], round: true, domain: [minYValue, maxYValue], }); const constrainedWidth = Math.min(200, xScale.bandwidth()); return width < 10 ? null : (
{ return { fill: Color.label, fontWeight: TICK_LABEL_FONT_WEIGHT, }; }} /> { return { fill: Color.label, fontWeight: TICK_LABEL_FONT_WEIGHT, }; }} /> {plotData.map((d: Stats, i) => ( { showTooltip({ tooltipTop: (yScale(getMin(d)) ?? 0) + toolTipTopOffset, tooltipLeft: xScale(getX(d))! + constrainedWidth + toolTipLeftOffset, tooltipData: { ...d.boxPlot, category: getX(d), }, }); }, onMouseLeave: () => { hideTooltip(); }, }} maxProps={{ onMouseOver: () => { showTooltip({ tooltipTop: (yScale(getMax(d)) ?? 0) + toolTipTopOffset, tooltipLeft: xScale(getX(d))! + constrainedWidth + toolTipLeftOffset, tooltipData: { ...d.boxPlot, category: getX(d), }, }); }, onMouseLeave: () => { hideTooltip(); }, }} boxProps={{ onMouseOver: () => { showTooltip({ tooltipTop: (yScale(getMedian(d)) ?? 0) + toolTipTopOffset, tooltipLeft: xScale(getX(d))! + constrainedWidth + toolTipLeftOffset, tooltipData: { ...d.boxPlot, category: getX(d), }, }); }, strokeWidth: 0, onMouseLeave: () => { hideTooltip(); }, }} medianProps={{ style: { stroke: Color.label, }, onMouseOver: () => { showTooltip({ tooltipTop: (yScale(getMedian(d)) ?? 0) + toolTipTopOffset, tooltipLeft: xScale(getX(d))! + constrainedWidth + toolTipLeftOffset, tooltipData: { ...d.boxPlot, category: getX(d), }, }); }, onMouseLeave: () => { hideTooltip(); }, }} /> ))} {tooltipOpen && tooltipData && (

{tooltipData.category}

max: {tooltipData.max}

third quartile: {tooltipData.thirdQuartile}

median: {tooltipData.median}

first quartile: {tooltipData.firstQuartile}

min: {tooltipData.min}

)}
); } );