Published
Edited
Jan 24, 2022
2 forks
3 stars
Insert cell
Insert cell
Insert cell
<style>
.container {
margin-bottom: 0.5rem;
}
h2 {
margin-bottom: 0;
}
.artist {
text-transform: uppercase;
}
.year {
margin-bottom: 0.5rem;
}
p {
margin: 0;
text-transform: capitalize;
}
.attribute {
font-style: italic;
text-transform: none;
}
.radio-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.capitalized {
text-transform: capitalize;
}
.label {
display: grid;
grid-template-columns: 6fr 2fr 3fr;
width: 100%;
}
</style>
<div class="container">
<h2>${data[selectedGlyphIndex].title}</h2>
<p class="artist">by ${data[selectedGlyphIndex].artist}</p>
<p><span class="attribute">Genre group: </span>${data[selectedGlyphIndex].category}</p>
<p><span class="attribute">Genre: </span>${data[selectedGlyphIndex].genre}</p>
<p class="year"><span class="attribute">Year: </span>${data[selectedGlyphIndex].year}</p>
${Object.keys(data[selectedGlyphIndex].attributes).map((attribute, i) => html`
<div class="radio-item">
<input
type="radio"
id="${attribute}"
name="attributes"
checked=${mutable selectedAttribute === attribute}
onchange=${() => mutable selectedAttribute = attribute }
/>
<label for="${attribute}" class="label">
<span>
<span class="capitalized">(${attribute.charAt(0)}) ${attribute} </span>
</span>
<span>
${data[selectedGlyphIndex].attributes[attribute]}%
</span>
<span>
${data[selectedGlyphIndex].originalAttributes[attribute + 'Orig']} ${attributeUnits[attribute]}
</span>
</label>
</div>
`)}
</div>
Insert cell
radarChart = {
const svg = d3.create("svg")
.attr("width", radarChartWidth)
.attr("height", radarChartHeight);

// circular grid
ticks.forEach(t =>
svg.append("circle")
.attr("cx", radarChartOffsets.x)
.attr("cy", radarChartOffsets.y)
.attr("fill", "none")
.attr("stroke", "gray")
.attr("r", radialScale(t))
);

// circular grid labels
ticks.forEach(t =>
svg.append("text")
.attr("x", radarChartOffsets.x * 1.02)
.attr("y", radarChartOffsets.y - radialScale(t))
.style("font-size", "10px")
.style("font-family", "Roboto, Arial, sans-serif")
.text(t.toString() + '%')
);
for (var i = 0; i < features.length; i++) {
let ft_name = features[i];
let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
let line_coordinate = angleToCoordinate(angle, 100, radarChartOffsets);
let label_coordinate = angleToCoordinate(angle, 110, radarChartOffsets);

//draw axis line
svg.append("line")
.attr("x1", radarChartOffsets.x)
.attr("y1", radarChartOffsets.y)
.attr("x2", line_coordinate.x)
.attr("y2", line_coordinate.y)
.attr("stroke","black");

//draw axis label
svg.append("text")
.attr("x", label_coordinate.x)
.attr("y", label_coordinate.y)
.style("font-size", "10px")
.style("font-family", "Roboto, Arial, sans-serif")
.text(ft_name.toUpperCase().charAt(0));
}

let d = data[selectedGlyphIndex].attributes;
let color_radar = color(data[selectedGlyphIndex].category);
let coordinates = getPathCoordinates(d, radarChartOffsets);

const t = svg.transition().duration(1000);
//draw the path element
svg.append("path")
.data([coordinates])
.transition(t)
.attr("d", line)
.attr("fill", color_radar)
.attr("stroke-opacity", 1)
.attr("opacity", 0.5);

return svg.node();
}
Insert cell
Insert cell
chart2 = {
let svg, t, x, y, yAxis;
// used to store the glyphs indexes in the data array
const index = d3.local();

const tooltip = makeToolTip();

// setup svg, scales, axes and grid
svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
x = makeXScale();
y = makeYScale(mutable selectedAttribute);
makeXAxis(svg, x);
yAxis = makeYAxis(svg, y);
makeGrid(svg, x, y);
t = svg.transition().duration(500);
let glyphs = svg.selectAll("path")
.data(data, d => d.title)
.join(
enter => enter.append("path")
.attr("transform", d =>`translate(${x(+d.year)},${y(+d.attributes[mutable selectedAttribute])})`)
.attr("class", "glyph-small")
.attr("fill", d => color(d.category))
.datum(d => getPathCoordinates(d.attributes, {x: 0, y: 0}, 0.25))
.attr("d", d => line(d) + 'Z') // Z ensures that the path will be closed
.each(function(d, i) {
index.set(this, i); // Store index in local variable.
})
.on("click", function (event, d) {
const previouslySelected = d3.select(".selected");
if(previouslySelected) previouslySelected.classed("selected", false);
const selected = d3.select(this);
if(selected) selected.classed("selected", true);
mutable selectedGlyphIndex = index.get(this);
})
.on("mouseover", function (event) {
d3.selectAll(".glyph-small:not(.selected)").style("opacity", 0.4);
const currentGlyph = d3.select(this);
currentGlyph.style("opacity", "1");
tooltip.style("visibility", "visible");
tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");
tooltip.text(currentGlyph.datum().title + ' - ' + currentGlyph.datum().artist);
})
.on("mouseout", function (event) {
d3.selectAll(".glyph-small").style("opacity", 1);
tooltip.style("visibility", "hidden");
}),
);

return Object.assign(svg.node(), {
update(selectedAttr) {
t = svg.transition().duration(500);
y = makeYScale(selectedAttr);

d3.select(".yaxis").remove();
yAxis = makeYAxis(svg, y);

glyphs = glyphs.data(data)
.call(glyphs => glyphs
.transition(t)
.attr("transform", d=>`translate(${x(+d.year)},${y(+d.attributes[selectedAttr])})`));
}
});
}
Insert cell
mutable selectedAttribute = 'valence'
Insert cell
mutable selectedGlyphIndex = 0;
Insert cell
chart2.update(selectedAttribute)
Insert cell
Insert cell
Insert cell
Insert cell
attributeUnits = ({
valence: "(0-100)",
loudness:"dB",
bpm: "BPM",
speechiness: "(0-100)",
danceability: "(0-100)",
popularity: "(0-100)"
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
margin = ({top: 40, right: 30, bottom: 40, left: 40})
Insert cell
height = 1000
Insert cell
color = d3.scaleOrdinal(data.map(d => d.category), d3.schemeTableau10)
Insert cell
unselectGlyph = (glyph) => {
glyph.attr("stroke", null);
glyph.attr("stroke-width", "0");
glyph.classed("selected", false);
}
Insert cell
makeToolTip = () => d3.select("body").append("div")
.attr("class", "svg-tooltip")
.style("background", '#4a4a4a')
.style("color", "#fff")
.style("height", 'auto')
.style("padding", "2px 10px")
.style("border-radius", "10px")
.style("position", "absolute")
.style("visibility", "hidden")
.text("");
Insert cell
Insert cell
radialScale = d3.scaleLinear()
.domain([0, 100])
.range([0, radarChartOffsets.x * 0.85]);
Insert cell
ticks = [20,40,60,80,100];
Insert cell
features = ["valence","loudness", "bpm","speechiness","danceability","popularity"];
Insert cell
function getPathCoordinates(data_point, offsets = {x: 0, y: 0}, scale = 1){
let coordinates = [];
for (var i = 0; i < features.length; i++){
let ft_name = features[i];
let angle = (Math.PI / 2) + (2 * Math.PI * i / features.length);
coordinates.push(angleToCoordinate(angle, data_point[ft_name], offsets, scale));
}
return coordinates;
}
Insert cell
function angleToCoordinate(angle, value, offsets, scale = 1){
let x = Math.cos(angle) * radialScale(value);
let y = Math.sin(angle) * radialScale(value);
return {"x": (offsets.x + x) * scale , "y": (offsets.y - y) * scale};
}
Insert cell
radarChartWidth = 250
Insert cell
radarChartHeight = 250
Insert cell
radarChartOffsets = ({ x: (radarChartWidth / 2), y: (radarChartHeight / 2) });
Insert cell
## Fullscreen

Insert cell
function fullscreen({
className = 'custom-fullscreen',
style = null,
breakLayoutAtCell = 2,
hideAfter = 9999,
hideBefore = 0,
left = 66,
right = 33,
button
} = {}) {
//WORK INITIALLY FROM @mootari/fullscreen-layout-demo
//THEN ADAPTED IN @severo/two-columns-layout-in-fullscreen-mode

function hide({ hideAfter = Infinity, hideBefore = 0, reset = false } = {}) {
let cells = document.getElementsByClassName('observablehq');
for (let i = 0; i < cells.length; i++) {
if ((i < hideBefore || i > hideAfter) && !reset)
cells[i].style.display = "none";
else cells[i].style.display = "block";
}
// console.log("Hide", cells);
return cells;
}

// Superfluous bling.
const buttonStyle =
style != null
? style
: 'font-size:1rem;font-weight:bold;padding:8px;background:hsl(50,100%,90%);border:5px solid hsl(40,100%,50%); border-radius:4px;box-shadow:0 .5px 2px 1px rgba(0,0,0,.2);cursor: pointer';

button = button || html`<button style="${buttonStyle}">Toggle fullscreen!`;

// Vanilla version for standards compliant browsers.
if (document.documentElement.requestFullscreen) {
button.onclick = () => {
if (document.location.host.match(/static.observableusercontent.com/))
hide({ hideBefore, hideAfter });
const parent = document.documentElement;
const cleanup = () => {
if (document.fullscreenElement) return;
parent.classList.remove(className);
if (document.location.host.match(/static.observableusercontent.com/))
hide({ reset: true });
document.removeEventListener('fullscreenchange', cleanup);
};
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
parent.requestFullscreen().then(() => {
parent.classList.add(className);
// Can't use {once: true}, because event fires too late.
document.addEventListener('fullscreenchange', cleanup);
});
}
};
}

// Because Safari is the new IE.
// Note: let as in https://observablehq.com/@mootari/fullscreen-layout-demo.
// The button will not toggle between states
else {
const screenfull = require('screenfull@4.2.0/dist/screenfull.js').catch(
() => window['screenfull']
);
// We would need some debouncing, in case screenfull isn't loaded
// yet and user clicks frantically. Then again, it's Safari.
button.onclick = () => {
screenfull.then(sf => {
const parent = document.documentElement;
sf.request(parent).then(() => {
const cleanup = () => {
if (sf.isFullscreen) return;
parent.classList.remove(className);
sf.off('change', cleanup);
};
parent.classList.add(className);
sf.on('change', cleanup);
});
});
};
}

// Styles don't rely on the :fullscreen selector to avoid interfering with
// Observable's fullscreen mode. Instead a class is applied to html during
// fullscreen.
return html`
${button}
<style>
html.${className},
html.${className} body {
overflow: auto;
height: 100vh;
width: 100vw;
}
html.${className} .observablehq-root {
padding: 0 0.5rem;
}
html.${className} body > div {
display: flex;
flex-direction: column;
justify-content: center;
flex-wrap: wrap;
gap: 0.25rem;
background: white;
height: 100%;
width: auto;
overflow: auto;
position: relative;
}
html.${className} body > div > div {
margin-bottom: 0 !important;
min-height: 0 !important;
width: ${left}vw;
max-height: 100%;
overflow: auto;
padding: .25rem;
box-sizing: border-box;
margin: 0;
}
html.${className} .observablehq:nth-of-type(${breakLayoutAtCell + hideBefore}) {
width: ${right}vw;
flex-basis: 95%;
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
}
`;
}
Insert cell
Insert cell
line = d3.line()
.x(d => d.x )
.y(d => d.y);
Insert cell
Insert cell
htl = require("htl@0.2")
Insert cell
html = htl.html
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