function bubbleMap({
data = data,
geography = geography,
tickSize = 6,
width = 960,
height = 600,
marginTop = 10,
marginRight = 10,
marginBottom = 10,
marginLeft = 60,
bubbleColor = 'DarkOrchid',
bubbleVariable = 'value',
bubbleDomain = [0, d3.max(data, d => Math.abs(d[bubbleVariable]))],
bubbleRange = [0, 17],
fillOpacity = 0.5,
strokeOpacity = 1,
geographyIdVariable = 'id',
dataIdVariable = 'id',
legendLabelFormat = ',',
legendTitle = null,
legendSteps = 5,
} = {}) {
const dataById = d3.map(data, d => d[dataIdVariable]);
const radiusScale = d3.scaleSqrt()
.domain(bubbleDomain)
.range(bubbleRange);
const shapePadding = 2 * bubbleRange[1] + 2;
const legend = legendGenerator.legendSize()
.scale(radiusScale)
.shape('circle')
.shapePadding(shapePadding)
.labelOffset(shapePadding / 2 + 5)
.ascending(true)
.labelFormat(legendLabelFormat)
.cells(legendSteps)
.title(legendTitle)
.cellFilter(d => d.data !== 0);
const extent = [
[marginLeft, marginTop],
[width - marginRight - marginLeft, height - marginBottom - marginTop]
];
const projection = d3.geoMercator()
.fitExtent(extent, geography);
const path = d3.geoPath()
.projection(projection);
const topo = topojson.topology({ geography: geography });
const border = topojson.merge(topo, topo.objects.geography.geometries);
const svg = d3.create('svg')
.attr('viewBox', [0, 0, 975, 610]);
svg.append('g')
.attr('fill', 'none')
.attr('stroke', '#ccc')
.selectAll('path')
.data(geography.features)
.join('path')
.attr('d', path);
svg.append('path')
.datum(border)
.attr('fill', 'none')
.attr('stroke', '#434343')
.attr('stroke-width', 1)
.attr('d', path);
svg.append('g')
.selectAll('circle')
.data(
geography.features
.map((d) => {
const id = d.properties[geographyIdVariable];
if (dataById.has(id)) d.data = dataById.get(id);
else d.data = false;
return d;
})
.filter(d => d.data !== false)
)
.join('circle')
.attr('transform', (d) => {
const [tx, ty] = path.centroid(d);
return `translate(${tx}, ${ty})`;
})
.attr('r', d => radiusScale(d.data[bubbleVariable]))
.attr('fill', bubbleColor)
.attr('stroke', bubbleColor)
.attr('fill-opacity', fillOpacity)
.attr('stroke-opacity', strokeOpacity);
const legendWidth = 400;
const legendHeight = 100;
const legendColor = typeof(bubbleColor) === 'string' ? bubbleColor : '#ccc';
const legendNode = svg.append('g')
.attr('transform', `translate(${shapePadding}, ${legendHeight / 2})`)
.call(legend);
legendNode.selectAll('circle')
.attr('fill', legendColor)
.attr('stroke', legendColor)
.attr('fill-opacity', 0.5)
.attr('stroke-opacity', 1);
legendNode.select('.legendCells')
.attr('transform', `translate(${shapePadding / 2}, ${shapePadding / 2 + 10})`)
.selectAll('text')
.attr('font-size', 12)
.attr('font-family', 'sans-serif');
legendNode.select('.legendTitle')
.attr('font-weight', 'bold')
.attr('font-size', 14)
.attr('font-family', 'sans-serif');
return svg.node();
}