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

Merge branch '16-add-scrobbles-hour-chart' into 'master'

Resolve "Add "scrobbles / hour" chart"

Closes #16

See merge request !10
parents 919fd097 3d4aa3d8
No related branches found
No related tags found
1 merge request!10Resolve "Add "scrobbles / hour" chart"
Pipeline #4462 passed
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
app.versionName=0.0.9 app.versionName=0.0.10
app.versionCode=9 app.versionCode=10
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
"timeline_title": "Recent scrobbles ({daysCount} days)", "timeline_title": "Recent scrobbles ({daysCount} days)",
"counts_by_day": "Counts by day ({daysCount} days)", "counts_by_day": "Counts by day ({daysCount} days)",
"counts_by_hour": "Counts by hour ({daysCount} days)",
"MON": "MON", "MON": "MON",
"TUE": "TUE", "TUE": "TUE",
"WED": "WED", "WED": "WED",
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
"timeline_title": "Écoutes récentes ({daysCount} jours)", "timeline_title": "Écoutes récentes ({daysCount} jours)",
"counts_by_day": "Écoutes par jour ({daysCount} jours)", "counts_by_day": "Écoutes par jour ({daysCount} jours)",
"counts_by_hour": "Écoutes par heure ({daysCount} jours)",
"MON": "LUN", "MON": "LUN",
"TUE": "MAR", "TUE": "MAR",
"WED": "MER", "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'; ...@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../models/counts_by_day.dart'; import '../models/counts_by_day.dart';
import '../models/counts_by_hour.dart';
import '../models/statistics.dart'; import '../models/statistics.dart';
import '../models/timeline.dart'; import '../models/timeline.dart';
...@@ -39,4 +40,15 @@ class ScrobblesApi { ...@@ -39,4 +40,15 @@ class ScrobblesApi {
throw Exception('Failed to get data from API.'); 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'; ...@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../widgets/header.dart'; import '../widgets/header.dart';
import '../widgets/main_screen/counts_by_day_card.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/statistics_card.dart';
import '../widgets/main_screen/timeline_card.dart'; import '../widgets/main_screen/timeline_card.dart';
...@@ -22,6 +23,8 @@ class MainScreen extends StatelessWidget { ...@@ -22,6 +23,8 @@ class MainScreen extends StatelessWidget {
const ChartTimelineCard(), const ChartTimelineCard(),
const SizedBox(height: 8), const SizedBox(height: 8),
const ChartCountsByDayCard(), const ChartCountsByDayCard(),
const SizedBox(height: 8),
const ChartCountsByHourCard(),
const SizedBox(height: 36), 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 ...@@ -3,7 +3,7 @@ description: Display scrobbles data and charts
publish_to: 'none' publish_to: 'none'
version: 0.0.9+9 version: 0.0.10+10
environment: environment:
sdk: '^3.0.0' sdk: '^3.0.0'
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment