viewof moodGalaxy = {
const width = 800, height = 800;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("background", "#1b1c4a")
.style("font-family", "sans-serif")
.style("color", "white");
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", 24)
.attr("font-weight", "bold")
.style("fill", "white")
.text("Mood Galaxy: Navigate the Universe of Songs");
svg.append("text")
.attr("x", width / 2)
.attr("y", 55)
.attr("text-anchor", "middle")
.attr("font-size", 14)
.style("fill", "white")
.text("Each star is a song — placement is randomized, size by tempo, color by valence");
const radius = d3.scaleSqrt()
.domain([60, 200])
.range([2, 10]);
const galaxyColors = d3.scaleSequential()
.domain([0, 1])
.interpolator(d3.interpolateCubehelixDefault);
const nodes = filteredSongs.map(d => ({
...d,
x: width / 2 + (Math.random() - 0.5) * 300,
y: height / 2 + (Math.random() - 0.5) * 300,
r: radius(d.tempo),
fill: galaxyColors(d.valence)
}));
const simulation = d3.forceSimulation(nodes)
.force("center", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(1))
.force("collision", d3.forceCollide(d => d.r + 1))
.on("tick", ticked);
const node = svg.selectAll("path")
.data(nodes)
.join("path")
.attr("transform", d => `translate(${d.x},${d.y})`)
.attr("d", d3.symbol().type(d3.symbolStar).size(d => d.r * d.r * 20))
.attr("fill", d => d.fill)
.attr("opacity", 0.85)
.append("title")
.text(d => `${d.song} by ${d.artist}\nEnergy: ${d.energy}\nValence: ${d.valence}`);
function ticked() {
svg.selectAll("path")
.data(nodes)
.attr("transform", d => `translate(${d.x},${d.y})`);
}
const legendX = width - 200;
const legendY = height - 280;
svg.append("text")
.attr("x", legendX)
.attr("y", legendY)
.attr("font-weight", "bold")
.attr("fill", "white")
.text("Mood (Valence)");
[0, 0.25, 0.5, 0.75, 1].forEach((v, i) => {
svg.append("path")
.attr("transform", `translate(${legendX}, ${legendY + 20 + i * 20})`)
.attr("d", d3.symbol().type(d3.symbolStar).size(60))
.attr("fill", galaxyColors(v));
svg.append("text")
.attr("x", legendX + 20)
.attr("y", legendY + 25 + i * 20)
.attr("alignment-baseline", "middle")
.attr("fill", "white")
.attr("font-size", 11)
.text(v);
});
svg.append("text")
.attr("x", legendX)
.attr("y", legendY + 140)
.attr("font-weight", "bold")
.attr("fill", "white")
.text("Tempo (Bubble Size)");
[60, 120, 180].forEach((t, i) => {
svg.append("circle")
.attr("cx", legendX + 6)
.attr("cy", legendY + 160 + i * 20)
.attr("r", radius(t))
.attr("fill", "white")
.attr("opacity", 0.6);
svg.append("text")
.attr("x", legendX + 20)
.attr("y", legendY + 160 + i * 20)
.attr("alignment-baseline", "middle")
.attr("fill", "white")
.attr("font-size", 11)
.text(`${t} BPM`);
});
return svg.node();
}