Public
Edited
May 1, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Position of map and focus etc
// Middle of Australia
mapConfig = ({
lat: -26,
long: 137,
z: 4
})
Insert cell
height = width / 1.6 < 600 ? 600 : width / 1.6
Insert cell
container = {
const { lat, long, z } = mapConfig;

let container = DOM.element("div", {
id: "map",
style: `width:${width}px;height:${height}px`
});

// Yield early so we have something to latch onto
yield container;

var osm = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "© OpenStreetMap"
});

// Now we create a map object and add a layer to it.
let map = L.map("map", {
center: new L.LatLng(lat, long),
zoom: z,
layers: [osm],
zoomControl: true,
scrollWheelZoom: false
});
let osmLayer = L.tileLayer(
"https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}@2x.png",
{
attribution:
'Wikimedia maps beta | &copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}
).addTo(map);

// Adds an svg tag to the map
L.svg().addTo(map);

const mapSelect = d3.select("#map");
mapSelect.style("cursor", "pointer");

const svg = d3.select("#map").select("svg");

const node = svg
.append("g")
.selectAll(".node")
.data(nodes)
.join("g")
.attr("class", "node")
.attr("transform", (d) => {
const { x, y } = map.latLngToLayerPoint([d.latitude, d.longitude]);
return `translate(${x}, ${y})`;
});

node
.style("pointer-events", "all")
.on("mouseover mousemove", function (event) {
const group = d3.select(this);
const text = group.select("text");
text.style("fill", "white");
})
.on("mouseout", function () {
const group = d3.select(this);
const text = group.select("text");
text.style("fill", "none");
})
.selectAll("circle")
.data((d) => [d])
.join("circle")
.attr("r", 3)
.attr("fill", "white");

// Text for story nodes
node
.selectAll("text")
.data((d) => [d])
.join("text")
.style("fill", "none")
.text(function (d) {
return d.slug;
});

const categories = uniqueCategories.map((category) => {
return { name: category };
});

const simulation = d3
.forceSimulation()
.nodes(categories)
.on("tick", simulationTick)
.on("end", simulationEnd);

simulation
.force(
"collide",
d3
.forceCollide()
.radius((d) => 10)
.strength(1)
)
.force("manyBody", d3.forceManyBody().strength(3));

const link = svg
.append("g")
.selectAll(".link")
.data(links)
.join("line")
.attr("class", "link")
.style("stroke", "white")
.style("opacity", 0.1);

const category = svg
.append("g")
.selectAll(".category")
.data(categories)
.join("g")
.attr("class", "category")
.attr("transform", (d) => `translate(${width * 0.5}, ${height * 0.5})`);

const circle = category
.selectAll("circle")
.data((d) => [d])
.join("circle")
.attr("r", 7)
.attr("fill", "orange");

function getOffset(width, height) {
return { x: width * 0.5, y: height * 0.5 };
}

function simulationTick() {
circle.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
update();
}

function simulationEnd() {
console.log("Simulation complete...");
}

// Update positions on map move or zoom
const update = () => {
node.attr("transform", (d) => {
return `translate(${
map.latLngToLayerPoint([d.latitude, d.longitude]).x
},${map.latLngToLayerPoint([d.latitude, d.longitude]).y})`;
});

// TODO: Don't repeat yourself DRY
link
.attr("x1", (d) => {
const foundNode = nodes.find((node) => node.slug === d.source);
if (!foundNode) return 0;
const { x, y } = map.latLngToLayerPoint([
foundNode.latitude,
foundNode.longitude
]);
return x;
})
.attr("y1", (d) => {
const foundNode = nodes.find((node) => node.slug === d.source);
if (!foundNode) return 0;
const { x, y } = map.latLngToLayerPoint([
foundNode.latitude,
foundNode.longitude
]);
return y;
})
.attr("x2", (d) => {
const foundNode = categories.find(
(category) => category.name === d.target
);
if (!foundNode) return 0;
const { x, y } = foundNode;
return x + getOffset(width, height).x;
})
.attr("y2", (d) => {
const foundNode = categories.find(
(category) => category.name === d.target
);
if (!foundNode) return 0;
const { x, y } = foundNode;
return y + getOffset(width, height).y;
});
};

map.on("moveend", update);
}
Insert cell
Insert cell
// Importing from another notebook. https://observablehq.com/d/a7664a7f25690ff6
import { uniqueCategories, geoStories, categoryLinks } from "a7664a7f25690ff6"
Insert cell
nodes = [...geoStories.values()]
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
Insert cell
html`<link href='${resolve('leaflet@1.2.0/dist/leaflet.css')}' rel='stylesheet' />`
Insert cell
Insert cell
Insert cell
Insert cell
randomNumber = (maxMinMax) => {
const randomNumber =
Math.ceil(Math.random() * maxMinMax) * (Math.round(Math.random()) ? 1 : -1);
return randomNumber;
}
Insert cell
categoryLocations = [
{ lat: -13, long: 112 },
{ lat: -11, long: 118 },
{ lat: -9, long: 128 },
{ lat: -9, long: 132 },
{ lat: -10, long: 135 },
{ lat: -11, long: 139 },
{ lat: -10, long: 144 },
{ lat: -13, long: 147 },
{ lat: -16, long: 150 },
{ lat: -19, long: 152 },
{ lat: -23, long: 156 },
{ lat: -29, long: 157 },
{ lat: -36, long: 156 },
{ lat: -42, long: 156 },
{ lat: -46, long: 153 },
{ lat: -48, long: 147 },
{ lat: -46, long: 138 },
{ lat: -45, long: 130 },
{ lat: -44, long: 122 },
{ lat: -42, long: 115 },
{ lat: -39, long: 108 },
{ lat: -35, long: 106 },
{ lat: -30, long: 107 },
{ lat: -25, long: 107 },
{ lat: -20, long: 108 },
{ lat: -17, long: 111 },
{ lat: -15, long: 111 }
]
Insert cell
categoriesForFlourish = uniqueCategories.map((category, index) => {
return {
code: category,
// latitud: categoryLocations[index].lat,
// longitude: categoryLocations[index].long,
latitud: mapConfig.lat + randomNumber(1),
longitude: mapConfig.long + randomNumber(1),
name: category,
category: "category"
};
})
Insert cell
nodesForFlourish = nodes.map((node) => {
return {
code: node.id,
latitud: node.latitude,
longitude: node.longitude,
name: node.slug,
category: "node"
};
})
Insert cell
combinedLocations = [...categoriesForFlourish, ...nodesForFlourish]
Insert cell
linksForFlourish = links.map((link) => {
return { source: link.sourceId, destination: link.target };
})
Insert cell
Insert cell
import { hover } from "@mkfreeman/plot-tooltip"
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