fix visual nits + tooltip

This commit is contained in:
e26chiu 2022-09-03 10:36:03 -04:00
parent e346069421
commit d53468a0eb
3 changed files with 187 additions and 216 deletions

View File

@ -9,7 +9,7 @@
.legend { .legend {
position: absolute; position: absolute;
display: flex; display: flex;
font-size: 16px; font-size: calc(16rem / 16);
top: 0; top: 0;
} }
@ -27,7 +27,7 @@
} }
.toolTip p { .toolTip p {
margin: 0 5px; margin: 0 calc(5rem / 16);
font-size: calc(16rem / 16); font-size: calc(16rem / 16);
} }

View File

@ -7,7 +7,9 @@ import { Point } from "@visx/point";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale"; import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { BarStack, Line } from "@visx/shape"; import { BarStack, Line } from "@visx/shape";
import { SeriesPoint } from "@visx/shape/lib/types"; import { SeriesPoint } from "@visx/shape/lib/types";
import { useTooltip, useTooltipInPortal, defaultStyles } from "@visx/tooltip"; //import { useTooltip, useTooltipInPortal, withTooltip } from "@visx/tooltip";
import { withTooltip, Tooltip } from "@visx/tooltip";
import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip";
import React from "react"; import React from "react";
import { Color } from "utils/Color"; import { Color } from "utils/Color";
@ -45,6 +47,8 @@ export type StackedBarProps = {
numTicksLeftAxis?: number; numTicksLeftAxis?: number;
/** Distance between the left axis labels and the start of the lines of the graph, in px. */ /** Distance between the left axis labels and the start of the lines of the graph, in px. */
valueAxisLeftOffset?: number; valueAxisLeftOffset?: number;
/** Distance between the bottom axis and the bottom of the container of the graph, in px. */
categoryAxisBottomOffset?: number;
/** Distance between the right side of the graph and the legend, in px. */ /** Distance between the right side of the graph and the legend, in px. */
legendLeftOffset?: number; legendLeftOffset?: number;
/** Distance between the top of the graph and the legend, in px. */ /** Distance between the top of the graph and the legend, in px. */
@ -57,204 +61,201 @@ export type StackedBarProps = {
scalePadding?: number; scalePadding?: number;
/** Margin for each item in the legend */ /** Margin for each item in the legend */
itemMargin?: string; itemMargin?: string;
/** Factor multiplied to the left offset to center the labels in the x-axis. >1 for width <600 and <1 for width >600 */
categoryAxisLeftFactor?: number;
}; };
let tooltipTimeout: number; let tooltipTimeout: number;
export function StackedBarGraph({ export const StackedBarGraph = withTooltip<StackedBarProps, TooltipData>(
data, ({
width, data,
height, width,
keys, height,
colorRange, keys,
margin, colorRange,
scalePadding = 0.3, margin,
numTicksLeftAxis = 6, scalePadding = 0.3,
valueAxisLeftOffset = 40, numTicksLeftAxis = 6,
strokeWidth = 2.5, valueAxisLeftOffset = 40,
strokeDashArray = "10,4", categoryAxisBottomOffset = 40,
legendLeftOffset = 40, strokeWidth = 2.5,
legendTopOffset = 40, strokeDashArray = "10,4",
itemMargin = "15px 0 0 0", legendLeftOffset = 40,
}: StackedBarProps) { legendTopOffset = 40,
const yTotals = data.reduce((allTotals, currCategory) => { itemMargin = "15px 0 0 0",
const yTotal = keys.reduce((categoryTotal, k) => { categoryAxisLeftFactor = 1,
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 xScale = scaleBand<string>({
domain: data.map(getCategory),
padding: scalePadding,
});
const yScale = scaleLinear<number>({
domain: [0, Math.max(...yTotals)],
nice: true,
});
const colorScale = scaleOrdinal<string, string>({
domain: keys,
range: colorRange,
});
const {
tooltipOpen, tooltipOpen,
tooltipLeft, tooltipLeft,
tooltipTop, tooltipTop,
tooltipData, tooltipData,
hideTooltip, hideTooltip,
showTooltip, showTooltip,
} = useTooltip<TooltipData>(); }: 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 { containerRef, TooltipInPortal } = useTooltipInPortal({ const TICK_LABEL_FONT_WEIGHT = 800;
// TooltipInPortal is rendered in a separate child of <body /> and positioned
// with page coordinates which should be updated on scroll.
scroll: true,
});
if (width < 10) return null; // accessors
// bounds const getCategory = (d: StackedBarData) => d.category;
const xMax = width;
const yMax = height - margin.top - 50;
xScale.rangeRound([0, xMax - valueAxisLeftOffset]); // scales
yScale.range([yMax, 0]); const xScale = scaleBand<string>({
domain: data.map(getCategory),
padding: scalePadding,
});
const yScale = scaleLinear<number>({
domain: [0, Math.max(...yTotals)],
nice: true,
});
const colorScale = scaleOrdinal<string, string>({
domain: keys,
range: colorRange,
});
return width < 10 ? null : ( // bounds
<div className={styles.container}> const xMax = width;
<svg ref={containerRef} width={width} height={height}> const yMax = height - margin.top - categoryAxisBottomOffset;
<Group top={margin.top} left={margin.left}>
<GridRows xScale.rangeRound([0, xMax - valueAxisLeftOffset]);
scale={yScale} yScale.range([yMax, 0]);
width={xMax}
height={yMax} return width < 10 ? null : (
left={valueAxisLeftOffset} <div className={styles.container}>
numTicks={numTicksLeftAxis} <svg width={width} height={height}>
stroke={Color.tertiaryBackground} <Group top={margin.top} left={margin.left}>
strokeWidth={strokeWidth} <GridRows
strokeDasharray={strokeDashArray} scale={yScale}
/> width={xMax}
<GridColumns height={yMax}
scale={xScale} left={valueAxisLeftOffset}
height={yMax} numTicks={numTicksLeftAxis}
left={xScale.bandwidth() / 2} stroke={Color.tertiaryBackground}
offset={xScale.bandwidth() / 2} strokeWidth={strokeWidth}
stroke={Color.tertiaryBackground} strokeDasharray={strokeDashArray}
strokeWidth={strokeWidth} />
strokeDasharray={strokeDashArray} <GridColumns
/> scale={xScale}
<Group left={valueAxisLeftOffset}> height={yMax}
<BarStack<StackedBarData, string> left={valueAxisLeftOffset}
data={data} offset={xScale.bandwidth() / 2}
keys={keys} stroke={Color.tertiaryBackground}
x={getCategory} strokeWidth={strokeWidth}
xScale={xScale} strokeDasharray={strokeDashArray}
yScale={yScale} />
color={colorScale} <Group left={valueAxisLeftOffset}>
> <BarStack<StackedBarData, string>
{(barStacks) => data={data}
barStacks.map((barStack) => keys={keys}
barStack.bars.map((bar) => ( x={getCategory}
<rect xScale={xScale}
className={styles.barStack} yScale={yScale}
key={`bar-stack-${barStack.index}-${bar.index}`} color={colorScale}
x={bar.x} >
y={bar.y} {(barStacks) =>
height={bar.height} barStacks.map((barStack) =>
width={bar.width / 2} barStack.bars.map((bar) => (
fill={bar.color} <rect
onMouseLeave={() => { className={styles.barStack}
tooltipTimeout = window.setTimeout(() => { key={`bar-stack-${barStack.index}-${bar.index}`}
hideTooltip(); x={bar.x}
}, 300); y={bar.y}
}} height={bar.height}
onMouseMove={(event) => { width={bar.width / 2}
if (tooltipTimeout) clearTimeout(tooltipTimeout); fill={bar.color}
// TooltipInPortal expects coordinates to be relative to containerRef onMouseLeave={() => {
// localPoint returns coordinates relative to the nearest SVG tooltipTimeout = window.setTimeout(() => {
const eventSvgCoords = localPoint(event); hideTooltip();
const left = bar.x + bar.width / 2; }, 300);
showTooltip({ }}
tooltipData: bar, onMouseMove={(event) => {
tooltipTop: eventSvgCoords?.y, if (tooltipTimeout) clearTimeout(tooltipTimeout);
tooltipLeft: left, 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: valueAxisLeftOffset, y: 0 })}
from={new Point({ x: valueAxisLeftOffset, y: yMax })}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<AxisBottom
top={yMax}
scale={xScale}
left={
((xScale.bandwidth() * 100) / width) * categoryAxisLeftFactor
} }
</BarStack> hideTicks
</Group> hideAxisLine
<Line labelProps={{
fill={Color.tertiaryBackground} fontSize: `${10 / 16}rem`,
to={new Point({ x: valueAxisLeftOffset, y: 0 })} }}
from={new Point({ x: valueAxisLeftOffset, y: yMax })} tickLabelProps={() => ({
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<AxisBottom
top={yMax}
scale={xScale}
left={xScale.bandwidth() / 6}
hideTicks
hideAxisLine
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => ({
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
})}
/>
<AxisLeft
scale={yScale}
top={5}
numTicks={numTicksLeftAxis}
hideAxisLine
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => {
return {
fill: Color.label, fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT, fontWeight: TICK_LABEL_FONT_WEIGHT,
}; })}
}} />
/> <AxisLeft
</Group> scale={yScale}
</svg> top={5}
<div numTicks={numTicksLeftAxis}
className={styles.legend} hideAxisLine
style={{ left: width + legendLeftOffset, top: legendTopOffset }} labelProps={{
> fontSize: `${10 / 16}rem`,
<LegendOrdinal }}
scale={colorScale} tickLabelProps={() => {
direction="column" return {
itemMargin={itemMargin} fill: Color.label,
/> fontWeight: TICK_LABEL_FONT_WEIGHT,
</div> };
}}
{tooltipOpen && tooltipData ? ( />
<TooltipInPortal </Group>
className={styles.toolTip} </svg>
top={tooltipTop} <div
left={tooltipLeft} className={styles.legend}
unstyled style={{ left: width + legendLeftOffset, top: legendTopOffset }}
> >
<p className={styles.key}>{tooltipData.key}</p> <LegendOrdinal
<p>{tooltipData.bar.data[tooltipData.key]}</p> scale={colorScale}
<p>{getCategory(tooltipData.bar.data)}</p> direction="column"
</TooltipInPortal> itemMargin={itemMargin}
) : null} />
</div> </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

@ -65,36 +65,6 @@ export const moreMockCategoricalData = [
{ key: "Dart", value: 2.21 }, { key: "Dart", value: 2.21 },
]; ];
export const mockBoxPlotData = [
{
x: "1A",
min: 20,
firstQuartile: 25,
median: 30,
thirdQuartile: 80,
max: 100,
outliers: [],
},
{
x: "1B",
min: 0,
firstQuartile: 20,
median: 30,
thirdQuartile: 50,
max: 100,
outliers: [],
},
{
x: "2A",
min: 25,
firstQuartile: 35,
median: 50,
thirdQuartile: 90,
max: 100,
outliers: [],
},
];
export const mockStackedBarGraphData = [ export const mockStackedBarGraphData = [
{ {
category: "1A", category: "1A",