add horizontal stacked bar graph

pull/37/head
e26chiu 3 months ago
parent d53468a0eb
commit b6d72f7d0b
  1. 5
      components/StackedBarGraph.module.css
  2. 256
      components/StackedBarGraph.tsx
  3. 33
      pages/playground.tsx

@ -15,7 +15,6 @@
.toolTip {
font-family: "Inconsolata", monospace;
font-weight: bold;
top: 0;
left: 0;
position: absolute;
@ -32,5 +31,5 @@
}
.key {
font-weight: 700;
}
font-weight: bold;
}

@ -5,9 +5,8 @@ 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 { BarStack, BarStackHorizontal, Line } from "@visx/shape";
import { SeriesPoint } from "@visx/shape/lib/types";
//import { useTooltip, useTooltipInPortal, withTooltip } from "@visx/tooltip";
import { withTooltip, Tooltip } from "@visx/tooltip";
import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip";
import React from "react";
@ -43,12 +42,12 @@ export type StackedBarProps = {
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;
/** Number of ticks for the value axis */
numTicksValueAxis?: number;
/** Distance between the left axis labels and the start of the lines of the graph, in px. */
valueAxisLeftOffset?: number;
axisLeftOffset?: number;
/** Distance between the bottom axis and the bottom of the container of the graph, in px. */
categoryAxisBottomOffset?: number;
axisBottomOffset?: number;
/** Distance between the right side of the graph and the legend, in px. */
legendLeftOffset?: number;
/** Distance between the top of the graph and the legend, in px. */
@ -61,13 +60,17 @@ export type StackedBarProps = {
scalePadding?: number;
/** Margin for each item in the legend */
itemMargin?: string;
/** Factor multiplied to the left offset to center the labels in the x-axis. >1 for width <600 and <1 for width >600 */
/** Factor multiplied with an offset to center the labels of the category-axis depending on the width/height of the graph.
* >1 for width/height <600 and <1 for width/height >600 (vertical=width/horizontal=height) */
categoryAxisLeftFactor?: number;
};
let tooltipTimeout: number;
export const StackedBarGraph = withTooltip<StackedBarProps, TooltipData>(
export const StackedBarGraphVertical = withTooltip<
StackedBarProps,
TooltipData
>(
({
data,
width,
@ -76,9 +79,9 @@ export const StackedBarGraph = withTooltip<StackedBarProps, TooltipData>(
colorRange,
margin,
scalePadding = 0.3,
numTicksLeftAxis = 6,
valueAxisLeftOffset = 40,
categoryAxisBottomOffset = 40,
numTicksValueAxis = 6,
axisLeftOffset = 40,
axisBottomOffset = 40,
strokeWidth = 2.5,
strokeDashArray = "10,4",
legendLeftOffset = 40,
@ -107,11 +110,11 @@ export const StackedBarGraph = withTooltip<StackedBarProps, TooltipData>(
const getCategory = (d: StackedBarData) => d.category;
// scales
const xScale = scaleBand<string>({
const categoryScale = scaleBand<string>({
domain: data.map(getCategory),
padding: scalePadding,
});
const yScale = scaleLinear<number>({
const valueScale = scaleLinear<number>({
domain: [0, Math.max(...yTotals)],
nice: true,
});
@ -122,41 +125,41 @@ export const StackedBarGraph = withTooltip<StackedBarProps, TooltipData>(
// bounds
const xMax = width;
const yMax = height - margin.top - categoryAxisBottomOffset;
const yMax = height - margin.top - axisBottomOffset;
xScale.rangeRound([0, xMax - valueAxisLeftOffset]);
yScale.range([yMax, 0]);
categoryScale.rangeRound([0, xMax - axisLeftOffset]);
valueScale.range([yMax, 0]);
return width < 10 ? null : (
<div className={styles.container}>
<svg width={width} height={height}>
<Group top={margin.top} left={margin.left}>
<GridRows
scale={yScale}
scale={valueScale}
width={xMax}
height={yMax}
left={valueAxisLeftOffset}
numTicks={numTicksLeftAxis}
left={axisLeftOffset}
numTicks={numTicksValueAxis}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<GridColumns
scale={xScale}
scale={categoryScale}
height={yMax}
left={valueAxisLeftOffset}
offset={xScale.bandwidth() / 2}
left={axisLeftOffset}
offset={categoryScale.bandwidth() / 2}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<Group left={valueAxisLeftOffset}>
<Group left={axisLeftOffset}>
<BarStack<StackedBarData, string>
data={data}
keys={keys}
x={getCategory}
xScale={xScale}
yScale={yScale}
xScale={categoryScale}
yScale={valueScale}
color={colorScale}
>
{(barStacks) =>
@ -193,17 +196,18 @@ export const StackedBarGraph = withTooltip<StackedBarProps, TooltipData>(
</Group>
<Line
fill={Color.tertiaryBackground}
to={new Point({ x: valueAxisLeftOffset, y: 0 })}
from={new Point({ x: valueAxisLeftOffset, y: yMax })}
to={new Point({ x: axisLeftOffset, y: 0 })}
from={new Point({ x: axisLeftOffset, y: yMax })}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<AxisBottom
top={yMax}
scale={xScale}
scale={categoryScale}
left={
((xScale.bandwidth() * 100) / width) * categoryAxisLeftFactor
((categoryScale.bandwidth() * 100) / width) *
categoryAxisLeftFactor
}
hideTicks
hideAxisLine
@ -216,9 +220,9 @@ export const StackedBarGraph = withTooltip<StackedBarProps, TooltipData>(
})}
/>
<AxisLeft
scale={yScale}
scale={valueScale}
top={5}
numTicks={numTicksLeftAxis}
numTicks={numTicksValueAxis}
hideAxisLine
labelProps={{
fontSize: `${10 / 16}rem`,
@ -259,3 +263,193 @@ export const StackedBarGraph = withTooltip<StackedBarProps, TooltipData>(
);
}
);
export const StackedBarGraphHorizontal = withTooltip<
StackedBarProps,
TooltipData
>(
({
data,
width,
height,
keys,
colorRange,
margin,
scalePadding = 0.3,
numTicksValueAxis = 6,
axisLeftOffset = 40,
axisBottomOffset = 40,
strokeWidth = 2.5,
strokeDashArray = "10,4",
legendLeftOffset = 40,
legendTopOffset = 40,
itemMargin = "15px 0 0 0",
categoryAxisLeftFactor = 1,
tooltipOpen,
tooltipLeft,
tooltipTop,
tooltipData,
hideTooltip,
showTooltip,
}: StackedBarProps & WithTooltipProvidedProps<TooltipData>) => {
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 valueScale = scaleLinear<number>({
domain: [0, Math.max(...yTotals)],
nice: true,
});
const categoryScale = scaleBand<string>({
domain: data.map(getCategory),
padding: scalePadding,
});
const colorScale = scaleOrdinal<string, string>({
domain: keys,
range: colorRange,
});
// bounds
const xMax = width;
const yMax = height - margin.top - axisBottomOffset;
categoryScale.rangeRound([yMax, 0]);
valueScale.range([0, xMax - axisLeftOffset]);
return width < 10 ? null : (
<div className={styles.container}>
<svg width={width} height={height}>
<Group top={margin.top} left={margin.left}>
<GridRows
scale={categoryScale}
width={xMax}
height={yMax}
offset={categoryScale.bandwidth() / 2}
left={axisLeftOffset}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<GridColumns
scale={valueScale}
height={yMax}
numTicks={numTicksValueAxis}
left={axisLeftOffset}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<Group left={axisLeftOffset}>
<BarStackHorizontal<StackedBarData, string>
data={data}
keys={keys}
y={getCategory}
xScale={valueScale}
yScale={categoryScale}
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 / 2}
width={bar.width}
fill={bar.color}
onMouseLeave={() => {
tooltipTimeout = window.setTimeout(() => {
hideTooltip();
}, 300);
}}
onMouseMove={(event) => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
const eventSvgCoords = localPoint(event);
const left = bar.x + bar.width / 2;
showTooltip({
tooltipData: bar,
tooltipTop: eventSvgCoords?.y,
tooltipLeft: left,
});
}}
/>
))
)
}
</BarStackHorizontal>
</Group>
<AxisBottom
top={yMax}
scale={valueScale}
left={axisLeftOffset}
numTicks={numTicksValueAxis}
hideAxisLine
hideTicks
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => ({
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
})}
/>
<AxisLeft
top={
-((categoryScale.bandwidth() * 100) / width) *
categoryAxisLeftFactor
}
scale={categoryScale}
hideAxisLine
hideTicks
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => {
return {
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
};
}}
/>
</Group>
</svg>
<div
className={styles.legend}
style={{ left: width + legendLeftOffset, top: legendTopOffset }}
>
<LegendOrdinal
scale={colorScale}
direction="column"
itemMargin={itemMargin}
/>
</div>
{tooltipOpen && tooltipData ? (
<Tooltip
className={styles.toolTip}
top={tooltipTop}
left={tooltipLeft}
unstyled
>
<p className={styles.key}>{tooltipData.key}</p>
<p>{tooltipData.bar.data[tooltipData.key]}</p>
<p>{getCategory(tooltipData.bar.data)}</p>
</Tooltip>
) : null}
</div>
);
}
);

@ -11,7 +11,10 @@ import React from "react";
import { Color } from "utils/Color";
import { QuotationCarousel } from "@/components/QuotationCarousel";
import { StackedBarGraph } from "@/components/StackedBarGraph";
import {
StackedBarGraphVertical,
StackedBarGraphHorizontal,
} from "@/components/StackedBarGraph";
import { ColorPalette } from "../components/ColorPalette";
import { WordCloud } from "../components/WordCloud";
@ -72,9 +75,9 @@ export default function Home() {
/>
<h2>
<code>{"<StackedBarGraph />"}</code>
<code>{"<StackedBarGraphVertical />"}</code>
</h2>
<StackedBarGraph
<StackedBarGraphVertical
width={600}
height={400}
keys={mockStackedBarKeys}
@ -89,6 +92,30 @@ export default function Home() {
left: 20,
}}
/>
<h2>
<code>{"<StackedBarGraphHorizontal />"}</code>
</h2>
<p>
<code>{"<StackedBarGraphHorizontal />"}</code> takes the same props as{" "}
<code>{"<StackedBarGraphVertical />"}</code>.
</p>
<StackedBarGraphHorizontal
width={600}
height={400}
keys={mockStackedBarKeys}
colorRange={[
Color.primaryAccent,
Color.secondaryAccentLight,
Color.primaryAccentLighter,
]}
data={mockStackedBarGraphData}
margin={{
top: 20,
left: 20,
}}
/>
<h2>
<code>{"<QuotationCarousel />"}</code>
</h2>

Loading…
Cancel
Save