Public
Edited
Apr 27, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
topN = {
return [...getTopN(2017), ...getTopN(2018), ...getTopN(2019), ...getTopN(2020), ...getTopN(2021), ...getTopN(2022)]
}
Insert cell
function getTopN(year) {
return data.filter(d => parseInt(d.year) === parseInt(year)).sort((x, y) => (y.rate - x.rate)).slice(0,n)
}
Insert cell
// Guam is not a state actually, so it's removed
data = rawData.filter(d => d.state !== "Guam")
Insert cell
rawData = FileAttachment("idiom4-data.csv").csv()
Insert cell
Insert cell
redOrBlue = {
const result = {}
rawRedOrBlue.forEach(d => {result[d.state] = d.redOrBlue})
return result
}
Insert cell
rawRedOrBlue = FileAttachment("red-blue-states@2.csv").csv()
Insert cell
Insert cell
function getRedBlue(state) {
if(redOrBlue[state] === "blue") {
return "steelblue"
} else if(redOrBlue[state] === "red") {
return "orangered"
}
}
Insert cell
// manually avoid collide
function collide(X, T, i) {
if (X[i].toString().includes("2018") && T[i] === "Michigan") {
return "1em"
} else if (X[i].toString().includes("2019") && T[i] === "Massachusetts") {
return "-0.3em"
} else if (X[i].toString().includes("2021") && T[i] === "Massachusetts") {
return "-0.3em"
} else if (X[i].toString().includes("2017") && T[i] === "Maryland") {
return "1em"
} else { return "0.35em" }
}
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// Extensively modified from https://observablehq.com/@d3/scatterplot
function Scatterplot(data, {
x = ([x]) => x, // given d in data, returns the (quantitative) x-value
y = ([, y]) => y, // given d in data, returns the (quantitative) y-value
r = 3, // (fixed) radius of dots, in pixels
title, // given d in data, returns the title
marginTop = 20, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
inset = r * 2, // inset the default range, in pixels
insetTop = inset, // inset the default y-range
insetRight = inset, // inset the default x-range
insetBottom = inset, // inset the default y-range
insetLeft = inset, // inset the default x-range
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
xType = d3.scaleLinear, // type of x-scale
xDomain, // [xmin, xmax]
xRange = [marginLeft + insetLeft, width - marginRight - insetRight], // [left, right]
yType = d3.scaleLinear, // type of y-scale
yDomain, // [ymin, ymax]
yRange = [height - marginBottom - insetBottom, marginTop + insetTop], // [bottom, top]
xLabel, // a label for the x-axis
yLabel, // a label for the y-axis
xFormat, // a format specifier string for the x-axis
yFormat, // a format specifier string for the y-axis
fill = "none", // fill color for dots
stroke = "currentColor", // stroke color for the dots
strokeWidth = 1.5, // stroke width for dots
halo = "#fff", // color of label halo
haloWidth = 3 // padding around the labels
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const T = title == null ? null : d3.map(data, title);
const I = d3.range(X.length).filter(i => !isNaN(X[i]) && !isNaN(Y[i]));
// console.log({X, Y, T, I})

// Compute default domains.
if (xDomain === undefined) xDomain = d3.extent(X);
if (yDomain === undefined) yDomain = d3.extent(Y);

// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
// const xAxis = d3.axisBottom(xScale).ticks(width / 80, xFormat);
const xAxis = d3.axisBottom(xScale).ticks(d3.timeYear.every(1));
const yAxis = d3.axisLeft(yScale).ticks(height / 50, yFormat);

// Tooltip
const container = d3.select(
html`<div style="position:relative;">${tooltipTemplate}</div>`
);
const tooltipDiv = container.select(".tooltip");

const svg = container.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("y2", marginTop + marginBottom - height)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", width)
.attr("y", marginBottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(xLabel));

svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));

if (T) svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.selectAll("text")
.data(I)
.join("text")
.attr("dx", 7)
.attr("dy", i => collide(X, T, i))
.attr("x", i => xScale(X[i]))
.attr("y", i => yScale(Y[i]))
.text(i => T[i])
.call(text => text.clone(true))
.attr("fill", "none")
.attr("stroke", halo)
.attr("stroke-width", haloWidth);

svg.append("g")
.attr("fill", fill)
.attr("stroke-width", strokeWidth)
.selectAll("circle")
.data(I)
.join("circle")
.attr("stroke", i => stroke(T[i]))
.attr("cx", i => xScale(X[i]))
.attr("cy", i => yScale(Y[i]))
.attr("r", r)
.call(tooltip, tooltipDiv);
return container.node();
}
Insert cell
Insert cell
tooltipId = DOM.uid().id
Insert cell
tooltipTemplate = html`<div class="tooltip tooltip-${tooltipId}">
${tooltipStyles}
<div class="tooltip-contents"></div>
</div>`
Insert cell
tooltip = (selectionGroup, tooltipDiv) => {
selectionGroup.each(function () {
d3.select(this)
.on("mouseover.tooltip", handleMouseover)
.on("mousemove.tooltip", handleMousemove)
.on("mouseleave.tooltip", handleMouseleave);
});

function handleMouseover() {
// show/reveal the tooltip, set its contents,
// style the element being hovered on
showTooltip();
setContents(d3.select(this).datum(), tooltipDiv);
setStyle(d3.select(this));
}

function handleMousemove(event) {
// update the tooltip's position
const [mouseX, mouseY] = d3.pointer(event, this);
// add the left & top margin values to account for the SVG g element transform
setPosition(mouseX + margin.left, mouseY + margin.top);
}

function handleMouseleave() {
// do things like hide the tooltip
// reset the style of the element being hovered on
hideTooltip();
resetStyle(d3.select(this));
}

function showTooltip() {
tooltipDiv.style("display", "block");
}

function hideTooltip() {
tooltipDiv.style("display", "none");
}

function setPosition(mouseX, mouseY) {
tooltipDiv
.style(
"top",
mouseY < height / 2 ? `${mouseY + MOUSE_POS_OFFSET}px` : "initial"
)
.style(
"right",
mouseX > width / 2
? `${width - mouseX + MOUSE_POS_OFFSET}px`
: "initial"
)
.style(
"bottom",
mouseY > height / 2
? `${height - mouseY + MOUSE_POS_OFFSET}px`
: "initial"
)
.style(
"left",
mouseX < width / 2 ? `${mouseX + MOUSE_POS_OFFSET}px` : "initial"
);
}
}
Insert cell
function setContents(i, tooltipDiv) {
const Y = d3.map(topN, d => d.rate);
const T = d3.map(topN, d => d.state);
// console.log(T[i])
// customize this function to set the tooltip's contents however you see fit
tooltipDiv
.selectAll("p")
.data([i])
.join("p")
.html(`${T[i]}: ${Y[i]}`)
}
Insert cell
function resetStyle(selection) {
selection.attr("fill", "none");
}
Insert cell
function setStyle(selection) {
selection.attr("fill", "grey");
}
Insert cell
tooltipStyles = htl.html`<style>
/* modify these styles to however you see fit */
div.tooltip-${tooltipId} {
box-sizing: border-box;
position: absolute;
display: none;
top: 0;
left: -100000000px;
padding: 8px 12px;
font-family: sans-serif;
font-size: 12px;
color: #333;
background-color: #fff;
border: 1px solid #333;
border-radius: 4px;
pointer-events: none;
z-index: 1;
}
div.tooltip-${tooltipId} p {
margin: 0;
}
</style>`
Insert cell
// padding between the tooltip and mouse cursor
MOUSE_POS_OFFSET = 8
Insert cell
margin = ({ top: 10, right: 10, bottom: 10, left: 10 })
Insert cell
height = 600
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