From 149e6d5a47a5dfd350c1c019bc6f9f4697de0714 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Thu, 12 Dec 2024 17:04:01 +0300 Subject: [PATCH] refactor(ui): Refactor charts to remove code duplication --- lib/theming/harmonized_basic_colors.dart | 17 ++ .../server_details/charts/bottom_title.dart | 29 -- lib/ui/pages/server_details/charts/chart.dart | 23 +- .../server_details/charts/cpu_chart.dart | 199 +------------ .../server_details/charts/disk_charts.dart | 196 ++----------- .../server_details/charts/generic_chart.dart | 261 +++++++++++++++++ .../server_details/charts/memory_chart.dart | 199 +------------ .../server_details/charts/network_charts.dart | 271 +++--------------- 8 files changed, 373 insertions(+), 822 deletions(-) delete mode 100644 lib/ui/pages/server_details/charts/bottom_title.dart create mode 100644 lib/ui/pages/server_details/charts/generic_chart.dart diff --git a/lib/theming/harmonized_basic_colors.dart b/lib/theming/harmonized_basic_colors.dart index 30ac672d..7f3a25a5 100644 --- a/lib/theming/harmonized_basic_colors.dart +++ b/lib/theming/harmonized_basic_colors.dart @@ -22,3 +22,20 @@ List harmonizedBasicColors(final BuildContext context) => [ Colors.blueGrey.harmonizeWith(Theme.of(context).colorScheme.primary), Colors.grey.harmonizeWith(Theme.of(context).colorScheme.primary), ]; + +List getGraphColors(final BuildContext context, final int length) { + final colors = [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.tertiary, + Theme.of(context).colorScheme.secondary, + ...harmonizedBasicColors(context), + ]; + if (length <= colors.length) { + return colors.sublist(0, length); + } else { + return List.generate( + length, + (final index) => colors[index % colors.length], + ); + } +} diff --git a/lib/ui/pages/server_details/charts/bottom_title.dart b/lib/ui/pages/server_details/charts/bottom_title.dart deleted file mode 100644 index d30e0471..00000000 --- a/lib/ui/pages/server_details/charts/bottom_title.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:intl/intl.dart'; -import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/models/metrics.dart'; - -String bottomTitle( - final int value, - final List data, - final Period period, -) { - final hhmm = DateFormat('HH:mm'); - final day = DateFormat('MMMd'); - String res; - - if (value <= 0 || value >= data.length) { - return ''; - } - - final time = data[value].time; - switch (period) { - case Period.hour: - case Period.day: - res = hhmm.format(time); - break; - case Period.month: - res = day.format(time); - } - - return res; -} diff --git a/lib/ui/pages/server_details/charts/chart.dart b/lib/ui/pages/server_details/charts/chart.dart index aae96dab..c20580d3 100644 --- a/lib/ui/pages/server_details/charts/chart.dart +++ b/lib/ui/pages/server_details/charts/chart.dart @@ -8,23 +8,6 @@ class _Chart extends StatelessWidget { final MetricsState state = cubit.state; List charts; - List getGraphColors(final BuildContext context, final int length) { - final colors = [ - Theme.of(context).colorScheme.primary, - Theme.of(context).colorScheme.tertiary, - Theme.of(context).colorScheme.secondary, - ...harmonizedBasicColors(context), - ]; - if (length <= colors.length) { - return colors.sublist(0, length); - } else { - return List.generate( - length, - (final index) => colors[index % colors.length], - ); - } - } - if (state is MetricsLoaded || state is MetricsLoading) { charts = [ FilledCard( @@ -332,7 +315,7 @@ class _Chart extends StatelessWidget { return SizedBox( height: 200, child: CpuChart( - data: data, + data: [data], period: state.period, start: state.metrics.start, ), @@ -349,7 +332,7 @@ class _Chart extends StatelessWidget { return SizedBox( height: 200, child: MemoryChart( - data: data.overallMetrics, + data: [data.overallMetrics], period: state.period, start: state.metrics.start, ), @@ -363,7 +346,7 @@ class _Chart extends StatelessWidget { return SizedBox( height: 200, child: NetworkChart( - listData: [ppsIn, ppsOut], + data: [ppsIn, ppsOut], period: state.period, start: state.metrics.start, ), diff --git a/lib/ui/pages/server_details/charts/cpu_chart.dart b/lib/ui/pages/server_details/charts/cpu_chart.dart index 0b9de6f0..bf4ca506 100644 --- a/lib/ui/pages/server_details/charts/cpu_chart.dart +++ b/lib/ui/pages/server_details/charts/cpu_chart.dart @@ -1,45 +1,29 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/models/metrics.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart'; +import 'package:selfprivacy/ui/pages/server_details/charts/generic_chart.dart'; -class CpuChart extends StatelessWidget { +class CpuChart extends GenericLineChart { const CpuChart({ - required this.data, - required this.period, - required this.start, + required super.data, + required super.period, + required super.start, super.key, }); - final List data; - final Period period; - final DateTime start; - - List getSpots() { - var i = 0; - final List res = []; - - for (final d in data) { - res.add(FlSpot(i.toDouble(), d.value)); - i++; - } - - return res; - } - + @override String screenReaderDescription(final BuildContext context) { - final lastData = data.last; + final lastData = data.first.last; final lastValue = lastData.value; - final averageUsage = - data.map((final e) => e.value).reduce((final a, final b) => a + b) / - data.length; - final maxUsage = data + final averageUsage = data.first + .map((final e) => e.value) + .reduce((final a, final b) => a + b) / + data.length; + final maxUsage = data.first .map((final e) => e.value) .reduce((final a, final b) => a > b ? a : b); - final maxUsageTime = data.firstWhere((final e) => e.value == maxUsage).time; + final maxUsageTime = + data.first.firstWhere((final e) => e.value == maxUsage).time; final label = 'resource_chart.cpu_chart_screen_reader_explanation'.tr( namedArgs: { @@ -54,159 +38,4 @@ class CpuChart extends StatelessWidget { return label; } - - @override - Widget build(final BuildContext context) => Semantics( - label: screenReaderDescription(context), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: true, - touchTooltipData: LineTouchTooltipData( - getTooltipColor: (final LineBarSpot _) => - Theme.of(context).colorScheme.surface, - tooltipPadding: const EdgeInsets.all(8), - getTooltipItems: (final List touchedBarSpots) { - final List res = []; - - for (final spot in touchedBarSpots) { - final value = spot.y; - final date = data[spot.x.toInt()].time; - - res.add( - LineTooltipItem( - '${value.toStringAsFixed(2)}% at ${DateFormat('HH:mm dd.MM.yyyy').format(date)}', - TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.bold, - ), - ), - ); - } - - return res; - }, - ), - ), - lineBarsData: [ - LineChartBarData( - spots: getSpots(), - isCurved: false, - barWidth: 2, - color: Theme.of(context).colorScheme.primary, - dotData: const FlDotData( - show: false, - ), - belowBarData: BarAreaData( - show: true, - gradient: LinearGradient( - colors: [ - Theme.of(context).colorScheme.primary.withOpacity(0.5), - Theme.of(context).colorScheme.primary.withOpacity(0.0), - ], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - ), - ), - ), - ], - minY: 0, - // Maximal value of data by 100 step - maxY: 100, - minX: 0, - titlesData: FlTitlesData( - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - interval: 40, - reservedSize: 30, - getTitlesWidget: (final value, final titleMeta) => Padding( - padding: const EdgeInsets.all(8.0), - child: ExcludeSemantics( - child: Text( - bottomTitle( - value.toInt(), - data, - period, - ), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - ), - ), - showTitles: true, - ), - ), - leftTitles: const AxisTitles( - sideTitles: SideTitles( - showTitles: false, - ), - ), - rightTitles: const AxisTitles( - sideTitles: SideTitles( - showTitles: false, - ), - ), - ), - gridData: FlGridData( - show: true, - drawVerticalLine: true, - horizontalInterval: 25, - verticalInterval: 40, - getDrawingHorizontalLine: (final value) => FlLine( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - strokeWidth: 1, - ), - getDrawingVerticalLine: (final value) => FlLine( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - strokeWidth: 1, - ), - ), - borderData: FlBorderData( - show: true, - border: Border( - bottom: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - left: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - right: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - top: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - ), - ), - ), - ), - ); - - bool checkToShowTitle( - final double minValue, - final double maxValue, - final SideTitles sideTitles, - final double appliedInterval, - final double value, - ) { - if (value < 0) { - return false; - } else if (value == 0) { - return true; - } - - final localValue = value - minValue; - final v = localValue / 20; - return v - v.floor() == 0; - } } diff --git a/lib/ui/pages/server_details/charts/disk_charts.dart b/lib/ui/pages/server_details/charts/disk_charts.dart index 9c86015b..359dc994 100644 --- a/lib/ui/pages/server_details/charts/disk_charts.dart +++ b/lib/ui/pages/server_details/charts/disk_charts.dart @@ -1,35 +1,22 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/models/metrics.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart'; +import 'package:selfprivacy/ui/pages/server_details/charts/generic_chart.dart'; import 'package:selfprivacy/ui/pages/server_details/server_details_screen.dart'; -class DiskChart extends StatelessWidget { - const DiskChart({ +class DiskChart extends GenericLineChart { + DiskChart({ required this.diskData, - required this.period, - required this.start, + required super.period, + required super.start, super.key, - }); + }) : super( + data: diskData.map((final e) => e.diskData).toList(), + ); final List diskData; - final Period period; - final DateTime start; - - List getSpots(final List data) { - var i = 0; - final List res = []; - - for (final d in data) { - res.add(FlSpot(i.toDouble(), d.value)); - i++; - } - - return res; - } + @override String screenReaderDescription(final BuildContext context) { final buffer = StringBuffer(); buffer.write( @@ -61,159 +48,18 @@ class DiskChart extends StatelessWidget { } @override - Widget build(final BuildContext context) => Semantics( - label: screenReaderDescription(context), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: true, - touchTooltipData: LineTouchTooltipData( - getTooltipColor: (final LineBarSpot _) => - Theme.of(context).colorScheme.surface, - tooltipPadding: const EdgeInsets.all(8), - getTooltipItems: (final List touchedBarSpots) { - final List res = []; - - bool timeShown = false; - for (final spot in touchedBarSpots) { - final value = spot.y; - final date = diskData.first.diskData[spot.x.toInt()].time; - - res.add( - LineTooltipItem( - '${timeShown ? '' : DateFormat('HH:mm dd.MM.yyyy').format(date)} ${diskData[spot.barIndex].volume.displayName} ${value.toInt()}%', - TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.bold, - ), - ), - ); - - timeShown = true; - } - - return res; - }, - ), - ), - lineBarsData: diskData - .map( - (final disk) => LineChartBarData( - spots: getSpots(disk.diskData), - isCurved: false, - barWidth: 2, - color: disk.color, - dotData: const FlDotData( - show: false, - ), - belowBarData: BarAreaData( - show: true, - gradient: LinearGradient( - colors: [ - disk.color.withOpacity(0.5), - disk.color.withOpacity(0.0), - ], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - ), - ), - ), - ) - .toList(), - minY: 0, - maxY: 100, - minX: 0, - titlesData: FlTitlesData( - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - interval: 40, - reservedSize: 30, - getTitlesWidget: (final value, final titleMeta) => Padding( - padding: const EdgeInsets.all(8.0), - child: ExcludeSemantics( - child: Text( - bottomTitle( - value.toInt(), - diskData.first.diskData, - period, - ), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - ), - ), - showTitles: true, - ), - ), - leftTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - rightTitles: const AxisTitles( - sideTitles: SideTitles( - showTitles: false, - ), - ), - ), - gridData: FlGridData( - show: true, - drawVerticalLine: true, - verticalInterval: 40, - horizontalInterval: 25, - getDrawingHorizontalLine: (final value) => FlLine( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - strokeWidth: 1, - ), - getDrawingVerticalLine: (final value) => FlLine( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - strokeWidth: 1, - ), - ), - borderData: FlBorderData( - show: true, - border: Border( - bottom: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - left: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - right: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - top: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - ), - ), - ), + LineTooltipItem generateTooltipItem({ + required final bool timeShown, + required final DateTime date, + required final double value, + required final LineBarSpot spot, + required final BuildContext context, + }) => + LineTooltipItem( + '${timeShown ? '' : DateFormat('HH:mm dd.MM.yyyy').format(date)} ${diskData[spot.barIndex].volume.displayName} ${value.toInt()}%', + TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, ), ); - - bool checkToShowTitle( - final double minValue, - final double maxValue, - final SideTitles sideTitles, - final double appliedInterval, - final double value, - ) { - if (value < 0) { - return false; - } else if (value == 0) { - return true; - } - - final diff = value - minValue; - final finalValue = diff / 20; - return finalValue - finalValue.floor() == 0; - } } diff --git a/lib/ui/pages/server_details/charts/generic_chart.dart b/lib/ui/pages/server_details/charts/generic_chart.dart new file mode 100644 index 00000000..07a397f5 --- /dev/null +++ b/lib/ui/pages/server_details/charts/generic_chart.dart @@ -0,0 +1,261 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/models/metrics.dart'; +import 'package:selfprivacy/theming/harmonized_basic_colors.dart'; + +class GenericLineChart extends StatelessWidget { + const GenericLineChart({ + required this.data, + required this.period, + required this.start, + super.key, + }); + + final List> data; + final Period period; + final DateTime start; + + bool get showRightTitle => false; + + static List getSpots(final List data) { + var i = 0; + final List res = []; + + for (final d in data) { + res.add(FlSpot(i.toDouble(), d.value)); + i++; + } + + return res; + } + + static bool checkToShowTitle( + final double minValue, + final double maxValue, + final SideTitles sideTitles, + final double appliedInterval, + final double value, + ) { + if (value < 0) { + return false; + } else if (value == 0) { + return true; + } + + final diff = value - minValue; + final finalValue = diff / 20; + return finalValue - finalValue.floor() == 0; + } + + String screenReaderDescription(final BuildContext context) => + 'Overrite this function'; + + LineTooltipItem generateTooltipItem({ + required final bool timeShown, + required final DateTime date, + required final double value, + required final LineBarSpot spot, + required final BuildContext context, + }) => + LineTooltipItem( + '${value.toStringAsFixed(2)}% at ${DateFormat('HH:mm dd.MM.yyyy').format(date)}', + TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ); + + double getMaxY() => 100; + + String getRightTitle(final double value) => ''; + + String bottomTitle( + final int value, + final List data, + final Period period, + ) { + final hhmm = DateFormat('HH:mm'); + final day = DateFormat('MMMd'); + String res; + + if (value <= 0 || value >= data.length) { + return ''; + } + + final time = data[value].time; + switch (period) { + case Period.hour: + case Period.day: + res = hhmm.format(time); + break; + case Period.month: + res = day.format(time); + } + + return res; + } + + @override + Widget build(final BuildContext context) { + final colors = getGraphColors(context, data.length); + return Semantics( + label: screenReaderDescription(context), + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + enabled: true, + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (final LineBarSpot _) => + Theme.of(context).colorScheme.surface, + tooltipPadding: const EdgeInsets.all(8), + getTooltipItems: (final List touchedBarSpots) { + final List tooltipItems = []; + bool timeShown = false; + + for (final spot in touchedBarSpots) { + final value = spot.y; + final date = data.first[spot.x.toInt()].time; + + tooltipItems.add( + generateTooltipItem( + timeShown: timeShown, + date: date, + value: value, + spot: spot, + context: context, + ), + ); + + timeShown = true; + } + + return tooltipItems; + }, + ), + ), + lineBarsData: data + .map( + (final List dataSeries) => LineChartBarData( + spots: getSpots(dataSeries), + isCurved: false, + barWidth: 2, + color: colors[data.indexOf(dataSeries)], + dotData: const FlDotData( + show: false, + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + colors[data.indexOf(dataSeries)].withOpacity(0.5), + colors[data.indexOf(dataSeries)].withOpacity(0.0), + ], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + ), + ), + ), + ) + .toList(), + minY: 0, + maxY: getMaxY(), + minX: 0, + titlesData: FlTitlesData( + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + interval: 40, + reservedSize: 30, + getTitlesWidget: (final value, final titleMeta) => Padding( + padding: const EdgeInsets.all(8.0), + child: ExcludeSemantics( + child: Text( + bottomTitle(value.toInt(), data.first, period), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ), + showTitles: true, + ), + ), + leftTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: showRightTitle + ? AxisTitles( + sideTitles: SideTitles( + reservedSize: 50, + getTitlesWidget: (final value, final titleMeta) => + Padding( + padding: const EdgeInsets.only(left: 5), + child: ExcludeSemantics( + child: Text( + getRightTitle(value), + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ), + ), + interval: getMaxY() * 2 / 6.5, + showTitles: true, + ), + ) + : const AxisTitles( + sideTitles: SideTitles( + showTitles: false, + ), + ), + ), + gridData: FlGridData( + show: true, + drawVerticalLine: true, + verticalInterval: 40, + horizontalInterval: getMaxY() == 100 ? 25 : getMaxY() * 2 / 6.5, + getDrawingHorizontalLine: (final value) => FlLine( + color: Theme.of(context).colorScheme.outline.withOpacity(0.3), + strokeWidth: 1, + ), + getDrawingVerticalLine: (final value) => FlLine( + color: Theme.of(context).colorScheme.outline.withOpacity(0.3), + strokeWidth: 1, + ), + ), + borderData: FlBorderData( + show: true, + border: Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.outline.withOpacity(0.3), + width: 1, + ), + left: BorderSide( + color: Theme.of(context).colorScheme.outline.withOpacity(0.3), + width: 1, + ), + right: BorderSide( + color: Theme.of(context).colorScheme.outline.withOpacity(0.3), + width: 1, + ), + top: BorderSide( + color: Theme.of(context).colorScheme.outline.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/pages/server_details/charts/memory_chart.dart b/lib/ui/pages/server_details/charts/memory_chart.dart index fc146b10..828eb370 100644 --- a/lib/ui/pages/server_details/charts/memory_chart.dart +++ b/lib/ui/pages/server_details/charts/memory_chart.dart @@ -1,45 +1,29 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/models/metrics.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart'; +import 'package:selfprivacy/ui/pages/server_details/charts/generic_chart.dart'; -class MemoryChart extends StatelessWidget { +class MemoryChart extends GenericLineChart { const MemoryChart({ - required this.data, - required this.period, - required this.start, + required super.data, + required super.period, + required super.start, super.key, }); - final List data; - final Period period; - final DateTime start; - - List getSpots() { - var i = 0; - final List res = []; - - for (final d in data) { - res.add(FlSpot(i.toDouble(), d.value)); - i++; - } - - return res; - } - + @override String screenReaderDescription(final BuildContext context) { - final lastData = data.last; + final lastData = data.first.last; final lastValue = lastData.value; - final averageUsage = - data.map((final e) => e.value).reduce((final a, final b) => a + b) / - data.length; - final maxUsage = data + final averageUsage = data.first + .map((final e) => e.value) + .reduce((final a, final b) => a + b) / + data.length; + final maxUsage = data.first .map((final e) => e.value) .reduce((final a, final b) => a > b ? a : b); - final maxUsageTime = data.firstWhere((final e) => e.value == maxUsage).time; + final maxUsageTime = + data.first.firstWhere((final e) => e.value == maxUsage).time; final label = 'resource_chart.memory_chart_screen_reader_explanation'.tr( namedArgs: { @@ -54,159 +38,4 @@ class MemoryChart extends StatelessWidget { return label; } - - @override - Widget build(final BuildContext context) => Semantics( - label: screenReaderDescription(context), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: true, - touchTooltipData: LineTouchTooltipData( - getTooltipColor: (final LineBarSpot _) => - Theme.of(context).colorScheme.surface, - tooltipPadding: const EdgeInsets.all(8), - getTooltipItems: (final List touchedBarSpots) { - final List res = []; - - for (final spot in touchedBarSpots) { - final value = spot.y; - final date = data[spot.x.toInt()].time; - - res.add( - LineTooltipItem( - '${value.toStringAsFixed(2)}% at ${DateFormat('HH:mm dd.MM.yyyy').format(date)}', - TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.bold, - ), - ), - ); - } - - return res; - }, - ), - ), - lineBarsData: [ - LineChartBarData( - spots: getSpots(), - isCurved: false, - barWidth: 2, - color: Theme.of(context).colorScheme.primary, - dotData: const FlDotData( - show: false, - ), - belowBarData: BarAreaData( - show: true, - gradient: LinearGradient( - colors: [ - Theme.of(context).colorScheme.primary.withOpacity(0.5), - Theme.of(context).colorScheme.primary.withOpacity(0.0), - ], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - ), - ), - ), - ], - minY: 0, - // Maximal value of data by 100 step - maxY: 100, - minX: 0, - titlesData: FlTitlesData( - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - interval: 40, - reservedSize: 30, - getTitlesWidget: (final value, final titleMeta) => Padding( - padding: const EdgeInsets.all(8.0), - child: ExcludeSemantics( - child: Text( - bottomTitle( - value.toInt(), - data, - period, - ), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - ), - ), - showTitles: true, - ), - ), - leftTitles: const AxisTitles( - sideTitles: SideTitles( - showTitles: false, - ), - ), - rightTitles: const AxisTitles( - sideTitles: SideTitles( - showTitles: false, - ), - ), - ), - gridData: FlGridData( - show: true, - drawVerticalLine: true, - horizontalInterval: 25, - verticalInterval: 40, - getDrawingHorizontalLine: (final value) => FlLine( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - strokeWidth: 1, - ), - getDrawingVerticalLine: (final value) => FlLine( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - strokeWidth: 1, - ), - ), - borderData: FlBorderData( - show: true, - border: Border( - bottom: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - left: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - right: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - top: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - ), - ), - ), - ), - ); - - bool checkToShowTitle( - final double minValue, - final double maxValue, - final SideTitles sideTitles, - final double appliedInterval, - final double value, - ) { - if (value < 0) { - return false; - } else if (value == 0) { - return true; - } - - final localValue = value - minValue; - final v = localValue / 20; - return v - v.floor() == 0; - } } diff --git a/lib/ui/pages/server_details/charts/network_charts.dart b/lib/ui/pages/server_details/charts/network_charts.dart index 019f48da..605cbfa6 100644 --- a/lib/ui/pages/server_details/charts/network_charts.dart +++ b/lib/ui/pages/server_details/charts/network_charts.dart @@ -3,61 +3,45 @@ import 'dart:math'; import 'package:easy_localization/easy_localization.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/disk_size.dart'; -import 'package:selfprivacy/logic/models/metrics.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart'; +import 'package:selfprivacy/ui/pages/server_details/charts/generic_chart.dart'; -class NetworkChart extends StatelessWidget { +class NetworkChart extends GenericLineChart { const NetworkChart({ - required this.listData, - required this.period, - required this.start, + required super.data, + required super.period, + required super.start, super.key, }); - final List> listData; - final Period period; - final DateTime start; - - List getSpots(final data) { - var i = 0; - final List res = []; - - for (final d in data) { - res.add(FlSpot(i.toDouble(), d.value)); - i++; - } - - return res; - } + @override + bool get showRightTitle => true; + @override String screenReaderDescription(final BuildContext context) { - final lastDataIn = listData[0].last; - final lastDataOut = listData[1].last; + final lastDataIn = data[0].last; + final lastDataOut = data[1].last; final lastValueIn = lastDataIn.value; final lastValueOut = lastDataOut.value; - final averageUsageIn = listData[0] - .map((final e) => e.value) - .reduce((final a, final b) => a + b) / - listData[0].length; - final averageUsageOut = listData[1] - .map((final e) => e.value) - .reduce((final a, final b) => a + b) / - listData[1].length; + final averageUsageIn = + data[0].map((final e) => e.value).reduce((final a, final b) => a + b) / + data[0].length; + final averageUsageOut = + data[1].map((final e) => e.value).reduce((final a, final b) => a + b) / + data[1].length; - final maxUsageIn = listData[0] + final maxUsageIn = data[0] .map((final e) => e.value) .reduce((final a, final b) => a > b ? a : b); - final maxUsageOut = listData[1] + final maxUsageOut = data[1] .map((final e) => e.value) .reduce((final a, final b) => a > b ? a : b); final maxUsageTimeIn = - listData[0].firstWhere((final e) => e.value == maxUsageIn).time; + data[0].firstWhere((final e) => e.value == maxUsageIn).time; final maxUsageTimeOut = - listData[1].firstWhere((final e) => e.value == maxUsageOut).time; + data[1].firstWhere((final e) => e.value == maxUsageOut).time; final label = 'resource_chart.network_chart_screen_reader_explanation'.tr( namedArgs: { @@ -81,199 +65,30 @@ class NetworkChart extends StatelessWidget { } @override - Widget build(final BuildContext context) { - final listDataMax = [ - ...listData[0].map((final e) => e.value), - ...listData[1].map((final e) => e.value), - ].reduce(max); - return Semantics( - label: screenReaderDescription(context), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: true, - touchTooltipData: LineTouchTooltipData( - getTooltipColor: (final LineBarSpot _) => - Theme.of(context).colorScheme.surface, - tooltipPadding: const EdgeInsets.all(8), - getTooltipItems: (final List touchedBarSpots) { - final List res = []; + double getMaxY() => + [ + ...data[0].map((final e) => e.value), + ...data[1].map((final e) => e.value), + ].reduce(max) * + 1.2; - bool timeShown = false; - - for (final spot in touchedBarSpots) { - final value = spot.y; - final date = listData[0][spot.x.toInt()].time; - - res.add( - LineTooltipItem( - '${timeShown ? '' : DateFormat('HH:mm dd.MM.yyyy').format(date)} ${spot.barIndex == 0 ? 'resource_chart.in'.tr() : 'resource_chart.out'.tr()} ${DiskSize(byte: value.toInt()).toString()}', - TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.bold, - ), - ), - ); - - timeShown = true; - } - - return res; - }, - ), - ), - lineBarsData: [ - // IN - LineChartBarData( - spots: getSpots(listData[0]), - isCurved: false, - barWidth: 2, - color: Theme.of(context).colorScheme.primary, - dotData: const FlDotData( - show: false, - ), - belowBarData: BarAreaData( - show: true, - gradient: LinearGradient( - colors: [ - Theme.of(context).colorScheme.primary.withOpacity(0.5), - Theme.of(context).colorScheme.primary.withOpacity(0.0), - ], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - ), - ), - ), - // OUT - LineChartBarData( - spots: getSpots(listData[1]), - isCurved: false, - barWidth: 2, - color: Theme.of(context).colorScheme.tertiary, - dotData: const FlDotData( - show: false, - ), - belowBarData: BarAreaData( - show: true, - gradient: LinearGradient( - colors: [ - Theme.of(context).colorScheme.tertiary.withOpacity(0.5), - Theme.of(context).colorScheme.tertiary.withOpacity(0.0), - ], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - ), - ), - ), - ], - minY: 0, - maxY: listDataMax * 1.2, - minX: 0, - titlesData: FlTitlesData( - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - interval: 40, - reservedSize: 30, - getTitlesWidget: (final value, final titleMeta) => Padding( - padding: const EdgeInsets.all(8.0), - child: ExcludeSemantics( - child: Text( - bottomTitle( - value.toInt(), - listData[0], - period, - ), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: - Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - ), - showTitles: true, - ), - ), - leftTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - rightTitles: AxisTitles( - sideTitles: SideTitles( - reservedSize: 50, - getTitlesWidget: (final value, final titleMeta) => Padding( - padding: const EdgeInsets.only(left: 5), - child: ExcludeSemantics( - child: Text( - DiskSize(byte: value.toInt()).toString(), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: - Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - ), - interval: listDataMax * 2 / 6.5, - showTitles: true, - ), - ), - ), - gridData: FlGridData( - show: true, - drawVerticalLine: true, - verticalInterval: 40, - horizontalInterval: listDataMax * 2 / 6.5, - getDrawingHorizontalLine: (final value) => FlLine( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - strokeWidth: 1, - ), - getDrawingVerticalLine: (final value) => FlLine( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - strokeWidth: 1, - ), - ), - borderData: FlBorderData( - show: true, - border: Border( - bottom: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - left: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - right: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - top: BorderSide( - color: Theme.of(context).colorScheme.outline.withOpacity(0.3), - width: 1, - ), - ), - ), + @override + LineTooltipItem generateTooltipItem({ + required final bool timeShown, + required final DateTime date, + required final double value, + required final LineBarSpot spot, + required final BuildContext context, + }) => + LineTooltipItem( + '${timeShown ? '' : DateFormat('HH:mm dd.MM.yyyy').format(date)} ${spot.barIndex == 0 ? 'resource_chart.in'.tr() : 'resource_chart.out'.tr()} ${DiskSize(byte: value.toInt()).toString()}', + TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, ), - ), - ); - } + ); - bool checkToShowTitle( - final double minValue, - final double maxValue, - final SideTitles sideTitles, - final double appliedInterval, - final double value, - ) { - if (value < 0) { - return false; - } else if (value == 0) { - return true; - } - - final diff = value - minValue; - final finalValue = diff / 20; - return finalValue - finalValue.floor() == 0; - } + @override + String getRightTitle(final double value) => + DiskSize(byte: value.toInt()).toString(); }