feat: render sparklines + wire trend-chart pill toggle (#6)

This commit is contained in:
Samuel Enocsson
2026-05-21 14:34:28 +02:00
parent 6129b6fd3b
commit b51ae19ae1
4 changed files with 130 additions and 0 deletions
+69
View File
@@ -592,3 +592,72 @@ a:hover {
color: var(--ink-3); color: var(--ink-3);
margin-top: 2px; margin-top: 2px;
} }
/* ── Table Toolbar + Pill Toggle ─────────────── */
.table-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.pill-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px 5px 10px;
border-radius: 999px;
border: 1px solid var(--line-2);
background: var(--paper-2);
color: var(--ink-2);
font-size: 12px;
font-weight: 600;
font-family: var(--font-sans);
cursor: pointer;
transition: background 150ms ease, border-color 150ms ease, color 150ms ease;
}
.pill-toggle:hover {
background: var(--hover);
border-color: var(--line);
}
.pill-toggle[aria-pressed="true"] {
background: color-mix(in oklab, var(--accent) 10%, white);
border-color: color-mix(in oklab, var(--accent) 35%, var(--line-2));
color: var(--accent-text);
}
.pill-icon {
width: 12px;
height: 12px;
flex-shrink: 0;
}
.pill-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--ink-3);
opacity: 0.4;
transition: background 150ms ease, opacity 150ms ease, box-shadow 150ms ease;
}
.pill-toggle[aria-pressed="true"] .pill-dot {
background: var(--accent);
opacity: 1;
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 20%, transparent);
}
/* ── Sparklines ───────────────────────────────── */
.sparkline {
display: inline-block;
vertical-align: middle;
line-height: 0;
margin-top: 4px;
}
body[data-sparklines="off"] .sparkline {
display: none;
}
+17
View File
@@ -480,3 +480,20 @@ function closeAddPlayerModal(event) {
document.getElementById('add-player-modal').style.display = 'none'; document.getElementById('add-player-modal').style.display = 'none';
pendingPlayerData = null; pendingPlayerData = null;
} }
// ── Sparkline toggle ───────────────────────────────
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('trendchart-toggle');
if (!btn) return;
var state = localStorage.getItem('ratingtracker.sparklines') || 'on';
document.body.dataset.sparklines = state;
btn.setAttribute('aria-pressed', state === 'on' ? 'true' : 'false');
btn.addEventListener('click', function() {
var next = document.body.dataset.sparklines === 'on' ? 'off' : 'on';
document.body.dataset.sparklines = next;
btn.setAttribute('aria-pressed', next === 'on' ? 'true' : 'false');
localStorage.setItem('ratingtracker.sparklines', next);
});
});
+9
View File
@@ -19,6 +19,15 @@
<div style="display: flex; justify-content: flex-end; gap: 12px; margin-bottom: 16px;"> <div style="display: flex; justify-content: flex-end; gap: 12px; margin-bottom: 16px;">
<a href="#" onclick="clearCache(); return false;" style="color: var(--text-muted); font-size: 12px; text-decoration: none; opacity: 0.4;" title="Clear cache"><i class="fas fa-cog"></i></a> <a href="#" onclick="clearCache(); return false;" style="color: var(--text-muted); font-size: 12px; text-decoration: none; opacity: 0.4;" title="Clear cache"><i class="fas fa-cog"></i></a>
</div> </div>
<div class="table-toolbar">
<button id="trendchart-toggle" class="pill-toggle" type="button" aria-pressed="false">
<svg class="pill-icon" width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<polyline points="1,9 4,5 7,7 11,2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="pill-label">Trend chart</span>
<span class="pill-dot"></span>
</button>
</div>
<div id="ratings-table" hx-get="/partials/ratings-table" hx-trigger="load"></div> <div id="ratings-table" hx-get="/partials/ratings-table" hx-trigger="load"></div>
`; %> `; %>
+35
View File
@@ -1,3 +1,33 @@
<%
function renderSparkline(values) {
if (!values || values.length < 2) return '';
var w = 96, h = 28;
var min = Math.min.apply(null, values);
var max = Math.max.apply(null, values);
var range = max - min || 1;
var xStep = w / (values.length - 1);
var pts = values.map(function(v, i) {
return {
x: (i * xStep).toFixed(1),
y: (((max - v) / range) * (h - 4) + 2).toFixed(1)
};
});
var linePath = pts.map(function(p, i) {
return (i === 0 ? 'M' : 'L') + ' ' + p.x + ' ' + p.y;
}).join(' ');
var last = pts[pts.length - 1];
var areaPath = linePath + ' L ' + last.x + ' ' + h + ' L 0 ' + h + ' Z';
return '<svg width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '" style="display:block;overflow:visible">' +
'<path d="' + areaPath + '" style="fill:var(--accent);fill-opacity:0.10"/>' +
'<path d="' + linePath + '" style="stroke:var(--accent);stroke-width:1.5;fill:none;stroke-linejoin:round;stroke-linecap:round"/>' +
'<circle cx="' + last.x + '" cy="' + last.y + '" r="2.5" style="fill:var(--accent)"/>' +
'</svg>';
}
%>
<% if (ratings.length === 0) { %> <% if (ratings.length === 0) { %>
<p style="text-align: center; color: var(--text-muted); padding: 40px 0;">No ratings found.</p> <p style="text-align: center; color: var(--text-muted); padding: 40px 0;">No ratings found.</p>
<% } else { %> <% } else { %>
@@ -20,6 +50,8 @@
var deltaPredicted = player.deltaPredicted ?? null; var deltaPredicted = player.deltaPredicted ?? null;
var deltaPredictedPillText = deltaPredicted != null ? (deltaPredicted > 0 ? '+' + deltaPredicted : deltaPredicted.toString()) : null; var deltaPredictedPillText = deltaPredicted != null ? (deltaPredicted > 0 ? '+' + deltaPredicted : deltaPredicted.toString()) : null;
var deltaPredictedPillClass = deltaPredicted > 0 ? 'up' : deltaPredicted < 0 ? 'down' : 'flat'; var deltaPredictedPillClass = deltaPredicted > 0 ? 'up' : deltaPredicted < 0 ? 'down' : 'flat';
var sparklineSvg = renderSparkline(player.monthlyHistory || []);
%> %>
<tr id="row-<%= player.pdgaNumber %>" class="expandable-row" onclick="togglePlayerHistory(<%= player.pdgaNumber %>)"> <tr id="row-<%= player.pdgaNumber %>" class="expandable-row" onclick="togglePlayerHistory(<%= player.pdgaNumber %>)">
<td class="mobile-hide"><%= index + 1 %></td> <td class="mobile-hide"><%= index + 1 %></td>
@@ -36,6 +68,9 @@
<% if (ratingChangePillText) { %> <% if (ratingChangePillText) { %>
<span class="delta-pill <%= ratingChangePillClass %>"><%= ratingChangePillText %></span> <span class="delta-pill <%= ratingChangePillClass %>"><%= ratingChangePillText %></span>
<% } %> <% } %>
<% if (sparklineSvg) { %>
<span class="sparkline"><%- sparklineSvg %></span>
<% } %>
<div class="std-dev-tooltip" id="tooltip-rating-<%= player.pdgaNumber %>"></div> <div class="std-dev-tooltip" id="tooltip-rating-<%= player.pdgaNumber %>"></div>
</td> </td>
<td class="predicted-rating mobile-hide" id="predicted-<%= player.pdgaNumber %>"> <td class="predicted-rating mobile-hide" id="predicted-<%= player.pdgaNumber %>">