import 'dart:math'; import 'package:flutter/material.dart'; import '../../../../core/theme/app_theme.dart'; import '../../models/technical_analysis_model.dart'; import 'chart_overlay_renderer.dart'; class CandlestickPainter 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; final double bottomPadding = 20.0; CandlestickPainter({ 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; int visibleCount = 0; for (int i = 0; i < candles.length; i++) { final dx = (i * totalCandleSpace) + panOffset; if (dx + candleWidth > -100 && dx < chartWidth + 100) { visibleCount++; final c = candles[i]; if (c.high > maxPrice) maxPrice = c.high; if (c.low < minPrice) minPrice = c.low; } } if (visibleCount == 0 || minPrice == double.infinity || maxPrice <= 0) { for (var c in candles) { if (c.high > maxPrice) maxPrice = c.high; if (c.low < minPrice) minPrice = c.low; } } if (minPrice == double.infinity || maxPrice <= 0) return; final range = maxPrice - minPrice; maxPrice += max(range * 0.1, 1.0); minPrice -= max(range * 0.1, 1.0); final paddedRange = maxPrice - minPrice; if (paddedRange <= 0) return; final double chartHeight = size.height - bottomPadding; final double volumeHeight = chartHeight * 0.15; 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; ChartOverlayRenderer.drawGridAndAxis( canvas: canvas, size: size, chartWidth: chartWidth, candleAreaHeight: candleAreaHeight, minPrice: minPrice, maxPrice: maxPrice, range: paddedRange, candles: candles, scale: scale, panOffset: panOffset, bottomPadding: bottomPadding, theme: theme, ); 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; double getXForTime(DateTime t) { if (candles.isEmpty) return 0.0; final lastCandle = candles.last; if (t.isAfter(lastCandle.timestamp) && candles.length > 1) { final totalSpan = lastCandle.timestamp.difference(candles.first.timestamp).inSeconds; final secPerCandle = totalSpan / (candles.length - 1); if (secPerCandle > 0) { final futureSecs = t.difference(lastCandle.timestamp).inSeconds; final futureCandles = futureSecs / secPerCandle; final lastDx = ((candles.length - 1) * totalCandleSpace) + panOffset + candleWidth / 2; return lastDx + (futureCandles * totalCandleSpace); } } int bestIndex = 0; int minDiff = 999999999; for (int i = 0; i < candles.length; i++) { final diff = candles[i].timestamp.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; } 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; final yHigh = getYForPrice(candle.high); final yLow = getYForPrice(candle.low); final yOpen = getYForPrice(candle.open); final yClose = getYForPrice(candle.close); canvas.drawLine( Offset(dx + candleWidth / 2, yHigh), Offset(dx + candleWidth / 2, yLow), isBullish ? paintWickBullish : paintWickBearish, ); final top = min(yOpen, yClose); final bottom = max(yOpen, yClose); final bodyHeight = max(bottom - top, 1.0); canvas.drawRect( Rect.fromLTWH(dx, top, candleWidth, bodyHeight), isBullish ? paintBullish : paintBearish, ); 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, ); if (indicators.isNotEmpty) { final cx = dx + candleWidth / 2; IndicatorModel? match; for (var ind in indicators) { if (ind.timestamp.isAtSameMomentAs(candle.timestamp) || ind.timestamp.difference(candle.timestamp).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) { ChartOverlayRenderer.drawPatterns( canvas: canvas, patterns: patterns, getX: getXForTime, getY: getYForPrice, ); ChartOverlayRenderer.drawFutureProjectionZone( canvas: canvas, candles: candles, patterns: patterns, chartWidth: chartWidth, candleAreaHeight: candleAreaHeight, getX: getXForTime, getY: getYForPrice, theme: theme, ); } if (showSignals) { ChartOverlayRenderer.drawSignals( canvas: canvas, signals: signals, getX: getXForTime, getY: getYForPrice, theme: theme, ); } if (tapPosition != null && tapPosition!.dx < chartWidth) { ChartOverlayRenderer.drawCrosshair( canvas: canvas, tapPosition: tapPosition!, chartWidth: chartWidth, chartHeight: chartHeight, theme: theme, ); } canvas.restore(); } @override bool shouldRepaint(covariant CandlestickPainter oldDelegate) { return oldDelegate.scale != scale || oldDelegate.panOffset != panOffset || oldDelegate.candles != candles || oldDelegate.patterns != patterns || oldDelegate.signals != signals || oldDelegate.indicators != indicators || oldDelegate.tapPosition != tapPosition; } }