Different node symbols for d3.js force-directed graph - svg

How do I display nodes as different symbols in d3.js's force-directed library? I wanted to implement something similar to what I wrote below:
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append(function(d){return d.shape;})
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
Each node would have an encoded shape ("rect", "circle", etc.). However, I get the error:
Uncaught TypeError: Object function (d){return "circle";} has no method 'indexOf'
The other question I have related to that is this: how would I toggle between applying different attributes for each shape? Circles need an "r" attribute refined, but rects require "height" and "width". Thanks!

Use d3.svg.symbol, as in the force-directed symbols example.

Related

Generating a working svg gradient with d3

I'm trying to generate an SVG gradient (for a stroke) with D3 (the rest of the project uses D3, so using D3 for this seemed to make sense...)
Here is the code that generates the gradient:
function generateBlindGradient(svg, color, side) {
// can't have a hash mark in the id or bad things will happen
idColor = color.replace('#', '');
side = side || 'right';
// this is a sneaky d3 way to select the element if present
// or create the element if it isn't
var defs = svg.selectAll('defs').data([0]);
defs.enter().append('svg:defs');
var id = 'gradient-' + idColor + '-' + side;
var gradient = defs.selectAll('lineargradient#'+id).data([0]);
gradient.enter().append('svg:lineargradient')
.attr('id', id);
var colors = [
{ offset : '50%', color : '#DFE2E6' },
{ offset : side === 'left' ? '100%' : '0%', color : color }
];
var stops = gradient.selectAll('stop').data(colors);
stops.enter().append('svg:stop');
stops.attr('stop-color', function(d) { return d.color; })
.attr('offset', function(d) { return d.offset; });
return id;
}
This works... almost right. It generates gradients like this:
<lineargradient id="gradient-a8d4a1-left">
<stop stop-color="#DFE2E6" offset="50%"></stop>
<stop stop-color="#a8d4a1" offset="100%"></stop>
</lineargradient>
That gradient does not work (as either a fill or a stroke)--the element it's applied to gets no stroke or fill.
If I use the web inspector to "edit the HTML" of the lineargradient element, even if I don't change anything, the gradients suddenly work — so I'm guessing there's something weird going on within Chrome's SVG parsing or d3's element generation.
I think it might be down to a confusion between lineargradient and linearGradient—d3 seems to have some issues with camelCased elements and when I have it create linearGradient elements, it doesn't select them (and I get lots and lots of copies). Also, while in Chrome's inspector, these elements show up as lineargradient; when I edit as HTML, they are linearGradient. I'm not sure what's going on here or how to fix it.
SVG is case sensitive so its linearGradient rather than lineargradient for creation.
I think Chrome has a selector bug that you can't select camel cased elements though.
The common workaround seems to be to assign a class to all your linearGradient elements and select by class rather than by tag name.

How to add single label per svg element?

I'm attempting to add a simple data label to this example
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("fill", "red")
.text(function(d) { return d[0] });
but it writes all the data in all labels which I think is because the graph is split over several individual svg-elements. How can I fix this?
Fiddle
You're already binding the data to the SVGs, so there's no need for you to bind it again to the text elements. svg is actually the selection of SVGs here, so all you have to do is
svg.append("text")
.attr("text-anchor", "middle")
.attr("fill", "red")
.text(function(d) { return d[0] });
Complete example here.

Add a circle to a d3.js map

I have generated a map of Phoenix from this GeoJson and made it show as I would like it to.
Now I would like to add circles to the map to represent something of interest, but the circles never show up. Here is the code:
<script type="text/javascript">
var h = 1280;
var w = 1280;
var projection = d3.geo.albers().scale(80000).center([0, 33.44]).rotate([112.07, 0]).translate([920, 850]);
var path = d3.geo.path().projection(projection);
var svg = d3.select("body").append("svg").attr("width", w).attr("height", h);
d3.json("data/phoenix.json", function(json) {
svg.selectAll("path").data(json.features).enter().append("path")
.attr("d", path).style("fill", "grey");
var coordinates = projection([33.46764,112.0785]);
svg.append("circle")
.attr("cx", coordinates[0])
.attr("cy", coordinates[1])
.attr("r", 5)
.style("fill", "red");
});
</script>
I have tried following different tutorial and howto's like from bost.ocks.org and this where it's with a csv file, but no matter what I do it won't draw the circle, what am I missing?
Adam Pearce is correct that the coordinates are [33.46764, -112.0785], however there is another problem: when translating from lat-long to the coords, you need to pass longitude as the first parameter, not latitude!
The tricky thing is that the albers projection, if called with a value not in (lower 48, alaska, hawaii) returns null silently.
Trying to translate [33.46764, -112.0785] in the console:
> proj = d3.geo.albersUsa()
function albersUsa(coordinates) {
var x = coordinates[0], y = coordinates[1];
point = null;
(lower48Point(x, y), point) || (alaskaPoint(x, y), point) || hawaiiPoint(x, y);
return point;
} d3.v3.js:3257
> proj( [33.46764, -112.0785] )
null
> proj( [-112.0785, 33.46762] )
[241.08874867733104, 327.6295325563234]
Bingo. In this case, it was useful to take a look at the actual function we are calling by using the console (in this case, in Chrome).
This was done using d3 version 3.3.8.
Schimmy's answer is correct, however I didn't understand at first. Here's how I added a circle on an Albers map:
//var projection = d3.geo.albersUsa();
var coordinates = projection([-112.0785,33.46764]);
svg.append("circle")
.attr("cx", coordinates[0])
.attr("cy", coordinates[1])
.attr("r", 5)
.style("fill", "red");
You may also want to use attr("transform", "translate") rather than attr("cx", coor[0].attr("cy", coor[1]).
If you have a GeoJson fie of the US and you want to plot a circle on each county:
// us = the geoJson file
svg.append("circle")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("r", 5)
.style("fill", "red");
You may find this much more efficient than "cx" and "cy".
From http://bost.ocks.org/mike/bubble-map/

How to show a tooltip with value when mouseover a svg line graph using d3.js?

I'm using D3.js. I want to show a tooltip with corresponding Y-axis value when i mouseover d3.svg.line().
I tried using this code:
d3.svg.line().append("title")
.text(function(d) { return d; });`
but it throws error has no method 'append'. Is there any other way?
d3.svg.line() is a line generator; it is not the actual line element. This function is designed to work with an area generator, though you can disable the appearance of the shape within by using "fill:none." For more detailed information, here is a link to its documentation: https://github.com/mbostock/d3/wiki/SVG-Shapes#wiki-line.
The code below creates a path element using the d3.svg.line() generator and then adds a tooltip to the path it generates. This title's text attribute shows the y-value of wherever the mouse is. This is done by using the mouse event "mousemove" which seems to be more what you want instead of using "mouseover." (Mouseover requires you to move in and out of the shape to update the text value, whereas mousemove will change the value even if your mouse moves along the line.)
var data = [[{x:100, y:200}, {x:0,y:400}, {x:2, y:300}]];
var line = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("basis");
var svg = d3.select("body").append("svg:svg")
.attr("width", 400)
.attr("height", 400);
var g = svg.selectAll("g").data(data).enter().append("svg:g")
.attr("width", 400)
.attr("height", 400);
g.append("path")
.attr("d", line)
.attr("id", "myPath")
.attr("stroke", "black")
.attr("stroke-width", 5)
.attr("fill", "none") // Remove this part to color the
// shape this path produces
.on("mousemove", mMove)
.append("title");
function mMove(){
var m = d3.svg.mouse(this);
d3.select("#myPath").select("title").text(m[1]);
}
There was a little error in your answer.
d3.svg.mouse(this)
doesn't work.
The correct answer is
d3.mouse(this)

adding force directed algorithm to Raphael SVG objects

I am building a user interface using Raphael JS. currently I have a .js script that draws out everything using Raphael JS 2.1 exactly as needed. However, because the drawing is driven by dynamic data it is highly likely that objects will overlap. Adding the d3.js Force Layout to the objects would cause them to scatter automatically so there is no overlap of various ux components. However I have not been able to apply the d3.js Force Layout to Raphael drawn SVG objects.
I've created a basic example using JSFiddle here. I used the d3.js collision detection example as a "template".
I've fixed up your example and posted the result at http://jsfiddle.net/gn6tZ/6/. You had a minor typo in your collide function (- y instead of - r) and when you want to update the nodes after the force layout runs you need to supply the update function with the new data.
var nodes = circleHolder.nodes();
force.on("tick", function(e){
var q = d3.geom.quadtree( nodes ),
i = 0,
n = nodes.length;
while( ++i < n ) {
q.visit(collide( nodes[i]));
}
d3.selectAll('circle')
.data(nodes)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
d3
One of the examples: Force-Directed Graph

Resources