Public
Edited
Apr 21, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
plot = {
let svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.style("display", "block");
const view = svg.append("g")
.attr("class", "view")
.attr("x", 0.5)
.attr("y", 0.5)
.attr("width", width - 1)
.attr("height", height - 1);

const gGrid = svg.append("g")
.attr("clip-path", "url(#clip)"); // Set clip rectangle for all elements in this group.

let x = d3.scaleLinear()
.domain(d3.extent(values, d => d.x))
.range([margin.left, width - margin.right]);

let y = d3.scaleLinear()
.domain(d3.extent(values, d => d.y))
.range([height - margin.bottom, margin.top]);

const pointerover = function(e) {
tooltip.style("display", "block");
d3.select(this)
.attr("r", (pointProps.size + 3) / currentTransform.k)
.style("opacity", 1);
}

const pointerleave = function(e) {
tooltip.style("display", "none");
d3.select(this)
.attr("r", (pointProps.size) / currentTransform.k)
.style("opacity", pointProps.opacity)
}
const pointermove = function(e, d) {
const xpos = d3.pointer(e, svg.node())[0];
const ypos = d3.pointer(e, svg.node())[1];

const row = tableData[d.index];
const values = Object.keys(row).map(k => [k, row[k]]);

const text = tooltip.selectAll("text")
.call(text => text
.selectAll("tspan")
.data(values)
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1}em`)
.attr("font-weight", (_, i) => i ? null : "bold")
.text(d => d[0] + ": " + d[1]));

const padding = 5;
const { y: texty, width: textw, height: texth } = text.node().getBBox();
const tipx = Math.max(padding, Math.min(xpos - textw / 2, width - textw - 2 * padding));
const tipy = Math.max(padding, Math.min(ypos - texth - 15, height - texth - 2 * padding));
tooltip.attr("transform", `translate(${tipx},${tipy})`);
text.attr("transform", `translate(${0},${-texty})`);
tooltip.selectAll("rect").attr("x", -padding)
.attr("y", -padding)
.attr("width", textw + 2 * padding)
.attr("height", texth + 2 * padding);
}

// Axes.
const xAxis = d3.axisBottom(x).ticks(width / 80);
const yAxis = d3.axisLeft(y).ticks(height / 50);

const gX = svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(xAxis)
.call(g => g.append("text")
.attr("x", width)
.attr("y", margin.bottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(plotProps.x + " →"));

const gY = svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(yAxis)
.call(g => g.append("text")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ " + plotProps.y));

// Append a clip rectangle to prevent drawing off-axis points when zooming/panning.
const clip = svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", margin.left)
.attr("y", margin.top)
.attr("height", height - margin.top - margin.bottom)
.attr("width", width - margin.left - margin.right);

// Append points in the scatterplot.
const dots = svg
.append("g")
.attr("clip-path", "url(#clip)") // Set clip rectangle for all elements in this group.
.selectAll("dot")
.data(values)
.join("circle")
.attr("cx", e => x(e.x))
.attr("cy", e => y(e.y))
.attr("r", pointProps.size)
.style("fill", "steelblue")
.style("opacity", pointProps.opacity)
// .append('title')
// .text((d) => `Sales were ${d.x} in ${d.y}`)
.on("pointerenter", pointerover)
.on("pointermove", pointermove)
.on("pointerleave", pointerleave)
.on("touchstart", event => event.preventDefault());


// Tooltip handling.
const tooltip = svg.append("g")
.style("pointer-events", "none")
.style("display", "none");

tooltip.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("rx", 6)
.attr("ry", 6)
.attr("height", 100)
.attr("width", 100)
.attr("fill", "white")
.attr("fill-opacity", 0.75)
.style("stroke", "gray")

tooltip.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("fill", "black")
.attr("text-anchor", "start")
.style("font-size", "14px");
let currentTransform = { k: 1, x: 0, y: 0 };
const zoomed = function({ transform }) {
const zx = transform.rescaleX(x).interpolate(d3.interpolateRound);
const zy = transform.rescaleY(y).interpolate(d3.interpolateRound);
currentTransform = transform;
view.attr("transform", transform);
//tooltip.attr("transform", transform);
dots
.attr("transform", transform)
.attr("stroke-width", 1 / transform.k)
.attr("r", pointProps.size / transform.k);
gX.call(xAxis.scale(transform.rescaleX(x)));
gY.call(yAxis.scale(transform.rescaleY(y)));
gGrid.call(grid, zx, zy);
}
// prevent scrolling then apply the default filter
const filter = function(event) {
event.preventDefault();
return (!event.ctrlKey || event.type === 'wheel') && !event.button;
}
const zoom = d3.zoom()
.scaleExtent([0.5, 32])
.filter(filter)
.on("zoom", zoomed);

const reset = function() {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity);
}

// Call zoom with identity transformation to initialize grid.
svg.call(zoom).call(zoom.transform, d3.zoomIdentity);
return Object.assign(svg.call(zoom).node(), {reset});
}
Insert cell
Insert cell
Insert cell
dataset = {
const source = {
name: "Cars",
notes: `1983 ASA Data Exposition`,
link: `http://lib.stat.cmu.edu/datasets/`,
data: await FileAttachment("cars.csv").csv({typed: true}),
id: "cars"
};

return source.data;
}
Insert cell
Insert cell
dataset
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
attributes = Object.keys(tableData[0]).splice(1)
Insert cell
Insert cell
data = tableData.map((e, i) => ({ x: e[plotProps.x], y: e[plotProps.y], index: i }))
Insert cell
Insert cell
values = data.filter(e => !Number.isNaN(e.x) && !Number.isNaN(e.y))
Insert cell
Insert cell
margin = ({top: 20, right: 20, bottom: 30, left: 40})
Insert cell
height = 600
Insert cell
aspectRatio = height / width
Insert cell
xAxis = (g, x, height) => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
Insert cell
yAxis = (g, y, title) => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".title").data([title]).join("text")
.attr("class", "title")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(title))
Insert cell
grid = (g, x, y) => g
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
.call(g => g
.selectAll(".x")
.data(x.ticks(12))
.join(
enter => enter.append("line").attr("class", "x").attr("y2", height),
update => update,
exit => exit.remove()
)
.attr("x1", d => 0.5 + x(d))
.attr("x2", d => 0.5 + x(d)))
.call(g => g
.selectAll(".y")
.data(y.ticks(12 * aspectRatio))
.join(
enter => enter.append("line").attr("class", "y").attr("x2", width),
update => update,
exit => exit.remove()
)
.attr("y1", d => 0.5 + y(d))
.attr("y2", d => 0.5 + y(d)));
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