refactor(ui): Refactor charts to remove code duplication

This commit is contained in:
Inex Code 2024-12-12 17:04:01 +03:00
parent d8d0ea0c3c
commit 149e6d5a47
No known key found for this signature in database
8 changed files with 373 additions and 822 deletions

View file

@ -22,3 +22,20 @@ List<Color> harmonizedBasicColors(final BuildContext context) => [
Colors.blueGrey.harmonizeWith(Theme.of(context).colorScheme.primary),
Colors.grey.harmonizeWith(Theme.of(context).colorScheme.primary),
];
List<Color> 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],
);
}
}

View file

@ -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<TimeSeriesData> 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;
}

View file

@ -8,23 +8,6 @@ class _Chart extends StatelessWidget {
final MetricsState state = cubit.state;
List<Widget> charts;
List<Color> 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,
),

View file

@ -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<TimeSeriesData> data;
final Period period;
final DateTime start;
List<FlSpot> getSpots() {
var i = 0;
final List<FlSpot> 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<LineBarSpot> touchedBarSpots) {
final List<LineTooltipItem> 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;
}
}

View file

@ -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<DiskGraphData> diskData;
final Period period;
final DateTime start;
List<FlSpot> getSpots(final List<TimeSeriesData> data) {
var i = 0;
final List<FlSpot> 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<LineBarSpot> touchedBarSpots) {
final List<LineTooltipItem> 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<LineChartBarData>(
(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;
}
}

View file

@ -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<List<TimeSeriesData>> data;
final Period period;
final DateTime start;
bool get showRightTitle => false;
static List<FlSpot> getSpots(final List<TimeSeriesData> data) {
var i = 0;
final List<FlSpot> 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<TimeSeriesData> 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<LineBarSpot> touchedBarSpots) {
final List<LineTooltipItem> 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<LineChartBarData>(
(final List<TimeSeriesData> 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,
),
),
),
),
),
);
}
}

View file

@ -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<TimeSeriesData> data;
final Period period;
final DateTime start;
List<FlSpot> getSpots() {
var i = 0;
final List<FlSpot> 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<LineBarSpot> touchedBarSpots) {
final List<LineTooltipItem> 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;
}
}

View file

@ -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<List<TimeSeriesData>> listData;
final Period period;
final DateTime start;
List<FlSpot> getSpots(final data) {
var i = 0;
final List<FlSpot> 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<LineBarSpot> touchedBarSpots) {
final List<LineTooltipItem> 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();
}