Compare commits

...

4 Commits

Author SHA1 Message Date
Rebecca-Chou dc59d058df merge main 2 months ago
Rebecca-Chou ad6436be2a merge main 2 months ago
Amy Wang 4458dcfa1f Add Grouped Bar Graph (#65) 2 months ago
Mark Chiu 2d34b84cb0 Add Stacked Bar Graph component (Closes #3) (#37) 2 months ago
  1. 4
      components/BarGraph.tsx
  2. 38
      components/GroupedBarGraph.module.css
  3. 621
      components/GroupedBarGraph.tsx
  4. 35
      components/StackedBarGraph.module.css
  5. 455
      components/StackedBarGraph.tsx
  6. 63
      data/mocks.ts
  7. 3039
      package-lock.json
  8. 102
      pages/playground.tsx

@ -25,9 +25,9 @@ interface BarGraphProps {
right: number;
};
className?: string;
/** Font size of the category tick labels, in pixels. Default is 16px. */
/** Font size of the category axis tick labels, in pixels. Default is 16px. */
categoryTickLabelSize?: number;
/** Font size of the value tick labels, in pixels. Default is 16px. */
/** Font size of the value axis tick labels, in pixels. Default is 16px. */
valueTickLabelSize?: number;
/** Font size of the value that appears when hovering over a bar, in pixels. */
hoverLabelSize?: number;

@ -0,0 +1,38 @@
.wrapper {
display: flex;
align-items: center;
width: min-content;
}
.barBackground {
fill: var(--card-background);
}
.barText {
visibility: hidden;
font-family: "Inconsolata", monospace;
font-weight: 800;
fill: var(--label);
}
.singleBar:hover .barText {
visibility: visible;
}
.tickLabel {
font-family: "Inconsolata", monospace;
font-weight: 800;
fill: var(--label);
}
.axisLabel {
font-family: "Inconsolata", monospace;
font-weight: 800;
fill: var(--label);
}
.legend {
display: flex;
margin: calc(16rem / 16);
}

@ -0,0 +1,621 @@
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 { LegendOrdinal } from "@visx/legend";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { Bar, BarGroup, BarGroupHorizontal } from "@visx/shape";
import { BarGroupBar as BarGroupBarType } from "@visx/shape/lib/types";
import { Text } from "@visx/text";
import React, { useState } from "react";
import { Color } from "utils/Color";
import styles from "./GroupedBarGraph.module.css";
interface GroupedBarGraphProps {
data: GroupedBarGraphData[];
/** Colours of bars in each group. */
barColors: string[];
/** Object mapping from the possible colours of bars in each group (barColors) to the colour of the bar when hovered. */
barHoverColorsMap: Record<string, string>;
/** Width of the entire graph, in pixels. */
width: number;
/** Height of the entire graph, in pixels. */
height: number;
/** Distance between the edge of the graph and the area where the bars are drawn, in pixels. */
margin: {
top: number;
bottom: number;
left: number;
right: number;
};
className?: string;
/** Font size of the category axis tick labels, in pixels. Default is 16px. */
categoryTickLabelSize?: number;
/** Font size of the value axis tick labels, in pixels. Default is 16px. */
valueTickLabelSize?: number;
/** Font size of the value that appears when hovering over a bar, in pixels. */
hoverLabelSize?: number;
/** Label text for the category axis. */
categoryAxisLabel?: string;
/** Font size of the label for the cateogry axis, in pixels. */
categoryAxisLabelSize?: number;
/** Controls the distance between the category axis label and the category axis. */
categoryAxisLabelOffset?: number;
/** Label text for the value axis. */
valueAxisLabel?: string;
/** Font size of the label for the value axis, in pixels. */
valueAxisLabelSize?: number;
/** Controls the distance between the value axis label and the value axis. */
valueAxisLabelOffset?: number;
legendProps?: LegendProps;
}
// Best format for props
interface GroupedBarGraphData {
category: string;
values: {
[key: string]: number;
};
}
// Best format for visx
interface BarGroupData {
category: string;
[key: string]: string | number;
}
interface LegendProps {
/** Position of the legend, relative to the graph. */
position?: "top" | "right";
/** Font size of the labels in the legend, in pixels. Default is 16px. */
itemLabelSize?: number;
/** Gap between items in the legend, in pixels. */
itemGap?: number;
/** Distance between the legend and other adjacent elements, in pixels. */
margin?: {
top?: number;
bottom?: number;
left?: number;
right?: number;
};
}
// BAR_PADDING must be in the range [0, 1)
const BAR_PADDING = 0.2;
const BAR_TEXT_PADDING = 12;
const DEFAULT_LABEL_SIZE = 16;
const DEFAULT_LEGEND_GAP = 16;
export function GroupedBarGraphVertical(props: GroupedBarGraphProps) {
const {
data: propsData,
barColors,
barHoverColorsMap,
width,
height,
margin,
className,
categoryTickLabelSize = DEFAULT_LABEL_SIZE,
valueTickLabelSize = DEFAULT_LABEL_SIZE,
hoverLabelSize,
categoryAxisLabel,
categoryAxisLabelSize = DEFAULT_LABEL_SIZE,
categoryAxisLabelOffset = 0,
valueAxisLabel,
valueAxisLabelSize = DEFAULT_LABEL_SIZE,
valueAxisLabelOffset = 0,
legendProps,
} = props;
const {
position: legendPosition = "right",
itemLabelSize: legendLabelSize = DEFAULT_LABEL_SIZE,
itemGap: legendItemGap = DEFAULT_LEGEND_GAP,
margin: legendMargin = {},
} = legendProps ?? {};
const data: BarGroupData[] = propsData.map((datum: GroupedBarGraphData) => {
return { category: datum.category, ...datum.values };
});
const keys = Object.keys(propsData[0].values);
propsData.forEach((d: GroupedBarGraphData) => {
const currentKeys = Object.keys(d.values);
if (
keys.length != currentKeys.length ||
!keys.every((key: string) => currentKeys.includes(key))
) {
throw new Error(
"Every category in a GroupedBarGraph must have the same keys. Check the data prop"
);
}
});
const allValues = propsData
.map((d: GroupedBarGraphData) => Object.values(d.values))
.flat();
const categoryMax = width - margin.left - margin.right;
const valueMax = height - margin.top - margin.bottom;
const getCategory = (d: BarGroupData) => d.category;
const categoryScale = scaleBand({
domain: data.map(getCategory),
padding: BAR_PADDING,
});
const keyScale = scaleBand({
domain: keys,
});
const valueScale = scaleLinear<number>({
domain: [0, Math.max(...allValues)],
});
const colorScale = scaleOrdinal<string, string>({
domain: keys,
range: barColors,
});
categoryScale.rangeRound([0, categoryMax]);
keyScale.rangeRound([0, categoryScale.bandwidth()]);
valueScale.rangeRound([valueMax, 0]);
return (
<div
className={className ? `${className} ${styles.wrapper}` : styles.wrapper}
style={{
flexDirection: legendPosition === "right" ? "row" : "column-reverse",
}}
>
<svg width={width} height={height}>
<defs>
{Object.keys(barHoverColorsMap).map((color: string) => {
// remove brackets from colour name to make ids work
const colorId = removeBrackets(color);
return (
<filter key={`glow-${color}`} id={`glow-${colorId}`}>
<feDropShadow
dx="0"
dy="0"
stdDeviation="4"
floodColor={barHoverColorsMap[color]}
/>
</filter>
);
})}
</defs>
<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 - BAR_PADDING);
return idx % 2 === 0 ? (
<Bar
className={styles.barBackground}
key={`bar-${barName}-background`}
x={
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
categoryScale(getCategory(d))! -
(backgroundBarWidth - barWidth) / 2
}
y={0}
width={backgroundBarWidth}
height={valueMax}
/>
) : null;
})}
</Group>
<GridRows
scale={valueScale}
width={categoryMax}
numTicks={5}
stroke={Color.label}
strokeWidth={4}
strokeDasharray="10"
strokeLinecap="round"
/>
<BarGroup
data={data}
keys={keys}
height={valueMax}
x0={getCategory}
x0Scale={categoryScale}
x1Scale={keyScale}
yScale={valueScale}
color={colorScale}
>
{(barGroups) =>
barGroups.map((barGroup) => (
<Group
key={`bar-group-${barGroup.x0}-${barGroup.index}`}
left={barGroup.x0}
>
{barGroup.bars.map((bar) => (
<HoverableBar
key={`bar-group-bar-${barGroup.x0}-${barGroup.index}-${bar.key}-${bar.index}`}
bar={bar}
valueMax={valueMax}
hoverFillColor={barHoverColorsMap[bar.color]}
hoverLabelSize={hoverLabelSize}
/>
))}
</Group>
))
}
</BarGroup>
</Group>
<AxisBottom
scale={categoryScale}
top={valueMax + margin.top}
left={margin.left}
hideAxisLine
hideTicks
tickLabelProps={() => {
return {
...bottomTickLabelProps(),
className: styles.tickLabel,
dy: "-0.25rem",
fontSize: `${categoryTickLabelSize / 16}rem`,
width: categoryScale.bandwidth(),
verticalAnchor: "start",
};
}}
label={categoryAxisLabel}
labelClassName={styles.axisLabel}
labelOffset={categoryAxisLabelOffset}
labelProps={{
fontSize: `${categoryAxisLabelSize / 16}rem`,
}}
/>
<AxisLeft
scale={valueScale}
top={margin.top}
left={margin.left}
hideAxisLine
hideTicks
numTicks={5}
tickLabelProps={() => {
return {
...leftTickLabelProps(),
className: styles.tickLabel,
dx: "-0.5rem",
dy: "0.25rem",
fontSize: `${valueTickLabelSize / 16}rem`,
};
}}
label={valueAxisLabel}
labelClassName={styles.axisLabel}
labelOffset={valueAxisLabelOffset}
labelProps={{
fontSize: `${valueAxisLabelSize / 16}rem`,
}}
/>
</svg>
<LegendOrdinal
className={styles.legend}
style={{
marginTop: legendMargin.top,
marginRight: legendMargin.right,
marginBottom: legendMargin.bottom,
marginLeft: legendMargin.left,
fontSize: legendLabelSize,
}}
scale={colorScale}
direction={legendPosition === "right" ? "column" : "row"}
itemMargin={
legendPosition === "right"
? `calc(${legendItemGap / 2}rem / 16) 0 calc(${
legendItemGap / 2
}rem / 16) 0`
: `0 calc(${legendItemGap / 2}rem / 16) 0 calc(${
legendItemGap / 2
}rem / 16)`
}
/>
</div>
);
}
export function GroupedBarGraphHorizontal(props: GroupedBarGraphProps) {
const {
data: propsData,
barColors,
barHoverColorsMap,
width,
height,
margin,
className,
categoryTickLabelSize = DEFAULT_LABEL_SIZE,
valueTickLabelSize = DEFAULT_LABEL_SIZE,
hoverLabelSize,
categoryAxisLabel,
categoryAxisLabelSize = DEFAULT_LABEL_SIZE,
categoryAxisLabelOffset = 0,
valueAxisLabel,
valueAxisLabelSize = DEFAULT_LABEL_SIZE,
valueAxisLabelOffset = 0,
legendProps,
} = props;
const {
position: legendPosition = "top",
itemLabelSize: legendLabelSize = DEFAULT_LABEL_SIZE,
itemGap: legendItemGap = DEFAULT_LEGEND_GAP,
margin: legendMargin = {},
} = legendProps ?? {};
const data: BarGroupData[] = propsData.map((datum: GroupedBarGraphData) => {
return { category: datum.category, ...datum.values };
});
const keys = Object.keys(propsData[0].values);
propsData.forEach((d: GroupedBarGraphData) => {
const currentKeys = Object.keys(d.values);
if (
keys.length != currentKeys.length ||
!keys.every((key: string) => currentKeys.includes(key))
) {
throw new Error(
"Every category in a GroupedBarGraph must have the same keys. Check the data prop"
);
}
});
const allValues = propsData
.map((d: GroupedBarGraphData) => Object.values(d.values))
.flat();
const categoryMax = height - margin.top - margin.bottom;
const valueMax = width - margin.left - margin.right;
const getCategory = (d: BarGroupData) => d.category;
const categoryScale = scaleBand({
domain: data.map(getCategory),
padding: BAR_PADDING,
});
const keyScale = scaleBand({
domain: keys,
});
const valueScale = scaleLinear<number>({
domain: [Math.max(...allValues), 0],
});
const colorScale = scaleOrdinal<string, string>({
domain: keys,
range: barColors,
});
categoryScale.rangeRound([0, categoryMax]);
keyScale.rangeRound([0, categoryScale.bandwidth()]);
valueScale.rangeRound([valueMax, 0]);
return (
<div
className={className ? `${className} ${styles.wrapper}` : styles.wrapper}
style={{
flexDirection: legendPosition === "right" ? "row" : "column-reverse",
}}
>
<svg width={width} height={height}>
<defs>
{Object.keys(barHoverColorsMap).map((color: string) => {
// remove brackets from colour name to make ids work
const colorId = removeBrackets(color);
return (
<filter key={`glow-${color}`} id={`glow-${colorId}`}>
<feDropShadow
dx="0"
dy="0"
stdDeviation="4"
floodColor={barHoverColorsMap[color]}
/>
</filter>
);
})}
</defs>
<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 - BAR_PADDING);
return idx % 2 === 0 ? (
<Bar
className={styles.barBackground}
key={`bar-${barName}-background`}
x={0}
y={
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
categoryScale(getCategory(d))! -
(backgroundBarWidth - barWidth) / 2
}
width={valueMax}
height={backgroundBarWidth}
/>
) : null;
})}
</Group>
<GridColumns
scale={valueScale}
height={categoryMax}
numTicks={5}
stroke={Color.label}
strokeWidth={4}
strokeDasharray="10"
strokeLinecap="round"
/>
<BarGroupHorizontal
data={data}
keys={keys}
width={valueMax}
y0={getCategory}
y0Scale={categoryScale}
y1Scale={keyScale}
xScale={valueScale}
color={colorScale}
>
{(barGroups) =>
barGroups.map((barGroup) => (
<Group
key={`bar-group-${barGroup.y0}-${barGroup.index}`}
top={barGroup.y0}
>
{barGroup.bars.map((bar) => (
<HoverableBar
key={`bar-group-bar-${barGroup.y0}-${barGroup.index}-${bar.key}-${bar.index}`}
bar={bar}
valueMax={valueMax}
hoverFillColor={barHoverColorsMap[bar.color]}
hoverLabelSize={hoverLabelSize}
isHorizontal
/>
))}
</Group>
))
}
</BarGroupHorizontal>
</Group>
<AxisLeft
scale={categoryScale}
top={margin.top}
left={margin.left}
hideAxisLine
hideTicks
tickLabelProps={() => {
return {
...leftTickLabelProps(),
className: styles.tickLabel,
dx: "-0.5rem",
dy: "0.25rem",
fontSize: `${valueTickLabelSize / 16}rem`,
height: categoryScale.bandwidth(),
};
}}
label={categoryAxisLabel}
labelClassName={styles.axisLabel}
labelOffset={categoryAxisLabelOffset}
labelProps={{
fontSize: `${categoryAxisLabelSize / 16}rem`,
}}
/>
<AxisBottom
scale={valueScale}
top={categoryMax + margin.top}
left={margin.left}
hideAxisLine
hideTicks
numTicks={5}
tickLabelProps={() => {
return {
...bottomTickLabelProps(),
className: styles.tickLabel,
dy: "-0.25rem",
fontSize: `${categoryTickLabelSize / 16}rem`,
verticalAnchor: "start",
};
}}
label={valueAxisLabel}
labelClassName={styles.axisLabel}
labelOffset={valueAxisLabelOffset}
labelProps={{
fontSize: `${valueAxisLabelSize / 16}rem`,
}}
/>
</svg>
<LegendOrdinal
className={styles.legend}
style={{
marginTop: legendMargin.top,
marginRight: legendMargin.right,
marginBottom: legendMargin.bottom,
marginLeft: legendMargin.left,
fontSize: legendLabelSize,
}}
scale={colorScale}
direction={legendPosition === "right" ? "column" : "row"}
itemMargin={
legendPosition === "right"
? `calc(${legendItemGap / 2}rem / 16) 0 calc(${
legendItemGap / 2
}rem / 16) 0`
: `0 calc(${legendItemGap / 2}rem / 16) 0 calc(${
legendItemGap / 2
}rem / 16)`
}
/>
</div>
);
}
interface HoverableBarProps {
bar: BarGroupBarType<string>;
valueMax: number;
hoverFillColor?: string;
hoverLabelSize?: number;
isHorizontal?: boolean;
}
function HoverableBar(props: HoverableBarProps) {
const {
bar,
valueMax,
hoverLabelSize,
hoverFillColor,
isHorizontal = false,
} = props;
const [isHovered, setIsHovered] = useState(false);
const colorId = removeBrackets(bar.color);
return (
<Group
className={styles.singleBar}
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => {
setIsHovered(false);
}}
>
<Bar
x={bar.x}
y={bar.y}
width={bar.width}
height={bar.height}
fill={isHovered && hoverFillColor ? hoverFillColor : bar.color}
// apply the glow effect when the bar is hovered
filter={isHovered ? `url(#glow-${colorId})` : undefined}
/>
<Text
className={styles.barText}
x={isHorizontal ? bar.width - BAR_TEXT_PADDING : bar.x + bar.width / 2}
y={
isHorizontal
? bar.y + bar.height / 2
: valueMax - bar.height + BAR_TEXT_PADDING
}
fontSize={
hoverLabelSize ?? (isHorizontal ? bar.height : bar.width) * 0.5
}
textAnchor={isHorizontal ? "end" : "middle"}
verticalAnchor={isHorizontal ? "middle" : "start"}
>
{bar.value}
</Text>
</Group>
);
}
function removeBrackets(str: string) {
return str.replace(/\(|\)/g, "");
}

@ -0,0 +1,35 @@
.container {
position: relative;
}
.barStack:hover {
filter: drop-shadow(0 0 calc(4rem / 16) var(--label));
}
.legend {
position: absolute;
display: flex;
font-size: calc(16rem / 16);
top: 0;
}
.toolTip {
font-family: "Inconsolata", monospace;
top: 0;
left: 0;
position: absolute;
background-color: var(--label);
color: var(--primary-background);
pointer-events: none;
border-radius: calc(10rem / 16);
padding: calc(10rem / 16);
}
.toolTip p {
margin: 0 calc(5rem / 16);
font-size: calc(16rem / 16);
}
.key {
font-weight: bold;
}

@ -0,0 +1,455 @@
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, BarStackHorizontal, Line } from "@visx/shape";
import { SeriesPoint } from "@visx/shape/lib/types";
import { withTooltip, Tooltip } from "@visx/tooltip";
import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip";
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 axis */
numTicksValueAxis?: number;
/** Distance between the left axis labels and the start of the lines of the graph, in px. */
axisLeftOffset?: number;
/** Distance between the bottom axis and the bottom of the container of the graph, in px. */
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. */
legendTopOffset?: 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;
/** Padding between each bar in the stacked bar graph, from 0 to 1 */
scalePadding?: number;
/** Margin for each item in the legend */
itemMargin?: string;
/** 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 StackedBarGraphVertical = 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 categoryScale = scaleBand<string>({
domain: data.map(getCategory),
padding: scalePadding,
});
const valueScale = scaleLinear<number>({
domain: [0, Math.max(...yTotals)],
nice: true,
});
const colorScale = scaleOrdinal<string, string>({
domain: keys,
range: colorRange,
});
// bounds
const xMax = width;
const yMax = height - margin.top - axisBottomOffset;
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={valueScale}
width={xMax}
height={yMax}
left={axisLeftOffset}
numTicks={numTicksValueAxis}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<GridColumns
scale={categoryScale}
height={yMax}
left={axisLeftOffset}
offset={categoryScale.bandwidth() / 2}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<Group left={axisLeftOffset}>
<BarStack<StackedBarData, string>
data={data}
keys={keys}
x={getCategory}
xScale={categoryScale}
yScale={valueScale}
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);
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: axisLeftOffset, y: 0 })}
from={new Point({ x: axisLeftOffset, y: yMax })}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<AxisBottom
top={yMax}
scale={categoryScale}
left={
((categoryScale.bandwidth() * 100) / width) *
categoryAxisLeftFactor
}
hideTicks
hideAxisLine
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => ({
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
})}
/>
<AxisLeft
scale={valueScale}
top={5}
numTicks={numTicksValueAxis}
hideAxisLine
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>
);
}
);
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>
);
}
);

@ -80,6 +80,45 @@ export const moreMockCategoricalData = [
{ key: "Dart", value: 2.21 },
];
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",
];
export const mockLineData = {
xValues: ["1A", "1B", "2A", "2B", "3A", "3B"],
lines: [
@ -162,3 +201,27 @@ export const mockQuoteDataLong = [
"Hello, world!",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla in enim neque. Sed sit amet convallis tellus. Integer condimentum a felis id gravida. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam metus libero, sagittis in consectetur in, scelerisque sed sapien. Nullam ut feugiat sapien. Praesent dictum ac ipsum ac lacinia.",
];
export const mockGroupedBarGraphData = [
{
category: "AJ",
values: {
Shooting: 7,
Melee: 9,
},
},
{
category: "Zen",
values: {
Shooting: 17,
Melee: 5,
},
},
{
category: "Lyra",
values: {
Shooting: 3,
Melee: 14,
},
},
];

3039
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -3,20 +3,32 @@ import { BoxPlot } from "components/Boxplot";
import {
mockCategoricalData,
moreMockCategoricalData,
mockStackedBarKeys,
mockStackedBarGraphData,
mockBoxPlotData,
mockLineData,
mockQuoteData,
mockQuoteDataLong,
mockPieData,
mockTimelineData,
mockGroupedBarGraphData,
} from "data/mocks";
import { sectionsData } from "data/routes";
import React from "react";
import { Color } from "utils/Color";
import { About } from "@/components/About";
import {
GroupedBarGraphHorizontal,
GroupedBarGraphVertical,
} from "@/components/GroupedBarGraph";
import { PieChart } from "@/components/PieChart";
import { QuotationCarousel } from "@/components/QuotationCarousel";
import { Sections } from "@/components/Sections";
import {
StackedBarGraphVertical,
StackedBarGraphHorizontal,
} from "@/components/StackedBarGraph";
import { Timeline } from "@/components/Timeline";
import { CenterWrapper } from "../components/CenterWrapper";
@ -28,7 +40,7 @@ import styles from "./playground.module.css";
export default function Home() {
return (
<div className={styles.page}>
<div className={styles.page} suppressHydrationWarning>
<h1>Playground</h1>
<p>Show off your components here!</p>
@ -97,6 +109,48 @@ export default function Home() {
}))}
/>
<h2>
<code>{"<StackedBarGraphVertical />"}</code>
</h2>
<StackedBarGraphVertical
width={600}
height={400}
keys={mockStackedBarKeys}
colorRange={[
Color.primaryAccent,
Color.secondaryAccentLight,
Color.primaryAccentLighter,
]}
data={mockStackedBarGraphData}
margin={{
top: 20,
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>{"<Timeline />"}</code>
</h2>
@ -185,6 +239,52 @@ export default function Home() {
circleDiameter={180}
/>
</div>
<h2>
<code>{"<GroupedBarGraphVertical />"}</code>
</h2>
<GroupedBarGraphVertical
className={styles.barGraphDemo}
data={mockGroupedBarGraphData}
barColors={[Color.primaryAccentLight, Color.secondaryAccentLight]}
barHoverColorsMap={{
[Color.primaryAccentLight]: Color.primaryAccent,
[Color.secondaryAccentLight]: Color.secondaryAccent,
}}
width={500}
height={400}
margin={{
top: 20,
bottom: 40,
left: 50,
right: 20,
}}
/>
<h2>
<code>{"<GroupedBarGraphHorizontal />"}</code>
</h2>
<p>
<code>{"<GroupedBarGraphHorizontal />"}</code> takes the same props as{" "}
<code>{"<GroupedBarGraphVertical />"}</code>.
</p>
<GroupedBarGraphHorizontal
className={styles.barGraphDemo}
data={mockGroupedBarGraphData}
barColors={[Color.primaryAccentLight, Color.secondaryAccentLight]}
barHoverColorsMap={{
[Color.primaryAccentLight]: Color.primaryAccent,
[Color.secondaryAccentLight]: Color.secondaryAccent,
}}