Public
Edited
May 2, 2023
3 stars
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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more