function* mapboxD3({
width = 900,
height = 600,
features = [],
id = (d, i) => i,
r = null,
fill = null,
stroke = "#777",
mapboxOptions = {
style: "mapbox://styles/mapbox/light-v10",
scrollZoom: true
},
container = html`<div style="height:${height}px; width:${width}px">`,
d3Background = null,
delay = null,
transitionDuration = 0,
alphaRestart = 0.3,
parentInvalidation = invalidation,
strokeWidth = 1,
strokeOpacity = 1,
controls = false
} = {}) {
let selected = null,
loaded = false;
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));
return [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)
.attr("stroke-opacity", strokeOpacity)
.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);
t.attr("cx", (d) => {
const [x, y] = projectPoint.apply({ stream: {} }, [d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return x;
})
.attr("cy", (d) => {
const [x, y] = projectPoint.apply({ stream: {} }, [d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return y;
});
} else {
dots.attr("d", path);
}
dots.attr("fill", fill);
dots.attr("stroke", stroke);
};
// Positioning
//position nodes manually;
if (r) {
dots
.attr("r", _r)
.attr("cx", (d) => {
const [x, y] = projectPoint.apply({ stream: {} }, [d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return x;
})
.attr("cy", (d) => {
const [x, y] = projectPoint.apply({ stream: {} }, [d.geometry.coordinates[0], d.geometry.coordinates[1]]);
return y;
});
} 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 = _;
},
get r() {
return r;
},
set fill(_) {
colorScale = null;
fill = _;
},
get fill() {
return fill;
},
set rScale(_) {
rScale = _;
},
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;
}