From 6735c52914d3f25ace962f2a40d951ec6772d8b8 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 27 Jul 2022 19:46:08 -0400 Subject: [PATCH] Bar Graph Component (#16) Adds a `` component and a `` component. Closes #1. Possible changes for the future: - Refactor to make the horizontal and vertical bar graphs into one component - Add (optional) graph title Co-authored-by: Amy Wang Reviewed-on: https://git.csclub.uwaterloo.ca/www/cs-2022-class-profile/pulls/16 Reviewed-by: Shahan Neda --- .vscode/settings.json | 1 + components/BarGraph.module.css | 36 ++++ components/BarGraph.tsx | 353 +++++++++++++++++++++++++++++++++ data/mocks.ts | 16 +- package-lock.json | 3 + package.json | 3 + pages/playground.module.css | 7 + pages/playground.tsx | 44 +++- 8 files changed, 453 insertions(+), 10 deletions(-) create mode 100644 components/BarGraph.module.css create mode 100644 components/BarGraph.tsx create mode 100644 pages/playground.module.css diff --git a/.vscode/settings.json b/.vscode/settings.json index 12f41f3..99e58e0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "typescript.tsdk": "node_modules/typescript/lib", "eslint.format.enable": true, "eslint.codeActionsOnSave.mode": "all", + "css.format.spaceAroundSelectorSeparator": true, "[css]": { "editor.suggest.insertMode": "replace", "gitlens.codeLens.scopes": ["document"], diff --git a/components/BarGraph.module.css b/components/BarGraph.module.css new file mode 100644 index 0000000..025976a --- /dev/null +++ b/components/BarGraph.module.css @@ -0,0 +1,36 @@ +.barBackground { + fill: var(--card-background); +} + +.bar { + fill: var(--primary-accent-light); +} + +.barText { + visibility: hidden; + + font-family: "Inconsolata", "monospace"; + font-weight: 800; + fill: var(--label); +} + +.barGroup:hover .bar { + fill: var(--primary-accent); + filter: drop-shadow(0 0 calc(4rem / 16) var(--primary-accent)); +} + +.barGroup: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); +} diff --git a/components/BarGraph.tsx b/components/BarGraph.tsx new file mode 100644 index 0000000..222770d --- /dev/null +++ b/components/BarGraph.tsx @@ -0,0 +1,353 @@ +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 { scaleBand, scaleLinear } from "@visx/scale"; +import { Bar } from "@visx/shape"; +import { Text } from "@visx/text"; +import React from "react"; +import { Color } from "utils/Color"; + +import styles from "./BarGraph.module.css"; + +interface BarGraphProps { + data: BarGraphData[]; + /** 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 tick labels, in pixels. Default is 16px. */ + categoryTickLabelSize?: number; + /** Font size of the value 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; +} + +interface BarGraphData { + category: string; + value: number; +} + +const DEFAULT_LABEL_SIZE = 16; + +export function BarGraphHorizontal(props: BarGraphProps) { + const { + width, + height, + margin, + data, + className, + categoryTickLabelSize = DEFAULT_LABEL_SIZE, + valueTickLabelSize = DEFAULT_LABEL_SIZE, + hoverLabelSize, + categoryAxisLabel, + categoryAxisLabelSize = DEFAULT_LABEL_SIZE, + categoryAxisLabelOffset = 0, + valueAxisLabel, + valueAxisLabelSize = DEFAULT_LABEL_SIZE, + valueAxisLabelOffset = 0, + } = props; + + const barPadding = 0.4; + + const categoryMax = height - margin.top - margin.bottom; + const valueMax = width - margin.left - margin.right; + + const getCategory = (d: BarGraphData) => d.category; + const getValue = (d: BarGraphData) => d.value; + + const categoryScale = scaleBand({ + range: [0, categoryMax], + domain: data.map(getCategory), + padding: barPadding, + }); + + const valueScale = scaleLinear({ + range: [0, valueMax], + nice: true, + domain: [0, Math.max(...data.map(getValue))], + }); + + const categoryPoint = (d: BarGraphData) => categoryScale(getCategory(d)); + const valuePoint = (d: BarGraphData) => valueScale(getValue(d)); + + return ( + + + + {data.map((d, idx) => { + const barName = `${getCategory(d)}-${idx}`; + const barWidth = categoryScale.bandwidth(); + const backgroundBarWidth = barWidth / (1 - barPadding); + return idx % 2 === 0 ? ( + + ) : null; + })} + + + + {data.map((d, idx) => { + const barName = `${getCategory(d)}-${idx}`; + const barLength = valuePoint(d); + const barWidth = categoryScale.bandwidth(); + return ( + + + + {getValue(d)} + + + ); + })} + + + { + return { + ...leftTickLabelProps(), + className: styles.tickLabel, + dx: "-0.5rem", + dy: "0.25rem", + fontSize: `${categoryTickLabelSize / 16}rem`, + }; + }} + label={categoryAxisLabel} + labelClassName={styles.axisLabel} + labelOffset={categoryAxisLabelOffset} + labelProps={{ + fontSize: `${categoryAxisLabelSize / 16}rem`, + }} + /> + { + return { + ...bottomTickLabelProps(), + className: styles.tickLabel, + dy: "0.25rem", + fontSize: `${valueTickLabelSize / 16}rem`, + }; + }} + label={valueAxisLabel} + labelClassName={styles.axisLabel} + labelOffset={valueAxisLabelOffset} + labelProps={{ + fontSize: `${valueAxisLabelSize / 16}rem`, + }} + /> + + ); +} + +export function BarGraphVertical(props: BarGraphProps) { + const { + width, + height, + margin, + data, + className, + categoryTickLabelSize = DEFAULT_LABEL_SIZE, + valueTickLabelSize = DEFAULT_LABEL_SIZE, + hoverLabelSize, + categoryAxisLabel, + categoryAxisLabelSize = DEFAULT_LABEL_SIZE, + categoryAxisLabelOffset = 0, + valueAxisLabel, + valueAxisLabelSize = DEFAULT_LABEL_SIZE, + valueAxisLabelOffset = 0, + } = props; + + const barPadding = 0.4; + + const categoryMax = width - margin.left - margin.right; + const valueMax = height - margin.top - margin.bottom; + + const getCategory = (d: BarGraphData) => d.category; + const getValue = (d: BarGraphData) => d.value; + + const categoryScale = scaleBand({ + range: [0, categoryMax], + domain: data.map(getCategory), + padding: barPadding, + }); + + const valueScale = scaleLinear({ + range: [valueMax, 0], + nice: true, + domain: [0, Math.max(...data.map(getValue))], + }); + + const categoryPoint = (d: BarGraphData) => categoryScale(getCategory(d)); + const valuePoint = (d: BarGraphData) => valueScale(getValue(d)); + + return ( + + + + {data.map((d, idx) => { + const barName = `${getCategory(d)}-${idx}`; + const barWidth = categoryScale.bandwidth(); + const backgroundBarWidth = barWidth / (1 - barPadding); + return idx % 2 === 0 ? ( + + ) : null; + })} + + + + {data.map((d, idx) => { + const barName = `${getCategory(d)}-${idx}`; + const barHeight = valueMax - valuePoint(d); + const barWidth = categoryScale.bandwidth(); + return ( + + + + {getValue(d)} + + + ); + })} + + + { + 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`, + }} + /> + { + 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`, + }} + /> + + ); +} diff --git a/data/mocks.ts b/data/mocks.ts index f7e9235..ae0a70a 100644 --- a/data/mocks.ts +++ b/data/mocks.ts @@ -3,35 +3,35 @@ */ export const mockCategoricalData = [ { - key: "Roboto", + category: "Roboto", value: 88, }, { - key: "Open Sans", + category: "Open Sans", value: 16, }, { - key: "Lato", + category: "Lato", value: 14, }, { - key: "Montserrat", + category: "Montserrat", value: 73, }, { - key: "Oswald", + category: "Oswald", value: 14, }, { - key: "Source Sans Pro", + category: "Source Sans Pro", value: 8, }, { - key: "Slabo 27px", + category: "Slabo 27px", value: 41, }, { - key: "Raleway", + category: "Raleway", value: 29, }, ]; diff --git a/package-lock.json b/package-lock.json index a0d527f..6990a90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,10 @@ "dependencies": { "@visx/axis": "^2.10.0", "@visx/grid": "^2.10.0", + "@visx/group": "^2.10.0", + "@visx/scale": "^2.2.2", "@visx/shape": "^2.10.0", + "@visx/text": "^2.10.0", "next": "12.1.6", "react": "18.1.0", "react-dom": "18.1.0" diff --git a/package.json b/package.json index 5ac9543..fc11697 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "dependencies": { "@visx/axis": "^2.10.0", "@visx/grid": "^2.10.0", + "@visx/group": "^2.10.0", + "@visx/scale": "^2.2.2", "@visx/shape": "^2.10.0", + "@visx/text": "^2.10.0", "next": "12.1.6", "react": "18.1.0", "react-dom": "18.1.0" diff --git a/pages/playground.module.css b/pages/playground.module.css new file mode 100644 index 0000000..a7aefd8 --- /dev/null +++ b/pages/playground.module.css @@ -0,0 +1,7 @@ +.page { + padding: calc(8rem / 16); +} + +.barGraphDemo { + border: calc(1rem / 16) solid black; +} diff --git a/pages/playground.tsx b/pages/playground.tsx index f430e95..3f8a90b 100644 --- a/pages/playground.tsx +++ b/pages/playground.tsx @@ -1,13 +1,53 @@ +import { BarGraphHorizontal, BarGraphVertical } from "components/BarGraph"; +import { mockCategoricalData } from "data/mocks"; import React from "react"; import { ColorPalette } from "../components/ColorPalette"; +import styles from "./playground.module.css"; + export default function Home() { return ( - <> +

Playground

Show off your components here!

- + +

+ {""} +

+ + +

+ {""} +

+

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

+ +
); }