Public
Edited
Apr 11, 2023
14 forks
Importers
34 stars
Insert cell
Insert cell
viewof map = mapboxD3({
features: depts.features,
fill: (d) => colorScale(+d.properties.STP27_PERS),

width,
mapboxOptions: {
center: [-74.0721, 4.711],
zoom: 4,
style: "mapbox://styles/mapbox/light-v10",
scrollZoom: true
},
r: (d) => rScale(+d.properties.STP27_PERS),
parentInvalidation: invalidation
})
Insert cell
viewof mapChoropleth = mapboxD3({
features: depts.features,
fill: (d) => colorScale(+d.properties.STP27_PERS),
mapboxOptions: {
center: [-74.0721, 4.711],
zoom: 4,
style: "mapbox://styles/mapbox/light-v10",
scrollZoom: true
},
width,
parentInvalidation: invalidation
})
Insert cell
Insert cell
viewof mapUpdate = mapboxD3({
features: depts.features,
fill: (d) => colorScale(+d.properties.STP27_PERS),

width,
mapboxOptions: {
center: [-74.0721, 4.711],
zoom: 4,
style: "mapbox://styles/mapbox/light-v10",
scrollZoom: true
},
r: () => 30,
delay: (_, i) => i * 10,
parentInvalidation: invalidation
})
Insert cell
{
await visibility();
mapUpdate.r = (d) => rScale(+d.properties.STP27_PERS);
mapUpdate.update();
}
Insert cell
Insert cell
viewof mapForces = mapboxD3({
features: depts.features.map((d) => ({ ...d })),
fill: (d) => colorScale(+d.properties.STP27_PERS),

width,
mapboxOptions: {
center: [-74.0721, 4.711],
zoom: 4,
style: "mapbox://styles/mapbox/light-v10",
scrollZoom: true
},
r: () => 10,
// delay: (_, i) => i * 10,
collide: true,
parentInvalidation: invalidation
})
Insert cell
{
// a little hack to show how to change the chart
await visibility();
mapForces.r = (d, i) => rScale(+d.properties.STP27_PERS);
mapForces.update();
}
Insert cell
depts.features[0]
Insert cell
depts = {
const depts = await FileAttachment(
"Colombia_departamentos_municipios_poblacion-topov2_simplifiedmapshaer_simpler.json"
).json();
return topojson.feature(depts, depts.objects.MGN_ANM_DPTOS);
}
Insert cell
rScale = d3
.scaleSqrt()
.domain([0, d3.max(depts.features, (d) => +d.properties.STP27_PERS)])
.range([0, 30])
Insert cell
colorScale = d3
.scaleSequential(d3.interpolateBlues)
.domain([0, d3.max(depts.features, (d) => +d.properties.STP27_PERS)])
Insert cell
function* mapboxD3({
width = 900,
height = 600,

features = [], // geoJSON features to draw
id = (d, i) => i,
r = null, // If not null, must be a function that returns the radius in pixels given the datum
fill = null, // If not null, must be a function that returns the color given the datum
stroke = "#777",
mapboxOptions = {
style: "mapbox://styles/mapbox/light-v10",
scrollZoom: true
},
container = html`<div style="height:${height}px; width:${width}px">`, // where to draw
d3Background = null,
delay = null,
transitionDuration = 0,
collide = false,
alphaRestart = 0.3, // Alpha to set before restarting if using simulation collide = true
parentInvalidation = invalidation,
strokeWidth = 1,
controls = false // Should display zooming controls
} = {}) {
let selected = null, // clicked element
loaded = false; // has mapbox finished loading

let rScale = null,
colorScale = null,
_r = r,
simulation = null,
hoverObj;
let dots;

if (!r) {
rScale = d3
.scaleSqrt()
.domain([0, d3.max(features, (d) => d?.properties?.value)])
.range([0, 15]);
_r = (d) => rScale(d?.properties?.value);
}

if (!fill) {
colorScale =
colorScale ||
d3
.scaleSequential(d3.interpolateInferno)
.domain([0, d3.max(features, (d) => d?.properties?.value)]);
fill = (d) => colorScale(d?.properties?.value);
}

yield container; // Give the container dimensions.

const map = (container.value = new mapboxgl.Map({
container,
...mapboxOptions
}));

// Setup our svg layer that we can manipulate with d3
const bb = container.getBoundingClientRect();

const svg = d3
.select(container)
.append("svg")
.attr("class", "d3Mapbox")
.style("position", "absolute")
.style("top", 0)
.style("left", 0)
// .attr("width", bb.width)
// .attr("height", bb.height)
.attr("width", width)
.attr("height", height)
.style("background", d3Background)
.style("pointer-events", "none"); // the svg shouldn't capture mouse events, so we can have pan and zoom from mapbox

//Project any point to map's current state
function projectPoint(lon, lat) {
let point = map.project(new mapboxgl.LngLat(lon, lat));
this.stream.point(point.x, point.y);
}

//Projection function
let transform = d3.geoTransform({ point: projectPoint });
let path = d3.geoPath().projection(transform);

svg.append("style").text(`
circle.highlight { stroke: red; }
`);

const updateDots = (features) => {
const dots = svg
.selectAll(".feature")
.data(features, id)
.join(r ? "circle" : "path")
.attr("class", "feature")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.on("click", (evt, d) => (selected = d))
.style("pointer-events", "all");

// hello-tippy needs nodes
if (dots.nodes().length) {
hoverObj = hover(dots, invalidation, { raise: false });
}

return dots;
};

dots = updateDots(features);

const getTransition = () => {
let t = dots;
if (transitionDuration || delay) {
t = t.transition().duration(transitionDuration).delay(delay);
}
return t;
};

const update = () => {
hoverObj && hoverObj.reset();
if (r) {
let t = getTransition();
t.attr("r", _r);

if (collide) {
features.forEach((d) => (d.foci = path.centroid(d)));
simulation.nodes(features);
simulation.alpha(alphaRestart).restart();
} else {
t.attr("cx", (d) => path.centroid(d)[0]).attr(
"cy",
(d) => path.centroid(d)[1]
);
}
} else {
dots.attr("d", path);
}
dots.attr("fill", fill);
dots.attr("stroke", stroke);
};

// Positioning
if (collide) {
// Position nodes via simulation;
features.forEach((d) => (d.foci = path.centroid(d)));
simulation = Simulation(features, {
clone: false,
x: (d) => d?.foci[0],
y: (d) => d?.foci[1],
r,
parentInvalidation,
tick: () => {
getTransition()
.attr("r", _r)
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y);
}
});
} else {
//position nodes manually;
if (r) {
dots
.attr("r", _r)
.attr("cx", (d) => path.centroid(d)[0])
.attr("cy", (d) => path.centroid(d)[1]);
} else {
dots.attr("d", path);
}
}

// Parameters to return
function set() {
container.value = {
transform,
path,
map,
dots,
hoverObj,
selected,
loaded,
update,
simulation,
set r(_) {
rScale = null;
_r = _;
if (collide) simulation.updateForces({ newR: _r });
},
get r() {
return r;
},
set fill(_) {
colorScale = null;
fill = _;
},
get fill() {
return fill;
},
set rScale(_) {
rScale = _;
if (collide) simulation.nodes(features);
},
get rScale() {
return rScale;
},
set colorScale(_) {
colorScale = _;
},
get colorScale() {
return colorScale;
},
set delay(_) {
delay = _;
},
get delay() {
return delay;
},
set features(_) {
features = _;
dots = updateDots(features);
},
set width(_) {
width = _;
svg.attr("width", width).attr("height", height);
},
set height(_) {
height = _;
svg.attr("width", width).attr("height", height);
}
};
container.dispatchEvent(new Event("input", { bubbles: true }));
}

if (!map) return;
// Every time the map changes, update the dots
map.on("viewreset", update);
map.on("move", update);
map.on("moveend", update);
map.on("load", () => {
loaded = true;
set();
});

parentInvalidation.then(() => {
map.off("viewreset", update);
map.off("move", update);
map.off("moveend", update);
map.remove();
});

if (controls) {
// Add zoom and rotation controls to the map.
map.addControl(new mapboxgl.NavigationControl());
}
// disable map rotation using right click + drag
map.dragRotate.disable();
// disable map rotation using touch rotation gesture
map.touchZoomRotate.disableRotation();

update();

set();
return container;
}
Insert cell
function Simulation(
data,
{
warmup = 200,
strengthY = 0.3,
strengthX = 0.3,
r = () => 3,
tick = () => {},
x = () => 0,
y = () => 0,
clone = true,
parentInvalidation
} = {}
) {
// return Generators.observe((notify) => {
const nodes = clone ? [...data.map((d) => ({ ...d }))] : data;

const simulation = d3.forceSimulation(nodes);

// To be able to update the forces
simulation.updateForces = ({ newX = x, newY = y, newR = r } = {}) => {
x = newX;
y = newY;
r = newR;
simulation
.force("x", d3.forceX(x).strength(strengthX))
.force("y", d3.forceY(y).strength(strengthY))
.force("collide", d3.forceCollide(r));
};

simulation.updateForces(x, y, r);

if (warmup) {
simulation.stop();
for (let i = 0; i < warmup; i++) {
simulation.tick();
}
}
simulation.restart();

simulation.on("tick", () => {
// notify(nodes);
// console.log(nodes[0]);
tick(nodes);
});

// notify(nodes);
parentInvalidation.then(() => simulation.stop());
// });
return simulation;
}
Insert cell
topojson = require("topojson-server@3", "topojson-simplify@3", "topojson-client@3")
Insert cell
function tipcontent(_d) {
const d = _d.properties;
return htl.html`
${Object.keys(d).map((c) => htl.html`<div>${c}: ${d[c]}</div>`)}`;
}
Insert cell
import { hover, link } with { tipcontent, tippytheme } from "@fil/hello-tippy"
// import { hover, link } with { tipcontent, tippytheme } from "01649316c88b8149"
Insert cell
tippytheme = "light"
Insert cell
link
Insert cell
Insert cell
Insert cell
Insert cell
viewof mapVegaLite = {
const container = html`<div style="height:600px; position:relative;">
<div id="mapbox" style="width: 100%;height: 100%"></div>
<div id="vegaLite" style="width: 100%;height: 100%; position: absolute; top: 0px; left:0px;pointer-events:none;"></div>
</div>`;
yield container; // Give the container dimensions.

const mapboxContainer = container.querySelector("#mapbox");
const vegaLiteContainer = container.querySelector("#vegaLite");

const map = (container.value = new mapboxgl.Map({
container: mapboxContainer,
// Bogotá 4.7110° N, 74.0721° W
center: [-74.0721, 4.711],
zoom: 10,
style: "mapbox://styles/jguerrag/cjuerw13z2vja1fndc9wzk7eo",
scrollZoom: true
}));

// //Project any point to map's current state
// function projectPoint() {
// function forward(lon, lat) {
// console.log("project", lon, lat);
// const point = map.project(new mapboxgl.LngLat(lon, lat));
// // this.stream.point(point.x, point.y);
// console.log(lon, lat, " -> ", [point.x, point.y]);
// return [point.x, point.y];
// }
// forward.invert = (x, y) => {
// return [x, y];
// };

// return forward;
// }

// const p = d3.geoProjectionMutator(projectPoint)();

//Project any point to map's current state
function projectPoint(lon, lat) {
var point = map.project(new mapboxgl.LngLat(lon, lat));
this.stream.point(point.x, point.y);
}

//Projection function
var transform = d3.geoTransform({ point: projectPoint });

const fakeProjection = (args) => {
const p = transform;
p.fitSize = () => {};

return p;
};

vl.vega.projection("mapbox", fakeProjection);

// Setup our svg layer that we can manipulate with d3
function update() {
vl.markGeoshape({ fill: "steelblue", stroke: "#706545", strokeWidth: 0.5 })
.project(vl.projection("mapbox"))
// .data(d3.geoProject(votingStations, transform).features)
.data(votingStations.features)
.config({ background: null })
.width(width)
.height(container.getBoundingClientRect().height)
.render()
.then((view) => {
vegaLiteContainer.innerHTML = "";
vegaLiteContainer.appendChild(view);
});
}

// Every time the map changes, update the dots
map.on("viewreset", update);
map.on("move", update);
map.on("moveend", update);

invalidation.then(() => {
map.off("viewreset", update);
map.off("move", update);
map.off("moveend", update);
map.remove();
});

update();
}
Insert cell
vl.markCircle()
.data(votingStations.features)
.encode(
vl.latitude().fieldQ("geometry.coordinates[1]"),
vl.longitude().fieldQ("geometry.coordinates[0]")
)
.render()
Insert cell
vl
.markGeoshape({ fill: 'steelblue', stroke: '#706545', strokeWidth: 0.5 })
.project(vl.projection('mercator'))
.data(votingStations.features)
.render()
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