Public
Edited
Nov 5, 2023
Insert cell
Insert cell
audio_features = ({
acousticness: 0.262,
analysis_url:
"https://api.spotify.com/v1/audio-analysis/4PXt1De2NogcDlcDkf9jJa",
danceability: 0.775,
duration_ms: 216000,
energy: 0.694,
id: "4PXt1De2NogcDlcDkf9jJa",
instrumentalness: 0,
key: 8,
liveness: 0.143,
loudness: -6.367,
mode: 1,
speechiness: 0.092,
tempo: 146.02,
time_signature: 4,
track_href: "https://api.spotify.com/v1/tracks/4PXt1De2NogcDlcDkf9jJa",
type: "audio_features",
uri: "spotify:track:4PXt1De2NogcDlcDkf9jJa",
valence: 0.891
})
Insert cell
Insert cell
radialChart = {
// Assuming `audioFeatures` is your JSON object
const audioFeatures = {
danceability: 0.808,
energy: 0.626,
speechiness: 0.168,
acousticness: 0.00187,
instrumentalness: 0.159,
liveness: 0.376,
valence: 0.369
};

const features = Object.keys(audioFeatures);
const values = Object.values(audioFeatures);
const maxValue = Math.max(...values);

// Create a radial scale
const radialScale = d3.scaleLinear().domain([0, maxValue]).range([0, 200]); // Adjust the outer radius accordingly

const angleToCoordinate = (angle, value) => {
const x = Math.cos(angle) * radialScale(value);
const y = Math.sin(angle) * radialScale(value);
return { x, y };
};

const lines = features.map((feature, i) => {
const angle = ((Math.PI * 2) / features.length) * i;
const coordinate = angleToCoordinate(angle, audioFeatures[feature]);
return { ...coordinate, feature };
});

// Create the plot
const chart = Plot.plot({
width: 500,
height: 500,
margin: 70,
marks: [
Plot.line(lines, {
x: "x",
y: "y",
stroke: "black",
curve: "cardinal-closed"
}),
Plot.dot(lines, { x: "x", y: "y", r: 4, fill: "currentColor" }),
Plot.text(lines, {
x: "x",
y: "y",
text: (d) => d.feature,
dy: -10,
dx: 5
})
],
x: {
axis: null
},
y: {
axis: null
}
});
return chart;
}
Insert cell
function createRadialChartFromFeatures(fullAudioFeatures) {
// Filter the features to include only those with values between 0 and 1
const audioFeatures = Object.fromEntries(
Object.entries(fullAudioFeatures).filter(
([key, value]) =>
key != "mode" && typeof value === "number" && value >= 0 && value <= 1
)
);

const features = Object.keys(audioFeatures);
const values = Object.values(audioFeatures);
const maxValue = Math.max(...values);

// Create a radial scale
const radialScale = d3.scaleLinear().domain([0, maxValue]).range([0, 200]); // Adjust the outer radius accordingly

const angleToCoordinate = (angle, value) => {
const x = Math.cos(angle) * radialScale(value);
const y = Math.sin(angle) * radialScale(value);
return { x, y };
};

const lines = features.map((feature, i) => {
const angle = ((Math.PI * 2) / features.length) * i;
const coordinate = angleToCoordinate(angle, audioFeatures[feature]);
return { ...coordinate, feature };
});

// Create the plot
const chart = Plot.plot({
width: 500,
height: 500,
margin: 70,
marks: [
Plot.line(lines, {
x: "x",
y: "y",
stroke: "black",
curve: "cardinal-closed"
}),
Plot.dot(lines, { x: "x", y: "y", r: 4, fill: "currentColor" }),
Plot.text(lines, {
x: "x",
y: "y",
text: (d) => d.feature,
dy: -10,
dx: 5
})
],
x: {
axis: null
},
y: {
axis: null
}
});

return chart;
}
Insert cell
createRadialChartFromFeatures(audio_features)
Insert cell
Plot.plot({
width: 450,
projection: {
type: "azimuthal-equidistant",
rotate: [0, -90],
// Note: 0.625° corresponds to max. length (here, 0.5), plus enough room for the labels
domain: d3.geoCircle().center([0, 90]).radius(0.625)()
},
color: { legend: true },
marks: [
// grey discs
Plot.geo([0.5, 0.4, 0.3, 0.2, 0.1], {
geometry: (r) => d3.geoCircle().center([0, 90]).radius(r)(),
stroke: "black",
fill: "black",
strokeOpacity: 0.3,
fillOpacity: 0.03,
strokeWidth: 0.5
}),

// white axes
Plot.link(longitude.domain(), {
x1: longitude,
y1: 90 - 0.57,
x2: 0,
y2: 90,
stroke: "white",
strokeOpacity: 0.5,
strokeWidth: 2.5
}),

// tick labels
Plot.text([0.3, 0.4, 0.5], {
x: 180,
y: (d) => 90 - d,
dx: 2,
textAnchor: "start",
text: (d) => `${100 * d}%`,
fill: "currentColor",
stroke: "white",
fontSize: 8
}),

// axes labels
Plot.text(longitude.domain(), {
x: longitude,
y: 90 - 0.57,
text: Plot.identity,
lineWidth: 5
}),

// areas
Plot.area(points, {
x1: ({ key }) => longitude(key),
y1: ({ value }) => 90 - value,
x2: 0,
y2: 90,
fill: "name",
stroke: "name",
curve: "cardinal-closed"
}),

// points
Plot.dot(points, {
x: ({ key }) => longitude(key),
y: ({ value }) => 90 - value,
fill: "name",
stroke: "white"
}),

// interactive labels
Plot.text(
points,
Plot.pointer({
x: ({ key }) => longitude(key),
y: ({ value }) => 90 - value,
text: (d) => `${(100 * d.value).toFixed(0)}%`,
textAnchor: "start",
dx: 4,
fill: "currentColor",
stroke: "white",
maxRadius: 10
})
),

// interactive opacity on the areas
() =>
svg`<style>
g[aria-label=area] path {fill-opacity: 0.1; transition: fill-opacity .2s;}
g[aria-label=area]:hover path:not(:hover) {fill-opacity: 0.05; transition: fill-opacity .2s;}
g[aria-label=area] path:hover {fill-opacity: 0.3; transition: fill-opacity .2s;}
`
]
})
Insert cell
longitude = d3.scalePoint(new Set(Plot.valueof(points, "key")), [180, -180]).padding(0.5).align(1)
Insert cell
points = [
{
name: "track",
key: "acousticness",
value: 0.7
}
]
Insert cell
Object.fromEntries(
Object.entries(audio_features).filter(
([key, value]) =>
key != "mode" && typeof value === "number" && value >= 0 && value <= 1
)
)
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