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

Add "Scrobbles per day" chart

parent f0dc07a7
No related branches found
No related tags found
1 merge request!9Resolve "Add "scrobbles / day" chart"
Pipeline #4454 passed
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=0.0.8
app.versionCode=8
app.versionName=0.0.9
app.versionCode=9
......@@ -8,5 +8,13 @@
"statistics_recent_scrobbles_count": "Scrobbles: {count}",
"statistics_discoveries": "Discoveries: {artistsCount} artists / {tracksCount} tracks",
"timeline_title": "Recent scrobbles ({daysCount} days)"
"timeline_title": "Recent scrobbles ({daysCount} days)",
"counts_by_day": "Counts by day ({daysCount} days)",
"MON": "MON",
"TUE": "TUE",
"WED": "WED",
"THU": "THU",
"FRI": "FRI",
"SAT": "SAT",
"SUN": "SUN"
}
......@@ -8,5 +8,13 @@
"statistics_recent_scrobbles_count": "Écoutes : {count}",
"statistics_discoveries": "Découvertes : {artistsCount} artistes / {tracksCount} morceaux",
"timeline_title": "Écoutes récentes ({daysCount} jours)"
"timeline_title": "Écoutes récentes ({daysCount} jours)",
"counts_by_day": "Écoutes par jour ({daysCount} jours)",
"MON": "LUN",
"TUE": "MAR",
"WED": "MER",
"THU": "JEU",
"FRI": "VEN",
"SAT": "SAM",
"SUN": "DIM"
}
Add chart "scrobbles counts per day of week".
Ajout du graphique du nombre d'écoutes par jour de la semaine.
import 'dart:convert';
class CountsByDayData {
final Map<int, double> data;
const CountsByDayData({
required this.data,
});
factory CountsByDayData.fromJson(Map<String, dynamic> json) {
Map<int, double> data = {};
if (json['counts-by-day'] != null) {
json['counts-by-day'].keys.forEach((day) {
data[int.parse(day)] = double.parse(json['counts-by-day'][day].toString());
});
}
return CountsByDayData(data: data);
}
factory CountsByDayData.createEmpty() {
return CountsByDayData.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-day': map});
}
}
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/counts_by_day.dart';
import '../models/statistics.dart';
import '../models/timeline.dart';
......@@ -27,4 +28,15 @@ class ScrobblesApi {
throw Exception('Failed to get data from API.');
}
}
static Future<CountsByDayData> fetchCountsByDay(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-day';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return CountsByDayData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
}
}
}
import 'package:flutter/material.dart';
import '../widgets/header.dart';
import '../widgets/main_screen/counts_by_day_card.dart';
import '../widgets/main_screen/statistics_card.dart';
import '../widgets/main_screen/timeline_card.dart';
......@@ -19,6 +20,8 @@ class MainScreen extends StatelessWidget {
const StatisticsCard(),
const SizedBox(height: 8),
const ChartTimelineCard(),
const SizedBox(height: 8),
const ChartCountsByDayCard(),
const SizedBox(height: 36),
],
),
......
import 'dart:convert';
import 'package:flutter/material.dart';
import '../../../models/counts_by_day.dart';
import '../../../network/scrobbles_api.dart';
import '../../../ui/widgets/error.dart';
import '../../../ui/widgets/main_screen/counts_by_day_content.dart';
class ChartCountsByDayCard extends StatelessWidget {
const ChartCountsByDayCard({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = 21;
late Future<CountsByDayData> futureCountsByDay = ScrobblesApi.fetchCountsByDay(daysCount);
return FutureBuilder<CountsByDayData>(
future: futureCountsByDay,
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: ChartCountsByDayCardContent(
daysCount: daysCount,
chartData: snapshot.hasData
? CountsByDayData.fromJson(jsonDecode(snapshot.data.toString()))
: CountsByDayData.createEmpty(),
isLoading: !snapshot.hasData,
),
),
);
},
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../config/app_colors.dart';
import '../../../models/counts_by_day.dart';
import '../../../utils/color_extensions.dart';
class CountsByDayCardContentChart extends StatelessWidget {
final CountsByDayData chartData;
const CountsByDayCardContentChart({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(), 10),
),
);
}),
);
}
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) {
Color barColor = AppColors.contentColorBlack;
switch (day) {
case 1:
barColor = Color.fromARGB(255, 255, 99, 132);
break;
case 2:
barColor = Color.fromARGB(255, 255, 159, 64);
break;
case 3:
barColor = Color.fromARGB(255, 255, 205, 86);
break;
case 4:
barColor = Color.fromARGB(255, 75, 192, 192);
break;
case 5:
barColor = Color.fromARGB(255, 54, 162, 235);
break;
case 6:
barColor = Color.fromARGB(255, 153, 102, 255);
break;
case 7:
barColor = Color.fromARGB(255, 201, 203, 207);
break;
default:
}
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,
);
}
FlTitlesData getTitlesData() {
const AxisTitles none = const AxisTitles(
sideTitles: SideTitles(showTitles: false),
);
final AxisTitles verticalTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: getVerticalTitlesWidget,
interval: 10,
),
);
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) {
final int day = value.toInt();
String dayShortName = '';
switch (day) {
case 1:
dayShortName = 'MON';
break;
case 2:
dayShortName = 'TUE';
break;
case 3:
dayShortName = 'WED';
break;
case 4:
dayShortName = 'THU';
break;
case 5:
dayShortName = 'FRI';
break;
case 6:
dayShortName = 'SAT';
break;
case 7:
dayShortName = 'SUN';
break;
default:
}
final String text = tr(dayShortName);
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_day.dart';
import '../../../ui/widgets/main_screen/counts_by_day_chart.dart';
class ChartCountsByDayCardContent extends StatelessWidget {
final int daysCount;
final CountsByDayData chartData;
final bool isLoading;
const ChartCountsByDayCardContent(
{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_day',
style: textTheme.titleLarge!.apply(fontWeightDelta: 2),
).tr(
namedArgs: {
'daysCount': this.daysCount.toString(),
},
),
const SizedBox(height: 8),
this.isLoading
? Text(placeholder)
: CountsByDayCardContentChart(
chartData: CountsByDayData.fromJson(jsonDecode(this.chartData.toString())),
),
],
);
}
}
......@@ -3,7 +3,7 @@ description: Display scrobbles data and charts
publish_to: 'none'
version: 0.0.8+8
version: 0.0.9+9
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