function BubbleMap(
data,
{
features,
position = (d) => d,
value = () => undefined,
width = 640,
height = 400,
margin = 10,
borders,
title,
scale = d3.scaleSqrt,
domain,
maxRadius = 40,
nice = false,
ticks = 3,
showTitle,
projection = d3.geoIdentity().reflectY(true).fitWidth(width, features),
backgroundFill = main.grey["200"],
backgroundStroke = "white",
backgroundStrokeWidth,
backgroundStrokeOpacity,
backgroundStrokeLinecap = "round",
backgroundStrokeLinejoin = "round",
fill = accent.blue,
fillOpacity = 0.5,
stroke = "white",
strokeWidth = 0.5,
strokeOpacity,
titleFill = main.grey["900"],
titleFontSize = 10,
titleFontWeight = "normal",
titleStroke = "white",
titleStrokeWidth = 1.5,
titleStrokeOpacity = 0.6,
avoidOverlapping = false
}
) {
const V = d3.map(data, value).map((d) => (d == null ? NaN : +d));
let P = d3.map(data, position);
const T = title == null ? null : d3.map(data, title);
features = normalizeWindingInPlace({ ...features });
borders = borders != null ? normalizeWindingInPlace({ ...borders }) : borders;
if (domain === undefined) domain = [0, d3.max(V)];
let radius = scale(domain, [0, maxRadius]);
if (nice) {
radius = radius.copy().nice();
}
projection = projection.scale === undefined ? projection() : projection;
projection.fitExtent(
[
[margin, margin],
[width - margin, height - margin]
],
features
);
const pathGenerator = d3.geoPath(projection);
if (avoidOverlapping) {
P = applySimulation(P, V, {
radius,
projection,
width: width - 2 * margin,
height: height - 2 * margin
});
}
const svg = DOM.svg(width, height);
d3.select(svg)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.style("background", "white");
d3.select(svg)
.append("g")
.attr("class", "boundaries")
.selectAll(".boundary")
.data(features.features)
.join("path")
.classed("boundary", true)
.attr("d", pathGenerator)
.attr("fill", backgroundFill);
if (borders != null) {
d3.select(svg)
.append("path")
.attr("pointer-events", "none")
.attr("fill", "none")
.attr("stroke", backgroundStroke)
.attr("stroke-linecap", backgroundStrokeLinecap)
.attr("stroke-linejoin", backgroundStrokeLinejoin)
.attr("stroke-width", backgroundStrokeWidth)
.attr("stroke-opacity", backgroundStrokeOpacity)
.attr("d", pathGenerator(borders));
}
const legend = circleLegend({
scale: radius,
marginTop: 0,
marginBottom: 0,
stroke: "#666",
strokeWidth: 0.75,
tickFormat: radius.tickFormat(4, "s"),
tickFont: `10px ${fontFamily}`,
tickStrokeWidth: 0.75,
tickStroke: "#666"
});
let { dimensions: legendDimensions } = legend;
legendDimensions =
legendDimensions == null ? { width: 0, height: 0 } : legendDimensions;
d3.select(svg)
.append("g")
.attr("class", "peripherals")
.attr(
"transform",
`translate(${width - legendDimensions.width - margin},${
height - legendDimensions.height - margin
})`
)
.node()
.append(legend);
const bubbles = d3
.select(svg)
.append("g")
.selectAll(".bubbles")
.data(
d3
.range(data.length)
.filter((i) => P[i])
.sort((i, j) => d3.descending(V[i], V[j]))
)
.join("g")
.attr(
"transform",
avoidOverlapping
? (i) => `translate(${P[i].x},${P[i].y})`
: (i) => `translate(${projection(P[i])})`
)
.attr("fill", fill)
.attr("fill-opacity", fillOpacity)
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity);
bubbles
.append("circle")
.attr("r", (i) => radius(V[i]))
.call(T ? (circle) => circle.append("title").text((i) => T[i]) : () => {});
if (showTitle) {
bubbles
.append("text")
.attr("stroke-width", 0)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("paint-order", "stroke")
.attr("fill", titleFill)
.attr("fill-opacity", 1)
.attr("stroke", titleStroke)
.attr("stroke-width", titleStrokeWidth)
.attr("stroke-opacity", titleStrokeOpacity)
.attr("font-size", titleFontSize)
.attr("font-weight", titleFontWeight)
.style("font-family", fontFamily)
.selectAll("tspan")
.data((i) => (T[i] == null ? [] : `${T[i]}`.split(/\n/g)))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i, D) => `${i * 1.5 - D.length / 4}em`)
.attr("fill-opacity", (d, i, D) => (i === D.length - 1 ? 0.85 : 1))
.text((d) => d);
}
return svg;
}