Published
Edited
Aug 27, 2019
2 forks
8 stars
Insert cell
md`# Dot plot with d3`
Insert cell
d3 = require("d3@5")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
dotPlot = {
// Access Data
const xAccessorAA = d => d["AllAdults"]
const xAccessorLV = d => d["LikelyVoters"]
const xAccessorNV = d => d["NonVoters"]
const yAccessor = d => d["Race"]
// Chart dimensions
let dimensions = {
width: width,
height: width * 0.334,
margin: {
top: 90,
right: 15,
bottom: 60,
left: 65,
},
}
dimensions.boundedWidth = dimensions.width - dimensions.margin.left - dimensions.margin.right
dimensions.boundedHeight = dimensions.height - dimensions.margin.top - dimensions.margin.bottom
// Draw canvas
const svg = d3.select(DOM.svg(dimensions.width, dimensions.height))
const bounds = svg.append("g")
.attr("transform", `translate(${dimensions.margin.left}, ${dimensions.margin.top})`)
// Scales
const xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, xAccessorLV)])
.range([0, dimensions.boundedWidth])
.nice()
const yScale = d3.scaleBand()
.domain(dataset.map(yAccessor))
.range([0, dimensions.boundedHeight])
.paddingInner(0.5)
.paddingOuter(1.5)
// Draw data
const lineGenerator = d3.line()
const axisLinePath = d => lineGenerator( [ [xScale(d) + 0.5, 0], [xScale(d) + 0.5, dimensions.boundedHeight]])
const dotsLinePathNVAA = d => lineGenerator([ [xScale(xAccessorNV(d)), 0], [xScale(xAccessorAA(d)), 0] ])
const dotsLinePathAALV = d => lineGenerator([ [xScale(xAccessorAA(d)), 0], [xScale(xAccessorLV(d)), 0] ])
const dotsGroup = bounds.append("g")
.attr("class", "dots")
const dots = dotsGroup.selectAll("g")
.data(dataset)
.enter().append("g")
.attr("class", "dot")
.attr("transform", d => `translate(0, ${(yScale(yAccessor(d)) + (yScale.bandwidth() / 2))})`)
dots.append("path")
.attr("class", "dots-line-NVAA")
.attr("d", dotsLinePathNVAA)
dots.append("path")
.attr("class", "dots-line-AALV")
.attr("d", dotsLinePathAALV)
const nonVoterCircles = dots.append("circle")
.attr("class", "non-voters")
.attr("r", radiusCircles)
.attr("cx", d => xScale(xAccessorNV(d)))
const likelyVoterCircles = dots.append("circle")
.attr("class", "likely-voters")
.attr("r", radiusCircles)
.attr("cx", d => xScale(xAccessorLV(d)))
const allAdultCircles = dots.append("circle")
.attr("class", "all-adults")
.attr("r", radiusAdults)
.attr("cx", d => xScale(xAccessorAA(d)))
// Make axes
const xAxisGenerator = d3.axisTop()
.scale(xScale)
.tickFormat(d3.format(".0%"))
.ticks(5)
const yAxisGenerator = d3.axisLeft()
.scale(yScale)
.tickSize(0)
const xAxis = bounds.append("g")
.attr("class", "x-axis")
.call(xAxisGenerator)
const yAxis = bounds.append("g")
.attr("class", "y-axis")
.attr("transform", "tranlate(-20, 0)")
.call(yAxisGenerator)
.select(".domain")
.attr("opacity", 0)
// gridlines
const gridLines = bounds.append("g")
.attr("class", "grid-lines")
gridLines.selectAll("path")
.data(xScale.ticks())
.enter().append("path")
.attr("class", "grid-line")
.attr("d", axisLinePath)
// legend
const legendLabels = [
{"label": "Non Voters", class: "non-voters"},
{"label": "All Adults", class: "all-adults"},
{"label": "Likely Voters", class: "likely-voters"},
]
const legendX = dimensions.boundedWidth / 2
const legendY = dimensions.margin.top / 2
const spaceBetween = dimensions.boundedWidth / 8
const titleOffset = -dimensions.boundedWidth / 2
const legend = svg.append("g")
.attr("transform", `translate(${legendX}, ${legendY})`)
legend.append("g")
.attr("class", "title")
.append("text")
.attr("x", titleOffset + dimensions.margin.left)
.attr("y", -20)
.text("Voters do not reflect California's racial diversity")
legend.selectAll("circle")
.data(legendLabels)
.enter().append("circle")
.attr("cx", (d, i) => spaceBetween * i + 180)
.attr("cy", 5)
.attr("r", 4)
.attr("class", d => d.class)
legend.append("g")
.selectAll("text")
.data(legendLabels)
.enter().append("text")
.attr("x", (d, i) => spaceBetween * i + 190)
.attr("y", 10)
.text(d => d.label)
return svg.node()
}
Insert cell
dataset = [
{"Race": "White", "AllAdults": .42, "LikelyVoters": .58, "NonVoters":.21},
{"Race": "Latinx", "AllAdults": .35, "LikelyVoters": .19, "NonVoters":.56},
{"Race": "Asian", "AllAdults": .15, "LikelyVoters": .13, "NonVoters":.18},
{"Race": "Black", "AllAdults": .06, "LikelyVoters": .06, "NonVoters":.03},
{"Race": "Other", "AllAdults": .03, "LikelyVoters": .04, "NonVoters": .01},
]
Insert cell
html`<style>
text {
vertical-align: middle;
font-family: sans-serif;
}

.title {
font-size: 1.8em;
}
.tick text {
font-size: 1.4em;
}
.grid-line {
stroke: #272727;
opacity: 0.2;
}

.dots-line-AALV {
stroke: ${likelyVotersColor};
stroke-width: 5px;
opacity: 0.4;
}

.dots-line-NVAA {
stroke: ${nonVotersColor};
stroke-width: 5px;
opacity: 0.4;
}


circle {
stroke-width: 0.2px;
stroke: black;
}

.non-voters{
fill: ${nonVotersColor};
}

.likely-voters {
fill: ${likelyVotersColor};
}

.all-adults {
fill: ${allAdultsColor};
}


div.tooltip {
position: absolute;
text-align: center;
color: white;
padding: 8px;
font: 12px sans-serif;
border-radius: 6px;
pointer-events: none;
}
</style>`
Insert cell
import {color, slider} from "@jashkenas/inputs"
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