MosswartOverlord/static_ws/graphs.html
2025-05-03 07:56:43 +00:00

162 lines
4.8 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Dereth Tracker Analytics</title>
<!-- D3.js -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<link rel="stylesheet" href="style.css">
<style>
#content { flex: 1; padding: 16px; overflow: auto; }
h1 { margin-bottom: 24px; color: var(--accent); }
section { margin-bottom: 48px; }
.chart-svg { max-width: 100%; height: 300px; }
.axis path, .axis line { stroke: #eee; }
.axis text { fill: #eee; }
</style>
</head>
<body>
<div id="content">
<h1>Session Analytics</h1>
<section>
<h2>Kills over Time</h2>
<div id="chartKills"></div>
</section>
<section>
<h2>Kills per Hour</h2>
<div id="chartKPH"></div>
</section>
</div>
<script>
// D3.js-based stacked area charts with optimized grouping
document.addEventListener('DOMContentLoaded', async () => {
// Fetch and prepare data
const resp = await fetch('/history');
const { data } = await resp.json();
data.forEach(d => {
d.timestampMs = new Date(d.timestamp).getTime();
d.kills = +d.kills;
d.kph = +d.kph;
});
// Pre-group by timestamp and character for O(1) lookups
const nested = d3.rollup(
data,
recs => recs[recs.length - 1],
d => d.timestampMs,
d => d.character_name
);
// Sorted list of times and player names
const times = Array.from(nested.keys()).sort((a, b) => a - b);
const names = Array.from(new Set(data.map(d => d.character_name))).sort();
// Draw charts using precomputed structures
drawStackedAreaChart({
container: '#chartKills',
times,
names,
nested,
valueKey: 'kills',
yLabel: 'Total Kills'
});
drawStackedAreaChart({
container: '#chartKPH',
times,
names,
nested,
valueKey: 'kph',
yLabel: 'Kills per Hour'
});
});
function drawStackedAreaChart({ container, times, names, nested, valueKey, yLabel }) {
const margin = { top: 20, right: 80, bottom: 30, left: 50 };
const width = 800 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select(container)
.append('svg')
.attr('class', 'chart-svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// Build array of points per series using nested map
const dataPoints = times.map(ms => {
const pt = { timestamp: new Date(ms) };
const mapChar = nested.get(ms);
names.forEach(name => {
const rec = mapChar && mapChar.get(name);
pt[name] = rec ? rec[valueKey] : 0;
});
return pt;
});
const series = d3.stack()
.keys(names)(dataPoints);
const xScale = d3.scaleTime()
.domain([new Date(times[0]), new Date(times[times.length - 1])])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(series, s => d3.max(s, pts => pts[1]))]).nice()
.range([height, 0]);
const color = d3.scaleOrdinal(d3.schemeCategory10).domain(names);
const area = d3.area()
.x(d => xScale(d.data.timestamp))
.y0(d => yScale(d[0]))
.y1(d => yScale(d[1]))
.curve(d3.curveMonotoneX);
svg.selectAll('.area')
.data(series)
.enter().append('path')
.attr('class', 'area')
.attr('d', d => area(d))
.attr('fill', d => color(d.key))
.attr('opacity', 0.8);
svg.append('g')
.attr('class', 'axis x-axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(xScale).tickFormat(d3.timeFormat('%H:%M')));
svg.append('g')
.attr('class', 'axis y-axis')
.call(d3.axisLeft(yScale));
svg.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 0 - margin.left)
.attr('x', 0 - height / 2)
.attr('dy', '1em')
.style('text-anchor', 'middle')
.style('fill', 'var(--text)')
.text(yLabel);
const legend = svg.append('g')
.attr('transform', `translate(${width + 20},0)`);
names.forEach((key, i) => {
const row = legend.append('g')
.attr('transform', `translate(0,${i * 20})`);
row.append('rect')
.attr('width', 10)
.attr('height', 10)
.attr('fill', color(key));
row.append('text')
.attr('x', 15)
.attr('y', 10)
.text(key)
.attr('text-anchor', 'start')
.style('alignment-baseline', 'middle')
.style('fill', 'var(--text)');
});
}
</script>
</body>
</html>