I'm very new to d3.js (and SVG in general), and I want to do something simple: a tree/dendrogram with angled connectors.
I have cannibalised the d3 example from here:http://mbostock.github.com/d3/ex/cluster.html
and I want to make it more like the protovis examples here:
http://mbostock.github.com/protovis/ex/indent.html
http://mbostock.github.com/protovis/ex/dendrogram.html
I have made a start here: http://jsbin.com/ugacud/2/edit#javascript,html and I think it's the following snippet that's wrong:
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
However there's no obvious replacement, I could use d3.svg.line, but I don't know how to integrate it properly, and ideally I'd like an elbow connector....although I am wondering if I am using the wrong library for this, as a lot of the d3 examples I've seen are using the gravitational force to do graphs of objects instead of trees.
Replace the diagonal function with a custom path generator, using SVG's "H" and "V" path commands.
function elbow(d, i) {
return "M" + d.source.y + "," + d.source.x
+ "V" + d.target.x + "H" + d.target.y;
}
Note that the source and target's coordinates (x and y) are swapped. This example displays the layout with a horizontal orientation, however the layout always uses the same coordinate system: x is the breadth of the tree, and y is the depth of the tree. So, if you want to display the tree with the leaf (bottommost) nodes on the right edge, then you need to swap x and y. That's what the diagonal's projection function does, but in the above elbow implementation I just hard-coded the behavior rather than using a configurable function.
As in:
svg.selectAll("path.link")
.data(cluster.links(nodes))
.enter().append("path")
.attr("class", "link")
.attr("d", elbow);
And a working example:
http://bl.ocks.org/d/2429963/
Related
I am trying to visualize russians regions. I got data from here, validate here and all was well - picture.
But when I try to draw it, I receive only one big black rectangle.
var width = 700, height = 400;
var svg = d3.select(".graph").append("svg")
.attr("viewBox", "0 0 " + (width) + " " + (height))
.style("max-width", "700px")
.style("margin", "10px auto");
d3.json("83.json", function (error, mapData) {
var features = mapData.features;
var path = d3.geoPath().projection(d3.geoMercator());
svg.append("g")
.attr("class", "region")
.selectAll("path")
.data(features)
.enter()
.append("path")
.attr("d", path)
});
Example - http://ustnv.ru/d3/index.html
Geojson file - http://ustnv.ru/d3/83.json
The issue is the winding order of the coordinates (see this block). Most tools/utilities/libraries/validators don't really care about winding order because they treat geoJSON as containing Cartesian coordinates. Not so with D3 - D3 uses ellipsoidal math - benefits of this is include being able to cross the antimeridian easily and being able to select an inverted polygon.
The consequence of using ellipsoidal coordinates is the wrong winding order will create a feature of everything on the planet that is not your target (inverted polygon). Your polygons actually contain a combination of both winding orders. You can see this by inspecting the svg paths:
Here one path appears to be accurately drawn, while another path on top of it covers the entire planet - except for the portion it is supposed to (the space it is supposed to occupy covered by other paths that cover the whole world).
This can be simple to fix - you just need to reorder the coordinates - but as you have features that contain both windings in the same collection, it'll be easier to use a library such as turf.js to create a new array of properly wound features:
var fixed = features.map(function(feature) {
return turf.rewind(feature,{reverse:true});
})
Note the reverse winding order - through an odd quirk, D3, which is probably the most widespread platform where winding order matters actually doesn't follow the geoJSON spec (RFC 7946) on winding order, it uses the opposite winding order, see this comment by Mike Bostock:
I’m disappointed that RFC 7946 standardizes the opposite winding order
to D3, Shapefiles and PostGIS. And I don’t see an easy way for D3 to
change its behavior, since it would break all existing (spherical)
GeoJSON used by D3. (source)
By rewinding each polygon we get a slightly more useful map:
An improvement, but the features are a bit small with these projection settings.
By adding a fitSize method to scale and translate we get a much better looking map (see block here):
Here's a quick fix to your problem, projection needs a little tuning, also path has fill:#000 by default and stroke: #FFF could make it more legible.
var width = 700, height = 400;
var svg = d3.select(".graph").append("svg")
.attr("viewBox", "0 0 " + (width) + " " + (height))
.style("max-width", "700px")
.style("margin", "10px auto");
d3.json("mercator_files/83.json", function (error, mapData) {
var features = mapData.features;
var center = d3.geoCentroid(mapData);
//arbitrary
var scale = 7000;
var offset = [width/2, height/2];
var projection = d3.geoMercator().scale(scale).center(center)
.translate(offset);
var path = d3.geoPath().projection(projection);
svg.append("g")
.attr("class", "region")
.selectAll("path")
.data(features)
.enter()
.append("path")
.attr("d", path)
});
I am working on a bubble chart that needs to look like this - its likely to have just 2 series.
My main concern is what to do - if the bubbles are of the same size or if the situation is reversed.
I thought of using this jsfiddle as a base..
http://jsfiddle.net/NYEaX/1450/
// generate data with calculated layout values
var nodes = bubble.nodes(data)
.filter(function(d) {
return !d.children;
}); // filter out the outer bubble
var vis = svg.selectAll('circle')
.data(nodes);
vis.enter()
.insert("circle")
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
.attr('r', function(d) {
return d.r;
})
.style("fill", function(d) {
return color(d.name);
})
.attr('class', function(d) {
return d.className;
});
vis
.transition().duration(1000)
vis.exit()
.remove();
Slightly updated your first fiddle http://jsfiddle.net/09fsu0v4/1/
So, lets go through your questions:
a bubble chart that needs to look like this - its likely to have just 2 series.
Bubble charts as part of pack layout rely on data structure - nested JSON with children array. Each node could be either root node (container) or leaf node (node that represents some end value). And its role depends on 'children' value presence. (https://github.com/d3/d3-3.x-api-reference/blob/master/Pack-Layout.md#nodes)
So, to get you chart looking like on picture just re-arrange json structure (or write children accessor function - https://github.com/d3/d3-3.x-api-reference/blob/master/Pack-Layout.md#children). Brief example is in updated fiddle.
if the bubbles are of the same size or if the situation is reversed
Parent nodes (containers) size is sum of all child nodes size. So if you have only one child node parent node will have the same size.
As far as you can't directly change parent node size - situation with oversized child nodes is impossible.
It might be easier if you start with this instead.
You could set the smaller bubble to be a child of the larger one.
As for when the series have the same size, I would either have a single bubble with both series given the same color, or I would separate the two bubbles. There isn't really much you can do when the bubbles need to be the same size.
I am working on this plnkr. I have three lines at angle 30, 45 and 60. I want to apply a brush on these lines so that when the chart is brushed the lines get redrawn at where it crossed the brushed rectangle with appropriate the values on the axis. Any help or hint to solve this problem is greatly appreciated.
EDIT: If you have different solutions to draw the rotated lines and brush on top of them it is welcomed too. Please help.
var ga = d3.select("svg")
.append("g")
.attr("class", "a axis")
.attr("transform", "translate(" + margin.left + "," + (height + margin.top) + ")")
.selectAll("g")
.data([30, 45, 60])
.enter()
.append("g")
.attr("class", "rotatedlines")
.attr("transform", function(d) { return "rotate(" + -d + ")"; })
.attr("stroke-width", 1)
.attr("stroke", "black")
.attr("stroke-dasharray", "5,5");
To explain my solution:
The fundamental steps to take are as follows:
update the domains of the x and y scales to the brush extent
redraw the axes
compute the scale factor and translation for the lines
scale and translate the line containers accordingly
reset the brush
Note that steps 3 and 4 are only necessary because you're not using the scales to draw everything -- a better approach would be to define two points for each line as the data that's bound to the elements and then use the scales to redraw. This would make the code simpler.
With your approach it's still possible though. In order to facilitate it, I've made a few modifications to your code -- in particular, I've cleaned up the various nested g elements with different translations and defined the lines through their x1, x2, y1, y2 attributes rather than through translation of the containers. Both of these changes make the functionality you want easier to implement as only a single transformation takes places that doesn't need to consider multiple other transformations. I've also nested the lines in multiple g elements so that they can be scaled and translated more easily.
The brush handler function now looks like this:
// update scales, redraw axes
var extent = brush.extent();
x.domain(brush.empty() ? x2.domain() : [ extent[0][0], extent[1][0] ]);
y.domain(brush.empty() ? y2.domain() : [ extent[0][1], extent[1][1] ]);
xAxisG.call(xAxis);
yAxisG.call(yAxis);
This code should be fairly self-explanatory -- the domains of the scales are updated according to the current extent of the brush and the axes are redrawn.
// compute and apply scaling and transformation of the g elements containing the lines
var sx = (x2.domain()[1] - x2.domain()[0])/(x.domain()[1] - x.domain()[0]),
sy = (y2.domain()[1] - y2.domain()[0])/(y.domain()[1] - y.domain()[0]),
dx = -x2(x.domain()[0]) - x2.range()[0],
dy = -y2(y.domain()[1]) - y2.range()[1];
d3.selectAll("g.container")
.attr("transform", "translate(" + [sx * dx, sy * dy] + ")scale(" + [sx, sy] + ")");
This is the tricky part -- based on the new domains of the scales, we need to compute the scale and translation for the lines. The scaling factors are simply the ratio of the old extent to the new extent (note that I have made copies of the scales that are not modified), i.e. a number greater than 1. The translation determines the shift of the (0,0) coordinate and is computed through the difference of the old (0,0) coordinate (I get this from the range of the original scales) and the position of the new domain origin according to the original scales.
When applying the translation and scale at the same time, we need to multiply the offsets with the scaling factors.
// reset brush
brush.clear();
d3.select(".brush").call(brush);
Finally, we clear the brush and reset it to get rid of the grey rectangle.
Complete demo here.
You can access the brush extent via d3.event.target.extent(). The flow for drawing the scale is this:
Set scale
Set axis
Draw axis
As soon as the brush is done, you have to modify the scale and then re-draw the axis according to the current x and y domain. Is that what you meant?
I cleaned up the code a bit and made a little demonstration: http://plnkr.co/edit/epKbXbcBR2MiwUOMlU5A?p=preview
I'm currently attempting to build a a multi-line graph with a d3.time.scale() for the x-axis.
I'm trying to add circles to each point on lines of the graph, but have been unsuccessful thus far.
When I do something like:
.attr('cx', function(d){ return x(d.price) })
I get a negative number.
I was thinking of setting up another scale (pointsScale) to handle this but have been largely unsuccessful.
What am I doing wrong?
Please refer to my JSBin for the code.
You're running into a few issues here:
Since you made the x-axis a time-scale, I'm guessing that you actually want price to be the y variable, while date is the x variable. That's why x(d.price) is negative - d3 is trying to interpret the prices as dates, which doesn't end up making much sense. So replace your line of code above with this: .attr('cy', function(d){ return y(d.price) })
In order to actually have circles be visible, they need to have three parameters set: cx, cy, and r. Since d3 already knows that your x axis is a time scale, you can set cx with .attr('cx', function(d){ return x(d.date) }). You can make r be whatever radius you want for the circles. Just choose one, or it will default to 0 and you won't be able to see the circles. .attr('r', 4), for instance, would set the radius to a perfectly visible value of 4.
You're drawing the circles before you draw the lines. As a result, the lines get drawn over the circles and it looks kind of weird. So move the circle code to after the line code if you want to avoid that.
Putting it all together, this is roughly what the code to create your circles should look like, and it should go after you declare var paths:
var circles = company.selectAll('circle')
.data(function(d){ return d.values; })
.enter().append('circle')
.attr('cy', function(d){
return y(d.price);}) //Price is the y variable, not the x
.attr('cx', function(d){
return x(d.date);}) //You also need an x variable
.attr('r',4); //And a radius - otherwise your circles have
//radius 0 and you can't see them!
Updated jsbin:
http://jsbin.com/gorukojoxu/edit?html,console,output
I was wondering if there is a way to create a force directed layout with d3.js and restrict it by an arbitrary shape in such a way that
all the nodes are equivalently distributed within the shape and
the distance between the border and the nodes is equally to the distance between the nodes
I hope there is already such a solution out there. Otherwise my idea is to start with the force directed layout and check the distances from the nodes to the borders in each iteration. Any suggestions from yourside?
Your idea is mine too. In the tick function you could add additional forces. This is my suggestion (not tested):
force.on('tick', function(e) {
node
.each(calcBorderDistance)
.attr('transform', function(d) {
d.x -= e.alpha*(1/Math.pow(d.borderDistance.x,2);
d.y -= e.alpha*(1/Math.pow(d.borderDistance.y,2);
return 'translate(' + d.x + ',' + d.y + ')'; // Move node
});
});
function calcBorderdistance(d) {
// Insert code here to calculate the distance to the nearest border of your shape
var x = ..., y = ...;
d.borderDistance = {'x':x,'y':y};
}
I have the inverse quadratic distance to the nearest border function loosely based on the formulas in excelent paper Drawing Graphs Nicely using Simulated Annealing. Following picture illustrates how methods from this paper affect drawing nodes bounded by a box:
And this picture illustrate case with different constraints, involving links between nodes: