Published
Edited
Jun 14, 2019
Insert cell
md`# d3 bubbles`
Insert cell
radiusExtent = [5, 40]
Insert cell
sizeExtent = [170, 180000]
Insert cell
height = 500
Insert cell
randomTopics = Array.from({length: 100}, createRandomTopic)
Insert cell
chart.viewport.node()
Insert cell
transform = createTransformStream()
Insert cell
selectedBubble = createSelectedBubbleStream(randomBubbles, transform)
Insert cell
randomBubbles = transformToBubbles(randomTopics)

Insert cell
chart = createChart();
Insert cell
drawBubbles(randomBubbles, {selectedBubble, transform})
Insert cell
drawBubbles = function(bubbles, {selectedBubble, transform}) {
const leaf = chart.contentOutlet.selectAll("g")
.data(bubbles.slice().sort((b1, b2) => b2.r - b1.r))
.join("g")
.attr("transform", bubbleTransform);

leaf.selectAll('*').remove();
leaf.append("circle")
.attr("id", d => (d.leafUid = DOM.uid("leaf")).id)
.attr("r", d => d.r * transform.k)
.attr("fill-opacity", 0.7)
.attr("fill", d => d === selectedBubble ? 'yellow' : 'red');
leaf.append("text")
.attr("text-anchor", "middle")
.text((d) => shortFormat(d.topic.size));
function bubbleTransform({x, y}) {
([x, y] = transform.apply([x, y]));
return `translate(${x},${y})`
}
}
Insert cell
drawScale(transform)
Insert cell
drawScale = function(transform) {
const scaleOutlet = chart.scaleOutlet

scaleOutlet.selectAll("g").remove();

drawScaleTick(45);
drawScaleTick(30);
drawScaleTick(15);

function drawScaleTick(radius) {
const tick = scaleOutlet
.append('g')
.attr("transform", `translate(0, ${-radius})`)
.attr("font-size", 12)
tick.append('circle')
.attr("r", radius)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0.9)
.attr("stroke", 'black');
const value = radiusConverter.toValue(radius / transform.k);
const labelText = shortFormat(value);
tick.append("text")
.attr("x", 0)
.attr("y", -radius - 5)
.attr("text-anchor", "middle")
.text(labelText);
}
}
Insert cell
shortFormat = function(value) {
const suffixes = ['K', 'M', 'B', 'T'];
const step = 1000;
let abbrev = Math.floor(value) + '';
for(let suffix of suffixes) {
value /= step;
if(value < 1) {
break;
}
abbrev = Math.floor(value) + suffix
}
return abbrev;
}
Insert cell
createChart = function() {
const viewport = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
const contentOutlet = viewport.append('g');
const scaleOutlet = viewport.append('g')
.classed('scale', true)
.attr("transform", `translate(${width - 50},${height - 10})`);
const interactionPane = createInteractionPane(viewport)
const zoom = initZoom(interactionPane);
return {viewport, contentOutlet, scaleOutlet, interactionPane, zoom};
}
Insert cell
createInteractionPane = function(viewport) {
return viewport.append('rect')
.attr('width', width)
.attr('height', height)
.style('fill', 'none')
.style('pointer-events', 'all');
}
Insert cell
initZoom = function(interactionPane) {
const zoom = d3.zoom()
.extent([[0,0], [width,height]])
.scaleExtent([1, Number.POSITIVE_INFINITY])
.wheelDelta(() => d3.event.deltaY < 0 ? 1 : -1);
interactionPane.call(zoom);
return zoom;
}
Insert cell
createSelectedBubbleStream = function(bubbles, transform) {
return Generators.observe(next => {
next(undefined);

chart.interactionPane.on('click.bubbles', function() {
const coords = transform.invert(d3.mouse(this));
const closest = findClosestBubble(bubbles, coords);
next(closest);
})

return () => chart.interactionPane.on('click.bubbles', null);
})
}
Insert cell
findClosestBubble = function(bubbles, coords) {
let threshold = Number.POSITIVE_INFINITY;
return bubbles.reduce((closest, bubble) => {
const distance = getDistance([bubble.x, bubble.y], coords);
const isCloser = distance < Math.min(threshold, bubble.r)
if(isCloser) {
threshold = distance;
return bubble;
} else {
return closest;
}
}, undefined);
}
Insert cell
getDistance =function ([x1, y1], [x2, y2]) {
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}
Insert cell
createTransformStream = function() {
return Generators.observe(next => {
next(d3.zoomIdentity);

chart.zoom.on('zoom.bubbles', function() {
next(d3.event.transform)
})

return () => chart.zoom.on('zoom.bubbles', null);
})
}
Insert cell
transformToBubbles = function(topics) {
return topics.map(topic => {
const [x, y] = coordinatesConverter.toViewport([topic.x, topic.y]);
const r = radiusConverter.fromValue(topic.size);
return { x, y, r, topic }
});
}
Insert cell
coordinatesConverter = {
const viewportBox = createBox([0, width], [0, height]).padding(-radiusExtent[1]);
const xExtent = d3.extent(randomTopics, b => b.x)
const yExtent = d3.extent(randomTopics, b => b.y)
const originalBox = createBox(xExtent, yExtent);
return createCoordinatesConverter(viewportBox, originalBox);
}
Insert cell
createCoordinatesConverter = function(viewportBox, originalBox) {
const transform = getTransform();

return {
toViewport(point) {
return transform.apply(point)
},
toOriginal(point) {
return transform.invert(point)
}
}

function getTransform() {
const scaleTransform = getScaleTransform();
return getTranslatedTransform(scaleTransform)
}

function getScaleTransform() {
const wK = viewportBox.width / originalBox.width;
const hK = viewportBox.height / originalBox.height;
const k = Math.min(wK, hK);
return d3.zoomIdentity.scale(k);
}

function getTranslatedTransform(scaleTransform) {
const originalCenter = originalBox.center;
const viewportCenter = viewportBox.center;
const transformedCenter = scaleTransform.invert(viewportCenter);
const tx = transformedCenter[0] - originalCenter[0];
const ty = transformedCenter[1] - originalCenter[1];
return scaleTransform.translate(tx, ty);
}
}
Insert cell
createBox = function createBox([minX, maxX], [minY, maxY]) {
return {
get center() {
return [
d3.mean([minX, maxX]),
d3.mean([minY, maxY])
];
},
get width() {
return maxX - minX;
},
get height() {
return maxY - minY;
},
padding(value) {
return createBox([minX - value, maxX + value], [minY - value, maxY + value])
}
}
}
Insert cell
radiusConverter = {
const [minValue, maxValue] = d3.extent(randomTopics, s => s.size);
return createRadiusConverter([radiusExtent, [minValue, maxValue]]);
}
Insert cell
createRadiusConverter = function([[minRadius, maxRadius], [minValue, maxValue]]) {
const a = (maxRadius - minRadius) * (maxRadius + minRadius) / (maxValue - minValue);
const b = maxRadius * maxRadius - a * maxValue;
return {
fromValue,
toValue
}
function fromValue(value) {
const rSq = a * value + b;
return Math.sqrt(rSq);
}
function toValue(radius) {
const rSq = radius * radius;
return (rSq - b) / a;
}
}
Insert cell
createRandomTopic = function() {
const [minSize, maxSize] = sizeExtent;
const sizeDif = maxSize - minSize;
const size = minSize + Math.floor(Math.random() * sizeDif);
return {
x: Math.floor(Math.random() * 1000),
y: Math.floor(Math.random() * 1000),
size
}
}
Insert cell
d3 = require("d3")
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