Fix tooltip not being correctly positioned for certain centered graphs (#118)
continuous-integration/drone/push Build is passing Details

Applied the fix used by the WordCloud to all graphs so that tooltips will be properly positioned, even if the graph is centered on the page. Did so by refactoring some tooltip logic in to a common getTooltipPosition used by all graphs.

One thing is that this simplifies all the tooltips to simply follow the mouse, instead of locking onto certain "blocks"/bars like in the grouped bar graph, which is more consitant with all the other tooltips, since we only had this in some cases.

Do we think that having the tooltip be sticky to a certain bar/group is important?

Closes #89

https://fix-tooltip-center-csc-class-profile-staging-snedadah.k8s.csclub.cloud/samplePage/
Co-authored-by: shahanneda <shahan.neda@gmail.com>
Reviewed-on: #118
Reviewed-by: Mark Chiu <e26chiu@csclub.uwaterloo.ca>
This commit is contained in:
Shahan Nedadahandeh 2022-12-28 01:54:32 -05:00
parent b8590f7a28
commit 743f050ec8
11 changed files with 121 additions and 171 deletions

View File

@ -1,17 +1,15 @@
import { AxisBottom, AxisLeft } from "@visx/axis";
import { bottomTickLabelProps } from "@visx/axis/lib/axis/AxisBottom";
import { leftTickLabelProps } from "@visx/axis/lib/axis/AxisLeft";
import { localPoint } from "@visx/event";
import { GridColumns, GridRows } from "@visx/grid";
import { Group } from "@visx/group";
import { Point } from "@visx/point";
import { scaleBand, scaleLinear } from "@visx/scale";
import { Bar } from "@visx/shape";
import { withTooltip } from "@visx/tooltip";
import React from "react";
import { Color } from "utils/Color";
import { TooltipWrapper } from "./TooltipWrapper";
import { getTooltipPosition, TooltipWrapper } from "./TooltipWrapper";
import styles from "./BarGraph.module.css";
@ -154,17 +152,11 @@ export const BarGraphHorizontal = withTooltip<BarGraphProps, TooltipData>(
<Group className={styles.barGroup} key={`bar-${barName}`}>
<Bar
onMouseMove={(e) => {
const eventSvgCoords = localPoint(
// ownerSVGElement is given by visx docs but not recognized by typescript
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.ownerSVGElement as Element,
e
) as Point;
const tooltipPos = getTooltipPosition(e);
showTooltip({
tooltipData: getValue(d).toString(),
tooltipTop: eventSvgCoords.y,
tooltipLeft: eventSvgCoords.x,
tooltipLeft: tooltipPos.x,
tooltipTop: tooltipPos.y,
});
}}
onMouseOut={hideTooltip}
@ -327,17 +319,11 @@ export const BarGraphVertical = withTooltip<BarGraphProps, TooltipData>(
<Group className={styles.barGroup} key={`bar-${barName}`}>
<Bar
onMouseMove={(e) => {
const eventSvgCoords = localPoint(
// ownerSVGElement is given by visx docs but not recognized by typescript
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.ownerSVGElement as Element,
e
) as Point;
const tooltipPos = getTooltipPosition(e);
showTooltip({
tooltipData: getValue(d).toString(),
tooltipTop: eventSvgCoords.y,
tooltipLeft: eventSvgCoords.x,
tooltipLeft: tooltipPos.x,
tooltipTop: tooltipPos.y,
});
}}
onMouseOut={hideTooltip}

View File

@ -11,7 +11,7 @@ import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withToolti
import React from "react";
import { Color } from "utils/Color";
import { TooltipWrapper } from "./TooltipWrapper";
import { getTooltipPosition, TooltipWrapper } from "./TooltipWrapper";
import styles from "./Boxplot.module.css";
@ -96,8 +96,8 @@ export const BoxPlot = withTooltip<StatsPlotProps, TooltipData>(
valueAxisLabelTopOffset = 5,
valueAxisLabelLeftOffset = 10,
categoryAxisLabelLeftOffset = 30,
toolTipTopOffset = 20,
toolTipLeftOffset = 5,
toolTipTopOffset = 0,
toolTipLeftOffset = 0,
categoryAxisLabelSize = DEFAULT_LABEL_SIZE,
valueAxisLabelSize = DEFAULT_LABEL_SIZE,
boxPlotWidthFactor = 0.4,
@ -149,6 +149,21 @@ export const BoxPlot = withTooltip<StatsPlotProps, TooltipData>(
const constrainedWidth = Math.min(200, xScale.bandwidth());
const mouseOverEventHandler =
(d: Stats) =>
(e: React.MouseEvent<SVGLineElement | SVGRectElement, MouseEvent>) => {
const pos = getTooltipPosition(e);
showTooltip({
tooltipLeft: pos.x + toolTipLeftOffset,
tooltipTop: pos.y + toolTipTopOffset,
tooltipData: {
...d.boxPlot,
category: getX(d),
},
});
};
return width < 10 ? null : (
<div>
<svg width={width} height={height}>
@ -254,81 +269,23 @@ export const BoxPlot = withTooltip<StatsPlotProps, TooltipData>(
strokeWidth={strokeWidth}
valueScale={yScale}
minProps={{
onMouseOver: () => {
showTooltip({
tooltipTop:
(yScale(getMin(d)) ?? 0) + toolTipTopOffset,
tooltipLeft:
xScale(getX(d))! +
constrainedWidth +
toolTipLeftOffset,
tooltipData: {
...d.boxPlot,
category: getX(d),
},
});
},
onMouseLeave: () => {
hideTooltip();
},
onMouseMove: mouseOverEventHandler(d),
onMouseLeave: hideTooltip,
}}
maxProps={{
onMouseOver: () => {
showTooltip({
tooltipTop:
(yScale(getMax(d)) ?? 0) + toolTipTopOffset,
tooltipLeft:
xScale(getX(d))! +
constrainedWidth +
toolTipLeftOffset,
tooltipData: {
...d.boxPlot,
category: getX(d),
},
});
},
onMouseLeave: () => {
hideTooltip();
},
onMouseMove: mouseOverEventHandler(d),
onMouseLeave: hideTooltip,
}}
boxProps={{
onMouseOver: () => {
showTooltip({
tooltipTop:
(yScale(getMedian(d)) ?? 0) + toolTipTopOffset,
tooltipLeft:
xScale(getX(d))! +
constrainedWidth +
toolTipLeftOffset,
tooltipData: {
...d.boxPlot,
category: getX(d),
},
});
},
onMouseMove: mouseOverEventHandler(d),
strokeWidth: 0,
onMouseLeave: () => {
hideTooltip();
},
onMouseLeave: hideTooltip,
}}
medianProps={{
style: {
stroke: Color.label,
},
onMouseOver: () => {
showTooltip({
tooltipTop:
(yScale(getMedian(d)) ?? 0) + toolTipTopOffset,
tooltipLeft:
xScale(getX(d))! +
constrainedWidth +
toolTipLeftOffset,
tooltipData: {
...d.boxPlot,
category: getX(d),
},
});
},
onMouseMove: mouseOverEventHandler(d),
onMouseLeave: () => {
hideTooltip();
},

View File

@ -2,7 +2,6 @@
display: flex;
flex-direction: column;
align-items: center;
width: min-content;
}
.barBackground {
@ -25,4 +24,4 @@
display: flex;
margin: calc(16rem / 16);
justify-content: center;
}
}

View File

@ -1,11 +1,9 @@
import { AxisBottom, AxisLeft } from "@visx/axis";
import { bottomTickLabelProps } from "@visx/axis/lib/axis/AxisBottom";
import { leftTickLabelProps } from "@visx/axis/lib/axis/AxisLeft";
import { localPoint } from "@visx/event";
import { GridColumns, GridRows } from "@visx/grid";
import { Group } from "@visx/group";
import { LegendOrdinal } from "@visx/legend";
import { Point } from "@visx/point";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { Bar, BarGroup, BarGroupHorizontal } from "@visx/shape";
import { BarGroupBar as BarGroupBarType } from "@visx/shape/lib/types";
@ -13,7 +11,7 @@ import { withTooltip } from "@visx/tooltip";
import React, { useState } from "react";
import { Color } from "utils/Color";
import { TooltipWrapper } from "./TooltipWrapper";
import { getTooltipPosition, TooltipWrapper } from "./TooltipWrapper";
import styles from "./GroupedBarGraph.module.css";
@ -257,17 +255,12 @@ export const GroupedBarGraphVertical = withTooltip<
{barGroup.bars.map((bar) => (
<HoverableBar
onMouseMove={(e) => {
const eventSvgCoords = localPoint(
// ownerSVGElement is given by visx docs but not recognized by typescript
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.ownerSVGElement as Element,
e
) as Point;
const tooltipPos = getTooltipPosition(e);
showTooltip({
tooltipData: bar.value.toString(),
tooltipTop: eventSvgCoords.y,
tooltipLeft: eventSvgCoords.x,
tooltipTop: tooltipPos.y,
tooltipLeft: tooltipPos.x,
});
}}
onMouseOut={hideTooltip}
@ -504,17 +497,11 @@ export const GroupedBarGraphHorizontal = withTooltip<
{barGroup.bars.map((bar) => (
<HoverableBar
onMouseMove={(e) => {
const eventSvgCoords = localPoint(
// ownerSVGElement is given by visx docs but not recognized by typescript
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.ownerSVGElement as Element,
e
) as Point;
const tooltipPos = getTooltipPosition(e);
showTooltip({
tooltipData: bar.value.toString(),
tooltipTop: eventSvgCoords.y,
tooltipLeft: eventSvgCoords.x,
tooltipLeft: tooltipPos.x,
tooltipTop: tooltipPos.y,
});
}}
onMouseOut={hideTooltip}

View File

@ -1,18 +1,16 @@
import { AxisBottom, AxisLeft } from "@visx/axis";
import { bottomTickLabelProps } from "@visx/axis/lib/axis/AxisBottom";
import { leftTickLabelProps } from "@visx/axis/lib/axis/AxisLeft";
import { localPoint } from "@visx/event";
import { GridColumns, GridRows } from "@visx/grid";
import { Group } from "@visx/group";
import { LegendOrdinal } from "@visx/legend";
import { Point } from "@visx/point";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { LinePath } from "@visx/shape";
import { withTooltip } from "@visx/tooltip";
import React from "react";
import { Color } from "utils/Color";
import { TooltipWrapper } from "./TooltipWrapper";
import { getTooltipPosition, TooltipWrapper } from "./TooltipWrapper";
import styles from "./LineGraph.module.css";
@ -195,17 +193,11 @@ export const LineGraph = withTooltip<LineGraphProps, TooltipData>(
<Group key={`line-${i}`}>
<LinePath
onMouseMove={(e) => {
const eventSvgCoords = localPoint(
// ownerSVGElement is given by visx docs but not recognized by typescript
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.ownerSVGElement as Element,
e
) as Point;
const tooltipPos = getTooltipPosition(e);
showTooltip({
tooltipData: data.lines[i].label,
tooltipTop: eventSvgCoords.y,
tooltipLeft: eventSvgCoords.x,
tooltipLeft: tooltipPos.x,
tooltipTop: tooltipPos.y,
});
}}
onMouseOut={hideTooltip}

View File

@ -1,12 +1,10 @@
import { localPoint } from "@visx/event";
import { Group } from "@visx/group";
import { Point } from "@visx/point";
import Pie, { ProvidedProps } from "@visx/shape/lib/shapes/Pie";
import { Text } from "@visx/text";
import { withTooltip } from "@visx/tooltip";
import React from "react";
import { TooltipWrapper } from "./TooltipWrapper";
import { getTooltipPosition, TooltipWrapper } from "./TooltipWrapper";
import styles from "./PieChart.module.css";
@ -90,17 +88,11 @@ export const PieChart = withTooltip<PieChartProps>(
>
<path
onMouseMove={(e) => {
const eventSvgCoords = localPoint(
// ownerSVGElement is given by visx docs but not recognized by typescript
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.ownerSVGElement as Element,
e
) as Point;
const tooltipPos = getTooltipPosition(e);
showTooltip({
tooltipData: `${arc.data.category}: ${arc.data.value}%`,
tooltipTop: eventSvgCoords.y,
tooltipLeft: eventSvgCoords.x,
tooltipLeft: tooltipPos.x,
tooltipTop: tooltipPos.y,
});
}}
onMouseOut={hideTooltip}

View File

@ -10,6 +10,7 @@
display: flex;
font-size: calc(16rem / 16);
top: 0;
justify-content: center;
}
.key {

View File

@ -1,5 +1,4 @@
import { AxisLeft, AxisBottom } from "@visx/axis";
import { localPoint } from "@visx/event";
import { GridRows, GridColumns } from "@visx/grid";
import { Group } from "@visx/group";
import { LegendOrdinal } from "@visx/legend";
@ -12,7 +11,7 @@ import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withToolti
import React from "react";
import { Color } from "utils/Color";
import { TooltipWrapper } from "./TooltipWrapper";
import { getTooltipPosition, TooltipWrapper } from "./TooltipWrapper";
import styles from "./StackedBarGraph.module.css";
@ -194,12 +193,11 @@ export const StackedBarGraphVertical = withTooltip<
}}
onMouseMove={(event) => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
const eventSvgCoords = localPoint(event);
const left = bar.x + bar.width / 2;
const tooltipPos = getTooltipPosition(event);
showTooltip({
tooltipData: bar,
tooltipTop: eventSvgCoords?.y,
tooltipLeft: left,
tooltipLeft: tooltipPos.x,
tooltipTop: tooltipPos.y,
});
}}
/>
@ -390,12 +388,11 @@ export const StackedBarGraphHorizontal = withTooltip<
}}
onMouseMove={(event) => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
const eventSvgCoords = localPoint(event);
const left = bar.x + bar.width / 2;
const tooltipPos = getTooltipPosition(event);
showTooltip({
tooltipData: bar,
tooltipTop: eventSvgCoords?.y,
tooltipLeft: left,
tooltipLeft: tooltipPos.x,
tooltipTop: tooltipPos.y,
});
}}
/>

View File

@ -1,3 +1,5 @@
import localPoint from "@visx/event/lib/localPoint";
import { Point } from "@visx/point";
import { Tooltip } from "@visx/tooltip";
import React from "react";
@ -11,6 +13,23 @@ type TooltipWrapperProps = {
children?: React.ReactNode;
};
// Finds the SVG Element which is the outmost from element (highest parent of element which is svg)
function getOutmostSVG(element: Element): SVGElement | undefined {
let rootSVG: HTMLElement | Element | null = element;
let current: HTMLElement | Element | null = element;
while (current) {
console.log(current);
if (current.tagName == "svg") {
rootSVG = current;
}
current = current.parentElement;
}
return rootSVG as SVGElement;
}
const TooltipWrapper = ({
top,
left,
@ -32,4 +51,35 @@ const TooltipWrapper = ({
);
};
export { TooltipWrapper };
function getTooltipPosition(
e: React.MouseEvent<
SVGTextElement | SVGPathElement | SVGLineElement,
MouseEvent
>
) {
// ownerSVGElement is given by visx docs but not recognized by typescript
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const eventElement = e.target.ownerSVGElement as Element;
const eventSvgCoords = localPoint(eventElement, e) as Point;
const rootSVG: SVGElement | undefined = getOutmostSVG(eventElement);
if (!rootSVG) {
console.error("Failed to find parent SVG for tooltip!");
return { x: 0, y: 0 };
}
const rootSVGLeft = rootSVG.getBoundingClientRect().left ?? 0;
const parentDivLeft =
rootSVG.parentElement?.getBoundingClientRect().left ?? 0;
// visx localPoint does not account for the horizontal shift due to centering of the parent element,
// so manually add any shift from that
const alignmentOffset = rootSVGLeft - parentDivLeft;
return {
x: eventSvgCoords.x + alignmentOffset,
y: eventSvgCoords.y,
};
}
export { TooltipWrapper, getTooltipPosition };

View File

@ -1,5 +1,3 @@
import { localPoint } from "@visx/event";
import { Point } from "@visx/point";
import { scaleLog } from "@visx/scale";
import { Text } from "@visx/text";
import { useTooltip, withTooltip } from "@visx/tooltip";
@ -9,7 +7,7 @@ import { Color } from "utils/Color";
import { inDevEnvironment } from "utils/inDevEnviroment";
import { useIsMobile } from "utils/isMobile";
import { TooltipWrapper } from "./TooltipWrapper";
import { getTooltipPosition, TooltipWrapper } from "./TooltipWrapper";
import styles from "./WordCloud.module.css";
@ -197,33 +195,22 @@ const WordCloudWords: React.FC<WordCloudWordsProps> = ({
className={styles.word}
textAnchor="middle"
onMouseMove={
((e: React.MouseEvent<SVGTextElement, MouseEvent>) => {
// ownerSVGElement is given by visx docs but not recognized by typescript
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const eventElement = e.target.ownerSVGElement as Element;
const eventSvgCoords = localPoint(eventElement, e) as Point;
const rootSVGLeft =
eventElement.parentElement?.parentElement?.getBoundingClientRect()
.left ?? 0;
const parentDivLeft =
eventElement.parentElement?.parentElement?.parentElement?.getBoundingClientRect()
.left ?? 0;
// visx localPoint does not account for the horizontal shift due to centering of the parent element,
// so manually add any shift from that
const alignmentOffset = rootSVGLeft - parentDivLeft;
((
e: React.MouseEvent<
SVGTextElement | SVGLineElement,
MouseEvent
>
) => {
const tooltipPos = getTooltipPosition(e);
if (word.text) {
showTooltip(
{
text: word.text,
value: (cloudWords[index] as WordData).value,
},
eventSvgCoords.x -
word.text.length * TOOLTIP_HORIZONTAL_SHIFT_SCALER +
alignmentOffset,
eventSvgCoords.y
tooltipPos.x -
word.text.length * TOOLTIP_HORIZONTAL_SHIFT_SCALER,
tooltipPos.y
);
}
}) as React.MouseEventHandler<SVGTextElement>

View File

@ -17,6 +17,7 @@ import { ComponentWrapper } from "@/components/ComponentWrapper";
import { Header } from "@/components/Header";
import { LineGraph } from "@/components/LineGraph";
import { SectionHeader } from "@/components/SectionHeader";
import { SectionWrapper } from "@/components/SectionWrapper";
import { WordCloud } from "@/components/WordCloud";
import styles from "./samplePage.module.css";
@ -46,6 +47,7 @@ export default function SamplePage() {
return (
<div className={styles.page}>
<Header />
<SectionWrapper title="Transfer" />
<SectionHeader
title="Demographics"
subtitle="An insight into the demographics of UWs CS programs"