Published
Edited
May 25, 2020
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

const gx = svg.append("g")
.call(xAxis);

const gy = svg.append("g")
.call(yAxis);

svg.append("g")
.call(grid);
const circles =
svg.append("g")
.selectAll("circle")
.data(data, d => d.name)
.join("circle")
.attr("cx", d => x(d.povs))
.attr("cy", d => y(d.mean))
.attr("r", d => radiusScale(d.total))
.attr("stroke", "steelblue")
.attr("fill", "steelblue")
.attr("stroke-width", 0)
.attr("fill-opacity", 0.5)
.on("mouseover", tooltipMouseOver)
.on("mouseout", tooltipMouseOut);

const labels = svg.append("g")
.attr("font-family", "sans-serif")
.style('font-size', fontSize)
.selectAll("text")
.data(data)
.join("text")
.attr("dy", "0.35em")
.attr("x", d => x(d.povs) + radiusScale(d.total) + 7)
.attr("y", d => y(d.mean))
.text(d => d.name);
svg
.selectAll('text').style('font-size', fontSize)
return Object.assign(svg.node(), {
update(xAttribute, yAttribute, aAttribute) {
x.domain([0, d3.max(data, d => d[xAttribute])]);
y.domain([0, d3.max(data, d => d[yAttribute])]);
radiusScale.domain([0, d3.max(data, d => d[aAttribute])]);
const t = svg.transition()
.duration(750);
circles
.transition(t)
.delay((d, i) => i * 20)
.attr('r', d => radiusScale(d[aAttribute]))
.attr("cx", d => x(d[xAttribute]))
.attr("cy", d => y(d[yAttribute]))
labels
.transition(t)
.delay((d, i) => i * 20)
.attr('x', d => x(d[xAttribute]) + radiusScale(d[aAttribute]) + 7)
.attr('y', d => y(d[yAttribute]));
gx.transition(t)
.call(xAxis)
.selectAll(".tick")
.delay((d, i) => i * 20);
gy.transition(t)
.call(yAxis)
.selectAll(".tick")
.delay((d, i) => i * 20)
svg
.selectAll('text').style('font-size', fontSize)
}
});
}
Insert cell
upt = chart.update(xAttribute, yAttribute, aAttribute)
Insert cell
dataSource = await FileAttachment("teotw-chapter-pov.csv").text()
Insert cell
data = {
const data = d3.csvParse(dataSource, ({character, chapter_abbr, chapter_order, chapter, word_count}) => ({character, chapter, chapter_abbr, chapter_order: +chapter_order, word_count: +word_count}))
const reducedData = data.reduce((characters, current) => {
let character = characters.find(c => c.name === current.character)
if (!character) {
character = {name: current.character, povs: 0, total: 0, values: [], chapters: []}
characters.push(character)
}
character.povs++
character.total += current.word_count
character.values.push(current.word_count)
character.chapters.push(current.chapter)
return characters
}, [])
return Object.assign(reducedData.map(({name, povs, total, values, chapters}) => {
const min = d3.min(values);
const max = d3.max(values);
const idxMin = values.findIndex(v => v === min)
const idxMax = values.findIndex(v => v === max)
values.sort()
const q1 = d3.quantile(values, 0.25);
const q2 = d3.quantile(values, 0.50);
const q3 = d3.quantile(values, 0.75);
const iqr = q3 - q1; // interquartile range
const r0 = Math.max(min, q1 - iqr * 1.5);
const r1 = Math.min(max, q3 + iqr * 1.5);
return {
name,
range: [r0, r1],
quartiles: [q1, q2, q3],
idxMin,
idxMax,
min,
max,
mean: total/povs,
median: q2,
povs,
chapters,
values,
total
}
}), {x: "Number of POV →", y: "↑ Mean POV words"})
}
Insert cell
radiusScale = d3.scaleSqrt()
.domain([0, d3.max(data, d => d.total)]).nice()
.range([5,30])
Insert cell
x = d3.scaleLinear()
.domain([0, d3.max(data, d => d.povs)]).nice()
.range([margin.left, width - margin.right])
Insert cell
y = d3.scaleLinear()
.domain(d3.extent(data, d => d.mean)).nice()
.range([height - margin.bottom, margin.top])
Insert cell
xAxis = g =>
g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(5))
.call(g => g.select(".domain").remove())
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).ticks(5))
.call(g => g.select(".domain").remove())
Insert cell
fontSize = width / 60
Insert cell
grid = g => g
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
.call(g => g.append("g")
.selectAll("line")
.data(x.ticks())
.join("line")
.attr("x1", d => 0.5 + x(d))
.attr("x2", d => 0.5 + x(d))
.attr("y1", margin.top)
.attr("y2", height - margin.bottom))
.call(g => g.append("g")
.selectAll("line")
.data(y.ticks())
.join("line")
.attr("y1", d => 0.5 + y(d))
.attr("y2", d => 0.5 + y(d))
.attr("x1", margin.left)
.attr("x2", width - margin.right));
Insert cell
margin = ({top: 30, right: width / 10, bottom: 35, left: width / 12})
Insert cell
height = (width < 600 ? 700 : width < 800 ? 1200 : 1500)
Insert cell
d3 = require("d3@5")
Insert cell
function tooltipMouseOver(d, i) {
let el = d3.select("body");
let div = el
.append("g")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);

div
.html(`
<span class="label">Name</span></span>: ${d.name} </br>
<span class="label">Smaller POV</span>: ${d.values[0]} (${d.chapters[d.idxMin]})</br>
<span class="label">Mean POV</span>: ${Math.round(d.mean)} </br>
<span class="label">Median POV</span>: ${d.median} </br>
<span class="label">Larger POV</span>: ${d.values[d.values.length - 1]} (${d.chapters[d.idxMax]}) </br>
<span class="label">Total Word Count</span>: ${d.total} </br>
`)
.style("opacity", 0.9)
.style("left", (d3.event.pageX + 28) + "px")
.style("top", (d3.event.pageY - 28) + "px");
let el2 = d3.select(this);
el2
.transition()
.duration(100)
.style("fill-opacity", 0.9)
.style("stroke", "black")
.style("stroke-width", "1px")
.style("stroke-opacity", 0.7);
let guides = d3.select("svg")
.append("g")
.attr("class", "guides")
.style("pointer-events", "none");
guides
.append("line")
.attr("x1", el2.attr("cx"))
.attr("x2", el2.attr("cx"))
.attr("y1", el2.attr("cy"))
.attr("y2", height - margin.bottom)
.style("stroke", el2.attr("fill"))
.style("opacity", 0)
.transition()
.duration(200)
.style("opacity", 0.5);
guides
.append("line")
.attr("x1", el2.attr("cx"))
.attr("x2", margin.left)
.attr("y1", el2.attr("cy"))
.attr("y2", el2.attr("cy"))
.style("stroke", el2.attr("fill"))
.style("opacity", 0)
.transition()
.duration(200)
.style("opacity", 0.5);
}

Insert cell
function tooltipMouseOut(d, i) {
let el = d3.selectAll(".tooltip");
el.each(function() {
el.remove();
});
let el2 = d3.select(this);
el2
.transition()
.duration(100)
.style("fill-opacity", 0.5)
.style("stroke-width", "0px");
d3.select(".guides").remove();

}
Insert cell
html`
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">

<style>
.text {
font-family: "Montserrat";
}
.grid {
font-family: "Montserrat";
}
div.tooltip {
font-family: "Montserrat";
position: absolute;
text-align: left;
padding: 0.5em;
font: 0.8em;
background: rgb(255, 255, 255);
border: 1px solid rgba(0, 0, 0, 0.4);
border-radius: 0.5em;
pointer-events: none;
}
.label {
font-weight: bold;
}
.bubble {
mix-blend-mode: multiply;
}
</style>`
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