From b6d72f7d0be54e305c2db89be7e9abac2786ffa1 Mon Sep 17 00:00:00 2001 From: e26chiu Date: Sat, 3 Sep 2022 13:53:28 -0400 Subject: [PATCH] add horizontal stacked bar graph --- components/StackedBarGraph.module.css | 5 +- components/StackedBarGraph.tsx | 256 ++++++++++++++++++++++---- pages/playground.tsx | 33 +++- 3 files changed, 257 insertions(+), 37 deletions(-) diff --git a/components/StackedBarGraph.module.css b/components/StackedBarGraph.module.css index 68e5a2a..ffe660f 100644 --- a/components/StackedBarGraph.module.css +++ b/components/StackedBarGraph.module.css @@ -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; +} \ No newline at end of file diff --git a/components/StackedBarGraph.tsx b/components/StackedBarGraph.tsx index c12fce1..7c195e0 100644 --- a/components/StackedBarGraph.tsx +++ b/components/StackedBarGraph.tsx @@ -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( +export const StackedBarGraphVertical = withTooltip< + StackedBarProps, + TooltipData +>( ({ data, width, @@ -76,9 +79,9 @@ export const StackedBarGraph = withTooltip( 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( const getCategory = (d: StackedBarData) => d.category; // scales - const xScale = scaleBand({ + const categoryScale = scaleBand({ domain: data.map(getCategory), padding: scalePadding, }); - const yScale = scaleLinear({ + const valueScale = scaleLinear({ domain: [0, Math.max(...yTotals)], nice: true, }); @@ -122,41 +125,41 @@ export const StackedBarGraph = withTooltip( // 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 : (
- + 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( ( })} /> ( ); } ); + +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) => { + 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({ + domain: [0, Math.max(...yTotals)], + nice: true, + }); + const categoryScale = scaleBand({ + domain: data.map(getCategory), + padding: scalePadding, + }); + const colorScale = scaleOrdinal({ + 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 : ( +
+ + + + + + + data={data} + keys={keys} + y={getCategory} + xScale={valueScale} + yScale={categoryScale} + color={colorScale} + > + {(barStacks) => + barStacks.map((barStack) => + barStack.bars.map((bar) => ( + { + 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, + }); + }} + /> + )) + ) + } + + + ({ + fill: Color.label, + fontWeight: TICK_LABEL_FONT_WEIGHT, + })} + /> + { + return { + fill: Color.label, + fontWeight: TICK_LABEL_FONT_WEIGHT, + }; + }} + /> + + +
+ +
+ + {tooltipOpen && tooltipData ? ( + +

{tooltipData.key}

+

{tooltipData.bar.data[tooltipData.key]}

+

{getCategory(tooltipData.bar.data)}

+
+ ) : null} +
+ ); + } +); diff --git a/pages/playground.tsx b/pages/playground.tsx index c52d11c..9ecc504 100644 --- a/pages/playground.tsx +++ b/pages/playground.tsx @@ -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() { />

- {""} + {""}

- + +

+ {""} +

+

+ {""} takes the same props as{" "} + {""}. +

+ +

{""}