ObservableHQ Plot maps for beginners — with some AI help
Part 4: Lat Lon points + a few extras
ObservableHQ Plot maps for beginners — with some AI help
Part 4: Lat Lon points + a few extras
This is a follow on from Part 3.
Step 1: Draw an LSOA map Step 2: Generate some random co-ordinates Step 3: Add the points to the map Step 4: Refine your map further Step 5: Adding your own font
Step 1: Draw an LSOA map
First step was to choose and LSOA and make a filtered dataset, like I did in Part 2 with Claude prompt
Can you use this code as a basis to create me a filtered GeoJSON from my base file banes_lsoa_geojson where the property LSOA21CD equals E01014388.
chew_valley = ({type: “FeatureCollection”,features: banes_wards.features.filter((d) => d.properties.Ward_name === “Chew Valley”)})
filtered_lsoa = ({
type: "FeatureCollection",
features: banes_lsoa_geojson.features.filter(
(d) => d.properties.LSOA21CD === "E01014388"
)
})
and follow with Claude prompt:
and can you alter this Plot code for my new cell results
Plot.plot({ width: 800, height: 600, projection: { type: “equal-earth”, domain: chew_valley }, marks: [Plot.geo(chew_valley, { stroke: “black”, fill: “#ddd” })] })
Plot.plot({
width: 800,
height: 600,
projection: { type: "equal-earth", domain: filtered_lsoa },
marks: [Plot.geo(filtered_lsoa, { stroke: "black", fill: "#ddd" })]
})
If you’re interested in the coding side of things, you’ll see that all that was really needed here was changing some field names, filter variables and file names — pretty achievable stuff if you’ve got the ambition start coding and wean yourself off AI help and do the work yourself…
Step 2: Generate some random co-ordinates
I asked this question to Claude and it wasn’t able to help so I tried ChatGPT and got the results I needed- ChatGPT prompt “can you generate me an array of 5 random lat lon co-ordinates within the LSOA E01014388”.
After the 1st answer it offered to calculate points based on precise geometry of the area and I said yes.
[
{ "lat": 51.362341, "lon": -2.344891 },
{ "lat": 51.361102, "lon": -2.343517 },
{ "lat": 51.363008, "lon": -2.346225 },
{ "lat": 51.359842, "lon": -2.345119 },
{ "lat": 51.364575, "lon": -2.342983 }
]
Step 3: Add the points to the map
I started with this Claude prompt:
I have a set of lat, lon coordinates in an array called randomPositions — field names are “lat” and “lon”.
Could you alter this ObservableHQ cell code to add the points onto the map
Plot.plot({ width: 800, height: 600, projection: { type: “equal-earth”, domain: filtered_lsoa }, marks: [Plot.geo(filtered_lsoa, { stroke: “black”, fill: “#ddd” })] })
Plot.plot({
width: 800,
height: 600,
projection: { type: "equal-earth", domain: filtered_lsoa },
marks: [
Plot.geo(filtered_lsoa, { stroke: "black", fill: "#ddd" }),
Plot.dot(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
r: 4,
fill: "red",
stroke: "white"
})
]
})

You’ll notice that one of the random points generated by ChatGPT is not within the boundaries of the LSOA — AI is not always right! Since this is for demo purposes only and the points mean nothing then it doesn’t worry me.
Step 4: Refine your map further
This is the point where the map becomes your own. Observable Plot has so many capabilities and it becomes a matter of asking the right questions and remembering that there will be some things it can’t do so you might have to compromise.
As a demo, I’ve asked it a few questions:
Can you make the dots a bit bigger and add numbers based on the order of the array?
Plot.plot({
width: 800,
height: 600,
projection: { type: "equal-earth", domain: filtered_lsoa },
marks: [
Plot.geo(filtered_lsoa, { stroke: "black", fill: "#ddd" }),
Plot.dot(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
r: 8,
fill: "red",
stroke: "white"
}),
Plot.text(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
text: (d, i) => i + 1,
fill: "white",
fontSize: 10,
fontWeight: "bold"
})
]
})

Can you join the dots with arrow lines?
Plot.plot({
width: 800,
height: 600,
projection: { type: "equal-earth", domain: filtered_lsoa },
marks: [
Plot.geo(filtered_lsoa, { stroke: "black", fill: "#ddd" }),
Plot.arrow(randomPositions.slice(0, -1), {
x1: (d) => d.lon,
y1: (d) => d.lat,
x2: (d, i) => randomPositions[i + 1].lon,
y2: (d, i) => randomPositions[i + 1].lat,
stroke: "blue",
strokeWidth: 1.5,
headLength: 8
}),
Plot.dot(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
r: 8,
fill: "red",
stroke: "white"
}),
Plot.text(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
text: (d, i) => i + 1,
fill: "white",
fontSize: 10,
fontWeight: "bold"
})
]
})

I can’t see the arrows — any compromise?
Plot.plot({
width: 800,
height: 600,
projection: { type: "equal-earth", domain: filtered_lsoa },
marks: [
Plot.geo(filtered_lsoa, { stroke: "black", fill: "#ddd" }),
Plot.arrow(randomPositions.slice(0, -1), {
x1: (d) => d.lon,
y1: (d) => d.lat,
x2: (d, i) => randomPositions[i + 1].lon,
y2: (d, i) => randomPositions[i + 1].lat,
stroke: "orange",
strokeWidth: 2,
headLength: 10,
insetStart: 10,
insetEnd: 10
}),
Plot.dot(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
r: 8,
fill: "red",
stroke: "white"
}),
Plot.text(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
text: (d, i) => i + 1,
fill: "white",
fontSize: 10,
fontWeight: "bold"
})
]
})

can you make the arrow points smaller and make the arrows the same color as the dots — maybe purple?
Plot.plot({
width: 800,
height: 600,
projection: { type: "equal-earth", domain: filtered_lsoa },
marks: [
Plot.geo(filtered_lsoa, { stroke: "black", fill: "#ddd" }),
Plot.arrow(randomPositions.slice(0, -1), {
x1: (d) => d.lon,
y1: (d) => d.lat,
x2: (d, i) => randomPositions[i + 1].lon,
y2: (d, i) => randomPositions[i + 1].lat,
stroke: "purple",
strokeWidth: 2,
headLength: 5,
insetStart: 10,
insetEnd: 10
}),
Plot.dot(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
r: 8,
fill: "purple",
stroke: "white"
}),
Plot.text(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
text: (d, i) => i + 1,
fill: "white",
fontSize: 10,
fontWeight: "bold"
})
]
})

This is where I’m going to stop — I feel happy with the result.
You’ll notice that Claude also gives you tips and hints regarding what it’s done so that you can understand the coding behind the chart if you so wish.
Step 5: Adding your own font
Depending one where you want to use your chart, you might want to add your own font.
I asked for a cell for the Google font Figjam but you can customise to your needs — although fonts can be tricky so you may need to compromise.
Claude prompt:
can you give me an ObservableHQ cell which allows me to use the Google Font Figjam in my notebook cells?
html`<style>
@import url('https://fonts.googleapis.com/css2?family=FigJam&display=swap');
</style>`
REMEMBER: this is an HTML cell not a Javascript cell — so when you press the + button, choose HTML for this code cell

Then you need to update the chart. I used another Claude prompt:
can you alter this ObservableHQ cell code to reference my new font and add a title — My chart — and subtitle — my subtitle.
Plot.plot({ width: 800, height: 600, projection: { type: “equal-earth”, domain: filtered_lsoa }, marks: [ Plot.geo(filtered_lsoa, { stroke: “black”, fill: “#ddd” }), Plot.arrow(randomPositions.slice(0, -1), { x1: (d) => d.lon, y1: (d) => d.lat, x2: (d, i) => randomPositions[i + 1].lon, y2: (d, i) => randomPositions[i + 1].lat, stroke: “purple”, strokeWidth: 2, headLength: 5, insetStart: 10, insetEnd: 10 }), Plot.dot(randomPositions, { x: (d) => d.lon, y: (d) => d.lat, r: 8, fill: “purple”, stroke: “white” }), Plot.text(randomPositions, { x: (d) => d.lon, y: (d) => d.lat, text: (d, i) => i + 1, fill: “white”, fontSize: 10, fontWeight: “bold” }) ] })
Plot.plot({
width: 800,
height: 600,
title: "My chart",
subtitle: "my subtitle",
style: {
fontFamily: "FigJam, sans-serif"
},
projection: { type: "equal-earth", domain: filtered_lsoa },
marks: [
Plot.geo(filtered_lsoa, { stroke: "black", fill: "#ddd" }),
Plot.arrow(randomPositions.slice(0, -1), {
x1: (d) => d.lon,
y1: (d) => d.lat,
x2: (d, i) => randomPositions[i + 1].lon,
y2: (d, i) => randomPositions[i + 1].lat,
stroke: "purple",
strokeWidth: 2,
headLength: 5,
insetStart: 10,
insetEnd: 10
}),
Plot.dot(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
r: 8,
fill: "purple",
stroke: "white"
}),
Plot.text(randomPositions, {
x: (d) => d.lon,
y: (d) => d.lat,
text: (d, i) => i + 1,
fill: "white",
fontSize: 10,
fontWeight: "bold"
})
]
})

Step 6: Exporting your map to use elsewhere
Unless you’re going through this process as a learning exercise, you’ll want to use your maps elsewhere.
Luckily this is an easy one. Click the ellipsis by your map and click Download as PNG

That’s it. Series finished. Hopefully this should be a useful enough intro that will allow you to explore and discover on your own —with less AI assistance in the future should you so wish.
Here’s the link to the notebook.
Thanks for reading and please do reach out if you’re looking for a d3 solution — big or small — www.bmdata.co.uk
메타데이터
- post_id
- 1272035ed130
- slug
- observablehq-plot-maps-for-beginners-with-some-ai-help-1272035ed130
- url
- https://ai.plainenglish.io/observablehq-plot-maps-for-beginners-with-some-ai-help-1272035ed130
- canonical_url
- https://ai.plainenglish.io/observablehq-plot-maps-for-beginners-with-some-ai-help-1272035ed130
- author_url
- https://medium.com/@bryony_17728
- status
- ok
- fetched_at
- 2026-07-30 14:02:18