I have a svg file in which zones are market in red rectangle box, and i want to create a animated bubble visualization in which each bubble moves zone to zone as defined in csv.
My Csv looks like below:
this is house_csv.csv file i am using
device_id | zone_name | time |
---|---|---|
Kid | Dinner | 2024-01-01T00:00:00 |
Kid | Family Room | 2024-01-01T01:00:00 |
Kid | Bedroom | 2024-01-01T02:00:00 |
Friend1 | Dinner | 2024-01-01T00:00:00 |
Friend1 | Family Room | 2024-01-01T01:00:00 |
Friend1 | Bedroom | 2024-01-01T02:00:00 |
Pops | Dinner | 2024-01-01T00:00:00 |
Pops | Garage Area | 2024-01-01T01:00:00 |
Pops | ,Bedroom | 2024-01-01T02:00:00 |
Below is how my svg looks like, each red box is marked by an id which is same as zone_name:
below is my code that i tried :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Device Movement on Floorplan</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
}
#chart {
width: 100%;
height: 100%;
max-width: 800px;
max-height: 600px;
margin: 0px auto;
border: 1px solid #ccc;
position: relative;
}
#playPauseButton {
padding: 10px 20px;
font-size: 16px;
margin: 20px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
}
#playPauseButton.paused {
background-color: #f44336;
}
.bubble {
fill-opacity: 0.8;
}
.object-label {
font-size: 12px;
font-weight: normal;
fill: white;
text-anchor: middle;
pointer-events: none;
}
</style>
</head>
<body>
<h1>Device Movement on Floorplan</h1>
<div id="chart"></div>
<button id="playPauseButton">Play</button>
<script>
window.onload = function() {
// Load and parse the CSV file
Papa.parse('house_csv.csv', {
download: true,
header: true,
dynamicTyping: true,
complete: function(results) {
const filteredData = results.data.filter(row => row.time && row.time !== 'undefined');
if (filteredData.length > 0) {
createBubbleChart(filteredData);
} else {
console.error('No valid data found in the CSV');
}
},
error: function(error) {
console.error("CSV Parse Error: ", error);
}
});
};
function createBubbleChart(data) {
const floorplanWidth = 627.89801;
const floorplanHeight = 354.04199;
const svg = d3.select("#chart").append("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", `0 0 ${floorplanWidth} ${floorplanHeight}`)
.attr("preserveAspectRatio", "xMidYMid meet");
d3.xml("HouseMap.svg").then(function(xml) {
const importedNode = document.importNode(xml.documentElement, true);
svg.node().appendChild(importedNode);
const containerWidth = document.getElementById("chart").offsetWidth;
const containerHeight = document.getElementById("chart").offsetHeight;
const scaleX = containerWidth / floorplanWidth;
const scaleY = containerHeight / floorplanHeight;
console.log(`ScaleX: ${scaleX}, ScaleY: ${scaleY}`);
// Create a map of zone names to positions (center of <rect> elements)
const zonePositions = {};
svg.selectAll("rect")
.each(function() {
const rectElement = d3.select(this);
const zoneId = rectElement.attr("id");
const bbox = rectElement.node().getBBox();
const centerX = (bbox.x + bbox.width / 2) * scaleX;
const centerY = (bbox.y + bbox.height / 2) * scaleY;
zonePositions[zoneId] = { x: centerX, y: centerY };
});
console.log("Zone Positions: ", zonePositions); // Debugging log to check mapping
// Create a color scale for different device IDs
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
// Create initial bubbles for each device_id
const bubbles = svg.selectAll(".bubble")
.data([...new Set(data.map(d => d.device_id))])
.enter().append("circle")
.attr("class", "bubble")
.attr("r", 10) // Set initial radius
.attr("cx", 0) // Initial X position
.attr("cy", 0) // Initial Y position
.attr("opacity", 0)
.style("fill", d => colorScale(d)) // Assign color to each device
.transition()
.duration(2000)
.attr("opacity", 1); // Fade in the bubbles
// Group data by device_id and zone_name
const groupedData = d3.nest()
.key(d => d.device_id)
.key(d => d.zone_name)
.entries(data);
// Create a path for each device_id based on zone names
const devicePaths = {};
groupedData.forEach(device => {
const deviceId = device.key;
const path = device.values.map(d => zonePositions[d.key]); // Get the corresponding zone position for each zone_name
devicePaths[deviceId] = path;
});
console.log("Device Paths: ", devicePaths); // Debugging log to check paths
let currentIndex = 0;
let isPlaying = false;
// Animate the bubbles across the floorplan
function animateBubbles() {
if (!isPlaying) return;
// Loop through each device and move its bubble
Object.keys(devicePaths).forEach(deviceId => {
const devicePath = devicePaths[deviceId];
const deviceBubble = svg.select(`circle[fill="${colorScale(deviceId)}"]`);
if (!deviceBubble.empty()) {
deviceBubble.transition()
.duration(1000) // Adjust duration for smooth transition
.ease(d3.easeLinear)
.attr("cx", devicePath[currentIndex].x)
.attr("cy", devicePath[currentIndex].y);
}
});
// Move to the next index in the data
currentIndex++;
if (currentIndex < data.length) {
setTimeout(animateBubbles, 1000); // Adjust the timeout for better pacing
} else {
currentIndex = 0; // Reset to the beginning for loop
}
}
// Play/Pause button functionality
d3.select("#playPauseButton").on("click", function() {
isPlaying = !isPlaying;
if (isPlaying) {
d3.select("#playPauseButton").text("Pause");
animateBubbles(); // Start the animation
} else {
d3.select("#playPauseButton").text("Play");
currentIndex = 0; // Reset index when paused
}
});
});
}
</script>
</body>
</html>
this is what my output look likes :
My code is not displaying id names (from svg file) as label name correctly neither bubbles are moving, Can anyone help me in moving bubble zone to zone.