D3 v4 forceSimulation moves elements that don't collide - javascript

I have a set of markers (circles) on a map that sometimes overlap. Using d3.forceSimulation to prevent this and it's working but some makers move that don't need to as they don't overlap:
const stationForce = d3
.forceSimulation(data)
.force('charge', d3.forceManyBody().strength(-1))
.force(
'collision',
d3
.forceCollide()
.radius(
... calculated radius ...
)
)
.alphaDecay(0.9);
stationForce.on('tick', () => {
// move circles to their adjusted spot
circles
.attr('cx', (d, i) => {
return d.x;
})
.attr('cy', d => {
return d.y;
});
}
I have a line, using d3.forceLink so I can see here they moved from and to. Quite a few circles move (to different degrees) that didn't need to.
For example, none of these circles (the white ones) ever collide with another one (the grey is the starting point)

Related

Bubble Map with leaflet and D3.js [problem] : bubbles overlapping

I have a basic map here, with dummy data. Basically a bubble map.
The problem is I have multiple dots (ex:20) with exact same GPS coordinates.
The following image is my csv with dummy data, color blue highlight overlapping dots in this basic example. Thats because many compagny have the same city gps coordinates.
Here is a fiddle with the code I'm working on :
https://jsfiddle.net/MathiasLauber/bckg8es4/45/
Many research later, I found that d3.js add this force simulation fonction, that avoid dots from colliding.
// Avoiding bubbles overlapping
var simulationforce = d3.forceSimulation(data)
.force('x', d3.forceX().x(d => xScale(d.longitude)))
.force('y', d3.forceY().y(d => yScale(d.latitude)))
.force('collide', d3.forceCollide().radius(function(d) {
return d.radius + 10
}))
simulationforce
.nodes(cities)
.on("tick", function(d){
node
.attr("cx", function(d) { return projection.latLngToLayerPoint([d.latitude, d.longitude]).x; })
.attr("cy", function(d) {return projection.latLngToLayerPoint([d.latitude, d.longitude]).y; })
});
The problem is I can't make force layout work and my dots are still on top of each other. (lines: 188-200 in the fiddle).
If you have any tips, suggestions, or if you notice basic errors in my code, just let me know =D
Bunch of code close to what i'm trying to achieve
https://d3-graph-gallery.com/graph/circularpacking_group.html
https://jsbin.com/taqewaw/edit?html,output
There are 3 problems:
For positioning the circles near their original position, the x and y initial positions need to be specified in the data passed to simulation.nodes() call.
When doing a force simulation, you need to provide the selection to be simulated in the on tick callback (see node in the on('tick') callback function).
The simulation needs to use the previous d.x and d.y values as calculated by the simulation
Relevant code snippets below
// 1. Add x and y (cx, cy) to each row (circle) in data
const citiesWithCenter = cities.map(c => ({
...c,
x: projection.latLngToLayerPoint([c.latitude, c.longitude]).x,
y: projection.latLngToLayerPoint([c.latitude, c.longitude]).y,
}))
// citiesWithCenter will be passed to selectAll('circle').data()
// 2. node selection you forgot
const node = selection
.selectAll('circle')
.data(citiesWithcenter)
.enter()
.append('circle')
...
// let used in simulation
simulationforce.nodes(citiesWithcenter).on('tick', function (d) {
node
.attr('cx', function (d) {
// 3. use previously computed x value
// on the first tick run, the values in citiesWithCenter is used
return d.x
})
.attr('cy', function (d) {
// 3. use previously computed y value
// on the first tick run, the values in citiesWithCenter is used
return d.y
})
})
Full working demo here: https://jsfiddle.net/b2Lhfuw5/

How to crossfilter histogram and scatterplot matrix in d3 v4?

I am using this kind of scatterplot matrix and a histogram as two views, in d3. Both of them get the data from the same csv file. This is how the histogram looks like (x axis):
To brush the histogram I use the code below, which is similar to this snippet:
svg.append("g")
.attr("class", "brush")
.call(d3.brushX()
.on("end", brushed));
function brushed() {
if (!d3.event.sourceEvent) return;
if (!d3.event.selection) return;
var d0 = d3.event.selection.map(x.invert),
d1 = [Math.floor(d0[0]*10)/10, Math.ceil(d0[1]*10)/10];
if (d1[0] >= d1[1]) {
d1[0] = Math.floor(d0[0]);
d1[1] = d1[0]+0.1;
}
d3.select(this).transition().call(d3.event.target.move, d1.map(x));
}
How can I link the two views, so that when I brush the histogram, the scatterplot matrix will show the brushed points as colored in red, and the other points as, lets say, grey?
This can get you started:
3 html files:
2 for the visuals (histogram.html and scatter.html)
1 to hold them in iframes (both.html):
Dependency:
jQuery (add to all 3 files)
Create table with 2 cells in both.html:
Add iframes to each cell:
<iframe id='histo_frame' width='100%' height='600px' src='histo.html'></iframe>
<iframe id='scatter_frame' width='100%' height='600px' src='scatter.html'></iframe>
I am using this histogram, and this scatterplot.
Add the linky_dink function to call the function inside your scatter.html (see below...):
function linky_dink(linked_data) {
document.getElementById('scatter_frame').contentWindow.color_by_value(linked_data);
}
In your scatter.html change your cell.selectAll function to this:
cell.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d[p.x]); })
.attr("cy", function(d) { return y(d[p.y]); })
.attr("r", 4)
.attr('data-x', function(d) { return d.frequency }) // get x value being plotted
.attr('data-y', function(d) { return d.year }) // get y value being plotted
.attr("class", "all_circles") // add custom class
.style("fill", function(d) { return color(d.species); });
}
Note the added lines in bold:
Now our histogram circle elements retain the x and y values, along with a custom class we can use for targeting.
Create a color_by_value function:
function color_by_value(passed_value) {
$('.all_circles').each(function(d, val) {
if(Number($(this).attr('data-x')) == passed_value) {
$(this).css({ fill: "#ff0000" })
}
});
}
We know from above this function will be called from the linky_dink function of the parent html file. If the passed value matches that of the circle it will be recolored to #ff0000.
Finally, look for the brushend() function inside your histogram.html file. Find where it says: d3.selectAll("rect.bar").style("opacity", function(d, i) { .... and change to:
d3.selectAll("rect.bar").style("opacity", function(d, i) {
if(d.x >= localBrushYearStart && d.x <= localBrushYearEnd || brush.empty()) {
parent.linky_dink(d.y)
return(1)
} else {
return(.4)
}
});
Now, in addition to controlling the rect opacity on brushing, we are also calling our linky_dink function in our both.html file, thus passing any brushed histogram value onto the scatterplot matrix for recoloring.
Result:
Not the greatest solution for obvious reasons. It only recolors the scatterplot when the brushing ends. It targets circles by sweeping over all classes which is horribly inefficient. The colored circles are not uncolored when the brushing leaves those values since this overwhelms the linky_dink function. And I imagine you'd rather not use iframes, let alone 3 independent files. Finally, jQuery isn't really needed as D3 provides the needed functionality. But there was also no posted solution, so perhaps this will help you or someone else come up with a better answer.

D3.js shade area between two lines using CSS fill [duplicate]

So I have a chart plotting traffic vs. date and rate vs. date. I'm trying to shade the area between the two lines. However, I want to shade it a different color depending on which line is higher. The following works without that last requirement:
var area = d3.svg.area()
.x0(function(d) { return x(d3.time.format("%m/%d/%Y").parse(d.original.date)); })
.x1(function(d) { return x(d3.time.format("%m/%d/%Y").parse(d.original.date)); })
.y0(function(d) { return y(parseInt(d.original.traffic)); })
.y1(function(d) { return y(parseInt(d.original.rate)); })
However, adding that last requirement, I tried to use defined():
.defined(function(d){ return parseInt(d.original.traffic) >= parseInt(d.original.rate); })
Now this mostly works, except when lines cross. How do I shade the area under one line BETWEEN points? It's shading based on the points and I want it to shade based on the line. If I don't have two consecutive points on one side of the line, I don't get any shading at all.
Since you don't have datapoints at the intersections, the simplest solution is probably to get the areas above and below each line and use clipPaths to crop the difference.
I'll assume you're using d3.svg.line to draw the lines that the areas are based on. This way we'll be able to re-use the .x() and .y() accessor functions on the areas later:
var trafficLine = d3.svg.line()
.x(function(d) { return x(d3.time.format("%m/%d/%Y").parse(d.original.date)); })
.y(function(d) { return y(parseInt(d.original.traffic)); });
var rateLine = d3.svg.line()
.x(trafficLine.x()) // reuse the traffic line's x
.y(function(d) { return y(parseInt(d.original.rate)); })
You can create separate area functions for calculating the areas both above and below your two lines. The area below each line will be used for drawing the actual path, and the area above will be used as a clipping path. Now we can re-use the accessors from the lines:
var areaAboveTrafficLine = d3.svg.area()
.x(trafficLine.x())
.y0(trafficLine.y())
.y1(0);
var areaBelowTrafficLine = d3.svg.area()
.x(trafficLine.x())
.y0(trafficLine.y())
.y1(height);
var areaAboveRateLine = d3.svg.area()
.x(rateLine.x())
.y0(rateLine.y())
.y1(0);
var areaBelowRateLine = d3.svg.area()
.x(rateLine.x())
.y0(rateLine.y())
.y1(height);
...where height is the height of your chart, and assuming 0 is the y-coordinate of the top of the chart, otherwise adjust those values accordingly.
Now you can use the area-above functions to create clipping paths like this:
var defs = svg.append('defs');
defs.append('clipPath')
.attr('id', 'clip-traffic')
.append('path')
.datum(YOUR_DATASET)
.attr('d', areaAboveTrafficLine);
defs.append('clipPath')
.attr('id', 'clip-rate')
.append('path')
.datum(YOUR_DATASET)
.attr('d', areaAboveRateLine);
The id attributes are necessary because we need to refer to those definitions when actually clipping the paths.
Finally, use the area-below functions to draw paths to the svg. The important thing to remember here is that for each area-below, we need to clip to the opposite area-above, so the Rate area will be clipped based on #clip-traffic and vice versa:
// TRAFFIC IS ABOVE RATE
svg.append('path')
.datum(YOUR_DATASET)
.attr('d', areaBelowTrafficLine)
.attr('clip-path', 'url(#clip-rate)')
// RATE IS ABOVE TRAFFIC
svg.append('path')
.datum(YOUR_DATASET)
.attr('d', areaBelowRateLine)
.attr('clip-path', 'url(#clip-traffic)')
After that you'll just need to give the two regions different fill colors or whatever you want to do to distinguish them from one another. Hope that helps!

d3js rescale redraw barchart without new data

I have a simple (o.k. not so simple) barchart which shows the electric power consumption of one consumer (C1). I add the consumption of another consumer (C2) as line. The max consumption of C2 if higher then the max consumption of C1 so I have to rescale. I have solved this problem but not as beautiful I wanted to.
I calculate the new yMax, set the domain, rescale the axis (beautiful) remove all 'rect' and redraw (not beautiful). Is there a possibility to say: hey bars, I have a new scale, go down with a beautiful animation :)
Here the rescale method:
var rescale = function () {
//in this function the new _maxYValue is set
renderLineView();
var data = _data;
y.domain([_minYValue, _maxYValue]);
_svg.select(".y.axis")
.transition().duration(1500).ease("sin-in-out")
.call(yAxis());
_svg.selectAll("rect").remove();
var barWidth = getBarWidth(data.length);
var bars = d3.select("#layer_1").selectAll(".bar").data(data, function (d) {
return d.xValue;
});
bars.enter().append("rect")
.attr("class", "daybarincomplete")
.attr("x", function (d, i) {
return x(d.xValue) + 4;
})
.attr("width", barWidth)
.attr("y", function (d) {
return Math.min(y(0), y(d.value));
})
.attr("height", function (d) {
return Math.abs(y(d.value) - y(0));
});
}
Here is the jsfiddle: http://jsfiddle.net/axman/v4qc7/5/
thx in advance
©a-x-i
Use the .transition() call on bars, to determine the behaviour you want when the data changes (e.g. Bar heights change). You'd chain the .attr() function after it to set bar height etc.
To deal with data points that disappear between refreshes (e.g. You had 10 bars originally but now only have 9), chain the .exit().remove() functions to bars.
With both of the above, you can additionally chain something like .duration(200).ease('linear') to make it look all pretty.
pretty much what #ninjaPixel said. There's a easy to follow example here
http://examples.oreilly.com/0636920026938/chapter_09/05_transition.html

Using d3 to shade area between two lines

So I have a chart plotting traffic vs. date and rate vs. date. I'm trying to shade the area between the two lines. However, I want to shade it a different color depending on which line is higher. The following works without that last requirement:
var area = d3.svg.area()
.x0(function(d) { return x(d3.time.format("%m/%d/%Y").parse(d.original.date)); })
.x1(function(d) { return x(d3.time.format("%m/%d/%Y").parse(d.original.date)); })
.y0(function(d) { return y(parseInt(d.original.traffic)); })
.y1(function(d) { return y(parseInt(d.original.rate)); })
However, adding that last requirement, I tried to use defined():
.defined(function(d){ return parseInt(d.original.traffic) >= parseInt(d.original.rate); })
Now this mostly works, except when lines cross. How do I shade the area under one line BETWEEN points? It's shading based on the points and I want it to shade based on the line. If I don't have two consecutive points on one side of the line, I don't get any shading at all.
Since you don't have datapoints at the intersections, the simplest solution is probably to get the areas above and below each line and use clipPaths to crop the difference.
I'll assume you're using d3.svg.line to draw the lines that the areas are based on. This way we'll be able to re-use the .x() and .y() accessor functions on the areas later:
var trafficLine = d3.svg.line()
.x(function(d) { return x(d3.time.format("%m/%d/%Y").parse(d.original.date)); })
.y(function(d) { return y(parseInt(d.original.traffic)); });
var rateLine = d3.svg.line()
.x(trafficLine.x()) // reuse the traffic line's x
.y(function(d) { return y(parseInt(d.original.rate)); })
You can create separate area functions for calculating the areas both above and below your two lines. The area below each line will be used for drawing the actual path, and the area above will be used as a clipping path. Now we can re-use the accessors from the lines:
var areaAboveTrafficLine = d3.svg.area()
.x(trafficLine.x())
.y0(trafficLine.y())
.y1(0);
var areaBelowTrafficLine = d3.svg.area()
.x(trafficLine.x())
.y0(trafficLine.y())
.y1(height);
var areaAboveRateLine = d3.svg.area()
.x(rateLine.x())
.y0(rateLine.y())
.y1(0);
var areaBelowRateLine = d3.svg.area()
.x(rateLine.x())
.y0(rateLine.y())
.y1(height);
...where height is the height of your chart, and assuming 0 is the y-coordinate of the top of the chart, otherwise adjust those values accordingly.
Now you can use the area-above functions to create clipping paths like this:
var defs = svg.append('defs');
defs.append('clipPath')
.attr('id', 'clip-traffic')
.append('path')
.datum(YOUR_DATASET)
.attr('d', areaAboveTrafficLine);
defs.append('clipPath')
.attr('id', 'clip-rate')
.append('path')
.datum(YOUR_DATASET)
.attr('d', areaAboveRateLine);
The id attributes are necessary because we need to refer to those definitions when actually clipping the paths.
Finally, use the area-below functions to draw paths to the svg. The important thing to remember here is that for each area-below, we need to clip to the opposite area-above, so the Rate area will be clipped based on #clip-traffic and vice versa:
// TRAFFIC IS ABOVE RATE
svg.append('path')
.datum(YOUR_DATASET)
.attr('d', areaBelowTrafficLine)
.attr('clip-path', 'url(#clip-rate)')
// RATE IS ABOVE TRAFFIC
svg.append('path')
.datum(YOUR_DATASET)
.attr('d', areaBelowRateLine)
.attr('clip-path', 'url(#clip-traffic)')
After that you'll just need to give the two regions different fill colors or whatever you want to do to distinguish them from one another. Hope that helps!

Categories