-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
1,277 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
const apiUrl = "https://<API_GATEWAY_DOMAIN_NAME>/teams"; | ||
// Function to fetch the latest data | ||
function fetchData() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
const response = yield fetch(apiUrl); | ||
const data = yield response.json(); | ||
return data; | ||
} | ||
catch (error) { | ||
console.error("Error fetching data:", error); | ||
return []; | ||
} | ||
}); | ||
} | ||
// Define color scale globally to use across both graphs | ||
const colorScale = d3.scaleOrdinal(d3.schemeTableau10); | ||
function renderTimeline(data) { | ||
const margin = { top: 40, right: 30, bottom: 50, left: 80 }; // Adjust left margin for larger Y-axis values | ||
const width = document.body.clientWidth * 0.8 - margin.left - margin.right; | ||
const height = 400 - margin.top - margin.bottom; | ||
// Create a title for the graph | ||
d3.select("#timeline-chart") | ||
.append("h2") | ||
.style("text-align", "center") | ||
.text("Timeline"); | ||
const svg = d3.select("#timeline-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})`); | ||
const parseDate = d3.timeParse("%Y-%m-%dT%H:%M:%S%Z"); | ||
data.forEach(d => { | ||
var _a, _b; | ||
d.timestamp = (_b = (_a = parseDate(d.timestamp)) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : ""; | ||
}); | ||
data = data.filter(d => !isNaN(d.score) && d.timestamp); | ||
const dates = data.map(d => new Date(d.timestamp)); | ||
const x = d3.scaleTime() | ||
.domain([d3.min(dates), d3.max(dates) || new Date()]) | ||
.range([0, width]); | ||
const maxYValue = d3.max(data, d => d.score) || 0; | ||
const y = d3.scaleLinear() | ||
.domain([0, maxYValue * 1.1]) // Add 10% padding above the maximum value | ||
.range([height, 0]); | ||
// X-axis with default time format (HH:MM) | ||
svg.append("g") | ||
.attr("transform", `translate(0,${height})`) | ||
.call(d3.axisBottom(x).tickSize(-height).tickPadding(10)); | ||
// Y-axis with full numbers (no abbreviation) | ||
svg.append("g") | ||
.call(d3.axisLeft(y).tickSize(-width).tickPadding(10)); | ||
// Lighter grid lines | ||
svg.selectAll(".tick line") | ||
.attr("stroke", "#ddd") | ||
.attr("stroke-opacity", 0.3); | ||
// Line path for each team | ||
const line = d3.line() | ||
.x(d => x(new Date(d.timestamp))) | ||
.y(d => y(d.score)) | ||
.curve(d3.curveLinear); | ||
const teams = d3.group(data, d => d.team); | ||
teams.forEach((teamData, teamName) => { | ||
// Draw the line for each team | ||
svg.append("path") | ||
.datum(teamData) | ||
.attr("fill", "none") | ||
.attr("stroke", colorScale(teamName)) | ||
.attr("stroke-width", 2) | ||
.attr("d", line); | ||
// Find the last data point for the team | ||
const lastDataPoint = teamData.reduce((latest, current) => new Date(current.timestamp) > new Date(latest.timestamp) ? current : latest); | ||
// Draw a horizontal line extending from the last data point to the right edge of the graph | ||
svg.append("line") | ||
.attr("x1", x(new Date(lastDataPoint.timestamp))) // Start at the last data point | ||
.attr("x2", width) // Extend to the right edge of the graph | ||
.attr("y1", y(lastDataPoint.score)) // Y position based on the last score | ||
.attr("y2", y(lastDataPoint.score)) // Keep Y constant to form a horizontal line | ||
.attr("stroke", colorScale(teamName)) // Use the team's color for the line | ||
.attr("stroke-width", 2) | ||
.attr("stroke-opacity", 0.3) // Lighter line using reduced opacity | ||
.attr("stroke-dasharray", "4,4"); // Dashed line for differentiation | ||
}); | ||
// Tooltip container | ||
const tooltip = d3.select("body").append("div").attr("class", "tooltip-modern").style("display", "none"); | ||
// Circles at data points and mouseover event for tooltip (showing only HH:MM:SS) | ||
const timeTooltipFormat = d3.timeFormat("%H:%M:%S"); | ||
svg.selectAll("dot") | ||
.data(data) | ||
.enter() | ||
.append("circle") | ||
.attr("cx", d => x(new Date(d.timestamp))) | ||
.attr("cy", d => y(d.score)) | ||
.attr("r", 5) | ||
.attr("fill", d => colorScale(d.team)) | ||
.on("mouseover", function (event, d) { | ||
tooltip.style("display", "block") | ||
.html(`Team: ${d.team}<br>Score: ${d.score}<br>Time: ${timeTooltipFormat(new Date(d.timestamp))}`) | ||
.style("left", (event.pageX + 10) + "px") | ||
.style("top", (event.pageY - 20) + "px"); | ||
}) | ||
.on("mouseout", function () { | ||
tooltip.style("display", "none"); | ||
}); | ||
} | ||
function renderBarChart(data) { | ||
const margin = { top: 40, right: 30, bottom: 50, left: 100 }; | ||
const width = document.body.clientWidth * 0.8 - margin.left - margin.right; | ||
const height = 400 - margin.top - margin.bottom; | ||
// Sort by timestamp first, then filter out old scores, keeping only the latest for each team | ||
const latestScores = Array.from(d3.group(data, d => d.team).values()).map(teamScores => { | ||
return teamScores.reduce((latest, current) => { | ||
return new Date(current.timestamp) > new Date(latest.timestamp) ? current : latest; | ||
}); | ||
}); | ||
// Sort the latest scores by score in descending order | ||
latestScores.sort((a, b) => b.score - a.score); | ||
// Create a title for the graph | ||
d3.select("#bar-chart") | ||
.append("h2") | ||
.style("text-align", "center") | ||
.text("Latest Score"); | ||
const svg = d3.select("#bar-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})`); | ||
const y = d3.scaleBand() | ||
.domain(latestScores.map(d => d.team)) | ||
.range([0, height]) | ||
.padding(0.1); | ||
const maxYValue = d3.max(latestScores, d => d.score) || 0; | ||
const x = d3.scaleLinear() | ||
.domain([0, maxYValue * 1.1]) // Add 10% padding above the maximum value | ||
.range([0, width]); | ||
// X-axis | ||
svg.append("g") | ||
.attr("transform", `translate(0,${height})`) | ||
.call(d3.axisBottom(x).tickSize(-height).tickPadding(10)); | ||
// Y-axis | ||
svg.append("g") | ||
.call(d3.axisLeft(y).tickSize(-width).tickPadding(10)); | ||
svg.selectAll(".tick line").attr("stroke", "#ddd").attr("stroke-opacity", 0.3); | ||
const tooltip = d3.select("body").append("div").attr("class", "tooltip-modern").style("display", "none"); | ||
svg.selectAll(".bar") | ||
.data(latestScores) | ||
.enter() | ||
.append("rect") | ||
.attr("x", 0) | ||
.attr("y", d => y(d.team)) | ||
.attr("width", d => x(d.score)) | ||
.attr("height", y.bandwidth()) | ||
.attr("fill", d => colorScale(d.team)) | ||
.on("mouseover", function (event, d) { | ||
tooltip.style("display", "block") | ||
.html(`Team: ${d.team}<br>Score: ${d.score}`) | ||
.style("left", (event.pageX + 10) + "px") | ||
.style("top", (event.pageY - 20) + "px"); | ||
}) | ||
.on("mouseout", function () { | ||
tooltip.style("display", "none"); | ||
}); | ||
} | ||
// Function to update the graph | ||
function updateGraph() { | ||
fetchData().then(data => { | ||
// Clear existing graphs | ||
d3.select("#timeline-chart").selectAll("*").remove(); | ||
d3.select("#bar-chart").selectAll("*").remove(); | ||
// Re-render the graphs with the latest data | ||
renderTimeline(data); | ||
renderBarChart(data); | ||
}); | ||
} | ||
// Initial graph rendering | ||
updateGraph(); | ||
// Fetch and refresh the graph every 3 minutes (180,000 ms) | ||
setInterval(updateGraph, 180000); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>Score Portal</title> | ||
<script src="https://d3js.org/d3.v7.min.js"></script> | ||
<style> | ||
body { | ||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||
background-color: #f4f4f9; | ||
text-align: center; | ||
} | ||
|
||
svg { | ||
margin: auto; | ||
display: block; | ||
} | ||
|
||
text { | ||
font-size: 12px; | ||
fill: #555; | ||
} | ||
|
||
.tooltip-modern { | ||
position: absolute; | ||
background-color: rgba(0, 0, 0, 0.7); | ||
color: #fff; | ||
padding: 8px; | ||
border-radius: 4px; | ||
font-size: 14px; | ||
pointer-events: none; | ||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); | ||
} | ||
|
||
.tick line { | ||
stroke: #ddd; | ||
stroke-opacity: 0.3; | ||
} | ||
|
||
h2 { | ||
font-size: 24px; | ||
color: #333; | ||
margin-bottom: 20px; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<h1>ISHOCON1 Score Portal</h1> | ||
|
||
<div id="timeline-chart"></div> | ||
<div id="bar-chart"></div> | ||
|
||
<!-- Use module type for JavaScript files --> | ||
<script type="module" src="./dist/main.js"></script> | ||
</body> | ||
</html> |
Oops, something went wrong.