Public
Edited
Apr 12
Paused
Importers
40 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function dotAndWhisker(data, options = {}) {
const {
// Default values specific to this custom mark.
r = 8,
fill = "white",
stroke = "currentColor",
strokeWidth = 2,
x,
y,
w = "spread",
...rest // Remaining standard options that can be passed to both dot and whisker
} = options;

const linkOptions = {
...rest,
stroke,
strokeWidth,
x1: (d) => d.x - d.z / 2, // Left whisker
x2: (d) => d.x + d.z / 2, // Right whisker
y1: "y",
y2: "y"
};

return [
Plot.link(
data.map((d) => ({ x: d[x], y: d[y], z: d[w] })),
linkOptions
),
Plot.dot(data, { ...options, r, fill, stroke, strokeWidth })
];
}
Insert cell
Insert cell
{
const data = [
{ question: "a", score: 20, stdev: 8 },
{ question: "b", score: 80, stdev: 12 },
{ question: "c", score: 50, stdev: 24 },
{ question: "d", score: 70, stdev: 13 }
];

return Plot.plot({
height: 190,
x: { domain: [0, 100] },
y: { label: "Question" },
marks: [dotAndWhisker(data, { x: "score", y: "question", w: "stdev" })]
});
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function fan(cxt, area) {
const r = sqrt(area) / 2; // Define the shape's 'radius' based on the area parameter
cxt.moveTo(0, r);
cxt.lineTo(-r, 0);
cxt.arc(0, 0, r, π, 0); // Semicircle of radius r from the '9 o'clock' (π radians) to '3 o'clock' (0) angle
cxt.closePath();
}
Insert cell
Insert cell
Plot.plot({
height: 120,
margin: 30,
r: { domain: [0, 10], range: [0, 24] }, // Set symbol's radius to range up to 24 pixel units
marks: [
Plot.dot(
d3.range(1, 11).map((d) => ({ x: d })),
{
x: "x",
r: "x",
fill: "rgb(91,131,149)",
symbol: { draw: fan } // Object within which we specify the custom draw function
}
)
]
})
Insert cell
Insert cell
function tri(cxt, area) {
const r = sqrt(area / 2);
const vertices = [π, (7 * π) / 3, (11 * π) / 3].map((a) => [
r * cos(a + π / 6),
r * sin(a + π / 6) - r / 4
]);
cxt.moveTo(...vertices[0]);
cxt.lineTo(...vertices[1]);
cxt.lineTo(...vertices[2]);
cxt.closePath();
}
Insert cell
Plot.plot({
height: 120,
margin: 30,
r: { domain: [0, 10], range: [0, 18] },
symbol: {
range: ["circle", { draw: tri }, "square", { draw: fan }]
}, // Mix custom and built-in symbols
marks: [
Plot.dot(
d3.range(1, 13).map((d) => ({ x: d })),
{ x: "x", r: "x", fill: "rgb(91,131,149)", symbol: "x" }
)
]
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function toTurtle(svgPath, options = {}) {
const { scaleFn = (area) => sqrt(area) / 16 } = options;
const type = { M: "moveTo", L: "lineTo", C: "bezierCurveTo", Z: "closePath" };
const segs = PathData.parse(svgPath, { normalize: true });
const cmds = segs.map(
(s) => (ctx, len) => ctx[type[s.type]](...s.values.map((d) => d * len))
);
return (ctx, area) => cmds.forEach((fn) => fn(ctx, scaleFn(area)));
}
Insert cell
Insert cell
Insert cell
Insert cell
person = toTurtle(personPath)
Insert cell
Insert cell
Plot.plot({
r: { range: [1, 12] }, // Enlarge the default size of symbols a little so we can see the shape
marks: [
Plot.dot(
d3.range(1, 11).map((d) => ({ x: d })),
{ x: "x", r: "x", fill: "rgb(91,131,149)", symbol: { draw: person } }
)
]
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
heart = toTurtle(heartPath, { scaleFn: (area) => Math.sqrt(area) / scaleDown })
Insert cell
{
const data = d3.range(1, 11).map((d) => ({ x: d }));
return Plot.plot({
height: 120,
r: { range: [1, 12] }, // Enlarge the default size of all symbols so we can see the shape clearly
marks: [
Plot.dot(data, {
x: "x",
r: "x",
fill: "rgb(91,131,149)",
stroke: "white",
symbol: { draw: heart }
}),
// Reference square for comparing custom size
Plot.dot(data, { x: "x", r: "x", symbol: "square", stroke: "firebrick" })
]
});
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
animals = [cowPath, pigPath, sheepPath].map((p) => ({
draw: toTurtle(p, { scaleFn: (area) => sqrt(area) / 5 })
}))
Insert cell
Insert cell
Plot.plot({
height: 300,
marginLeft: 120,
marginRight: 40,
x: { axis: null },
y: { axis: null },
color: { range: ["rgb(194,81,64)", "rgb(93,93,93)", "rgb(91,131,149)"] }, // Colours for the 3 animal types
symbol: { range: animals }, // Symbol shapes for the three animal types
fy: { label: null, tickPadding: 40, padding: 0.2 }, // space for facet tick labels and vertical facet gap
marks: [
Plot.dot(livestockData, {
x: "x",
y: "livestock",
symbol: "livestock",
r: 14,
fy: "country",
fill: "livestock"
})
]
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function teardrop(ctx, len) {
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(len * 0.3, len * -0.1, 0, -len);
ctx.quadraticCurveTo(len * -0.3, len * -0.1, 0, 0);
ctx.closePath();
}
Insert cell
Insert cell
Plot.plot({
margin: 20,
length: { range: [0, 28] },
x: { axis: null },
y: { axis: null },
color: { range: ["#9d4310", "#24837b"] },
marks: [
Plot.vector(vData, {
x: "x",
y: "y",
rotate: "dir",
length: "mag",
shape: { draw: teardrop }, // Use the custom draw function for vector marks
fill: (d) => cos((π * d.dir) / 180) // Change colour according to vector direction
})
]
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
// Define the SVG definitions
const svgDefs = () => htl.svg`<defs>
<pattern id="myFill" patternUnits="userSpaceOnUse" width="10" height="10" patternTransform="rotate(-45)">
<line x1="0" x2="10" stroke="currentColor" />
</pattern>
</defs>`;

return Plot.plot({
marks: [
svgDefs, // Reference the SVG defs in the marks list so other marks can use it.
Plot.rectY(animalData, {
x: "animal",
y: "count",
fill: "url(#myFill)", // The url needs to match one of the ids in <defs>.
stroke: "currentColor"
})
]
});
}
Insert cell
Insert cell
function hachure(id, options = {}) {
const {
stroke = "currentColor",
strokeWidth = 1,
opacity = 1,
spacing = 8,
rotate = 45
} = options;
const size = 10;
return `<pattern id="${id}" patternUnits="userSpaceOnUse" width="${size}" height="${spacing}" patternTransform="rotate(${
rotate - 90
})">
<line x1="0" x2="${size}" stroke="${stroke}" stroke-width="${strokeWidth*2}" opacity="${opacity}" />
</pattern>`;
}
Insert cell
Insert cell
{
const patternDefs = [
{ id: "hach1", options: { strokeWidth: 0.6, spacing: 3, rotate: 90 } },
{ id: "hach2", options: { strokeWidth: 1.6, spacing: 6, rotate: 45 } },
{ id: "hach3", options: { strokeWidth: 1.6, spacing: 6, rotate: 90 } },
{ id: "hach4", options: { strokeWidth: 1.2, spacing: 5, rotate: 0 } },
{ id: "hach5", options: { strokeWidth: 1.6, spacing: 6, rotate: -45 } },
{ id: "hach6", options: { strokeWidth: 1.2, spacing: 6, rotate: 0 } }
];

// Generate the SVG pattern definitions.
const patterns = patternDefs.map(({ id, options }) => hachure(id, options));

// Embed the SVG with <defs> and Plot spec inside some html:
return html`<svg width="0" height="0"><defs>${patterns}</defs></svg>
${Plot.plot({
color: { range: patternDefs.map(({ id }) => `url(#${id})`) },
x: { tickSize: 0, label: null },
marks: [
Plot.rectY(animalData, {
x: "animal",
y: "count",
fill: "animal",
stroke: "currentColor",
strokeWidth: 1.5
})
]
})}
`;
}
Insert cell
Insert cell
function stipple(id, options = {}) {
const {
fill = "currentColor",
stroke = "none",
strokeWidth = 1,
opacity = 1,
spacing = 10,
r = 2,
rotate = 7
} = options;

const [w, h] = [spacing * 2, spacing * sqrt(3)];
return `<pattern id="${id}" patternUnits="userSpaceOnUse" width="${w}" height="${h}" patternTransform="rotate(${
rotate - 90
})">
<circle cx="${spacing}" cy="0" r="${r}" stroke="${stroke}" stroke-width="${strokeWidth}" fill="${fill}" opacity="${opacity}" />
<circle cx="0" cy="${
h / 2
}" r="${r}" stroke="${stroke}" stroke-width="${strokeWidth}" fill="${fill}" opacity="${opacity}" />
<circle cx="${w}" cy="${
h / 2
}" r="${r}" stroke="${stroke}" stroke-width="${strokeWidth}" fill="${fill}" opacity="${opacity}" />
<circle cx="${spacing}" cy="${h}" r="${r}" stroke="${stroke}" stroke-width="${strokeWidth}" fill="${fill}" opacity="${opacity}" />
</pattern>`;
}
Insert cell
Insert cell
stipplePatterns = [
{ id: "stipple1", options: { spacing: 10, r: 1.1 } },
{ id: "stipple2", options: { spacing: 8, r: 1.4 } },
{ id: "stipple3", options: { spacing: 6, r: 1.6 } },
{ id: "stipple4", options: { spacing: 4, r: 1.6 } },
{ id: "stipple5", options: { spacing: 3.1, r: 1.7 } },
{ id: "stipple6", options: { spacing: 3, r: 2 } }
].map((d) => ({ ...d, options: { ...d.options, fill: "rgb(91,131,149)" } }))
Insert cell
{
// Generate the SVG pattern definitions.
const patterns = stipplePatterns
.map(({ id, options }) => stipple(id, options))
.join("\n");

const data = stipplePatterns.map((_, i) => ({ x: i }));

return html`<svg width="0" height="0"><defs>${patterns}</defs></svg>
${Plot.plot({
height: 120,
margin: 50,
color: { range: stipplePatterns.map(({ id }) => `url(#${id})`) },
x: { axis: null },
marks: [
Plot.dot(data, {
x: "x",
fill: "x", // The fill texture will use the color range defined above
r: 48,
stroke: "currentColor"
})
]
})}
`;
}
Insert cell
Insert cell
Plot.plot({
width: 640,
height: 500,
projection: {
// Approximation of the OSGB map projection
type: "transverse-mercator",
rotate: [2, 0, 0],
domain: { type: "FeatureCollection", features: londonBoroughData }
},
color: {
type: "threshold", // We need to assign patterns to discrete categories
domain: [0.3, 0.4, 0.5, 0.6, 0.7],
range: stipplePatterns.map(({ id }) => `url(#${id})`), // Use patterns instead of colours
legend: true,
label: "Households with access to a car",
tickFormat: (d) => `${d * 100}%`
},
marks: [
// A copy of the patterns is needed to allow "download as PNG / SVG"
() =>
svg`<defs>${stipplePatterns.map(({ id, options }) =>
stipple(id, options)
)}`,
Plot.geo(londonBoroughData, {
fill: (d) => boroughAccess[d.id],
stroke: "black"
})
]
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class Ellipse extends Plot.Mark {
static defaults = { fill: "rgb(91,131,149)", stroke: null }; // Set the default options for this mark.

constructor(data, options = {}) {
const { x, y, rx, ry = rx } = options; // Extract the channel options relevant to this mark
super(
data, // The data to be encoded
{
// Channels and their associated scalings relevant to this mark
x: { value: x, scale: "x" },
y: { value: y, scale: "y" },
rx: { value: rx, scale: "r" },
ry: { value: ry, scale: "r", optional: true }
},
options, // The options object for user-customisation
Ellipse.defaults // The default option values specific to this mark
);
}

// The method that will do the custom drawing of the mark.
// indices: uInt32 array of numbers corresponding to the number of data items
// scales: The scaling functions used by this mark
// channels: The data items that have been encoded via channels
// dimensions: width, height and the 4 margins in pixel units of the drawing area
// cxt: The mark's context, that includes several elements used by D3 etc. for rendering
render(indices, scales, channels, dimensions, cxt) {
const { x: xs, y: ys, rx: rxs, ry: rys, fill: fs } = channels;

return htl.svg`<g>${[...indices].map((i) => {
const fill = fs?.value ?? (fs ? fs[i] : this.fill); // Is colour a channel, constant or default?
return htl.svg`
<ellipse cx="${xs[i]}" cy="${ys[i]}"
rx="${abs(rxs[i])}" ry="${abs(rys[i])}"
fill="${fill}" />`;
})}</g>`;
}
}
Insert cell
Insert cell
{
const data = d3.range(0, π, 0.1).map((d) => ({ a: d, b: π * cos(d) }));
return Plot.plot({
r: { range: [0, 10] }, // Set both rx and ry radii to be between 0 and 10 pixels
marks: [new Ellipse(data, { x: "a", y: "b", rx: "a", ry: "b" })]
});
}
Insert cell
Insert cell
class FlowCurve extends Plot.Mark {
static defaults = {
stroke: "black",
strokeWidth: 1,
opacity: 1
};

constructor(data, options = {}) {
const { x1, y1, x2, y2, sweep = 1 } = options;
super(
data,
{
x1: { value: x1, scale: "x" },
y1: { value: y1, scale: "y" },
x2: { value: x2, scale: "x" },
y2: { value: y2, scale: "y" }
},
options,
FlowCurve.defaults
);
this.sweep = sweep < 0 ? -1 : sweep > 0 ? 1 : 0; // +ve: bend left; 0: straight; -ve: bend right
}

render(indices, scales, channels, dimensions, cxt) {
const {
x1: x1s,
y1: y1s,
x2: x2s,
y2: y2s,
stroke: clrs,
strokeWidth: sws,
opacity: ops
} = channels;

return htl.svg`<g>${[...indices].map((i) => {
const clr = clrs?.value ?? (clrs ? clrs[i] : this.stroke);
const op = ops?.value ?? (ops ? ops[i] : this.opacity);
const sw = sws?.value ?? (sws ? sws[i] : this.strokeWidth);

const [x1, y1, cx, cy, x2, y2] = FlowCurve.cPts(
x1s[i],
y1s[i],
x2s[i],
y2s[i],
this.sweep
);
// The SVG curve drawing
const d = `M ${x1},${y1} C ${cx},${cy} ${x2},${y2} ${x2},${y2}`;
return htl.svg`<path d="${d}" fill="none" stroke="${clr}" stroke-width="${sw}" stroke-opacity="${op}" />`;
})}</g>`;
}

// For calculating the Bezier control point for a given pair of endpoints
static cPts = (x1, y1, x2, y2, d = 1) => {
const [cx, cy] = [x2 - d * 0.25 * (y1 - y2), y2 + d * 0.25 * (x1 - x2)];
return [x1, y1, cx, cy, x2, y2];
};
}
Insert cell
Insert cell
Insert cell
Plot.plot({
width: 900,
height: 510,
title: "Journeys made on the London bike hire scheme, January 2025",
subtitle: `Lines indicate journey direction (travelling from straight to curved end). Line thickness represents number of journeys made. Lines coloured by the start 'village'.`,
projection: {
type: "transverse-mercator",
rotate: [2, 0, 0],
domain: bikehireBounds
},
color: { domain: villages }, // Fix the colour scheme
opacity: { type: "sqrt", domain: [0, 100] }, // Fade out smaller flows
marks: [
Plot.geo(thamesGeodata, {
x: "longitude",
y: "latitude",
fill: "steelblue",
opacity: 0.5
}),
// Custom mark in place of Plot.arrow
new FlowCurve(
odData.filter((d) => d.numJourneys >= minJourneys),
{
x1: "oLon",
y1: "oLat",
x2: "dLon",
y2: "dLat",
strokeWidth: (d) => Math.pow(d.numJourneys, 1.5) / 800,
opacity: "numJourneys",
stroke: "oVillage"
}
)
]
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const purples = d3
.scaleSequential(d3.interpolatePurples)
.domain(d3.extent(flowers1, (d) => d.a));
const oranges = d3
.scaleSequential(d3.interpolateOranges)
.domain(d3.extent(flowers2, (d) => d.a));

return Plot.plot({
margin: 40,
marginTop: 250,
width: 900,
height: 600,
y: { axis: null },
r: { range: [0, 8] },

marks: [
new Stem(flowers1, {
x: "x",
y: "y",
len: "len",
w: 0.02,
asym: 7,
opacity: 0.8
}),
new Stem(flowers2, {
x: "x",
y: "y",
len: "len",
w: 0.03,
asym: -3,
opacity: 0.8
}),
// Purple flowers
new Flower(flowers1, {
x: "x",
y: "y",
len: "len",
r: (d) => abs(d.len * d.b),
nPetals: 9,
fill: (d) => purples(d.a),
cClr: "rgb(246, 226, 160)",
strokeWidth: 0.7
}),

// Orange flowers
new Flower(flowers2, {
x: "x",
y: "y",
len: "len",
r: (d) => abs(d.len * d.b),
nPetals: 5,
petalW: 3,
fill: (d) => oranges(d.a),
cClr: "white",
strokeWidth: 0.7
})
]
});
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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