Public
Edited
May 4
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
viewof selection1 = Inputs.table(search,{required: false})
Insert cell
Insert cell
viewof selection2 = Inputs.table(data,{required: false})
Insert cell
Insert cell
viewof y = Inputs.range([0, 200], {step: 1})
Insert cell
Insert cell
Insert cell
Insert cell
THREE = await require("three@0.150.1");
Insert cell
chartExport = {
const text = d3.create("div")
.style("position", "absolute")
.style("top", "20px")
.style("left", "20px")
.style("z-index", 1)
.style("font-size", "2em")
.style("color", "white");

const container = d3.create("div").node();

const graph = new ForceGraph3D(container, { controlType: "orbit" })
.width([width])
.height([600])
.linkOpacity([0.5])
.backgroundColor("#000003") // Darker background
.linkWidth(arg => linkWidthScale(arg.value) / 8) // Scale link width dynamically
.nodeAutoColorBy("genre")
.linkColor("gray")
.onNodeHover((arg) => {
text.text(arg ? "Song Genre: " + arg.genre : "");
})
.nodeLabel("name") //🙃 changed that from "id"
.graphData(dataset)
.showNavInfo(true)
.onNodeClick(node => {
// Aim at clicked node
const distance = 40;
const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);

const newPos = node.x || node.y || node.z
? { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }
: { x: 0, y: 0, z: distance };

graph.cameraPosition(newPos, node, 3000);
});

// Customize nodes in "selection" dataset
graph.nodeThreeObject(node => {
if (selectionSet.has(node.id)) { // Check if the node is in the "selection" dataset
console.log(`Node in selection: ${node.id}`); // Debugging information
const material = new THREE.MeshBasicMaterial({ color: "yellow", transparent: true, opacity: 0.9 });
const sphere = new THREE.Mesh(new THREE.SphereGeometry(15), material); // Larger sphere for selected nodes
return sphere;
}

console.log(`Node not in selection: ${node.id}`); // Debugging information
return null; // Default appearance for other nodes
});

return html`${container}${text.node()}`;
}
Insert cell
Insert cell
Insert cell
import {Pack} from "@d3/pack"
Insert cell
selection = [...new Set([...selection1, ...selection2])]
Insert cell
selectionSet = new Set(selection.map(song => song.id))
Insert cell
function createRealData(selection, data, y) {
// Get 200 random unique songs from data
const randomSongs = data.sort(() => 0.5 - Math.random()).slice(0, y);

// Combine random songs with all songs in selection
const combinedSet = [...new Set([...selection, ...randomSongs])]; // Ensures uniqueness

return combinedSet;
}

Insert cell
realdata = generateDataButton ? createRealData(selection, data,y) : selection;
Insert cell
ForceGraph3D = require('3d-force-graph@1.77.0/dist/3d-force-graph.min.js')
Insert cell
dataset = createSongGraph(realdata);
Insert cell
linkWidthScale = d3.scaleSqrt().domain([0,1]).range([0,2])

Insert cell
function createSongGraph(songs) {
// Assign unique group numbers based on track_genre
const genreGroups = {};
let groupCounter = 1;

const nodes = songs.map(song => {
if (!(song.track_genre in genreGroups)) {
genreGroups[song.track_genre] = groupCounter++;
}
// 🙃 changed here to id and kept song names in "name"
return { id: song.id, name: song.track_name, group: genreGroups[song.track_genre], genre: song.track_genre };
});

const links = [];

// 🙃 below changed all source target definitions to song.id instad of song.track_name
songs.forEach((songA, i) => {
// Get similarities for songs within the same genre
const sameGenreLinks = songs
.filter((songB, j) => i !== j && songA.track_genre === songB.track_genre)
.filter(songB => Math.abs(songA.track_score - songB.track_score) <= 5) // Ensure difference is <= 5
.map(songB => ({ target: songB.id, value: Math.abs(songA.track_score - songB.track_score) }))
.sort((a, b) => a.value - b.value) // Sort by closest scores
.slice(0, 5); // Keep only the closest 5 links within the same genre

// Add up to 5 links within the same genre
links.push(...sameGenreLinks.map(({ target, value }) => ({ source: songA.id, target, value })));

if (sameGenreLinks.length === 0) {
// If no matches within the same genre, allow up to 3 matches outside the genre
const differentGenreLinks = songs
.filter((songB, j) => i !== j && songA.track_genre !== songB.track_genre)
.filter(songB => Math.abs(songA.track_score - songB.track_score) <= 50) // Ensure difference is <= 10
.map(songB => ({ target: songB.id, value: Math.abs(songA.track_score - songB.track_score) }))
.sort((a, b) => a.value - b.value) // Sort by closest scores
.slice(0, 3); // Keep only the closest 3 links for different genres

// Add up to 3 links to different genres
links.push(...differentGenreLinks.map(({ target, value }) => ({ source: songA.id, target, value })));
} else {
// Otherwise, only match up to 1 song outside the genre
const differentGenreLinks = songs
.filter((songB, j) => i !== j && songA.track_genre !== songB.track_genre)
.filter(songB => Math.abs(songA.track_score - songB.track_score) <= 3) // Ensure difference is <= 10
.map(songB => ({ target: songB.id, value: Math.abs(songA.track_score - songB.track_score) }))
.sort((a, b) => a.value - b.value) // Sort by closest scores
.slice(0, 1); // Keep only the closest 1 link for different genres

// Add one link to a different genre
links.push(...differentGenreLinks.map(({ target, value }) => ({ source: songA.id, target, value })));
}
});

return { nodes, links };
}
Insert cell
data = {
const rawData = await FileAttachment("spotify_dataset_with_scoring - dataset.csv").csv({typed: true})
const dataWithAddedIDs = rawData.map(song => ({...song, id: getUniqueishId(song)}))
return dedupeBy(dataWithAddedIDs, 'id')
}
Insert cell
getUniqueishId = (song) => song.artists + " | " + song.track_name;
Insert cell
new Set(data.map(getUniqueishId))
Insert cell
dedupeBy = (arr, idKey) => {
const seen = new Set();
return arr.filter(o => {
if (seen.has(o[idKey])) return false;
seen.add(o[idKey]);
return true;
});
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more