add stacked bar graph component

pull/37/head
Miniapple8888 5 months ago
parent 2496e34798
commit c3b33493e5
  1. 22
      components/StackedBarGraph.module.css
  2. 253
      components/StackedBarGraph.tsx
  3. 39
      data/mocks.ts
  4. 12
      package-lock.json
  5. 1
      package.json
  6. 26
      pages/playground.tsx

@ -0,0 +1,22 @@
.barStack:hover {
filter: drop-shadow(0 0 calc(4rem / 16) var(--label));
}
.legend {
position: relative;
display: flex;
justify-content: center;
font-size: 16px;
}
.toolTip {
padding: 10px 0;
}
.toolTip p {
margin: 0 5px;
}
.key {
font-weight: 700;
}

@ -0,0 +1,253 @@
import { AxisLeft, AxisBottom } from "@visx/axis";
import { localPoint } from "@visx/event";
import { GridRows, GridColumns } from "@visx/grid";
import { Group } from "@visx/group";
import { LegendOrdinal } from "@visx/legend";
import { Point } from "@visx/point";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { BarStack, Line } from "@visx/shape";
import { SeriesPoint } from "@visx/shape/lib/types";
import { useTooltip, useTooltipInPortal, defaultStyles } from "@visx/tooltip";
import React from "react";
import { Color } from "utils/Color";
import styles from "./StackedBarGraph.module.css";
interface StackedBarData {
category: string;
[key: string]: number | string;
}
type TooltipData = {
bar: SeriesPoint<StackedBarData>;
key: string;
index: number;
height: number;
width: number;
x: number;
y: number;
color: string;
};
export type StackedBarProps = {
data: StackedBarData[];
/** Width of the entire graph, in pixels, greater than 10. */
width: number;
/** Height of the entire graph, in pixels. */
height: number;
/** Names of the groups appearing in the legend */
keys: string[];
/** Colours for each key */
colorRange: string[];
/** Distance between the edge of the graph and the area where the bars are drawn, in pixels. */
margin: { top: number; left: number };
/** Number of ticks for the value (y-)axis */
numTicksLeftAxis?: number;
/** Distance between the left axis labels and the start of the lines of the graph, in px. */
valueAxisLeftOffset?: number;
/** Width of the lines in the graph, in px. */
strokeWidth?: number;
/** Length of the dashes and the gaps in the graph, in px. */
strokeDashArray?: string;
scalePadding?: number;
};
const tooltipStyles = {
...defaultStyles,
minWidth: 60,
backgroundColor: Color.primaryAccentLighter,
color: Color.primaryBackground,
};
let tooltipTimeout: number;
export function StackedBarGraph({
data,
width,
height,
keys,
colorRange,
margin,
scalePadding = 0.3,
numTicksLeftAxis = 6,
valueAxisLeftOffset = 40,
strokeWidth = 2.5,
strokeDashArray = "10,4",
}: StackedBarProps) {
const yTotals = data.reduce((allTotals, currCategory) => {
const yTotal = keys.reduce((categoryTotal, k) => {
categoryTotal += currCategory[k] as number;
return categoryTotal;
}, 0);
allTotals.push(yTotal);
return allTotals;
}, [] as number[]);
const TICK_LABEL_FONT_WEIGHT = 800;
// accessors
const getCategory = (d: StackedBarData) => d.category;
// scales
const xScale = scaleBand<string>({
domain: data.map(getCategory),
padding: scalePadding,
});
const yScale = scaleLinear<number>({
domain: [0, Math.max(...yTotals)],
nice: true,
});
const colorScale = scaleOrdinal<string, string>({
domain: keys,
range: colorRange,
});
const {
tooltipOpen,
tooltipLeft,
tooltipTop,
tooltipData,
hideTooltip,
showTooltip,
} = useTooltip<TooltipData>();
const { containerRef, TooltipInPortal } = useTooltipInPortal({
// TooltipInPortal is rendered in a separate child of <body /> and positioned
// with page coordinates which should be updated on scroll.
scroll: true,
});
if (width < 10) return null;
// bounds
const xMax = width;
const yMax = height - margin.top - 50;
xScale.rangeRound([0, xMax - valueAxisLeftOffset]);
yScale.range([yMax, 0]);
return width < 10 ? null : (
<div>
<svg ref={containerRef} width={width} height={height}>
<Group top={margin.top} left={margin.left}>
<GridRows
scale={yScale}
width={xMax}
height={yMax}
left={valueAxisLeftOffset}
numTicks={numTicksLeftAxis}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<GridColumns
scale={xScale}
height={yMax}
left={valueAxisLeftOffset}
offset={xScale.bandwidth() / 2}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<Group left={valueAxisLeftOffset}>
<BarStack<StackedBarData, string>
data={data}
keys={keys}
x={getCategory}
xScale={xScale}
yScale={yScale}
color={colorScale}
>
{(barStacks) =>
barStacks.map((barStack) =>
barStack.bars.map((bar) => (
<rect
className={styles.barStack}
key={`bar-stack-${barStack.index}-${bar.index}`}
x={bar.x}
y={bar.y}
height={bar.height}
width={bar.width / 2}
fill={bar.color}
onMouseLeave={() => {
tooltipTimeout = window.setTimeout(() => {
hideTooltip();
}, 300);
}}
onMouseMove={(event) => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
// TooltipInPortal expects coordinates to be relative to containerRef
// localPoint returns coordinates relative to the nearest SVG
const eventSvgCoords = localPoint(event);
const left = bar.x + bar.width / 2;
showTooltip({
tooltipData: bar,
tooltipTop: eventSvgCoords?.y,
tooltipLeft: left,
});
}}
/>
))
)
}
</BarStack>
</Group>
<Line
fill={Color.tertiaryBackground}
to={new Point({ x: valueAxisLeftOffset, y: 0 })}
from={new Point({ x: valueAxisLeftOffset, y: yMax })}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<AxisBottom
top={yMax}
scale={xScale}
left={valueAxisLeftOffset / 5}
hideTicks
hideAxisLine
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => ({
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
})}
/>
<AxisLeft
scale={yScale}
numTicks={numTicksLeftAxis}
hideAxisLine
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => {
return {
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
};
}}
/>
</Group>
</svg>
<div className={styles.legend} style={{ width: width }}>
<LegendOrdinal
scale={colorScale}
direction="row"
labelMargin="0 15px 0 0"
/>
</div>
{tooltipOpen && tooltipData ? (
<TooltipInPortal
className={styles.toolTip}
top={tooltipTop}
left={tooltipLeft}
style={tooltipStyles}
>
<p className={styles.key}>{tooltipData.key}</p>
<p>{tooltipData.bar.data[tooltipData.key]}</p>
<p>{getCategory(tooltipData.bar.data)}</p>
</TooltipInPortal>
) : null}
</div>
);
}

@ -94,3 +94,42 @@ export const mockBoxPlotData = [
outliers: [],
},
];
export const mockStackedBarGraphData = [
{
category: "1A",
"geese watchers": 60,
"geese breeders": 80,
"geese catchers": 90,
},
{
category: "1B",
"geese watchers": 25,
"geese breeders": 37,
"geese catchers": 80,
},
{
category: "2A",
"geese watchers": 40,
"geese breeders": 50,
"geese catchers": 70,
},
{
category: "2B",
"geese watchers": 40,
"geese breeders": 80,
"geese catchers": 88,
},
{
category: "3A",
"geese watchers": 15,
"geese breeders": 30,
"geese catchers": 45,
},
];
export const mockStackedBarKeys = [
"geese watchers",
"geese breeders",
"geese catchers",
];

12
package-lock.json generated

@ -580,6 +580,18 @@
"prop-types": "^15.6.2"
}
},
"@visx/legend": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@visx/legend/-/legend-2.10.0.tgz",
"integrity": "sha512-OI8BYE6QQI9eXAng/C7UzuVw7d0fwlzrth6RmrdhlyT1K+BA3WpExapV+pDfwxu/tkEik8Ps5cZRV6HjX1/Mww==",
"requires": {
"@types/react": "*",
"@visx/group": "2.10.0",
"@visx/scale": "2.2.2",
"classnames": "^2.3.1",
"prop-types": "^15.5.10"
}
},
"@visx/mock-data": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@visx/mock-data/-/mock-data-2.1.2.tgz",

@ -19,6 +19,7 @@
"@visx/event": "^2.6.0",
"@visx/grid": "^2.10.0",
"@visx/group": "^2.10.0",
"@visx/legend": "^2.10.0",
"@visx/mock-data": "^2.1.2",
"@visx/scale": "^2.2.2",
"@visx/shape": "^2.10.0",

@ -3,9 +3,14 @@ import Boxplot from "components/Boxplot";
import {
mockCategoricalData,
moreMockCategoricalData,
mockBoxPlotData
mockBoxPlotData,
mockStackedBarGraphData,
mockStackedBarKeys,
} from "data/mocks";
import React from "react";
import { Color } from "utils/Color";
import { StackedBarGraph } from "@/components/StackedBarGraph";
import { ColorPalette } from "../components/ColorPalette";
import { WordCloud } from "../components/WordCloud";
@ -79,6 +84,25 @@ export default function Home() {
strokeWidth={2.5}
strokeDashArray="10,4"
/>
<h2>
<code>{"<StackedBarGraph />"}</code>
</h2>
<StackedBarGraph
width={600}
height={400}
keys={mockStackedBarKeys}
colorRange={[
Color.primaryAccent,
Color.secondaryAccentLight,
Color.primaryAccentLighter,
]}
data={mockStackedBarGraphData}
margin={{
top: 20,
left: 20,
}}
/>
</div>
);
}

Loading…
Cancel
Save