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

Merge branch '33-add-top-artists-streamline' into 'master'

Resolve "Add "top artists" streamline"

Closes #33

See merge request !39
parents 5a81060e 7d9aca6a
No related branches found
No related tags found
1 merge request!39Resolve "Add "top artists" streamline"
Pipeline #4675 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.37 app.versionName=0.0.38
app.versionCode=37 app.versionCode=38
Add top artists streamline chart.
Ajout graphique d'évolution du "top artistes".
...@@ -14,15 +14,46 @@ class TopArtistsDataValue { ...@@ -14,15 +14,46 @@ class TopArtistsDataValue {
} }
} }
class TopArtistsStreamDataValue {
final String artistName;
final double value;
const TopArtistsStreamDataValue({
required this.artistName,
required this.value,
});
factory TopArtistsStreamDataValue.fromJson(Map<String, dynamic>? json) {
return TopArtistsStreamDataValue(
artistName: json?['artistName'] as String,
value: json?['value'] as double,
);
}
Map<String, dynamic>? toJson() {
return {
artistName: value,
};
}
@override
String toString() {
return jsonEncode(this.toJson());
}
}
class TopArtistsData { class TopArtistsData {
final List<TopArtistsDataValue> topArtists; final List<TopArtistsDataValue> topArtists;
final Map<String, List<TopArtistsStreamDataValue>> topArtistsStream;
const TopArtistsData({ const TopArtistsData({
required this.topArtists, required this.topArtists,
required this.topArtistsStream,
}); });
factory TopArtistsData.fromJson(Map<String, dynamic>? json) { factory TopArtistsData.fromJson(Map<String, dynamic>? json) {
List<TopArtistsDataValue> topArtists = []; List<TopArtistsDataValue> topArtists = [];
Map<String, List<TopArtistsStreamDataValue>> topArtistsStream = {};
json?['top-artists'].forEach((element) { json?['top-artists'].forEach((element) {
TopArtistsDataValue value = TopArtistsDataValue( TopArtistsDataValue value = TopArtistsDataValue(
...@@ -33,13 +64,39 @@ class TopArtistsData { ...@@ -33,13 +64,39 @@ class TopArtistsData {
topArtists.add(value); topArtists.add(value);
}); });
json?['top-artists-stream-by-date'].keys.forEach((date) {
if (json['top-artists-stream-by-date'][date] is Map<String, dynamic>) {
Map<String, dynamic> content = json['top-artists-stream-by-date'][date];
List<TopArtistsStreamDataValue> items = [];
content.forEach((String artistName, dynamic rawValue) {
double value = 0.0;
if (rawValue is double) {
value = rawValue;
} else if (rawValue is int) {
value = rawValue.toDouble();
}
TopArtistsStreamDataValue item = TopArtistsStreamDataValue(
artistName: artistName,
value: value,
);
items.add(item);
});
topArtistsStream[date] = items;
}
});
return TopArtistsData( return TopArtistsData(
topArtists: topArtists, topArtists: topArtists,
topArtistsStream: topArtistsStream,
); );
} }
Map<String, Object?>? toJson() { Map<String, Object?>? toJson() {
List<Map<String, Object>> listArtists = []; List<Map<String, Object>> listArtists = [];
Map<String, List<Map<String, double>>> artistsStreamMap = {};
this.topArtists.forEach((TopArtistsDataValue? item) { this.topArtists.forEach((TopArtistsDataValue? item) {
listArtists.add({ listArtists.add({
...@@ -48,8 +105,20 @@ class TopArtistsData { ...@@ -48,8 +105,20 @@ class TopArtistsData {
}); });
}); });
this.topArtistsStream.keys.forEach((dateAsString) {
List<TopArtistsStreamDataValue>? items = this.topArtistsStream[dateAsString];
List<Map<String, double>> values = [];
items?.forEach((item) {
values.add({
item.artistName: item.value,
});
});
artistsStreamMap[dateAsString] = values;
});
return { return {
'top-artists': listArtists, 'top-artists': listArtists,
'top-artists-stream-by-date': artistsStreamMap,
}; };
} }
......
...@@ -11,7 +11,7 @@ class CustomBarChart extends StatelessWidget { ...@@ -11,7 +11,7 @@ class CustomBarChart extends StatelessWidget {
final double chartHeight = 150.0; final double chartHeight = 150.0;
final double verticalTicksInterval = 10; final double verticalTicksInterval = 10;
final String verticalAxisTitleSuffix = ''; final String verticalAxisTitleSuffix = '';
final double titleFontSize = 10; final double titleFontSize = 9;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
......
...@@ -7,7 +7,7 @@ class CustomLineChart extends StatelessWidget { ...@@ -7,7 +7,7 @@ class CustomLineChart extends StatelessWidget {
const CustomLineChart({super.key}); const CustomLineChart({super.key});
final double chartHeight = 150.0; final double chartHeight = 150.0;
final double titleFontSize = 10; final double titleFontSize = 9;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
......
...@@ -8,6 +8,7 @@ import 'package:scrobbles/models/topartists.dart'; ...@@ -8,6 +8,7 @@ import 'package:scrobbles/models/topartists.dart';
import 'package:scrobbles/network/scrobbles.dart'; import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart'; import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/top_artists.dart'; import 'package:scrobbles/ui/widgets/charts/top_artists.dart';
import 'package:scrobbles/ui/widgets/charts/top_artists_stream.dart';
import 'package:scrobbles/ui/widgets/error.dart'; import 'package:scrobbles/ui/widgets/error.dart';
class CardTopArtists extends StatelessWidget { class CardTopArtists extends StatelessWidget {
...@@ -21,6 +22,8 @@ class CardTopArtists extends StatelessWidget { ...@@ -21,6 +22,8 @@ class CardTopArtists extends StatelessWidget {
return BlocBuilder<DataTopArtistsCubit, DataTopArtistsState>( return BlocBuilder<DataTopArtistsCubit, DataTopArtistsState>(
builder: (BuildContext context, DataTopArtistsState state) { builder: (BuildContext context, DataTopArtistsState state) {
TopArtistsData artistsData = state.topArtists ?? TopArtistsData.fromJson({});
return CardContent( return CardContent(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
title: 'top_artists_title'.tr( title: 'top_artists_title'.tr(
...@@ -29,8 +32,18 @@ class CardTopArtists extends StatelessWidget { ...@@ -29,8 +32,18 @@ class CardTopArtists extends StatelessWidget {
}, },
), ),
loader: updateTopArtists(daysCount), loader: updateTopArtists(daysCount),
content: ChartTopArtists( content: Column(
chartData: TopArtistsData.fromJson(state.topArtists?.toJson()), mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ChartTopArtists(
chartData: artistsData,
),
const SizedBox(height: 8),
ChartTopArtistsStream(
chartData: artistsData,
),
],
), ),
); );
}, },
......
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:scrobbles/config/app_colors.dart';
import 'package:scrobbles/models/topartists.dart';
import 'package:scrobbles/ui/widgets/abstracts/custom_line_chart.dart';
import 'package:scrobbles/utils/color_extensions.dart';
class ChartTopArtistsStream extends CustomLineChart {
final TopArtistsData chartData;
const ChartTopArtistsStream({super.key, required this.chartData});
final double verticalTicksInterval = 10;
@override
Widget build(BuildContext context) {
final horizontalScale = getHorizontalScale();
if (this.chartData.topArtistsStream.keys.isEmpty) {
return SizedBox(
height: this.chartHeight,
);
}
return Container(
height: this.chartHeight,
child: LineChart(
LineChartData(
lineBarsData: getDataStreamLine(),
betweenBarsData: getBetweenBarsData(),
borderData: getBorderData(),
gridData: getGridData(),
titlesData: getTitlesData(),
lineTouchData: const LineTouchData(enabled: false),
minX: horizontalScale['min'],
maxX: horizontalScale['max'],
maxY: getNextRoundNumber(getMaxVerticalValue(), this.verticalTicksInterval),
minY: 0,
),
duration: const Duration(milliseconds: 250),
),
);
}
double getNextRoundNumber(double number, double scale) {
return scale * ((number ~/ scale).toInt() + 1);
}
FlGridData getGridData() {
return const FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
);
}
double getMaxVerticalValue() {
double maxValue = 0;
this.chartData.topArtistsStream.keys.forEach((dateAsString) {
double totalValue = 0.0;
List<TopArtistsStreamDataValue> artists =
this.chartData.topArtistsStream[dateAsString] ?? [];
artists.forEach((artist) {
final double value = artist.value;
totalValue = totalValue + value;
});
if (totalValue > maxValue) {
maxValue = totalValue;
}
});
return maxValue;
}
Map<String, double> getHorizontalScale() {
double minDateAsDouble = double.maxFinite;
double maxDateAsDouble = -double.maxFinite;
this.chartData.topArtistsStream.keys.forEach((dateAsString) {
final double date = DateTime.parse(dateAsString).millisecondsSinceEpoch.toDouble();
if (date < minDateAsDouble) {
minDateAsDouble = date;
}
if (date > maxDateAsDouble) {
maxDateAsDouble = date;
}
});
return {
'min': minDateAsDouble,
'max': maxDateAsDouble,
};
}
Color getColorIndex(int index) {
const List<int> hexValues = [
0x8dd3c7,
0xffffb3,
0xbebada,
0xfb8072,
0x80b1d3,
0xfdb462,
0xb3de69,
0xfccde5,
0xd9d9d9,
0xbc80bd,
0xccebc5,
0xffed6f,
];
return Color(hexValues[index % hexValues.length] + 0xff000000);
}
List<LineChartBarData> getDataStreamLine() {
int artistsCount =
this.chartData.topArtistsStream[this.chartData.topArtistsStream.keys.first]?.length ??
0;
List<LineChartBarData> lines = [];
LineChartBarData getZeroHorizontalLine() {
final baseColor = getColorIndex(0);
final borderColor = baseColor.darken(20);
List<FlSpot> spots = [];
this.chartData.topArtistsStream.keys.forEach((dateAsString) {
final double date = DateTime.parse(dateAsString).millisecondsSinceEpoch.toDouble();
spots.add(FlSpot(date, 0));
});
return LineChartBarData(
color: borderColor,
dotData: const FlDotData(show: false),
spots: spots,
);
}
// First horizontal "zero" line
lines.add(getZeroHorizontalLine());
LineChartBarData getLinesFromIndex(int index) {
final baseColor = getColorIndex(index);
final borderColor = baseColor.darken(20);
List<FlSpot> spots = [];
this.chartData.topArtistsStream.keys.forEach((dateAsString) {
final double date = DateTime.parse(dateAsString).millisecondsSinceEpoch.toDouble();
List<TopArtistsStreamDataValue> artists =
this.chartData.topArtistsStream[dateAsString] ?? [];
double value = 0;
for (int i = 0; i <= index; i++) {
value = value + artists[i].value;
}
spots.add(FlSpot(date, value));
});
return LineChartBarData(
isCurved: true,
curveSmoothness: 0.25,
color: borderColor,
dotData: const FlDotData(show: false),
spots: spots,
);
}
Iterable<int>.generate(artistsCount)
.toList()
.map((index) => lines.add(getLinesFromIndex(index)))
.toList();
return lines;
}
List<BetweenBarsData> getBetweenBarsData() {
int artistsCount =
this.chartData.topArtistsStream[this.chartData.topArtistsStream.keys.first]?.length ??
0;
return Iterable<int>.generate(artistsCount)
.toList()
.map((index) => BetweenBarsData(
fromIndex: index,
toIndex: index + 1,
color: getColorIndex(index),
))
.toList();
}
Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
final DateFormat formatter = DateFormat('dd/MM');
final DateTime date = DateTime.fromMillisecondsSinceEpoch(value.toInt());
final String text = formatter.format(date);
return SideTitleWidget(
axisSide: meta.axisSide,
space: 4,
child: Padding(
padding: EdgeInsets.only(right: 10),
child: RotationTransition(
turns: new AlwaysStoppedAnimation(-30 / 360),
child: Text(
text,
style: TextStyle(
color: AppColors.mainTextColor1,
fontSize: this.titleFontSize,
),
),
),
),
);
}
}
...@@ -8,6 +8,8 @@ class ShowErrorWidget extends StatelessWidget { ...@@ -8,6 +8,8 @@ class ShowErrorWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
print(message);
return Text( return Text(
'⚠️ ' + tr(message), '⚠️ ' + tr(message),
textAlign: TextAlign.start, textAlign: TextAlign.start,
......
...@@ -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.37+37 version: 0.0.38+38
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