From d56db4fe3a9808e234adf4f436a0f915d3725d82 Mon Sep 17 00:00:00 2001 From: checkedear <271323618+checkedear@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:58:18 +0200 Subject: [PATCH] fix: grade chart style --- firka/lib/ui/phone/widgets/grade_chart.dart | 299 ++++++++++---------- 1 file changed, 148 insertions(+), 151 deletions(-) diff --git a/firka/lib/ui/phone/widgets/grade_chart.dart b/firka/lib/ui/phone/widgets/grade_chart.dart index 94c1eb3..058e190 100644 --- a/firka/lib/ui/phone/widgets/grade_chart.dart +++ b/firka/lib/ui/phone/widgets/grade_chart.dart @@ -1,3 +1,8 @@ +import 'dart:math' as math; +import 'package:firka/app/app_state.dart'; +import 'package:firka/core/settings.dart'; +import 'package:firka_common/firka_common.dart'; +import 'package:intl/intl.dart'; import 'package:kreta_api/kreta_api.dart'; import 'package:firka/routing/chart_interaction_scope.dart'; import 'package:firka/ui/components/grade_helpers.dart'; @@ -13,20 +18,18 @@ class GradeChart extends StatefulWidget { State createState() => _GradeChartState(); } +class DateSpot extends FlSpot { + final DateTime date; + + const DateSpot(super.x, super.y, this.date); +} + class _GradeChartState extends State { - bool _tooltipActive = false; double? _tooltipY; int? _touchedIndex; + DateFormat tooltipFormat = DateFormat("MM.dd."); - List gradientColors = [ - appStyle.colors.grade5, - appStyle.colors.grade4, - appStyle.colors.grade3, - appStyle.colors.grade2, - appStyle.colors.grade1, - ]; - - late List spots; + late List spots; double? _subjectAverageInList(List grades, String subjectUid) { double weightedSum = 0; @@ -75,25 +78,28 @@ class _GradeChartState extends State { } void _computeSpots() { - final sortedGrades = List.from(widget.grades) - ..sort((a, b) => a.creationDate.compareTo(b.creationDate)); + final sortedGrades = + widget.grades + .where( + (grade) => + grade.numericValue != null && grade.weightPercentage != null, + ) + .toList() + ..sort((a, b) => a.recordDate.compareTo(b.recordDate)); if (sortedGrades.isEmpty) { - spots = [const FlSpot(0, 0)]; + spots = [DateSpot(0, 0, DateTime.now()), DateSpot(1, 0, DateTime.now())]; return; } + if (sortedGrades.length == 1) { + sortedGrades.insert(0, sortedGrades[0]); + } + spots = []; for (var i = 0; i < sortedGrades.length; i++) { - final grade = sortedGrades[i]; - if (grade.numericValue != null && grade.weightPercentage != null) { - final partialAvg = _runningSubjectAverage(sortedGrades, i); - spots.add(FlSpot(i.toDouble(), partialAvg)); - } - } - - if (spots.isEmpty) { - spots = [const FlSpot(0, 0)]; + final partialAvg = _runningSubjectAverage(sortedGrades, i); + spots.add(DateSpot(i.toDouble(), partialAvg, sortedGrades[i].recordDate)); } } @@ -128,48 +134,57 @@ class _GradeChartState extends State { @override Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(12), - child: DecoratedBox( - decoration: BoxDecoration(color: appStyle.colors.card), - child: AspectRatio( - aspectRatio: 1.90, - child: Padding( - padding: const EdgeInsets.only( - right: 28, - left: 12, - top: 6, - bottom: 12, - ), - child: LineChart(avgData()), - ), - ), + return FirkaCard.single( + padding: 0, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: AspectRatio(aspectRatio: 1.82, child: LineChart(avgData())), ), ); } Widget bottomTitleWidgets(double value, TitleMeta meta) { - final style = TextStyle( - fontFamily: appStyle.fonts.B_16R.fontFamily, - fontWeight: FontWeight.bold, - fontSize: 16, - color: appStyle.colors.textSecondary, - ); - final firstX = spots.first.x.toInt(); final lastX = spots.last.x.toInt(); - String text = ''; - const epsilon = 0.01; + String content = ''; - if ((value - firstX).abs() < epsilon) { - text = 'Szeptember'; - } else if ((value - lastX).abs() < epsilon) { - text = 'Most'; + final shouldHideNow = + _touchedIndex != null && + meta.parentAxisSize / lastX * _touchedIndex! > meta.parentAxisSize - 62; + + if (value == _touchedIndex) { + final date = spots[_touchedIndex!].date; + content = tooltipFormat.format(date); + } else if (value == firstX) { + content = 'Szeptember'; + } else if (value == lastX && !shouldHideNow) { + content = 'Most'; } + final text = Text( + content, + style: appStyle.fonts.B_12R.apply(color: appStyle.colors.textSecondary), + ); + return SideTitleWidget( meta: meta, - child: Text(text, style: style), + space: 0, + fitInside: SideTitleFitInsideData.fromTitleMeta( + meta, + distanceFromEdge: value == lastX ? 12 : 0, + ), + child: value == _touchedIndex + ? Container( + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: ShapeDecoration( + color: appStyle.colors.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(27), + ), + ), + child: text, + ) + : text, ); } @@ -200,38 +215,55 @@ class _GradeChartState extends State { ); } + Color colorForY(double y) { + final rounding = initData.settings + .group("settings") + .subGroup("application") + .subGroup("rounding"); + return y == 0 + ? appStyle.colors.card + : getGradeColor( + y, + t1: rounding.dbl("1"), + t2: rounding.dbl("2"), + t3: rounding.dbl("3"), + t4: rounding.dbl("4"), + ); + } + Widget leftTitleWidgets(double value, TitleMeta meta) { - String text = switch (value.toInt()) { - 1 => '1', - 2 => '2', - 3 => '3', - 4 => '4', - 5 => '5', - _ => '', - }; - Color gradeColor; - if (text != "") { - gradeColor = getGradeColor(int.parse(text).toDouble()); - } else { - gradeColor = getGradeColor(0); + if (value == 0) { + return SizedBox(); } - final currentValue = _tooltipActive && _tooltipY != null - ? _tooltipY!.round() - : spots.last.y.round(); - final isActive = text == currentValue.toString(); + + final rounding = initData.settings + .group("settings") + .subGroup("application") + .subGroup("rounding"); + final currentValue = spots.first.y == 0 + ? 0 + : roundGrade( + _tooltipY ?? spots.first.y, + t1: rounding.dbl("1"), + t2: rounding.dbl("2"), + t3: rounding.dbl("3"), + t4: rounding.dbl("4"), + ); + final isActive = value == currentValue; if (isActive) { + final gradeColor = getGradeColor(value); return buildCircle( - text: text, + text: value.toStringAsFixed(0), bgColor: gradeColor.withAlpha(38), textColor: gradeColor, ); } return buildCircle( - text: text, + text: value.toStringAsFixed(0), bgColor: appStyle.colors.card, - textColor: appStyle.colors.textPrimary.withValues(alpha: 0.2), + textColor: appStyle.colors.textTertiary, ); // return Text(text, style: style, textAlign: TextAlign.left); @@ -240,29 +272,6 @@ class _GradeChartState extends State { LineChartData avgData() { final smoothedSpots = _smoothSpots(spots); - var firstX = smoothedSpots.first.x; - var lastX = smoothedSpots.last.x; - if (firstX == lastX) { - lastX = firstX + 1; - } - - Color colorForY(double y) { - switch (y.round()) { - case 1: - return appStyle.colors.grade1; - case 2: - return appStyle.colors.grade2; - case 3: - return appStyle.colors.grade3; - case 4: - return appStyle.colors.grade4; - case 5: - return appStyle.colors.grade5; - default: - return appStyle.colors.grade1; - } - } - return LineChartData( lineTouchData: LineTouchData( handleBuiltInTouches: true, @@ -270,49 +279,41 @@ class _GradeChartState extends State { enabled: true, touchCallback: (FlTouchEvent event, LineTouchResponse? response) { setState(() { - if (event is FlLongPressEnd || - event is FlPanEndEvent || - event is FlTapUpEvent) { - _tooltipActive = false; - _tooltipY = null; - _touchedIndex = null; - return; - } - - if (response?.lineBarSpots != null && - response!.lineBarSpots!.isNotEmpty) { - final spot = response.lineBarSpots!.first; - - _tooltipActive = true; - _tooltipY = spot.y; - _touchedIndex = spot.spotIndex; + if (event.isInterestedForInteractions) { + _touchedIndex = response!.lineBarSpots!.first.spotIndex; + _tooltipY = spots[_touchedIndex!].y; + if (_tooltipY! > 0) { + return; + } } + _tooltipY = null; + _touchedIndex = null; }); }, touchTooltipData: LineTouchTooltipData( - tooltipMargin: 0, + tooltipMargin: 4, getTooltipColor: (touchedSpot) => appStyle.colors.buttonSecondaryFill, - tooltipBorderRadius: BorderRadius.circular(90), - fitInsideVertically: true, + tooltipBorderRadius: BorderRadius.circular(27), + tooltipPadding: EdgeInsets.symmetric(vertical: 2, horizontal: 6), + fitInsideHorizontally: true, - showOnTopOfTheChartBoxArea: true, + showOnTopOfTheChartBoxArea: false, getTooltipItems: (touchedSpots) { return touchedSpots.map((LineBarSpot touchedSpot) { - final textStyle = TextStyle( - color: colorForY(touchedSpot.y), - fontWeight: FontWeight.bold, - fontSize: 14, - ); - return LineTooltipItem( - touchedSpot.y.toStringAsFixed(2), - textStyle, + if (touchedSpot.y == 0) { + return null; + } + final spot = spots[touchedSpot.spotIndex]; + final textStyle = appStyle.fonts.B_14SB.apply( + color: colorForY(spot.y), ); + return LineTooltipItem(spot.y.toStringAsFixed(2), textStyle); }).toList(); }, ), getTouchedSpotIndicator: (barData, spotIndexes) { return spotIndexes.map((index) { - final touchedSpot = barData.spots[index]; + final touchedSpot = spots[index]; return TouchedSpotIndicatorData( FlLine(color: colorForY(touchedSpot.y), strokeWidth: 3), FlDotData(show: false), @@ -320,33 +321,24 @@ class _GradeChartState extends State { }).toList(); }, ), - backgroundColor: appStyle.colors.card, + backgroundColor: Colors.transparent, + extraLinesData: ExtraLinesData( + horizontalLines: [ + HorizontalLine( + y: 5, + color: const Color(0xFFC8C8C8), + strokeWidth: 1.0, + dashArray: [8, 12], + ), + ], + extraLinesOnTop: false, + ), gridData: FlGridData( show: true, drawHorizontalLine: true, drawVerticalLine: false, horizontalInterval: 1, getDrawingHorizontalLine: (value) { - if (!_tooltipActive || _tooltipY == null) { - return FlLine( - color: const Color(0xFFC8C8C8), - strokeWidth: 1.0, - dashArray: [8, 12], - ); - } - - const epsilon = 0.01; - if ((value - _tooltipY!.round()).abs() < epsilon) { - // return FlLine( - // color: const Color(0xFFC8C8C8), - // strokeWidth: 1.2, - // ); - return FlLine( - color: const Color(0xFFC8C8C8), - strokeWidth: 1.0, - dashArray: [8, 12], - ); - } return FlLine( color: const Color(0xFFC8C8C8), strokeWidth: 1.0, @@ -359,30 +351,35 @@ class _GradeChartState extends State { bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - reservedSize: 30, getTitlesWidget: bottomTitleWidgets, interval: 1, ), + drawBelowEverything: false, + sideTitleAlignment: SideTitleAlignment.inside, ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: leftTitleWidgets, - reservedSize: 35, + reservedSize: 26, interval: 1, ), ), - topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 20, + getTitlesWidget: (v, meta) => SizedBox(), + ), + ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData(show: false), - minX: firstX, - maxX: lastX, minY: 0, - maxY: 6, + maxY: 5, lineBarsData: [ LineChartBarData(