Closed alex-bynd closed 2 years ago
Confirmed this is possible with the code below:
import React from 'react';
import { BarStackHorizontal } from '@visx/shape';
import { Group } from '@visx/group';
import { AxisBottom, AxisLeft } from '@visx/axis';
import cityTemperature, { CityTemperature } from '@visx/mock-data/lib/mocks/cityTemperature';
import { scaleBand, scaleLinear, scaleOrdinal } from '@visx/scale';
import { timeParse, timeFormat } from 'd3-time-format';
import { withTooltip, Tooltip, defaultStyles } from '@visx/tooltip';
import { WithTooltipProvidedProps } from '@visx/tooltip/lib/enhancers/withTooltip';
import { LegendOrdinal } from '@visx/legend';
const purple1 = '#6c5efb';
const purple2 = '#c998ff';
export const purple3 = '#a44afe';
export const background = '#eaedff';
const defaultMargin = { top: 40, left: 50, right: 40, bottom: 100 };
const tooltipStyles = {
...defaultStyles,
minWidth: 60,
backgroundColor: 'rgba(0,0,0,0.9)',
color: 'white',
};
const data = cityTemperature.slice(0, 2);
const keys = Object.keys(data[0]).filter((d) => d !== 'date');
const temperatureTotals = data.reduce((allTotals, currentDate) => {
const totalTemperature = keys.reduce((dailyTotal, k) => {
dailyTotal += Number(currentDate[k]);
return dailyTotal;
}, 0);
allTotals.push(totalTemperature);
return allTotals;
}, []);
const parseDate = timeParse('%Y-%m-%d');
const format = timeFormat('%b %d');
const formatDate = (date) => format(parseDate(date));
// accessors
const getDate = (d) => d.date;
// scales
const temperatureScale = scaleLinear({
domain: [0, Math.max(...temperatureTotals)],
nice: true,
});
const dateScale = scaleBand({
domain: data.map(getDate),
padding: 0.2,
});
const colorScale = scaleOrdinal({
domain: keys,
range: [purple1, purple2, purple3],
});
let tooltipTimeout;
export default withTooltip(
({
width,
height,
events = false,
margin = defaultMargin,
tooltipOpen,
tooltipLeft,
tooltipTop,
tooltipData,
hideTooltip,
showTooltip,
}) => {
// bounds
const xMax = width - margin.left - margin.right;
const yMax = height - margin.top - margin.bottom;
temperatureScale.rangeRound([0, xMax]);
dateScale.rangeRound([yMax, 0]);
return width < 10 ? null : (
<div>
<svg width={width} height={height}>
<rect width={width} height={height} fill={background} rx={14} />
<Group top={margin.top} left={margin.left}>
<BarStackHorizontal
data={data}
keys={keys}
height={yMax}
y={getDate}
xScale={temperatureScale}
yScale={dateScale}
color={colorScale}
>
{(barStacks) =>
barStacks.map((barStack) =>
barStack.bars.map((bar, index) => (
<rect
key={`barstack-horizontal-${barStack.index}-${bar.index}`}
x={bar.x}
y={index * 40}
width={bar.width}
height={30}
fill={bar.color}
onClick={() => {
if (events) alert(`clicked: ${JSON.stringify(bar)}`);
}}
onMouseLeave={() => {
tooltipTimeout = window.setTimeout(() => {
hideTooltip();
}, 300);
}}
onMouseMove={() => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
const top = bar.y + margin.top;
const left = bar.x + bar.width + margin.left;
showTooltip({
tooltipData: bar,
tooltipTop: top,
tooltipLeft: left,
});
}}
/>
)),
)
}
</BarStackHorizontal>
</Group>
<Group top={margin.top + 110} left={margin.left}>
<BarStackHorizontal
data={data}
keys={keys}
height={yMax}
y={getDate}
xScale={temperatureScale}
yScale={dateScale}
color={colorScale}
>
{(barStacks) =>
barStacks.map((barStack) =>
barStack.bars.map((bar, index) => (
<rect
key={`barstack-horizontal-${barStack.index}-${bar.index}`}
x={bar.x}
y={index * 40}
width={bar.width}
height={30}
fill={bar.color}
onClick={() => {
if (events) alert(`clicked: ${JSON.stringify(bar)}`);
}}
onMouseLeave={() => {
tooltipTimeout = window.setTimeout(() => {
hideTooltip();
}, 300);
}}
onMouseMove={() => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
const top = bar.y + margin.top;
const left = bar.x + bar.width + margin.left;
showTooltip({
tooltipData: bar,
tooltipTop: top,
tooltipLeft: left,
});
}}
/>
)),
)
}
</BarStackHorizontal>
</Group>
<Group top={margin.top + 220} left={margin.left}>
<BarStackHorizontal
data={data}
keys={keys}
height={yMax}
y={getDate}
xScale={temperatureScale}
yScale={dateScale}
color={colorScale}
>
{(barStacks) =>
barStacks.map((barStack) =>
barStack.bars.map((bar, index) => (
<rect
key={`barstack-horizontal-${barStack.index}-${bar.index}`}
x={bar.x}
y={index * 40}
width={bar.width}
height={30}
fill={bar.color}
onClick={() => {
if (events) alert(`clicked: ${JSON.stringify(bar)}`);
}}
onMouseLeave={() => {
tooltipTimeout = window.setTimeout(() => {
hideTooltip();
}, 300);
}}
onMouseMove={() => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
const top = bar.y + margin.top;
const left = bar.x + bar.width + margin.left;
showTooltip({
tooltipData: bar,
tooltipTop: top,
tooltipLeft: left,
});
}}
/>
)),
)
}
</BarStackHorizontal>
</Group>
<AxisLeft
hideAxisLine
hideTicks
scale={dateScale}
tickFormat={formatDate}
stroke={purple3}
tickStroke={purple3}
tickLabelProps={() => ({
fill: purple3,
fontSize: 11,
textAnchor: 'end',
dy: '0.33em',
})}
/>
<AxisBottom
left={50}
top={yMax + 50}
scale={temperatureScale}
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 && (
<Tooltip 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>
</Tooltip>
)}
</div>
);
},
);
Thanks @alex-bynd for exploring this, I'd never seen an example of it. We could look into adding this as a gallery example if you're interested / happy to 👀 a PR.
Another implementation now available here
The implementation above doesn't work, it's missing some props. Would be nice to have an actual example in the gallery, trying currently to implement it but if anybody has a better example I'm interested.
Ideally it would look like:
<BarGroup> Something like bargroup.map(group => <BarStack>.... </BarStack> </BarGroup>
Is it possible to display groups of stacked bars? The gallery does not show an example like this but it seems like it should be possible.