208 lines
7.7 KiB
TypeScript
208 lines
7.7 KiB
TypeScript
import React from 'react';
|
|
import { BarStack } from '@visx/shape';
|
|
import { SeriesPoint } from '@visx/shape/lib/types';
|
|
import { Group } from '@visx/group';
|
|
import { Grid } from '@visx/grid';
|
|
import { AxisBottom } from '@visx/axis';
|
|
import cityTemperature, { CityTemperature } from '@visx/mock-data/lib/mocks/cityTemperature';
|
|
import { scaleBand, scaleLinear, scaleOrdinal } from '@visx/scale';
|
|
import { timeParse, timeFormat } from '@visx/vendor/d3-time-format';
|
|
import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip';
|
|
import { LegendOrdinal } from '@visx/legend';
|
|
import { localPoint } from '@visx/event';
|
|
|
|
type CityName = 'New York' | 'San Francisco' | 'Austin';
|
|
|
|
type TooltipData = {
|
|
bar: SeriesPoint<CityTemperature>;
|
|
key: CityName;
|
|
index: number;
|
|
height: number;
|
|
width: number;
|
|
x: number;
|
|
y: number;
|
|
color: string;
|
|
};
|
|
|
|
export type BarStackProps = {
|
|
width: number;
|
|
height: number;
|
|
margin?: { top: number; right: number; bottom: number; left: number };
|
|
events?: boolean;
|
|
};
|
|
|
|
const purple1 = '#6c5efb';
|
|
const purple2 = '#c998ff';
|
|
export const purple3 = '#a44afe';
|
|
export const background = 'transparent';
|
|
const defaultMargin = { top: 40, right: 0, bottom: 0, left: 0 };
|
|
const tooltipStyles = {
|
|
...defaultStyles,
|
|
minWidth: 60,
|
|
backgroundColor: 'rgba(0,0,0,0.9)',
|
|
color: 'white',
|
|
};
|
|
|
|
const data = cityTemperature.slice(0, 12);
|
|
const keys = Object.keys(data[0]).filter((d) => d !== 'date') as CityName[];
|
|
|
|
const temperatureTotals = data.reduce((allTotals, currentDate) => {
|
|
const totalTemperature = keys.reduce((dailyTotal, k) => {
|
|
dailyTotal += Number(currentDate[k]);
|
|
return dailyTotal;
|
|
}, 0);
|
|
allTotals.push(totalTemperature);
|
|
return allTotals;
|
|
}, [] as number[]);
|
|
|
|
const parseDate = timeParse('%Y-%m-%d');
|
|
const format = timeFormat('%b %d');
|
|
const formatDate = (date: string) => format(parseDate(date) as Date);
|
|
|
|
// accessors
|
|
const getDate = (d: CityTemperature) => d.date;
|
|
|
|
// scales
|
|
const dateScale = scaleBand<string>({
|
|
domain: data.map(getDate),
|
|
padding: 0.2,
|
|
});
|
|
const temperatureScale = scaleLinear<number>({
|
|
domain: [0, Math.max(...temperatureTotals)],
|
|
nice: true,
|
|
});
|
|
const colorScale = scaleOrdinal<CityName, string>({
|
|
domain: keys,
|
|
range: [purple1, purple2, purple3],
|
|
});
|
|
|
|
let tooltipTimeout: number;
|
|
|
|
console.log(cityTemperature);
|
|
|
|
export default function BarStackChart({
|
|
width,
|
|
height,
|
|
events = false,
|
|
margin = defaultMargin,
|
|
}: BarStackProps) {
|
|
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
|
|
useTooltip<TooltipData>();
|
|
|
|
const { containerRef, TooltipInPortal } = useTooltipInPortal({
|
|
// TooltipInPortal is rendered in a separate child of <body /> and positioned
|
|
// with page coordinates which should be updated on scroll. consider using
|
|
// Tooltip or TooltipWithBounds if you don't need to render inside a Portal
|
|
scroll: true,
|
|
});
|
|
|
|
if (width < 10) return null;
|
|
// bounds
|
|
const xMax = width;
|
|
const yMax = height - margin.top - 100;
|
|
|
|
dateScale.rangeRound([0, xMax]);
|
|
temperatureScale.range([yMax, 0]);
|
|
|
|
return width < 10 ? null : (
|
|
<div style={{ position: 'relative' }}>
|
|
<svg ref={containerRef} width={width} height={height}>
|
|
<rect x={0} y={0} width={width} height={height} fill={background} rx={14} />
|
|
<Grid
|
|
top={margin.top}
|
|
left={margin.left}
|
|
xScale={dateScale}
|
|
yScale={temperatureScale}
|
|
width={xMax}
|
|
height={yMax}
|
|
stroke="black"
|
|
strokeOpacity={0.1}
|
|
xOffset={dateScale.bandwidth() / 2}
|
|
/>
|
|
<Group top={margin.top}>
|
|
<BarStack<CityTemperature, CityName>
|
|
data={data}
|
|
keys={keys}
|
|
x={getDate}
|
|
xScale={dateScale}
|
|
yScale={temperatureScale}
|
|
color={colorScale}
|
|
>
|
|
{(barStacks) =>
|
|
barStacks.map((barStack) =>
|
|
barStack.bars.map((bar) => (
|
|
<rect
|
|
key={`bar-stack-${barStack.index}-${bar.index}`}
|
|
x={bar.x}
|
|
y={bar.y}
|
|
height={bar.height}
|
|
width={bar.width}
|
|
fill={bar.color}
|
|
onClick={() => {
|
|
if (events) alert(`clicked: ${JSON.stringify(bar)}`);
|
|
}}
|
|
onMouseLeave={() => {
|
|
tooltipTimeout = window.setTimeout(() => {
|
|
hideTooltip();
|
|
}, 300);
|
|
}}
|
|
onMouseMove={(event) => {
|
|
if (tooltipTimeout) clearTimeout(tooltipTimeout);
|
|
// TooltipInPortal expects coordinates to be relative to containerRef
|
|
// localPoint returns coordinates relative to the nearest SVG, which
|
|
// is what containerRef is set to in this example.
|
|
const eventSvgCoords = localPoint(event);
|
|
const left = bar.x + bar.width / 2;
|
|
showTooltip({
|
|
tooltipData: bar,
|
|
tooltipTop: eventSvgCoords?.y,
|
|
tooltipLeft: left,
|
|
});
|
|
}}
|
|
/>
|
|
)),
|
|
)
|
|
}
|
|
</BarStack>
|
|
</Group>
|
|
<AxisBottom
|
|
top={yMax + margin.top}
|
|
scale={dateScale}
|
|
tickFormat={formatDate}
|
|
stroke={purple3}
|
|
tickStroke={purple3}
|
|
tickLabelProps={{
|
|
fill: purple3,
|
|
fontSize: 11,
|
|
textAnchor: 'middle',
|
|
}}
|
|
/>
|
|
</svg>
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: margin.top / 2 - 10,
|
|
width: '100%',
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
fontSize: '14px',
|
|
}}
|
|
>
|
|
<LegendOrdinal scale={colorScale} direction="row" labelMargin="0 15px 0 0" />
|
|
</div>
|
|
|
|
{tooltipOpen && tooltipData && (
|
|
<TooltipInPortal top={tooltipTop} left={tooltipLeft} style={tooltipStyles}>
|
|
<div style={{ color: colorScale(tooltipData.key) }}>
|
|
<strong>{tooltipData.key}</strong>
|
|
</div>
|
|
<div>{tooltipData.bar.data[tooltipData.key]}℉</div>
|
|
<div>
|
|
<small>{formatDate(getDate(tooltipData.bar.data))}</small>
|
|
</div>
|
|
</TooltipInPortal>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|