diff --git a/components/BarGraph.module.css b/components/BarGraph.module.css index d709ac5..baddec2 100644 --- a/components/BarGraph.module.css +++ b/components/BarGraph.module.css @@ -19,10 +19,6 @@ filter: drop-shadow(0 0 calc(4rem / 16) var(--primary-accent)); } -.barGroup:hover .barText { - visibility: visible; -} - .tickLabel { font-family: "Inconsolata", monospace; font-weight: 800; diff --git a/components/BarGraph.tsx b/components/BarGraph.tsx index 6884ab3..a98acf8 100644 --- a/components/BarGraph.tsx +++ b/components/BarGraph.tsx @@ -1,14 +1,18 @@ import { AxisBottom, AxisLeft } from "@visx/axis"; import { bottomTickLabelProps } from "@visx/axis/lib/axis/AxisBottom"; import { leftTickLabelProps } from "@visx/axis/lib/axis/AxisLeft"; +import { localPoint } from "@visx/event"; import { GridColumns, GridRows } from "@visx/grid"; import { Group } from "@visx/group"; +import { Point } from "@visx/point"; import { scaleBand, scaleLinear } from "@visx/scale"; import { Bar } from "@visx/shape"; -import { Text } from "@visx/text"; +import { withTooltip } from "@visx/tooltip"; import React from "react"; import { Color } from "utils/Color"; +import { TooltipWrapper } from "./TooltipWrapper"; + import styles from "./BarGraph.module.css"; interface BarGraphProps { @@ -29,8 +33,6 @@ interface BarGraphProps { categoryTickLabelSize?: number; /** Font size of the value axis tick labels, in pixels. Default is 16px. */ valueTickLabelSize?: number; - /** Font size of the value that appears when hovering over a bar, in pixels. */ - hoverLabelSize?: number; /** Label text for the category axis. */ categoryAxisLabel?: string; /** Font size of the label for the cateogry axis, in pixels. */ @@ -62,8 +64,11 @@ interface BarGraphData { const DEFAULT_LABEL_SIZE = 16; -export function BarGraphHorizontal(props: BarGraphProps) { - const { +type TooltipData = string; + +export const BarGraphHorizontal = withTooltip( + ({ + width, height, margin, data, @@ -71,7 +76,6 @@ export function BarGraphHorizontal(props: BarGraphProps) { minWidth = 500, categoryTickLabelSize = DEFAULT_LABEL_SIZE, valueTickLabelSize = DEFAULT_LABEL_SIZE, - hoverLabelSize, categoryAxisLabel, categoryAxisLabelSize = DEFAULT_LABEL_SIZE, categoryAxisLabelOffset = 0, @@ -79,139 +83,158 @@ export function BarGraphHorizontal(props: BarGraphProps) { valueAxisLabelSize = DEFAULT_LABEL_SIZE, valueAxisLabelOffset = 0, defaultLabelDy = "0", - } = props; - const width = props.width < minWidth ? minWidth : props.width; // Ensuring graph's width >= minWidth - const barPadding = 0.4; + tooltipOpen, + tooltipLeft, + tooltipTop, + tooltipData, + hideTooltip, + showTooltip, + }) => { + width = width < minWidth ? minWidth : width; // Ensuring graph's width >= minWidth + const barPadding = 0.4; - const categoryMax = height - margin.top - margin.bottom; - const valueMax = width - margin.left - margin.right; + const categoryMax = height - margin.top - margin.bottom; + const valueMax = width - margin.left - margin.right; - const getCategory = (d: BarGraphData) => d.category; - const getValue = (d: BarGraphData) => d.value; + const getCategory = (d: BarGraphData) => d.category; + const getValue = (d: BarGraphData) => d.value; - const categoryScale = scaleBand({ - range: [0, categoryMax], - domain: data.map(getCategory), - padding: barPadding, - }); + const categoryScale = scaleBand({ + range: [0, categoryMax], + domain: data.map(getCategory), + padding: barPadding, + }); - const valueScale = scaleLinear({ - range: [0, valueMax], - nice: true, - domain: [0, Math.max(...data.map(getValue))], - }); + const valueScale = scaleLinear({ + range: [0, valueMax], + nice: true, + domain: [0, Math.max(...data.map(getValue))], + }); - const categoryPoint = (d: BarGraphData) => categoryScale(getCategory(d)); - const valuePoint = (d: BarGraphData) => valueScale(getValue(d)); + const categoryPoint = (d: BarGraphData) => categoryScale(getCategory(d)); + const valuePoint = (d: BarGraphData) => valueScale(getValue(d)); - return ( - - - - {data.map((d, idx) => { - const barName = `${getCategory(d)}-${idx}`; - const barWidth = categoryScale.bandwidth(); - const backgroundBarWidth = barWidth / (1 - barPadding); - return idx % 2 === 0 ? ( - - ) : null; - })} - - - - {data.map((d, idx) => { - const barName = `${getCategory(d)}-${idx}`; - const barLength = valuePoint(d); - const barWidth = categoryScale.bandwidth(); - return ( - - - - {getValue(d)} - - - ); - })} - - - { - return { - ...leftTickLabelProps(), - className: styles.tickLabel, - fontSize: `${categoryTickLabelSize / 16}rem`, - }; - }} - label={categoryAxisLabel} - labelClassName={styles.axisLabel} - labelOffset={categoryAxisLabelOffset} - labelProps={{ - fontSize: `${categoryAxisLabelSize / 16}rem`, - }} - /> - { - return { - ...bottomTickLabelProps(), - className: styles.tickLabel, - dy: defaultLabelDy, - fontSize: `${valueTickLabelSize / 16}rem`, - }; - }} - label={valueAxisLabel} - labelClassName={styles.axisLabel} - labelOffset={valueAxisLabelOffset} - labelProps={{ - fontSize: `${valueAxisLabelSize / 16}rem`, - }} - /> - - ); -} + return ( +
+ + + + {data.map((d, idx) => { + const barName = `${getCategory(d)}-${idx}`; + const barWidth = categoryScale.bandwidth(); + const backgroundBarWidth = barWidth / (1 - barPadding); + return idx % 2 === 0 ? ( + + ) : null; + })} + + + + {data.map((d, idx) => { + const barName = `${getCategory(d)}-${idx}`; + const barLength = valuePoint(d); + const barWidth = categoryScale.bandwidth(); + return ( + + { + const eventSvgCoords = localPoint( + // ownerSVGElement is given by visx docs but not recognized by typescript + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + e.target.ownerSVGElement as Element, + e + ) as Point; + showTooltip({ + tooltipData: getValue(d).toString(), + tooltipTop: eventSvgCoords.y, + tooltipLeft: eventSvgCoords.x, + }); + }} + onMouseOut={hideTooltip} + className={styles.bar} + x={0} + y={categoryPoint(d)} + width={barLength} + height={barWidth} + /> + + ); + })} + + { + return { + ...leftTickLabelProps(), + className: styles.tickLabel, + fontSize: `${categoryTickLabelSize / 16}rem`, + }; + }} + label={categoryAxisLabel} + labelClassName={styles.axisLabel} + labelOffset={categoryAxisLabelOffset} + labelProps={{ + fontSize: `${categoryAxisLabelSize / 16}rem`, + }} + /> + { + return { + ...bottomTickLabelProps(), + className: styles.tickLabel, + dy: defaultLabelDy, + fontSize: `${valueTickLabelSize / 16}rem`, + }; + }} + label={valueAxisLabel} + labelClassName={styles.axisLabel} + labelOffset={valueAxisLabelOffset} + labelProps={{ + fontSize: `${valueAxisLabelSize / 16}rem`, + }} + /> + + -export function BarGraphVertical(props: BarGraphProps) { - const { + {tooltipOpen && ( + + )} +
+ ); + } +); + +export const BarGraphVertical = withTooltip( + ({ + width, height, margin, data, @@ -219,7 +242,6 @@ export function BarGraphVertical(props: BarGraphProps) { minWidth = 500, categoryTickLabelSize = DEFAULT_LABEL_SIZE, valueTickLabelSize = DEFAULT_LABEL_SIZE, - hoverLabelSize, categoryAxisLabel, categoryAxisLabelSize = DEFAULT_LABEL_SIZE, categoryAxisLabelOffset = 0, @@ -230,142 +252,161 @@ export function BarGraphVertical(props: BarGraphProps) { alternatingLabelSpace = 80, defaultLabelDy = `0px`, lowerLabelDy = `30px`, - } = props; - const width = props.width < minWidth ? minWidth : props.width; // Ensuring graph's width >= minWidth - const barPadding = 0.4; - const alternatingLabel = width <= widthAlternatingLabel; - const final_margin_bottom = alternatingLabel - ? margin.bottom + alternatingLabelSpace - : margin.bottom; + tooltipOpen, + tooltipLeft, + tooltipTop, + tooltipData, + hideTooltip, + showTooltip, + }) => { + width = width < minWidth ? minWidth : width; // Ensuring graph's width >= minWidth + const barPadding = 0.4; + const alternatingLabel = width <= widthAlternatingLabel; + const final_margin_bottom = alternatingLabel + ? margin.bottom + alternatingLabelSpace + : margin.bottom; - const categoryMax = width - margin.left - margin.right; - const valueMax = height - margin.top - final_margin_bottom; + const categoryMax = width - margin.left - margin.right; + const valueMax = height - margin.top - final_margin_bottom; - const getCategory = (d: BarGraphData) => d.category; - const getValue = (d: BarGraphData) => d.value; + const getCategory = (d: BarGraphData) => d.category; + const getValue = (d: BarGraphData) => d.value; - const categoryScale = scaleBand({ - range: [0, categoryMax], - domain: data.map(getCategory), - padding: barPadding, - }); + const categoryScale = scaleBand({ + range: [0, categoryMax], + domain: data.map(getCategory), + padding: barPadding, + }); - const valueScale = scaleLinear({ - range: [valueMax, 0], - nice: true, - domain: [0, Math.max(...data.map(getValue))], - }); + const valueScale = scaleLinear({ + range: [valueMax, 0], + nice: true, + domain: [0, Math.max(...data.map(getValue))], + }); - const categoryPoint = (d: BarGraphData) => categoryScale(getCategory(d)); - const valuePoint = (d: BarGraphData) => valueScale(getValue(d)); + const categoryPoint = (d: BarGraphData) => categoryScale(getCategory(d)); + const valuePoint = (d: BarGraphData) => valueScale(getValue(d)); - return ( - - - - {data.map((d, idx) => { - const barName = `${getCategory(d)}-${idx}`; - const barWidth = categoryScale.bandwidth(); - const backgroundBarWidth = barWidth / (1 - barPadding); - return idx % 2 === 0 ? ( - - ) : null; - })} - - - - {data.map((d, idx) => { - const barName = `${getCategory(d)}-${idx}`; - const barHeight = valueMax - valuePoint(d); - const barWidth = categoryScale.bandwidth(); - return ( - - - - {getValue(d)} - - - ); - })} - - - { - const alternatingDy = index % 2 == 0 ? defaultLabelDy : lowerLabelDy; - return { - ...bottomTickLabelProps(), - className: styles.tickLabel, - dy: alternatingLabel ? alternatingDy : defaultLabelDy, - fontSize: `${categoryTickLabelSize / 16}rem`, - width: categoryScale.bandwidth(), - verticalAnchor: "start", - }; - }} - label={categoryAxisLabel} - labelClassName={styles.axisLabel} - labelOffset={categoryAxisLabelOffset} - labelProps={{ - fontSize: `${categoryAxisLabelSize / 16}rem`, - }} - /> - { - return { - ...leftTickLabelProps(), - className: styles.tickLabel, - dx: "-0.5rem", - dy: "0.25rem", - fontSize: `${valueTickLabelSize / 16}rem`, - }; - }} - label={valueAxisLabel} - labelClassName={styles.axisLabel} - labelOffset={valueAxisLabelOffset} - labelProps={{ - fontSize: `${valueAxisLabelSize / 16}rem`, - }} - /> - - ); -} + return ( +
+ + + + {data.map((d, idx) => { + const barName = `${getCategory(d)}-${idx}`; + const barWidth = categoryScale.bandwidth(); + const backgroundBarWidth = barWidth / (1 - barPadding); + return idx % 2 === 0 ? ( + + ) : null; + })} + + + + {data.map((d, idx) => { + const barName = `${getCategory(d)}-${idx}`; + const barHeight = valueMax - valuePoint(d); + const barWidth = categoryScale.bandwidth(); + return ( + + { + const eventSvgCoords = localPoint( + // ownerSVGElement is given by visx docs but not recognized by typescript + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + e.target.ownerSVGElement as Element, + e + ) as Point; + showTooltip({ + tooltipData: getValue(d).toString(), + tooltipTop: eventSvgCoords.y, + tooltipLeft: eventSvgCoords.x, + }); + }} + onMouseOut={hideTooltip} + className={styles.bar} + x={categoryPoint(d)} + y={valueMax - barHeight} + width={barWidth} + height={barHeight} + /> + + ); + })} + + { + const alternatingDy = + index % 2 == 0 ? defaultLabelDy : lowerLabelDy; + return { + ...bottomTickLabelProps(), + className: styles.tickLabel, + dy: alternatingLabel ? alternatingDy : defaultLabelDy, + fontSize: `${categoryTickLabelSize / 16}rem`, + width: categoryScale.bandwidth(), + verticalAnchor: "start", + }; + }} + label={categoryAxisLabel} + labelClassName={styles.axisLabel} + labelOffset={categoryAxisLabelOffset} + labelProps={{ + fontSize: `${categoryAxisLabelSize / 16}rem`, + }} + /> + { + return { + ...leftTickLabelProps(), + className: styles.tickLabel, + dx: "-0.5rem", + dy: "0.25rem", + fontSize: `${valueTickLabelSize / 16}rem`, + }; + }} + label={valueAxisLabel} + labelClassName={styles.axisLabel} + labelOffset={valueAxisLabelOffset} + labelProps={{ + fontSize: `${valueAxisLabelSize / 16}rem`, + }} + /> + + + + {tooltipOpen && ( + + )} +
+ ); + } +);