From 97dea71ceecfe436dadd9ccace114cd373192acd Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Mon, 24 Feb 2025 12:46:09 +0000 Subject: [PATCH 1/2] Downloads chart: Add helper function for computing closest point on line --- .../widget/downloads_chart/computations.dart | 37 ++++++++++ .../downloads_chart/downloads_chart_test.dart | 74 +++++++++++++++++++ 2 files changed, 111 insertions(+) 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 025dedf726..3ab86602df 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/computations.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/computations.dart @@ -41,3 +41,40 @@ Iterable prepareRanges(List rangeDownloads) { return (ranges: ranges, weekLists: result.reversed.toList()); } + +/// Calculates closest point from [point] to line defined by [startPoint] and +/// [endPoint]. +/// +/// This assumes that the closest point is on the line, that is within the +/// two endpoints. Returns `(double.maxFinite, double.maxFinite)` if this is +/// not the case. +(num, num) closestPointOnLine( + (num, num) startPoint, (num, num) endPoint, (num, num) point) { + final directionVector = + (endPoint.$1 - startPoint.$1, endPoint.$2 - startPoint.$2); + + if (directionVector.$1 == 0 && directionVector.$2 == 0) { + return startPoint; + } + + final vector = (point.$1 - startPoint.$1, point.$2 - startPoint.$2); + + // The dot product ((v · d) / (d · d)) where v = vector and d = directionVector + final t = ((vector.$1 * directionVector.$1 + vector.$2 * directionVector.$2) / + (directionVector.$1 * directionVector.$1 + + directionVector.$2 * directionVector.$2)); + + if (t < 0 || t > 1) { + // Closest point is before or after the line. This should not happen in + // our use case. + return (double.maxFinite, double.maxFinite); + } + + // t * d + final projectionVOntoD = (t * directionVector.$1, t * directionVector.$2); + final closestPoint = ( + startPoint.$1 + projectionVOntoD.$1, + startPoint.$2 + projectionVOntoD.$2 + ); + return (closestPoint.$1, closestPoint.$2); +} 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 6ffd9a6466..549215d578 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 @@ -78,4 +78,78 @@ void main() { expect(w3[i], [0, 0, 0, 0, 0, 0]); } }); + + group('closestPointOnLine tests', () { + test('point on the line', () { + final lineStart = (0, 0); + final lineEnd = (10, 10); + final point = (5, 5); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (5.0, 5.0)); + }); + + test('point before the line', () { + final lineStart = (0, 0); + final lineEnd = (10, 10); + final point = (-2, -5); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (double.maxFinite, double.maxFinite)); + }); + + test('point after the line', () { + final lineStart = (0, 0); + final lineEnd = (10, 10); + final point = (15, 15); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (double.maxFinite, double.maxFinite)); + }); + + test('point off the line', () { + final lineStart = (0, 0); + final lineEnd = (10, 10); + final point = (5, 3); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (4.0, 4.0)); + }); + + test('vertical line', () { + final lineStart = (1, 2); + final lineEnd = (1, 10); + final point = (5, 5); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (1.0, 5.0)); + }); + + test('horizontal line', () { + final lineStart = (2, 1); + final lineEnd = (10, 1); + final point = (5, 5); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (5.0, 1.0)); + }); + + test('same start and end points', () { + final lineStart = (5, 5); + final lineEnd = (5, 5); + final point = (10, 10); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (5.0, 5.0)); + }); + + test('line with negative coordinates', () { + final lineStart = (-5, -5); + final lineEnd = (5, 5); + final point = (0, 10); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (5.0, 5.0)); + }); + + test('line with negative and positive coordinates', () { + final lineStart = (-5, 5); + final lineEnd = (5, -5); + final point = (0, 0); + final closest = closestPointOnLine(lineStart, lineEnd, point); + expect(closest, (0.0, 0.0)); + }); + }); } From 6d9d6e473559d3a3cb4f56e6336d6cb40ef70e43 Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Tue, 25 Feb 2025 11:53:32 +0000 Subject: [PATCH 2/2] comments --- .../widget/downloads_chart/computations.dart | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) 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 3ab86602df..d73426e9f6 100644 --- a/pkg/web_app/lib/src/widget/downloads_chart/computations.dart +++ b/pkg/web_app/lib/src/widget/downloads_chart/computations.dart @@ -42,12 +42,14 @@ Iterable prepareRanges(List rangeDownloads) { return (ranges: ranges, weekLists: result.reversed.toList()); } -/// Calculates closest point from [point] to line defined by [startPoint] and -/// [endPoint]. +/// Calculates the closest point on the line segment between [startPoint] +/// and [endPoint] to a given [point]. /// -/// This assumes that the closest point is on the line, that is within the -/// two endpoints. Returns `(double.maxFinite, double.maxFinite)` if this is -/// not the case. +/// If [startPoint] and [endPoint] are the same, [startPoint] is returned. +/// +/// If [point] is outside the line segment, that is the closest point would not +/// be within the thwo endpoints, `(double.maxFinite, double.maxFinite)` +/// is returned. (num, num) closestPointOnLine( (num, num) startPoint, (num, num) endPoint, (num, num) point) { final directionVector = @@ -57,16 +59,15 @@ Iterable prepareRanges(List rangeDownloads) { return startPoint; } - final vector = (point.$1 - startPoint.$1, point.$2 - startPoint.$2); + final v = (point.$1 - startPoint.$1, point.$2 - startPoint.$2); - // The dot product ((v · d) / (d · d)) where v = vector and d = directionVector - final t = ((vector.$1 * directionVector.$1 + vector.$2 * directionVector.$2) / + // The dot product ((v · d) / (d · d)) + final t = ((v.$1 * directionVector.$1 + v.$2 * directionVector.$2) / (directionVector.$1 * directionVector.$1 + directionVector.$2 * directionVector.$2)); if (t < 0 || t > 1) { - // Closest point is before or after the line. This should not happen in - // our use case. + // Closest point is before or after the line. return (double.maxFinite, double.maxFinite); }