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

const g = svg.append("g")
.selectAll("g")
.data(bins)
.join("g");

g.append("path")
.attr("stroke", "currentColor")
.attr("d", d => `
M${x(d.key) + x.bandwidth() / 2},${y(d.value.range[1])}
V${y(d.value.range[0])}
`);

g.append("path")
.attr("fill",d => color(d.value.gender))
.attr("opacity", 0.5)
.attr("d", d => `
M${x(d.key) + 1},${y(d.value.quartiles[2])}
H${x(d.key) + x.bandwidth()}
V${y(d.value.quartiles[0])}
H${x(d.key) + 1}
Z
`)
.on('mouseover', function(d, i) {
const t = d3.transition('enlarge').duration(500)
tooltipMouseOver(d, i, t, allInfo)
})
.on('mouseout', function(d, i) {
const t = d3.transition('dwindle').duration(500)
tooltipMouseOut(d, i, t, allInfo)
});

g.append("path")
.attr("stroke", "currentColor")
.attr("stroke-width", 3)
.attr("d", d => `
M${x(d.key) + 1},${y(d.value.quartiles[1])}
H${x(d.key) + x.bandwidth()}
`)
.on('mouseover', function(d, i) {
const t = d3.transition('enlarge').duration(500)
tooltipMouseOver(d, i, t, medianInfo)
})
.on('mouseout', function(d, i) {
const t = d3.transition('dwindle').duration(500)
tooltipMouseOut(d, i, t, medianInfo)
});;
g.append("path")
.attr("stroke", "currentColor")
.attr("stroke-width", 3)
.attr("d", d => `
M${x(d.key) + x.bandwidth() * 0.25},${y(d.value.range[0])}
H${x(d.key) + x.bandwidth() * 0.75}
`)
.on('mouseover', function(d, i) {
const t = d3.transition('enlarge').duration(500)
tooltipMouseOver(d, i, t, minIQRInfo)
})
.on('mouseout', function(d, i) {
const t = d3.transition('dwindle').duration(500)
tooltipMouseOut(d, i, t, minIQRInfo)
});
g.append("path")
.attr("stroke", "currentColor")
.attr("stroke-width", 3)
.attr("d", d => `
M${x(d.key) + x.bandwidth() * 0.25},${y(d.value.range[1])}
H${x(d.key) + x.bandwidth() * 0.75}
`)
.on('mouseover', function(d, i) {
const t = d3.transition('enlarge').duration(500)
tooltipMouseOver(d, i, t, maxIQRInfo)
})
.on('mouseout', function(d, i) {
const t = d3.transition('dwindle').duration(500)
tooltipMouseOut(d, i, t, maxIQRInfo)
});
g.append("g")
.attr("fill", "currentColor")
.attr("fill-opacity", 0.5)
.attr("stroke", "none")
.attr("transform", d => `translate(${x(d.key) + x.bandwidth() / 2},0)`)
.selectAll("circle")
.data(d => d.value.values)
.join("circle")
.attr("r", 8)
.attr("cx", () => (Math.random() - 0.5) * 10)
.attr("cy", d => y(d.y))
.on('mouseover', function(d, i) {
const t = d3.transition('enlarge').duration(500)
tooltipMouseOver(d, i, t, chapterInfo)
})
.on('mouseout', function(d, i) {
const t = d3.transition('dwindle').duration(500)
tooltipMouseOut(d, i, t, chapterInfo)
});

svg.append("g")
.call(xAxis);

svg.append("g")
.call(yAxis);

return svg.node();
}
Insert cell
bins = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) { return d.x;})
.rollup(function(d) {
const values = d.map(function(g) { return { y: g.y, chapter: g.chapter};}).sort((a, b) => d3.ascending(a.y, b.y))
const min = values[0].y;
const max = values[values.length - 1].y;
const q1 = d3.quantile(values.map(ch => ch.y), 0.25);
const q2 = d3.quantile(values.map(ch => ch.y), 0.50);
const q3 = d3.quantile(values.map(ch => ch.y), 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 {
values,
gender: d[0].gender,
nationality: d[0].nationality,
quartiles:[q1, q2, q3],
range: [r0, r1],
outliers: values.filter(v => v.y < r0 || v.y > r1),
};
})
.entries(data)
Insert cell
data = d3.csvParse(await FileAttachment("teotw_pov_box@1.csv").text(), ({character, word_count, gender, chapter, nationality}) => ({x: character, y: +word_count, gender, nationality, chapter}))
Insert cell
x = d3.scaleBand()
.domain(bins.map(bin => bin.key))
.rangeRound([margin.left, width - margin.right])
.paddingInner(0.05)
.paddingOuter(.1)
Insert cell
y = d3.scaleLinear()
.domain([d3.min(bins, d => d.value.range[0]), d3.max(bins, d => d.value.range[1])]).nice()
.range([height - margin.bottom, margin.top])
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(n).tickSizeOuter(0))
.call(g => g.selectAll('text').attr("y", 0)
.attr("x", 10)
.attr("dy", ".35em")
.attr("transform", "rotate(85)")
.attr("text-anchor", "start")
.style('font-size', fontSize)
.classed('box-plot-tick', true))
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).ticks(null, "s"))
.call(g => g.selectAll('.tick').style('font-size', fontSize).classed('box-plot-tick', true))
.call(g => g.select(".domain").remove())
Insert cell
color = d3.scaleOrdinal(["#888888", "#bbbbbb", "green", "#5B8B9B", "#77C0A0", "#264D54", "#324280", "#D27AE9", "#867AC4", "#72329E", "#BEE091", "#48853A", "#8BEC6E", "#8B9662", "#663B11", "#F3A852", "#E0502F", "#932846", "#A17965", "#E7799E", "#EC3686", "#414B17", "#F0D447", "#4141F5", "#E743F4"]).domain(["last appeared", "last mentioned", "continue", "not specified", "Female", "Male", "Andoran", "Unknown nationality", "Shienaran", "Malkieri", "Manetherenite", "Illianer", "Taraboner", "Ghealdanin", "Aridholin", "Essenian", "Far Madding", "Darmovanin", "Saldaean", "Murandian" ])
Insert cell
n = width / 40
Insert cell
height = (width < 600 ? 700 : width < 800 ? 1200 : 1500)
Insert cell
margin = ({top: 20, right: 0, bottom: width / 4, left: width / 15})
Insert cell
d3 = require("d3@5")
Insert cell
allInfo = function(d) {
return `
<span class="label">Minimum IQR</span>: ${d.value.range[0]} words<br>
<span class="label">Q1 (25%)</span>: ${d.value.quartiles[0]} words<br>
<span class="label">Q2 (Median)</span>: ${d.value.quartiles[1]} words<br>
<span class="label">Q3 (75%)</span>: ${d.value.quartiles[2]} words<br>
<span class="label">Maximum IQR</span>: ${d.value.range[1]} words<br>
<span class="label">Number of POVs.</span>: ${d.value.values.length}
`
}
Insert cell
minIQRInfo = function(d) {
return `
<span class="label">Minimum IQR</span>: ${d.value.range[0]} words
`
}
Insert cell
maxIQRInfo = function(d) {
return `
<span class="label">Maximum IQR</span>: ${d.value.range[1]} words
`
}
Insert cell
chapterInfo = function(d) {
return `
<strong>${d.y}</strong> words POV in <strong>${d.chapter}</strong>.
`
}
Insert cell
medianInfo = function(d) {
return `
<span class="label">Q2 (Median)</span>: ${d.value.quartiles[1]} words
`
}
Insert cell
function tooltipMouseOver(d, i, t, info) {
let el = d3.select("body");
let div = el
.append("g")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);

div
.html(info(d))
.style("left", (d3.event.pageX + 10) + "px")
.style("top", (d3.event.pageY - 10) + "px")
.transition(t)
.style("opacity", 0.9)
;
let el2 = d3.select(this);
el2
.transition(t)
.style("fill-opacity", 0.9)
.style("stroke", "black")
.style("stroke-width", "1px")
.style("stroke-opacity", 0.7);
}

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

}
Insert cell
tooltip = 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.5em;
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