Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/troubleshooting/import-build-fails.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ Several users have reported unexpected "JSON Syntax" errors when importing or bu
* https://github.com/robiningelbrecht/statistics-for-strava/issues/1288
* https://github.com/robiningelbrecht/statistics-for-strava/issues/1180

![Import error](../assets/images/troubleshoot-import-error.png)
![Import error](../assets/images/troubleshoot-import-error.png ':size=700')

![Build error](../assets/images/troubleshoot-build-error.png)
![Build error](../assets/images/troubleshoot-build-error.png ':size=700')

All of these issues share the same root cause: at some point, data became corrupted.
Unfortunately, we still don’t know exactly how or why this corruption occurs.
Expand Down
2 changes: 1 addition & 1 deletion public/css/dist/tailwind.min.css

Large diffs are not rendered by default.

28 changes: 25 additions & 3 deletions public/css/tailwind.output.css
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,9 @@
.me-3 {
margin-inline-end: calc(var(--spacing) * 3);
}
.-mt-0\.5 {
margin-top: calc(var(--spacing) * -0.5);
}
.mt-1 {
margin-top: calc(var(--spacing) * 1);
}
Expand Down Expand Up @@ -2239,6 +2242,9 @@
border-color: var(--color-gray-200);
}
}
.self-start {
align-self: flex-start;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
Expand Down Expand Up @@ -2596,9 +2602,6 @@
.bg-\[\#89D7D2\] {
background-color: #89D7D2;
}
.bg-\[\#F26722\] {
background-color: #F26722;
}
.bg-black\/0 {
background-color: color-mix(in srgb, #000000 0%, transparent);
@supports (color: color-mix(in lab, red, red)) {
Expand Down Expand Up @@ -3228,6 +3231,10 @@
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-sm {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
Expand Down Expand Up @@ -3399,6 +3406,16 @@
color: var(--color-strava-orange);
}
}
.group-\[\.fullscreen-is-enabled\]\:h-full {
&:is(:where(.group):is(.fullscreen-is-enabled) *) {
height: 100%;
}
}
.group-\[\.fullscreen-is-enabled\]\:grow {
&:is(:where(.group):is(.fullscreen-is-enabled) *) {
flex-grow: 1;
}
}
.group-\[\.sidebar-is-collapsed\]\:block {
&:is(:where(.group):is(.sidebar-is-collapsed) *) {
display: block;
Expand Down Expand Up @@ -4378,6 +4395,11 @@
background-color: var(--color-gray-100);
}
}
.\[\&\.full-screen-enabled\]\:hidden {
&.full-screen-enabled {
display: none;
}
}
.\[\&\.sidebar-is-collapsed\]\:w-32 {
&.sidebar-is-collapsed {
width: calc(var(--spacing) * 32);
Expand Down
3 changes: 3 additions & 0 deletions public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import MapManager from "./ui/maps";
import TabsManager from "./ui/tabs";
import LazyLoad from "../libraries/lazyload.min";
import DataTableManager from "./ui/data-tables";
import FullscreenManager from "./fullscreen";

const $main = document.querySelector("main");
const dataTableStorage = new DataTableStorage();
Expand All @@ -25,6 +26,7 @@ const chartManager = new ChartManager(router, dataTableStorage, modalManager);
const mapManager = new MapManager();
const tabsManager = new TabsManager(chartManager);
const dataTableManager = new DataTableManager(dataTableStorage);
const fullscreenManager = new FullscreenManager(chartManager);
const lazyLoad = new LazyLoad({
thresholds: "50px",
callback_error: (img) => {
Expand All @@ -44,6 +46,7 @@ const initElements = (rootNode) => {
modalManager.init(rootNode);
chartManager.init(rootNode);
mapManager.init(rootNode);
fullscreenManager.init(rootNode);
}

modalManager.setInitElements(initElements)
Expand Down
2 changes: 1 addition & 1 deletion public/js/dist/app.min.js

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions public/js/fullscreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export default class FullscreenManager {
constructor(chartManager) {
this.chartManager = chartManager;
}

init(rootNode) {
rootNode.querySelectorAll('[data-fullscreen-trigger]').forEach((el) => {
el.addEventListener('click', (e) => {
e.preventDefault();

if (document.fullscreenElement) {
return;
}

const fullScreenContent = el.closest('[data-fullscreen-content]');
fullScreenContent.requestFullscreen().then(() => {
this.chartManager.resizeAll();
});

fullScreenContent.addEventListener('fullscreenchange', () => {
el.classList.toggle(
'hidden',
Boolean(document.fullscreenElement)
);
fullScreenContent.classList.toggle(
'fullscreen-is-enabled',
Boolean(document.fullscreenElement)
);
fullScreenContent.classList.toggle(
'group',
Boolean(document.fullscreenElement)
);
});

});
});
}
}
23 changes: 19 additions & 4 deletions src/Domain/Activity/ActivityIntensity.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

final class ActivityIntensity
{
public const int HIGH_THRESHOLD_VALUE = 67;

/** @var array<string, int|null> */
public static array $cachedIntensities = [];

Expand Down Expand Up @@ -46,8 +48,13 @@ public function calculateForDate(SerializableDateTime $on): int
return self::$cachedIntensities[$cacheKey];
}

private function calculateForActivity(Activity $activity): ?int
public function calculateForActivity(Activity $activity): int
{
$cacheKey = (string) $activity->getId();
if (array_key_exists($cacheKey, self::$cachedIntensities) && null !== self::$cachedIntensities[$cacheKey]) {
return self::$cachedIntensities[$cacheKey];
}

if (ActivityType::RIDE === $activity->getSportType()->getActivityType()) {
try {
// To calculate intensity, we need
Expand All @@ -59,7 +66,10 @@ private function calculateForActivity(Activity $activity): ?int
// Use more complicated and more accurate calculation.
// intensityFactor = averagePower / FTP
// (durationInSeconds * averagePower * intensityFactor) / (FTP x 3600) * 100
return (int) round(($activity->getMovingTimeInSeconds() * $averagePower * ($averagePower / $ftp->getValue())) / ($ftp->getValue() * 3600) * 100);
$intensity = (int) round(($activity->getMovingTimeInSeconds() * $averagePower * ($averagePower / $ftp->getValue())) / ($ftp->getValue() * 3600) * 100);
self::$cachedIntensities[$cacheKey] = $intensity;

return self::$cachedIntensities[$cacheKey];
}
} catch (EntityNotFound) {
}
Expand All @@ -74,9 +84,14 @@ private function calculateForActivity(Activity $activity): ?int
// (durationInSeconds x averageHeartRate x intensityFactor) / (maxHeartRate x 3600) x 100
$maxHeartRate = round($athleteMaxHeartRate * 0.92);

return (int) round(($activity->getMovingTimeInSeconds() * $averageHeartRate * ($averageHeartRate / $maxHeartRate)) / ($maxHeartRate * 3600) * 100);
$intensity = (int) round(($activity->getMovingTimeInSeconds() * $averageHeartRate * ($averageHeartRate / $maxHeartRate)) / ($maxHeartRate * 3600) * 100);
self::$cachedIntensities[$cacheKey] = $intensity;

return self::$cachedIntensities[$cacheKey];
}

return null;
self::$cachedIntensities[$cacheKey] = 0;

return 0;
}
}
16 changes: 8 additions & 8 deletions src/Domain/Dashboard/DashboardLayout.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,32 @@ public function getIterator(): \Traversable
/**
* @return list<array{widget: string, width: int, enabled: bool}>
*/
private static function default(): array
public static function default(): array
{
return [
['widget' => 'mostRecentActivities', 'width' => 66, 'enabled' => true, 'config' => ['numberOfActivitiesToDisplay' => 5]],
['widget' => 'introText', 'width' => 33, 'enabled' => true],
['widget' => 'trainingGoals', 'width' => 33, 'enabled' => false, 'config' => ['goals' => []]],
['widget' => 'weeklyStats', 'width' => 100, 'enabled' => true, 'config' => ['metricsDisplayOrder' => ['distance', 'movingTime', 'elevation']]],
['widget' => 'activityGrid', 'width' => 100, 'enabled' => true],
['widget' => 'streaks', 'width' => 33, 'enabled' => true],
['widget' => 'athleteProfile', 'width' => 33, 'enabled' => true],
['widget' => 'eddington', 'width' => 33, 'enabled' => true],
['widget' => 'peakPowerOutputs', 'width' => 50, 'enabled' => true],
['widget' => 'heartRateZones', 'width' => 50, 'enabled' => true],
['widget' => 'activityGrid', 'width' => 100, 'enabled' => true],
['widget' => 'monthlyStats', 'width' => 100, 'enabled' => true, 'config' => [
'enableLastXYearsByDefault' => 10, 'metricsDisplayOrder' => ['distance', 'movingTime', 'elevation'],
]],
['widget' => 'trainingLoad', 'width' => 100, 'enabled' => true],
['widget' => 'weekdayStats', 'width' => 50, 'enabled' => true],
['widget' => 'dayTimeStats', 'width' => 50, 'enabled' => true],
['widget' => 'distanceBreakdown', 'width' => 100, 'enabled' => true],
['widget' => 'bestEfforts', 'width' => 100, 'enabled' => true],
['widget' => 'distanceBreakdown', 'width' => 50, 'enabled' => true],
['widget' => 'gearStats', 'width' => 50, 'enabled' => true, 'config' => ['includeRetiredGear' => true]],
['widget' => 'yearlyStats', 'width' => 100, 'enabled' => true, 'config' => ['enableLastXYearsByDefault' => 10, 'metricsDisplayOrder' => ['distance', 'movingTime', 'elevation']]],
['widget' => 'zwiftStats', 'width' => 50, 'enabled' => true],
['widget' => 'gearStats', 'width' => 50, 'enabled' => true, 'config' => ['includeRetiredGear' => true]],
['widget' => 'streaks', 'width' => 50, 'enabled' => true],
['widget' => 'eddington', 'width' => 50, 'enabled' => true],
['widget' => 'ftpHistory', 'width' => 50, 'enabled' => true],
['widget' => 'challengeConsistency', 'width' => 50, 'enabled' => true, 'config' => ['challenges' => []]],
['widget' => 'mostRecentChallengesCompleted', 'width' => 50, 'enabled' => true, 'config' => ['numberOfChallengesToDisplay' => 5]],
['widget' => 'ftpHistory', 'width' => 50, 'enabled' => true],
['widget' => 'athleteWeightHistory', 'width' => 50, 'enabled' => true],
];
}
Expand Down
87 changes: 87 additions & 0 deletions src/Domain/Dashboard/Widget/AthleteProfile/AthleteProfileChart.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace App\Domain\Dashboard\Widget\AthleteProfile;

use Symfony\Contracts\Translation\TranslatorInterface;

final readonly class AthleteProfileChart
{
private function __construct(
/** @var array<int, float[]> */
private array $chartData,
private TranslatorInterface $translator,
) {
}

/**
* @param array<int, float[]> $chartData
*/
public static function create(
array $chartData,
TranslatorInterface $translator,
): self {
return new self(
chartData: $chartData,
translator: $translator
);
}

/**
* @return array<string, mixed>
*/
public function build(): array
{
$chartData = [];

foreach ($this->chartData as $lastXDays => $data) {
$chartData[] = [
'value' => $data,
'name' => $this->translator->trans('last {numberOfDays} days', ['{numberOfDays}' => $lastXDays]),
];
}

return [
'backgroundColor' => null,
'animation' => true,
'grid' => [
'left' => '5px',
'right' => '5px',
'bottom' => '5px',
'containLabel' => true,
],
'legend' => [
'show' => true,
'orient' => 'vertical',
'right' => 0,
],
'tooltip' => [
'show' => true,
'valueFormatter' => 'formatPercentage',
],
'radar' => [
'indicator' => [
['name' => $this->translator->trans('Volume'), 'max' => 100],
['name' => $this->translator->trans('Consistency'), 'max' => 100],
['name' => $this->translator->trans('Intensity'), 'max' => 100],
['name' => $this->translator->trans('Duration'), 'max' => 100],
['name' => $this->translator->trans('Density'), 'max' => 100],
['name' => $this->translator->trans('Variety'), 'max' => 100],
],
],
'series' => [
[
'type' => 'radar',
'symbolSize' => 5,
'lineStyle' => [
'width' => 1,
'opacity' => 1,
],
'areaStyle' => [
'opacity' => 0.1,
],
'data' => $chartData,
],
],
];
}
}
Loading