diff --git a/components/BarGraph.module.css b/components/BarGraph.module.css index d709ac5..af16d7f 100644 --- a/components/BarGraph.module.css +++ b/components/BarGraph.module.css @@ -6,23 +6,11 @@ fill: var(--primary-accent-light); } -.barText { - visibility: hidden; - - font-family: "Inconsolata", monospace; - font-weight: 800; - fill: var(--label); -} - .barGroup:hover .bar { fill: var(--primary-accent); 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 && ( + + )} +
+ ); + } +); diff --git a/components/GroupedBarGraph.module.css b/components/GroupedBarGraph.module.css index 4e6dcd0..b452211 100644 --- a/components/GroupedBarGraph.module.css +++ b/components/GroupedBarGraph.module.css @@ -9,18 +9,6 @@ fill: var(--card-background); } -.barText { - visibility: hidden; - - font-family: "Inconsolata", monospace; - font-weight: 800; - fill: var(--label); -} - -.singleBar:hover .barText { - visibility: visible; -} - .tickLabel { font-family: "Inconsolata", monospace; font-weight: 800; diff --git a/components/GroupedBarGraph.tsx b/components/GroupedBarGraph.tsx index bce8fdf..7db46e6 100644 --- a/components/GroupedBarGraph.tsx +++ b/components/GroupedBarGraph.tsx @@ -1,16 +1,20 @@ 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 { LegendOrdinal } from "@visx/legend"; +import { Point } from "@visx/point"; import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale"; import { Bar, BarGroup, BarGroupHorizontal } from "@visx/shape"; import { BarGroupBar as BarGroupBarType } from "@visx/shape/lib/types"; -import { Text } from "@visx/text"; +import { withTooltip } from "@visx/tooltip"; import React, { useState } from "react"; import { Color } from "utils/Color"; +import { TooltipWrapper } from "./TooltipWrapper"; + import styles from "./GroupedBarGraph.module.css"; interface GroupedBarGraphProps { @@ -79,15 +83,20 @@ interface BarGroupData { // BAR_PADDING must be in the range [0, 1) const BAR_PADDING = 0.2; -const BAR_TEXT_PADDING = 12; const DEFAULT_LABEL_SIZE = 16; -export function GroupedBarGraphVertical(props: GroupedBarGraphProps) { - const { +type TooltipData = string; + +export const GroupedBarGraphVertical = withTooltip< + GroupedBarGraphProps, + TooltipData +>( + ({ data: propsData, barColors, barHoverColorsMap, + width, height, margin, className, @@ -106,209 +115,242 @@ export function GroupedBarGraphVertical(props: GroupedBarGraphProps) { alternatingLabelSpace = 80, defaultLabelDy = `0px`, lowerLabelDy = `30px`, - } = props; - const width = props.width < minWidth ? minWidth : props.width; // Ensuring graph's width >= minWidth - 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 alternatingLabel = width <= widthAlternatingLabel; + const final_margin_bottom = alternatingLabel + ? margin.bottom + alternatingLabelSpace + : margin.bottom; - const data: BarGroupData[] = propsData.map((datum: GroupedBarGraphData) => { - return { category: datum.category, ...datum.values }; - }); + const data: BarGroupData[] = propsData.map((datum: GroupedBarGraphData) => { + return { category: datum.category, ...datum.values }; + }); - const keys = Object.keys(propsData[0].values); - propsData.forEach((d: GroupedBarGraphData) => { - const currentKeys = Object.keys(d.values); - if ( - keys.length != currentKeys.length || - !keys.every((key: string) => currentKeys.includes(key)) - ) { - throw new Error( - "Every category in a GroupedBarGraph must have the same keys. Check the data prop" - ); - } - }); + const keys = Object.keys(propsData[0].values); + propsData.forEach((d: GroupedBarGraphData) => { + const currentKeys = Object.keys(d.values); + if ( + keys.length != currentKeys.length || + !keys.every((key: string) => currentKeys.includes(key)) + ) { + throw new Error( + "Every category in a GroupedBarGraph must have the same keys. Check the data prop" + ); + } + }); - const allValues = propsData - .map((d: GroupedBarGraphData) => Object.values(d.values)) - .flat(); + const allValues = propsData + .map((d: GroupedBarGraphData) => Object.values(d.values)) + .flat(); - 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: BarGroupData) => d.category; + const getCategory = (d: BarGroupData) => d.category; - const categoryScale = scaleBand({ - domain: data.map(getCategory), - padding: BAR_PADDING, - }); + const categoryScale = scaleBand({ + domain: data.map(getCategory), + padding: BAR_PADDING, + }); - const keyScale = scaleBand({ - domain: keys, - }); + const keyScale = scaleBand({ + domain: keys, + }); - const valueScale = scaleLinear({ - domain: [0, Math.max(...allValues)], - }); + const valueScale = scaleLinear({ + domain: [0, Math.max(...allValues)], + }); - const colorScale = scaleOrdinal({ - domain: keys, - range: barColors, - }); + const colorScale = scaleOrdinal({ + domain: keys, + range: barColors, + }); - categoryScale.rangeRound([0, categoryMax]); - keyScale.rangeRound([0, categoryScale.bandwidth()]); - valueScale.rangeRound([valueMax, 0]); + categoryScale.rangeRound([0, categoryMax]); + keyScale.rangeRound([0, categoryScale.bandwidth()]); + valueScale.rangeRound([valueMax, 0]); - return ( -
-
- -
- - - {Object.keys(barHoverColorsMap).map((color: string) => { - // remove brackets from colour name to make ids work - const colorId = removeBrackets(color); - return ( - - - - ); - })} - - - - {data.map((d, idx) => { - const barName = `${getCategory(d)}-${idx}`; - const barWidth = categoryScale.bandwidth(); - const backgroundBarWidth = barWidth / (1 - BAR_PADDING); - return idx % 2 === 0 ? ( - - ) : null; - })} - - +
+ - - {(barGroups) => - barGroups.map((barGroup) => ( - - {barGroup.bars.map((bar) => ( - - ))} - - )) - } - - - { - 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`, - }} - /> - -
- ); -} +
+ + + {Object.keys(barHoverColorsMap).map((color: string) => { + // remove brackets from colour name to make ids work + const colorId = removeBrackets(color); + return ( + + + + ); + })} + + + + {data.map((d, idx) => { + const barName = `${getCategory(d)}-${idx}`; + const barWidth = categoryScale.bandwidth(); + const backgroundBarWidth = barWidth / (1 - BAR_PADDING); + return idx % 2 === 0 ? ( + + ) : null; + })} + + + + {(barGroups) => + barGroups.map((barGroup) => ( + + {barGroup.bars.map((bar) => ( + { + 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: bar.value.toString(), + tooltipTop: eventSvgCoords.y, + tooltipLeft: eventSvgCoords.x, + }); + }} + onMouseOut={hideTooltip} + key={`bar-group-bar-${barGroup.x0}-${barGroup.index}-${bar.key}-${bar.index}`} + bar={bar} + valueMax={valueMax} + hoverFillColor={barHoverColorsMap[bar.color]} + hoverLabelSize={hoverLabelSize} + /> + ))} + + )) + } + + { + 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`, + }} + /> + + -export function GroupedBarGraphHorizontal(props: GroupedBarGraphProps) { - const { + {tooltipOpen && ( + + )} + + ); + } +); + +export const GroupedBarGraphHorizontal = withTooltip< + GroupedBarGraphProps, + TooltipData +>( + ({ data: propsData, barColors, barHoverColorsMap, + width, height, margin, className, @@ -324,198 +366,227 @@ export function GroupedBarGraphHorizontal(props: GroupedBarGraphProps) { valueAxisLabelOffset = 0, itemMargin = "0 0 0 15px", defaultLabelDy = "0", - } = props; - const width = props.width < minWidth ? minWidth : props.width; // Ensuring graph's width >= minWidth + tooltipOpen, + tooltipLeft, + tooltipTop, + tooltipData, + hideTooltip, + showTooltip, + }) => { + width = width < minWidth ? minWidth : width; // Ensuring graph's width >= minWidth - const data: BarGroupData[] = propsData.map((datum: GroupedBarGraphData) => { - return { category: datum.category, ...datum.values }; - }); + const data: BarGroupData[] = propsData.map((datum: GroupedBarGraphData) => { + return { category: datum.category, ...datum.values }; + }); - const keys = Object.keys(propsData[0].values); - propsData.forEach((d: GroupedBarGraphData) => { - const currentKeys = Object.keys(d.values); - if ( - keys.length != currentKeys.length || - !keys.every((key: string) => currentKeys.includes(key)) - ) { - throw new Error( - "Every category in a GroupedBarGraph must have the same keys. Check the data prop" - ); - } - }); + const keys = Object.keys(propsData[0].values); + propsData.forEach((d: GroupedBarGraphData) => { + const currentKeys = Object.keys(d.values); + if ( + keys.length != currentKeys.length || + !keys.every((key: string) => currentKeys.includes(key)) + ) { + throw new Error( + "Every category in a GroupedBarGraph must have the same keys. Check the data prop" + ); + } + }); - const allValues = propsData - .map((d: GroupedBarGraphData) => Object.values(d.values)) - .flat(); + const allValues = propsData + .map((d: GroupedBarGraphData) => Object.values(d.values)) + .flat(); - 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: BarGroupData) => d.category; + const getCategory = (d: BarGroupData) => d.category; - const categoryScale = scaleBand({ - domain: data.map(getCategory), - padding: BAR_PADDING, - }); + const categoryScale = scaleBand({ + domain: data.map(getCategory), + padding: BAR_PADDING, + }); - const keyScale = scaleBand({ - domain: keys, - }); + const keyScale = scaleBand({ + domain: keys, + }); - const valueScale = scaleLinear({ - domain: [Math.max(...allValues), 0], - }); + const valueScale = scaleLinear({ + domain: [Math.max(...allValues), 0], + }); - const colorScale = scaleOrdinal({ - domain: keys, - range: barColors, - }); + const colorScale = scaleOrdinal({ + domain: keys, + range: barColors, + }); - categoryScale.rangeRound([0, categoryMax]); - keyScale.rangeRound([0, categoryScale.bandwidth()]); - valueScale.rangeRound([valueMax, 0]); + categoryScale.rangeRound([0, categoryMax]); + keyScale.rangeRound([0, categoryScale.bandwidth()]); + valueScale.rangeRound([valueMax, 0]); - return ( -
-
- -
- - - {Object.keys(barHoverColorsMap).map((color: string) => { - // remove brackets from colour name to make ids work - const colorId = removeBrackets(color); - return ( - - - - ); - })} - - - - {data.map((d, idx) => { - const barName = `${getCategory(d)}-${idx}`; - const barWidth = categoryScale.bandwidth(); - const backgroundBarWidth = barWidth / (1 - BAR_PADDING); - return idx % 2 === 0 ? ( - - ) : null; - })} - - +
+ - - {(barGroups) => - barGroups.map((barGroup) => ( - - {barGroup.bars.map((bar) => ( - - ))} - - )) - } - - - { - return { - ...leftTickLabelProps(), - className: styles.tickLabel, - dx: "-0.5rem", - dy: defaultLabelDy, - fontSize: `${valueTickLabelSize / 16}rem`, - height: categoryScale.bandwidth(), - }; - }} - label={categoryAxisLabel} - labelClassName={styles.axisLabel} - labelOffset={categoryAxisLabelOffset} - labelProps={{ - fontSize: `${categoryAxisLabelSize / 16}rem`, - }} - /> - { - return { - ...bottomTickLabelProps(), - className: styles.tickLabel, - dy: "-0.25rem", - fontSize: `${categoryTickLabelSize / 16}rem`, - verticalAnchor: "start", - }; - }} - label={valueAxisLabel} - labelClassName={styles.axisLabel} - labelOffset={valueAxisLabelOffset} - labelProps={{ - fontSize: `${valueAxisLabelSize / 16}rem`, - }} - /> - -
- ); -} +
+ + + {Object.keys(barHoverColorsMap).map((color: string) => { + // remove brackets from colour name to make ids work + const colorId = removeBrackets(color); + return ( + + + + ); + })} + + + + {data.map((d, idx) => { + const barName = `${getCategory(d)}-${idx}`; + const barWidth = categoryScale.bandwidth(); + const backgroundBarWidth = barWidth / (1 - BAR_PADDING); + return idx % 2 === 0 ? ( + + ) : null; + })} + + + + {(barGroups) => + barGroups.map((barGroup) => ( + + {barGroup.bars.map((bar) => ( + { + 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: bar.value.toString(), + tooltipTop: eventSvgCoords.y, + tooltipLeft: eventSvgCoords.x, + }); + }} + onMouseOut={hideTooltip} + key={`bar-group-bar-${barGroup.y0}-${barGroup.index}-${bar.key}-${bar.index}`} + bar={bar} + valueMax={valueMax} + hoverFillColor={barHoverColorsMap[bar.color]} + hoverLabelSize={hoverLabelSize} + isHorizontal + /> + ))} + + )) + } + + { + return { + ...leftTickLabelProps(), + className: styles.tickLabel, + dx: "-0.5rem", + dy: defaultLabelDy, + fontSize: `${valueTickLabelSize / 16}rem`, + height: categoryScale.bandwidth(), + }; + }} + label={categoryAxisLabel} + labelClassName={styles.axisLabel} + labelOffset={categoryAxisLabelOffset} + labelProps={{ + fontSize: `${categoryAxisLabelSize / 16}rem`, + }} + /> + { + return { + ...bottomTickLabelProps(), + className: styles.tickLabel, + dy: "-0.25rem", + fontSize: `${categoryTickLabelSize / 16}rem`, + verticalAnchor: "start", + }; + }} + label={valueAxisLabel} + labelClassName={styles.axisLabel} + labelOffset={valueAxisLabelOffset} + labelProps={{ + fontSize: `${valueAxisLabelSize / 16}rem`, + }} + /> + + + + {tooltipOpen && ( + + )} + + ); + } +); interface HoverableBarProps { bar: BarGroupBarType; @@ -523,16 +594,12 @@ interface HoverableBarProps { hoverFillColor?: string; hoverLabelSize?: number; isHorizontal?: boolean; + onMouseMove?: (e: React.MouseEvent) => void; + onMouseOut?: () => void; } function HoverableBar(props: HoverableBarProps) { - const { - bar, - valueMax, - hoverLabelSize, - hoverFillColor, - isHorizontal = false, - } = props; + const { bar, hoverFillColor, onMouseMove, onMouseOut } = props; const [isHovered, setIsHovered] = useState(false); @@ -549,6 +616,8 @@ function HoverableBar(props: HoverableBarProps) { }} > - - {bar.value} - ); } diff --git a/components/PieChart.tsx b/components/PieChart.tsx index 536ccfc..20ee93e 100644 --- a/components/PieChart.tsx +++ b/components/PieChart.tsx @@ -1,8 +1,13 @@ +import { localPoint } from "@visx/event"; import { Group } from "@visx/group"; +import { Point } from "@visx/point"; import Pie, { ProvidedProps } from "@visx/shape/lib/shapes/Pie"; import { Text } from "@visx/text"; +import { withTooltip } from "@visx/tooltip"; import React from "react"; +import { TooltipWrapper } from "./TooltipWrapper"; + import styles from "./PieChart.module.css"; interface PieChartProps { @@ -39,105 +44,107 @@ interface PieChartData { value: number; } -export function PieChart({ - data, - width, - labelWidth, - padRadius = width * 0.35, - innerRadius = width * 0.015, - pieTextSize = 40, - pieTextXOffset = 0, - pieTextYOffset = 10, - getPieDisplayValueFromDatum = (datum: PieChartData) => `${datum.value}%`, - labelTextSize = 40, - labelTextXOffset = 0, - labelTextYOffset = 0, - getLabelDisplayValueFromDatum = (datum: PieChartData) => `${datum.category}`, - className, -}: PieChartProps) { - const pieWidth = width * 0.5 - labelWidth; - return ( - - - d.value} - cornerRadius={10} - padAngle={0.075} - padRadius={padRadius} - innerRadius={innerRadius} - outerRadius={pieWidth} - > - {(pie) => ( - - )} - - d.value} - innerRadius={pieWidth} - outerRadius={width * 0.5} - > - {(pie) => ( - - )} - - - - ); -} - -type PieSliceProps = ProvidedProps & { - pieTextSize: number; - pieTextXOffset: number; - pieTextYOffset: number; - getPieDisplayValueFromDatum: (datum: PieChartData) => string; -}; - -export function PieSlice({ - path, - arcs, - pieTextSize, - pieTextXOffset, - pieTextYOffset, - getPieDisplayValueFromDatum, -}: PieSliceProps) { - return ( - <> - {arcs.map((arc) => { - const [centroidX, centroidY] = path.centroid(arc); - const pathArc = path(arc) as string; - - return ( - - - ( + ({ + data, + width, + labelWidth, + padRadius = width * 0.35, + innerRadius = width * 0.015, + pieTextSize = 40, + pieTextXOffset = 0, + pieTextYOffset = 10, + getPieDisplayValueFromDatum = (datum: PieChartData) => `${datum.value}%`, + labelTextSize = 40, + labelTextXOffset = 0, + labelTextYOffset = 0, + getLabelDisplayValueFromDatum = (datum: PieChartData) => + `${datum.category}`, + className, + tooltipOpen, + tooltipLeft, + tooltipTop, + tooltipData, + hideTooltip, + showTooltip, + }) => { + const pieWidth = width * 0.5 - labelWidth; + return ( +
+ + + d.value} + cornerRadius={10} + padAngle={0.075} + padRadius={padRadius} + innerRadius={innerRadius} + outerRadius={pieWidth} > - {`${getPieDisplayValueFromDatum(arc.data)}`} - + {({ arcs, path }) => { + return arcs.map((arc) => { + const [centroidX, centroidY] = path.centroid(arc); + const pathArc = path(arc) as string; + 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: `${arc.data.category}: ${arc.data.value}%`, + tooltipTop: eventSvgCoords.y, + tooltipLeft: eventSvgCoords.x, + }); + }} + onMouseOut={hideTooltip} + className={styles.piePath} + d={pathArc} + /> + + ); + }); + }} + + d.value} + innerRadius={pieWidth} + outerRadius={width * 0.5} + > + {(pie) => ( + + )} + - ); - })} - - ); -} + + + {tooltipOpen && ( + + )} +
+ ); + } +); type PieSliceLabelProps = ProvidedProps & { labelTextSize: number;