Public
Edited
Apr 25, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/mirrored-beeswarm
function BeeswarmChart(data, {
value = d => d, // convience alias for x
label, // convenience alias for xLabel
domain, // convenience alias for xDomain
x = value, // given d in data, returns the quantitative x value
title = null, // given d in data, returns the title
radius = 3, // (fixed) radius of the circles≠
padding = 1.5, // (fixed) padding between the circles
marginTop = 10, // top margin, in pixels
marginRight = 20, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
backgroundColor = null, // Tanya's addition - plot backgorund color
xLabelColor = null, // Tanya's addition - color of x axis title
xAxisColor = null, // Tanya's addition - color of x axis
xAxisTitleSize = 15,
xAxisFontSize = 12,
xTitleTopMargin = 10,
xTitleLeftMargin = null,
xAxisGridColor = null,
xAxisGridOpacity = 0.4,
strokeWidth= 2, // Tanya's addition
strokeColor = null, // Tanya's addition
image, // Tanya's addition - pass in image url
marginLeft = 20, // left margin, in pixels
width = 640, // outer width, in pixels
height, // outer height, in pixels
xLabel = label, // a label for the x-axis
xDomain = domain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight] // [left, right]
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Image = d3.map(data, image);
const T = title == null ? null : d3.map(data, title);
// Compute which data points are considered defined.
const I = d3.range(X.length).filter(i => !isNaN(X[i]));

// Compute default domains.
if (xDomain === undefined) xDomain = d3.extent(X);

// Construct scales and axes.
const xScale = d3.scaleLog(xDomain, xRange);
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);


// Compute the y-positions.
const Y = dodge(I.map(i => xScale(X[i])), radius * 2 + padding);

// Compute the default height;
if (height === undefined) height = (d3.max(Y, Math.abs) + radius + padding) * 2 + marginTop + marginBottom;

// Given an array of x-values and a separation radius, returns an array of y-values.
function dodge(X, radius) {
const Y = new Float64Array(X.length);
const radius2 = radius ** 2;
const epsilon = 1e-3;
let head = null, tail = null;
// Returns true if circle ⟨x,y⟩ intersects with any circle in the queue.
function intersects(x, y) {
let a = head;
while (a) {
const ai = a.index;
if (radius2 - epsilon > (X[ai] - x) ** 2 + (Y[ai] - y) ** 2) return true;
a = a.next;
}
return false;
}
// Place each circle sequentially.
for (const bi of d3.range(X.length).sort((i, j) => X[i] - X[j])) {
// Remove circles from the queue that can’t intersect the new circle b.
while (head && X[head.index] < X[bi] - radius2) head = head.next;
// Choose the minimum non-intersecting tangent.
if (intersects(X[bi], Y[bi] = 0)) {
let a = head;
Y[bi] = Infinity;
do {
const ai = a.index;
let y1 = Y[ai] + Math.sqrt(radius2 - (X[ai] - X[bi]) ** 2);
let y2 = Y[ai] - Math.sqrt(radius2 - (X[ai] - X[bi]) ** 2);
if (Math.abs(y1) < Math.abs(Y[bi]) && !intersects(X[bi], y1)) Y[bi] = y1;
if (Math.abs(y2) < Math.abs(Y[bi]) && !intersects(X[bi], y2)) Y[bi] = y2;
a = a.next;
} while (a);
}
// Add b to the queue.
const b = {index: bi, next: null};
if (head === null) head = tail = b;
else tail = tail.next = b;
}
return Y;
}


const svg = d3.create("svg")
// .attr("width", width)
// .attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic; background-color:" + backgroundColor);


const tooltip = d3
.select("body")
.append("div")
.attr("class", "svg-tooltip")
.style("position", "absolute")
.style("visibility", "hidden");

function xGrid (g){ g
.attr('class', 'grid-lines')
.selectAll('line')
.data(xScale.ticks())
.join('line')
.attr('x1', d => xScale(d))
.attr('x2', d => xScale(d))
.attr('y1', marginTop)
.attr('y2', height - marginBottom)
.style('stroke', xAxisGridColor)
.style('stroke-opacity', xAxisGridOpacity)
}


svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.attr("color", xAxisColor)
.style("font-size", xAxisFontSize)
.call(g => g.append("text")
.attr("x", width - xTitleLeftMargin)
.attr("y", marginBottom - xTitleTopMargin)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.style("color", xLabelColor)
.style("font-size", xAxisTitleSize)
.text(xLabel));

svg.append('g').call(xGrid)

// Define the image size
let imageWidth = radius*2;
let imageHeight = radius*2;

const patterns = svg.append("defs")
.selectAll("pattern")
.data(Image)
.enter()
.append("pattern")
.attr("id", (d, i) => "image-pattern-" + i)
.attr("x", 0)
.attr("y", 0)
.attr("width", imageWidth)
.attr("height", imageHeight)
.append("image")
.attr("x", 0)
.attr("y", 0)
.attr("width", imageWidth)
.attr("height", imageHeight)
.attr("xlink:href", d => d);





// Create the circle elements and set their fill patterns
const dot = svg.append("g")
.selectAll("circle")
.data(I)
.join("circle")
.attr("cx", i => xScale(X[i]))
.attr("cy", i => (marginTop + height - marginBottom) / 2 + Y[i])
.attr("r", radius)
.attr("fill", (d, i) => "url(#image-pattern-" + i + ")")
.attr("stroke-width",strokeWidth)
.style("stroke",strokeColor)
.datum(d => d)
.on("mouseover", function(d, i) {


tooltip
.style("visibility", "visible")
.text(`${subset[i].name}}`);
const newRadius = radius * 1.5;
const newImageWidth = newRadius * 2;
const newImageHeight = newRadius * 2;
const pattern = d3.select(`#image-pattern-${i}`);
pattern
.attr("width", newImageWidth)
.attr("height", newImageHeight)
.attr("x", 0)
.attr("y",0);
pattern.select("image")
.attr("width", newImageWidth)
.attr("height", newImageHeight)
.attr("x",0)
.attr("y",0);
d3.select(this)
.raise() // bring to front
.transition()
.attr('stroke-width', 3)
.attr('r', newRadius)
.style('stroke', "#32E875");
})
.on("mousemove", function(d, i) {
tooltip
.style("top", d3.event.pageY - 100 + "px")
.style("left", d3.event.pageX - 100 + "px");
})
.on("mouseout", function(d, i) {
d3.select(this)
.lower() // return to original position
.transition()
.attr('stroke-width', strokeWidth)
.attr('r', radius)
.style('stroke', "white");

tooltip.style("visibility", "hidden");

const pattern = d3.select(`#image-pattern-${i}`);
pattern
.attr("width", imageWidth)
.attr("height", imageHeight)
.attr("x", 0)
.attr("y",0);
pattern.select("image")
.attr("width", imageWidth)
.attr("height", imageHeight)
.attr("x",0)
.attr("y",0);
});

if (T) dot.append("title")
.text(i => T[i])
.style('color','green');

return svg.node();
}
Insert cell
Insert cell
spotify-artists.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
spotifyArtists
Type SQL, then Shift-Enter. Ctrl-space for more options.

Insert cell
spotifyArtists
SELECT name, popularity, trim(image_url) as image,
'https://i.scdn.co/image/ab6761610000e5eb5a00969a4698c3132a15fbb0' as test
FROM spotifyArtists
ORDER BY popularity desc
LIMIT ${numberArtists}
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