commit
87ed199f11
@ -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); |
||||
} |
@ -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 ( |
||||
<svg className={className} width={width} height={height}> |
||||
<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 - barPadding); |
||||
return idx % 2 === 0 ? ( |
||||
<Bar |
||||
className={styles.barBackground} |
||||
key={`bar-${barName}-background`} |
||||
x={0} |
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
y={categoryPoint(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" |
||||
/> |
||||
<Group> |
||||
{data.map((d, idx) => { |
||||
const barName = `${getCategory(d)}-${idx}`; |
||||
const barLength = valuePoint(d); |
||||
const barWidth = categoryScale.bandwidth(); |
||||
return ( |
||||
<Group className={styles.barGroup} key={`bar-${barName}`}> |
||||
<Bar |
||||
className={styles.bar} |
||||
x={0} |
||||
y={categoryPoint(d)} |
||||
width={barLength} |
||||
height={barWidth} |
||||
/> |
||||
<Text |
||||
className={styles.barText} |
||||
x={valuePoint(d) - 12} |
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
y={categoryPoint(d)! + barWidth / 2} |
||||
fontSize={hoverLabelSize ?? barWidth * 0.75} |
||||
textAnchor="end" |
||||
verticalAnchor="middle" |
||||
> |
||||
{getValue(d)} |
||||
</Text> |
||||
</Group> |
||||
); |
||||
})} |
||||
</Group> |
||||
</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: `${categoryTickLabelSize / 16}rem`, |
||||
}; |
||||
}} |
||||
label={categoryAxisLabel} |
||||
labelClassName={styles.axisLabel} |
||||
labelOffset={categoryAxisLabelOffset} |
||||
labelProps={{ |
||||
fontSize: `${categoryAxisLabelSize / 16}rem`, |
||||
}} |
||||
/> |
||||
<AxisBottom |
||||
scale={valueScale} |
||||
top={margin.top + categoryMax} |
||||
left={margin.left} |
||||
hideAxisLine |
||||
hideTicks |
||||
numTicks={5} |
||||
tickLabelProps={() => { |
||||
return { |
||||
...bottomTickLabelProps(), |
||||
className: styles.tickLabel, |
||||
dy: "0.25rem", |
||||
fontSize: `${valueTickLabelSize / 16}rem`, |
||||
}; |
||||
}} |
||||
label={valueAxisLabel} |
||||
labelClassName={styles.axisLabel} |
||||
labelOffset={valueAxisLabelOffset} |
||||
labelProps={{ |
||||
fontSize: `${valueAxisLabelSize / 16}rem`, |
||||
}} |
||||
/> |
||||
</svg> |
||||
); |
||||
} |
||||
|
||||
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 ( |
||||
<svg className={className} width={width} height={height}> |
||||
<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 - barPadding); |
||||
return idx % 2 === 0 ? ( |
||||
<Bar |
||||
className={styles.barBackground} |
||||
key={`bar-${barName}-background`} |
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
x={categoryPoint(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" |
||||
/> |
||||
<Group> |
||||
{data.map((d, idx) => { |
||||
const barName = `${getCategory(d)}-${idx}`; |
||||
const barHeight = valueMax - valuePoint(d); |
||||
const barWidth = categoryScale.bandwidth(); |
||||
return ( |
||||
<Group className={styles.barGroup} key={`bar-${barName}`}> |
||||
<Bar |
||||
className={styles.bar} |
||||
x={categoryPoint(d)} |
||||
y={valueMax - barHeight} |
||||
width={barWidth} |
||||
height={barHeight} |
||||
/> |
||||
<Text |
||||
className={styles.barText} |
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
x={categoryPoint(d)! + barWidth / 2} |
||||
y={valueMax - barHeight + 12} |
||||
fontSize={hoverLabelSize ?? barWidth * 0.5} |
||||
textAnchor="middle" |
||||
verticalAnchor="start" |
||||
> |
||||
{getValue(d)} |
||||
</Text> |
||||
</Group> |
||||
); |
||||
})} |
||||
</Group> |
||||
</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> |
||||
); |
||||
} |
@ -0,0 +1,7 @@ |
||||
.page { |
||||
padding: calc(8rem / 16); |
||||
} |
||||
|
||||
.barGraphDemo { |
||||
border: calc(1rem / 16) solid black; |
||||
} |
@ -1,19 +1,59 @@ |
||||
import { mockPieData } from "data/mocks"; |
||||
import { BarGraphHorizontal, BarGraphVertical } from "components/BarGraph"; |
||||
import { mockCategoricalData } from "data/mocks"; |
||||
import React from "react"; |
||||
|
||||
import { PieChart } from "@/components/PieChart"; |
||||
|
||||
import { ColorPalette } from "../components/ColorPalette"; |
||||
|
||||
import styles from "./playground.module.css"; |
||||
|
||||
export default function Home() { |
||||
return ( |
||||
<> |
||||
<div className={styles.page}> |
||||
<h1>Playground</h1> |
||||
<p>Show off your components here!</p> |
||||
<div style={{ padding: "30px" }}> |
||||
<PieChart data={mockPieData} width={800} labelWidth={215} /> |
||||
</div> |
||||
<ColorPalette /> |
||||
</> |
||||
|
||||
<h2> |
||||
<code>{"<BarGraphHorizontal />"}</code> |
||||
</h2> |
||||
<BarGraphHorizontal |
||||
className={styles.barGraphDemo} |
||||
data={mockCategoricalData} |
||||
width={800} |
||||
height={500} |
||||
margin={{ |
||||
top: 20, |
||||
bottom: 40, |
||||
left: 150, |
||||
right: 20, |
||||
}} |
||||
/> |
||||
|
||||
<h2> |
||||
<code>{"<BarGraphVertical />"}</code> |
||||
</h2> |
||||
<p> |
||||
<code>{"<BarGraphVertical />"}</code> takes the same props as{" "} |
||||
<code>{"<BarGraphHorizontal />"}</code>. |
||||
</p> |
||||
<BarGraphVertical |
||||
className={styles.barGraphDemo} |
||||
data={mockCategoricalData} |
||||
width={800} |
||||
height={500} |
||||
margin={{ |
||||
top: 20, |
||||
bottom: 80, |
||||
left: 60, |
||||
right: 20, |
||||
}} |
||||
/> |
||||
</div> |
||||
); |
||||
} |
||||
|
Loading…
Reference in new issue