add horizontal stacked bar graph
This commit is contained in:
parent
d53468a0eb
commit
b6d72f7d0b
|
@ -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…
Reference in New Issue