improve leaderboard chart

This commit is contained in:
SowinskiBraeden committed 2026-04-28 13:15:59 -07:00
1 parent 32d212ad46
commit 1265e3047f
4 files changed
+272 -23

No files matched your search

+247 -13
View File
@@ -17,6 +17,7 @@
<h2>Cumulative profit over time</h2>
</div>
<div class="panel-actions">
<button class="ghost-button compact-button" type="button" id="resetLeaderboardZoom" hidden>Reset zoom</button>
<button class="secondary-button" type="button" id="expandLeaderboardChart">Expand chart</button>
</div>
</div>
@@ -137,7 +138,70 @@
const textMuted = rootStyles.getPropertyValue('--text-muted').trim();
const lineColor = rootStyles.getPropertyValue('--line').trim();
function buildLeaderboardChartConfig(chartData) {
function mainLeaderboardChartData(chartData) {
const repeatedPlayerDatasets = chartData.datasets.filter((dataset) => {
return Number(dataset.sessions_played || 0) > 1;
});
const visibleDatasets = repeatedPlayerDatasets.length > 0
? repeatedPlayerDatasets
: chartData.datasets;
return {
labels: [...chartData.labels],
datasets: visibleDatasets.map((dataset) => ({
...dataset,
data: [...dataset.data],
pointRadius: 0,
pointHoverRadius: 5,
borderWidth: 2.5,
tension: 0.28,
})),
};
}
function moneyTickLabel(value) {
const numericValue = Number(value);
const absoluteValue = Math.abs(numericValue);
const formattedValue = Number.isInteger(absoluteValue)
? absoluteValue.toFixed(0)
: absoluteValue.toFixed(2);
return `${numericValue < 0 ? '-' : ''}$${formattedValue}`;
}
function chartYScaleOptions(chartData, compact) {
const values = chartData.datasets
.flatMap((dataset) => dataset.data)
.filter((value) => value !== null && value !== undefined)
.map(Number);
if (values.length === 0) {
return {};
}
const minValue = Math.min(...values);
const maxValue = Math.max(...values);
const maxAbsValue = Math.max(Math.abs(minValue), Math.abs(maxValue));
const stepSize = maxAbsValue <= 75 ? 25 : undefined;
return {
suggestedMin: minValue < 0 ? minValue * 1.12 : 0,
suggestedMax: maxValue > 0 ? maxValue * 1.12 : 0,
ticks: {
color: textMuted,
maxTicksLimit: compact ? 8 : 10,
stepSize,
callback(value) {
return moneyTickLabel(value);
},
},
grid: { color: lineColor },
};
}
function buildLeaderboardChartConfig(chartData, options = {}) {
const compact = options.compact === true;
return {
type: 'line',
data: chartData,
@@ -145,15 +209,26 @@
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'nearest', intersect: false },
elements: {
point: {
radius: compact ? 0 : 2,
hoverRadius: 5,
},
line: {
borderWidth: compact ? 2.5 : 2,
tension: 0.28,
},
},
plugins: {
legend: {
display: true,
position: 'bottom',
labels: {
color: textMuted,
usePointStyle: true,
boxWidth: 10,
boxHeight: 10,
padding: 18,
boxWidth: compact ? 8 : 10,
boxHeight: compact ? 8 : 10,
padding: compact ? 12 : 18,
},
},
tooltip: {
@@ -170,27 +245,186 @@
},
scales: {
x: {
ticks: { color: textMuted, maxRotation: 0 },
grid: { color: lineColor },
},
y: {
ticks: {
color: textMuted,
callback(value) {
return `$${value}`;
},
maxRotation: 0,
autoSkip: true,
maxTicksLimit: compact ? 6 : 10,
},
grid: {
color: lineColor,
display: !compact,
},
grid: { color: lineColor },
},
y: chartYScaleOptions(chartData, compact),
},
},
};
}
const zoomSelectionPlugin = {
id: 'zoomSelection',
beforeDraw(chart) {
const drag = chart.$zoomDrag;
if (!drag || !drag.active || drag.startX === drag.endX) {
return;
}
const { ctx, chartArea } = chart;
const left = Math.max(chartArea.left, Math.min(drag.startX, drag.endX));
const right = Math.min(chartArea.right, Math.max(drag.startX, drag.endX));
ctx.save();
ctx.fillStyle = 'rgba(229, 139, 58, 0.14)';
ctx.strokeStyle = 'rgba(229, 139, 58, 0.74)';
ctx.lineWidth = 1;
ctx.fillRect(left, chartArea.top, right - left, chartArea.bottom - chartArea.top);
ctx.strokeRect(left, chartArea.top, right - left, chartArea.bottom - chartArea.top);
ctx.restore();
},
};
Chart.register(zoomSelectionPlugin);
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function enableDragZoom(chart, resetButton) {
if (!chart || !chart.canvas || !resetButton) {
return;
}
const canvas = chart.canvas;
const minDragDistance = 14;
let draggedToZoom = false;
function pointerX(event) {
const rect = canvas.getBoundingClientRect();
return event.clientX - rect.left;
}
function eventInChartArea(event) {
const x = pointerX(event);
const y = event.clientY - canvas.getBoundingClientRect().top;
const area = chart.chartArea;
return x >= area.left && x <= area.right && y >= area.top && y <= area.bottom;
}
canvas.addEventListener('pointerdown', (event) => {
if (!eventInChartArea(event)) {
return;
}
canvas.setPointerCapture(event.pointerId);
const x = pointerX(event);
chart.$zoomDrag = { active: true, startX: x, endX: x };
chart.update('none');
});
canvas.addEventListener('pointermove', (event) => {
if (!chart.$zoomDrag?.active) {
return;
}
chart.$zoomDrag.endX = pointerX(event);
chart.draw();
});
canvas.addEventListener('pointerup', (event) => {
const drag = chart.$zoomDrag;
if (!drag?.active) {
return;
}
drag.active = false;
const distance = Math.abs(drag.endX - drag.startX);
if (distance < minDragDistance) {
chart.draw();
return;
}
draggedToZoom = true;
const area = chart.chartArea;
const startPixel = clamp(Math.min(drag.startX, drag.endX), area.left, area.right);
const endPixel = clamp(Math.max(drag.startX, drag.endX), area.left, area.right);
const xScale = chart.scales.x;
const minIndex = clamp(Math.floor(xScale.getValueForPixel(startPixel)), 0, chart.data.labels.length - 1);
const maxIndex = clamp(Math.ceil(xScale.getValueForPixel(endPixel)), 0, chart.data.labels.length - 1);
if (maxIndex <= minIndex) {
chart.draw();
return;
}
xScale.options.min = minIndex;
xScale.options.max = maxIndex;
resetButton.hidden = false;
chart.update();
});
canvas.addEventListener('click', (event) => {
if (!draggedToZoom) {
return;
}
event.stopPropagation();
event.preventDefault();
draggedToZoom = false;
});
canvas.addEventListener('pointercancel', () => {
if (chart.$zoomDrag) {
chart.$zoomDrag.active = false;
chart.draw();
}
});
resetButton.addEventListener('click', () => {
delete chart.options.scales.x.min;
delete chart.options.scales.x.max;
resetButton.hidden = true;
chart.update();
});
}
function suppressLegendClickExpansion(chart) {
if (!chart?.canvas) {
return;
}
chart.canvas.addEventListener('click', (event) => {
const legend = chart.legend;
if (!legend || !legend.legendHitBoxes) {
return;
}
const rect = chart.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const clickedLegendItem = legend.legendHitBoxes.some((box) => {
return x >= box.left
&& x <= box.left + box.width
&& y >= box.top
&& y <= box.top + box.height;
});
if (clickedLegendItem) {
event.stopPropagation();
}
});
}
const leaderboardCtx = document.getElementById('leaderboardChart');
const resetZoomButton = document.getElementById('resetLeaderboardZoom');
let leaderboardChart = null;
if (leaderboardCtx && leaderboardChartData.labels.length > 0) {
leaderboardChart = new Chart(leaderboardCtx, buildLeaderboardChartConfig(leaderboardChartData));
leaderboardChart = new Chart(
leaderboardCtx,
buildLeaderboardChartConfig(mainLeaderboardChartData(leaderboardChartData), { compact: true })
);
enableDragZoom(leaderboardChart, resetZoomButton);
suppressLegendClickExpansion(leaderboardChart);
}
const chartModal = document.getElementById('chartModal');