Skip to content

Commit

Permalink
Event results enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
mayfield committed Jan 18, 2024
1 parent d0c8043 commit 89eecda
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 57 deletions.
76 changes: 75 additions & 1 deletion pages/scss/events.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ html {
}

.trophy {
font-size: 1.3em;
font-size: 1.1em;
border-radius: 50%;
width: 1.5em;
height: 1.5em;
Expand Down Expand Up @@ -291,6 +291,24 @@ table {
margin-bottom: -0.36em;
}

> tbody > tr.summary.self:not(.expanded) {
clip-path: xywh(0 0 100% 100% round 0.5em); // tr border-radius
background-image: linear-gradient(to bottom, // must be vertical heading for Safari
color.shade(primary, -30%, 0.9),
color.shade(primary, -40%, 0.8));

td {
padding-top: 0.65em;
padding-bottom: 0.65em;
}

&:hover {
background-image: linear-gradient(to bottom, // must be vertical heading for Safari
color.shade(primary, -22%, 0.9),
color.shade(primary, -32%, 0.8));
}
}

> thead > tr {
background-color: #0003;

Expand All @@ -299,6 +317,58 @@ table {
}
}

&.results {
> thead th.time,
> thead th.distance {
text-align: right;
}

> tbody > tr.summary {
&.invalid {
opacity: 0.7;
font-weight: 200 !important;
}

> td.time,
> td.distance {
text-align: right;
font-variant-numeric: tabular-nums;

&.relative {
font-size: 0.8em;
}
}

> td.place {
text-align: center;
}

> td.icons {
ms {
font-size: 1.3em;
font-weight: 600;
line-height: 0.7;
filter: drop-shadow(0 0 1px black);
}

.flag {
color: #f32d24;
}

.warning {
color: #ffe200;
}
}

> td.power {
&[data-power-type="VIRTUAL_POWER"] {
text-decoration: line-through;
text-decoration-color: darkred;
}
}
}
}

td.icon {
padding: 0.1em;
width: 0;
Expand All @@ -312,6 +382,10 @@ table {
margin-left: -0.15em;
margin-right: -0.15em;
}

.danger {
color: color.get(danger);
}
}
}

Expand Down
24 changes: 15 additions & 9 deletions pages/src/events.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {render as profileRender} from './profile.mjs';
common.enableSentry();
common.settingsStore.setDefault({});

const settings = common.settingsStore.get();

let filterText;
let filterType;
let templates;
let nations;
let flags;
let worldList;
let gcs;
let selfAthlete;

const chartRefs = new Set();
const allEvents = new Map();
Expand Down Expand Up @@ -42,7 +44,7 @@ async function loadEventsWithRetry() {
// mutual startup races with backend.
let data;
for (let retry = 100;; retry += 100) {
data = await common.rpc.getEvents();
data = await common.rpc.getCachedEvents();
if (data.length) {
for (const x of data) {
allEvents.set(x.id, x);
Expand All @@ -69,9 +71,9 @@ async function fillInEvents() {

function applyEventFilters(el) {
const hide = new Set();
if (filterType) {
if (settings.filterType) {
for (const x of allEvents.values()) {
if (x.eventType !== filterType) {
if (x.eventType !== settings.filterType) {
hide.add('' + x.id);
}
}
Expand All @@ -98,7 +100,11 @@ function applyEventFilters(el) {

export async function main() {
common.initInteractionListeners();
[,templates, {nations, flags}, worldList, gcs] = await Promise.all([
addEventListener('resize', resizeCharts);
if (settings.filterType) {
document.querySelector('#titlebar select[name="type"]').value = settings.filterType;
}
[,templates, {nations, flags}, worldList, gcs, selfAthlete] = await Promise.all([
loadEventsWithRetry(),
getTemplates([
'events/list',
Expand All @@ -109,11 +115,13 @@ export async function main() {
common.initNationFlags(),
common.getWorldList(),
common.rpc.getGameConnectionStatus(),
common.rpc.getAthlete('self'),
]);
common.subscribe('status', x => (gcs = x), {source: 'gameConnection'});
document.querySelector('#titlebar select[name="type"]').addEventListener('change', ev => {
const type = ev.currentTarget.value;
filterType = type || undefined;
settings.filterType = type || undefined;
common.settingsStore.set(null, settings);
applyEventFilters(contentEl);
});
document.querySelector('#titlebar input[name="filter"]').addEventListener('input', ev => {
Expand Down Expand Up @@ -235,6 +243,7 @@ async function render() {
teamBadge: common.teamBadge,
eventBadge: common.eventBadge,
fmtFlag: common.fmtFlag,
selfAthlete,
}));
for (const el of eventDetailsEl.querySelectorAll('.elevation-chart[data-sg-id]')) {
const sg = subgroups.find(x => x.id === Number(el.dataset.sgId));
Expand Down Expand Up @@ -280,6 +289,3 @@ export async function settingsMain() {
common.initInteractionListeners();
await common.initSettingsForm('form')();
}


addEventListener('resize', resizeCharts);
131 changes: 87 additions & 44 deletions pages/templates/events/details.html.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,14 @@
<% } %>

<% if (!sg.results || !sg.results.length) { %>
<table class="entrants expandable">
<table class="entrants expandable startlist">
<thead>
<tr>
<th class="icon"><!-- marked --></th>
<th class="icon"><!-- following --></th>
<th class="icon"><!-- in-game --></th>
<th class="icon"><!-- power-meter --></th>
<th class="icon"><!-- gender --></th>
<th class="icon"></th>
<th class="icon"></th>
<th class="icon"></th>
<th class="icon"></th>
<th class="icon"></th>
<th class="name">Name</th>
<th class="team">Team</th>
<th class="ftp">FTP</th>
Expand All @@ -110,28 +110,26 @@
</thead>
<tbody>
<% for (const {id, athlete, likelyInGame} of sg.entrants) { %>
<tr data-id="{{id}}" class="summary">
<td class="icon">
<% if (athlete.marked) { %><ms class="marked" title="Is marked">bookmark_added</ms><% } %>
</td>
<td class="icon">
<% if (athlete.following) { %><ms class="following" title="You are following">follow_the_signs</ms><% } %>
</td>
<td class="icon">
<% if (likelyInGame) { %><ms title="Likely in game" class="in-game">check_circle</ms><% } %>
</td>
<td class="icon">
<% if (athlete.powerMeter) { %>
<% if (athlete.powerSourceModel === 'Smart Trainer') { %>
<ms class="power" title="Has smart trainer">offline_bolt</ms>
<% } else { %>
<ms class="power" title="Has power meter">bolt</ms>
<% } %>
<tr data-id="{{id}}" class="summary {{id === selfAthlete.id ? 'self' : ''}}">
<td class="icon"><% if (athlete.marked) { %>
<ms class="marked" title="Is marked">bookmark_added</ms>
<% } %></td>
<td class="icon"><% if (athlete.following) { %>
<ms class="following" title="You are following">follow_the_signs</ms>
<% } %></td>
<td class="icon"><% if (likelyInGame) { %>
<ms title="Likely in game" class="in-game">check_circle</ms>
<% } %></td>
<td class="icon"><% if (athlete.powerMeter) { %>
<% if (athlete.powerSourceModel === 'Smart Trainer') { %>
<ms class="power" title="Has smart trainer">offline_bolt</ms>
<% } else { %>
<ms class="power" title="Has power meter">bolt</ms>
<% } %>
</td>
<td class="icon">
<% if (athlete.gender === 'female') { %><ms class="female" title="Is female">female</ms><% } %>
</td>
<% } %></td>
<td class="icon"><% if (athlete.gender === 'female') { %>
<ms class="female" title="Is female">female</ms>
<% } %></td>
<td class="name">{-fmtFlag(athlete.countryCode, {empty: ''})-} {{athlete.sanitizedFullname}}</td>
<td class="team"><% if (athlete.team) { %>{-teamBadge(athlete.team)-}<% } %></td>
<td class="power">{-humanPower(athlete.ftp, {suffix: true, html: true})-}</td>
Expand All @@ -142,32 +140,64 @@
</tbody>
</table>
<% } else { %>
<table class="entrants expandable">
<table class="entrants expandable results">
<thead>
<tr>
<th></th>
<th><!--place--></th>
<th><!--flags--></th>
<th>Name</th>
<th>Team</th>
<th>Time</th>
<% if (sg.durationInSeconds) { %>
<th class="distance">Distance</th>
<% } else { %>
<th class="time">Time</th>
<% } %>
<th>Power</th>
<th>HR</th>
<th>Weight</th>
</tr>
</thead>
<tbody>
<% for (const x of sg.results) { %>
<tr data-id="{{x.profileId}}" class="summary">
<% let place = 0; %>
<% let groupStart; %>
<% for (const [i, x] of sg.results.entries()) { %>
<% const noPower = event.sport !== 'running' && x.sensorData.powerType === 'VIRTUAL_POWER'; %>
<% const validResult = !noPower && !x.flaggedCheating && !x.flaggedSandbagging; %>
<tr data-id="{{x.profileId}}"
class="summary
{{x.profileId === selfAthlete.id ? 'self' : ''}}
{{x.flaggedCheating ? 'cheating' : ''}}
{{x.flaggedSandbagging ? 'sandbagging' : ''}}
{{noPower ? 'nopower' : ''}}
{{!validResult ? 'invalid' : ''}}
">
<% place += validResult ? 1 : 0; %>
<td class="place">
<% if (x.rank === 1) { %>
<ms class="trophy gold">trophy</ms>
<% } else if (x.rank === 2) { %>
<ms class="trophy silver">trophy</ms>
<% } else if (x.rank === 3) { %>
<ms class="trophy bronze">trophy</ms>
<% } else { %>
{-humanPlace(x.rank, {suffix: true, html: true})-}
<% } %>
<% if (validResult) { %>
<% if (place === 1) { %>
<ms class="trophy gold">trophy</ms>
<% } else if (place === 2) { %>
<ms class="trophy silver">trophy</ms>
<% } else if (place === 3) { %>
<ms class="trophy bronze">trophy</ms>
<% } else { %>
{-humanPlace(place, {suffix: true, html: true})-}
<% } %>
<% } else { %>
-
<% } %>
</td>
<td class="icons">
<% if (x.flaggedCheating) { %>
<ms title="Flagged for cheating" class="flag">warning</ms>
<% } if (x.flaggedSandbagging) { %>
<ms title="Flagged for sandbagging" class="flag">emergency_heat</ms>
<% } if (noPower) { %>
<ms title="No power device" class="flag">power_off</ms>
<% } if (x.lateJoin) { %>
<ms title="Joined late" class="warning">acute</ms>
<% } %>
</td>
<td class="name">
{-fmtFlag(x.athlete.countryCode, {empty: ''})-}
<% if (x.athlete.gender === 'female') { %>
Expand All @@ -176,12 +206,25 @@
{{x.athlete.sanitizedFullname}}
</td>
<td class="team"><% if (x.athlete.team) { %>{-teamBadge(x.athlete.team)-}<% } %></td>
<td class="time">{-humanTimer(x.activityData.durationInMilliseconds / 1000, {html: true, ms: true})-}</td>
<td class="power" data-power-type="{{x.sensorData.powerType}}">{-humanPower(x.sensorData.avgWatts, {suffix: true, html: true})-}</td>
<% if (sg.durationInSeconds) { %>
<td class="distance">{-humanDistance(x.activityData.segmentDistanceInCentimeters / 100, {html: true, suffix: true})-}</td>
<% } else { %>
<% const t = x.activityData.durationInMilliseconds / 1000; %>
<% const prevT = i ? sg.results[i - 1].activityData.durationInMilliseconds / 1000 : null; %>
<% if (prevT && t - prevT < 2) { %>
<td class="time relative" title="{-humanTimer(t, {ms: true})-}">
+{-humanTimer(t - groupStart, {ms: true})-}
</td>
<% } else { %>
<td class="time">{-humanTimer(t, {html: true, ms: true})-}</td>
<% groupStart = t; %>
<% } %>
<% } %>
<td class="power">{-humanPower(x.sensorData.avgWatts, {suffix: true, html: true})-}</td>
<td class="hr">{-humanNumber(x.sensorData.heartRateData?.avgHeartRate, {suffix: 'bpm', html: true})-}</td>
<td class="weight">{-humanWeightClass(x.profileData.weightInGrams / 1000, {suffix: true, html: true})-}</td>
</tr>
<tr class="details"><td colspan="7"></td></tr>
<tr class="details"><td colspan="8"></td></tr>
<% } %>
</tbody>
</table>
Expand Down
17 changes: 14 additions & 3 deletions src/stats.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,8 @@ export class StatsProcessor extends events.EventEmitter {
rpc.register(this.getMarkedAthletes, {scope: this});
rpc.register(this.searchAthletes, {scope: this});
rpc.register(this.getEvent, {scope: this});
rpc.register(this.getEvents, {scope: this});
rpc.register(this.getCachedEvent, {scope: this});
rpc.register(this.getCachedEvents, {scope: this});
rpc.register(this.getEventSubgroup, {scope: this});
rpc.register(this.getEventSubgroupEntrants, {scope: this});
rpc.register(this.getEventSubgroupResults, {scope: this});
Expand Down Expand Up @@ -437,11 +438,21 @@ export class StatsProcessor extends events.EventEmitter {
}
}

getEvent(id) {
getCachedEvent(id) {
return this._recentEvents.get(id);
}

getEvents() {
async getEvent(id) {
if (!this._recentEvents.has(id)) {
const event = await this.zwiftAPI.getEvent(id);
if (event) {
this._addEvent(event);
}
}
return this._recentEvents.get(id);
}

getCachedEvents() {
return Array.from(this._recentEvents.values()).sort((a, b) => a.ts - b.ts);
}

Expand Down

0 comments on commit 89eecda

Please sign in to comment.