import { AxisLeft, AxisBottom } from "@visx/axis"; import { localPoint } from "@visx/event"; import { GridRows, GridColumns } from "@visx/grid"; import { Group } from "@visx/group"; import { LegendOrdinal } from "@visx/legend"; import { Point } from "@visx/point"; import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale"; import { BarStack, BarStackHorizontal, Line } from "@visx/shape"; import { SeriesPoint } from "@visx/shape/lib/types"; 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 "./StackedBarGraph.module.css"; interface StackedBarData { category: string; [key: string]: number | string; } type TooltipData = { bar: SeriesPoint; key: string; index: number; height: number; width: number; x: number; y: number; color: string; }; export type StackedBarProps = { data: StackedBarData[]; /** Width of the entire graph, in pixels, greater than 10. */ width: number; /** Height of the entire graph, in pixels. */ height: number; /** Names of the groups appearing in the legend */ keys: string[]; /** Colours for each key */ colorRange: string[]; /** Distance between the edge of the graph and the area where the bars are drawn, in pixels. */ margin: { top: number; left: number }; /** Number of ticks for the value axis */ numTicksValueAxis?: number; /** Distance between the left axis labels and the start of the lines of the graph, in px. */ axisLeftOffset?: number; /** Distance between the bottom axis and the bottom of the container of the graph, in px. */ axisBottomOffset?: number; /** Distance between the right side of the graph and the legend, in px. */ legendLeftOffset?: number; /** Distance between the top of the graph and the legend, in px. */ legendTopOffset?: 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; /** Padding between each bar in the stacked bar graph, from 0 to 1 */ scalePadding?: number; /** Margin for each item in the legend */ itemMargin?: string; /** Factor multiplied with an offset to center the labels of the category-axis depending on the width/height of the graph. * >1 for width/height <600 and <1 for width/height >600 (vertical=width/horizontal=height) */ categoryAxisLeftFactor?: number; }; let tooltipTimeout: number; export const StackedBarGraphVertical = withTooltip< StackedBarProps, TooltipData >( ({ data, width, height, keys, colorRange, margin, scalePadding = 0.3, numTicksValueAxis = 6, axisLeftOffset = 40, axisBottomOffset = 40, strokeWidth = 2.5, strokeDashArray = "10,4", legendLeftOffset = 40, legendTopOffset = 40, itemMargin = "15px 0 0 0", categoryAxisLeftFactor = 1, tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip, }: StackedBarProps & WithTooltipProvidedProps) => { const yTotals = data.reduce((allTotals, currCategory) => { const yTotal = keys.reduce((categoryTotal, k) => { categoryTotal += currCategory[k] as number; return categoryTotal; }, 0); allTotals.push(yTotal); return allTotals; }, [] as number[]); const TICK_LABEL_FONT_WEIGHT = 800; // accessors const getCategory = (d: StackedBarData) => d.category; // scales const categoryScale = scaleBand({ domain: data.map(getCategory), padding: scalePadding, }); const valueScale = scaleLinear({ domain: [0, Math.max(...yTotals)], nice: true, }); const colorScale = scaleOrdinal({ domain: keys, range: colorRange, }); // bounds const xMax = width; const yMax = height - margin.top - axisBottomOffset; categoryScale.rangeRound([0, xMax - axisLeftOffset]); valueScale.range([yMax, 0]); return width < 10 ? null : (
data={data} keys={keys} x={getCategory} xScale={categoryScale} yScale={valueScale} color={colorScale} > {(barStacks) => barStacks.map((barStack) => barStack.bars.map((bar) => ( { tooltipTimeout = window.setTimeout(() => { hideTooltip(); }, 300); }} onMouseMove={(event) => { if (tooltipTimeout) clearTimeout(tooltipTimeout); const eventSvgCoords = localPoint(event); const left = bar.x + bar.width / 2; showTooltip({ tooltipData: bar, tooltipTop: eventSvgCoords?.y, tooltipLeft: left, }); }} /> )) ) } ({ fill: Color.label, fontWeight: TICK_LABEL_FONT_WEIGHT, })} /> { return { fill: Color.label, fontWeight: TICK_LABEL_FONT_WEIGHT, }; }} />
{tooltipOpen && tooltipData ? (

{tooltipData.key}

{tooltipData.bar.data[tooltipData.key]}

{getCategory(tooltipData.bar.data)}

) : null}
); } ); export const StackedBarGraphHorizontal = withTooltip< StackedBarProps, TooltipData >( ({ data, width, height, keys, colorRange, margin, scalePadding = 0.3, numTicksValueAxis = 6, axisLeftOffset = 40, axisBottomOffset = 40, strokeWidth = 2.5, strokeDashArray = "10,4", legendLeftOffset = 40, legendTopOffset = 40, itemMargin = "15px 0 0 0", categoryAxisLeftFactor = 1, tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip, }: StackedBarProps & WithTooltipProvidedProps) => { const yTotals = data.reduce((allTotals, currCategory) => { const yTotal = keys.reduce((categoryTotal, k) => { categoryTotal += currCategory[k] as number; return categoryTotal; }, 0); allTotals.push(yTotal); return allTotals; }, [] as number[]); const TICK_LABEL_FONT_WEIGHT = 800; // accessors const getCategory = (d: StackedBarData) => d.category; // scales const valueScale = scaleLinear({ domain: [0, Math.max(...yTotals)], nice: true, }); const categoryScale = scaleBand({ domain: data.map(getCategory), padding: scalePadding, }); const colorScale = scaleOrdinal({ domain: keys, range: colorRange, }); // bounds const xMax = width; const yMax = height - margin.top - axisBottomOffset; categoryScale.rangeRound([yMax, 0]); valueScale.range([0, xMax - axisLeftOffset]); return width < 10 ? null : (
data={data} keys={keys} y={getCategory} xScale={valueScale} yScale={categoryScale} color={colorScale} > {(barStacks) => barStacks.map((barStack) => barStack.bars.map((bar) => ( { tooltipTimeout = window.setTimeout(() => { hideTooltip(); }, 300); }} onMouseMove={(event) => { if (tooltipTimeout) clearTimeout(tooltipTimeout); const eventSvgCoords = localPoint(event); const left = bar.x + bar.width / 2; showTooltip({ tooltipData: bar, tooltipTop: eventSvgCoords?.y, tooltipLeft: left, }); }} /> )) ) } ({ fill: Color.label, fontWeight: TICK_LABEL_FONT_WEIGHT, })} /> { return { fill: Color.label, fontWeight: TICK_LABEL_FONT_WEIGHT, }; }} />
{tooltipOpen && tooltipData ? (

{tooltipData.key}

{tooltipData.bar.data[tooltipData.key]}

{getCategory(tooltipData.bar.data)}

) : null}
); } );