merge main
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Rebecca-Chou 2022-10-02 14:55:43 -04:00
commit dc59d058df
8 changed files with 3575 additions and 58 deletions

View File

@ -25,9 +25,9 @@ interface BarGraphProps {
right: number; right: number;
}; };
className?: string; 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; 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; valueTickLabelSize?: number;
/** Font size of the value that appears when hovering over a bar, in pixels. */ /** Font size of the value that appears when hovering over a bar, in pixels. */
hoverLabelSize?: number; hoverLabelSize?: number;

View File

@ -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);
}

View File

@ -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, "");
}

View File

@ -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;
}

View File

@ -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>
);
}
);

View File

@ -80,6 +80,45 @@ export const moreMockCategoricalData = [
{ key: "Dart", value: 2.21 }, { 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 = { export const mockLineData = {
xValues: ["1A", "1B", "2A", "2B", "3A", "3B"], xValues: ["1A", "1B", "2A", "2B", "3A", "3B"],
lines: [ lines: [
@ -162,3 +201,27 @@ export const mockQuoteDataLong = [
"Hello, world!", "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.", "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,
},
},
];

2315
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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