I am using d3 to make a line chart that has to support up to 100 points on it, making it very crowded. The problem is that some of the labels overlap.
The method I was trying involved drawing all the points, then separately drawing all the labels and running a force collision on the labels to stop them overlapping, then after the force collision drawing a line between each of the labels and their associated point.
I can't make the forces work, let alone the drawing of lines after.
Any suggestions for a better way to do this are heartily welcomed also.
Here is my code:
$.each(data.responseJSON.responsedata, function(k, v) {
var thispoint = svg.append("g").attr("transform", "translate("+pointx+","+pointy+")");
thispoint.append("circle").attr("r", 10).style("fill","darkBlue").style("stroke","black");
var label = svg.append("text").text(v.conceptName).style("text-anchor", "end").attr("font-family", "Calibri");
label.attr("transform", "translate("+(pointx)+","+(pointy-12)+") rotate(90)")
});
nodes = d3.selectAll("text")
simulation = d3.forceSimulation(nodes)
.force("x", d3.forceX().strength(10))
.force("y", d3.forceY().strength(10))
.force("collide",d3.forceCollide(20).strength(5))
.velocityDecay(0.15);
ticks = 0;
simulation.nodes(data)
.on("tick", d => {
ticks = ticks + 1;
d3.select(this).attr("x", function(d) { return d.x }).attr("y", function(d) { return d.x });
console.log("updated" + this)
});
Force layout is a relatively expensive way of moving labels to avoid collision. It is iteratively and computationally intensive.
More efficient algorithms add the labels one at a time, determining the best position for each. For example a 'greedy' strategy adds each label in sequence, selecting the position where the label has the lowest overlap with already added labels.
I've created a D3 components, d3fc-label-layout, that implements a number of label layout strategies:
https://github.com/d3fc/d3fc-label-layout
Here's an example of how to use it:
// Use the text label component for each datapoint. This component renders both
// a text label and a circle at the data-point origin. For this reason, we don't
// need to use a scatter / point series.
const labelPadding = 2;
const textLabel = fc.layoutTextLabel()
.padding(2)
.value(d => d.language);
// a strategy that combines simulated annealing with removal
// of overlapping labels
const strategy = fc.layoutRemoveOverlaps(fc.layoutGreedy());
// create the layout that positions the labels
const labels = fc.layoutLabel(strategy)
.size((d, i, g) => {
// measure the label and add the required padding
const textSize = g[i].getElementsByTagName('text')[0].getBBox();
return [
textSize.width,
textSize.height
];
})
.position((d) => {
return [
d.users,
d.orgs
]
})
.component(textLabel);
https://bl.ocks.org/ColinEberhardt/27508a7c0832d6e8132a9d1d8aaf231c
Related
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/
I thought I'd learn a little about ES6 classes while doing some d3 work, and so I made an ordinal bar chart class (fiddle here). It displays multiple series of data (eg:
[
[{"label":"apple", "value" :25},
{"label":"orange", "value": 16},
{"label":"pear", "value":19}],
[{"label":"banana", "value" :12},
{"label":"grape", "value": 6},
{"label":"peach", "value":5}]
];
I'm trying to get the update part working (where you provide new data and the bars/axis transition nicely). Unfortunately much of the example code is for v3, which doesn't seem to work with v4 like I'm using. The specific method is:
updateData(data){
//get an array of the ordinal labels out of the data
let labels = function(data){
let result = [];
for(let i=0; i<data.length; i++){
for (let j=0; j<data[i].length; j++){
result.push(data[i][j].label);
}
}
return result;
}(data);
//loop through the (potentially multiple) series in the data array
for(let i=0; i<data.length; i++){
let series = data[i],
bars = this.svg.selectAll(".series" + i)
bars
.data(series)
.enter().append("rect")
.attr("class", ("series" + i))
.classed("bar", true)
.merge(bars)
.attr("x", 0)
.attr("height", this.y.bandwidth())
.attr("y", (d) => { return this.y(d.label); })
.transition().duration(500) //grow bars horizontally on load
.attr("width", (d) => { return this.x(d.value); });
bars.exit().remove();
}
//update domain with new labels
this.y.domain(labels);
//change the y axis
this.svg.select(".yaxis")
.transition().duration(500)
.call(this.yAxis)
}
I'm trying to base the update pattern on Mike Bostock's code.
I'm getting an internal d3 error from the .call(this.yAxis), the transition doesn't animate, and the yaxis doesn't update. Additionally, the bars don't transition either. What's going wrong?
Several problems:
Update a scaleOrdinal/scaleBand axis BEFORE updating data join, otherwise the bars won't be able to find their y scale attribute (y(d.yourOrdinalLabel). The axis code hence needs to go before the bars code.
bars (or whatever element you join to the data) should be declared as the result of that join operation, so that it can be chained with .attr() for the visual attributes. let bars = svg.selectAll(".yourClass").data(yourData);
It's more sensible to have a simple update method in the class if you're only going to be toggling exclusion of existing data series, not adding new data. See updated fiddle.
Working jsfiddle.
Background
I'm working with a set of 8000 geographical points contained in csv file. On one hand I create a visualisation of Voronoi diagrams built using these points - it's done using D3 library. On the other hand I calculate these Voronoi diagrams in Python using Scipy.
My work logic is simple - I mess with my data on Python's side, making heatmaps, analysis and so on and then I visualise effects using D3. But today I accidentally found that Voronoi diagrams made by Scipy and D3 are different. I noticed that after using geojson.io to plot GeoJsons of Voronois made in Python just to see if I can visualise everything there.
As I said, the Voronois were different - some of them had different angles and some even had additional vertices.
Question:
Why is that happening? Why Voronoi diagrams calculated by these 2 libraries (D3 and Scipy) differ?
Further description
How it is done on D3 side: Based on Chris Zetter example http://chriszetter.com/blog/2014/06/15/building-a-voronoi-map-with-d3-and-leaflet/ I translate latitude and longitude into custom projection to visualise it on the mapbox map.
var voronoi = d3.geom.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.clipExtent([[N_W.x , N_W.y],[S_E.x, S_E.y]])
I create Voronoi based on points that are visible within map border + some padding (filteredPoints)
filteredPoints = points.filter(function(d) {
var latlng = new L.LatLng(d.latitude, d.longitude);
if (!drawLimit.contains(latlng)) { return false };
// this translates points from coordinates to pixels
var point = map.latLngToLayerPoint(latlng);
key = point.toString();
if (existing.has(key)) { return false };
existing.add(key);
d.x = point.x;
d.y = point.y;
return true;
});
voronoi(filteredPoints).forEach(function(d) { d.point.cell = d});
How it is done on Python side: I use scipy.spatial.Voronoi.
from scipy.spatial import Voronoi
def create_voronois():
points = numpy.array(points_list)
vor = Voronoi(points)
Where "points_list" is a list of my 8000 geographical points.
EDIT:
Screenshot from my visualisation - black borders are Voronois made with D3, white ones are made by scipy.spatial.Voronoi. As we can see scipy is wrong. Did anyone compare these 2 libraries before?
http://imgur.com/b1ndx0F
Code to run. It prints GeoJson with badly calculated Voronois.
import numpy
from scipy.spatial import Voronoi
from geojson import FeatureCollection, Feature, Polygon
points = [
[22.7433333333000, 53.4869444444000],
[23.2530555556000, 53.5683333333000],
[23.1066666667000, 53.7200000000000],
[22.8452777778000, 53.7758333333000],
[23.0952777778000, 53.4413888889000],
[23.4152777778000, 53.5233333333000],
[22.9175000000000, 53.5322222222000],
[22.7197222222000 ,53.7322222222000],
[22.9586111111000, 53.4594444444000],
[23.3425000000000, 53.6541666667000],
[23.0900000000000, 53.5777777778000],
[23.2283333333000, 53.4713888889000],
[23.3488888889000, 53.5072222222000],
[23.3647222222000 ,53.6447222222000]]
def create_voronois(points_list):
points = numpy.array(points_list)
vor = Voronoi(points)
point_voronoi_list = []
feature_list = []
for region in range(len(vor.regions) - 1):
vertice_list = []
for x in vor.regions[region]:
vertice = vor.vertices[x]
vertice = (vertice[1], vertice[0])
vertice_list.append(vertice)
polygon = Polygon([vertice_list])
feature = Feature(geometry=polygon, properties={})
feature_list.append(feature)
feature_collection = FeatureCollection(feature_list)
print feature_collection
create_voronois(points)
Apparently your javascript code is applying a transformation to the data before computing the Voronoi diagram. This transformation does not preserve the relative distances of the points, so it does not generate the same result as your scipy code. Note that I'm not saying that your d3 version is incorrect. Given that the data are latitude and longitude, what you are doing in the javascript code might be correct. But to compare it to the scipy code, you have to do the same transformations if you expect to get the same Voronoi diagram.
The scripts below show that, if you preserve the relative distance of the input points, scipy's Voronoi function and d3.geom.voronoi generate the same diagram.
Here's a script that uses scipy's Voronoi code:
import numpy
from scipy.spatial import Voronoi, voronoi_plot_2d
import matplotlib.pyplot as plt
points = [
[22.7433333333000, 53.4869444444000],
[23.2530555556000, 53.5683333333000],
[23.1066666667000, 53.7200000000000],
[22.8452777778000, 53.7758333333000],
[23.0952777778000, 53.4413888889000],
[23.4152777778000, 53.5233333333000],
[22.9175000000000, 53.5322222222000],
[22.7197222222000, 53.7322222222000],
[22.9586111111000, 53.4594444444000],
[23.3425000000000, 53.6541666667000],
[23.0900000000000, 53.5777777778000],
[23.2283333333000, 53.4713888889000],
[23.3488888889000, 53.5072222222000],
[23.3647222222000, 53.6447222222000]]
vor = Voronoi(points)
voronoi_plot_2d(vor)
plt.axis('equal')
plt.xlim(22.65, 23.50)
plt.ylim(53.35, 53.85)
plt.show()
It generates this plot:
Now here's a javascript program that uses d3.geom.voronoi:
<html>
<head>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.geom.js"></script>
</head>
<body>
<div id="chart">
</div>
<script type="text/javascript">
// This code is a hacked up version of http://bl.ocks.org/njvack/1405439
var w = 800,
h = 400;
var data = [
[22.7433333333000, 53.4869444444000],
[23.2530555556000, 53.5683333333000],
[23.1066666667000, 53.7200000000000],
[22.8452777778000, 53.7758333333000],
[23.0952777778000, 53.4413888889000],
[23.4152777778000, 53.5233333333000],
[22.9175000000000, 53.5322222222000],
[22.7197222222000, 53.7322222222000],
[22.9586111111000, 53.4594444444000],
[23.3425000000000, 53.6541666667000],
[23.0900000000000, 53.5777777778000],
[23.2283333333000, 53.4713888889000],
[23.3488888889000, 53.5072222222000],
[23.3647222222000, 53.6447222222000]
];
// Translate and scale the points. The same scaling factor (2*h) must be used
// on x and y to preserve the relative distances among the points.
// The y coordinates are also flipped.
var vertices = data.map(function(point) {return [2*h*(point[0]-22.5), h - 2*h*(point[1]-53.4)]})
var svg = d3.select("#chart")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
var paths, points;
points = svg.append("svg:g").attr("id", "points");
paths = svg.append("svg:g").attr("id", "point-paths");
paths.selectAll("path")
.data(d3.geom.voronoi(vertices))
.enter().append("svg:path")
.attr("d", function(d) { return "M" + d.join(",") + "Z"; })
.attr("id", function(d,i) {
return "path-"+i; })
.attr("clip-path", function(d,i) { return "url(#clip-"+i+")"; })
.style("fill", d3.rgb(230, 230, 230))
.style('fill-opacity', 0.4)
.style("stroke", d3.rgb(50,50,50));
points.selectAll("circle")
.data(vertices)
.enter().append("svg:circle")
.attr("id", function(d, i) {
return "point-"+i; })
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 2)
.attr('stroke', d3.rgb(0, 50, 200));
</script>
</body>
</html>
It generates:
Based on a visual inspection of the results, I'd say they are generating the same Voronoi diagram.
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
I'm hand cranking a network diagram from D3 (I didn't like the output of Force Directed). To do this, I've generated an array of nodes, each with an x/y co-ordinate.
{
"nodes" : [
{
"name" : "foo",
"to" : ["bar"]
},
{
"name" : "bar",
"to" : ["baz"]
},
{
"name" : "baz"
}
]
}
I then generate an svg, with a parent svg:g, and bind this data to a series of svg:g elements hanging off the parent.
addSvg = function () {
// add the parent svg element
return d3.select('#visualisation')
.append('svg')
.attr('width', width)
.attr('height', height);
};
addSvgGroup = function (p) {
// add a containing svg:g
return p.append('svg:g').
attr('transform', 'translate(0,0)');
};
addSvgNodes = function(p, nodes) {
// attach all nodes to the parent p data
// and render to svg:g
return p.selectAll('g')
.data(nodes)
.enter()
.append('svg:g')
.attr('class', 'node');
};
Then I manually position the nodes (this will be dynamic later, I'm just getting my feet)
transformNodes = function (nodes) {
// position nodes manually
// deprecate later for column concept
nodes.attr('transform', function (o, i) {
offset = (i + 1) * options.nodeOffset;
// options.nodeOffset = 150
o.x = offset;
o.y = offset / 2;
return 'translate(' + offset + ',' + offset / 2 + ')';
});
};
Then I attach these items to the parent svg:g, and hang some text off them.
This results in a staircase of text descending left to right within the svg. So far, so good.
Next, I want to generate some links, so I use a method to determine if the current node has a relationship, and then get that nodes location. Finally, I generate a series of links using d3.svg.diagonal and set their source/target to the appropriate nodes. Written longhand for clarity.
getLinkGenerator = function (o) {
return d3.svg.diagonal()
.source(o.source)
.target(o.target)
.projection(function (d) {
console.log('projection', d);
return [d.x, d.y]
});
};
Now, so far, so good - except the control handles for the bezier are not where I would like them to be. For example from node A to node B the path d attribute is thus:
<path d="M150,75C150,112.5 300,112.5 300,150" style="fill: none" stroke="#000"></path>
But I'd like it to alter the orientation of the control handles - i.e
<path d="M150,75C200,75 250,150 300,150" style="fill: none" stroke="#000"></path>
This would make it look more like a dendrograph from the page of examples. What I noticed in the collapsible dendrograph example is that it returns an inversion of the axes:
return [d.y, d.x]
But if I do this, while the control points are oriented as I would like, the location of the points is out of whack (i.e their x/y co-ordinates are also reversed, effectively translating them.
Has anyone else encountered an issue like this or have an idea of how to fix it?
OK, so I took a look at this and figured out a solution. It appears that some of the layouts (dendrogram, collapsed tree) are inverting the co-ordinates of source/target in the path links so that when they hit the projection call, they get reversed back into their correct location, but with the orientation of their bezier points rotated.
So, if you're hand cranking a custom layout and you want to orient the bezier controls horizontally (like the examples), that's what you need to do.