Public
Edited
Aug 8
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Plot.plot({
marginLeft: 70,
title: 'Total streaming hours during a day',
y: { label: 'Hours streamed' },
marks: [
Plot.rectY(data, Plot.binX(
{y: 'sum'},
{
x: d => dateToGeneric24Hour(d['ts']),
y: d => ms_to_hrs(d['ms_played']),
scale: 'time'
}
)),
Plot.ruleY([0])
]
})
Insert cell
viewof sel_artist = Inputs.select(top_20_artists, { label:'Select an artist to highlight below:' })
Insert cell
Insert cell
// Plot.plot({
// legend
// })
Insert cell
Plot.plot({
width: width,
grid: true,
y: { label: 'month of the year', domain: months_short },
x: { label: 'hour of the day', ticks: 23, tickFormat: '%H:%M' },
// y: { label: 'day of the month', ticks: 12 },
y: { label: 'month of the year', ticks: 12 },
fy: { label: 'years', scale: 'number' },
color: {
scheme: "BuRd"
},
height: 8000,
marginRight: 50,
marks: [
Plot.dot(data, {
x: d => dateToGeneric24Hour(d['ts']),
y: d => months_short[new Date(d['ts']).getUTCMonth()],
fy: d => new Date(d['ts']).getFullYear(),
channels: {name: 'track_name', artist: 'artist_name', time: d => clock_time(d['ts']) },
// stroke: (d) => d['artist_name'] == sel_artist ? 'red' : 'grey',
fill: (d) => top_artists.indexOf(d['artist_name']) != -1 ? artist_colors(d['artist_name']) : 'grey',
// tip: true,
fillOpacity: 0.3,
// sort: { y: 'x' }
}),
Plot.tip(data, Plot.pointer({
x: d => dateToGeneric24Hour(d['ts']),
y: d => months_short[new Date(d['ts']).getUTCMonth()],
fy: d => new Date(d['ts']).getFullYear(),
// filter: (d) => d['track_name'],
title: (d) => [d.track_name, d.artist_name].join("\n\n")
}))
]
})
Insert cell
Plot.plot({
marginLeft: 130,
x: { domain: [0, 70] },
marks: [
Plot.barX(data,
Plot.groupY(
{x: "sum"},
{ y: "artist_name", x: d => ms_to_hrs(d["ms_played"]), sort: {y: "x", reverse: true, limit: 20}})),
Plot.ruleX([0])
]
})
Insert cell
Plot.plot({
marginLeft: 300,
marginBottom: 50,
x: { domain: [0, 20], label: 'hours streamed' },
y: { label: 'top tracks' },
marks: [
Plot.barX(data,
Plot.groupY(
{x: "sum"},
{y: "track_name", x: d => ms_to_hrs(d["ms_played"]), sort: {y: "x", reverse: true, limit: 20}}
)),
Plot.ruleX([0])
]
})
Insert cell
Insert cell
Insert cell
streaming_data_all.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
months_short = 'Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec'.split(', ')
Insert cell
total_hours = data.map(d => d['ms_played']).reduce((a, b) => a + b) / (1000 * 60 * 60)
Insert cell
years = [...new Set(data.map(d => new Date(d['ts']).getFullYear()))].sort((a, b) => a - b)
Insert cell
avg_hour_per_day = Math.round((years.length - 1) * 365 / total_hours * 100) / 100
Insert cell
/**
Brings any datetime to a single 24 hour scale format. Ignores the 'date' part of the time
@param dt_str `string` - takes date time string format
@returns `Date` - object format for a date
**/
function dateToGeneric24Hour(dt_str) {
const dt = new Date(dt_str)
// "January 31, 1975, 23:15:30 GMT+5:00"
return new Date(2000, 0, 1, dt.getHours(), dt.getMinutes(), dt.getSeconds())
}
Insert cell
/*
given a date string, convert the string to datetime in 24 clock format
@param <str> `dt_str` - the string format of the date
@return <str> - formatted string in HH:MM:SS 24 hour format
*/
function clock_time(dt_str) {
const dt = new Date(dt_str)
const hrs = ((dt.getUTCHours() < 10) ? '0' : '') + dt.getUTCHours()
const mins = ((dt.getUTCMinutes() < 10) ? '0' : '') + dt.getUTCMinutes()
const secs = ((dt.getUTCSeconds() < 10) ? '0' : '') + dt.getUTCSeconds()
return `${hrs}:${mins}:${secs}`
}
Insert cell
// grouped by artist_name
d3.group(data, d => d['artist_name'])
Insert cell
// grouped by `track_name`
d3.group(data, d => d['track_name'])
Insert cell
d3.sort(data, d => -d['ms_played'])
Insert cell
ms_to_hrs(10144422)
Insert cell
function ms_to_hrs(ms) {
return ms / (1000 * 60 * 60)
}
Insert cell
top_artists = d3.groupSort(data, v => -d3.sum(v, d => d.ms_played), d => d.artist_name).slice(0, 10)
Insert cell
artist_colors = d3.scaleOrdinal().domain(top_20_artists).range(d3.schemeObservable10)
Insert cell
function runme_once() {
// Replace with your actual Spotify API credentials
const CLIENT_ID = '773e44aa91654ed5982264f6c5b0c1a5';
const CLIENT_SECRET = 'f7ef145c61334f408c56ba7daf8443bb';
/**
* Get Spotify access token using Client Credentials Flow
*/
async function getAccessToken() {
const authString = `${CLIENT_ID}:${CLIENT_SECRET}`;
// const authBase64 = Buffer.from(authString).toString('base64');
const response = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
// 'Authorization': `Basic ${authBase64}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({ grant_type: 'client_credentials' })
});
const data = await response.json();
return data.access_token;
}
/**
* Get genres by artist name
* @param {string} artistName
*/
async function getGenresByArtistName(artistName) {
const token = await getAccessToken();
const searchUrl = `https://api.spotify.com/v1/search?q=${encodeURIComponent(artistName)}&type=artist&limit=1`;
const searchRes = await fetch(searchUrl, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const searchData = await searchRes.json();
if (!searchData.artists || searchData.artists.items.length === 0) {
console.log(`No artist found for "${artistName}"`);
return [];
}
const artist = searchData.artists.items[0];
console.log(`🎤 Found Artist: ${artist.name}`);
console.log(`🎧 Genres: ${artist.genres.join(', ')}`);
return artist.genres;
}
// ✅ Example usage:
return getGenresByArtistName('Taylor Swift');
}

Insert cell
runme_once()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// import {axios} from "npm:axios";
Insert cell
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