cs-2022-class-profile/components/BarGraph.tsx

247 lines
7.0 KiB
TypeScript

import { AxisBottom, AxisLeft } from "@visx/axis";
import { bottomTickLabelProps } from "@visx/axis/lib/axis/AxisBottom";
import { leftTickLabelProps } from "@visx/axis/lib/axis/AxisLeft";
import { GridColumns, GridRows } from "@visx/grid";
import { Group } from "@visx/group";
import { scaleBand, scaleLinear } from "@visx/scale";
import { Bar } from "@visx/shape";
import { Text } from "@visx/text";
import React, { useState } from "react";
// TODO: refine props for styles
interface BarGraphProps {
data: BarGraphData[];
width: number;
height: number;
margin: {
top: number;
bottom: number;
left: number;
right: number;
};
className?: string;
}
interface BarGraphData {
category: string;
value: number;
}
const COLOURS = {
salmon: "#ffcad0",
navy: "#2c3651",
white: "#ffffff",
pink: "#ef83b1",
darkpink: "#cc5773",
};
// TODO: styling, possibly refactor all into one component
export function BarGraphVertical(props: BarGraphProps) {
const { width, height, margin, data, className } = props;
const categoryMax = width - margin.left - margin.right;
const valueMax = height - margin.top - margin.bottom;
const getCategory = (d: BarGraphData) => d.category;
const getValue = (d: BarGraphData) => d.value;
const categoryScale = scaleBand({
range: [0, categoryMax],
domain: data.map(getCategory),
padding: 0.1,
});
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));
return (
<svg className={className} width={width} height={height}>
<GridRows
scale={valueScale}
width={categoryMax}
top={margin.top}
left={margin.left}
/>
<Group top={margin.top} left={margin.left}>
{data.map((d) => {
const barHeight = valueMax - valuePoint(d);
return (
<Bar
key={`bar-${getCategory(d)}`}
x={categoryPoint(d)}
y={valueMax - barHeight}
width={categoryScale.bandwidth()}
height={barHeight}
fill="aqua"
/>
);
})}
</Group>
<AxisLeft scale={valueScale} top={margin.top} left={margin.left} />
<AxisBottom
scale={categoryScale}
top={valueMax + margin.top}
left={margin.left}
/>
</svg>
);
}
export function BarGraphHorizontal(props: BarGraphProps) {
const { width, height, margin, data, className } = props;
const barPadding = 0.4;
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 [isBarHovered, setIsBarHovered] = useState(
Object.fromEntries(
data.map((d, idx) => [`${getCategory(d)}-${idx}`, false])
)
);
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 categoryPoint = (d: BarGraphData) => categoryScale(getCategory(d));
const valuePoint = (d: BarGraphData) => valueScale(getValue(d));
return (
<svg className={className} width={width} height={height}>
<filter id="glow">
<feDropShadow
dx="0"
dy="0"
stdDeviation={4}
floodColor={COLOURS.darkpink}
/>
</filter>
<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 - barPadding);
return idx % 2 === 0 ? (
<Bar
key={`bar-${barName}-background`}
x={0}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
y={categoryPoint(d)! - (backgroundBarWidth - barWidth) / 2}
width={valueMax}
height={backgroundBarWidth}
fill={COLOURS.navy}
/>
) : null;
})}
</Group>
<GridColumns
scale={valueScale}
height={categoryMax}
numTicks={5}
stroke={COLOURS.white}
strokeWidth={4}
strokeDasharray="10"
strokeLinecap="round"
/>
<Group>
{data.map((d, idx) => {
const barName = `${getCategory(d)}-${idx}`;
const barLength = valuePoint(d);
const barWidth = categoryScale.bandwidth();
return (
<Group
key={`bar-${barName}`}
onMouseEnter={() => {
setIsBarHovered({ ...isBarHovered, [barName]: true });
}}
onMouseLeave={() => {
setIsBarHovered({ ...isBarHovered, [barName]: false });
}}
>
<Bar
x={0}
y={categoryPoint(d)}
width={barLength}
height={barWidth}
fill={isBarHovered[barName] ? COLOURS.pink : COLOURS.salmon}
filter={isBarHovered[barName] ? "url(#glow)" : undefined}
/>
{isBarHovered[barName] ? (
<Text
x={valuePoint(d) - 14}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
y={categoryPoint(d)! + barWidth * 0.75}
fill={COLOURS.white}
fontFamily="Inconsolata"
fontWeight={800}
fontSize={barWidth * 0.75}
textAnchor="end"
>
{getValue(d)}
</Text>
) : null}
</Group>
);
})}
</Group>
</Group>
<AxisLeft
scale={categoryScale}
top={margin.top}
left={margin.left}
hideAxisLine
hideTicks
tickLabelProps={() => {
return {
...leftTickLabelProps(),
dx: "-0.5rem",
dy: "0.25rem",
fill: COLOURS.white,
fontSize: "1rem",
fontFamily: "Inconsolata",
fontWeight: 800,
};
}}
/>
<AxisBottom
scale={valueScale}
top={margin.top + categoryMax}
left={margin.left}
hideAxisLine
hideTicks
numTicks={5}
tickLabelProps={() => {
return {
...bottomTickLabelProps(),
dy: "0.25rem",
fill: COLOURS.white,
fontSize: "1rem",
fontFamily: "Inconsolata",
fontWeight: 800,
};
}}
/>
</svg>
);
}