import 'dart:math'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; import 'package:charts_flutter/flutter.dart' as charts; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; typedef DatumKeyFormatter = String Function(AvesDonutDatum d); typedef DatumValueFormatter = String Function(int d); typedef DatumColorizer = Color Function(BuildContext context, AvesDonutDatum d); typedef DatumCallback = void Function(AvesDonutDatum d); class AvesDonut extends StatefulWidget { final Widget title; final Map byTypes; final Duration animationDuration; final DatumKeyFormatter formatKey; final DatumValueFormatter formatValue; final DatumColorizer colorize; final DatumCallback? onTap; const AvesDonut({ super.key, required this.title, required this.byTypes, required this.animationDuration, required this.formatKey, required this.formatValue, required this.colorize, this.onTap, }); @override State createState() => _AvesDonutState(); } class _AvesDonutState extends State with AutomaticKeepAliveClientMixin { Map get byTypes => widget.byTypes; DatumKeyFormatter get formatKey => widget.formatKey; DatumValueFormatter get formatValue => widget.formatValue; DatumColorizer get colorize => widget.colorize; static const avesDonutMinWidth = 124.0; @override Widget build(BuildContext context) { super.build(context); if (byTypes.isEmpty) return const SizedBox(); final sum = byTypes.values.sum; final seriesData = byTypes.entries.map((kv) { final type = kv.key; return AvesDonutDatum( key: type, value: kv.value, ); }).toList(); seriesData.sort((d1, d2) { final c = d2.value.compareTo(d1.value); return c != 0 ? c : compareAsciiUpperCase(formatKey(d1), formatKey(d2)); }); return AvesColorsProvider( allowMonochrome: false, child: LayoutBuilder( builder: (context, constraints) { final series = [ charts.Series( id: 'type', colorFn: (d, i) => charts.ColorUtil.fromDartColor(colorize(context, d)), domainFn: (d, i) => formatKey(d), measureFn: (d, i) => d.value, data: seriesData, labelAccessorFn: (d, _) => '${formatKey(d)}: ${d.value}', ), ]; final textScaler = MediaQuery.textScalerOf(context); final minWidth = textScaler.scale(avesDonutMinWidth); final availableWidth = constraints.maxWidth; final dim = max(minWidth, availableWidth / (availableWidth > 4 * minWidth ? 4 : (availableWidth > 2 * minWidth ? 2 : 1))); final donut = SizedBox( width: dim, height: dim, child: Stack( alignment: Alignment.center, children: [ ClipRRect( borderRadius: BorderRadius.all(Radius.circular(dim)), child: Container( width: dim * .7, height: dim * .7, color: Themes.secondLayerColor(context), ), ), charts.PieChart( series, animate: context.select((v) => v.animate), animationDuration: widget.animationDuration, defaultRenderer: charts.ArcRendererConfig( arcWidth: 16, ), ), Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ widget.title, Text( formatValue(sum), textAlign: TextAlign.center, ), ], ), ), ], ), ); final onTap = widget.onTap; final legend = SizedBox( width: dim, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: seriesData .map((d) => InkWell( onTap: onTap != null ? () => onTap(d) : null, borderRadius: const BorderRadius.all(Radius.circular(123)), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(AIcons.circle, fill: 1, color: colorize(context, d)), const SizedBox(width: 8), Flexible( child: Text( formatKey(d), overflow: TextOverflow.fade, softWrap: false, maxLines: 1, ), ), const SizedBox(width: 8), Text( formatValue(d.value), style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 4), ], ), )) .toList(), ), ); final children = [ donut, legend, ]; return availableWidth > minWidth * 2 ? Row( mainAxisSize: MainAxisSize.min, children: children, ) : Column( mainAxisSize: MainAxisSize.min, children: children, ); }, ), ); } @override bool get wantKeepAlive => true; } @immutable class AvesDonutDatum extends Equatable { final String key; final int value; @override List get props => [key, value]; const AvesDonutDatum({ required this.key, required this.value, }); }