Public
Edited
Sep 11, 2024
1 fork
Importers
Insert cell
Insert cell
chart = Scatterplot(cars, {
x: d => d.mpg,
y: d => d.hp,
title: d => d.name,
xLabel: "Miles per gallon →",
yLabel: "↑ Horsepower",
stroke: "steelblue",
width,
height: 600
})
Insert cell
Scatterplot(cars, {
x: d => d.hp,
y: d => d.disp,
xLabel: "Horsepower →",
yLabel: "↑ Displacement",
stroke: "transparent",
fill: "firebrick",
width: 600,
height: 400
})
Insert cell
cars = FileAttachment("mtcars.csv").csv({typed: true})
Insert cell
// Modified from https://observablehq.com/@d3/scatterplot
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
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
aspectRatio, // inner aspect ratio
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 = "transparent", // fill color for dots
fillOpacity = 1, // fill opacity for dots
stroke = "currentColor", // stroke color for the dots
strokeWidth = 1.5, // stroke width for dots
strokeOpacity = 1, // stroke opacity 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]));

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

// Construct scales.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);

// Compute dimensions.
// Adapted from Observable Plot.
// Released under the ISC license.
// https://github.com/observablehq/plot/blob/main/src/dimensions.js
if (aspectRatio != null) {
aspectRatio = +aspectRatio;
if (!(isFinite(aspectRatio) && aspectRatio > 0)) throw new Error(`invalid aspectRatio: ${aspectRatio}`);
const ratio = aspectRatioLength(yScale) / (aspectRatioLength(xScale) * aspectRatio);
const w = width - marginLeft - marginRight - insetLeft - insetRight;
height = ratio * w + insetTop + insetBottom + marginTop + marginBottom;
xScale.range([marginLeft + insetLeft, width - marginRight - insetRight]);
yScale.range([height - marginBottom - insetBottom, marginTop + insetTop]);
}

// Create canvas.
const ctx = DOM.context2d(width, height);

// x-axis

const xFormatter = xScale.tickFormat(width / 80, xFormat);
const xTicks = xScale.ticks(width / 80);

ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.strokeStyle = 'black';

xTicks.forEach(d => {
// tick
ctx.globalAlpha = 1.0;
ctx.beginPath();
ctx.moveTo(xScale(d), height - marginBottom);
ctx.lineTo(xScale(d), height - marginBottom + 6);
ctx.stroke();

// grid lines
ctx.globalAlpha = 0.1;
ctx.beginPath();
ctx.moveTo(xScale(d), height - marginBottom);
ctx.lineTo(xScale(d), marginTop);
ctx.stroke();
// label
ctx.globalAlpha = 1.0;
ctx.fillText(xFormatter(d), xScale(d), height - marginBottom + 9);
});

if (xLabel) {
ctx.globalAlpha = 1.0;
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillText(xLabel, width, height - 1);
}

// y-axis

const yFormatter = yScale.tickFormat(height / 50, yFormat);
const yTicks = yScale.ticks(height / 50);

ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.strokeStyle = 'black';

yTicks.forEach(d => {
// tick
ctx.globalAlpha = 1.0;
ctx.beginPath();
ctx.moveTo(marginLeft, yScale(d));
ctx.lineTo(marginLeft - 6, yScale(d));
ctx.stroke();

// grid lines
ctx.globalAlpha = 0.1;
ctx.beginPath();
ctx.moveTo(marginLeft, yScale(d));
ctx.lineTo(width - marginRight, yScale(d));
ctx.stroke();
// label
ctx.globalAlpha = 1.0;
ctx.fillText(yFormatter(d), marginLeft - 9, yScale(d));
});

if (yLabel) {
ctx.globalAlpha = 1.0;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(yLabel, 0, 1);
}

// text

if (T) {
ctx.globalAlpha = 1;
ctx.font = '10px sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.strokeStyle = halo;
ctx.lineWidth = haloWidth;
I.forEach(i => {
ctx.strokeText(T[i], xScale(X[i]) + 7, yScale(Y[i]));
ctx.fillText(T[i], xScale(X[i]) + 7, yScale(Y[i]));
});
}

// dots

ctx.strokeStyle = stroke;
ctx.fillStyle = fill;
ctx.lineWidth = strokeWidth;

I.forEach(i => {
ctx.beginPath();

ctx.arc(xScale(X[i]), yScale(Y[i]), r, 0, 2 * Math.PI);

ctx.globalAlpha = strokeOpacity;
ctx.stroke();

ctx.globalAlpha = fillOpacity;
ctx.fill();
});

return ctx.canvas;
}
Insert cell
// Adapted from Observable Plot.
// Released under the ISC license.
// https://github.com/observablehq/plot/blob/main/src/dimensions.js
function aspectRatioLength(scale) {
let transform;

if (scale.exponent) {
// pow
transform = (x) => Math.pow(x, scale.exponent());
} else if (scale.bandwidth) {
// point or band
return scale.domain().length;
} else if (scale.base) {
// log
transform = Math.log;
} else {
transform = Number;
}
const [min, max] = d3.extent(scale.domain());
return Math.abs(transform(max) - transform(min));
}
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