iamvisual/index.html
kjuulh bbf5770828
feat: horizontal
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-11-21 21:52:20 +01:00

217 lines
7.5 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Space-Themed Heatmap</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
background-color: black;
color: white;
font-family: Arial, sans-serif;
}
svg {
display: block;
margin: auto;
}
.legend {
font-size: 12px;
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
margin: 5px;
}
.legend-color {
width: 15px;
height: 15px;
margin-right: 5px;
}
</style>
</head>
<body>
<h2>Live Activity</h2>
<div class="divider"></div>
<div id="chart"></div>
<div id="legend" class="legend"></div>
<script>
const horizontal = true;
const categoryAmount = 5;
// Dimensions
const margin = { top: 20, right: 20, bottom: 20, left: 50 };
const width = 800 - margin.left - margin.right;
const height = 85 * categoryAmount / 2 - margin.top - margin.bottom;
// Categories with labels
const categories = [
{ id: 0, label: "User Onboarded", color: "#FFD700" },
{ id: 1, label: "Payment Accepted", color: "#00BFFF" },
{ id: 2, label: "Payment Failed", color: "#FF4500" },
{ id: 3, label: "Product Added to Cart", color: "#9400D3" },
{ id: 4, label: "Checkout Started", color: "#32CD32" },
{ id: 5, label: "Order Placed", color: "#8B4513" },
{ id: 6, label: "Order Cancelled", color: "#FF69B4" },
{ id: 7, label: "Refund Processed", color: "#A9A9A9" },
{ id: 8, label: "Subscription Renewed", color: "#BDB76B" },
{ id: 9, label: "Feedback Submitted", color: "#00FFFF" },
].filter((c) => c.id < categoryAmount);
// Store last intensity for each category
const lastIntensity = new Map(categories.map(c => [c.id, Math.random()]));
// Create Legend
const legend = d3.select("#legend");
categories.forEach(category => {
const item = legend.append("div").attr("class", "legend-item");
item.append("div")
.attr("class", "legend-color")
.style("background-color", category.color);
item.append("span").text(category.label);
});
// Create SVG
const svg = d3.select("#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})`);
// Time scales
var x, y;
if (horizontal) {
x = d3.scaleTime()
.domain([new Date(Date.now() - 60000), new Date()]) // Past 1 min to now
.range([width, 0]);
y = d3.scaleBand()
.domain(categories.map(c => c.id)) // Category IDs
.range([0, height]);
} else {
y = d3.scaleTime()
.domain([new Date(Date.now() - 60000), new Date()]) // Past 1 min to now
.range([height, 0]);
x = d3.scaleBand()
.domain(categories.map(c => c.id)) // Category IDs
.range([0, width]);
}
// Heatmap group
const grid = svg.append("g");
// Initialize heatmap
function updateHeatmap(data) {
const cells = grid.selectAll("rect").data(data, d => d.id);
if (horizontal) {
cells.enter()
.append("rect")
.attr("y", d => y(d.category))
.attr("width", 12)
.attr("x", d => x(new Date(d.timestamp)))
.attr("height", 25) // Minimized spacing between blocks
.attr("fill", d => categories[d.category].color)
.attr("opacity", d => d.intensity)
.merge(cells) // Merge updates
.attr("x", d => x(new Date(d.timestamp)))
.attr("opacity", d => d.intensity)
.attr("visibility", d =>
x(new Date(d.timestamp)) >= 0 && x(new Date(d.timestamp)) <= width
? "visible"
: "hidden"
);
} else {
// Enter new data
cells.enter()
.append("rect")
.attr("x", d => x(d.category))
.attr("width", x.bandwidth())
.attr("y", d => y(new Date(d.timestamp)))
.attr("height", 4) // Minimized spacing between blocks
.attr("fill", d => categories[d.category].color)
.attr("opacity", d => d.intensity)
.merge(cells) // Merge updates
.attr("y", d => y(new Date(d.timestamp)))
.attr("opacity", d => d.intensity)
.attr("visibility", d =>
y(new Date(d.timestamp)) >= 0 && y(new Date(d.timestamp)) <= height
? "visible"
: "hidden"
);
}
// Remove old data that moves off the screen
cells.exit().remove();
}
// Real-time simulation
let allData = [];
const scrollingSpeed = 20; // Pixels per second
function generateData() {
// Simulate sporadic events with intensity
const newData = categories.map(c => {
if (Math.random() < 0.7) return null; // 70% chance no data for this category
const newIntensity = Math.random(); // Random intensity
const smoothIntensity = d3.interpolate(lastIntensity.get(c.id), newIntensity)(0.5); // Smooth transition
lastIntensity.set(c.id, smoothIntensity); // Update last intensity
return {
id: `${Date.now()}-${c.id}`,
category: c.id,
timestamp: Date.now(),
intensity: smoothIntensity,
};
}).filter(Boolean); // Remove null values
// Append new data and remove older ones beyond the last 60 seconds
allData = [...allData, ...newData].filter(d =>
new Date(d.timestamp) >= new Date(Date.now() - 60000)
);
}
// Continuous scroll
let lastTimestamp = Date.now();
d3.timer(elapsed => {
const now = Date.now();
const delta = now - lastTimestamp;
// Generate data periodically (every 500ms for more activity)
if (delta >= 500) {
generateData();
lastTimestamp = now;
}
// Update the domain of the y-axis
const currentTime = new Date();
const pastTime = new Date(currentTime.getTime() - 60000);
if (horizontal) {
x.domain([pastTime, currentTime]);
} else {
y.domain([pastTime, currentTime]);
}
// Update positions of all heatmap cells
updateHeatmap(allData);
});
</script>
</body>
</html>