diff --git a/pkg/web_app/lib/src/widget/downloads_chart/computations.dart b/pkg/web_app/lib/src/widget/downloads_chart/computations.dart index d73426e9f6..2dfe7ae95b 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/computations.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/computations.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:math'; + import 'package:_pub_shared/data/download_counts_data.dart'; Iterable prepareRanges(List rangeDownloads) { @@ -79,3 +81,47 @@ Iterable prepareRanges(List rangeDownloads) { ); return (closestPoint.$1, closestPoint.$2); } + +/// Calculates the Euclidean distance between two points. +double distance((num, num) point, (double, double) point2) { + final dx = point.$1 - point2.$1; + final dy = point.$2 - point2.$2; + return sqrt(dx * dx + dy * dy); +} + +/// Finds the closest point on [path] (a series of points defining the line +/// segments) to a given [point]. +(num, num) closestPointOnPath( + List<(double, double)> path, (double, double) point) { + if (path.length < 2) { + return (double.maxFinite, double.maxFinite); + } + (num, num) closestPoint = (double.maxFinite, double.maxFinite); + var minDistance = double.infinity; + for (int i = 0; i < path.length - 1; i++) { + final p = closestPointOnLine(path[i], path[i + 1], point); + final dist = distance(p, point); + if (dist < minDistance) { + minDistance = dist; + closestPoint = p; + } + } + return closestPoint; +} + +/// Determines if a given [point] is within a specified [tolerance] distance of +/// a [path] defined by a series of points. +bool isPointOnPathWithTolerance( + List<(double, double)> path, (double, double) point, double tolerance) { + if (path.length < 2) { + // Not enough points to define a line segment. + return false; + } + + final closestPoint = closestPointOnPath(path, point); + final dist = distance(closestPoint, point); + if (dist < tolerance) { + return true; + } + return false; +} diff --git a/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart b/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart index 549215d578..a9ac960280 100644 --- a/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart +++ b/pkg/web_app/test/widget/downloads_chart/downloads_chart_test.dart @@ -152,4 +152,34 @@ void main() { expect(closest, (0.0, 0.0)); }); }); + + group('isPointOnPathWithTolerance', () { + test('Points on and off the line segment', () { + final path = [(0.0, 0.0), (2.0, 2.0), (4.0, 0.0)]; + + final pointOnLine = (1.0, 1.0); + expect(isPointOnPathWithTolerance(path, pointOnLine, 0.001), isTrue); + + final pointCloseToLine = (1.0, 1.1); + expect(isPointOnPathWithTolerance(path, pointCloseToLine, 0.2), isTrue); + expect( + isPointOnPathWithTolerance(path, pointCloseToLine, 0.001), isFalse); + + final pointFurtherFromLine = (1.0, 1.5); + expect( + isPointOnPathWithTolerance(path, pointFurtherFromLine, 0.1), isFalse); + }); + + test('Path with fewer than 2 points', () { + final path = [(1.0, 1.0)]; + final point = (1.0, 1.0); + expect(isPointOnPathWithTolerance(path, point, 0.001), isFalse); + }); + + test('Point on zero length segment', () { + final chart = [(1.0, 1.0), (1.0, 1.0), (5.0, 1.0)]; + final point = (1.0, 1.0); + expect(isPointOnPathWithTolerance(chart, point, 0.001), isTrue); + }); + }); }