diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index ba464d311dc..5b9bb53f72e 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -14,6 +14,7 @@ import { minBy, maxBy, exposeInstanceOnWindow, + PointVector, } from "@ourworldindata/utils" import { TextWrap } from "@ourworldindata/components" import { observable, computed, action } from "mobx" @@ -1044,26 +1045,68 @@ class LabelledSlopes this.mouseFrame = requestAnimationFrame(() => { if (this.props.bounds.contains(mouse)) { - const distToSlope = new Map() + if (this.slopeData.length === 0) return + + const { x1: startX, x2: endX } = this.slopeData[0] + + // whether the mouse is over the chart area, + // the left label area, or the right label area + const mousePosition = + mouse.x < startX + ? "left" + : mouse.x > endX + ? "right" + : "chart" + + const distToSlopeOrLabel = new Map() for (const s of this.slopeData) { - const dist = - Math.abs( - (s.y2 - s.y1) * mouse.x - - (s.x2 - s.x1) * mouse.y + - s.x2 * s.y1 - - s.y2 * s.x1 - ) / - Math.sqrt((s.y2 - s.y1) ** 2 + (s.x2 - s.x1) ** 2) - distToSlope.set(s, dist) + let line: { + x1: number + x2: number + y1: number + y2: number + } + if (mousePosition === "chart") { + line = s + } else if (mousePosition === "left") { + const labelBox = s.leftLabelBounds.toProps() + // "strike-through"" line that stretches from the left side + // of the left label to the start point of the slopes + line = { + x1: labelBox.x, + x2: startX, + y1: labelBox.y + labelBox.height / 2, + y2: labelBox.y + labelBox.height / 2, + } + } else { + const labelBox = s.rightLabelBounds.toProps() + // "strike-through"" line that stretches from the end point + // of the slopes to the right side of the right label + line = { + x1: endX, + x2: labelBox.x + labelBox.width, + y1: labelBox.y + labelBox.height / 2, + y2: labelBox.y + labelBox.height / 2, + } + } + // calculate the distance to the slope or label + const dist = PointVector.distanceFromPointToLineSegment( + mouse, + new PointVector(line.x1, line.y1), + new PointVector(line.x2, line.y2) + ) + distToSlopeOrLabel.set(s, dist) } const closestSlope = minBy(this.slopeData, (s) => - distToSlope.get(s) + distToSlopeOrLabel.get(s) ) + const distance = distToSlopeOrLabel.get(closestSlope!)! + const tolerance = mousePosition === "chart" ? 20 : 10 if ( closestSlope && - (distToSlope.get(closestSlope) as number) < 20 && + distance < tolerance && this.props.onMouseOver ) { this.props.onMouseOver(closestSlope) diff --git a/packages/@ourworldindata/utils/src/PointVector.ts b/packages/@ourworldindata/utils/src/PointVector.ts index 75c42dc6f18..cf6c122e15c 100644 --- a/packages/@ourworldindata/utils/src/PointVector.ts +++ b/packages/@ourworldindata/utils/src/PointVector.ts @@ -79,10 +79,10 @@ export class PointVector { } // From: http://stackoverflow.com/a/1501725/1983739 - static distanceFromPointToLineSq( + static distanceFromPointToLineSegmentSq( p: PointVector, - v: PointVector, - w: PointVector + v: PointVector, // start of line segment + w: PointVector // end of line segment ): number { const l2 = PointVector.distanceSq(v, w) if (l2 === 0) return PointVector.distanceSq(p, v) @@ -94,4 +94,31 @@ export class PointVector { new PointVector(v.x + t * (w.x - v.x), v.y + t * (w.y - v.y)) ) } + + static distanceFromPointToLineSegment( + p: PointVector, + v: PointVector, // start of line segment + w: PointVector // end of line segment + ): number { + return Math.sqrt(PointVector.distanceFromPointToLineSegmentSq(p, v, w)) + } + + // From: https://stackoverflow.com/questions/5254838/calculating-distance-between-a-point-and-a-rectangular-box-nearest-point + static distanceFromPointToRectSq( + p: PointVector, + r: PointVector, // top left + s: PointVector // bottom right + ): number { + const dx = Math.max(r.x - p.x, 0, p.x - s.x) + const dy = Math.max(r.y - p.y, 0, p.y - s.y) + return dx * dx + dy * dy + } + + static distanceFromPointToRect( + p: PointVector, + r: PointVector, // top left + s: PointVector // bottom right + ): number { + return Math.sqrt(PointVector.distanceFromPointToRectSq(p, r, s)) + } }