import 'dart:math'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import '../../../../core/theme/app_theme.dart'; import '../../utils/pattern_explanations.dart'; class CandleModel { final DateTime time; final double open; final double high; final double low; final double close; final double volume; CandleModel({ required this.time, required this.open, required this.high, required this.low, required this.close, required this.volume, }); factory CandleModel.fromJson(Map json) { return CandleModel( time: DateTime.tryParse(json['timestamp'] ?? json['time'] ?? '') ?? DateTime.now(), open: (json['open'] ?? 0).toDouble(), high: (json['high'] ?? 0).toDouble(), low: (json['low'] ?? 0).toDouble(), close: (json['close'] ?? 0).toDouble(), volume: (json['volume'] ?? 0).toDouble(), ); } } class IndicatorModel { final DateTime timestamp; final double? ema20; final double? sma50; final double? sma200; final double? supertrendUpper; final double? supertrendLower; final String? supertrendDirection; IndicatorModel({ required this.timestamp, this.ema20, this.sma50, this.sma200, this.supertrendUpper, this.supertrendLower, this.supertrendDirection, }); factory IndicatorModel.fromJson(Map json) { return IndicatorModel( timestamp: DateTime.tryParse(json['timestamp'] ?? '') ?? DateTime.now(), ema20: json['ema20'] != null ? (json['ema20'] as num).toDouble() : null, sma50: json['sma50'] != null ? (json['sma50'] as num).toDouble() : null, sma200: json['sma200'] != null ? (json['sma200'] as num).toDouble() : null, supertrendUpper: json['supertrendUpper'] != null ? (json['supertrendUpper'] as num).toDouble() : null, supertrendLower: json['supertrendLower'] != null ? (json['supertrendLower'] as num).toDouble() : null, supertrendDirection: json['supertrendDirection']?.toString(), ); } } class PatternPoint { final DateTime time; final double price; PatternPoint(this.time, this.price); factory PatternPoint.fromJson(Map json) => PatternPoint(DateTime.tryParse(json['time'] ?? '') ?? DateTime.now(), (json['price'] as num).toDouble()); } class ChartPatternModel { final String type; final List upperLine; final List lowerLine; ChartPatternModel({required this.type, required this.upperLine, required this.lowerLine}); factory ChartPatternModel.fromJson(Map json) { return ChartPatternModel( type: json['type'] ?? '', upperLine: (json['upperLine'] as List? ?? []).map((e) => PatternPoint.fromJson(e)).toList(), lowerLine: (json['lowerLine'] as List? ?? []).map((e) => PatternPoint.fromJson(e)).toList(), ); } } class StrategySignalModel { final String type; final DateTime timestamp; final String direction; final double price; final String description; StrategySignalModel({required this.type, required this.timestamp, required this.direction, required this.price, required this.description}); factory StrategySignalModel.fromJson(Map json) { return StrategySignalModel( type: json['type'] ?? '', timestamp: DateTime.tryParse(json['timestamp'] ?? '') ?? DateTime.now(), direction: json['direction'] ?? '', price: (json['price'] as num).toDouble(), description: json['description'] ?? '', ); } } class CandlestickChart extends StatefulWidget { final List candles; final List patterns; final List signals; final List indicators; final bool showPatterns; final bool showSma50; final bool showSma200; final bool showEma; final bool showSignals; final bool showSupertrend; const CandlestickChart({ super.key, required this.candles, this.patterns = const [], this.signals = const [], this.indicators = const [], this.showPatterns = true, this.showSma50 = true, this.showSma200 = true, this.showEma = true, this.showSignals = true, this.showSupertrend = true, }); @override State createState() => _CandlestickChartState(); } class _CandlestickChartState extends State { double _scale = 1.0; double _panOffset = 0.0; Offset? _tapPosition; CandleModel? _selectedCandle; @override Widget build(BuildContext context) { if (widget.candles.isEmpty) { return const Center(child: Text('No chart data')); } final theme = AppTheme.activePreset; return LayoutBuilder( builder: (context, constraints) { final double baseWidth = 10.0; final double spacing = 5.0; final double totalCandleSpace = (baseWidth + spacing) * _scale; final double totalContentWidth = (widget.candles.length + 15) * totalCandleSpace; final double minOffset = constraints.maxWidth - totalContentWidth - 60.0; final double maxOffset = 100.0; _panOffset = _panOffset.clamp(minOffset < maxOffset ? minOffset : maxOffset, maxOffset); return Listener( onPointerSignal: (pointerSignal) { if (pointerSignal is PointerScrollEvent) { setState(() { final double zoomFactor = pointerSignal.scrollDelta.dy > 0 ? 0.9 : 1.1; _scale = (_scale * zoomFactor).clamp(0.2, 5.0); }); } }, child: GestureDetector( onScaleUpdate: (details) { setState(() { _scale = (_scale * details.scale).clamp(0.2, 5.0); _panOffset += details.focalPointDelta.dx; _panOffset = _panOffset.clamp(minOffset, maxOffset); if (_tapPosition != null) { _handleTap(Offset(_tapPosition!.dx + details.focalPointDelta.dx, _tapPosition!.dy), constraints.maxWidth); } }); }, onScaleEnd: (_) => setState(() { _tapPosition = null; _selectedCandle = null; }), onTapDown: (details) { _handleTap(details.localPosition, constraints.maxWidth); }, child: Stack( children: [ ClipRect( child: CustomPaint( size: Size.infinite, painter: _CandlePainter( candles: widget.candles, patterns: widget.patterns, signals: widget.signals, indicators: widget.indicators, scale: _scale, panOffset: _panOffset, theme: theme, showPatterns: widget.showPatterns, showSma50: widget.showSma50, showSma200: widget.showSma200, showEma: widget.showEma, showSignals: widget.showSignals, showSupertrend: widget.showSupertrend, tapPosition: _tapPosition, ), ), ), if (_selectedCandle != null) _buildTooltip(theme), // Floating Zoom & Pan Controls (Top-Left) Positioned( left: 12, top: 12, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), decoration: BoxDecoration( color: theme.cardSurface.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(20), border: Border.all(color: theme.glassBorder), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.zoom_in, size: 18), color: theme.primaryColor, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), onPressed: () => setState(() => _scale = (_scale * 1.25).clamp(0.2, 5.0)), tooltip: 'Zoom In', ), IconButton( icon: const Icon(Icons.zoom_out, size: 18), color: theme.primaryColor, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), onPressed: () => setState(() => _scale = (_scale * 0.8).clamp(0.2, 5.0)), tooltip: 'Zoom Out', ), IconButton( icon: const Icon(Icons.center_focus_strong, size: 18), color: theme.textMuted, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), onPressed: () => setState(() { _scale = 1.0; _panOffset = 0.0; }), tooltip: 'Reset Zoom & Pan', ), ], ), ), ), ], ), ), ); }, ); } void _handleTap(Offset pos, double width) { if (widget.candles.isEmpty) return; // Right side is for axis, don't tap there if (pos.dx > width - 60) return; final double baseWidth = 10.0; final double spacing = 5.0; final double candleWidth = baseWidth * _scale; final double totalCandleSpace = candleWidth + (spacing * _scale); // dx = (i * totalCandleSpace) + _panOffset; // (dx - _panOffset) / totalCandleSpace = i; final int index = ((pos.dx - _panOffset) / totalCandleSpace).round(); if (index >= 0 && index < widget.candles.length) { setState(() { _tapPosition = pos; _selectedCandle = widget.candles[index]; }); } } Widget _buildTooltip(ThemePreset theme) { final candle = _selectedCandle!; final dateStr = "${candle.time.year}-${candle.time.month.toString().padLeft(2,'0')}-${candle.time.day.toString().padLeft(2,'0')}"; return Positioned( left: 10, top: 10, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: theme.cardSurface.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(8), border: Border.all(color: theme.glassBorder), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(dateStr, style: TextStyle(color: theme.textMuted, fontSize: 12)), Text('O: ${candle.open.toStringAsFixed(2)}', style: TextStyle(color: theme.textPrimary, fontSize: 12)), Text('H: ${candle.high.toStringAsFixed(2)}', style: TextStyle(color: theme.textPrimary, fontSize: 12)), Text('L: ${candle.low.toStringAsFixed(2)}', style: TextStyle(color: theme.textPrimary, fontSize: 12)), Text('C: ${candle.close.toStringAsFixed(2)}', style: TextStyle(color: theme.textPrimary, fontSize: 12)), Text('Vol: ${candle.volume.toStringAsFixed(0)}', style: TextStyle(color: theme.textPrimary, fontSize: 12)), ], ), ), ); } } class _CandlePainter extends CustomPainter { final List candles; final List patterns; final List signals; final List indicators; final double scale; final double panOffset; final ThemePreset theme; final bool showPatterns; final bool showSma50; final bool showSma200; final bool showEma; final bool showSignals; final bool showSupertrend; final Offset? tapPosition; final double rightPadding = 60.0; // Space for price axis final double bottomPadding = 20.0; // Space for X-axis labels _CandlePainter({ required this.candles, required this.patterns, required this.signals, required this.indicators, required this.scale, required this.panOffset, required this.theme, required this.showPatterns, required this.showSma50, required this.showSma200, required this.showEma, required this.showSignals, required this.showSupertrend, this.tapPosition, }); @override void paint(Canvas canvas, Size size) { if (candles.isEmpty) return; final double chartWidth = size.width - rightPadding; final double baseWidth = 10.0; final double spacing = 5.0; final double candleWidth = baseWidth * scale; final double totalCandleSpace = candleWidth + (spacing * scale); double maxPrice = 0; double minPrice = double.infinity; // Find min/max in view int firstVisibleIndex = -1; for (int i = 0; i < candles.length; i++) { final dx = (i * totalCandleSpace) + panOffset; if (dx + candleWidth > 0 && dx < chartWidth) { if (firstVisibleIndex == -1) firstVisibleIndex = i; final c = candles[i]; if (c.high > maxPrice) maxPrice = c.high; if (c.low < minPrice) minPrice = c.low; } } if (minPrice == double.infinity || maxPrice == 0) return; // Add 10% padding to top/bottom final range = maxPrice - minPrice; maxPrice += range * 0.1; minPrice -= range * 0.1; final paddedRange = maxPrice - minPrice; if (paddedRange <= 0) return; final double chartHeight = size.height - bottomPadding; final double volumeHeight = chartHeight * 0.15; // Bottom 15% for volume final double candleAreaHeight = chartHeight - volumeHeight; double maxVolume = 0; for (int i = 0; i < candles.length; i++) { if (candles[i].volume > maxVolume) maxVolume = candles[i].volume; } if (maxVolume == 0) maxVolume = 1; _drawGridAndAxis(canvas, size, chartWidth, candleAreaHeight, minPrice, maxPrice, paddedRange); final paintBullish = Paint()..color = theme.primaryColor..style = PaintingStyle.fill; final paintBearish = Paint()..color = theme.accentRed..style = PaintingStyle.fill; final paintWickBullish = Paint()..color = theme.primaryColor..strokeWidth = 1.5; final paintWickBearish = Paint()..color = theme.accentRed..strokeWidth = 1.5; final ema20Path = Path(); final sma50Path = Path(); final sma200Path = Path(); final supertrendPath = Path(); bool firstEma20 = true; bool firstSma50 = true; bool firstSma200 = true; bool firstSupertrend = true; // Map DateTime to X for patterns and signals double getXForTime(DateTime t) { int bestIndex = 0; int minDiff = 999999999; for (int i = 0; i < candles.length; i++) { final diff = candles[i].time.difference(t).inSeconds.abs(); if (diff < minDiff) { minDiff = diff; bestIndex = i; } } return (bestIndex * totalCandleSpace) + panOffset + candleWidth / 2; } double getYForPrice(double price) { return candleAreaHeight - ((price - minPrice) / paddedRange) * candleAreaHeight; } // Clip to chart area so we don't draw over the axis canvas.save(); canvas.clipRect(Rect.fromLTWH(0, 0, chartWidth, chartHeight)); for (int i = 0; i < candles.length; i++) { final candle = candles[i]; final isBullish = candle.close >= candle.open; final dx = (i * totalCandleSpace) + panOffset; if (dx < -candleWidth || dx > chartWidth) continue; // Culling final yHigh = getYForPrice(candle.high); final yLow = getYForPrice(candle.low); final yOpen = getYForPrice(candle.open); final yClose = getYForPrice(candle.close); // Draw Wick canvas.drawLine( Offset(dx + candleWidth / 2, yHigh), Offset(dx + candleWidth / 2, yLow), isBullish ? paintWickBullish : paintWickBearish, ); // Draw Body final top = min(yOpen, yClose); final bottom = max(yOpen, yClose); final bodyHeight = max(bottom - top, 1.0); // minimum 1px height canvas.drawRect( Rect.fromLTWH(dx, top, candleWidth, bodyHeight), isBullish ? paintBullish : paintBearish, ); // Draw Volume final vHeight = (candle.volume / maxVolume) * volumeHeight; final vTop = chartHeight - vHeight; canvas.drawRect( Rect.fromLTWH(dx, vTop, candleWidth, vHeight), Paint()..color = (isBullish ? theme.primaryColor : theme.accentRed).withValues(alpha: 0.3)..style = PaintingStyle.fill, ); // Indicators mapping by time if (indicators.isNotEmpty) { final cx = dx + candleWidth / 2; IndicatorModel? match; for (var ind in indicators) { if (ind.timestamp.isAtSameMomentAs(candle.time) || ind.timestamp.difference(candle.time).inHours.abs() < 12) { match = ind; break; } } if (match != null) { if (showEma && match.ema20 != null) { final y = getYForPrice(match.ema20!); if (firstEma20) { ema20Path.moveTo(cx, y); firstEma20 = false; } else { ema20Path.lineTo(cx, y); } } if (showSma50 && match.sma50 != null) { final y = getYForPrice(match.sma50!); if (firstSma50) { sma50Path.moveTo(cx, y); firstSma50 = false; } else { sma50Path.lineTo(cx, y); } } if (showSma200 && match.sma200 != null) { final y = getYForPrice(match.sma200!); if (firstSma200) { sma200Path.moveTo(cx, y); firstSma200 = false; } else { sma200Path.lineTo(cx, y); } } if (showSupertrend) { final stVal = match.supertrendDirection == 'BULLISH' ? match.supertrendLower : match.supertrendUpper; if (stVal != null) { final y = getYForPrice(stVal); if (firstSupertrend) { supertrendPath.moveTo(cx, y); firstSupertrend = false; } else { supertrendPath.lineTo(cx, y); } } } } } } if (showEma && !firstEma20) { canvas.drawPath(ema20Path, Paint()..color = Colors.blueAccent..style = PaintingStyle.stroke..strokeWidth = 1.5); } if (showSma50 && !firstSma50) { canvas.drawPath(sma50Path, Paint()..color = Colors.orangeAccent..style = PaintingStyle.stroke..strokeWidth = 1.5); } if (showSma200 && !firstSma200) { canvas.drawPath(sma200Path, Paint()..color = Colors.redAccent..style = PaintingStyle.stroke..strokeWidth = 2.0); } if (showSupertrend && !firstSupertrend) { canvas.drawPath(supertrendPath, Paint()..color = AppTheme.primaryEmerald..style = PaintingStyle.stroke..strokeWidth = 2.0); } if (showPatterns) { _drawPatterns(canvas, getXForTime, getYForPrice); _drawFutureProjectionZone(canvas, size, chartWidth, candleAreaHeight, getXForTime, getYForPrice); } if (showSignals) { _drawSignals(canvas, getXForTime, getYForPrice); } if (tapPosition != null && tapPosition!.dx < chartWidth) { _drawCrosshair(canvas, size, chartWidth, chartHeight); } canvas.restore(); // Restore clip } void _drawFutureProjectionZone(Canvas canvas, Size size, double chartWidth, double candleAreaHeight, double Function(DateTime) getX, double Function(double) getY) { if (candles.isEmpty) return; final lastCandle = candles.last; final double lastX = getX(lastCandle.time); if (lastX < chartWidth) { // 1. Shaded background for Future Zone (No divider line) final futureRect = Rect.fromLTRB(lastX, 0, chartWidth, candleAreaHeight); final futureBgPaint = Paint() ..color = const Color(0xFF001F3F).withValues(alpha: 0.25) ..style = PaintingStyle.fill; canvas.drawRect(futureRect, futureBgPaint); // Label for Future Zone final textPainter = TextPainter(textDirection: TextDirection.ltr); textPainter.text = TextSpan( text: 'PROGNOSE (MUSTER-SCHÄTZUNG)', style: TextStyle(color: theme.primaryColor, fontSize: 9, fontWeight: FontWeight.bold, letterSpacing: 0.8), ); textPainter.layout(); textPainter.paint(canvas, Offset(lastX + 8, 8)); // 2. Projected Ghost Candles & Target Line for active patterns for (var pattern in patterns) { if (pattern.lowerLine.isNotEmpty || pattern.upperLine.isNotEmpty) { final targetPrice = pattern.lowerLine.isNotEmpty ? pattern.lowerLine.last.price : (pattern.upperLine.isNotEmpty ? pattern.upperLine.last.price : 0); if (targetPrice > 0) { final targetY = getY(targetPrice.toDouble()); final int numSteps = 10; final double stepWidth = (chartWidth - lastX - 30) / numSteps; if (stepWidth <= 0) continue; final isBullish = targetPrice >= lastCandle.close; final projColor = isBullish ? Colors.greenAccent : Colors.redAccent; double currX = lastX; double currPrice = lastCandle.close; final double priceDeltaPerStep = (targetPrice - lastCandle.close) / numSteps; for (int k = 1; k <= numSteps; k++) { final nextX = lastX + k * stepWidth; final waveNoise = sin(k * 0.8) * (priceDeltaPerStep.abs() * 0.3); final nextPrice = lastCandle.close + (priceDeltaPerStep * k) + waveNoise; final highPrice = max(currPrice, nextPrice) + priceDeltaPerStep.abs() * 0.2; final lowPrice = min(currPrice, nextPrice) - priceDeltaPerStep.abs() * 0.2; final yOpen = getY(currPrice); final yClose = getY(nextPrice); final yHigh = getY(highPrice); final yLow = getY(lowPrice); final cWidth = max(stepWidth * 0.6, 3.0); final cLeft = nextX - cWidth / 2; final isStepBullish = nextPrice >= currPrice; final stepColor = isStepBullish ? Colors.greenAccent : Colors.redAccent; // Draw Ghost Candle Wick canvas.drawLine( Offset(nextX, yHigh), Offset(nextX, yLow), Paint()..color = stepColor.withValues(alpha: 0.4)..strokeWidth = 1.0, ); // Draw Ghost Candle Body final top = min(yOpen, yClose); final bottom = max(yOpen, yClose); canvas.drawRect( Rect.fromLTWH(cLeft, top, cWidth, max(bottom - top, 1.0)), Paint()..color = stepColor.withValues(alpha: 0.35)..style = PaintingStyle.fill, ); currX = nextX; currPrice = nextPrice; } // Target Price Badge at final step final targetX = currX; final targetBadgePainter = TextPainter( text: TextSpan( text: ' ZIEL: ${targetPrice.toStringAsFixed(2)} € ', style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), ), textDirection: TextDirection.ltr, ); targetBadgePainter.layout(); final badgeRect = RRect.fromLTRBR( targetX - targetBadgePainter.width / 2, targetY - targetBadgePainter.height / 2 - 2, targetX + targetBadgePainter.width / 2, targetY + targetBadgePainter.height / 2 + 2, const Radius.circular(6), ); canvas.drawRRect(badgeRect, Paint()..color = projColor.withValues(alpha: 0.9)); targetBadgePainter.paint(canvas, Offset(targetX - targetBadgePainter.width / 2, targetY - targetBadgePainter.height / 2)); } } } } } void _drawGridAndAxis(Canvas canvas, Size size, double chartWidth, double candleAreaHeight, double minPrice, double maxPrice, double range) { final gridPaint = Paint() ..color = theme.glassBorder ..strokeWidth = 1; final textPainter = TextPainter(textDirection: TextDirection.ltr); // Y Axis final int gridLines = 5; for (int i = 0; i <= gridLines; i++) { final y = candleAreaHeight - (i / gridLines) * candleAreaHeight; final price = minPrice + (i / gridLines) * range; canvas.drawLine(Offset(0, y), Offset(chartWidth, y), gridPaint); textPainter.text = TextSpan( text: price.toStringAsFixed(2), style: TextStyle(color: theme.textMuted, fontSize: 11), ); textPainter.layout(); textPainter.paint(canvas, Offset(chartWidth + 5, y - 6)); } // X Axis if (candles.isEmpty) return; final double baseWidth = 10.0; final double spacing = 5.0; final double candleWidth = baseWidth * scale; final double totalCandleSpace = candleWidth + (spacing * scale); final int xSteps = (chartWidth / 80).floor(); // label every 80px if (xSteps <= 0) return; for (int i = 1; i < xSteps; i++) { double x = i * (chartWidth / xSteps); int candleIndex = ((x - panOffset) / totalCandleSpace).round(); if (candleIndex >= 0 && candleIndex < candles.length) { final t = candles[candleIndex].time; textPainter.text = TextSpan( text: "${t.month.toString().padLeft(2,'0')}-${t.day.toString().padLeft(2,'0')}", style: TextStyle(color: theme.textMuted, fontSize: 10), ); textPainter.layout(); textPainter.paint(canvas, Offset(x - textPainter.width / 2, size.height - bottomPadding + 4)); canvas.drawLine(Offset(x, 0), Offset(x, size.height - bottomPadding), gridPaint); } } } void _drawPatterns(Canvas canvas, double Function(DateTime) getX, double Function(double) getY) { for (var pattern in patterns) { final color = PatternExplanations.getColorForPattern(pattern.type); final paint = Paint() ..color = color ..style = PaintingStyle.stroke ..strokeWidth = 2.0; void drawLine(List points) { if (points.length < 2) return; final path = Path(); path.moveTo(getX(points[0].time), getY(points[0].price)); for (int i = 1; i < points.length; i++) { path.lineTo(getX(points[i].time), getY(points[i].price)); } canvas.drawPath(path, paint); } drawLine(pattern.upperLine); drawLine(pattern.lowerLine); } } void _drawSignals(Canvas canvas, double Function(DateTime) getX, double Function(double) getY) { for (var signal in signals) { final x = getX(signal.timestamp); final y = getY(signal.price); final isBuy = signal.direction.toUpperCase() == 'BUY'; final isSell = signal.direction.toUpperCase() == 'SELL'; if (!isBuy && !isSell) continue; final color = isBuy ? theme.primaryColor : theme.accentRed; final label = isBuy ? '▲ BUY' : '▼ SELL'; // Draw Pill Badge for Signal final textPainter = TextPainter( text: TextSpan( text: label, style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), ), textDirection: TextDirection.ltr, ); textPainter.layout(); final badgeWidth = textPainter.width + 12; final badgeHeight = textPainter.height + 6; final badgeY = isBuy ? y + 12 : y - badgeHeight - 12; final badgeRect = RRect.fromLTRBR( x - badgeWidth / 2, badgeY, x + badgeWidth / 2, badgeY + badgeHeight, const Radius.circular(10), ); // Pill Background canvas.drawRRect(badgeRect, Paint()..color = color.withValues(alpha: 0.95)); // Pointer Line to price point canvas.drawLine( Offset(x, y), Offset(x, isBuy ? badgeY : badgeY + badgeHeight), Paint()..color = color..strokeWidth = 1.5, ); // Text paint textPainter.paint(canvas, Offset(x - textPainter.width / 2, badgeY + 3)); } } void _drawCrosshair(Canvas canvas, Size size, double chartWidth, double chartHeight) { final paint = Paint() ..color = theme.textMuted.withValues(alpha: 0.5) ..strokeWidth = 1 ..style = PaintingStyle.stroke; // Vertical canvas.drawLine(Offset(tapPosition!.dx, 0), Offset(tapPosition!.dx, chartHeight), paint); // Horizontal if (tapPosition!.dy <= chartHeight) { canvas.drawLine(Offset(0, tapPosition!.dy), Offset(chartWidth, tapPosition!.dy), paint); } } @override bool shouldRepaint(covariant _CandlePainter oldDelegate) { return oldDelegate.scale != scale || oldDelegate.panOffset != panOffset || oldDelegate.candles != candles || oldDelegate.patterns != patterns || oldDelegate.signals != signals || oldDelegate.indicators != indicators || oldDelegate.tapPosition != tapPosition; } }