forked from firka/firka
fix: grade chart style
This commit is contained in:
@@ -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<GradeChart> createState() => _GradeChartState();
|
||||
}
|
||||
|
||||
class DateSpot extends FlSpot {
|
||||
final DateTime date;
|
||||
|
||||
const DateSpot(super.x, super.y, this.date);
|
||||
}
|
||||
|
||||
class _GradeChartState extends State<GradeChart> {
|
||||
bool _tooltipActive = false;
|
||||
double? _tooltipY;
|
||||
int? _touchedIndex;
|
||||
DateFormat tooltipFormat = DateFormat("MM.dd.");
|
||||
|
||||
List<Color> gradientColors = [
|
||||
appStyle.colors.grade5,
|
||||
appStyle.colors.grade4,
|
||||
appStyle.colors.grade3,
|
||||
appStyle.colors.grade2,
|
||||
appStyle.colors.grade1,
|
||||
];
|
||||
|
||||
late List<FlSpot> spots;
|
||||
late List<DateSpot> spots;
|
||||
|
||||
double? _subjectAverageInList(List<Grade> grades, String subjectUid) {
|
||||
double weightedSum = 0;
|
||||
@@ -75,25 +78,28 @@ class _GradeChartState extends State<GradeChart> {
|
||||
}
|
||||
|
||||
void _computeSpots() {
|
||||
final sortedGrades = List<Grade>.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)];
|
||||
spots.add(DateSpot(i.toDouble(), partialAvg, sortedGrades[i].recordDate));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,48 +134,57 @@ class _GradeChartState extends State<GradeChart> {
|
||||
|
||||
@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<GradeChart> {
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
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"),
|
||||
);
|
||||
}
|
||||
final currentValue = _tooltipActive && _tooltipY != null
|
||||
? _tooltipY!.round()
|
||||
: spots.last.y.round();
|
||||
final isActive = text == currentValue.toString();
|
||||
|
||||
Widget leftTitleWidgets(double value, TitleMeta meta) {
|
||||
if (value == 0) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
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<GradeChart> {
|
||||
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<GradeChart> {
|
||||
enabled: true,
|
||||
touchCallback: (FlTouchEvent event, LineTouchResponse? response) {
|
||||
setState(() {
|
||||
if (event is FlLongPressEnd ||
|
||||
event is FlPanEndEvent ||
|
||||
event is FlTapUpEvent) {
|
||||
_tooltipActive = false;
|
||||
_tooltipY = null;
|
||||
_touchedIndex = null;
|
||||
if (event.isInterestedForInteractions) {
|
||||
_touchedIndex = response!.lineBarSpots!.first.spotIndex;
|
||||
_tooltipY = spots[_touchedIndex!].y;
|
||||
if (_tooltipY! > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response?.lineBarSpots != null &&
|
||||
response!.lineBarSpots!.isNotEmpty) {
|
||||
final spot = response.lineBarSpots!.first;
|
||||
|
||||
_tooltipActive = true;
|
||||
_tooltipY = spot.y;
|
||||
_touchedIndex = spot.spotIndex;
|
||||
}
|
||||
_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<GradeChart> {
|
||||
}).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<GradeChart> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user