enrollment-app/lib/presentation/claims_overview/claims_overview_charts.dart
2026-06-11 09:21:16 +05:30

2077 lines
61 KiB
Dart

import 'dart:math' as math;
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart' show NumberFormat;
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'claims_overview_scope.dart';
import 'claims_overview_theme.dart';
const _chartAnimDuration = Duration(milliseconds: 900);
const _chartRiseDuration = Duration(milliseconds: 1200);
Duration _chartRiseDurationFor(BuildContext context) =>
ClaimsPdfExportScope.of(context) ? Duration.zero : _chartRiseDuration;
Duration _chartAnimDurationFor(BuildContext context) =>
ClaimsPdfExportScope.of(context) ? Duration.zero : _chartAnimDuration;
const _chartAnimCurve = Curves.easeOutCubic;
const _chartAxisColor = Color(0xFF9CA3AF);
const _axisLabelGap = 6.0;
const _axisTickLength = 8.0;
const _verticalLeftAxisWidth = 44.0;
/// Five Y-axis ticks from 0 → [maxY] (for dynamic-scale vertical charts).
List<double> _yTicksForMaxY(double maxY) {
if (maxY <= 0) return const [0.0, 5.0, 10.0, 15.0, 20.0];
return List<double>.generate(5, (i) => i == 4 ? maxY : (maxY / 4) * i);
}
String _formatLakhsAxisTick(double v) {
if (v == 0) return '0';
if (v >= 100) return '${v.round()}L';
return '${v.toStringAsFixed(1)}L';
}
/// Lakhs display for KPI tiles (e.g. incurred-by-status grid).
String formatClaimsLakhsDisplay(double lakhs) => _formatLakhsAxisTick(lakhs);
String _formatLakhsAmountTick(double amount) {
if (amount == 0) return '0';
final lakhs = amount / 100000;
if (lakhs >= 100) return '${lakhs.round()}L';
return '${lakhs.toStringAsFixed(1)}L';
}
/// Colors aligned with the Nhance reference design (top → bottom).
final incurredByStatusColors = [
ClaimsOverviewTheme.teal,
const Color(0xFFFFB74D),
ClaimsOverviewTheme.purple,
ClaimsOverviewTheme.green,
ClaimsOverviewTheme.orange,
ClaimsOverviewTheme.red,
ClaimsOverviewTheme.cyan,
];
/// Horizontal incurred-by-status chart with value labels at bar front (right end).
class ClaimsHorizontalIncurredBarChart extends StatefulWidget {
final List<String> labels;
final List<double> valuesLakhs;
final int animationKey;
final double maxLakhs;
ClaimsHorizontalIncurredBarChart({
super.key,
required this.labels,
required this.valuesLakhs,
required this.animationKey,
this.maxLakhs = 180,
});
@override
State<ClaimsHorizontalIncurredBarChart> createState() =>
_ClaimsHorizontalIncurredBarChartState();
}
class _ClaimsHorizontalIncurredBarChartState
extends State<ClaimsHorizontalIncurredBarChart> {
static const double _labelColumnWidth = 112;
static const double _bottomAxisHeight = 36;
static const double _xAxisRightInset = 32;
static const double _barThickness = 26;
double get _maxAmount => widget.maxLakhs * 100000;
List<double> get _xTicks {
final max = _maxAmount;
if (max <= 0) {
return const [0, 450000, 900000, 1350000, 1800000];
}
final step = max / 4;
return [0, step, step * 2, step * 3, max];
}
double _toAmount(double lakhs) => lakhs * 100000;
List<Widget> _barValueOverlays({
required Size plotSize,
required List<double> barValues,
required double animProgress,
}) {
final n = barValues.length;
if (n == 0) return const [];
final rowH = plotSize.height / n;
const labelH = 22.0;
return List.generate(n, (i) {
if (barValues[i] <= 0 || animProgress <= 0) {
return const SizedBox.shrink();
}
final centerY = i * rowH + rowH / 2;
final barW = _maxAmount > 0
? (barValues[i] / _maxAmount) * plotSize.width * animProgress
: 0.0;
if (barW <= 0) return const SizedBox.shrink();
const labelW = 52.0;
const edgePad = 6.0;
// Always outside the bar tip (front / right end), vertically centered.
final left = (barW + edgePad).clamp(4.0, plotSize.width - labelW - 4);
final top =
(centerY - labelH / 2).clamp(2.0, plotSize.height - labelH - 2);
return Positioned(
left: left,
top: top,
child: _BarValueLabel(value: _formatLakhsAmountTick(barValues[i])),
);
});
}
@override
Widget build(BuildContext context) {
final barValues = widget.valuesLakhs.map(_toAmount).toList();
final rowCount = widget.labels.length;
return LayoutBuilder(
builder: (context, constraints) {
return TweenAnimationBuilder<double>(
key: ValueKey(widget.animationKey),
tween: Tween(begin: 0, end: 1),
duration: _chartRiseDurationFor(context),
curve: Curves.easeOutCubic,
builder: (context, animProgress, _) {
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
width: _labelColumnWidth,
child: Column(
children: [
Expanded(
child: Column(
children: List.generate(rowCount, (i) {
return Expanded(
child: Align(
alignment: Alignment.centerRight,
child: _IncurredStatusLabel(
label: widget.labels[i],
axisColor: _chartAxisColor,
),
),
);
}),
),
),
const SizedBox(height: _bottomAxisHeight),
],
),
),
Expanded(
child: LayoutBuilder(
builder: (context, plotConstraints) {
final chartWidth =
plotConstraints.maxWidth - _xAxisRightInset;
final plotHeight =
plotConstraints.maxHeight - _bottomAxisHeight;
final plotSize = Size(chartWidth, plotHeight);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: plotHeight,
child: Padding(
padding: const EdgeInsets.only(
right: _xAxisRightInset,
),
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
CustomPaint(
size: plotSize,
painter: _IncurredHorizontalPlotPainter(
values: barValues,
maxValue: _maxAmount,
colors: incurredByStatusColors,
animProgress: animProgress,
barThickness: _barThickness,
axisColor: _chartAxisColor,
gridColor: ClaimsOverviewTheme.border,
),
),
..._barValueOverlays(
plotSize: plotSize,
barValues: barValues,
animProgress: animProgress,
),
],
),
),
),
_IncurredBottomAxis(
ticks: _xTicks,
maxValue: _maxAmount,
axisColor: _chartAxisColor,
rightInset: _xAxisRightInset,
),
],
);
},
),
),
],
);
},
);
},
);
}
}
/// Status label with tick mark pointing at the Y-axis line.
class _IncurredStatusLabel extends StatelessWidget {
final String label;
final Color axisColor;
const _IncurredStatusLabel({
required this.label,
required this.axisColor,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: Text(
label,
style: GoogleFonts.poppins(
fontSize: 10,
color: ClaimsOverviewTheme.textPrimary,
height: 1.2,
),
textAlign: TextAlign.right,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: _axisLabelGap),
Container(width: _axisTickLength, height: 1, color: axisColor),
],
);
}
}
class _IncurredBottomAxis extends StatelessWidget {
final List<double> ticks;
final double maxValue;
final Color axisColor;
final double rightInset;
const _IncurredBottomAxis({
required this.ticks,
required this.maxValue,
required this.axisColor,
required this.rightInset,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: _ClaimsHorizontalIncurredBarChartState._bottomAxisHeight,
child: LayoutBuilder(
builder: (context, constraints) {
final chartW = constraints.maxWidth - rightInset;
return Column(
children: [
Padding(
padding: EdgeInsets.only(right: rightInset),
child: Container(height: 1, color: axisColor),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(right: rightInset),
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
for (var i = 0; i < ticks.length; i++)
_tickLabel(
ticks[i],
chartW,
isFirst: i == 0,
isLast: i == ticks.length - 1,
),
],
),
),
),
],
);
},
),
);
}
Widget _tickLabel(
double t,
double chartW, {
required bool isFirst,
required bool isLast,
}) {
final label = Text(
_formatLakhsAmountTick(t),
style: GoogleFonts.poppins(
fontSize: 9,
color: ClaimsOverviewTheme.textSecondary,
),
);
final tick = Container(width: 1, height: 6, color: axisColor);
final column = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: isLast
? CrossAxisAlignment.end
: isFirst
? CrossAxisAlignment.start
: CrossAxisAlignment.center,
children: [
tick,
const SizedBox(height: 4),
label,
],
);
if (isFirst) {
return Positioned(left: 0, top: 0, child: column);
}
if (isLast) {
return Positioned(right: 0, top: 0, child: column);
}
final x = (t / maxValue) * chartW;
return Positioned(
left: x,
top: 0,
child: FractionalTranslation(
translation: const Offset(-0.5, 0),
child: column,
),
);
}
}
class _BarValueLabel extends StatelessWidget {
final String value;
const _BarValueLabel({required this.value});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: ClaimsOverviewTheme.border),
),
child: Text(
value,
style: GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w600,
color: ClaimsOverviewTheme.textPrimary,
),
),
);
}
}
class _IncurredHorizontalPlotPainter extends CustomPainter {
final List<double> values;
final double maxValue;
final List<Color> colors;
final double animProgress;
final double barThickness;
final Color axisColor;
final Color gridColor;
_IncurredHorizontalPlotPainter({
required this.values,
required this.maxValue,
required this.colors,
required this.animProgress,
required this.barThickness,
required this.axisColor,
required this.gridColor,
});
@override
void paint(Canvas canvas, Size size) {
final n = values.length;
if (n == 0) return;
final rowH = size.height / n;
final axisPaint = Paint()
..color = axisColor
..strokeWidth = 1;
// Dashed grid — vertical (value steps).
final xSteps = [0.0, 0.25, 0.5, 0.75, 1.0];
for (final f in xSteps) {
final x = size.width * f;
_drawDashedLine(
canvas,
Offset(x, 0),
Offset(x, size.height),
gridColor,
);
}
// Dashed grid — horizontal (between rows).
for (var i = 0; i <= n; i++) {
final y = i * rowH;
_drawDashedLine(
canvas,
Offset(0, y),
Offset(size.width, y),
gridColor,
);
}
// Solid L-shaped axes meeting at bottom-left origin.
canvas.drawLine(Offset.zero, Offset(0, size.height), axisPaint);
canvas.drawLine(
Offset(0, size.height),
Offset(size.width, size.height),
axisPaint,
);
for (var i = 0; i < n; i++) {
final rowTop = i * rowH;
final centerY = rowTop + rowH / 2;
final barTop = centerY - barThickness / 2;
final barBottom = centerY + barThickness / 2;
final barW = (values[i] / maxValue) * size.width * animProgress;
if (barW <= 0) continue;
final barRect = RRect.fromRectAndCorners(
Rect.fromLTRB(0, barTop, barW, barBottom),
topRight: const Radius.circular(8),
bottomRight: const Radius.circular(8),
);
canvas.drawRRect(
barRect,
Paint()..color = colors[i % colors.length],
);
}
}
@override
bool shouldRepaint(covariant _IncurredHorizontalPlotPainter old) {
return old.animProgress != animProgress || old.values != values;
}
}
void _drawDashedLine(Canvas canvas, Offset a, Offset b, Color color) {
const dash = 4.0;
const gap = 4.0;
final paint = Paint()
..color = color
..strokeWidth = 1;
final total = (b - a).distance;
if (total == 0) return;
final dir = (b - a) / total;
var dist = 0.0;
while (dist < total) {
final end = dist + dash;
canvas.drawLine(
a + dir * dist,
a + dir * end.clamp(0.0, total),
paint,
);
dist += dash + gap;
}
}
/// Shared vertical bar chart: aligned axes, labels inside card, rise animation.
class ClaimsCustomVerticalBarChart extends StatefulWidget {
final List<String> labels;
final List<double> values;
final double maxY;
final List<Color> colors;
final int animationKey;
final double barWidth;
final double barSlotFraction;
final List<double> yTicks;
final double leftAxisWidth;
final double bottomAxisHeight;
final double plotRightInset;
final String Function(double value)? formatYTick;
const ClaimsCustomVerticalBarChart({
super.key,
required this.labels,
required this.values,
required this.maxY,
required this.colors,
required this.animationKey,
required this.yTicks,
this.barWidth = 56,
this.barSlotFraction = 0.72,
this.leftAxisWidth = _verticalLeftAxisWidth,
this.bottomAxisHeight = 52,
this.plotRightInset = 0,
this.formatYTick,
});
@override
State<ClaimsCustomVerticalBarChart> createState() =>
_ClaimsCustomVerticalBarChartState();
}
class _ClaimsCustomVerticalBarChartState
extends State<ClaimsCustomVerticalBarChart> {
String _formatTick(double v) =>
widget.formatYTick?.call(v) ?? v.toInt().toString();
String _formatBarValue(double v) {
if (widget.formatYTick != null) {
return widget.formatYTick!(v);
}
return v.round().toString();
}
List<Widget> _barValueOverlays({
required Size plotSize,
required double animProgress,
}) {
final n = widget.values.length;
if (n == 0 || widget.maxY <= 0) return const [];
final chartW = plotSize.width - widget.plotRightInset;
final slotW = chartW / n;
const labelH = 22.0;
return List.generate(n, (i) {
if (widget.values[i] <= 0 || animProgress <= 0) {
return const SizedBox.shrink();
}
final centerX = (i + 0.5) * slotW;
final barH =
(widget.values[i] / widget.maxY) * plotSize.height * animProgress;
final barTop = plotSize.height - barH;
return Positioned(
left: (centerX - 38).clamp(2.0, chartW - 74),
width: 76,
top: (barTop - labelH - 4).clamp(2.0, plotSize.height - labelH),
child: Center(
child: _BarValueLabel(value: _formatBarValue(widget.values[i])),
),
);
});
}
Widget _buildChartBody(double animProgress) {
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_VerticalLeftAxis(
yTicks: widget.yTicks,
maxY: widget.maxY,
axisWidth: widget.leftAxisWidth,
bottomSpacer: widget.bottomAxisHeight,
axisColor: _chartAxisColor,
formatTick: _formatTick,
),
Expanded(
child: LayoutBuilder(
builder: (context, plotConstraints) {
final plotHeight =
plotConstraints.maxHeight - widget.bottomAxisHeight;
final plotSize = Size(
plotConstraints.maxWidth,
plotHeight,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: plotHeight,
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
CustomPaint(
size: plotSize,
painter: _VerticalBarPlotPainter(
values: widget.values,
maxY: widget.maxY,
yTicks: widget.yTicks,
colors: widget.colors,
animProgress: animProgress,
barWidth: widget.barWidth,
barSlotFraction: widget.barSlotFraction,
plotRightInset: widget.plotRightInset,
axisColor: _chartAxisColor,
gridColor: ClaimsOverviewTheme.border,
),
),
..._barValueOverlays(
plotSize: plotSize,
animProgress: animProgress,
),
],
),
),
_VerticalCategoryAxis(
labels: widget.labels,
height: widget.bottomAxisHeight,
axisColor: _chartAxisColor,
rightInset: widget.plotRightInset,
),
],
);
},
),
),
],
);
}
@override
Widget build(BuildContext context) {
if (ClaimsPdfExportScope.of(context)) {
return LayoutBuilder(
builder: (context, constraints) => _buildChartBody(1.0),
);
}
return LayoutBuilder(
builder: (context, constraints) {
return TweenAnimationBuilder<double>(
key: ValueKey(widget.animationKey),
tween: Tween(begin: 0, end: 1),
duration: _chartRiseDurationFor(context),
curve: Curves.easeOutCubic,
builder: (context, animProgress, _) => _buildChartBody(animProgress),
);
},
);
}
}
/// Y-axis value with gap + tick (matches horizontal status labels).
class _VerticalYTickLabel extends StatelessWidget {
final String text;
final Color axisColor;
const _VerticalYTickLabel({
required this.text,
required this.axisColor,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
text,
style: GoogleFonts.poppins(
fontSize: 10,
color: ClaimsOverviewTheme.textSecondary,
height: 1.2,
),
textAlign: TextAlign.right,
),
const SizedBox(width: _axisLabelGap),
Container(width: _axisTickLength, height: 1, color: axisColor),
],
);
}
}
class _VerticalLeftAxis extends StatelessWidget {
final List<double> yTicks;
final double maxY;
final double axisWidth;
final double bottomSpacer;
final Color axisColor;
final String Function(double) formatTick;
const _VerticalLeftAxis({
required this.yTicks,
required this.maxY,
required this.axisWidth,
required this.bottomSpacer,
required this.axisColor,
required this.formatTick,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: axisWidth,
child: Column(
children: [
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final h = constraints.maxHeight;
return Stack(
clipBehavior: Clip.hardEdge,
children: [
for (final t in yTicks)
Positioned(
right: 0,
bottom: (t / maxY) * h,
child: FractionalTranslation(
translation: const Offset(0, -0.5),
child: _VerticalYTickLabel(
text: formatTick(t),
axisColor: axisColor,
),
),
),
],
);
},
),
),
SizedBox(height: bottomSpacer),
],
),
);
}
}
class _VerticalCategoryAxis extends StatelessWidget {
final List<String> labels;
final double height;
final Color axisColor;
final double rightInset;
const _VerticalCategoryAxis({
required this.labels,
required this.height,
required this.axisColor,
this.rightInset = 0,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.only(right: rightInset),
child: Container(height: 1, color: axisColor),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(right: rightInset),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: labels.map((label) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 1, height: 6, color: axisColor),
const SizedBox(height: 4),
Text(
label,
style: GoogleFonts.poppins(
fontSize: 10,
color: ClaimsOverviewTheme.textPrimary,
height: 1.2,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}).toList(),
),
),
),
],
),
);
}
}
class _VerticalBarPlotPainter extends CustomPainter {
final List<double> values;
final double maxY;
final List<double> yTicks;
final List<Color> colors;
final double animProgress;
final double barWidth;
final double barSlotFraction;
final double plotRightInset;
final Color axisColor;
final Color gridColor;
_VerticalBarPlotPainter({
required this.values,
required this.maxY,
required this.yTicks,
required this.colors,
required this.animProgress,
required this.barWidth,
required this.barSlotFraction,
required this.plotRightInset,
required this.axisColor,
required this.gridColor,
});
@override
void paint(Canvas canvas, Size size) {
final n = values.length;
if (n == 0) return;
const chartLeft = 0.0;
final chartW = size.width - plotRightInset;
final slotW = chartW / n;
final axisPaint = Paint()
..color = axisColor
..strokeWidth = 1;
for (final t in yTicks) {
final y = size.height - (t / maxY) * size.height;
_drawDashedLine(
canvas,
Offset(chartLeft, y),
Offset(chartLeft + chartW, y),
gridColor,
);
}
for (var i = 0; i <= n; i++) {
final x = chartLeft + i * slotW;
_drawDashedLine(
canvas,
Offset(x, 0),
Offset(x, size.height),
gridColor,
);
}
canvas.drawLine(
Offset(chartLeft, 0),
Offset(chartLeft, size.height),
axisPaint,
);
canvas.drawLine(
Offset(chartLeft, size.height),
Offset(chartLeft + chartW, size.height),
axisPaint,
);
final effectiveBarW = barWidth.clamp(8.0, slotW * barSlotFraction);
for (var i = 0; i < n; i++) {
final centerX = chartLeft + (i + 0.5) * slotW;
final left = centerX - effectiveBarW / 2;
final barH = (values[i] / maxY) * size.height * animProgress;
if (barH <= 0) continue;
final barTop = size.height - barH;
final barRect = RRect.fromRectAndCorners(
Rect.fromLTRB(left, barTop, left + effectiveBarW, size.height),
topLeft: const Radius.circular(8),
topRight: const Radius.circular(8),
);
canvas.drawRRect(
barRect,
Paint()..color = colors[i % colors.length],
);
}
}
@override
bool shouldRepaint(covariant _VerticalBarPlotPainter old) {
return old.animProgress != animProgress || old.values != values;
}
}
/// Colors for Policy Experience — Total Claims Count by Status.
final claimsCountByStatusColors = [
ClaimsOverviewTheme.teal,
ClaimsOverviewTheme.orange,
const Color(0xFF4169E1),
ClaimsOverviewTheme.green,
ClaimsOverviewTheme.yellow,
ClaimsOverviewTheme.red,
ClaimsOverviewTheme.cyan,
];
/// Vertical claims-count chart: wide bars, value labels on top, rise animation.
class ClaimsCountByStatusBarChart extends StatefulWidget {
final List<String> labels;
final List<double> values;
final int animationKey;
final double maxY;
ClaimsCountByStatusBarChart({
super.key,
required this.labels,
required this.values,
required this.animationKey,
this.maxY = 220,
});
@override
State<ClaimsCountByStatusBarChart> createState() =>
_ClaimsCountByStatusBarChartState();
}
class _ClaimsCountByStatusBarChartState extends State<ClaimsCountByStatusBarChart> {
static const _yTicks = [0.0, 55.0, 110.0, 165.0, 220.0];
@override
Widget build(BuildContext context) {
return ClaimsCustomVerticalBarChart(
labels: widget.labels,
values: widget.values,
maxY: widget.maxY,
colors: claimsCountByStatusColors,
animationKey: widget.animationKey,
yTicks: _yTicks,
barWidth: 56,
bottomAxisHeight: 52,
);
}
}
/// Colors for Claims Analysis — Total Counts by Claim Type.
final claimTypeCountColors = [
ClaimsOverviewTheme.teal,
ClaimsOverviewTheme.orange,
ClaimsOverviewTheme.purple,
];
/// Vertical claim-type count chart with L-border axes and wide rounded bars.
class ClaimsCountByClaimTypeChart extends StatefulWidget {
final List<String> labels;
final List<double> values;
final int animationKey;
final double maxY;
ClaimsCountByClaimTypeChart({
super.key,
required this.labels,
required this.values,
required this.animationKey,
this.maxY = 260,
});
@override
State<ClaimsCountByClaimTypeChart> createState() =>
_ClaimsCountByClaimTypeChartState();
}
class _ClaimsCountByClaimTypeChartState
extends State<ClaimsCountByClaimTypeChart> {
@override
Widget build(BuildContext context) {
final maxY = widget.maxY > 0 ? widget.maxY.toDouble() : 25.0;
return ClaimsCustomVerticalBarChart(
labels: widget.labels,
values: widget.values,
maxY: maxY,
colors: claimTypeCountColors,
animationKey: widget.animationKey,
yTicks: _yTicksForMaxY(maxY),
formatYTick: _formatLakhsAxisTick,
leftAxisWidth: 48,
barWidth: 56,
bottomAxisHeight: 52,
);
}
}
class ClaimsVerticalBarChart extends StatelessWidget {
final List<String> labels;
final List<double> values;
final double maxY;
final List<Color>? colors;
final int replayToken;
const ClaimsVerticalBarChart({
super.key,
required this.labels,
required this.values,
required this.maxY,
this.colors,
this.replayToken = 0,
});
static const _ageBandYTicks = [
0.0,
4000000.0,
8000000.0,
12000000.0,
16000000.0,
];
@override
Widget build(BuildContext context) {
final palette = colors ?? ClaimsOverviewTheme.chartPalette;
return ClaimsCustomVerticalBarChart(
labels: labels,
values: values,
maxY: maxY,
colors: palette,
animationKey: replayToken,
yTicks: _ageBandYTicks,
barWidth: 48,
barSlotFraction: 0.88,
leftAxisWidth: 56,
bottomAxisHeight: 48,
plotRightInset: 8,
formatYTick: (v) => v.toInt().toString(),
);
}
}
/// Donut chart with external indicator lines, segment dots, sweep animation, legend.
class ClaimsDonutChart extends StatefulWidget {
final List<double> values;
final List<String>? labels;
final List<Color>? colors;
final bool showLegend;
final double size;
final int replayToken;
final int? animationKey;
const ClaimsDonutChart({
super.key,
required this.values,
this.labels,
this.colors,
this.showLegend = false,
this.size = 200,
this.replayToken = 0,
this.animationKey,
});
@override
State<ClaimsDonutChart> createState() => _ClaimsDonutChartState();
}
class _ClaimsDonutChartState extends State<ClaimsDonutChart> {
int _hoveredIndex = -1;
int get _animKey => widget.animationKey ?? widget.replayToken;
List<Color> get _palette {
if (widget.colors != null && widget.colors!.length >= widget.values.length) {
return widget.colors!;
}
return List.generate(
widget.values.length,
(i) => ClaimsOverviewTheme.chartPalette[i % ClaimsOverviewTheme.chartPalette.length],
);
}
int? _hitSegment(Offset local, Size size) {
if (widget.values.isEmpty) return null;
final center = Offset(size.width / 2, size.height / 2);
final outerR = math.min(size.width, size.height) / 2 * 0.78;
final innerR = outerR * 0.58;
final dx = local.dx - center.dx;
final dy = local.dy - center.dy;
final dist = math.sqrt(dx * dx + dy * dy);
if (dist < innerR || dist > outerR) return null;
var angle = math.atan2(dy, dx) - _ClaimsDonutPainter._startAngle;
while (angle < 0) {
angle += 2 * math.pi;
}
while (angle >= 2 * math.pi) {
angle -= 2 * math.pi;
}
final total = widget.values.fold<double>(0, (a, b) => a + b);
if (total <= 0) return null;
var cumulative = 0.0;
for (var i = 0; i < widget.values.length; i++) {
final sweep = (widget.values[i] / total) * 2 * math.pi;
if (angle >= cumulative && angle < cumulative + sweep) return i;
cumulative += sweep;
}
return widget.values.length - 1;
}
String _formatDonutValue(double v) {
final total = widget.values.fold<double>(0, (a, b) => a + b);
if (total > 0 && total <= 100.5) {
return '${v.toStringAsFixed(v == v.roundToDouble() ? 0 : 2)}%';
}
if (v == v.roundToDouble()) {
return NumberFormat('#,##0').format(v.round());
}
return NumberFormat('#,##0.##').format(v);
}
Widget _buildDonutColumn(BuildContext context, double animProgress) {
final showLegend =
widget.showLegend && widget.labels != null && widget.labels!.isNotEmpty;
final isPdf = ClaimsPdfExportScope.of(context);
return Column(
children: [
Expanded(
child: Center(
child: AspectRatio(
aspectRatio: 1,
child: LayoutBuilder(
builder: (context, constraints) {
final side = math.min(
constraints.maxWidth,
constraints.maxHeight,
);
final chartSize = Size(side, side);
final chart = CustomPaint(
size: chartSize,
painter: _ClaimsDonutPainter(
values: widget.values,
colors: _palette,
animProgress: animProgress,
hoveredIndex: isPdf ? -1 : _hoveredIndex,
compactIndicators: isPdf,
),
);
if (isPdf) {
return ClipRect(child: chart);
}
return MouseRegion(
onHover: (e) {
final hit = _hitSegment(e.localPosition, chartSize);
final next = hit ?? -1;
if (next != _hoveredIndex) {
setState(() => _hoveredIndex = next);
}
},
onExit: (_) {
if (_hoveredIndex != -1) {
setState(() => _hoveredIndex = -1);
}
},
cursor: SystemMouseCursors.click,
child: Stack(
clipBehavior: Clip.none,
children: [
chart,
if (_hoveredIndex >= 0)
Positioned.fill(
child: IgnorePointer(
child: Center(
child: _DonutHoverTooltip(
title: widget.labels != null &&
_hoveredIndex <
widget.labels!.length
? widget.labels![_hoveredIndex]
: 'Segment ${_hoveredIndex + 1}',
value: _formatDonutValue(
widget.values[_hoveredIndex],
),
color: _palette[_hoveredIndex],
),
),
),
),
],
),
);
},
),
),
),
),
if (showLegend) ...[
const SizedBox(height: 8),
_DonutLegend(
labels: widget.labels!,
colors: _palette,
),
],
],
);
}
@override
Widget build(BuildContext context) {
if (ClaimsPdfExportScope.of(context)) {
return _buildDonutColumn(context, 1.0);
}
return TweenAnimationBuilder<double>(
key: ValueKey('donut-$_animKey-${widget.values.join()}'),
tween: Tween(begin: 0, end: 1),
duration: _chartRiseDurationFor(context),
curve: Curves.easeOutCubic,
builder: (context, animProgress, _) =>
_buildDonutColumn(context, animProgress),
);
}
}
class _DonutHoverTooltip extends StatelessWidget {
final String title;
final String value;
final Color color;
const _DonutHoverTooltip({
required this.title,
required this.value,
required this.color,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Container(
constraints: const BoxConstraints(minWidth: 88),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: ClaimsOverviewTheme.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 12,
offset: const Offset(0, 3),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: color,
),
),
const SizedBox(height: 4),
Text(
value,
style: GoogleFonts.poppins(
fontSize: 22,
fontWeight: FontWeight.w700,
color: ClaimsOverviewTheme.textPrimary,
height: 1.1,
),
),
],
),
),
);
}
}
class _DonutLegend extends StatelessWidget {
final List<String> labels;
final List<Color> colors;
const _DonutLegend({required this.labels, required this.colors});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 20,
runSpacing: 8,
alignment: WrapAlignment.center,
children: List.generate(labels.length, (i) {
final color = colors[i % colors.length];
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(
labels[i],
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: color,
),
),
],
);
}),
);
}
}
class _ClaimsDonutPainter extends CustomPainter {
final List<double> values;
final List<Color> colors;
final double animProgress;
final int hoveredIndex;
final bool compactIndicators;
_ClaimsDonutPainter({
required this.values,
required this.colors,
required this.animProgress,
this.hoveredIndex = -1,
this.compactIndicators = false,
});
static const double _sectionGapRad = 0.04;
static const double _startAngle = -math.pi / 2;
@override
void paint(Canvas canvas, Size size) {
if (values.isEmpty) return;
final center = Offset(size.width / 2, size.height / 2);
final outerR = math.min(size.width, size.height) / 2 * 0.78;
final innerR = outerR * 0.58;
final ringWidth = outerR - innerR;
final midR = (outerR + innerR) / 2;
final total = values.fold<double>(0, (a, b) => a + b);
if (total <= 0) return;
var angle = _startAngle;
for (var i = 0; i < values.length; i++) {
final fraction = values[i] / total;
final fullSweep = fraction * 2 * math.pi;
final gap = values.length > 1 ? _sectionGapRad : 0.0;
final drawableSweep = math.max(fullSweep - gap, 0.0);
final animatedSweep = drawableSweep * animProgress;
final sectionStart = angle + gap / 2;
if (animatedSweep > 0) {
final baseColor = colors[i % colors.length];
final isHovered = i == hoveredIndex;
final ringPaint = Paint()
..color = isHovered
? baseColor
: baseColor.withValues(alpha: hoveredIndex >= 0 ? 0.45 : 1.0)
..style = PaintingStyle.stroke
..strokeWidth = isHovered ? ringWidth + 4 : ringWidth
..strokeCap = StrokeCap.butt;
canvas.drawArc(
Rect.fromCircle(center: center, radius: midR),
sectionStart,
animatedSweep,
false,
ringPaint,
);
}
// Indicators + labels use full segment mid-angle (stable once drawn).
if (animProgress > 0.8) {
final midAngle = sectionStart + drawableSweep / 2;
_drawIndicator(
canvas,
center: center,
midAngle: midAngle,
outerR: outerR,
value: values[i],
color: colors[i % colors.length],
segmentCount: values.length,
opacity: ((animProgress - 0.8) / 0.2).clamp(0.0, 1.0),
);
}
angle += fullSweep;
}
// Center hole (white donut core).
canvas.drawCircle(
center,
innerR - 1,
Paint()..color = Colors.white,
);
}
void _drawIndicator(
Canvas canvas, {
required Offset center,
required double midAngle,
required double outerR,
required double value,
required Color color,
required int segmentCount,
required double opacity,
}) {
if (opacity <= 0) return;
final c = color.withValues(alpha: opacity);
final fontSize = compactIndicators
? (segmentCount <= 2 ? 12.0 : 10.0)
: segmentCount <= 2
? 18.0
: segmentCount <= 4
? 15.0
: 12.0;
final leaderLen = compactIndicators
? 6.0
: segmentCount <= 4
? 14.0
: 10.0;
final unitX = math.cos(midAngle);
final unitY = math.sin(midAngle);
final arcEdge = Offset(
center.dx + outerR * unitX,
center.dy + outerR * unitY,
);
final label = _formatValue(value);
final tp = TextPainter(
text: TextSpan(
text: label,
style: GoogleFonts.poppins(
fontSize: fontSize,
fontWeight: FontWeight.w700,
color: c,
),
),
textDirection: TextDirection.ltr,
)..layout();
// Place value right after the tick (minimal 2px padding).
const labelPadding = 2.0;
final textCenterDist =
outerR + leaderLen + labelPadding + _textExtentAlongRay(tp, midAngle);
final lineEnd = Offset(
center.dx + (outerR + leaderLen) * unitX,
center.dy + (outerR + leaderLen) * unitY,
);
final labelCenter = Offset(
center.dx + textCenterDist * unitX,
center.dy + textCenterDist * unitY,
);
canvas.drawLine(
arcEdge,
lineEnd,
Paint()
..color = c
..strokeWidth = 1.2,
);
tp.paint(
canvas,
labelCenter - Offset(tp.width / 2, tp.height / 2),
);
}
String _formatValue(double v) {
if (v == v.roundToDouble()) {
return NumberFormat('#,##0').format(v.round());
}
return NumberFormat('#,##0.#').format(v);
}
/// Half of the text bbox projected along the indicator ray.
double _textExtentAlongRay(TextPainter tp, double angle) {
final c = math.cos(angle).abs();
final s = math.sin(angle).abs();
return (tp.width * c + tp.height * s) / 2;
}
@override
bool shouldRepaint(covariant _ClaimsDonutPainter old) {
return old.animProgress != animProgress ||
old.values != values ||
old.hoveredIndex != hoveredIndex ||
old.colors != colors ||
old.compactIndicators != compactIndicators;
}
}
/// Area / line chart — L-axes, ticks, dashed grid, hover crosshair, draw animation.
class ClaimsAreaChart extends StatefulWidget {
final List<String> labels;
final List<double> values;
final double maxY;
final List<double>? yTicks;
final int replayToken;
final Color lineColor;
final String Function(double value)? formatTooltipValue;
const ClaimsAreaChart({
super.key,
required this.labels,
required this.values,
required this.maxY,
this.yTicks,
this.replayToken = 0,
this.lineColor = ClaimsOverviewTheme.teal,
this.formatTooltipValue,
});
@override
State<ClaimsAreaChart> createState() => _ClaimsAreaChartState();
}
class _ClaimsAreaChartState extends State<ClaimsAreaChart> {
static const double _bottomAxisHeight = 48;
static const double _plotRightInset = 8;
int _hoveredIndex = -1;
Offset? _tooltipAnchor;
List<double> get _yTicks =>
widget.yTicks ?? [0, widget.maxY * 0.25, widget.maxY * 0.5, widget.maxY * 0.75, widget.maxY];
void _onPlotHover(Offset local, Size plotSize, int count) {
if (count == 0) return;
final chartW = plotSize.width;
final slotW = chartW / count;
final index = (local.dx / slotW).floor().clamp(0, count - 1);
if (index == _hoveredIndex) return;
final value = widget.values[index];
final pointY = widget.maxY > 0
? plotSize.height * (1 - value / widget.maxY)
: plotSize.height;
setState(() {
_hoveredIndex = index;
_tooltipAnchor = Offset((index + 0.5) * slotW, pointY);
});
}
@override
Widget build(BuildContext context) {
final count = widget.labels.length;
return TweenAnimationBuilder<double>(
key: ValueKey('area-${widget.replayToken}-${widget.values.join()}'),
tween: Tween(begin: 0, end: 1),
duration: _chartRiseDurationFor(context),
curve: Curves.easeOutCubic,
builder: (context, animProgress, _) {
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_VerticalLeftAxis(
yTicks: _yTicks,
maxY: widget.maxY,
axisWidth: _verticalLeftAxisWidth,
bottomSpacer: _bottomAxisHeight,
axisColor: _chartAxisColor,
formatTick: (v) => v == v.roundToDouble()
? v.round().toString()
: v.toStringAsFixed(1),
),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final plotHeight =
constraints.maxHeight - _bottomAxisHeight;
final plotSize = Size(
constraints.maxWidth - _plotRightInset,
plotHeight,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: plotHeight,
child: Padding(
padding: const EdgeInsets.only(right: _plotRightInset),
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
MouseRegion(
onHover: (e) => _onPlotHover(
e.localPosition,
plotSize,
count,
),
onExit: (_) => setState(() {
_hoveredIndex = -1;
_tooltipAnchor = null;
}),
cursor: SystemMouseCursors.click,
child: CustomPaint(
size: plotSize,
painter: _AreaLinePlotPainter(
values: widget.values,
maxY: widget.maxY,
yTicks: _yTicks,
animProgress: animProgress,
hoveredIndex: _hoveredIndex,
lineColor: widget.lineColor,
axisColor: _chartAxisColor,
gridColor: ClaimsOverviewTheme.border,
plotBorderColor: ClaimsOverviewTheme.border,
),
),
),
if (_hoveredIndex >= 0 &&
_tooltipAnchor != null)
Positioned(
left: (_tooltipAnchor!.dx - 38)
.clamp(4.0, plotSize.width - 76),
top: (_tooltipAnchor!.dy - 26)
.clamp(4.0, plotSize.height - 24),
child: _BarValueLabel(
value: (widget.formatTooltipValue ??
_formatLakhsAxisTick)(
widget.values[_hoveredIndex],
),
),
),
],
),
),
),
_VerticalCategoryAxis(
labels: widget.labels,
height: _bottomAxisHeight,
axisColor: _chartAxisColor,
rightInset: _plotRightInset,
),
],
);
},
),
),
],
);
},
);
}
}
class _AreaLinePlotPainter extends CustomPainter {
final List<double> values;
final double maxY;
final List<double> yTicks;
final double animProgress;
final int hoveredIndex;
final Color lineColor;
final Color axisColor;
final Color gridColor;
final Color plotBorderColor;
_AreaLinePlotPainter({
required this.values,
required this.maxY,
required this.yTicks,
required this.animProgress,
required this.hoveredIndex,
required this.lineColor,
required this.axisColor,
required this.gridColor,
required this.plotBorderColor,
});
List<Offset> _chartPoints(Size size) {
final n = values.length;
if (n == 0) return [];
final chartW = size.width;
final slotW = chartW / n;
return List.generate(n, (i) {
final x = (i + 0.5) * slotW;
final y = size.height - (values[i] / maxY) * size.height;
return Offset(x, y);
});
}
Path _smoothLinePath(List<Offset> points) {
final path = Path();
if (points.isEmpty) return path;
if (points.length == 1) {
path.moveTo(points[0].dx, points[0].dy);
return path;
}
path.moveTo(points[0].dx, points[0].dy);
for (var i = 0; i < points.length - 1; i++) {
final current = points[i];
final next = points[i + 1];
final controlX = (current.dx + next.dx) / 2;
path.cubicTo(
controlX,
current.dy,
controlX,
next.dy,
next.dx,
next.dy,
);
}
return path;
}
Path _animatedPath(Path fullPath, double progress) {
if (progress >= 1) return fullPath;
final metrics = fullPath.computeMetrics();
final out = Path();
for (final metric in metrics) {
out.addPath(metric.extractPath(0, metric.length * progress), Offset.zero);
}
return out;
}
Path _fillPath(Path linePath, Size size) {
final fill = Path.from(linePath);
final bounds = linePath.getBounds();
fill.lineTo(bounds.right, size.height);
fill.lineTo(bounds.left, size.height);
fill.close();
return fill;
}
@override
void paint(Canvas canvas, Size size) {
if (values.isEmpty) return;
final plotRect = Rect.fromLTWH(0, 0, size.width, size.height);
final axisPaint = Paint()
..color = axisColor
..strokeWidth = 1;
// Light inner plot border.
canvas.drawRect(
plotRect,
Paint()
..color = plotBorderColor.withValues(alpha: 0.65)
..style = PaintingStyle.stroke
..strokeWidth = 1,
);
final n = values.length;
final slotW = size.width / n;
for (final t in yTicks) {
final y = size.height - (t / maxY) * size.height;
_drawDashedLine(canvas, Offset(0, y), Offset(size.width, y), gridColor);
}
for (var i = 0; i <= n; i++) {
final x = i * slotW;
_drawDashedLine(canvas, Offset(x, 0), Offset(x, size.height), gridColor);
}
canvas.drawLine(Offset.zero, Offset(0, size.height), axisPaint);
canvas.drawLine(
Offset(0, size.height),
Offset(size.width, size.height),
axisPaint,
);
final points = _chartPoints(size);
final linePath = _smoothLinePath(points);
final animatedLine = _animatedPath(linePath, animProgress);
final fillPath = _fillPath(animatedLine, size);
final fillPaint = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
lineColor.withValues(alpha: 0.35),
lineColor.withValues(alpha: 0.02),
],
).createShader(plotRect);
canvas.drawPath(fillPath, fillPaint);
canvas.drawPath(
animatedLine,
Paint()
..color = lineColor
..style = PaintingStyle.stroke
..strokeWidth = 3
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round,
);
if (hoveredIndex >= 0 && hoveredIndex < points.length) {
final pt = points[hoveredIndex];
canvas.drawLine(
Offset(pt.dx, size.height),
Offset(pt.dx, pt.dy),
Paint()
..color = const Color(0xFFD1D5DB)
..strokeWidth = 1,
);
canvas.drawCircle(
pt,
5,
Paint()..color = Colors.white,
);
canvas.drawCircle(
pt,
5,
Paint()
..color = lineColor
..style = PaintingStyle.stroke
..strokeWidth = 2,
);
}
}
@override
bool shouldRepaint(covariant _AreaLinePlotPainter old) {
return old.animProgress != animProgress ||
old.hoveredIndex != hoveredIndex ||
old.values != values;
}
}
class ClaimsScatterChart extends StatelessWidget {
final List<double>? values;
final List<String>? labels;
final double? maxY;
final double? pointY;
final String? label;
final int replayToken;
const ClaimsScatterChart({
super.key,
this.values,
this.labels,
this.maxY,
this.pointY,
this.label,
this.replayToken = 0,
});
bool get _isMulti =>
values != null && labels != null && values!.isNotEmpty && labels!.isNotEmpty;
@override
Widget build(BuildContext context) {
if (_isMulti) {
return _buildMulti(context);
}
return _buildSingle(context);
}
Widget _buildMulti(BuildContext context) {
final vals = values!;
final labs = labels!;
final chartMaxY = maxY ?? (vals.reduce((a, b) => a > b ? a : b) * 1.1);
final yInterval = chartMaxY > 0 ? chartMaxY / 4 : 1.0;
final lastIndex = math.max(0, vals.length - 1);
final maxX = math.max(1.0, lastIndex.toDouble());
const xEdgePadding = 0.5;
return Padding(
padding: const EdgeInsets.only(right: 6),
child: LineChart(
duration: _chartAnimDurationFor(context),
curve: _chartAnimCurve,
key: ValueKey('scatter-multi-$replayToken-${vals.join(",")}'),
LineChartData(
clipData: const FlClipData.none(),
maxY: chartMaxY,
minY: 0,
minX: -xEdgePadding,
maxX: maxX + xEdgePadding,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (v) => FlLine(
color: ClaimsOverviewTheme.border,
strokeWidth: 1,
dashArray: [4, 4],
),
),
borderData: FlBorderData(
show: true,
border: const Border(
left: BorderSide(color: _chartAxisColor, width: 1),
bottom: BorderSide(color: _chartAxisColor, width: 1),
),
),
titlesData: FlTitlesData(
topTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false, reservedSize: 8),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: yInterval,
getTitlesWidget: (v, _) => Text(
v >= 1 ? v.toStringAsFixed(1) : v.toStringAsFixed(2),
style: GoogleFonts.poppins(fontSize: 10, color: Colors.grey),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 48,
interval: 1,
getTitlesWidget: (v, meta) {
final i = v.round();
if (i < 0 || i >= labs.length) {
return const SizedBox.shrink();
}
return SideTitleWidget(
meta: meta,
space: 6,
fitInside: SideTitleFitInsideData.fromTitleMeta(meta),
child: Text(
labs[i],
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.visible,
style: GoogleFonts.poppins(
fontSize: 9,
color: Colors.black54,
),
),
);
},
),
),
),
lineBarsData: [
LineChartBarData(
spots: [
for (var i = 0; i < vals.length; i++)
FlSpot(i.toDouble(), vals[i]),
],
color: ClaimsOverviewTheme.orange,
barWidth: 0,
dotData: FlDotData(
show: true,
getDotPainter: (spot, _, __, ___) => FlDotCirclePainter(
radius: 6,
color: ClaimsOverviewTheme.orange,
strokeWidth: 2,
strokeColor: Colors.white,
),
),
belowBarData: BarAreaData(show: false),
),
],
),
),
);
}
Widget _buildSingle(BuildContext context) {
final y = pointY ?? 0;
final bottomLabel = label ?? '';
return LineChart(
duration: _chartAnimDurationFor(context),
curve: _chartAnimCurve,
key: ValueKey('scatter-$replayToken-$y'),
LineChartData(
maxY: 600,
minY: 0,
minX: 0,
maxX: 2,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (v) => FlLine(
color: ClaimsOverviewTheme.border,
strokeWidth: 1,
dashArray: [4, 4],
),
),
borderData: FlBorderData(
show: true,
border: const Border(
left: BorderSide(color: _chartAxisColor, width: 1),
bottom: BorderSide(color: _chartAxisColor, width: 1),
),
),
titlesData: FlTitlesData(
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 36,
interval: 150,
getTitlesWidget: (v, _) => Text(
v.toInt().toString(),
style: GoogleFonts.poppins(fontSize: 10, color: Colors.grey),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
reservedSize: 36,
getTitlesWidget: (v, __) {
if (v.round() != 1) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
bottomLabel,
style: GoogleFonts.poppins(fontSize: 10, color: Colors.black54),
),
);
},
),
),
),
lineBarsData: [
LineChartBarData(
spots: [FlSpot(1, y)],
color: ClaimsOverviewTheme.orange,
barWidth: 0,
dotData: FlDotData(
show: true,
getDotPainter: (spot, _, __, ___) => FlDotCirclePainter(
radius: 8,
color: ClaimsOverviewTheme.orange,
strokeWidth: 2,
strokeColor: Colors.white,
),
),
belowBarData: BarAreaData(show: false),
),
],
extraLinesData: ExtraLinesData(
verticalLines: [
VerticalLine(
x: 1,
color: ClaimsOverviewTheme.orange.withValues(alpha: 0.4),
dashArray: [4, 4],
),
],
),
),
);
}
}