Public
Edited
May 14
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
duration_by_year = {
const grouped = new Map();

for (const row of data) {
const year = +row.year;
const duration = +row.duration_ms;

if (!isNaN(year) && !isNaN(duration) && duration > 0) {
if (!grouped.has(year)) grouped.set(year, []);
grouped.get(year).push(duration);
}
}

return Array.from(grouped, ([year, durations]) => {
if (durations.length === 0) return null;
const avg = durations.reduce((a, b) => a + b) / durations.length;
return {
year,
avg_duration_min: avg / 60000
};
}).filter(d => d !== null);
}
Insert cell
Insert cell
Plot.plot({
marks: [
Plot.lineY(duration_by_year, {
x: "year",
y: "avg_duration_min",
stroke: "#1DB954",
strokeWidth: 2.5
})
],
x: {
label: "Year",
grid: true,
tickFormat: d => d,
ticks: 10,
labelAnchor: "center",
labelFontWeight: "bold"
},
y: {
label: "Duration (minutes)",
grid: true,
labelFontWeight: "bold"
},
style: {
background: "black",
color: "white",
fontFamily: "Inter, sans-serif",
fontSize: 13
},
marginLeft: 60,
marginBottom: 60,
width: 800,
height: 400,
caption: "Figure 1: Average song duration over time (1921–2020)."
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Plot.plot({
marks: [
Plot.lineY(energy_by_year, {
x: "year",
y: "avg_energy",
stroke: "#444",
strokeWidth: 1.5,
strokeOpacity: 0.2
}),
Plot.lineY(
energy_by_year.filter(d => d.year <= selected_year),
{
x: "year",
y: "avg_energy",
stroke: "#1DB954",
strokeWidth: 2.5
}
)
],
x: {
label: "Year",
ticks: 10,
labelFontWeight: "bold",
grid: true
},
y: {
label: "Energy (0 to 1)",
labelFontWeight: "bold",
grid: true
},
style: {
background: "black",
color: "white",
fontFamily: "Inter, sans-serif",
fontSize: 13
},
marginBottom: 60,
width: 800,
height: 400,
caption: `Figure 2: Average energy of Spotify songs (1921–${selected_year}).`
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
explicit_share_by_year = {
const grouped = new Map();

for (const row of data) {
const year = +row.year;
const isExplicit = row.explicit === "1" || row.explicit === 1;

if (!isNaN(year)) {
if (!grouped.has(year)) grouped.set(year, []);
grouped.get(year).push(isExplicit);
}
}

return Array.from(grouped, ([year, flags]) => {
const total = flags.length;
const explicitCount = flags.filter(Boolean).length;
return {
year,
percent_explicit: (explicitCount / total) * 100
};
}).filter(d => !isNaN(d.percent_explicit));
}
Insert cell
Insert cell
Plot.plot({
marks: [
Plot.lineY(explicit_share_by_year, {
x: "year",
y: "percent_explicit",
stroke: "#1DB954",
strokeWidth: 2.5
})
],
x: {
label: "Year",
grid: true,
tickFormat: d => d,
ticks: 10,
labelFontWeight: "bold"
},
y: {
label: "Explicit Songs (%)",
grid: true,
labelFontWeight: "bold"
},
style: {
background: "black",
color: "white",
fontFamily: "Inter, sans-serif",
fontSize: 13
},
marginLeft: 60,
marginBottom: 60,
width: 800,
height: 400,
caption: "Figure 3: Percentage of explicit songs by release year (1921–2020)."
})
Insert cell
Insert cell
Insert cell
Insert cell
valence_danceability = data
.map(d => ({
valence: +d.valence,
danceability: +d.danceability
}))
.filter(d =>
!isNaN(d.valence) &&
!isNaN(d.danceability) &&
d.valence >= 0 && d.valence <= 1 &&
d.danceability >= 0 && d.danceability <= 1
);
Insert cell
valence_danceability_sampled = d3.shuffle(valence_danceability).slice(0, 5000)
Insert cell
Insert cell
Plot.plot({
marks: [
Plot.dot(valence_danceability_sampled, {
x: "valence",
y: "danceability",
fill: d => {
const intensity = Math.round((d.valence + d.danceability) / 2 * 60 + 30);
return `hsl(145, 70%, ${intensity}%)`;
},
r: 2.3,
stroke: "none",
fillOpacity: 0.85
}),

Plot.linearRegressionY(valence_danceability_sampled, {
x: "valence",
y: "danceability",
stroke: "white",
strokeWidth: 2
})
],
x: {
label: "Valence (Musical Positivity)",
labelFontWeight: "bold",
grid: true
},
y: {
label: "Danceability",
labelFontWeight: "bold",
grid: true
},
style: {
background: "black",
color: "white",
fontFamily: "Inter, sans-serif",
fontSize: 13
},
width: 800,
height: 420,
marginBottom: 60,
caption: "Figure 4: Relationship between valence and danceability (sample of 5,000 Spotify tracks)."
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Inputs.table(data.slice(0, 10))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {toc} from "@nebrius/indented-toc"
Insert cell
import {howTo} from "@clokman/howto"
Insert cell
data = FileAttachment("data (1).csv").csv()
Insert cell
import {Plot} from "@observablehq/plot"
Insert cell
import { slider } from "@jashkenas/inputs"
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