WIP: snapshot of all local changes
This commit is contained in:
parent
4f9fdb911e
commit
dc774beb6b
6 changed files with 942 additions and 0 deletions
162
static_ws/graphs.html
Normal file
162
static_ws/graphs.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue