Skip to content
Snippets Groups Projects
Commit 3d4aa3d8 authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Add "Scrobbles per hour" chart

parent 919fd097
No related branches found
No related tags found
1 merge request!10Resolve "Add "scrobbles / hour" chart"
Pipeline #4458 passed
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=0.0.9
app.versionCode=9
app.versionName=0.0.10
app.versionCode=10
......@@ -10,6 +10,8 @@
"timeline_title": "Recent scrobbles ({daysCount} days)",
"counts_by_day": "Counts by day ({daysCount} days)",
"counts_by_hour": "Counts by hour ({daysCount} days)",
"MON": "MON",
"TUE": "TUE",
"WED": "WED",
......
......@@ -10,6 +10,8 @@
"timeline_title": "Écoutes récentes ({daysCount} jours)",
"counts_by_day": "Écoutes par jour ({daysCount} jours)",
"counts_by_hour": "Écoutes par heure ({daysCount} jours)",
"MON": "LUN",
"TUE": "MAR",
"WED": "MER",
......
Add chart "scrobbles counts per hour".
Ajout du graphique du nombre d'écoutes par heure.
import 'dart:convert';
class CountsByHourData {
final Map<int, double> data;
const CountsByHourData({
required this.data,
});
factory CountsByHourData.fromJson(Map<String, dynamic> json) {
Map<int, double> data = {};
if (json['counts-by-hour'] != null) {
json['counts-by-hour'].keys.forEach((day) {
if (int.parse(day) != 24) {
data[int.parse(day)] = double.parse(json['counts-by-hour'][day].toString());
}
});
}
return CountsByHourData(data: data);
}
factory CountsByHourData.createEmpty() {
return CountsByHourData.fromJson({});
}
String toString() {
Map<String, double> map = {};
this.data.keys.forEach((day) {
double? value = this.data[day];
map[day.toString()] = value != null ? value.toDouble() : 0.0;
});
return jsonEncode({'counts-by-hour': map});
}
}
......@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/counts_by_day.dart';
import '../models/counts_by_hour.dart';
import '../models/statistics.dart';
import '../models/timeline.dart';
......@@ -39,4 +40,15 @@ class ScrobblesApi {
throw Exception('Failed to get data from API.');
}
}
static Future<CountsByHourData> fetchCountsByHour(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-hour';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return CountsByHourData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
}
}
}
......@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../widgets/header.dart';
import '../widgets/main_screen/counts_by_day_card.dart';
import '../widgets/main_screen/counts_by_hour_card.dart';
import '../widgets/main_screen/statistics_card.dart';
import '../widgets/main_screen/timeline_card.dart';
......@@ -22,6 +23,8 @@ class MainScreen extends StatelessWidget {
const ChartTimelineCard(),
const SizedBox(height: 8),
const ChartCountsByDayCard(),
const SizedBox(height: 8),
const ChartCountsByHourCard(),
const SizedBox(height: 36),
],
),
......
import 'dart:convert';
import 'package:flutter/material.dart';
import '../../../models/counts_by_hour.dart';
import '../../../network/scrobbles_api.dart';
import '../../../ui/widgets/error.dart';
import '../../../ui/widgets/main_screen/counts_by_hour_content.dart';
class ChartCountsByHourCard extends StatelessWidget {
const ChartCountsByHourCard({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = 21;
late Future<CountsByHourData> futureCountsByHour =
ScrobblesApi.fetchCountsByHour(daysCount);
return FutureBuilder<CountsByHourData>(
future: futureCountsByHour,
builder: (context, snapshot) {
if (snapshot.hasError) {
return ShowErrorWidget(message: '${snapshot.error}');
}
return Card(
elevation: 2,
shadowColor: Theme.of(context).colorScheme.shadow,
color: Theme.of(context).colorScheme.primary,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: ChartCountsByHourCardContent(
daysCount: daysCount,
chartData: snapshot.hasData
? CountsByHourData.fromJson(jsonDecode(snapshot.data.toString()))
: CountsByHourData.createEmpty(),
isLoading: !snapshot.hasData,
),
),
);
},
);
}
}
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../config/app_colors.dart';
import '../../../models/counts_by_hour.dart';
import '../../../utils/color_extensions.dart';
class CountsByHourCardContentChart extends StatelessWidget {
final CountsByHourData chartData;
const CountsByHourCardContentChart({super.key, required this.chartData});
@override
Widget build(BuildContext context) {
return Container(
height: 100.0,
child: LayoutBuilder(builder: (context, constraints) {
final double maxWidth = constraints.maxWidth;
final int barsCount = this.chartData.data.keys.length;
final double barWidth = 0.7 * (maxWidth / barsCount);
return BarChart(
BarChartData(
barGroups: getDataCounts(barWidth),
backgroundColor: Theme.of(context).colorScheme.onBackground,
borderData: getBorderData(),
gridData: getGridData(),
titlesData: getTitlesData(),
barTouchData: getBarTouchData(),
maxY: getNextRoundNumber(getMaxCountsValue(), 5),
),
);
}),
);
}
double getMaxCountsValue() {
double maxValue = 0;
this.chartData.data.keys.forEach((key) {
double? counts = this.chartData.data[key];
if (counts != null) {
if (counts > maxValue) {
maxValue = counts;
}
}
});
return maxValue;
}
double getNextRoundNumber(double number, int scale) {
return scale * ((number ~/ scale).toInt() + 1);
}
List<BarChartGroupData> getDataCounts(double barWidth) {
List<BarChartGroupData> data = [];
this.chartData.data.keys.forEach((day) {
final double? counts = this.chartData.data[day];
if (counts != null) {
final Color barColor = AppColors.contentColorCyan.darken(30);
data.add(BarChartGroupData(
x: day,
barRods: [
BarChartRodData(
toY: counts,
color: barColor,
width: barWidth,
borderRadius: BorderRadius.all(Radius.zero),
borderSide: BorderSide(
color: barColor.darken(20),
),
),
],
));
}
});
return data;
}
FlBorderData getBorderData() {
return FlBorderData(
show: true,
border: Border.all(
color: AppColors.borderColor,
width: 2,
),
);
}
FlGridData getGridData() {
return const FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
);
}
FlTitlesData getTitlesData() {
const AxisTitles none = const AxisTitles(
sideTitles: SideTitles(showTitles: false),
);
final AxisTitles verticalTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: getVerticalTitlesWidget,
interval: 5,
),
);
final AxisTitles horizontalTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 20,
getTitlesWidget: getHorizontalTitlesWidget,
),
);
return FlTitlesData(
show: true,
bottomTitles: horizontalTitles,
leftTitles: none,
topTitles: none,
rightTitles: verticalTitles,
);
}
Widget getVerticalTitlesWidget(double value, TitleMeta meta) {
return SideTitleWidget(
axisSide: meta.axisSide,
space: 4,
child: Text(
value.toInt().toString(),
style: const TextStyle(
color: AppColors.mainTextColor1,
fontSize: 12,
),
),
);
}
Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
String text = '';
if (value % 4 == 0 || value == 23) {
text = value.toInt().toString().padLeft(2, '0');
}
return SideTitleWidget(
axisSide: meta.axisSide,
space: 2,
child: Text(
text,
style: const TextStyle(
color: AppColors.mainTextColor1,
fontSize: 11,
),
),
);
}
BarTouchData getBarTouchData() {
return BarTouchData(
enabled: true,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: Colors.transparent,
tooltipPadding: EdgeInsets.zero,
tooltipMargin: 2,
getTooltipItem: (
BarChartGroupData group,
int groupIndex,
BarChartRodData rod,
int rodIndex,
) {
return BarTooltipItem(
rod.toY.round().toString(),
const TextStyle(
color: AppColors.mainTextColor2,
fontWeight: FontWeight.bold,
fontSize: 10,
),
);
},
),
);
}
}
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../../../models/counts_by_hour.dart';
import '../../../ui/widgets/main_screen/counts_by_hour_chart.dart';
class ChartCountsByHourCardContent extends StatelessWidget {
final int daysCount;
final CountsByHourData chartData;
final bool isLoading;
const ChartCountsByHourCardContent(
{super.key, required this.daysCount, required this.chartData, required this.isLoading});
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).primaryTextTheme;
final String placeholder = '⏳';
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'counts_by_hour',
style: textTheme.titleLarge!.apply(fontWeightDelta: 2),
).tr(
namedArgs: {
'daysCount': this.daysCount.toString(),
},
),
const SizedBox(height: 8),
this.isLoading
? Text(placeholder)
: CountsByHourCardContentChart(
chartData: CountsByHourData.fromJson(jsonDecode(this.chartData.toString())),
),
],
);
}
}
......@@ -3,7 +3,7 @@ description: Display scrobbles data and charts
publish_to: 'none'
version: 0.0.9+9
version: 0.0.10+10
environment:
sdk: '^3.0.0'
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment