Public
Edited
May 2, 2023
3 stars
Also listed in…
misc
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viz = d3.select(svg)
Insert cell
curve = viz.select("#curve1") // find it in the svg by id
Insert cell
dotsLayer = viz.select("#layer1") // a layer to draw the dots into. Just for organization.
Insert cell
Insert cell
beeswarm = dvBeeswarmForce(dotsLayer,data,1000,300,"val2",options)
Insert cell
options = ({
//classField: "categoricalfield", // to set their class/color
strength: -0.2,
alphaTarget: 0.09,
direction: "path",
axisField: "val",
axisScaleFunction: distanceAlongPath,
dotScaleFactor: 1
})
Insert cell
Insert cell
distanceAlongPath = (value) => curve.node().getPointAtLength(value) // a simple function for distance along the path
Insert cell
Insert cell
Insert cell
Insert cell
function dvBeeswarmForce(elem,data,width,height,sizeField,options) {
nodes.length = 0; // clear any existing node data (not node, the svg elements.) // do I want this??
// options: fociField, strength, kFactor, collisionFactor, alphaTarget, alphaMin, maxRadius, dotScaleFactor, colorSet
if (typeof(options) === "undefined") {var options = {}}
if (typeof(options.strength) === "undefined") {options.strength = -1}
if (typeof(options.kFactor) === "undefined") {options.kFactor = 0.1}
if (typeof(options.collisionFactor) === "undefined") {options.collisionFactor = 1.1}
if (typeof(options.alphaTarget) === "undefined") {options.alphaTarget = 0.1}
if (typeof(options.alphaMin) === "undefined") {options.alphaMin = 0.1}
if (options.alphaTarget > options.alphaMin) {console.log("Warning: alphaTarget > alphaMin. Simulation will run forever.")}
if (typeof(options.maxRadius) === "undefined" || options.maxRadius == '') {options.maxRadius = 20}
if (typeof(options.dotScaleFactor) === "undefined" || options.dotScaleFactor == '') {options.dotScaleFactor = 0.005}

// options.axisField
// options.axisScaleFunction
// create axisScaleFunction if it is not passed in through options.
if (typeof(options.axisScaleFunction) === "undefined") {
let scaleField;
if (typeof(options.axisField) === "undefined") {
options.axisScaleFunction = d3.scaleLinear().domain([0,1000]).range([0,width]);
} else {
if (typeof(options.axisField) === "string") {scaleField = options.axisField}
options.axisScaleFunction = d3.scaleLinear().domain(d3.extent(data, d => d[scaleField])).range([0,width]);
}
}

// set positions for swarm lines. could be either horizontal or vertical. default is horizontal at middle.
if (typeof(options.direction) === "undefined") {options.direction = "horizontal"}
if (typeof(options.fociPositions) === "undefined") {
if (options.direction == "horizontal" || options.direction == "h") {options.fociPositions = {default: height/2}}
else if (options.direction == "vertical" || options.direction == "v") {options.fociPositions = {default: width/2}}
}
mutable foci = options.fociPositions; // default or passed in
// Setup the Force Simulation (the algorithm that does this)
force
.force('nbody', d3.forceManyBody().strength(options.strength))
.force('foci', alpha => {
for (var i = 0, n = nodes.length, o, k = alpha * options.kFactor; i < n; ++i) {
o = nodes[i];
if (typeof(options.axisField) !== "undefined" && options.axisField != "") {
if (options.direction == "horizontal" || options.direction == "h") {
o.vx += (o.axisPos - o.x) * k;
o.vy += (mutable foci[o.id] - o.y) * k;
} else if (options.direction == "vertical" || options.direction == "v") {
o.vx += (mutable foci[o.id] - o.x) * k;
o.vy += (o.axisPos - o.y) * k;
} else if (options.direction == "path") {
o.vx += (o.axisPos.x - o.x) * k;
o.vy += (o.axisPos.y - o.y) * k;
}
} else {
o.vx += (width/2 - o.x) * k;
o.vy += (height/2 - o.y) * k;
}
}
})
.force('collision', d3.forceCollide(function (d) {return options.collisionFactor*d.r;}))
.alphaTarget(options.alphaTarget)
.alphaMin(options.alphaMin)
.on("tick", tick);

// the tick is what updates their position.
function tick(e) {
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// this creates the dots
function addDots(dot) {
let fociID;
let axisPosition;
var radius = rFromArea(dot[sizeField]) * options.dotScaleFactor;
if (isFinite(sizeField)) {radius = sizeField};
if (radius < 0 || isNaN(radius)) {radius = 1;}
if (typeof(options.fociField) !== "undefined") {fociID = dot[options.fociField]} else {fociID = "default";}
if (typeof(options.axisField) !== "undefined") {axisPosition = options.axisScaleFunction(toNum(dot[options.axisField]))} else {axisPosition = options.axisScaleFunction(0);}
nodes.push({
id: fociID,
axisPos: axisPosition,
x: 0, // could also set starting position to center or other input?
y: 0,
r: radius,
data: dot
});
}

function drawNodes() {
node = svgElem.selectAll("circle").data(nodes);
node = node.join("circle")
.attr("class", function(d) { return "node" + options.classField ? d.data[options.classField] : ""})
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) {return d.r})
.style("fill", function(d) {return options.colorSet ? options.colorSet[d.id] : ""});
}
// draw it.
let svgElem = elem;
let node = svgElem.selectAll(".node");
data.forEach(function(d){addDots(d)}); // adds each time we run. would rather join.
force.nodes(nodes);
drawNodes();
return node;
}
Insert cell
Insert cell
mutable foci = []
Insert cell
nodes = []
Insert cell
force = d3.forceSimulation(nodes)
Insert cell
import {rFromArea,toNum} from "@emfielduva/dvlib"
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