diff --git a/static_content/js/score-chart.js b/static_content/js/score-chart.js new file mode 100644 index 0000000..5efbaa2 --- /dev/null +++ b/static_content/js/score-chart.js @@ -0,0 +1,120 @@ +/** + * Score Progression Chart + * Renders a D3.js line chart showing player scores over time + * + * @param {string} dataUrl - URL to the JSON file containing score data + */ +function renderScoreChart(dataUrl) { + // Load score progression data from JSON file + d3.json(dataUrl) + .then(function(scoreData) { + console.log("Loaded score data:", scoreData); + + if (!scoreData || scoreData.length === 0) { + console.log("No score data available"); + return; + } + + // Set up dimensions + const margin = {top: 20, right: 120, bottom: 50, left: 60}; + const width = 800 - margin.left - margin.right; + const height = 400 - margin.top - margin.bottom; + + // Create SVG + const svg = d3.select("#score-chart") + .append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // Find max values for scales + const maxSecond = d3.max(scoreData, d => d3.max(d.data, p => p.second)); + const maxScore = d3.max(scoreData, d => d3.max(d.data, p => p.score)); + + // Create scales + const xScale = d3.scaleLinear() + .domain([0, maxSecond]) + .range([0, width]); + + const yScale = d3.scaleLinear() + .domain([0, maxScore]) + .range([height, 0]); + + // Create axes + const xAxis = d3.axisBottom(xScale); + const yAxis = d3.axisLeft(yScale); + + // Add axes to SVG + svg.append("g") + .attr("transform", `translate(0,${height})`) + .call(xAxis) + .append("text") + .attr("x", width / 2) + .attr("y", 35) + .attr("fill", "#000") + .style("text-anchor", "middle") + .text("Time (seconds)"); + + svg.append("g") + .call(yAxis) + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", -45) + .attr("x", -height / 2) + .attr("fill", "#000") + .style("text-anchor", "middle") + .text("Score"); + + // Color scale + const color = d3.scaleOrdinal() + .domain(scoreData.map(d => d.player)) + .range(d3.schemeCategory10); + + // Line generator + const line = d3.line() + .x(d => xScale(d.second)) + .y(d => yScale(d.score)); + + // Draw lines for each player + scoreData.forEach(playerData => { + svg.append("path") + .datum(playerData.data) + .attr("fill", "none") + .attr("stroke", color(playerData.player)) + .attr("stroke-width", 2) + .attr("d", line); + + // Add dots at data points + svg.selectAll(`.dot-${playerData.playerid}`) + .data(playerData.data) + .enter() + .append("circle") + .attr("class", `dot-${playerData.playerid}`) + .attr("cx", d => xScale(d.second)) + .attr("cy", d => yScale(d.score)) + .attr("r", 3) + .attr("fill", color(playerData.player)) + .append("title") + .text(d => `${playerData.player}: ${d.score} at ${d.second}s`); + }); + + // Create legend + const legend = d3.select("#score-legend"); + scoreData.forEach(playerData => { + const item = legend.append("div") + .attr("class", "legend-item"); + + item.append("div") + .attr("class", "legend-color") + .style("background-color", color(playerData.player)); + + const finalScore = playerData.data[playerData.data.length - 1].score; + item.append("span") + .text(`${playerData.player} (${finalScore})`); + }); + }) + .catch(function(error) { + console.error("Error loading score data:", error); + }); +} diff --git a/static_content/js/weapon-pie-chart.js b/static_content/js/weapon-pie-chart.js new file mode 100644 index 0000000..64c7598 --- /dev/null +++ b/static_content/js/weapon-pie-chart.js @@ -0,0 +1,82 @@ +/** + * Weapon Kills Pie Chart + * Renders a D3.js pie chart showing weapon kill distribution + * + * @param {Array} weaponData - Array of objects with {name: string, value: number} + */ +function renderWeaponPieChart(weaponData) { + // Set up dimensions + const width = 500; + const height = 400; + const radius = Math.min(width, height) / 2 - 40; + + // Color scale + const color = d3.scaleOrdinal() + .domain(weaponData.map(d => d.name)) + .range(d3.schemeCategory10); + + // Create SVG + const svg = d3.select("#weapon-pie-chart") + .append("svg") + .attr("width", width) + .attr("height", height) + .append("g") + .attr("transform", `translate(${width/2},${height/2})`); + + // Create pie layout + const pie = d3.pie() + .value(d => d.value) + .sort(null); + + // Create arc generator + const arc = d3.arc() + .innerRadius(0) + .outerRadius(radius); + + const labelArc = d3.arc() + .innerRadius(radius * 0.6) + .outerRadius(radius * 0.6); + + // Create pie slices + const arcs = svg.selectAll(".arc") + .data(pie(weaponData)) + .enter() + .append("g") + .attr("class", "arc"); + + arcs.append("path") + .attr("class", "pie-slice") + .attr("d", arc) + .attr("fill", d => color(d.data.name)) + .append("title") + .text(d => `${d.data.name}: ${d.data.value} kills`); + + // Add labels for larger slices + arcs.append("text") + .attr("class", "pie-label") + .attr("transform", d => `translate(${labelArc.centroid(d)})`) + .attr("text-anchor", "middle") + .style("display", d => { + // Only show label if slice is large enough + const percent = (d.endAngle - d.startAngle) / (2 * Math.PI) * 100; + return percent > 5 ? "block" : "none"; + }) + .text(d => { + const percent = ((d.endAngle - d.startAngle) / (2 * Math.PI) * 100).toFixed(1); + return `${percent}%`; + }); + + // Create legend + const legend = d3.select("#weapon-legend"); + weaponData.forEach(d => { + const item = legend.append("div") + .attr("class", "legend-item"); + + item.append("div") + .attr("class", "legend-color") + .style("background-color", color(d.name)); + + item.append("span") + .text(`${d.name} (${d.value})`); + }); +} diff --git a/templates/game.tpl b/templates/game.tpl index 5ff6b00..5f024c4 100644 --- a/templates/game.tpl +++ b/templates/game.tpl @@ -34,6 +34,7 @@ +