Published
Edited
Jan 26, 2022
4 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chocolate.view(10)
Insert cell
Insert cell
Insert cell
// get the top 10 most common characteristics
most_common_chars = chocolate
.select("most_memorable_characteristics")
.spread({ most_memorable_characteristics: d => op.split(d.most_memorable_characteristics, ",") })
.fold(aq.not("country_of_bean_origin"), {as: ["list", "char"]})
.derive({ char: d => op.trim(d.char) })
.filter(d => d.char != null)
.select(aq.not("list"))
.groupby("char")
.count()
.orderby(aq.desc("count"))
.slice(0, 10)
.objects()
.map(d => d.char)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
groupedData = d3.rollup(
top_chars_country.objects(),
// reduce fn
g => d3.sum(g, d => d.count),
// grouping keys
d => d.char,
d => d.country_of_bean_origin
)
Insert cell
root = d3.hierarchy(groupedData)
.sum(d => d[1])
.sort((a, b) => d3.descending(a.value, b.value))
.eachAfter(d => d.index = d.parent ? d.parent.index = d.parent.index + 1 || 0 : 0)
Insert cell
Insert cell
Insert cell
barStep = 30
Insert cell
barPadding = 3 / barStep
Insert cell
duration = 750
Insert cell
height = {
let max = 1
root.each(d => d.children && (max = Math.max(max, d.children.length)))
return max * barStep + margin.top + margin.bottom
}
Insert cell
margin = ({
top: 40,
right: 10,
bottom: 0,
left: 120
})
Insert cell
x = d3.scaleLinear()
.range([margin.left, width - margin.right])
.nice()
Insert cell
xAxis = g => g
.attr("class", "x-axis")
.attr("transform", `translate(0, ${margin.top})`)
.style("font", "10px 'JetBrains Mono'")
.call(d3.axisTop(x).ticks(5, "s"))
.call(g => (g.selection ? g.selection() : g).select(".domain").remove())
.call(g => g.selectAll("text").style("fill", "#708090"))
.call(g => g.selectAll("line").style("stroke", "#708090"))
Insert cell
function bar(svg, d, down, subtitle) {
const g = svg.insert("g")
.attr("class", "enter")
.attr("transform", `translate(0, ${margin.top + barStep * barPadding})`)
.attr("text-anchor", "end")
.style("font", "12px Inter")
.style("font-weight", 400)

const bar = g.selectAll("g")
.data(d.children)
.join("g")
.attr("cursor", d => !d.children ? null : "pointer")
.on("click", (event, d) => down(svg, d, subtitle))

bar.append("text")
.attr("x", margin.left - 5)
.attr("y", barStep * (1 - barPadding) / 2)
.attr("dy", "0.35em")
.text(d => d.data[0])

bar.append("rect")
.attr("x", x(0))
.attr("width", d => x(d.value) - x(0))
.attr("height", barStep * (1 - barPadding))
.style("fill", colorMain)

return g
}
Insert cell
function down(svg, d, subtitle) {
if (!d.children) return

const transition1 = () => svg.transition().duration(duration)
const transition2 = () => transition1().transition()

svg.select(".backButton").datum(d)
.transition(transition2())
.on("end", function() { d3.select(this).transition().style("fill-opacity", !d.parent ? 0 : 1) })

const exit = svg.selectAll(".enter")
.attr("class", "exit")

exit.selectAll("rect")
.attr("fill-opacity", p => p === d ? 0 : null)

exit.transition(transition1())
.attr("fill-opacity", 0)
.remove()

const enter = bar(svg, d, down, subtitle)
.attr("fill-opacity", 0)

enter.transition(transition1())
.attr("fill-opacity", 1)

enter.selectAll("g")
.attr("transform", stack(d.index))
.transition(transition1())
.attr("transform", stagger())

x.domain([0, d3.max(d.children, d => d.value)])

svg.selectAll(".x-axis")
.transition(transition2())
.call(xAxis)

enter
.selectAll("g")
.transition(transition2())
.attr("transform", (d, i) => `translate(0, ${barStep * i})`)

enter.selectAll("rect")
.attr("fill-opacity", 1)
.transition(transition2())
.attr("width", d => x(d.value) - x(0))
.style("fill", (d, i) => d.children ? colorMain : !i ? colorMain : colorSec)

// reveal the subtitle
subtitle
.html(`Was the taste <span style='color: ${colorMain}; border-bottom: 3px solid'>${d.data[0]}</span>? The cocoa beans most likely came from <span style='color: ${colorMain}; border-bottom: 3px solid'>${d.children[0].data[0]}</span>`)
.style("opacity", !d.parent ? 0 : 1)
}
Insert cell
function up(svg, d, subtitle) {
if (!d.parent) return

const transition1 = () => svg.transition().duration(duration)
const transition2 = () => transition1().transition()

svg.select(".backButton").datum(d.parent)
.transition(transition1())
.style("fill-opacity", 0)

const exit = svg.selectAll(".enter")
.attr("class", "exit")

x.domain([0, d3.max(d.parent.children, d => d.value)])

svg.selectAll(".x-axis").transition(transition1())
.call(xAxis)
exit.selectAll("g").transition(transition1())
.attr("transform", stagger())

exit.selectAll("rect").transition(transition1())
.attr("width", d => x(d.value) - x(0))
.style("fill", colorMain)

exit.selectAll("g").transition(transition2())
.attr("transform", stack(d.index))

exit.transition(transition2())
.attr("fill-opacity", 0)
.remove()

const enter = bar(svg, d.parent, down, subtitle)
.attr("fill-opacity", 0)

enter.selectAll("g")
.attr("transform", (d, i) => `translate(0, ${barStep * i})`)

enter.transition(transition2())
.attr("fill-opacity", 1)

enter.selectAll("rect")
.attr("fill-opacity", p => p === d ? 0 : null)
.transition(transition2())
.on("end", function() { d3.select(this).attr("fill-opacity", 1) })

// hide subtitle
subtitle.style("opacity", 0)
}
Insert cell
function stack(i) {
let value = 0
return d => {
const t = `translate(${x(value) - x(0)}, ${barStep * i})`
value += d.value
return t
}
}
Insert cell
function stagger() {
let value = 0
return (d, i) => {
const t = `translate(${x(value) - x(0)}, ${barStep * i})`
value += d.value
return t
}
}
Insert cell
Insert cell
Insert cell
d3 = require("d3@7")
Insert cell
Insert cell
colorMain = "#73412F"
Insert cell
colorSec = "#A67153"
Insert cell
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