WIP: snapshot of all local changes

This commit is contained in:
erik 2025-05-03 07:56:43 +00:00
parent 4f9fdb911e
commit dc774beb6b
6 changed files with 942 additions and 0 deletions

162
static_ws/graphs.html Normal file
View file

@ -0,0 +1,162 @@
<!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>