Published
Edited
Dec 2, 2020
1 fork
Insert cell
Insert cell
chart = {
const svg = d3.select(DOM.svg(width, height))
.attr("viewBox", `${-width / 2} ${-height / 2} ${width} ${height}`)
.style("width", "100%")
.style("height", "auto")
.style("font", "10px sans-serif");

svg.append("g")
.selectAll("g")
.data(filledData)
.join("g")
.selectAll("path")
.data(filledData)
.join("path")
.attr("fill", (d,i) => z(i))
.attr("d", d => arc(d));

svg.append("text")
.attr("transform", `translate(0,${-outerRadius + 80})`)
.attr("text-anchor", "middle")
.style("font", "40px sans-serif")
.text(title);
svg.append("g")
.call(xAxis);

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

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

return svg.node();
}
Insert cell
chart2 = {
const svg2 = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
svg2.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", xBar(0))
.attr("y", (d, i) => yBar(i))
.attr("width", d => xBar(d.proportion) - xBar(0))
.attr("height", yBar.bandwidth())
.attr("fill", (d,i) => z(filledData.findIndex(x => x.spice === d.spice)))
svg2.append("g")
.attr("fill", "white")
.attr("text-anchor", "end")
.attr("font-family", "sans-serif")
.attr("font-size", 30)
.selectAll("text")
.data(data)
.join("text")
.attr("x", d => xBar(d.proportion) - 4)
.attr("y", (d, i) => yBar(i) + yBar.bandwidth() / 2)
.attr("dy", "0.35em")
.text(d => d.proportion);

svg2.append("g")
.call(xAxisBar);

svg2.append("g")
.call(yAxisBar);
svg2.append("text")
.attr("transform", `translate(${(width) / 2},${margin.top / 2})`)
.attr("text-anchor", "middle")
.style("font", "40px sans-serif")
.text(title);

return svg2.node();
}
Insert cell
Insert cell
blendsData = Object.entries(spiceBlendRawData).map(entry => ({
name: entry[0],
recipe: entry[1].map(item => item.replace('spoons', 'spoon')).map(item => {
const [amount, spice] = item.split('spoon ')
const [quantity, unit] = amount.split(' ')
const proportion = unit === 'table' ? Number(quantity) * 3 : Number(quantity)
return { originalQuantity: Number(quantity), originalUnit: `${unit}spoon`, spice, proportion }
})
}))
Insert cell
blendsIngredients = blendsData.map(blend => blend.recipe.map(item => item.spice))
Insert cell
spices = []
Insert cell
blendsIngredients.forEach(list => list.forEach(ingredient => {
if (!spices.includes(ingredient)) {
spices.push(ingredient)
}
}))
Insert cell
Insert cell
data = blendsData[1].recipe
Insert cell
title = blendsData[1].name
Insert cell
getSpicesNotIncludedInRecipe = (spices, spicesInRecipe) => spices.filter(x => !spicesInRecipe.includes(x))
Insert cell
spicesInRecipe = data.map(d => d.spice);
Insert cell
spicesNotIncludedInRecipe = getSpicesNotIncludedInRecipe(spices, spicesInRecipe)
Insert cell
dataToFill = spicesNotIncludedInRecipe.map(spice => ({spice, originalQuantity: 0, originalUnit: '', proportion: 0 }))
Insert cell
filledData = [...data, ...dataToFill].sort((a,b) => a.spice > b.spice ? 1 : -1)
Insert cell
arc = d3.arc()
.innerRadius(d => y(0))
.outerRadius(d => y(d.proportion))
.startAngle(d => x(d.spice))
.endAngle(d => x(d.spice) + x.bandwidth())
.padAngle(1)
.padRadius(10)
Insert cell
arc(data[0])
Insert cell
x = d3.scaleBand()
.domain(filledData.map(d => d.spice))
.range([0, 2 * Math.PI])
.align(0)
Insert cell
y = {
// This scale maintains area proportionality of radial bars!
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.proportion)])
.range([innerRadius * innerRadius, outerRadius * outerRadius]);
return Object.assign(d => Math.sqrt(y(d)), y);
}
Insert cell
domain = filledData.map(d => d.spice);
Insert cell
z = d3.scaleLinear()
.domain([0, filledData.length / 4, filledData.length / 2, (3*filledData.length) / 4, filledData.length])
.range(["#dbcccc","#8faab3","#c4bd8b","#81b14f","#451b04"])
Insert cell
getIsInLeftHalf = d => (x(d.spice) + x.bandwidth() / 2) % (2 * Math.PI) < Math.PI
Insert cell
xAxis = g => g
.attr("text-anchor", "middle")
.call(g => g.selectAll("g")
.data(filledData)
.join("g")
.attr("transform", d => `
rotate(${((x(d.spice) + x.bandwidth() / 2) * 180 / Math.PI - 90)})
translate(${y(0)},0)
`)
.call(g => g.append("text")
.attr("transform", d => getIsInLeftHalf(d)
? "translate(8,4)"
: "rotate(180)translate(-8,4)")
.attr("fill", "white")
.style("font", "bold 12px sans-serif")
.style("text-anchor", d => getIsInLeftHalf(d) ? "start" : "end")
.text(d => d.proportion > 0 ? `${d.spice}`: "")))
Insert cell
yAxis = g => g
.attr("text-anchor", "middle")
.call(g => g.selectAll("g")
.data(y.ticks(8).slice(1))
.join("g")
.attr("fill", "none")
.call(g => g.append("circle")
.attr("stroke", "#fff")
.attr("stroke-opacity", 0.5)
.attr("r", y))
)
Insert cell
legend = g => g.append("g")
.selectAll("g")
.data(data)
.join("g")
.attr("transform", (d, i) => `translate(-40,${(i - (data.length - 1) / 2) * 20})`)
.call(g => g.append("rect")
.attr("width", 18)
.attr("height", 18)
.attr("fill", (d,i) => z(filledData.findIndex(x => x.spice === d.spice))))
.call(g => g.append("text")
.attr("x", 9)
.attr("y", 9)
.attr("dy", "0.35em")
.attr("fill", "white")
.style("text-anchor", "middle")
.text(d => d.proportion))
.call(g => g.append("text")
.attr("x", 24)
.attr("y", 9)
.attr("dy", "0.35em")
.text(d => d.spice))
Insert cell
width = 975
Insert cell
height = width
Insert cell
innerRadius = data.length * 15
Insert cell
outerRadius = Math.min(width, height) / 2
Insert cell
d3 = require("d3@5")
Insert cell
xBar = d3.scaleLinear()
.domain([0, d3.max(data, d => d.proportion)])
.range([margin.left, width - margin.right])
Insert cell
yBar = d3.scaleBand()
.domain(d3.range(data.length))
.rangeRound([margin.top, height - margin.bottom])
.padding(0.2)
Insert cell
xAxisBar = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisTop(xBar).tickSizeInner(height - margin.top - margin.bottom))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick:not(:first-of-type) line")
.attr("stroke", "white")
.attr("stroke-dasharray", "4,4"))
Insert cell
yAxisBar = g => g
.style("font", "20px arial")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yBar).tickFormat(i => data[i].spice).tickSizeOuter(0))
Insert cell
barHeight = (height - margin.top - margin.bottom) / (data.length + 20)
Insert cell
margin = ({top: 100, right: 60, bottom: 10, left: 160})
Insert cell
md`Regular horizontal bar chart will probably make more sense on a mobile screen though..`
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