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

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