commit b6933188
Add chart to the game page
Changed files
| M | src/oastat_static_web_generator.cpp before |
| M | templates/game.tpl before |
diff --git a/src/oastat_static_web_generator.cpp b/src/oastat_static_web_generator.cpp
index ebaec36..856d4e9 100644
--- a/src/oastat_static_web_generator.cpp
+++ b/src/oastat_static_web_generator.cpp
@@ -189,6 +189,12 @@ struct OastatGame {
std::string servername;
};
+struct ScorePoint {
+ int playerid = 0;
+ int second = 0;
+ int score = 0;
+};
+
void write_html_game(cppdb::session& database, const OastatGame& game);
@@ -218,6 +224,18 @@ void getGameScoreTotal(cppdb::session& database, int gamenumber, std::vector<std
}
}
+void getGameScoreProgression(cppdb::session& database, int gamenumber, std::vector<ScorePoint>& progression) {
+ std::string sql = "SELECT player, `second` as the_second, score FROM oastat.oastat_points where gamenumber = ? ORDER BY the_second, player";
+ cppdb::statement st = database.prepare(sql);
+ st.bind(1, gamenumber);
+ cppdb::result res = st.query();
+ while(res.next()) {
+ ScorePoint point;
+ res >> point.playerid >> point.second >> point.score;
+ progression.push_back(point);
+ }
+}
+
void addPlayerToTemplateDict(ctemplate::TemplateDictionary* dict, const OastatPlayer& player) {
dict->SetValue("PLAYER_ID", std::to_string(player.playerid));
dict->SetValue("PLAYER_HEADMODEL", player.headmodel);
@@ -238,6 +256,7 @@ void write_html_game(cppdb::session& database, const OastatGame& game) {
game_tpl.SetValue("GAME_NUMBER", std::to_string(game.gamenumber));
game_tpl.SetValue("GAME_MAP", game.mapname);
game_tpl.SetValue("GAME_SERVERNAME", game.servername);
+
std::vector<std::pair<int,int>> scores;
getGameScoreTotal(database, game.gamenumber, scores);
for (size_t i = 0; i < scores.size(); ++i) {
@@ -247,6 +266,45 @@ void write_html_game(cppdb::session& database, const OastatGame& game) {
sub_dict->SetValue("SCORE", std::to_string(scores.at(i).second));
addPlayerToTemplateDict(sub_dict, p);
}
+
+ // Get score progression for chart
+ std::vector<ScorePoint> progression;
+ getGameScoreProgression(database, game.gamenumber, progression);
+
+ // Build JSON data for D3.js chart
+ // Group by player to create series
+ std::map<int, std::vector<ScorePoint>> playerSeries;
+ for (const auto& point : progression) {
+ playerSeries[point.playerid].push_back(point);
+ }
+
+ // Generate JSON for each player's score progression
+ std::stringstream json_data;
+ json_data << "[";
+ bool first_player = true;
+ for (const auto& series : playerSeries) {
+ if (!first_player) json_data << ",";
+ first_player = false;
+
+ const OastatPlayer& p = getPlayer(database, series.first);
+ json_data << "{\"player\":\"" << p.nickname << "\",\"playerid\":" << series.first << ",\"data\":[";
+
+ bool first_point = true;
+ for (const auto& point : series.second) {
+ if (!first_point) json_data << ",";
+ first_point = false;
+ json_data << "{\"second\":" << point.second << ",\"score\":" << point.score << "}";
+ }
+ json_data << "]}";
+ }
+ json_data << "]";
+
+ // Write JSON to separate file
+ std::ofstream json_file;
+ json_file.open(cmdargs.output_dir+"/game/"+std::to_string(game.gamenumber)+".json");
+ json_file << json_data.str();
+ json_file.close();
+
std::string output;
ctemplate::ExpandTemplate("templates/game.tpl", ctemplate::DO_NOT_STRIP, &game_tpl, &output);
std::ofstream myfile;
diff --git a/templates/game.tpl b/templates/game.tpl
index 15faf68..5ff6b00 100644
--- a/templates/game.tpl
+++ b/templates/game.tpl
@@ -32,6 +32,8 @@
<meta http-equiv="Content-Language" content="en">
<meta name="description" content="OpenArena stats by oastats">
<link rel="stylesheet" href="../static/css/oastat.css" type="text/css">
+ <link rel="stylesheet" href="../static/css/oastat_d3.css" type="text/css">
+ <script src="https://d3js.org/d3.v7.min.js"></script>
<title>OpenArena Stats - Game {{GAME_NUMBER}}</title>
</head>
<body>
@@ -41,6 +43,12 @@
<div class="levelshot">
<img src="../static/images/oa640x400/{{GAME_MAP}}.jpg" alt="{{GAME_MAP}}">
</div>
+
+ <h2>Score Progression Over Time</h2>
+ <div id="score-chart" style="margin: 20px 0;"></div>
+ <div id="score-legend" class="legend"></div>
+
+ <h2>Final Scores</h2>
<table>
<tr>
<th>Player</th>
@@ -53,6 +61,121 @@
</tr>
{{/SCORES_LIST}}
</table>
+
+ <script>
+ // Load score progression data from JSON file
+ d3.json("{{GAME_NUMBER}}.json")
+ .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);
+ });
+ </script>
</div>
</body>
</html>