function createSongGraph(songs) {
const genreGroups = {};
let groupCounter = 1;
const nodes = songs.map(song => {
if (!(song.track_genre in genreGroups)) {
genreGroups[song.track_genre] = groupCounter++;
}
return { id: song.id, name: song.track_name, group: genreGroups[song.track_genre], genre: song.track_genre };
});
const links = [];
songs.forEach((songA, i) => {
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)
.map(songB => ({ target: songB.id, value: Math.abs(songA.track_score - songB.track_score) }))
.sort((a, b) => a.value - b.value)
.slice(0, 5);
links.push(...sameGenreLinks.map(({ target, value }) => ({ source: songA.id, target, value })));
if (sameGenreLinks.length === 0) {
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)
.map(songB => ({ target: songB.id, value: Math.abs(songA.track_score - songB.track_score) }))
.sort((a, b) => a.value - b.value)
.slice(0, 3);
// 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 };
}