Published
Edited
Jun 24, 2021
3 forks
77 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const svg = d3
.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height])
.style("width", "100%")
.style("height", "auto")
.style("font", "14px sans-serif");

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

g.append("path")
.attr("d", arc)
.attr("stroke", "white")
.attr("fill", d =>
color(["Maximum", "Partial"].includes(d.status) ? d.category : null)
)
.attr("fill-opacity", d => (d.status === "Partial" ? 0.4 : 1));

g.append("title").text(d => `${d.state}: ${d.category}, ${d.status}`);

g.filter(d => d.status === "Banned")
.append("text")
.text("⃠") // "Prohibited" symbol
.attr("font-weight", "bold")
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.attr("dy", "0.27em")
.attr("fill", d => color(d.category))
.attr("x", bannedX)
.attr("y", bannedY)
.style("pointer-events", "none");

svg.append("g").call(dividers);
svg.append("g").call(regionLabels);
svg.append("g").call(stateLabels);
svg.append("g").call(legend);

return svg.node();
}
Insert cell
csv = FileAttachment("lgbt-rights-by-state-2015-guardian.csv").csv({ typed: true })
Insert cell
Inputs.table(csv)
Insert cell
wideData = csv.sort(
(a, b) =>
d3.ascending(
regionOrder.indexOf(a.Region),
regionOrder.indexOf(b.Region)
) || d3.ascending(a.Abbreviation, b.Abbreviation)
)
Insert cell
grouped = d3.groups(wideData, d => d.Region)
Insert cell
regions = d3
.groups(wideData, d => d.Region)
.map(([region, states]) => {
const startAngle = x(states[0].State);
const endAngle = x(states[states.length - 1].State) + x.bandwidth();
const labelAngle =
((startAngle + endAngle) / 2 - Math.PI / 2) % (Math.PI * 2);
return {
region,
startAngle,
endAngle,
labelAngle
};
})
Insert cell
longData = wideData.flatMap(row =>
categories.map(category => ({
state: row.State,
abbreviation: row.Abbreviation,
region: row.Region,
category,
status: row[category]
}))
)
Insert cell
arc = d3
.arc()
.innerRadius(d => y(categoryOrder.indexOf(d.category)))
.outerRadius(d => y(categoryOrder.indexOf(d.category) + 1))
.startAngle(d => x(d.state))
.endAngle(d => x(d.state) + x.bandwidth())
.padRadius(innerRadius)
Insert cell
rotation = (rotate / wideData.length) * Math.PI * 2
Insert cell
x = d3
.scaleBand()
.domain(wideData.map(d => d.State))
.range([rotation, 2 * Math.PI + rotation])
Insert cell
radiusScale = scale === "radial" ? d3.scaleRadial : d3.scaleLinear
Insert cell
y = radiusScale()
.domain([0, categories.length])
.range([innerRadius, outerRadius])
Insert cell
width = 975
Insert cell
height = width
Insert cell
innerRadius = 200
Insert cell
outerRadius = width / 2 - 30
Insert cell
regionOrder = ["Northeast", "Southeast", "Southwest", "Northwest", "Midwest"]
Insert cell
categoryOrder = order === "outside"
? categories.slice().reverse()
: categories.slice()
Insert cell
categories = wideData.columns.slice(3)
Insert cell
color = d3
.scaleOrdinal()
.domain(categories)
.range(colors)
.unknown("rgb(234,234,234)")
Insert cell
colors = palette === "original" ? original : rainbow
Insert cell
original = [
"rgb(188,27,30)",
"rgb(235,176,69)",
"rgb(9,76,148)",
"rgb(118,175,49)",
"rgb(194,0,120)",
"rgb(110,39,135)",
"rgb(110,181,227)"
]
Insert cell
rainbow = [
"rgb(187,51,57)",
"rgb(229,120,57)",
"rgb(233,190,71)",
"rgb(117,170,82)",
"rgb(72,130,191)",
"rgb(43,68,149)",
"rgb(124,72,148)"
]
Insert cell
labelX = (angle, radius) => Math.cos(angle) * radius
Insert cell
labelY = (angle, radius) => Math.sin(angle) * radius
Insert cell
bannedX = d =>
labelX(
x(d.state) + x.bandwidth() / 2 - Math.PI / 2,
y(categoryOrder.indexOf(d.category) + 0.5)
)
Insert cell
bannedY = d =>
labelY(
x(d.state) + x.bandwidth() / 2 - Math.PI / 2,
y(categoryOrder.indexOf(d.category) + 0.5)
)
Insert cell
dividers = g =>
g
.selectAll("g")
.data(regions)
.join("g")
.attr("transform", d => `rotate(${(d.startAngle * 180) / Math.PI - 90})`)
.append("line")
.attr("x1", innerRadius)
.attr("x2", outerRadius + 30)
.attr("stroke", "black")
.attr("stroke-width", 5)
Insert cell
labelArc = d3
.arc()
.innerRadius(innerRadius - 20)
.outerRadius(innerRadius - 20)
.startAngle(d => d.startAngle)
.endAngle(d => d.endAngle)
Insert cell
regionLabels = g => {
const uids = new Map(regions.map(({ region }) => [region, DOM.uid(region)]));
const shouldFlip = angle => angle > 0 && angle < Math.PI;
g.selectAll("path")
.data(regions)
.join("path")
.attr("d", labelArc)
.attr("id", d => uids.get(d.region).id)
.attr("stroke", "none");
g.append("g")
.style("text-transform", "uppercase")
.attr("fill", "#888")
.attr("text-anchor", "middle")
.selectAll("text")
.data(regions)
.join("text")
.attr("transform", d =>
shouldFlip(d.labelAngle)
? `translate(${labelX(d.labelAngle, 11)}, ${labelY(d.labelAngle, 11)})`
: null
)
.append("textPath")
.attr("xlink:href", d => uids.get(d.region).href)
.attr("startOffset", d => (shouldFlip(d.labelAngle) ? "75%" : "25%"))
.text(d => d.region);
}
Insert cell
stateLabels = g =>
g
.attr("text-anchor", "middle")
.attr("fill", "#888")
.selectAll("text")
.data(wideData)
.join("text")
.attr("x", d =>
labelX(x(d.State) + x.bandwidth() / 2 - Math.PI / 2, outerRadius + 16)
)
.attr("y", d =>
labelY(x(d.State) + x.bandwidth() / 2 - Math.PI / 2, outerRadius + 16)
)
.attr("dy", "0.31em")
.text(d => d.Abbreviation)
Insert cell
legend = g => {
const legend = g
.selectAll("g")
.data(categories.concat(["No law or unclear", "Banned"]))
.join("g")
.attr(
"transform",
(d, i) => `translate(-45,${(i - (categories.length + 2) / 2) * 20})`
);

legend
.filter(d => categories.includes(d))
.append("rect")
.attr("x", -36)
.attr("width", 36)
.attr("height", 18)
.attr("fill", color)
.attr("fill-opacity", 0.4);

legend
.append("rect")
.attr("x", 0)
.attr("width", 36)
.attr("height", 18)
.attr("fill", color);

legend
.append("text")
.attr("x", 42)
.attr("y", 9)
.attr("dy", "0.35em")
.text(d => d);

legend
.filter(d => d === "Banned")
.append("text")
.attr("x", 18)
.attr("y", 9)
.attr("dy", "0.27em")
.attr("font-weight", "bold")
.attr("text-anchor", "middle")
.attr("fill", "#888")
.text("⃠");

const label = g
.append("g")
.attr("transform", `translate(-45, ${-((categories.length + 2) / 2) * 20})`)
.attr("text-anchor", "middle")
.attr("fill", "#888");

label
.append("text")
.attr("x", -18)
.attr("dy", "-0.35em")
.text("Part");

label
.append("text")
.attr("x", 18)
.attr("dy", "-0.35em")
.text("Max");
}
Insert cell
d3 = require("d3@6")
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