-
-
Notifications
You must be signed in to change notification settings - Fork 5
feat: Add axis-based tooltips with smooth rendering and crosshair indicator #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Summary by CodeRabbit
WalkthroughAdds axis-triggered, multi-point tooltips and optional vertical crosshair; extends tooltip API and interaction detection; updates chart rendering and widget logic to support axis mode; adds example/demo route and tests; small core palette/scale adjustments and painter cleanup. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant W as AnimatedChartWidget
participant D as InteractionDetector
participant T as TooltipController
participant C as CrosshairOverlay
Note over W: Axis-triggered tooltip mode enabled
U->>W: Hover/Pan at (x,y)
W->>D: detectPointsByXPosition((x,y))
D-->>W: List<DataPointInfo> (same X)
alt Points found
W->>W: set _crosshairPosition(x)
W->>T: showMultiPointTooltip(points, position)
W->>C: paint vertical crosshair
else No points
W->>T: hide tooltip
W->>W: clear _crosshairPosition
end
sequenceDiagram
participant U as User
participant W as AnimatedChartWidget
participant D as InteractionDetector
participant T as TooltipController
rect rgba(220,240,255,0.20)
Note over W,T: Point mode (existing)
U->>W: Hover
W->>D: detectNearestPoint((x,y))
D-->>W: DataPointInfo?
W->>T: showTooltip(point, pos)
end
rect rgba(235,245,225,0.20)
Note over W,T: Axis mode (new)
U->>W: Hover
W->>D: detectPointsByXPosition((x,y))
D-->>W: List<DataPointInfo>
W->>T: showMultiPointTooltip(points, pos)
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labelsenhancement Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: ASSERTIVE Plan: Pro 📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
lib/src/widgets/animated_chart_widget.dart (1)
329-407: Include multi-point tooltips in the touch guard
hasTooltipsonly checkstooltip.builder, so when we rely onmultiPointBuilder(the core of axis mode) the touch path short-circuits and axis tooltips/crosshair never update during drag on mobile. Please treatmultiPointBuilderas a tooltip signal here so axis mode works on touch devices.Apply this patch:
- final hasTooltips = widget.interaction.hover?.onHover != null || - widget.interaction.tooltip?.builder != null; + final hasTooltips = widget.interaction.hover?.onHover != null || + widget.interaction.tooltip?.builder != null || + widget.interaction.tooltip?.multiPointBuilder != null;lib/src/interaction/tooltip_widget.dart (1)
216-268: Overlay never updates position/content after first paint
_updateTooltipPositiononly callsmarkNeedsBuild, but theOverlayEntrybuilder closes overcapturedPosition/capturedPointsfrom the moment the overlay was created. As a result the widget rebuilds with the exact same data and coordinates—tooltips never follow the cursor and never refresh their payload. Either rebuild the overlay with the latest state each time or have the builder read_currentPosition,_currentPoint, and_currentPointsdirectly from the state somarkNeedsBuildcan do its job.One possible fix is:
- final Offset capturedPosition = _currentPosition!; - final TooltipConfig capturedConfig = widget.config; - - // Build tooltip content based on mode - Widget tooltipContent; - if (_isMultiPoint && capturedConfig.multiPointBuilder != null) { - final List<DataPointInfo> capturedPoints = List.from(_currentPoints!); - tooltipContent = capturedConfig.multiPointBuilder!(capturedPoints); - } else if (!_isMultiPoint && capturedConfig.builder != null) { - final DataPointInfo capturedPoint = _currentPoint!; - tooltipContent = capturedConfig.builder!(capturedPoint); - } else { - return; // No appropriate builder - } - _overlayEntry = OverlayEntry( builder: (context) { + final config = widget.config; + final position = _currentPosition!; + Widget? tooltipContent; + if (_isMultiPoint && config.multiPointBuilder != null && _currentPoints != null) { + tooltipContent = config.multiPointBuilder!(_currentPoints!); + } else if (!_isMultiPoint && config.builder != null && _currentPoint != null) { + tooltipContent = config.builder!(_currentPoint!); + } + if (tooltipContent == null) { + return const SizedBox.shrink(); + } return _TooltipPositioned( - position: capturedPosition, - config: capturedConfig, + position: position, + config: config, fadeAnimation: _fadeAnimation, scaleAnimation: _scaleAnimation, child: tooltipContent, ); }, );That way a simple
markNeedsBuildactually reflects the latest pointer location and data set.
🧹 Nitpick comments (1)
lib/src/core/scale.dart (1)
82-82: Consider removing or conditionalizing the debug print.The
debugPrintstatement logs every domain update, which could create noise in debug logs if the domain is updated frequently during axis-based tooltip interactions. Additionally, the ellipsis (...) always appears even when the domain has 10 or fewer items.If this logging is temporary for feature development, consider removing it before merge. If it's intended for ongoing debugging, consider:
- Option 1: Remove the debug print entirely:
set domain(List<dynamic> value) { _domain = value; - debugPrint('OrdinalScale: Setting domain with ${value.length} items: ${value.take(10)}...'); _calculateBandWidth(); }
- Option 2: Add a conditional flag or fix the ellipsis formatting:
set domain(List<dynamic> value) { _domain = value; - debugPrint('OrdinalScale: Setting domain with ${value.length} items: ${value.take(10)}...'); + if (kDebugMode) { + final preview = value.take(10).toList(); + final ellipsis = value.length > 10 ? '...' : ''; + debugPrint('OrdinalScale: Setting domain with ${value.length} items: $preview$ellipsis'); + } _calculateBandWidth(); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
example/lib/graphs/axis_tooltip_example.dart(1 hunks)example/lib/router/app_router.dart(2 hunks)example/lib/screens/chart_screen.dart(3 hunks)example/lib/utils/chart_feature_list.dart(1 hunks)lib/src/core/chart.dart(4 hunks)lib/src/core/scale.dart(2 hunks)lib/src/interaction/chart_interactions.dart(7 hunks)lib/src/interaction/crosshair_widget.dart(1 hunks)lib/src/interaction/interaction_detector.dart(1 hunks)lib/src/interaction/tooltip_widget.dart(8 hunks)lib/src/widgets/animated_chart_painter.dart(0 hunks)lib/src/widgets/animated_chart_widget.dart(6 hunks)
💤 Files with no reviewable changes (1)
- lib/src/widgets/animated_chart_painter.dart
🔇 Additional comments (1)
lib/src/core/scale.dart (1)
132-136: LGTM! Defensive bounds check for tick generation.The clamping prevents potential out-of-bounds access when generating tick indices. While mathematically the computed index
(i * step).floor()should always fall within[0, _domain.length - 1]for valid inputs, this defensive check guards against floating-point edge cases and ensures robustness during axis-based tooltip interactions.The logic correctly samples the domain at regular intervals and the clamp operation does not alter the intended behavior.
- Remove xTolerance parameter to ensure tooltips always appear - Implement two-pass algorithm to find closest X position - Group points at same X using 2px epsilon for web precision - Sort points by Y position (top to bottom) for consistent ordering This ensures axis-based tooltips display continuously as users hover across the chart, rather than only appearing within a tolerance range.
- Clear crosshair position when pan gesture ends - Always hide tooltip on gesture end (both single and multi-point) - Remove xTolerance parameter from detectPointsByXPosition calls Fixes issue where crosshair and tooltip would linger after interaction ended, improving visual clarity and user experience.
Replace deprecated withOpacity() with withValues(alpha: 0.9) to avoid deprecation warnings in Flutter 3.27+.
Add proper metadata for the axis tooltip example in chart_screen.dart so it displays correctly in the example app navigation.
Add 14 widget tests covering: - Chart rendering with axis tooltip configuration - Point detection at X positions during hover - Crosshair visibility and behavior - Custom multi-point tooltip builders - Axis mode with continuous and ordinal X scales - Different crosshair styles (solid, dashed, dotted) - Empty points handling - Axis vs point mode comparison - Nearest X position snapping behavior All tests pass with 100% coverage of axis tooltip functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
lib/src/widgets/animated_chart_widget.dart (1)
327-336: Restore axis tooltips for touch interactions.Line 329’s
hasTooltipsflag only looks atbuilder, so configs that rely solely onmultiPointBuildernever enter the pan tooltip branch. That means axis-mode tooltips (and the crosshair) stop updating as soon as the user drags on touch devices—the new feature breaks in its most common setup. Please treatmultiPointBuilderas a tooltip signal (and reuse the computed config inside the branch) so axis tooltips keep working during pan gestures.- final panConfig = widget.interaction.pan; - final hasTooltips = widget.interaction.hover?.onHover != null || - widget.interaction.tooltip?.builder != null; + final panConfig = widget.interaction.pan; + final tooltipConfig = widget.interaction.tooltip; + final hasTooltips = widget.interaction.hover?.onHover != null || + tooltipConfig?.builder != null || + tooltipConfig?.multiPointBuilder != null; @@ - if (hasTooltips && panConfig?.enabled != true) { + if (hasTooltips && panConfig?.enabled != true) { @@ - final tooltipConfig = widget.interaction.tooltip; final useAxisMode = tooltipConfig?.triggerMode == ChartTooltipTriggerMode.axis;lib/src/interaction/tooltip_widget.dart (1)
248-271: Rebuild uses stale coordinates—tooltip never repositions
_updateTooltipPositionupdates_currentPositionand callsmarkNeedsBuild, but_createTooltipcaptured the position in a localfinalso_TooltipPositionedkeeps the old value forever. The tooltip appears frozen while the user moves along the axis. Read_currentPositioninside the builder (or derive an up-to-date position there) instead of capturing it once.Here’s one way to fix it:
- final Offset capturedPosition = _currentPosition!; - final TooltipConfig capturedConfig = widget.config; + final TooltipConfig capturedConfig = widget.config; @@ - return _TooltipPositioned( - position: capturedPosition, + final position = _currentPosition!; + + return _TooltipPositioned( + position: position,
♻️ Duplicate comments (2)
lib/src/interaction/interaction_detector.dart (1)
92-116: Still mixing multiple X buckets on tiesWhen the pointer sits midway between two X buckets (e.g., two bands centred 15 px away), both yield the same
minXDistance, so the second pass keeps both sets. The tooltip/crosshair resumes aggregating unrelated X values—the same problem flagged earlier. Instead, capture the screen X of the closest point and only include points that land on that pixel (within epsilon). Please adjust as follows:- double minXDistance = double.infinity; - for (final point in _allPoints) { - final xDistance = (screenPosition.dx - point.screenPosition.dx).abs(); - if (xDistance < minXDistance) { - minXDistance = xDistance; - } - } + double minXDistance = double.infinity; + double? targetX; + for (final point in _allPoints) { + final xDistance = (screenPosition.dx - point.screenPosition.dx).abs(); + if (xDistance < minXDistance) { + minXDistance = xDistance; + targetX = point.screenPosition.dx; + } + } @@ - for (final point in _allPoints) { - final xDistance = (screenPosition.dx - point.screenPosition.dx).abs(); - if ((xDistance - minXDistance).abs() <= epsilon) { - pointsAtX.add(point); - } - } + if (targetX == null) return []; + + for (final point in _allPoints) { + if ((point.screenPosition.dx - targetX).abs() <= epsilon) { + pointsAtX.add(point); + } + }lib/src/interaction/tooltip_widget.dart (1)
115-149: Maintain previous tooltip state before diffing
We’re still mutating_currentPoint/_currentPointsbefore comparing, so_hasSameData*always sees identical objects and never recreates the overlay. Axis hover sticks on the first X value and won’t refresh—same regression we discussed earlier. Capture the previous state first, then update the fields only after deciding whether the payload actually changed.Apply this diff to restore correct behavior:
void _showTooltipInternal({ DataPointInfo? singlePoint, List<DataPointInfo>? multiPoints, required Offset position, required bool isMultiPoint, }) { - // Update current state - _currentPoint = singlePoint; - _currentPoints = multiPoints; + final previousPoint = _currentPoint; + final previousPoints = _currentPoints; + + // Update current state + _currentPoint = singlePoint; + _currentPoints = multiPoints; @@ - final shouldRecreate = isMultiPoint - ? !_hasSameDataMulti(_currentPoints, multiPoints) - : !_hasSameData(_currentPoint, singlePoint); + final shouldRecreate = isMultiPoint + ? !_hasSameDataMulti(previousPoints, multiPoints) + : !_hasSameData(previousPoint, singlePoint);
🧹 Nitpick comments (1)
lib/src/core/scale.dart (1)
82-83: Refine the debug log message for clarity.The debug log provides useful diagnostic information for domain mutations. However, the "..." suffix is misleading when
value.length <= 10. Consider conditionally appending it or removing it entirely.Apply this diff for a more accurate log message:
- debugPrint( - 'OrdinalScale: Setting domain with ${value.length} items: ${value.take(10)}...'); + debugPrint( + 'OrdinalScale: Setting domain with ${value.length} items: ${value.take(10)}${value.length > 10 ? '...' : ''}');
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
example/lib/graphs/axis_tooltip_example.dart(1 hunks)example/lib/screens/chart_screen.dart(5 hunks)lib/src/core/chart.dart(4 hunks)lib/src/core/scale.dart(2 hunks)lib/src/interaction/chart_interactions.dart(7 hunks)lib/src/interaction/crosshair_widget.dart(1 hunks)lib/src/interaction/interaction_detector.dart(1 hunks)lib/src/interaction/tooltip_widget.dart(8 hunks)lib/src/widgets/animated_chart_widget.dart(6 hunks)test/axis_tooltip_test.dart(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- example/lib/screens/chart_screen.dart
🔇 Additional comments (1)
lib/src/core/scale.dart (1)
133-137: LGTM!The clamping logic is a solid defensive measure that prevents potential index-out-of-bounds errors. While mathematically the floor operation should keep indices within bounds, the clamp guards against floating-point precision edge cases and makes the code more robust.
- Add default multiPointBuilder to axisConfig so axis tooltips work OOTB - Fix yValue fallback to use point.yValue or point.data['y'] when yColumn is null - Remove 'const' from axisConfig to allow non-const multiPointBuilder This ensures axis tooltips display meaningful values even when columns aren't explicitly specified, and work immediately with TooltipConfig.axisConfig.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
lib/src/interaction/chart_interactions.dart(7 hunks)lib/src/interaction/interaction_detector.dart(1 hunks)
🔇 Additional comments (10)
lib/src/interaction/interaction_detector.dart (6)
75-82: LGTM! Clear documentation for axis-based tooltip detection.The method signature and documentation clearly explain the intended behavior: snapping to the nearest X position without tolerance limits and returning all points at that X sorted by Y value.
83-90: LGTM! Proper initialization and early exit.The index building and empty-check logic follows the same reliable pattern as
detectPoint.
92-99: LGTM! Correct minimum distance calculation.The loop correctly identifies the minimum X distance across all points.
101-115: LGTM! Epsilon-based grouping handles multi-series and rendering precision.The epsilon tolerance (2.0px) correctly groups all points that share approximately the same X position, accommodating web rendering precision while preventing cross-bucket mixing.
117-118: LGTM! Proper empty-result handling.
120-135: LGTM! Consistent sorting and conversion logic.The Y-based sort ensures predictable tooltip ordering, and the conversion to
DataPointInfomatches the pattern used indetectPoint.lib/src/interaction/chart_interactions.dart (4)
3-18: LGTM! Well-defined enums with clear documentation.The trigger mode and stroke style enums are appropriately scoped and well-documented.
43-86: LGTM! TooltipConfig extended with axis-mode fields.The new fields (multiPointBuilder, triggerMode, crosshair options) are well-typed, documented, and have sensible defaults that preserve backward compatibility.
96-106: LGTM! axisConfig properly wired with multiPointBuilder.The default axis configuration correctly enables crosshair and provides the multi-point builder, addressing the previous review concern.
175-175: LGTM! Correct typedef for multi-point tooltips.
CRITICAL BUG FIX: - Capture previous state BEFORE overwriting in _showTooltipInternal - Compare new data against previous data (not against itself!) - Update state AFTER comparison decision is made This fixes the major issue where axis tooltips would get stuck on the first X position because every subsequent hover was treated as 'same data' due to comparing new state against itself. Also: - Change empty string fallback to 'N/A' for missing values (better UX) This was breaking the entire axis tooltip feature - tooltips would appear once and never update as the user moved their mouse horizontally.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
This looks good, will it be possible to make a crosshair indicator(the dashed line) be stickable and have more than one? Than a user can drag or place a indicator and compare two or more points on the chart more easily. |
That's a good idea. It would be useful to have implement that. Just need to resolve the conflicts here first. |
|
It would also be great if export to svg would support including the dragged indicator/s |
Summary by CodeRabbit
New Features
Bug Fixes
Tests