From e67f712d64500426288c3c8c5b4e15f5c417ca46 Mon Sep 17 00:00:00 2001 From: behnam-deriv <133759298+behnam-deriv@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:39:26 +0800 Subject: [PATCH 1/2] chore: remove previousTick marker --- .../markers/chart_marker.dart | 6 ---- .../accumulator_marker_icon_painter.dart | 30 +++++-------------- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/lib/src/deriv_chart/chart/data_visualization/markers/chart_marker.dart b/lib/src/deriv_chart/chart/data_visualization/markers/chart_marker.dart index 17cfd8746..6e302f318 100644 --- a/lib/src/deriv_chart/chart/data_visualization/markers/chart_marker.dart +++ b/lib/src/deriv_chart/chart/data_visualization/markers/chart_marker.dart @@ -46,12 +46,6 @@ enum MarkerType { /// quickly identify the current market price. latestTick, - /// Represents the tick immediately before the latest tick. - /// - /// This can be used for comparison or to show price movement between the previous - /// and latest ticks. It's useful for visualizing short-term price changes. - previousTick, - /// Represents a standard tick marker. /// /// This is a generic marker for any price tick that needs to be highlighted. diff --git a/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/accumulator_marker_icon_painter.dart b/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/accumulator_marker_icon_painter.dart index d01239766..b8d80be64 100644 --- a/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/accumulator_marker_icon_painter.dart +++ b/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/accumulator_marker_icon_painter.dart @@ -102,8 +102,6 @@ class AccumulatorMarkerIconPainter extends TickMarkerIconPainter { final ChartMarker? highMarker = markers[MarkerType.highBarrier]; final ChartMarker? endMarker = markers[MarkerType.exitSpot]; - final ChartMarker? previousTickMarker = markers[MarkerType.previousTick]; - if (lowMarker != null && highMarker != null) { final Offset lowOffset = _getOffset(lowMarker, epochToX, quoteToY); final Offset highOffset = _getOffset(highMarker, epochToX, quoteToY); @@ -123,7 +121,6 @@ class AccumulatorMarkerIconPainter extends TickMarkerIconPainter { top: highOffset.dy, markerGroup: markerGroup, bottom: lowOffset.dy, - previousTickMarker: previousTickMarker, ); } } @@ -162,7 +159,6 @@ class AccumulatorMarkerIconPainter extends TickMarkerIconPainter { required double top, required MarkerGroup markerGroup, required double bottom, - ChartMarker? previousTickMarker, }) { final double endTop = size.height; @@ -197,17 +193,14 @@ class AccumulatorMarkerIconPainter extends TickMarkerIconPainter { ); YAxisConfig.instance.yAxisClipping(canvas, size, () { - if (previousTickMarker != null && previousTickMarker.color != null) { - _drawPreviousTickBarrier( - size, - canvas, - startLeft, - endLeft, - middleTop, - previousTickMarker.color!, - barrierColor, - ); - } + _drawPreviousTickBarrier( + size, + canvas, + startLeft, + endLeft, + middleTop, + barrierColor, + ); if (isTopVisible || hasPersistentBorders) { final Path path = Path() @@ -304,15 +297,8 @@ class AccumulatorMarkerIconPainter extends TickMarkerIconPainter { double startX, double endX, double y, - Color circleColor, Color barrierColor, ) { - canvas.drawCircle( - Offset(startX, y), - 1.5, - Paint()..color = circleColor, - ); - paintHorizontalDashedLine( canvas, startX, From cd80747ed7c066d37164d518b63ff78861061f57 Mon Sep 17 00:00:00 2001 From: behnam-deriv <133759298+behnam-deriv@users.noreply.github.com> Date: Fri, 24 Oct 2025 11:16:53 +0800 Subject: [PATCH 2/2] feat: wip accumulator markers --- example/lib/main.dart | 66 +++--- .../markers/active_marker_group_painter.dart | 130 +++--------- .../markers/chart_marker.dart | 6 + .../accumulator_marker_icon_painter.dart | 125 ++++++++++-- .../tick_marker_icon_painter.dart | 193 +++++++++++------- .../paint_functions/paint_marker_pill.dart | 153 ++++++++++++++ lib/src/deriv_chart/chart/main_chart.dart | 2 +- 7 files changed, 461 insertions(+), 214 deletions(-) create mode 100644 lib/src/deriv_chart/chart/helpers/paint_functions/paint_marker_pill.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index d71ddd993..e84a830d5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -867,35 +867,47 @@ class _FullscreenChartState extends State { final bool showStandardMarkers = currentEpoch < endEpoch + 500; if (showStandardMarkers) { chartMarkers.addAll([ - ChartMarker( - epoch: marker.epoch - 1000, - quote: marker.quote, - direction: marker.direction, - markerType: MarkerType.startTimeCollapsed, - ), - ChartMarker( - epoch: marker.epoch - 1000, - quote: marker.quote, - direction: marker.direction, - markerType: MarkerType.startTime, - ), + // ChartMarker( + // epoch: marker.epoch - 1000, + // quote: marker.quote, + // direction: marker.direction, + // markerType: MarkerType.startTimeCollapsed, + // ), + // ChartMarker( + // epoch: marker.epoch - 1000, + // quote: marker.quote, + // direction: marker.direction, + // markerType: MarkerType.startTime, + // ), + // ChartMarker( + // epoch: marker.epoch, + // quote: marker.quote, + // direction: marker.direction, + // markerType: MarkerType.entrySpot, + // ), + // ChartMarker( + // epoch: endEpoch, + // quote: marker.quote, + // direction: marker.direction, + // markerType: MarkerType.exitTimeCollapsed, + // ), + // ChartMarker( + // epoch: endEpoch, + // quote: marker.quote, + // direction: marker.direction, + // markerType: MarkerType.exitTime, + // ), ChartMarker( epoch: marker.epoch, - quote: marker.quote, - direction: marker.direction, - markerType: MarkerType.entrySpot, - ), - ChartMarker( - epoch: endEpoch, - quote: marker.quote, + quote: marker.quote - 0.1, direction: marker.direction, - markerType: MarkerType.exitTimeCollapsed, + markerType: MarkerType.lowBarrier, ), ChartMarker( - epoch: endEpoch, - quote: marker.quote, + epoch: marker.epoch, + quote: marker.quote + 0.1, direction: marker.direction, - markerType: MarkerType.exitTime, + markerType: MarkerType.highBarrier, ), ChartMarker( epoch: marker.epoch, @@ -920,6 +932,12 @@ class _FullscreenChartState extends State { }); }, ), + ChartMarker( + epoch: marker.epoch, + quote: marker.quote, + direction: marker.direction, + markerType: MarkerType.latestTick, + ), ]); } @@ -974,7 +992,7 @@ class _FullscreenChartState extends State { return MarkerGroupSeries( SplayTreeSet(), - markerGroupIconPainter: TickMarkerIconPainter(), + markerGroupIconPainter: AccumulatorMarkerIconPainter(), markerGroupList: _convertMarkersToGroups(currentEpoch), activeMarkerGroup: activeGroupForBuild, ); diff --git a/lib/src/deriv_chart/chart/data_visualization/markers/active_marker_group_painter.dart b/lib/src/deriv_chart/chart/data_visualization/markers/active_marker_group_painter.dart index 94b373861..4669b36d7 100644 --- a/lib/src/deriv_chart/chart/data_visualization/markers/active_marker_group_painter.dart +++ b/lib/src/deriv_chart/chart/data_visualization/markers/active_marker_group_painter.dart @@ -1,8 +1,7 @@ -import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/markers/marker.dart'; import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/chart_data.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/helpers/paint_functions/paint_marker_pill.dart'; import 'package:deriv_chart/src/deriv_chart/chart/helpers/paint_functions/paint_text.dart'; import 'package:flutter/material.dart'; -import 'dart:math' as math; import 'package:deriv_chart/src/theme/painting_styles/marker_style.dart'; import 'package:deriv_chart/src/theme/chart_theme.dart'; @@ -61,10 +60,11 @@ class ActiveMarkerGroupPainter extends CustomPainter { return; } - // Find the contractMarker in the active group to anchor the pill and icon + // Find the contractMarker or contractMarkerFixed in the active group to anchor the pill and icon ChartMarker? contractMarker; for (final ChartMarker marker in activeMarkerGroup.markers) { - if (marker.markerType == MarkerType.contractMarker) { + if (marker.markerType == MarkerType.contractMarker || + marker.markerType == MarkerType.contractMarkerFixed) { contractMarker = marker; break; } @@ -86,111 +86,31 @@ class ActiveMarkerGroupPainter extends CustomPainter { animationInfo, ); - // Positioning rules: - // - Contract icon is painted by TickMarkerIconPainter at x defined by - // props.contractMarkerLeftPadding + outerRadius, where outerRadius is - // (12 * zoom) + (1 * zoom), matching the circle's drawn outer border. - // - The profit/loss pill starts as the right semicircle (arc) of that icon and expands rightward to form a pill - final double _outerRadius = - (12 * painterProps.zoom) + (1 * painterProps.zoom); - final double iconCenterX = - activeMarkerGroup.props.contractMarkerLeftPadding + _outerRadius; - final double iconOuterRadius = - style.radius + 4; // Matches contract marker outer circle - final double centerY = quoteToY(contractMarker.quote); - - // Fade text opacity directly with clamped animation progress + // Calculate pill width for animation + final String profitLossText = activeMarkerGroup.profitAndLossText ?? ''; final TextPainter textPainter = makeTextPainter( - activeMarkerGroup.profitAndLossText ?? '', - style.activeMarkerText.copyWith( - color: Colors.white.withOpacity(animationProgress.clamp(0.0, 1.0)), - ), + profitLossText, + style.activeMarkerText, ); - - // Make pill height match the icon border circle for a seamless joint - final double pillRadius = iconOuterRadius; - - // Left reference where the arc meets the horizontal center line - final double arcRightX = iconCenterX + iconOuterRadius; - - // Total width = full capsule width + animated text width contribution - final double animatedTextWidth = - (style.textLeftPadding + textPainter.width + style.textRightPadding) * - animationProgress; - final double pillWidth = animatedTextWidth; - - // Build the pill path that starts with the right semicircle of the icon - final double rightEndX = arcRightX + pillWidth; - final Offset rightEndCenter = Offset(rightEndX - pillRadius, centerY); - - final Path pillPath = Path() - // Move to top of the icon circle - ..moveTo(iconCenterX, centerY - iconOuterRadius) - // Right semicircle of icon (from top to bottom) - ..arcTo( - Rect.fromCircle( - center: Offset(iconCenterX, centerY), radius: iconOuterRadius), - -math.pi / 2, - math.pi, - false, - ) - // Bottom edge to the right rounded end - ..lineTo(rightEndCenter.dx, centerY + pillRadius) - // Right rounded end (bottom to top) - ..arcTo( - Rect.fromCircle(center: rightEndCenter, radius: pillRadius), - math.pi / 2, - -math.pi, - false, - ) - // Top edge back to the top of the icon arc - ..lineTo(iconCenterX, centerY - iconOuterRadius) - ..close(); - - // Pill background (dark container) - final Color pillFillColor = const Color(0xFF181C25); - canvas.drawPath(pillPath, Paint()..color = pillFillColor); - - // Pill border uses profit/loss color (exclude the left arc next to the icon) - final Color borderColor = contractMarker.direction == MarkerDirection.up - ? style.upColor - : style.downColor; - final Path pillBorderPath = Path() - ..moveTo(iconCenterX, centerY + pillRadius) - ..lineTo(rightEndCenter.dx, centerY + pillRadius) - ..arcTo( - Rect.fromCircle(center: rightEndCenter, radius: pillRadius), - math.pi / 2, - -math.pi, - false, - ) - ..lineTo(iconCenterX, centerY - pillRadius); - canvas.drawPath( - pillBorderPath, - Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 1 - ..strokeJoin = StrokeJoin.round - ..strokeCap = StrokeCap.round - ..color = borderColor, - ); - - // Label: paint while expanding but clip to animated pill area so it reveals smoothly - canvas.save(); - canvas.clipPath(pillPath); - paintWithTextPainter( - canvas, - painter: textPainter, - anchor: Offset(arcRightX + style.textLeftPadding, centerY), - anchorAlignment: Alignment.centerLeft, + final double naturalPillWidth = + style.textLeftPadding + textPainter.width + style.textRightPadding; + + // Paint the profit/loss pill with animation + final MarkerPillResult pillResult = paintMarkerPill( + canvas: canvas, + contractMarker: contractMarker, + style: style, + painterProps: painterProps, + profitAndLossText: profitLossText, + quoteToY: quoteToY, + contractMarkerLeftPadding: + activeMarkerGroup.props.contractMarkerLeftPadding, + pillWidth: naturalPillWidth, + animationProgress: animationProgress, ); - canvas.restore(); - // Expand tap area to include both the icon and the pill - final Rect iconArea = Rect.fromCircle( - center: Offset(iconCenterX, centerY), radius: iconOuterRadius); - final Rect pillBounds = pillPath.getBounds(); - contractMarker.tapArea = pillBounds.expandToInclude(iconArea); + // Update the contract marker's tap area + contractMarker.tapArea = pillResult.tapArea; } @override diff --git a/lib/src/deriv_chart/chart/data_visualization/markers/chart_marker.dart b/lib/src/deriv_chart/chart/data_visualization/markers/chart_marker.dart index 6e302f318..795c6921e 100644 --- a/lib/src/deriv_chart/chart/data_visualization/markers/chart_marker.dart +++ b/lib/src/deriv_chart/chart/data_visualization/markers/chart_marker.dart @@ -15,6 +15,12 @@ enum MarkerType { /// markers like entry points, exit points, and barrier levels. contractMarker, + /// Represents a contract marker with fixed positioning. + /// + /// Similar to contractMarker but with fixed 24px spacing between the contract marker and the current spot. + /// Used specifically for accumulator contracts to maintain consistent spacing. + contractMarkerFixed, + /// Represents the start time of a contract with a vertical dashed line and a clock icon at the bottom. /// /// This marker visually indicates the contract's start time on the chart using a vertical dashed line diff --git a/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/accumulator_marker_icon_painter.dart b/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/accumulator_marker_icon_painter.dart index b8d80be64..899a736de 100644 --- a/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/accumulator_marker_icon_painter.dart +++ b/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/accumulator_marker_icon_painter.dart @@ -3,12 +3,16 @@ import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/markers/mar import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/painter_props.dart'; import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/tick_marker_icon_painter.dart'; import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/markers/chart_marker.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/markers/marker.dart'; import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/models/animation_info.dart'; import 'package:deriv_chart/src/deriv_chart/chart/helpers/paint_functions/paint_line.dart'; import 'package:deriv_chart/src/deriv_chart/chart/helpers/paint_functions/paint_text.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/helpers/paint_functions/paint_marker_pill.dart'; import 'package:deriv_chart/src/deriv_chart/chart/y_axis/y_axis_config.dart'; import 'package:deriv_chart/src/theme/chart_theme.dart'; +import 'package:deriv_chart/src/theme/painting_styles/marker_style.dart'; import 'package:flutter/material.dart'; +import 'dart:math' as math; /// AccumulatorMarkerIconPainter is a specialized painter for rendering accumulator contract markers on charts. /// @@ -79,29 +83,22 @@ class AccumulatorMarkerIconPainter extends TickMarkerIconPainter { PainterProps painterProps, AnimationInfo animationInfo, ) { - super.paintMarkerGroup( - canvas, - size, - theme, - markerGroup, - epochToX, - quoteToY, - painterProps, - animationInfo, - ); - + // 1. Collect marker references final Map markers = {}; - for (final ChartMarker marker in markerGroup.markers) { if (marker.markerType != null) { markers[marker.markerType!] = marker; } } + final ChartMarker? contractMarker = markers[MarkerType.contractMarker] ?? + markers[MarkerType.contractMarkerFixed]; + final ChartMarker? latestTickMarker = markers[MarkerType.latestTick]; final ChartMarker? lowMarker = markers[MarkerType.lowBarrier]; final ChartMarker? highMarker = markers[MarkerType.highBarrier]; final ChartMarker? endMarker = markers[MarkerType.exitSpot]; + // 2. Draw barriers first (so they appear behind the contract marker) if (lowMarker != null && highMarker != null) { final Offset lowOffset = _getOffset(lowMarker, epochToX, quoteToY); final Offset highOffset = _getOffset(highMarker, epochToX, quoteToY); @@ -123,6 +120,106 @@ class AccumulatorMarkerIconPainter extends TickMarkerIconPainter { bottom: lowOffset.dy, ); } + + // 3. Draw contract marker with pill + double pillRightEdge = 0; + if (contractMarker != null) { + final MarkerStyle style = markerGroup.style; + final double outerRadius = + (12 * painterProps.zoom) + (1 * painterProps.zoom); + final double iconCenterX = + markerGroup.props.contractMarkerLeftPadding + outerRadius; + final double iconOuterRadius = style.radius + 4; + final double centerY = quoteToY(contractMarker.quote); + + // Draw the circular contract marker first using parent's protected method + final Offset anchor = Offset(iconCenterX, centerY); + YAxisConfig.instance.yAxisClipping(canvas, size, () { + drawContractMarkerCircle( + canvas, + contractMarker, + anchor, + style, + 1.2, + painterProps.granularity, + 1, + animationInfo, + markerGroup, + ); + }); + + // Calculate pill width based on text and marker type + final String profitLossText = markerGroup.profitAndLossText ?? ''; + final TextPainter textPainter = makeTextPainter( + profitLossText, + style.activeMarkerText, + ); + + final double arcRightX = iconCenterX + iconOuterRadius; + + // Determine pill width based on marker type + double pillWidth; + if (contractMarker.markerType == MarkerType.contractMarkerFixed && + latestTickMarker != null) { + // Fixed positioning: 24px gap from latest tick + final double latestTickX = epochToX(latestTickMarker.epoch); + const double fixedGap = 24; + final double maxPillRight = latestTickX - fixedGap; + final double naturalPillWidth = + style.textLeftPadding + textPainter.width + style.textRightPadding; + pillWidth = math.max(0, maxPillRight - arcRightX); + // If natural width fits, use it; otherwise constrain to available space + pillWidth = math.min(pillWidth, naturalPillWidth); + } else { + // Standard positioning: pill extends naturally based on text width + pillWidth = + style.textLeftPadding + textPainter.width + style.textRightPadding; + } + + // Paint the profit/loss pill (no animation, always fully expanded) + final MarkerPillResult pillResult = paintMarkerPill( + canvas: canvas, + contractMarker: contractMarker, + style: style, + painterProps: painterProps, + profitAndLossText: profitLossText, + quoteToY: quoteToY, + contractMarkerLeftPadding: markerGroup.props.contractMarkerLeftPadding, + pillWidth: pillWidth, + ); + + pillRightEdge = pillResult.pillRightEdge; + contractMarker.tapArea = pillResult.tapArea; + } + + // [AI] + // 4. Draw horizontal dashed connector line from pill to current spot + if (contractMarker != null && latestTickMarker != null) { + final double lineEndX = epochToX(latestTickMarker.epoch); + final double lineY = quoteToY(contractMarker.quote); + + // Only draw the line if there's space between pill and latest tick + final double distance = lineEndX - pillRightEdge; + + if (distance > 0) { + final Color lineColor = contractMarker.direction == MarkerDirection.up + ? theme.markerStyle.upColorProminent + : theme.markerStyle.downColorProminent; + + YAxisConfig.instance.yAxisClipping(canvas, size, () { + paintHorizontalDashedLine( + canvas, + pillRightEdge, + lineEndX, + lineY, + lineColor, + 1, + dashWidth: 2, + dashSpace: 2, + ); + }); + } + } } /// Draws shaded barriers between high and low price levels. @@ -305,9 +402,9 @@ class AccumulatorMarkerIconPainter extends TickMarkerIconPainter { endX, y, barrierColor, - 1.5, + 1, dashWidth: 2, - dashSpace: 4, + dashSpace: 2, ); } } diff --git a/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/tick_marker_icon_painter.dart b/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/tick_marker_icon_painter.dart index a431ea5bf..3aeebbe9a 100644 --- a/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/tick_marker_icon_painter.dart +++ b/lib/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/tick_marker_icon_painter.dart @@ -79,8 +79,9 @@ class TickMarkerIconPainter extends MarkerGroupIconPainter { for (final ChartMarker marker in markerGroup.markers) { final Offset center; - // Special handling for contractMarker - position with left padding - if (marker.markerType == MarkerType.contractMarker) { + // Special handling for contractMarker and contractMarkerFixed - position with left padding + if (marker.markerType == MarkerType.contractMarker || + marker.markerType == MarkerType.contractMarkerFixed) { center = Offset( markerGroup.props.contractMarkerLeftPadding + _contractOuterRadius, quoteToY(marker.quote), @@ -115,7 +116,8 @@ class TickMarkerIconPainter extends MarkerGroupIconPainter { for (final ChartMarker marker in markerGroup.markers) { final Offset center = points[marker.markerType!] ?? - (marker.markerType == MarkerType.contractMarker + (marker.markerType == MarkerType.contractMarker || + marker.markerType == MarkerType.contractMarkerFixed ? Offset( markerGroup.props.contractMarkerLeftPadding + _contractOuterRadius, @@ -311,6 +313,10 @@ class TickMarkerIconPainter extends MarkerGroupIconPainter { _drawContractMarker(canvas, marker, anchor, style, 1.2, granularity, opacity, animationInfo, markerGroupId, markerGroup); break; + case MarkerType.contractMarkerFixed: + _drawContractMarker(canvas, marker, anchor, style, 1.2, granularity, + opacity, animationInfo, markerGroupId, markerGroup); + break; case MarkerType.startTime: paintStartLine(canvas, size, marker, anchor, style, theme, zoom, markerGroup.props); @@ -591,9 +597,116 @@ class TickMarkerIconPainter extends MarkerGroupIconPainter { AnimationInfo animationInfo, String? markerGroupId, MarkerGroup markerGroup, + ) { + // Use the extracted protected method + drawContractMarkerCircle( + canvas, + marker, + anchor, + style, + zoom, + granularity, + opacity, + animationInfo, + markerGroup, + ); + } + + /// Protected helper method to draw a label inside the contract marker circle. + /// + /// This method is accessible to subclasses for reuse. + @protected + void drawContractMarkerLabel(Canvas canvas, Offset anchor, String label, + double opacity, double zoom, MarkerStyle style) { + // Base radius used in _drawContractMarker + final double radius = 12 * zoom; + final double padding = 5 * zoom; // small padding from circle border + final double maxWidth = (radius - padding) * 2; + final double maxHeight = (radius - padding) * 2; + + final TextStyle baseTextStyle = style.markerLabelTextStyle.copyWith( + fontSize: style.markerLabelTextStyle.fontSize! * zoom, + color: style.markerLabelTextStyle.color!.withOpacity(opacity), + height: 1, + ); + + final TextPainter painter = makeFittedTextPainter( + label, + baseTextStyle, + maxWidth: maxWidth, + maxHeight: maxHeight, + ); + + paintWithTextPainter( + canvas, + painter: painter, + anchor: anchor, + ); + } + + /// Protected helper method to draw a diagonal arrow icon inside the contract marker. + /// + /// This method is accessible to subclasses for reuse. + @protected + void drawContractMarkerArrow(Canvas canvas, Offset center, ChartMarker marker, + Color color, double zoom) { + final double dir = marker.direction == MarkerDirection.up ? 1 : -1; + final double iconSize = 22 * zoom; + + final Path path = Path(); + + canvas + ..save() + ..translate( + center.dx - iconSize / 2, + center.dy - (iconSize / 2) * dir, + ) + // Scale from 24x24 original SVG size to desired icon size + ..scale( + iconSize / 24, + (iconSize / 24) * dir, + ); + + // Arrow-up path (will be flipped for down direction) + path + ..moveTo(17, 8) + ..lineTo(17, 15) + ..cubicTo(17, 15.5625, 16.5312, 16, 16, 16) + ..cubicTo(15.4375, 16, 15, 15.5625, 15, 15) + ..lineTo(15, 10.4375) + ..lineTo(8.6875, 16.7188) + ..cubicTo(8.3125, 17.125, 7.65625, 17.125, 7.28125, 16.7188) + ..cubicTo(6.875, 16.3438, 6.875, 15.6875, 7.28125, 15.3125) + ..lineTo(13.5625, 9) + ..lineTo(9, 9) + ..cubicTo(8.4375, 9, 8, 8.5625, 8, 8) + ..cubicTo(8, 7.46875, 8.4375, 7, 9, 7) + ..lineTo(16, 7) + ..cubicTo(16.5312, 7, 17, 7.46875, 17, 8) + ..close(); + + canvas + ..drawPath(path, Paint()..color = color) + ..restore(); + } + + /// Protected helper method to draw the contract marker circle with progress arc. + /// + /// This method is accessible to subclasses for reuse. + @protected + void drawContractMarkerCircle( + Canvas canvas, + ChartMarker marker, + Offset anchor, + MarkerStyle style, + double zoom, + int granularity, + double opacity, + AnimationInfo animationInfo, + MarkerGroup markerGroup, ) { final double radius = 12 * zoom; - final double borderRadius = radius + (1 * zoom); // Add 1 pixel padding + final double borderRadius = radius + (1 * zoom); // Determine colors based on marker direction final Color markerColor = marker.direction == MarkerDirection.up @@ -648,48 +761,25 @@ class TickMarkerIconPainter extends MarkerGroupIconPainter { canvas.drawArc( Rect.fromCircle(center: anchor, radius: radius), - -math.pi / 2, // Start from top + -math.pi / 2, sweepAngle, false, progressPaint, ); + // Draw label or arrow icon if (markerGroup.props.markerLabel != null) { - _drawMarkerLabel( + drawContractMarkerLabel( canvas, anchor, markerGroup.props.markerLabel!, opacity, zoom, style); } else { - // Draw arrow icon in the center - _drawArrowIcon( + drawContractMarkerArrow( canvas, anchor, marker, Colors.white.withOpacity(opacity), zoom); } } void _drawMarkerLabel(Canvas canvas, Offset anchor, String label, double opacity, double zoom, MarkerStyle style) { - // Base radius used in _drawContractMarker - final double radius = 12 * zoom; - final double padding = 5 * zoom; // small padding from circle border - final double maxWidth = (radius - padding) * 2; - final double maxHeight = (radius - padding) * 2; - - final TextStyle baseTextStyle = style.markerLabelTextStyle.copyWith( - fontSize: style.markerLabelTextStyle.fontSize! * zoom, - color: style.markerLabelTextStyle.color!.withOpacity(opacity), - height: 1, - ); - - final TextPainter painter = makeFittedTextPainter( - label, - baseTextStyle, - maxWidth: maxWidth, - maxHeight: maxHeight, - ); - - paintWithTextPainter( - canvas, - painter: painter, - anchor: anchor, - ); + drawContractMarkerLabel(canvas, anchor, label, opacity, zoom, style); } /// Draws a diagonal arrow icon inside the contract marker. @@ -701,44 +791,7 @@ class TickMarkerIconPainter extends MarkerGroupIconPainter { /// @param zoom The current zoom level of the chart. void _drawArrowIcon(Canvas canvas, Offset center, ChartMarker marker, Color color, double zoom) { - final double dir = marker.direction == MarkerDirection.up ? 1 : -1; - final double iconSize = 22 * zoom; - - final Path path = Path(); - - canvas - ..save() - ..translate( - center.dx - iconSize / 2, - center.dy - (iconSize / 2) * dir, - ) - // Scale from 24x24 original SVG size to desired icon size - ..scale( - iconSize / 24, - (iconSize / 24) * dir, - ); - - // Arrow-up path (will be flipped for down direction) - path - ..moveTo(17, 8) - ..lineTo(17, 15) - ..cubicTo(17, 15.5625, 16.5312, 16, 16, 16) - ..cubicTo(15.4375, 16, 15, 15.5625, 15, 15) - ..lineTo(15, 10.4375) - ..lineTo(8.6875, 16.7188) - ..cubicTo(8.3125, 17.125, 7.65625, 17.125, 7.28125, 16.7188) - ..cubicTo(6.875, 16.3438, 6.875, 15.6875, 7.28125, 15.3125) - ..lineTo(13.5625, 9) - ..lineTo(9, 9) - ..cubicTo(8.4375, 9, 8, 8.5625, 8, 8) - ..cubicTo(8, 7.46875, 8.4375, 7, 9, 7) - ..lineTo(16, 7) - ..cubicTo(16.5312, 7, 17, 7.46875, 17, 8) - ..close(); - - canvas - ..drawPath(path, Paint()..color = color) - ..restore(); + drawContractMarkerArrow(canvas, center, marker, color, zoom); } /// Renders a tick point marker. diff --git a/lib/src/deriv_chart/chart/helpers/paint_functions/paint_marker_pill.dart b/lib/src/deriv_chart/chart/helpers/paint_functions/paint_marker_pill.dart new file mode 100644 index 000000000..1da7015ac --- /dev/null +++ b/lib/src/deriv_chart/chart/helpers/paint_functions/paint_marker_pill.dart @@ -0,0 +1,153 @@ +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/markers/chart_marker.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/markers/marker.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/data_visualization/markers/marker_icon_painters/painter_props.dart'; +import 'package:deriv_chart/src/deriv_chart/chart/helpers/paint_functions/paint_text.dart'; +import 'package:deriv_chart/src/theme/painting_styles/marker_style.dart'; +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +/// Result of painting a marker pill, containing dimensions and tap area. +class MarkerPillResult { + /// Creates a marker pill result. + const MarkerPillResult({ + required this.pillRightEdge, + required this.tapArea, + }); + + /// The right edge x-coordinate of the painted pill. + final double pillRightEdge; + + /// The tap area for the marker (icon + pill combined). + final Rect tapArea; +} + +/// Paints a pill-shaped profit/loss indicator attached to a contract marker. +/// +/// This helper draws a rounded pill that extends from the right side of a +/// circular contract marker icon, containing profit/loss text. The pill +/// can be animated or static based on the [animationProgress] parameter. +/// +/// The pill consists of: +/// - A right semicircle that continues from the contract marker icon +/// - A rectangular body with rounded right end +/// - A dark background with colored border +/// - White text showing profit/loss value +/// +/// @param canvas The canvas to draw on +/// @param contractMarker The contract marker this pill is attached to +/// @param style The marker style defining colors and dimensions +/// @param painterProps Properties like zoom level affecting marker size +/// @param profitAndLossText The text to display in the pill +/// @param quoteToY Function to convert quote value to y-coordinate +/// @param contractMarkerLeftPadding Left padding for the contract marker +/// @param pillWidth The desired width of the pill (excluding the icon) +/// @param animationProgress Animation progress from 0.0 to 1.0 (defaults to 1.0 for no animation) +/// @return MarkerPillResult containing the right edge position and tap area +MarkerPillResult paintMarkerPill({ + required Canvas canvas, + required ChartMarker contractMarker, + required MarkerStyle style, + required PainterProps painterProps, + required String profitAndLossText, + required double Function(double) quoteToY, + required double contractMarkerLeftPadding, + required double pillWidth, + double animationProgress = 1.0, +}) { + // Calculate dimensions based on zoom and style + final double outerRadius = (12 * painterProps.zoom) + (1 * painterProps.zoom); + final double iconCenterX = contractMarkerLeftPadding + outerRadius; + final double iconOuterRadius = style.radius + 4; + final double centerY = quoteToY(contractMarker.quote); + + // Create text painter with opacity based on animation progress + final TextPainter textPainter = makeTextPainter( + profitAndLossText, + style.activeMarkerText.copyWith( + color: Colors.white.withOpacity(animationProgress.clamp(0.0, 1.0)), + ), + ); + + final double pillRadius = iconOuterRadius; + final double arcRightX = iconCenterX + iconOuterRadius; + + // Apply animation to pill width + final double animatedPillWidth = pillWidth * animationProgress; + + // Build the pill path starting from the right semicircle of the icon + final double rightEndX = arcRightX + animatedPillWidth; + final Offset rightEndCenter = Offset(rightEndX - pillRadius, centerY); + + final Path pillPath = Path() + ..moveTo(iconCenterX, centerY - iconOuterRadius) + // Right semicircle of icon (from top to bottom) + ..arcTo( + Rect.fromCircle( + center: Offset(iconCenterX, centerY), radius: iconOuterRadius), + -math.pi / 2, + math.pi, + false, + ) + // Bottom edge to the right rounded end + ..lineTo(rightEndCenter.dx, centerY + pillRadius) + // Right rounded end (bottom to top) + ..arcTo( + Rect.fromCircle(center: rightEndCenter, radius: pillRadius), + math.pi / 2, + -math.pi, + false, + ) + // Top edge back to the top of the icon arc + ..lineTo(iconCenterX, centerY - iconOuterRadius) + ..close(); + + // Pill background (dark container) + final Color pillFillColor = const Color(0xFF181C25); + canvas.drawPath(pillPath, Paint()..color = pillFillColor); + + // Pill border uses profit/loss color (exclude the left arc next to the icon) + final Color borderColor = contractMarker.direction == MarkerDirection.up + ? style.upColor + : style.downColor; + final Path pillBorderPath = Path() + ..moveTo(iconCenterX, centerY + pillRadius) + ..lineTo(rightEndCenter.dx, centerY + pillRadius) + ..arcTo( + Rect.fromCircle(center: rightEndCenter, radius: pillRadius), + math.pi / 2, + -math.pi, + false, + ) + ..lineTo(iconCenterX, centerY - pillRadius); + canvas.drawPath( + pillBorderPath, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..strokeJoin = StrokeJoin.round + ..strokeCap = StrokeCap.round + ..color = borderColor, + ); + + // Label: paint while clipping to animated pill area so it reveals smoothly + canvas.save(); + canvas.clipPath(pillPath); + paintWithTextPainter( + canvas, + painter: textPainter, + anchor: Offset(arcRightX + style.textLeftPadding, centerY), + anchorAlignment: Alignment.centerLeft, + ); + canvas.restore(); + + // Calculate tap area to include both the icon and the pill + final Rect iconArea = Rect.fromCircle( + center: Offset(iconCenterX, centerY), radius: iconOuterRadius); + final Rect pillBounds = pillPath.getBounds(); + final Rect tapArea = pillBounds.expandToInclude(iconArea); + + return MarkerPillResult( + pillRightEdge: arcRightX + animatedPillWidth, + tapArea: tapArea, + ); +} diff --git a/lib/src/deriv_chart/chart/main_chart.dart b/lib/src/deriv_chart/chart/main_chart.dart index 8b84b960e..72073d73e 100644 --- a/lib/src/deriv_chart/chart/main_chart.dart +++ b/lib/src/deriv_chart/chart/main_chart.dart @@ -420,8 +420,8 @@ class _ChartImplementationState extends BasicChartState { super.build(context), if (widget.overlaySeries != null) _buildSeries(widget.overlaySeries!), - _buildAnnotations(), if (widget.markerSeries != null) _buildMarkerArea(), + _buildAnnotations(), if (widget.drawingTools != null && widget.useDrawingToolsV2) _buildInteractiveLayer(context, xAxis) else if (widget.drawingTools != null)