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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more