import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:uae_stat/domain/use_cases/language.dart'; import 'package:uae_stat/infrastructure/services/packages/list.dart'; import 'package:uae_stat/presentation/components/space.dart'; class HorizontalBarChart extends ConsumerWidget { const HorizontalBarChart( this.data, { super.key, required this.color, this.shortlistTopN, }); final Map< String, ({ num value, String? barEndText, })> data; final MaterialColor color; final int? shortlistTopN; List get _colorList => [ color.shade50, color.shade300, color.shade600, color.shade900, ]; void _sortData() { // Convert the Map to a List of Map entries final entriesList = data.entries.toList(); // Sort the list by value in descending order entriesList.sort( (a, b) => b.value.value.compareTo(a.value.value), ); // repopulate data data.removeWhere((key, value) => true); data.addEntries(entriesList); } void _shortListData() { // Take the first 5 entries final top5Entries = Map.fromEntries( data.entries.take(shortlistTopN!), ); data.removeWhere( (key, value) => !top5Entries.keys.contains(key), ); } @override Widget build(BuildContext context, ref) { _sortData(); if (shortlistTopN != null) _shortListData(); final titles = Column( crossAxisAlignment: CrossAxisAlignment.end, children: data.keys .map( (e) => Expanded( child: Padding( padding: const EdgeInsetsDirectional.only(end: 4), child: Align( alignment: AlignmentDirectional.centerEnd, child: Text( e, textAlign: TextAlign.end, style: TextStyle( fontFamily: context.translate( 'Roboto', 'NotoKufi', ), fontSize: 9, color: Colors.black, ), ), ), ), ), ) .toList() ..intersperseDivider( 4.verticalSpace, ), ); final minVal = data.values.fold( double.maxFinite, (prev, e) => e.value < prev ? e.value : prev, ); final isAnyBarNegative = minVal < 0; final maxVal = data.values.fold( double.negativeInfinity, (prev, e) => e.value.abs() > prev ? e.value.abs() : prev, ); final totalWidth = maxVal; final bars = Column( crossAxisAlignment: CrossAxisAlignment.start, children: List.generate( data.length, (i) { final e = data.entries.elementAt(i); if (!isAnyBarNegative) { final bar = Container( decoration: BoxDecoration( borderRadius: const BorderRadiusDirectional.horizontal( end: Radius.circular(10), ), color: _colorList[i % _colorList.length], ), child: FractionallySizedBox( widthFactor: e.value.value / totalWidth, alignment: AlignmentDirectional.centerStart, child: double.infinity.verticalSpace, ), ); final barSegment = Expanded( child: Row( children: [ Flexible( child: bar, ), if (e.value.barEndText != null) ...[ 4.horizontalSpace, SizedBox( width: 40, child: FittedBox( fit: BoxFit.scaleDown, child: Text( e.value.barEndText!, style: TextStyle( fontFamily: context.translate( 'Roboto', 'NotoKufi', ), fontSize: 11, color: Colors.black, ), ), ), ), ], ], ), ); return barSegment; } final isBarNegative = e.value.value < 0; final bar = DecoratedBox( decoration: BoxDecoration( color: _colorList[i % _colorList.length], borderRadius: BorderRadiusDirectional.horizontal( end: isBarNegative ? Radius.zero : const Radius.circular(10), start: isBarNegative ? const Radius.circular(10) : Radius.zero, ), ), child: FractionallySizedBox( widthFactor: e.value.value.abs() / totalWidth, alignment: AlignmentDirectional.centerStart, child: double.infinity.verticalSpace, ), ); final barSegment = isBarNegative ? Expanded( child: Row( children: [ Expanded( child: Align( alignment: AlignmentDirectional.centerEnd, child: bar, ), ), Expanded( child: e.value.barEndText == null ? const SizedBox.shrink() : Row( children: [ 4.horizontalSpace, Text( e.value.barEndText!, style: TextStyle( fontFamily: context.translate( 'Roboto', 'NotoKufi', ), fontSize: 11, color: Colors.black, ), ), ], ), ), ], ), ) : Expanded( child: Row( children: [ Expanded( child: e.value.barEndText == null ? const SizedBox.shrink() : Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Text( e.value.barEndText!, textAlign: TextAlign.end, style: TextStyle( fontFamily: context.translate( 'Roboto', 'NotoKufi', ), fontSize: 11, color: Colors.black, ), ), 4.horizontalSpace, ], ), ), Expanded( child: Align( alignment: AlignmentDirectional.centerStart, child: bar, ), ), ], ), ); return barSegment; }, )..intersperseDivider( 8.verticalSpace, ), ); return Row( children: [ Expanded( child: titles, ), if (isAnyBarNegative) const VerticalDivider(), Expanded( flex: 3, child: bars, ), ], ); } } class HorizontalGroupedBarChart extends ConsumerWidget { const HorizontalGroupedBarChart( this.data, { super.key, this.shortlistTopN, }); final Map< String, Map< String, ({ num value, String? barEndText, Color barColor, })>> data; final int? shortlistTopN; void _sortData() { // Convert the Map to a List of Map entries final entriesList = data.entries.toList(); // Sort the list by value in descending order entriesList.sort( (a, b) { final bVal = b.value.values.fold( 0, (prev, e) => e.value > prev ? e.value : prev, ); final aVal = a.value.values.fold( 0, (prev, e) => e.value > prev ? e.value : prev, ); return bVal.compareTo(aVal); }, ); // repopulate data data.removeWhere((key, value) => true); data.addEntries(entriesList); } void _shortListData() { // Take the first 5 entries final top5Entries = Map.fromEntries( data.entries.take(shortlistTopN!), ); data.removeWhere( (key, value) => !top5Entries.keys.contains(key), ); } num minVal() => data.values.fold( double.infinity, (prev, e) { final localMin = e.values.map((r) => r.value).deriveMinimum()!; return localMin < prev ? localMin : prev; }, ); num maxVal() => data.values.fold( double.negativeInfinity, (prev, e) { final localMax = e.values.map((r) => r.value).deriveMaximum()!; return localMax > prev ? localMax : prev; }, ); @override Widget build(BuildContext context, ref) { _sortData(); if (shortlistTopN != null) _shortListData(); final legend = Column( children: data.values.first.entries .map( (key) => Row( children: [ Container( decoration: BoxDecoration( color: key.value.barColor, borderRadius: BorderRadius.circular(2), ), height: 8, width: 8, ), 3.horizontalSpace, Text( key.key, style: const TextStyle( fontSize: 11, ), ), ], ), ) .toList() ..intersperseDivider(2.verticalSpace), ); final titles = Column( crossAxisAlignment: CrossAxisAlignment.end, children: data.keys .map( (e) => Expanded( child: Padding( padding: const EdgeInsetsDirectional.only(end: 4), child: Align( alignment: AlignmentDirectional.centerEnd, child: Text( e, textAlign: TextAlign.end, style: TextStyle( fontFamily: context.translate( 'Roboto', 'NotoKufi', ), fontSize: 9, color: Colors.black, ), ), ), ), ), ) .toList() ..intersperseDivider( 4.verticalSpace, ), ); final minimum = minVal(); final isAnyBarNegative = minimum < 0; final totalWidth = maxVal(); final graph = Column( crossAxisAlignment: CrossAxisAlignment.start, children: data.entries.map( (groupData) { final stackedBarGroup = Column( children: groupData.value.values.map( (barData) { final barOnly = Container( decoration: BoxDecoration( color: barData.barColor, borderRadius: const BorderRadiusDirectional.horizontal( end: Radius.circular(10), ), ), child: FractionallySizedBox( widthFactor: barData.value / totalWidth, alignment: AlignmentDirectional.centerStart, child: double.infinity.verticalSpace, ), ); final barWithText = Row( children: [ Flexible( child: barOnly, ), if (barData.barEndText != null) ...[ 4.horizontalSpace, SizedBox( width: 40, child: FittedBox( fit: BoxFit.scaleDown, child: Text( barData.barEndText!, style: TextStyle( fontFamily: context.translate( 'Roboto', 'NotoKufi', ), fontSize: 11, color: Colors.black, ), ), ), ), ], ], ); return Expanded(child: barWithText); }, ).toList() ..intersperseDivider(4.verticalSpace), ); final group = Expanded( child: stackedBarGroup, ); return group; }, ).toList() ..intersperseDivider( 8.verticalSpace, ), ); return Column( children: [ Row( children: [ const Expanded( child: SizedBox.shrink(), ), Expanded( flex: 3, child: legend, ), ], ), 6.verticalSpace, Expanded( child: Row( children: [ Expanded( child: titles, ), if (isAnyBarNegative) const VerticalDivider(), Expanded( flex: 3, child: graph, ), ], ), ), ], ); } }