diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index ecaab91..15457b3 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -42,7 +42,7 @@ const Map rankCutoffs = { "": 0.5 }; const Map rankTargets = { - "x": 24008, + "x": 24503.75, // where that comes from? "u": 23038, "ss": 21583, "s+": 20128, diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index a129592..a6842f9 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -185,15 +185,17 @@ class _MainState extends State with TickerProviderStateMixin { topTR = requests.elementAtOrNull(3) as double?; // No TR - no Top TR meAmongEveryone = teto.getCachedLeaderboardPositions(me.userId); - if (meAmongEveryone == null && prefs.getBool("showPositions") == true){ + if (prefs.getBool("showPositions") == true){ // Get tetra League leaderboard everyone = teto.getCachedLeaderboard(); everyone ??= await teto.fetchTLLeaderboard(); - meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); - if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); - if (me.tlSeason1.rank != "z") { - thatRankCutoff = everyone!.cutoffs[me.tlSeason1.rank]; - nextRankCutoff = everyone!.cutoffs[ranks.indexOf(me.tlSeason1.rank)+1]; + if (meAmongEveryone == null){ + meAmongEveryone = await compute(everyone!.getLeaderboardPosition, me); + if (meAmongEveryone != null) teto.cacheLeaderboardPositions(me.userId, meAmongEveryone!); + } + if (me.tlSeason1.gamesPlayed > 9) { + thatRankCutoff = everyone!.cutoffs[me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank]; + nextRankCutoff = everyone!.cutoffs[ranks.elementAtOrNull(ranks.indexOf(me.tlSeason1.rank != "z" ? me.tlSeason1.rank : me.tlSeason1.percentileRank)+1)]; nextRankCutoff = nextRankCutoff??25000; } } @@ -515,7 +517,7 @@ class _MainState extends State with TickerProviderStateMixin { thatRankCutoff: thatRankCutoff, thatRankTarget: snapshot.data![0].tlSeason1.rank != "z" ? rankTargets[snapshot.data![0].tlSeason1.rank] : null, nextRankCutoff: nextRankCutoff, - nextRankTarget: snapshot.data![0].tlSeason1.rank != "z" || snapshot.data![0].tlSeason1.rank != "x" ? rankTargets[ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1] : null, + nextRankTarget: snapshot.data![0].tlSeason1.rank != "z" || snapshot.data![0].tlSeason1.rank != "x" ? rankTargets[ranks.elementAtOrNull(ranks.indexOf(snapshot.data![0].tlSeason1.rank)+1)] : null, averages: rankAverages, lbPositions: meAmongEveryone ), diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index 075c7ca..12cfde1 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'dart:math'; +import 'dart:ui'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -10,10 +11,13 @@ import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/views/main_view.dart' show MainView; import 'package:tetra_stats/utils/text_shadow.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:syncfusion_flutter_charts/sparkcharts.dart'; var _chartsShortTitlesDropdowns = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value),)]; Stats _chartsX = Stats.tr; Stats _chartsY = Stats.apm; +late TooltipBehavior _tooltipBehavior; List _itemStats = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value))]; List<_MyScatterSpot> _spots = []; Stats _sortBy = Stats.tr; @@ -49,7 +53,7 @@ class RankState extends State with SingleTickerProviderStateMixin { late double yScale; String headerTooltip = t.pseudoTooltipHeaderInit; String footerTooltip = t.pseudoTooltipFooterInit; - int hoveredPointId = -1; + ValueNotifier hoveredPointId = ValueNotifier(-1); double scaleFactor = 5e2; double dragFactor = 7e2; @@ -57,6 +61,32 @@ class RankState extends State with SingleTickerProviderStateMixin { void initState() { _scrollController = ScrollController(); _tabController = TabController(length: 6, vsync: this); + _tooltipBehavior = TooltipBehavior( + enable: true, + animationDuration: 0, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "${data.nickname} (${data.rank.toUpperCase()})", + style: TextStyle(color: Colors.black, fontFamily: "Eurostile Round Extended", fontSize: 20), + ), + ), + Text( + '${_f4.format(data.x)} ${chartsShortTitles[_chartsX]}\n${_f4.format(data.y)} ${chartsShortTitles[_chartsY]}', + style: TextStyle(color: Colors.black), + ), + ], + ), + ); + } + ); if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => _oldWindowTitle = value); windowManager.setTitle("Tetra Stats: ${widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())}"); @@ -113,7 +143,9 @@ class RankState extends State with SingleTickerProviderStateMixin { entry.getStatByEnum(_chartsY).toDouble(), entry.userId, entry.username, - dotPainter: FlDotCirclePainter(color: rankColors[entry.rank]??Colors.white, radius: 3)) + entry.rank, + rankColors[entry.rank]??Colors.white + ) ]; } @@ -328,60 +360,19 @@ class RankState extends State with SingleTickerProviderStateMixin { recalculateScales(); }); }, - // TODO: Figure out wtf is going on with gestures child: Padding( padding: bigScreen ? const EdgeInsets.fromLTRB(40, 40, 40, 48) : const EdgeInsets.fromLTRB(0, 40, 16, 48), - child: Stack( - children: [ - ScatterChart( - key: graphKey, - ScatterChartData( - minX: minX, - maxX: maxX, - minY: minY, - maxY: maxY, - clipData: const FlClipData.all(), - scatterSpots: _spots, - scatterTouchData: ScatterTouchData( - handleBuiltInTouches: false, - touchCallback:(touchEvent, touchResponse) { - if (touchEvent is FlPanUpdateEvent){ - dragHandler(touchEvent.details); - return; - } - if (touchEvent is FlPointerHoverEvent){ - setState(() { - if (touchResponse?.touchedSpot == null) { - hoveredPointId = -1; - } else { - hoveredPointId = touchResponse!.touchedSpot!.spotIndex; - _MyScatterSpot castedPoint = touchResponse.touchedSpot!.spot as _MyScatterSpot; - headerTooltip = castedPoint.nickname; - footerTooltip = "${_f4.format(castedPoint.x)} ${chartsShortTitles[_chartsX]}; ${_f4.format(castedPoint.y)} ${chartsShortTitles[_chartsY]}"; - } - }); - } - if (touchEvent is FlPointerExitEvent){ - setState(() {hoveredPointId = -1;}); - } - if (touchEvent is FlTapUpEvent && touchResponse?.touchedSpot?.spot != null){ - _MyScatterSpot spot = touchResponse!.touchedSpot!.spot as _MyScatterSpot; - Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: spot.nickname), maintainState: false)); - } - }, - ), - ), - swapAnimationDuration: const Duration(milliseconds: 150), // Optional - swapAnimationCurve: Curves.linear, // Optional - ), - Padding( - padding: EdgeInsets.fromLTRB(graphStartX+8, padding.top/2+8, 0, 0), - child: Column( - children: [ - AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 24, color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(headerTooltip)), - AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round", color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(footerTooltip)), - ], - ), + child: SfCartesianChart( + tooltipBehavior: _tooltipBehavior, + //primaryXAxis: CategoryAxis(), + series: [ + ScatterSeries( + enableTooltip: true, + dataSource: _spots, + animationDuration: 0, + pointColorMapper: (data, _) => data.color, + xValueMapper: (data, _) => data.x, + yValueMapper: (data, _) => data.y ) ], ), @@ -642,10 +633,12 @@ class _ListEntry extends StatelessWidget { } } -class _MyScatterSpot extends ScatterSpot { +class _MyScatterSpot{ + num x; + num y; String id; String nickname; - //Color color; - //FlDotPainter painter = FlDotCirclePainter(color: color, radius: 2); - _MyScatterSpot(super.x, super.y, this.id, this.nickname, {super.dotPainter}); + String rank; + Color color; + _MyScatterSpot(this.x, this.y, this.id, this.nickname, this.rank, this.color); } diff --git a/lib/widgets/tl_progress_bar.dart b/lib/widgets/tl_progress_bar.dart index aa183db..3366d74 100644 --- a/lib/widgets/tl_progress_bar.dart +++ b/lib/widgets/tl_progress_bar.dart @@ -1,8 +1,11 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/utils/numers_formats.dart'; class TLProgress extends StatelessWidget{ final double tr; @@ -20,16 +23,15 @@ class TLProgress extends StatelessWidget{ const TLProgress({super.key, required this.tr, required this.rank, required this.position, required this.nextRankPosition, required this.previousRankPosition, this.nextRank, this.previousRank, this.nextRankTRcutoff, this.previousRankTRcutoff, this.nextRankTRcutoffTarget, this.previousRankTRcutoffTarget}); double getBarPosition(){ - return 1 - (position - nextRankPosition)/(previousRankPosition - nextRankPosition); + return min(max(0, 1 - (position - nextRankPosition)/(previousRankPosition - nextRankPosition)), 1); } - double? getBarTR(){ - return null; + double? getBarTR(double tr){ + return min(max(0, (tr - previousRankTRcutoff!)/(nextRankTRcutoff! - previousRankTRcutoff!)), 1); } @override Widget build(BuildContext context) { - print(getBarPosition()); // return Container( // alignment: Alignment.centerLeft, // height: 50, @@ -47,11 +49,27 @@ class TLProgress extends StatelessWidget{ minimum: 0, maximum: 1, interval: 1, - ranges: [LinearGaugeRange(endValue: getBarPosition(), color: Colors.cyanAccent,)], - markerPointers: [LinearShapePointer(value: getBarPosition(), position: LinearElementPosition.inside, shapeType: LinearShapePointerType.triangle, color: Colors.white, height: 20), - LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: getBarPosition(), child: Text(NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(position)))], + ranges: [ + if (previousRankTRcutoff != null && nextRankTRcutoff != null) LinearGaugeRange(endValue: getBarTR(tr)!, color: Colors.cyanAccent, position: LinearElementPosition.cross), + if (position != -1) LinearGaugeRange(endValue: getBarPosition(), color: Colors.cyanAccent, position: LinearElementPosition.cross), + if (previousRankTRcutoff != null && previousRankTRcutoffTarget != null) LinearGaugeRange(endValue: getBarTR(previousRankTRcutoffTarget!)!, color: Colors.greenAccent, position: LinearElementPosition.inside), + if (nextRankTRcutoff != null && nextRankTRcutoffTarget != null) LinearGaugeRange(startValue: getBarTR(nextRankTRcutoffTarget!)!, endValue: 1, color: Colors.yellowAccent, position: LinearElementPosition.inside) + ], + markerPointers: [ + LinearShapePointer(value: getBarPosition(), position: LinearElementPosition.cross, shapeType: LinearShapePointerType.diamond, color: Colors.white, height: 20), + LinearWidgetPointer(offset: 4, position: LinearElementPosition.outside, value: getBarPosition(), child: Text("№ ${NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 0).format(position)}")) + ], isMirrored: true, showTicks: true, + axisLabelStyle: TextStyle(), + onGenerateLabels: () => [ + LinearAxisLabel(text: "№ ${f0.format(previousRankPosition)}\n ${intf.format(previousRankTRcutoff)} TR", value: 0), + LinearAxisLabel(text: "№ ${f0.format(nextRankPosition)}\n ${intf.format(nextRankTRcutoff)} TR", value: 1), + ], + // labelFormatterCallback: (value) { + // if (value == "0") return "${f0.format(previousRankPosition)}\n 26,700 TR"; + // else return f0.format(nextRankPosition); + // }, showLabels: true ) ); diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 97a02b3..b90f314 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -140,7 +140,8 @@ class _TLThingyState extends State { ), ], ), - if (currentTl.gamesPlayed >= 10 && currentTl.rd! < 100 && currentTl.nextAt >=0 && currentTl.prevAt >= 0) + if (currentTl.gamesPlayed > 9) + // if (currentTl.gamesPlayed >= 10 && currentTl.rd! < 100 && currentTl.nextAt >=0 && currentTl.prevAt >= 0) // Padding( // padding: const EdgeInsets.all(8.0), // child: SfLinearGauge( @@ -161,7 +162,7 @@ class _TLThingyState extends State { // ), TLProgress( tr: currentTl.rating, - rank: currentTl.rank, + rank: currentTl.rank != "z" ? currentTl.rank : currentTl.percentileRank, position: currentTl.standing, nextRankPosition: currentTl.nextAt, previousRankPosition: currentTl.prevAt, diff --git a/pubspec.lock b/pubspec.lock index ff532e5..b4e8059 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -866,6 +866,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + syncfusion_flutter_charts: + dependency: "direct main" + description: + name: syncfusion_flutter_charts + sha256: ab73109c586f5ec2b01adc2672026a1fb3f93b2b5f6061ba8d7126c119061002 + url: "https://pub.dev" + source: hosted + version: "24.2.9" syncfusion_flutter_core: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index acb5c88..79ace8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: flutter_markdown: ^0.6.18 flutter_colorpicker: ^1.0.3 go_router: ^13.0.0 + syncfusion_flutter_charts: ^24.2.9 dev_dependencies: flutter_test: