import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../../theme/etm_tokens.dart'; import '../models/dashboard_models.dart'; class DayPlanPainter extends CustomPainter { final DayPlan plan; final Color surfaceColor; final Color lineColor; final Color inkColor; final Color mutedColor; final bool isDark; const DayPlanPainter({ required this.plan, required this.surfaceColor, required this.lineColor, required this.inkColor, required this.mutedColor, required this.isDark, }); static const double _weatherH = 44.0; static const double _xAxisH = 22.0; static const double _leftPad = 28.0; static const double _maxPosKwh = 4.0; static const double _maxNegKwh = 1.8; @override void paint(Canvas canvas, Size size) { final chartH = size.height - _weatherH - _xAxisH; final chartW = size.width - _leftPad; final zeroY = _weatherH + chartH * (_maxPosKwh / (_maxPosKwh + _maxNegKwh)); final posScale = chartH * (_maxPosKwh / (_maxPosKwh + _maxNegKwh)) / _maxPosKwh; final negScale = chartH * (_maxNegKwh / (_maxPosKwh + _maxNegKwh)) / _maxNegKwh; final nowX = _leftPad + plan.nowHour / 24 * chartW; // ── Future background if (nowX < size.width) { canvas.drawRect( Rect.fromLTRB(nowX, _weatherH, size.width, _weatherH + chartH), Paint() ..color = (isDark ? const Color(0xFF06141D) : const Color(0xFFEEF2F5)) .withValues(alpha: 0.55), ); _drawHatch(canvas, Rect.fromLTRB(nowX, _weatherH, size.width, _weatherH + chartH)); } // ── Bars (24 hours) final barW = chartW / 24; for (final h in plan.hours) { final x = _leftPad + h.hour * barW; _drawStackedBars(canvas, x, barW, zeroY, posScale, negScale, h); } // ── Y axis labels _drawYLabels(canvas, zeroY, posScale, size); // ── Zero line canvas.drawLine( Offset(_leftPad, zeroY), Offset(size.width, zeroY), Paint() ..color = lineColor ..strokeWidth = 1.0, ); // ── X axis labels (0h 6h 12h 18h 24h) for (final h in [0, 6, 12, 18, 24]) { final x = _leftPad + h / 24 * chartW; _paintText( canvas, h == 24 ? '24h' : '${h}h', Offset(x, _weatherH + chartH + 4), mutedColor, 9, center: true, ); } // ── "Now" line + label if (nowX > _leftPad && nowX < size.width) { canvas.drawLine( Offset(nowX, _weatherH), Offset(nowX, _weatherH + chartH), Paint() ..color = inkColor.withValues(alpha: 0.6) ..strokeWidth = 1.2 ..style = PaintingStyle.stroke, ); final now = DateTime.now(); final label = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; _drawNowBadge(canvas, nowX, _weatherH, label); } // ── Weather band _drawWeatherBand(canvas, chartW, plan.weather); } void _drawStackedBars(Canvas canvas, double x, double barW, double zeroY, double posScale, double negScale, HourlyFlow h) { const gap = 1.5; final bw = barW - gap; // Positive stack (upward from zeroY) double topY = zeroY; void stackPos(double kWh, Color color) { if (kWh < 0.01) return; final h2 = kWh * posScale; canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromLTWH(x + gap / 2, topY - h2, bw, h2), const Radius.circular(1.5), ), Paint()..color = color, ); topY -= h2; } stackPos(h.solToHouse, EtmTokens.solToHouse); stackPos(h.solToEcs, EtmTokens.solToEcs); stackPos(h.solToVe, EtmTokens.solToVe); stackPos(h.solToBatt, EtmTokens.solToBatt); stackPos(h.solToGrid, EtmTokens.solToGrid); // Negative stack (downward from zeroY) double botY = zeroY; void stackNeg(double kWh, Color color) { if (kWh < 0.01) return; final h2 = kWh * negScale; canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromLTWH(x + gap / 2, botY, bw, h2), const Radius.circular(1.5), ), Paint()..color = color.withValues(alpha: 0.7), ); botY += h2; } stackNeg(h.battToHouse, EtmTokens.battToHouse); stackNeg(h.gridToHouse, EtmTokens.gridToHouse); } void _drawHatch(Canvas canvas, Rect rect) { canvas.save(); canvas.clipRect(rect); final hatchPaint = Paint() ..color = surfaceColor.withValues(alpha: 0.5) ..strokeWidth = 1.0 ..style = PaintingStyle.stroke; const step = 8.0; for (double d = -rect.height; d < rect.width + rect.height; d += step) { canvas.drawLine( Offset(rect.left + d, rect.top), Offset(rect.left + d + rect.height, rect.bottom), hatchPaint, ); } canvas.restore(); } void _drawYLabels(Canvas canvas, double zeroY, double posScale, Size size) { for (final kWh in [2.0, 4.0]) { final y = zeroY - kWh * posScale; _paintText(canvas, '${kWh.toInt()}', Offset(0, y - 5), mutedColor, 8); canvas.drawLine( Offset(_leftPad, y), Offset(size.width, y), Paint() ..color = lineColor.withValues(alpha: 0.6) ..strokeWidth = 0.5, ); } } void _drawNowBadge(Canvas canvas, double x, double top, String label) { final tp = TextPainter( text: TextSpan( text: label, style: TextStyle( fontFamily: 'IBMPlexMono', fontSize: 9, fontWeight: FontWeight.w600, color: surfaceColor, ), ), textDirection: TextDirection.ltr, )..layout(); const pad = 4.0; final rect = Rect.fromCenter( center: Offset(x, top - tp.height / 2 - pad - 2), width: tp.width + pad * 2, height: tp.height + pad, ); canvas.drawRRect( RRect.fromRectAndRadius(rect, const Radius.circular(4)), Paint()..color = inkColor.withValues(alpha: 0.75), ); tp.paint(canvas, Offset(rect.left + pad, rect.top + pad / 2)); } void _drawWeatherBand( Canvas canvas, double chartW, List points) { for (final p in points) { final x = _leftPad + p.hour / 24 * chartW; final iconColor = p.kind == WeatherKind.sun || p.kind == WeatherKind.partlyCloudy ? EtmTokens.solar : mutedColor; _drawWeatherIcon(canvas, Offset(x, 12), p.kind, iconColor); _paintText( canvas, '${p.tempC}°', Offset(x, 26), mutedColor, 9, center: true, ); } } void _drawWeatherIcon( Canvas canvas, Offset center, WeatherKind kind, Color color) { final paint = Paint()..color = color..style = PaintingStyle.fill; switch (kind) { case WeatherKind.sun: _drawSun(canvas, center, color); case WeatherKind.partlyCloudy: _drawSun(canvas, center + const Offset(-3, -3), color, r: 5); _drawCloud(canvas, center + const Offset(2, 2), color.withValues(alpha: 0.9)); case WeatherKind.cloudy: _drawCloud(canvas, center, mutedColor); case WeatherKind.moon: _drawMoon(canvas, center, color, paint); } } void _drawSun(Canvas canvas, Offset c, Color color, {double r = 6}) { final p = Paint()..color = color..style = PaintingStyle.fill; canvas.drawCircle(c, r, p); final rp = Paint() ..color = color ..strokeWidth = 1.2 ..style = PaintingStyle.stroke; for (int i = 0; i < 8; i++) { final a = i * math.pi / 4; canvas.drawLine( c + Offset(math.cos(a), math.sin(a)) * (r + 2), c + Offset(math.cos(a), math.sin(a)) * (r + 5), rp, ); } } void _drawCloud(Canvas canvas, Offset c, Color color) { final p = Paint()..color = color..style = PaintingStyle.fill; canvas.drawCircle(c + const Offset(-4, 1), 4, p); canvas.drawCircle(c + const Offset(1, -1), 5.5, p); canvas.drawCircle(c + const Offset(6, 2), 3.5, p); canvas.drawRect( Rect.fromLTWH(c.dx - 7, c.dy + 1, 16, 4), p, ); } void _drawMoon(Canvas canvas, Offset c, Color color, Paint paint) { final path = Path()..addOval(Rect.fromCircle(center: c, radius: 7)); final cut = Path()..addOval(Rect.fromCircle(center: c + const Offset(4, -3), radius: 6)); final crescent = Path.combine(PathOperation.difference, path, cut); canvas.drawPath(crescent, paint); } void _paintText(Canvas canvas, String text, Offset pos, Color color, double size, {bool center = false}) { final tp = TextPainter( text: TextSpan( text: text, style: TextStyle(fontSize: size, color: color, fontWeight: FontWeight.w500), ), textDirection: TextDirection.ltr, )..layout(); tp.paint( canvas, center ? Offset(pos.dx - tp.width / 2, pos.dy) : pos, ); } @override bool shouldRepaint(DayPlanPainter old) => old.plan != plan || old.isDark != isDark; }