import io.data2viz.color.*
import io.data2viz.geom.*
import io.data2viz.math.*
import io.data2viz.viz.*
import io.data2viz.force.*
import io.data2viz.dsv.Dsv
import org.w3c.fetch.Response
import kotlinx.browser.window
import kotlin.js.Promise
val vizSize = 900.0
val rockColor = Colors.Web.red
val folkColor = Colors.Web.orange
val popColor = Colors.Web.blue
val punkColor = Colors.Web.green
data class Style(
val rock: Percent,
val folk: Percent,
val pop: Percent,
val punk: Percent
) {
val color = Colors.rgb(
((rockColor.r * rock) + (folkColor.r * folk) + (popColor.r * pop) + (punkColor.r * punk)).toInt(),
((rockColor.g * rock) + (folkColor.g * folk) + (popColor.g * pop) + (punkColor.g * punk)).toInt(),
((rockColor.b * rock) + (folkColor.b * folk) + (popColor.b * pop) + (punkColor.b * punk)).toInt()
)
}
// An artist with the number of different songs played and musical style
data class Artist(
val name:String,
val songs:Int,
val style: Style
)
// Parse an artist from the CSV file
private fun parsePlaylist(row: List<String>) = Artist(
name = row[0],
songs = row[1].toInt(),
style = Style(
rock = (row[2].toInt() * 10.0).pct,
folk = (row[3].toInt() * 10.0).pct,
pop = (row[4].toInt() * 10.0).pct,
punk = (row[5].toInt() * 10.0).pct
)
)
fun main() {
// Animation counter
var animationCount = 0
// Storing the visual nodes (texts and bubbles)
val particleTexts = mutableListOf<TextNode>()
val particleNodes = mutableListOf<CircleNode>()
// Requesting data (CSV file)
val request: Promise<Response> =
window.fetch("https://docs.google.com/spreadsheets/d/e/2PACX-1vRRlQ8Cd69-0ysrA2TK33hIN1HkqF37XtbjCiS5jvBWj8A6BzuoaWzNUyPdCdFnCLMf-keu-6V8KYDf/pub?gid=0&single=true&output=csv")
// On download, parse file and build playlist
request.then { result ->
result.text().then { csvText ->
val parsedArtists = Dsv()
.parseRows(csvText)
.drop(1)
.map { parsePlaylist(it) }
// Node 0 is the listener keep it apart as it will be treated differently
val playlist = mutableListOf(
Artist("Louise", 10, Style(0.pct, 0.pct, 0.pct, 0.pct))
)
playlist.addAll(parsedArtists)
// Creating force simulation
val simulation = createSimulation(playlist)
// Finally create the vizualisation itself
viz {
size = size(vizSize, vizSize)
simulation.nodes.forEachIndexed { index, node ->
particleNodes += circle {
fill = node.domain.style.color
radius = 2.0 + node.domain.songs
}
particleTexts += text {
textColor = node.domain.style.color
textContent = playlist[node.index].name
textAlign = textAlign(TextHAlign.MIDDLE, TextVAlign.MIDDLE)
if (index == 0) fontWeight = FontWeight.BOLD
}
}
animation {
animationCount++
if (animationCount > 60) {
animationCount = 0
}
simulation.nodes.forEach { node ->
particleTexts[node.index].x = node.x
particleTexts[node.index].y = node.y - 15.0 - node.domain.songs
particleNodes[node.index].x = node.x
particleNodes[node.index].y = node.y
}
}
}.bindRendererOnNewCanvas()
}
}
}
private fun createSimulation(playlist: List<Artist>) = forceSimulation<Artist> {
// This is used to center the whole simulation on screen
forceCenter {
center = point(vizSize / 2, vizSize / 2)
}
// Links between artists and their musical style
forcePoint {
pointGet = { Point(100.0, 100.0) }
strengthGet = { domain.style.rock / 5.0 }
}
forcePoint {
pointGet = { Point(vizSize - 100.0, 100.0) }
strengthGet = { domain.style.folk / 5.0 }
}
forcePoint {
pointGet = { Point(vizSize - 100.0, vizSize - 100.0) }
strengthGet = { domain.style.pop / 5.0 }
}
forcePoint {
pointGet = { Point(100.0, vizSize - 100.0) }
strengthGet = { domain.style.punk / 5.0 }
}
// Building links between artists and Louise
forceLink {
linkGet = {
if (this.index == 0) {
(1 .. playlist.size).mapIndexed { index, artist ->
Link<Artist>(this, nodes[index], 270.0 - (nodes[index].domain.songs * 10.0))
}
} else {
listOf()
}
}
iterations = 1
}
// Add collisions to avoid overlapping of artists
forceCollision {
radiusGet = {
if (this.index == 0) {
1.0
} else {
22.0 + (domain.songs * 2.5)
}
}
iterations = 1
}
// The whole dataset for this simulation is the "playlist" object
domainObjects = playlist
}