This commit is contained in:
e26chiu 2022-12-21 21:25:47 -05:00
commit b837032ca0
5 changed files with 858 additions and 781 deletions

View File

@ -6,23 +6,11 @@
fill: var(--primary-accent-light); fill: var(--primary-accent-light);
} }
.barText {
visibility: hidden;
font-family: "Inconsolata", monospace;
font-weight: 800;
fill: var(--label);
}
.barGroup:hover .bar { .barGroup:hover .bar {
fill: var(--primary-accent); fill: var(--primary-accent);
filter: drop-shadow(0 0 calc(4rem / 16) var(--primary-accent)); filter: drop-shadow(0 0 calc(4rem / 16) var(--primary-accent));
} }
.barGroup:hover .barText {
visibility: visible;
}
.tickLabel { .tickLabel {
font-family: "Inconsolata", monospace; font-family: "Inconsolata", monospace;
font-weight: 800; font-weight: 800;

View File

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

View File

@ -9,18 +9,6 @@
fill: var(--card-background); fill: var(--card-background);
} }
.barText {
visibility: hidden;
font-family: "Inconsolata", monospace;
font-weight: 800;
fill: var(--label);
}
.singleBar:hover .barText {
visibility: visible;
}
.tickLabel { .tickLabel {
font-family: "Inconsolata", monospace; font-family: "Inconsolata", monospace;
font-weight: 800; font-weight: 800;

View File

@ -1,16 +1,20 @@
import { AxisBottom, AxisLeft } from "@visx/axis"; import { AxisBottom, AxisLeft } from "@visx/axis";
import { bottomTickLabelProps } from "@visx/axis/lib/axis/AxisBottom"; import { bottomTickLabelProps } from "@visx/axis/lib/axis/AxisBottom";
import { leftTickLabelProps } from "@visx/axis/lib/axis/AxisLeft"; import { leftTickLabelProps } from "@visx/axis/lib/axis/AxisLeft";
import { localPoint } from "@visx/event";
import { GridColumns, GridRows } from "@visx/grid"; import { GridColumns, GridRows } from "@visx/grid";
import { Group } from "@visx/group"; import { Group } from "@visx/group";
import { LegendOrdinal } from "@visx/legend"; import { LegendOrdinal } from "@visx/legend";
import { Point } from "@visx/point";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale"; import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { Bar, BarGroup, BarGroupHorizontal } from "@visx/shape"; import { Bar, BarGroup, BarGroupHorizontal } from "@visx/shape";
import { BarGroupBar as BarGroupBarType } from "@visx/shape/lib/types"; 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 React, { useState } from "react";
import { Color } from "utils/Color"; import { Color } from "utils/Color";
import { TooltipWrapper } from "./TooltipWrapper";
import styles from "./GroupedBarGraph.module.css"; import styles from "./GroupedBarGraph.module.css";
interface GroupedBarGraphProps { interface GroupedBarGraphProps {
@ -79,15 +83,20 @@ interface BarGroupData {
// BAR_PADDING must be in the range [0, 1) // BAR_PADDING must be in the range [0, 1)
const BAR_PADDING = 0.2; const BAR_PADDING = 0.2;
const BAR_TEXT_PADDING = 12;
const DEFAULT_LABEL_SIZE = 16; const DEFAULT_LABEL_SIZE = 16;
export function GroupedBarGraphVertical(props: GroupedBarGraphProps) { type TooltipData = string;
const {
export const GroupedBarGraphVertical = withTooltip<
GroupedBarGraphProps,
TooltipData
>(
({
data: propsData, data: propsData,
barColors, barColors,
barHoverColorsMap, barHoverColorsMap,
width,
height, height,
margin, margin,
className, className,
@ -106,209 +115,242 @@ export function GroupedBarGraphVertical(props: GroupedBarGraphProps) {
alternatingLabelSpace = 80, alternatingLabelSpace = 80,
defaultLabelDy = `0px`, defaultLabelDy = `0px`,
lowerLabelDy = `30px`, lowerLabelDy = `30px`,
} = props; tooltipOpen,
const width = props.width < minWidth ? minWidth : props.width; // Ensuring graph's width >= minWidth tooltipLeft,
const alternatingLabel = width <= widthAlternatingLabel; tooltipTop,
const final_margin_bottom = alternatingLabel tooltipData,
? margin.bottom + alternatingLabelSpace hideTooltip,
: margin.bottom; 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) => { const data: BarGroupData[] = propsData.map((datum: GroupedBarGraphData) => {
return { category: datum.category, ...datum.values }; return { category: datum.category, ...datum.values };
}); });
const keys = Object.keys(propsData[0].values); const keys = Object.keys(propsData[0].values);
propsData.forEach((d: GroupedBarGraphData) => { propsData.forEach((d: GroupedBarGraphData) => {
const currentKeys = Object.keys(d.values); const currentKeys = Object.keys(d.values);
if ( if (
keys.length != currentKeys.length || keys.length != currentKeys.length ||
!keys.every((key: string) => currentKeys.includes(key)) !keys.every((key: string) => currentKeys.includes(key))
) { ) {
throw new Error( throw new Error(
"Every category in a GroupedBarGraph must have the same keys. Check the data prop" "Every category in a GroupedBarGraph must have the same keys. Check the data prop"
); );
} }
}); });
const allValues = propsData const allValues = propsData
.map((d: GroupedBarGraphData) => Object.values(d.values)) .map((d: GroupedBarGraphData) => Object.values(d.values))
.flat(); .flat();
const categoryMax = width - margin.left - margin.right; const categoryMax = width - margin.left - margin.right;
const valueMax = height - margin.top - final_margin_bottom; const valueMax = height - margin.top - final_margin_bottom;
const getCategory = (d: BarGroupData) => d.category; const getCategory = (d: BarGroupData) => d.category;
const categoryScale = scaleBand({ const categoryScale = scaleBand({
domain: data.map(getCategory), domain: data.map(getCategory),
padding: BAR_PADDING, padding: BAR_PADDING,
}); });
const keyScale = scaleBand({ const keyScale = scaleBand({
domain: keys, domain: keys,
}); });
const valueScale = scaleLinear<number>({ const valueScale = scaleLinear<number>({
domain: [0, Math.max(...allValues)], domain: [0, Math.max(...allValues)],
}); });
const colorScale = scaleOrdinal<string, string>({ const colorScale = scaleOrdinal<string, string>({
domain: keys, domain: keys,
range: barColors, range: barColors,
}); });
categoryScale.rangeRound([0, categoryMax]); categoryScale.rangeRound([0, categoryMax]);
keyScale.rangeRound([0, categoryScale.bandwidth()]); keyScale.rangeRound([0, categoryScale.bandwidth()]);
valueScale.rangeRound([valueMax, 0]); valueScale.rangeRound([valueMax, 0]);
return ( return (
<div <div
className={className ? `${className} ${styles.wrapper}` : styles.wrapper} className={
> className ? `${className} ${styles.wrapper}` : styles.wrapper
<div className={styles.legend}> }
<LegendOrdinal >
scale={colorScale} <div className={styles.legend}>
direction="row" <LegendOrdinal
itemMargin={itemMargin} scale={colorScale}
labelAlign="center" direction="row"
/> itemMargin={itemMargin}
</div> labelAlign="center"
<svg width={width} height={height}>
<defs>
{Object.keys(barHoverColorsMap).map((color: string) => {
// remove brackets from colour name to make ids work
const colorId = removeBrackets(color);
return (
<filter key={`glow-${color}`} id={`glow-${colorId}`}>
<feDropShadow
dx="0"
dy="0"
stdDeviation="4"
floodColor={barHoverColorsMap[color]}
/>
</filter>
);
})}
</defs>
<Group top={margin.top} left={margin.left}>
<Group>
{data.map((d, idx) => {
const barName = `${getCategory(d)}-${idx}`;
const barWidth = categoryScale.bandwidth();
const backgroundBarWidth = barWidth / (1 - BAR_PADDING);
return idx % 2 === 0 ? (
<Bar
className={styles.barBackground}
key={`bar-${barName}-background`}
x={
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
categoryScale(getCategory(d))! -
(backgroundBarWidth - barWidth) / 2
}
y={0}
width={backgroundBarWidth}
height={valueMax}
/>
) : null;
})}
</Group>
<GridRows
scale={valueScale}
width={categoryMax}
numTicks={5}
stroke={Color.label}
strokeWidth={4}
strokeDasharray="10"
strokeLinecap="round"
/> />
<BarGroup </div>
data={data} <svg width={width} height={height}>
keys={keys} <defs>
height={valueMax} {Object.keys(barHoverColorsMap).map((color: string) => {
x0={getCategory} // remove brackets from colour name to make ids work
x0Scale={categoryScale} const colorId = removeBrackets(color);
x1Scale={keyScale} return (
yScale={valueScale} <filter key={`glow-${color}`} id={`glow-${colorId}`}>
color={colorScale} <feDropShadow
> dx="0"
{(barGroups) => dy="0"
barGroups.map((barGroup) => ( stdDeviation="4"
<Group floodColor={barHoverColorsMap[color]}
key={`bar-group-${barGroup.x0}-${barGroup.index}`} />
left={barGroup.x0} </filter>
> );
{barGroup.bars.map((bar) => ( })}
<HoverableBar </defs>
key={`bar-group-bar-${barGroup.x0}-${barGroup.index}-${bar.key}-${bar.index}`} <Group top={margin.top} left={margin.left}>
bar={bar} <Group>
valueMax={valueMax} {data.map((d, idx) => {
hoverFillColor={barHoverColorsMap[bar.color]} const barName = `${getCategory(d)}-${idx}`;
hoverLabelSize={hoverLabelSize} const barWidth = categoryScale.bandwidth();
/> const backgroundBarWidth = barWidth / (1 - BAR_PADDING);
))} return idx % 2 === 0 ? (
</Group> <Bar
)) className={styles.barBackground}
} key={`bar-${barName}-background`}
</BarGroup> x={
</Group> // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
<AxisBottom categoryScale(getCategory(d))! -
scale={categoryScale} (backgroundBarWidth - barWidth) / 2
top={valueMax + margin.top} }
left={margin.left} y={0}
hideAxisLine width={backgroundBarWidth}
hideTicks height={valueMax}
tickLabelProps={(value, index) => { />
const alternatingDy = ) : null;
index % 2 == 0 ? defaultLabelDy : lowerLabelDy; })}
return { </Group>
...bottomTickLabelProps(), <GridRows
className: styles.tickLabel, scale={valueScale}
dy: alternatingLabel ? alternatingDy : defaultLabelDy, width={categoryMax}
fontSize: `${categoryTickLabelSize / 16}rem`, numTicks={5}
width: categoryScale.bandwidth(), stroke={Color.label}
verticalAnchor: "start", strokeWidth={4}
}; strokeDasharray="10"
}} strokeLinecap="round"
label={categoryAxisLabel} />
labelClassName={styles.axisLabel} <BarGroup
labelOffset={categoryAxisLabelOffset} data={data}
labelProps={{ keys={keys}
fontSize: `${categoryAxisLabelSize / 16}rem`, height={valueMax}
}} x0={getCategory}
/> x0Scale={categoryScale}
<AxisLeft x1Scale={keyScale}
scale={valueScale} yScale={valueScale}
top={margin.top} color={colorScale}
left={margin.left} >
hideAxisLine {(barGroups) =>
hideTicks barGroups.map((barGroup) => (
numTicks={5} <Group
tickLabelProps={() => { key={`bar-group-${barGroup.x0}-${barGroup.index}`}
return { left={barGroup.x0}
...leftTickLabelProps(), >
className: styles.tickLabel, {barGroup.bars.map((bar) => (
dx: "-0.5rem", <HoverableBar
dy: "0.25rem", onMouseMove={(e) => {
fontSize: `${valueTickLabelSize / 16}rem`, const eventSvgCoords = localPoint(
}; // ownerSVGElement is given by visx docs but not recognized by typescript
}} // eslint-disable-next-line @typescript-eslint/ban-ts-comment
label={valueAxisLabel} // @ts-ignore
labelClassName={styles.axisLabel} e.target.ownerSVGElement as Element,
labelOffset={valueAxisLabelOffset} e
labelProps={{ ) as Point;
fontSize: `${valueAxisLabelSize / 16}rem`, showTooltip({
}} tooltipData: bar.value.toString(),
/> tooltipTop: eventSvgCoords.y,
</svg> tooltipLeft: eventSvgCoords.x,
</div> });
); }}
} onMouseOut={hideTooltip}
key={`bar-group-bar-${barGroup.x0}-${barGroup.index}-${bar.key}-${bar.index}`}
bar={bar}
valueMax={valueMax}
hoverFillColor={barHoverColorsMap[bar.color]}
hoverLabelSize={hoverLabelSize}
/>
))}
</Group>
))
}
</BarGroup>
<AxisBottom
scale={categoryScale}
top={valueMax}
hideAxisLine
hideTicks
tickLabelProps={(value, index) => {
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`,
}}
/>
<AxisLeft
scale={valueScale}
hideAxisLine
hideTicks
numTicks={5}
tickLabelProps={() => {
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`,
}}
/>
</Group>
</svg>
export function GroupedBarGraphHorizontal(props: GroupedBarGraphProps) { {tooltipOpen && (
const { <TooltipWrapper
top={tooltipTop}
left={tooltipLeft}
header={tooltipData as string}
></TooltipWrapper>
)}
</div>
);
}
);
export const GroupedBarGraphHorizontal = withTooltip<
GroupedBarGraphProps,
TooltipData
>(
({
data: propsData, data: propsData,
barColors, barColors,
barHoverColorsMap, barHoverColorsMap,
width,
height, height,
margin, margin,
className, className,
@ -324,198 +366,227 @@ export function GroupedBarGraphHorizontal(props: GroupedBarGraphProps) {
valueAxisLabelOffset = 0, valueAxisLabelOffset = 0,
itemMargin = "0 0 0 15px", itemMargin = "0 0 0 15px",
defaultLabelDy = "0", defaultLabelDy = "0",
} = props; tooltipOpen,
const width = props.width < minWidth ? minWidth : props.width; // Ensuring graph's width >= minWidth tooltipLeft,
tooltipTop,
tooltipData,
hideTooltip,
showTooltip,
}) => {
width = width < minWidth ? minWidth : width; // Ensuring graph's width >= minWidth
const data: BarGroupData[] = propsData.map((datum: GroupedBarGraphData) => { const data: BarGroupData[] = propsData.map((datum: GroupedBarGraphData) => {
return { category: datum.category, ...datum.values }; return { category: datum.category, ...datum.values };
}); });
const keys = Object.keys(propsData[0].values); const keys = Object.keys(propsData[0].values);
propsData.forEach((d: GroupedBarGraphData) => { propsData.forEach((d: GroupedBarGraphData) => {
const currentKeys = Object.keys(d.values); const currentKeys = Object.keys(d.values);
if ( if (
keys.length != currentKeys.length || keys.length != currentKeys.length ||
!keys.every((key: string) => currentKeys.includes(key)) !keys.every((key: string) => currentKeys.includes(key))
) { ) {
throw new Error( throw new Error(
"Every category in a GroupedBarGraph must have the same keys. Check the data prop" "Every category in a GroupedBarGraph must have the same keys. Check the data prop"
); );
} }
}); });
const allValues = propsData const allValues = propsData
.map((d: GroupedBarGraphData) => Object.values(d.values)) .map((d: GroupedBarGraphData) => Object.values(d.values))
.flat(); .flat();
const categoryMax = height - margin.top - margin.bottom; const categoryMax = height - margin.top - margin.bottom;
const valueMax = width - margin.left - margin.right; const valueMax = width - margin.left - margin.right;
const getCategory = (d: BarGroupData) => d.category; const getCategory = (d: BarGroupData) => d.category;
const categoryScale = scaleBand({ const categoryScale = scaleBand({
domain: data.map(getCategory), domain: data.map(getCategory),
padding: BAR_PADDING, padding: BAR_PADDING,
}); });
const keyScale = scaleBand({ const keyScale = scaleBand({
domain: keys, domain: keys,
}); });
const valueScale = scaleLinear<number>({ const valueScale = scaleLinear<number>({
domain: [Math.max(...allValues), 0], domain: [Math.max(...allValues), 0],
}); });
const colorScale = scaleOrdinal<string, string>({ const colorScale = scaleOrdinal<string, string>({
domain: keys, domain: keys,
range: barColors, range: barColors,
}); });
categoryScale.rangeRound([0, categoryMax]); categoryScale.rangeRound([0, categoryMax]);
keyScale.rangeRound([0, categoryScale.bandwidth()]); keyScale.rangeRound([0, categoryScale.bandwidth()]);
valueScale.rangeRound([valueMax, 0]); valueScale.rangeRound([valueMax, 0]);
return ( return (
<div <div
className={className ? `${className} ${styles.wrapper}` : styles.wrapper} className={
> className ? `${className} ${styles.wrapper}` : styles.wrapper
<div className={styles.legend}> }
<LegendOrdinal >
scale={colorScale} <div className={styles.legend}>
direction="row" <LegendOrdinal
itemMargin={itemMargin} scale={colorScale}
labelAlign="center" direction="row"
/> itemMargin={itemMargin}
</div> labelAlign="center"
<svg width={width} height={height}>
<defs>
{Object.keys(barHoverColorsMap).map((color: string) => {
// remove brackets from colour name to make ids work
const colorId = removeBrackets(color);
return (
<filter key={`glow-${color}`} id={`glow-${colorId}`}>
<feDropShadow
dx="0"
dy="0"
stdDeviation="4"
floodColor={barHoverColorsMap[color]}
/>
</filter>
);
})}
</defs>
<Group top={margin.top} left={margin.left}>
<Group>
{data.map((d, idx) => {
const barName = `${getCategory(d)}-${idx}`;
const barWidth = categoryScale.bandwidth();
const backgroundBarWidth = barWidth / (1 - BAR_PADDING);
return idx % 2 === 0 ? (
<Bar
className={styles.barBackground}
key={`bar-${barName}-background`}
x={0}
y={
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
categoryScale(getCategory(d))! -
(backgroundBarWidth - barWidth) / 2
}
width={valueMax}
height={backgroundBarWidth}
/>
) : null;
})}
</Group>
<GridColumns
scale={valueScale}
height={categoryMax}
numTicks={5}
stroke={Color.label}
strokeWidth={4}
strokeDasharray="10"
strokeLinecap="round"
/> />
<BarGroupHorizontal </div>
data={data} <svg width={width} height={height}>
keys={keys} <defs>
width={valueMax} {Object.keys(barHoverColorsMap).map((color: string) => {
y0={getCategory} // remove brackets from colour name to make ids work
y0Scale={categoryScale} const colorId = removeBrackets(color);
y1Scale={keyScale} return (
xScale={valueScale} <filter key={`glow-${color}`} id={`glow-${colorId}`}>
color={colorScale} <feDropShadow
> dx="0"
{(barGroups) => dy="0"
barGroups.map((barGroup) => ( stdDeviation="4"
<Group floodColor={barHoverColorsMap[color]}
key={`bar-group-${barGroup.y0}-${barGroup.index}`} />
top={barGroup.y0} </filter>
> );
{barGroup.bars.map((bar) => ( })}
<HoverableBar </defs>
key={`bar-group-bar-${barGroup.y0}-${barGroup.index}-${bar.key}-${bar.index}`} <Group top={margin.top} left={margin.left}>
bar={bar} <Group>
valueMax={valueMax} {data.map((d, idx) => {
hoverFillColor={barHoverColorsMap[bar.color]} const barName = `${getCategory(d)}-${idx}`;
hoverLabelSize={hoverLabelSize} const barWidth = categoryScale.bandwidth();
isHorizontal const backgroundBarWidth = barWidth / (1 - BAR_PADDING);
/> return idx % 2 === 0 ? (
))} <Bar
</Group> className={styles.barBackground}
)) key={`bar-${barName}-background`}
} x={0}
</BarGroupHorizontal> y={
</Group> // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
<AxisLeft categoryScale(getCategory(d))! -
scale={categoryScale} (backgroundBarWidth - barWidth) / 2
top={margin.top} }
left={margin.left} width={valueMax}
hideAxisLine height={backgroundBarWidth}
hideTicks />
tickLabelProps={() => { ) : null;
return { })}
...leftTickLabelProps(), </Group>
className: styles.tickLabel, <GridColumns
dx: "-0.5rem", scale={valueScale}
dy: defaultLabelDy, height={categoryMax}
fontSize: `${valueTickLabelSize / 16}rem`, numTicks={5}
height: categoryScale.bandwidth(), stroke={Color.label}
}; strokeWidth={4}
}} strokeDasharray="10"
label={categoryAxisLabel} strokeLinecap="round"
labelClassName={styles.axisLabel} />
labelOffset={categoryAxisLabelOffset} <BarGroupHorizontal
labelProps={{ data={data}
fontSize: `${categoryAxisLabelSize / 16}rem`, keys={keys}
}} width={valueMax}
/> y0={getCategory}
<AxisBottom y0Scale={categoryScale}
scale={valueScale} y1Scale={keyScale}
top={categoryMax + margin.top} xScale={valueScale}
left={margin.left} color={colorScale}
hideAxisLine >
hideTicks {(barGroups) =>
numTicks={5} barGroups.map((barGroup) => (
tickLabelProps={() => { <Group
return { key={`bar-group-${barGroup.y0}-${barGroup.index}`}
...bottomTickLabelProps(), top={barGroup.y0}
className: styles.tickLabel, >
dy: "-0.25rem", {barGroup.bars.map((bar) => (
fontSize: `${categoryTickLabelSize / 16}rem`, <HoverableBar
verticalAnchor: "start", onMouseMove={(e) => {
}; const eventSvgCoords = localPoint(
}} // ownerSVGElement is given by visx docs but not recognized by typescript
label={valueAxisLabel} // eslint-disable-next-line @typescript-eslint/ban-ts-comment
labelClassName={styles.axisLabel} // @ts-ignore
labelOffset={valueAxisLabelOffset} e.target.ownerSVGElement as Element,
labelProps={{ e
fontSize: `${valueAxisLabelSize / 16}rem`, ) as Point;
}} showTooltip({
/> tooltipData: bar.value.toString(),
</svg> tooltipTop: eventSvgCoords.y,
</div> 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
/>
))}
</Group>
))
}
</BarGroupHorizontal>
<AxisLeft
scale={categoryScale}
hideAxisLine
hideTicks
tickLabelProps={() => {
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`,
}}
/>
<AxisBottom
scale={valueScale}
top={categoryMax}
hideAxisLine
hideTicks
numTicks={5}
tickLabelProps={() => {
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`,
}}
/>
</Group>
</svg>
{tooltipOpen && (
<TooltipWrapper
top={tooltipTop}
left={tooltipLeft}
header={tooltipData as string}
></TooltipWrapper>
)}
</div>
);
}
);
interface HoverableBarProps { interface HoverableBarProps {
bar: BarGroupBarType<string>; bar: BarGroupBarType<string>;
@ -523,16 +594,12 @@ interface HoverableBarProps {
hoverFillColor?: string; hoverFillColor?: string;
hoverLabelSize?: number; hoverLabelSize?: number;
isHorizontal?: boolean; isHorizontal?: boolean;
onMouseMove?: (e: React.MouseEvent<SVGPathElement, MouseEvent>) => void;
onMouseOut?: () => void;
} }
function HoverableBar(props: HoverableBarProps) { function HoverableBar(props: HoverableBarProps) {
const { const { bar, hoverFillColor, onMouseMove, onMouseOut } = props;
bar,
valueMax,
hoverLabelSize,
hoverFillColor,
isHorizontal = false,
} = props;
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
@ -549,6 +616,8 @@ function HoverableBar(props: HoverableBarProps) {
}} }}
> >
<Bar <Bar
onMouseMove={onMouseMove}
onMouseOut={onMouseOut}
x={bar.x} x={bar.x}
y={bar.y} y={bar.y}
width={bar.width} width={bar.width}
@ -557,22 +626,6 @@ function HoverableBar(props: HoverableBarProps) {
// apply the glow effect when the bar is hovered // apply the glow effect when the bar is hovered
filter={isHovered ? `url(#glow-${colorId})` : undefined} filter={isHovered ? `url(#glow-${colorId})` : undefined}
/> />
<Text
className={styles.barText}
x={isHorizontal ? bar.width - BAR_TEXT_PADDING : bar.x + bar.width / 2}
y={
isHorizontal
? bar.y + bar.height / 2
: valueMax - bar.height + BAR_TEXT_PADDING
}
fontSize={
hoverLabelSize ?? (isHorizontal ? bar.height : bar.width) * 0.5
}
textAnchor={isHorizontal ? "end" : "middle"}
verticalAnchor={isHorizontal ? "middle" : "start"}
>
{bar.value}
</Text>
</Group> </Group>
); );
} }

View File

@ -1,8 +1,13 @@
import { localPoint } from "@visx/event";
import { Group } from "@visx/group"; import { Group } from "@visx/group";
import { Point } from "@visx/point";
import Pie, { ProvidedProps } from "@visx/shape/lib/shapes/Pie"; import Pie, { ProvidedProps } from "@visx/shape/lib/shapes/Pie";
import { Text } from "@visx/text"; import { Text } from "@visx/text";
import { withTooltip } from "@visx/tooltip";
import React from "react"; import React from "react";
import { TooltipWrapper } from "./TooltipWrapper";
import styles from "./PieChart.module.css"; import styles from "./PieChart.module.css";
interface PieChartProps { interface PieChartProps {
@ -39,105 +44,107 @@ interface PieChartData {
value: number; value: number;
} }
export function PieChart({ export const PieChart = withTooltip<PieChartProps>(
data, ({
width, data,
labelWidth, width,
padRadius = width * 0.35, labelWidth,
innerRadius = width * 0.015, padRadius = width * 0.35,
pieTextSize = 40, innerRadius = width * 0.015,
pieTextXOffset = 0, pieTextSize = 40,
pieTextYOffset = 10, pieTextXOffset = 0,
getPieDisplayValueFromDatum = (datum: PieChartData) => `${datum.value}%`, pieTextYOffset = 10,
labelTextSize = 40, getPieDisplayValueFromDatum = (datum: PieChartData) => `${datum.value}%`,
labelTextXOffset = 0, labelTextSize = 40,
labelTextYOffset = 0, labelTextXOffset = 0,
getLabelDisplayValueFromDatum = (datum: PieChartData) => `${datum.category}`, labelTextYOffset = 0,
className, getLabelDisplayValueFromDatum = (datum: PieChartData) =>
}: PieChartProps) { `${datum.category}`,
const pieWidth = width * 0.5 - labelWidth; className,
return ( tooltipOpen,
<svg className={className} width={width} height={width}> tooltipLeft,
<Group top={width * 0.5} left={width * 0.5}> tooltipTop,
<Pie tooltipData,
data={data} hideTooltip,
pieValue={(d: PieChartData) => d.value} showTooltip,
cornerRadius={10} }) => {
padAngle={0.075} const pieWidth = width * 0.5 - labelWidth;
padRadius={padRadius} return (
innerRadius={innerRadius} <div>
outerRadius={pieWidth} <svg className={className} width={width} height={width}>
> <Group top={width * 0.5} left={width * 0.5}>
{(pie) => ( <Pie
<PieSlice data={data}
{...pie} pieValue={(d: PieChartData) => d.value}
pieTextSize={pieTextSize} cornerRadius={10}
pieTextXOffset={pieTextXOffset} padAngle={0.075}
pieTextYOffset={pieTextYOffset} padRadius={padRadius}
getPieDisplayValueFromDatum={getPieDisplayValueFromDatum} innerRadius={innerRadius}
/> outerRadius={pieWidth}
)}
</Pie>
<Pie
data={data}
pieValue={(d: PieChartData) => d.value}
innerRadius={pieWidth}
outerRadius={width * 0.5}
>
{(pie) => (
<PieSliceLabel
{...pie}
labelTextSize={labelTextSize}
labelTextXOffset={labelTextXOffset}
labelTextYOffset={labelTextYOffset}
getLabelDisplayValueFromDatum={getLabelDisplayValueFromDatum}
/>
)}
</Pie>
</Group>
</svg>
);
}
type PieSliceProps<PieChartData> = ProvidedProps<PieChartData> & {
pieTextSize: number;
pieTextXOffset: number;
pieTextYOffset: number;
getPieDisplayValueFromDatum: (datum: PieChartData) => string;
};
export function PieSlice({
path,
arcs,
pieTextSize,
pieTextXOffset,
pieTextYOffset,
getPieDisplayValueFromDatum,
}: PieSliceProps<PieChartData>) {
return (
<>
{arcs.map((arc) => {
const [centroidX, centroidY] = path.centroid(arc);
const pathArc = path(arc) as string;
return (
<Group className={styles.group} key={`arc-${arc.data.category}`}>
<path className={styles.piePath} d={pathArc} />
<Text
className={styles.pieText}
x={centroidX + pieTextXOffset}
y={centroidY + pieTextYOffset}
textAnchor="middle"
fontSize={pieTextSize}
> >
{`${getPieDisplayValueFromDatum(arc.data)}`} {({ arcs, path }) => {
</Text> return arcs.map((arc) => {
const [centroidX, centroidY] = path.centroid(arc);
const pathArc = path(arc) as string;
return (
<Group
className={styles.group}
key={`arc-${arc.data.category}`}
>
<path
onMouseMove={(e) => {
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}
/>
</Group>
);
});
}}
</Pie>
<Pie
data={data}
pieValue={(d: PieChartData) => d.value}
innerRadius={pieWidth}
outerRadius={width * 0.5}
>
{(pie) => (
<PieSliceLabel
{...pie}
labelTextSize={labelTextSize}
labelTextXOffset={labelTextXOffset}
labelTextYOffset={labelTextYOffset}
getLabelDisplayValueFromDatum={getLabelDisplayValueFromDatum}
/>
)}
</Pie>
</Group> </Group>
); </svg>
})}
</> {tooltipOpen && (
); <TooltipWrapper
} top={tooltipTop}
left={tooltipLeft}
header={tooltipData as string}
></TooltipWrapper>
)}
</div>
);
}
);
type PieSliceLabelProps<PieChartData> = ProvidedProps<PieChartData> & { type PieSliceLabelProps<PieChartData> = ProvidedProps<PieChartData> & {
labelTextSize: number; labelTextSize: number;