I'm using d3 to animate a route (path) on a map. When the route reaches a point along the route I'd like to popup some information.
Most of my code is based on the following example. http://bl.ocks.org/mbostock/1705868. I'm really just trying to determine if there is a way to detect when the transitioning circle collides or overlaps any of the stationary circles in this example.
You can detect collision in your tween function. Define a collide function to be called from inside the tween function as follows:
function collide(node){
var trans = d3.transform(d3.select(node).attr("transform")).translate,
x1 = trans[0],
x2 = trans[0] + (+d3.select(node).attr("r")),
y1 = trans[1],
y2 = trans[1] + (+d3.select(node).attr("r"));
var colliding = false;
points.each(function(d,i){
var ntrans = d3.transform(d3.select(this).attr("transform")).translate,
nx1 = ntrans[0],
nx2 = ntrans[0] + (+d3.select(this).attr("r")),
ny1 = ntrans[1],
ny2 = ntrans[1] + (+d3.select(this).attr("r"));
if(!(x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1))
colliding=true;
})
return colliding;
}
Where points are the stationary points, and node is the transitioning element. What collide does is check whether node overlaps with any of the points (as shown in collision detection example here).
Because we need the node to be passed to the tween function, we replace attrTween used in Mike's example, with tween:
circle.transition()
.duration(10000)
.tween("attr", translateAlong(path.node()))
.each("end", transition);
Finally, the tween function calling our collide:
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
d3.select(this).attr("transform","translate(" + p.x + "," + p.y + ")");
if(collide(this))
d3.select(this).style("fill", "red")
else
d3.select(this).style("fill", "steelblue")
};
};
}
See the full demo here
The easiest way is to just check how "close" the transitioning circle is to the other points.
var pop = d3.select("body").append("div")
.style("position","absolute")
.style("top",0)
.style("left",0)
.style("display", "none")
.style("background", "yellow")
.style("border", "1px solid black");
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
var epsilon = 5;
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
points.forEach(function(d,i){
if ((Math.abs(d[0] - p.x) < epsilon) &&
(Math.abs(d[1] - p.y) < epsilon)){
pop.style("left",d[0]+"px")
.style("top",d[1]+20+"px")
.style("display","block")
.html(d);
return false;
}
})
return "translate(" + p.x + "," + p.y + ")";
};
};
}
The faster the circle moves the greater your epsilon will need to be.
Example here.
Related
I have two elements I need to render and a context of the big picture I am trying to achieve (a complete dashboard).
One is a chart that renders fine.
$scope.riskChart = new dc.pieChart('#risk-chart');
$scope.riskChart
.width(width)
.height(height)
.radius(Math.round(height/2.0))
.innerRadius(Math.round(height/4.0))
.dimension($scope.quarter)
.group($scope.quarterGroup)
.transitionDuration(250);
The other is a triangle, to be used for a more complex shape
$scope.openChart = d3.select("#risk-chart svg g")
.enter()
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);
On invocation of render functions, the dc.js render function is recognized and the chart is seen, but the d3.js render() function is not recognized.
How do I add this shape to my dc.js canvas (an svg element).
$scope.riskChart.render(); <--------------Works!
$scope.openChart.render(); <--------------Doesn't work (d3.js)!
How do I make this work?
EDIT:
I modified dc.js to include my custom chart, it is a work in progress.
dc.starChart = function(parent, fill) {
var _chart = {};
var _count = null, _category = null;
var _width, _height;
var _root = null, _svg = null, _g = null;
var _region;
var _minHeight = 20;
var _dispatch = d3.dispatch('jump');
_chart.count = function(count) {
if(!arguments.length)
return _count;
_count = count;
return _chart;
};
_chart.category = function(category) {
if(!arguments.length)
return _category
_category = category;
return _chart;
};
function count() {
return _count;
}
function category() {
return _category;
}
function y(height) {
return isNaN(height) ? 3 : _y(0) - _y(height);
}
_chart.redraw = function(fill) {
var color = fill;
var triangle = d3.symbol('triangle-up');
this._g.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", triangle)
.attr("transform", function(d) { return "translate(" + 25 + "," + 25 + ")"; })
.style("fill", fill);
return _chart;
};
_chart.render = function() {
_g = _svg
.append('g');
_svg.on('click', function() {
if(_x)
_dispatch.jump(_x.invert(d3.mouse(this)[0]));
});
if (_root.select('svg'))
_chart.redraw();
else{
resetSvg();
generateSvg();
}
return _chart;
};
_chart.on = function(event, callback) {
_dispatch.on(event, callback);
return _chart;
};
_chart.width = function(w) {
if(!arguments.length)
return this._width;
this._width = w;
return _chart;
};
_chart.height = function(h) {
if(!arguments.length)
return this._height;
this._height = h;
return _chart;
};
_chart.select = function(s) {
return this._root.select(s);
};
_chart.selectAll = function(s) {
return this._root.selectAll(s);
};
function resetSvg() {
if (_root.select('svg'))
_chart.select('svg').remove();
generateSvg();
}
function generateSvg() {
this._svg = _root.append('svg')
.attr({width: _chart.width(),
height: _chart.height()});
}
_root = d3.select(parent);
return _chart;
}
I think I confused matters by talking about how to create a new chart, when really you just want to add a symbol to an existing chart.
In order to add things to an existing chart, the easiest thing to do is put an event handler on its pretransition or renderlet event. The pretransition event fires immediately once a chart is rendered or redrawn; the renderlet event fires after its animated transitions are complete.
Adapting your code to D3v4/5 and sticking it in a pretransition handler might look like this:
yearRingChart.on('pretransition', chart => {
let tri = chart.select('svg g') // 1
.selectAll('path.triangle') // 2
.data([0]); // 1
tri = tri.enter()
.append('path')
.attr('class', 'triangle')
.merge(tri);
tri
.attr("d", d3.symbol().type(d3.symbolTriangle).size(200))
.style("fill", 'darkgreen'); // 5
})
Some notes:
Use chart.select to select items within the chart. It's no different from using D3 directly, but it's a little safer. We select the containing <g> here, which is where we want to add the triangle.
Whether or not the triangle is already there, select it.
.data([0]) is a trick to add an element once, only if it doesn't exist - any array of size 1 will do
If there is no triangle, append one and merge it into the selection. Now tri will contain exactly one old or new triangle.
Define any attributes on the triangle, here using d3.symbol to define a triangle of area 200.
Example fiddle.
Because the triangle is not bound to any data array, .enter() should not be called.
Try this way:
$scope.openChart = d3.select("#risk-chart svg g")
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);
I'm trying to do a proof of concept of a SVG floorplan that pans and zooms and also have the ability to place markers on top. When zooming/panning happens the marker doesn't stay in position. I understand why this happens but not sure about the best way to keep the marker in position when panning/zooming.
Heres the code:
var svg = d3.select(".floorplan")
.attr("width", "100%")
.attr("height", "100%")
.call(d3.zoom().on("zoom", zoomed))
.select("g")
var marker = d3.selectAll(".marker")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
)
function zoomed() {
svg.attr("transform", d3.event.transform);
}
function dragstarted(d) {
console.log('dragstarted');
}
function dragged(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
}
function dragended(d) {
console.log('drag ended: marker:'+ d3.select(this).attr('data-id') + ' position: ' + d3.event.x +', ' + d3.event.y);
}
Theres also a codepen to visually see here: https://codepen.io/danielhoff/pen/WzQbRr
I have an additional constraints that the marker element shouldn't be contained inside the floorplan svg.
Here is a modified version of your codepen which fixes the movements of the marker during drag events while keeping the marker outside the floorplan svg container:
https://codepen.io/xavierguihot/pen/OvyRPY?editors=0010
To bring back into context, an easy solution would have been to include the marker element inside the floorplan container (in order for the marker to get the same zoom events as the floorplan), but here we want the marker to be in its own svg container.
And it is not trivial!
Appart from including ids in html tags (in order to select these elements from the html), only the javascript part has ben modified.
Let's dig a little bit on the steps necessary to get to this point:
First: Let's modify the zoomed function to apply to the marker as well:
Initially this was the zoom function:
function zoomed() {
svg.attr("transform", d3.event.transform);
}
And the modified version:
function zoomed() {
// Before zooming the floor, let's find the previous scale of the floor:
var curFloor = document.getElementById('floorplan');
var curFloorScale = 1;
if (curFloor.getAttribute("transform")) {
var curFloorTransf = getTransformation(curFloor.getAttribute("transform"));
curFloorScale = curFloorTransf.scaleX;
}
// Let's apply the zoom
svg.attr("transform", d3.event.transform);
// And let's now find the new scale of the floor:
var newFloorTransf = getTransformation(curFloor.getAttribute("transform"));
var newFloorScale = newFloorTransf.scaleX;
// This way we get the diff of scale applied to the floor, which we'll apply to the marker:
var dscale = newFloorScale - curFloorScale;
// Then let's find the current x, y coordinates of the marker:
var marker = document.getElementById('Layer_1');
var currentTransf = getTransformation(marker.getAttribute("transform"));
var currentx = currentTransf.translateX;
var currenty = currentTransf.translateY;
// And the position of the mouse:
var center = d3.mouse(marker);
// In order to find out the distance between the mouse and the marker:
// (43 is based on the size of the marker)
var dx = currentx - center[0] + 43;
var dy = currenty - center[1];
// Which allows us to find out the exact place of the new x, y coordinates of the marker after the zoom:
// 38.5 and 39.8 comes from the ratio between the size of the floor container and the marker container.
// "/2" comes (I think) from the fact that the floor container is initially translated at the center of the screen:
var newx = currentx + dx * dscale / (38.5/2);
var newy = currenty + dy * dscale / (39.8/2);
// And we can finally apply the translation/scale of the marker!:
d3.selectAll(".marker").attr("transform", "translate(" + newx + "," + newy + ") scale(" + d3.event.transform.k + ")");
}
This heavily uses the getTransformation function which allows to retrieve the current transform details of an element.
Then: But now, after having zoomed, when we drag the marker, it takes back its original size:
This means we have to tweak the marker's dragg function to keep its current scale when applying the drag transform:
Here was the initial drag function:
function dragged(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
}
And its modified version:
function draggedMarker(d) {
var x = d3.event.x;
var y = d3.event.y;
// As we want to keep the same current scale of the marker during the transform, let's find out the current scale of the marker:
var marker = document.getElementById('Layer_1');
var curScale = 1;
if (marker.getAttribute("transform")) {
curScale = getTransformation(marker.getAttribute("transform")).scaleX;
}
// We can thus apply the translate And keep the current scale:
d3.select(this).attr("transform", "translate(" + x + "," + y + "), scale(" + curScale + ")");
}
Finally: When dragging the floor we also have to drag the marker accordingly:
We thus have to override the default dragging of the floor in order to include the same dragg event to the marker.
Here is the drag function applied to the floor:
function draggedFloor(d) {
// Overriding the floor drag to do the exact same thing as the default drag behaviour^^:
var dx = d3.event.dx;
var dy = d3.event.dy;
var curFloor = document.getElementById('svg-floor');
var curScale = 1;
var curx = 0;
var cury = 0;
if (curFloor.getAttribute("transform")) {
curScale = getTransformation(curFloor.getAttribute("transform")).scaleX;
curx = getTransformation(curFloor.getAttribute("transform")).translateX;
cury = getTransformation(curFloor.getAttribute("transform")).translateY;
}
d3.select(this).attr("transform", "translate(" + (curx + dx) + "," + (cury + dy) + ")");
// We had to override the floor drag in order to include in the same method the drag of the marker:
var marker = document.getElementById('Layer_1');
var currentTransf = getTransformation(marker.getAttribute("transform"));
var currentx = currentTransf.translateX;
var currenty = currentTransf.translateY;
var currentScale = currentTransf.scaleX;
d3.selectAll(".marker").attr("transform", "translate(" + (currentx + dx) + "," + (currenty + dy) + ") scale(" + currentScale + ")");
}
I want to remove the axis from the Parallel coordinate on dragend. Here is my code snippnet.
It works partially as dimensions get removed but the lines corresponding to the dimensions still remain there.
I need to update the xscale.domain. But somehow it doesn't work
if (dragging[d] < 12 || dragging[d] > w() - 12)
{
pc.remove_axis(d, g);
}
pc.remove_axis = function (d, g) {
g_data = pc.getOrderedDimensionKeys();
g_data = _.difference(g_data, [d]);
xscale.domain(g_data);
g.attr("transform", function (p) {
return "translate(" + position(p) + ")";
});
g.filter(function (p) {
return p === d;
}).remove();}
I am working on horizontal segment bar chart. I want to make it so that the bar chart will animate the colour transition between individual segments depending on the value that is generated randomly every few seconds.
At the beginning I set two variables midRange and highRange that split my segments into 3 groups - green, yellow and red. Then I create an 2 arrays. rectArrays holds my segments/rectangles. colorArray holds the colour for each rectangle.
In animate() function I am using these arrays for transition purposes. At the moment first 25 segments should be animated as green then few yellow and the remaining segments should be red. The transition between colours does not work when more than 25 segments should be turned on. They are all either yellow or red. It seems like the transition is remembering only the colour that is stored on the last index before exiting the for loop in my animate function. There are 3 cases so the animation can go from left to right and vice versa.
This picture shows the undesired effect.First half of the segments should be green and remein 5 yellow. But for some reason they are all yellow.
Here is my fiddle code
Thank you for any suggestions
var configObject = {
svgWidth : 1000,
svgHeight : 500,
minValue : 1,
maxValue : 100,
midRange : 50,
highRange : 75,
numberOfSegments : 50
};
//define variables
var newValue;
var gaugeValue = configObject.minValue - 1;
var mySegmentMappingScale;
var reverseScale;
var rectArray=[];
var segmentIndex=configObject.maxValue/configObject.numberOfSegments;
var dynamicArc=true;
var staticArc="yellow";
var gradientArray=[];
var colorArray=[];
var rectWidth=(configObject.svgWidth/1.5)/configObject.numberOfSegments;
var rectPadding=3;
getColor();
setmySegmentMappingScale();
//define svg
var svg = d3.select("body").append("svg")
.attr("width", configObject.svgWidth)
.attr("height", configObject.svgHeight)
.append("g")
.attr("transform", 'translate('+ 0 +',' + configObject.svgHeight/2 + ')');
var valueLabel= svg.append("text")
.attr('x',0)
.attr('y', (configObject.svgHeight/13)+15)
.attr('transform',"translate(" + 0 + "," + 0 + ")")
.text(configObject.minValue)
.attr('fill', "white");
var HueGreenIndex=1;
var HueYellowIndex=1;
var HueRedIndex=1;
function addGradient(c){
//debugger
if (c=="green"){
var hslString =d3.hsl(HueGreenIndex + 160, .40, .29).toString();
HueGreenIndex=HueGreenIndex+0.5;
return hslString;
}
else if(c=="yellow"){
var hslString=d3.hsl(HueYellowIndex + 39, .67, .57).toString();
HueYellowIndex=HueYellowIndex+0.5;
return hslString;
}
else if (c=="red"){
var hslString=d3.hsl(1+HueRedIndex , 1, .58).toString();
HueRedIndex=HueRedIndex+0.10;
return hslString;
}
}
function getColor(){
if (dynamicArc){
for(i = 0; i <= configObject.numberOfSegments; i++){
if(i<=(configObject.numberOfSegments/100)*configObject.midRange){
//gradientArray.push(addGradient("green"));
colorArray.push("green");
}
else if(i > ((configObject.numberOfSegments/100)*configObject.midRange) && i<= ((configObject.numberOfSegments/100)*configObject.highRange)){
//gradientArray.push(addGradient("yellow"));
colorArray.push("yellow");
}
else if (i > ((configObject.numberOfSegments/100)*configObject.highRange)){
//gradientArray.push(addGradient("red"));
colorArray.push("red");
}
}
}
else{
if (staticArc=="green"){
//gradientArray.push(addGradient("green"));
colorArray.push("green")
}
else if(staticArc=="yellow"){
//gradientArray.push(addGradient("yellow"));
colorArray.push("yellow")
}
else {
//gradientArray.push(addGradient("red"));
colorArray.push("red")
}
}
}
for(i = 0; i <= configObject.numberOfSegments; i++){
var myRect=svg.append("rect")
.attr("fill", "#2D2D2D")
.attr("x",i * rectWidth)
.attr("y", 0)
.attr("id","rect"+i)
.attr("width", rectWidth-rectPadding)
.attr("height", configObject.svgHeight/13);
rectArray.push(myRect);
}
//define scale
function setmySegmentMappingScale(){
var domainArray = [];
var x=0;
for(i = configObject.minValue; i <= configObject.maxValue+1; i = i + (configObject.maxValue - configObject.minValue)/configObject.numberOfSegments){
if(Math.floor(i) != domainArray[x-1]){
var temp=Math.floor(i);
domainArray.push(Math.floor(i));
x++;
}
}
var rangeArray = [];
for(i = 0; i <= configObject.numberOfSegments+1; i++){// <=
rangeArray.push(i);
}
mySegmentMappingScale = d3.scale.threshold().domain(domainArray).range(rangeArray);
reverseScale= d3.scale.threshold().domain(rangeArray).range(domainArray);
}
function widgetScale (x,y,r){
return (x*r)/y;
}
//generate random number
function generate(){
var randomNumber = Math.random() * (configObject.maxValue - configObject.minValue) + configObject.minValue;
newValue = Math.floor(randomNumber);
animateSVG();
}
function animateSVG(){
var previousSegment = mySegmentMappingScale(gaugeValue) -1;
var newSegment = mySegmentMappingScale(newValue) -1;
if(previousSegment <= -1 && newSegment > -1){
for(i = 0; i <= newSegment; i++){
var temp=colorArray[i];
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolateRgb( getComputedStyle(this).getPropertyValue("fill"), temp );});
valueLabel.transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.text(i==newSegment ? newValue : i*segmentIndex);
valueLabel.transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * (rectWidth)+(rectWidth)) + "," + 0 + ")")
}
}
else if(newSegment > previousSegment){
for(i = previousSegment; i <= newSegment; i++){
var temp=colorArray[i];
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.styleTween("fill", function() { return d3.interpolateRgb( getComputedStyle(this).getPropertyValue("fill"),temp);});
valueLabel.transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.text(i==newSegment ? newValue : i*segmentIndex);
valueLabel.transition()
.ease("linear")
.duration(50)
.delay(function(d){return i * 90})
.attr("transform","translate(" + (i * (rectWidth)+(rectWidth)) + "," + 0 + ")")
}
}
else if(newSegment < previousSegment){
for(i = previousSegment; i > newSegment; i--){
var temp=colorArray[i];
rectArray[i].transition()
.ease("linear")
.duration(50)
.delay(function(d){return Math.abs(i -previousSegment)*90})
.styleTween("fill", function() { return d3.interpolateRgb( getComputedStyle(this).getPropertyValue("fill"),"#2D2D2D"); });
valueLabel.transition()
.ease("linear")
.duration(50)
.delay(function(d){return Math.abs(i -previousSegment)*90})
.text(i==newSegment+1 ? newValue : i*segmentIndex);
valueLabel.transition()
.ease("linear")
.duration(50)
.delay(function(d){return Math.abs(i -previousSegment)*90})
.attr("transform","translate(" + (i * (rectWidth)-(rectWidth)) + "," + 0 + ")")
}
}
gaugeValue = newValue;
}
setInterval(function() {
generate()
}, 6000);
If you want that every styleTween gets a different i instance, you'll have to use let, not var.
Just change:
var temp = colorArray[i];
To:
let temp = colorArray[i];
Here is the updated fiddle: https://jsfiddle.net/x2mL97x7/
Trying to figure out the best way of bin packing/ distributing a bunch of known width svg images in a horizontal row, where they can be stacked on top of each other if the width of the container is too tight.
Height of container should optimally be self-adjusted, and the gravity should be the vertical center point. Created a few images to illustrate the desired solution.
Are there any JS library out there that solves this problem, d3 perhaps? It feels like a bin packing problem, but perhaps with some added complexity for order and gravity. Not interested in canvas solutions.
If container is wide enough
Too tight, stack some elements
Even tighter, stack all
For a pure D3-based SVG solution, my proposal here is using a force simulation with collision detection. The collision detection in the D3 force simulation (d3.forceCollide) is a circular one, that is, it uses the elements' radii as arguments. So, since you have square/rectangular elements, I'm using this rectangular collision detection I found.
The idea is setting the x and y positions using the simulation based on your data and the available width, with the collision based on the elements' size. Then, in the resize event, you run the simulation again with the new width.
Have in mind that, contrary to most D3 force directed charts you'll find, we don't want to show the entire simulation developing, but only the final positions. So, you'll set the simulation and stop it immediately:
const simulation = d3.forceSimulation(data)
//etc...
.stop();
Then, you do:
simulation.tick(n);
Or, in the resize handler, re-heating it:
simulation.alpha(1).tick(n);
Where n is the number of iterations you want. The more the better, but also the more the slower...
Here is a very crude example, move the blue handle on the right-hand side to squeeze the rectangles:
const svg = d3.select("svg");
let width = parseInt(svg.style("width"));
const data = d3.range(15).map(d => ({
id: d,
size: 5 + (~~(Math.random() * 30))
}));
const collisionForce = rectCollide()
.size(function(d) {
return [d.size * 1.2, d.size * 1.2]
})
const simulation = d3.forceSimulation(data)
.force("x", d3.forceX(d => (width / data.length) * d.id).strength(0.8))
.force("y", d3.forceY(d => 100 - d.size / 2).strength(0.1))
.force("collision", collisionForce.strength(1))
.stop();
simulation.tick(100);
const rect = svg.selectAll("rect")
.data(data, d => "id" + d.id);
rect.enter()
.append("rect")
.style("fill", d => d3.schemePaired[d.id % 12])
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("width", d => d.size)
.attr("height", d => d.size);
const drag = d3.drag()
.on("drag", function() {
const width = Math.max(d3.mouse(this.parentNode)[0], 70);
simulation.nodes(data)
.force("x", d3.forceX(d => (width / data.length) * d.id).strength(0.8))
.stop();
simulation.alpha(1).tick(100);
const rect = svg.selectAll("rect")
.data(data, d => "id" + d.id);
rect.attr("x", d => d.x)
.attr("y", d => d.y);
d3.select("#outer").style("width", width + "px");
});
d3.select("#inner").call(drag);
function rectCollide() {
var nodes, sizes, masses
var size = constant([0, 0])
var strength = 1
var iterations = 1
function force() {
var node, size, mass, xi, yi
var i = -1
while (++i < iterations) {
iterate()
}
function iterate() {
var j = -1
var tree = d3.quadtree(nodes, xCenter, yCenter).visitAfter(prepare)
while (++j < nodes.length) {
node = nodes[j]
size = sizes[j]
mass = masses[j]
xi = xCenter(node)
yi = yCenter(node)
tree.visit(apply)
}
}
function apply(quad, x0, y0, x1, y1) {
var data = quad.data
var xSize = (size[0] + quad.size[0]) / 2
var ySize = (size[1] + quad.size[1]) / 2
if (data) {
if (data.index <= node.index) {
return
}
var x = xi - xCenter(data)
var y = yi - yCenter(data)
var xd = Math.abs(x) - xSize
var yd = Math.abs(y) - ySize
if (xd < 0 && yd < 0) {
var l = Math.sqrt(x * x + y * y)
var m = masses[data.index] / (mass + masses[data.index])
if (Math.abs(xd) < Math.abs(yd)) {
node.vx -= (x *= xd / l * strength) * m
data.vx += x * (1 - m)
} else {
node.vy -= (y *= yd / l * strength) * m
data.vy += y * (1 - m)
}
}
}
return x0 > xi + xSize || y0 > yi + ySize ||
x1 < xi - xSize || y1 < yi - ySize
}
function prepare(quad) {
if (quad.data) {
quad.size = sizes[quad.data.index]
} else {
quad.size = [0, 0]
var i = -1
while (++i < 4) {
if (quad[i] && quad[i].size) {
quad.size[0] = Math.max(quad.size[0], quad[i].size[0])
quad.size[1] = Math.max(quad.size[1], quad[i].size[1])
}
}
}
}
}
function xCenter(d) {
return d.x + d.vx + sizes[d.index][0] / 2
}
function yCenter(d) {
return d.y + d.vy + sizes[d.index][1] / 2
}
force.initialize = function(_) {
sizes = (nodes = _).map(size)
masses = sizes.map(function(d) {
return d[0] * d[1]
})
}
force.size = function(_) {
return (arguments.length ?
(size = typeof _ === 'function' ? _ : constant(_), force) :
size)
}
force.strength = function(_) {
return (arguments.length ? (strength = +_, force) : strength)
}
force.iterations = function(_) {
return (arguments.length ? (iterations = +_, force) : iterations)
}
return force
};
function constant(_) {
return function() {
return _
}
};
svg {
width: 100%;
height: 100%;
}
#outer {
position: relative;
width: 95%;
height: 200px;
}
#inner {
position: absolute;
width: 10px;
top: 0;
bottom: 0;
right: -10px;
background: blue;
opacity: .5;
cursor: pointer;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="outer">
<svg></svg>
<div id="inner"></div>
</div>
As I said, this is a very crude code, just as an example. You can tweak the strengths and improve other parts, for fitting your needs.
What you try to achieve is a masonry layout. This is typically used when you don't know the height and number of elements. A real masonry layout will work even with variable width elements.
Here is a JSFIDDLE example (resize to see how the elements packs themselves).
A lot of codes will require some js AND still won't be real masonry, as each column will have the same width (here, or here). Though you can actually achieve this result in pure css (no js) by using a multi-column layout, alongside with media queries.
However, as this is only suitable when all elements have the same width, and it seems it's not your case, I would advise you to pick one from the list below:
https://isotope.metafizzy.co/layout-modes/masonry.html
Supports a lot of features:
Fit-width
Horizontal/vertical ordering
Gutter
Fully-responsive
Has a vertical (default) masonry mode and also horizontal mode
https://masonry.desandro.com/v3/
https://vestride.github.io/Shuffle/
https://codepen.io/jh3y/pen/mPgyqw this one is pure css, but is
trickier to use
Here is the first one in action:
I might have forgotten some, if so, please propose in comment section, and if it is indeed a real masonry (support for variable width), in accordance to what the OP ask for, I will add it to the list.