I cannot get the linkDistance and linkStrength based on link value functions to work for curved links, how and where am I supposed to specify these parameters?
I've made a JSFiddle based on the Curved Links example in which the code
.linkDistance(function(d) { return d.value; })
is written in two different places (and currently commented out because it doesn't work in either place). My best guess is that it belongs in the lower location, after force.links(links) is specified. I thought maybe that wasn't working because d is referring to the bilinks instead of the links, so I changed it to
.linkDistance(function(d) { return d[3]; })
in which d[3] is the link.value stored in the bilinks and that also doesn't work. Both versions return NaN errors.
I expected implementing this refinement to be easy and straightforward, so maybe it's just a simple and obvious thing I'm missing. But nothing I've tried and nothing I've found online has helped me make any progress for a few hours so hopefully somebody out there knows what's going on and how to fix it.
For both linkDistance and linkStrength function you will get the source and target node so depending on that, you can return the linkDistance value in this case i am returning weight of the target node:
var force = d3.layout.force()
.linkDistance(function(d) { return d.target.weight; })
.linkStrength(function(d) { console.log(d);return 2; })
In your case you doing
.linkDistance(function(d) { return d.value; })
//this is going to return undefined, as there is nothing like that
Working code here
EDIT
Since you need the link value in the link object add that value when you create the links like shown below:
var nodes = graph.nodes.slice(),
links = [],
bilinks = [];
graph.links.forEach(function (link) {
var s = nodes[link.source],
t = nodes[link.target],
i = {}, // intermediate node
linkValue = link.value // for transfering value from the links to the bilinks
;
nodes.push(i);
links.push({
source: s,
target: i,
linkValue: linkValue //add the link value
}, {
source: i,
target: t,
linkValue: linkValue//add the link value
});
bilinks.push([s, i, t, linkValue]);
});
so now in the linkdistance/linkStrength function you can get the value
var force = d3.layout.force()
.linkDistance(function (d) {
return d.linkValue;
})
.linkStrength(function (d) {
console.log(d.linkValue);
return d.linkValue;
})
Working code here
Hope this helps!
Related
I'm having some problem when i try to migrate my d3js graphs from version 3 to version 4. I have solved a lot o issue, but i'm not understandig how "merge" working. In this code data-title attribute is not setted in V4 and contextmenu is not working.
What's the problem? I don't understand how merge is working. Can someone explain me how to fix this code and why i have to fix it in this way since i have more graphs to fix.
var slice = self.svg.select(".slices").selectAll("path.slice")
.data(pie(new_node_data), key);
slice.enter()
.insert("path")
.attr("class", "slice").attr("fill", function(d2) { return color(d2.data.name);} )
.merge(slice) //<-- merge back in update selection
.transition().duration(1000).attrTween("d", tweenIn);
slice.attr('data-title',function(d){
return d.data.period + ' ' + d.data.name;
})
slice.on("contextmenu", function(d,i){
console.log(d.data);
self.context_menu(d.data, i, false);
});
self.attach_graph_items_click_event_handler(slice, false, true);
You are missing something simple, after your merge you don't save the combined update + enter back to a variable (you are missing the slice =):
slice = slice.enter() //<-- SAVE IT TO A VARIABLE
.insert("path")
.attr("class", "slice").attr("fill", function(d2) { return color(d2.data.name);} )
.merge(slice)
....
Here's it broken down with comments:
// this is your update selection
// slice is a selection of all the things being updated
// in your collection path.slice
var slice = self.svg.select(".slices").selectAll("path.slice")
.data(pie(new_node_data), key);
// .enter returns the enter selection
// all the things that are being added
slice = slice.enter()
.insert("path")
.attr("class", "slice").attr("fill", function(d2) { return color(d2.data.name);} )
// this merges the update slice with the enter slice
.merge(slice)
// this is now operating on enter + update
.transition().duration(1000).attrTween("d", tweenIn);
I am having problems setting my Y scale dynamically in this example. Would love if someone could help!
I am able to get the X axis setup perfectly, but I can't find how to refer to my Y data to define the extent of my axis. Right now I get the scale to go from -2 to 500, the part of the code that's not working is the commented out line.
Thanks for your time!
Plunk here:
https://plnkr.co/edit/eQ4HgxQC49CZIPVBXjQX
data.forEach(function(d) {
d["year"] = parseDate(d["year"])
});
var subset = data.filter(function(el) {
return el.metric === "rank"
});
var concentrations = productCategories.map(function(category) {
return {
category: category,
datapoints: subset.map(function(d) {
return {
date: d["year"],
concentration: +d[category]
}
})
}
})
xScale.domain(d3.extent(subset, function(d) {
return d["year"];
}));
yScale.domain([-2, 500]);
//yScale.domain(d3.extent(subset, function(d) {return d["rank"] })); //
Actually, i was finally able to solve that in a clean way:
yScale.domain([
d3.min(concentrations, function(c) { return d3.min(c.datapoints,function(v) { return v.concentration; }); }),
d3.max(concentrations, function(c) { return d3.max(c.datapoints, function(v) { return v.concentration; }); })
]);
But now I have another problem, since I want the Y scale to display the extremes of only one selected array. That means I have to filter while I parse the data. Not really sure how to do it, will probably open another question. Thank you so much #gerardofurtado for your time and patience!
Ok, I'll provide a temporary solution, because I'm sure there must be a shorter and cleaver way to do it.
First, let's extract all values that are not "year" from your data:
var values = productCategories.map(function(category){
return subset.map(function(d){
return +d[category]
})
});
Then, let's merge this array of arrays:
var merged = [].concat.apply([], values);
And, finally:
yScale.domain(d3.extent(merged));
I have the following issue:
I have small scenario -
Adding three interlinked nodes to the graph,
removing one using a filter function,
adding it back again
Once I re-add the removed one it gets a bit scrambled and stays in the corner. I'm sure I'm missing setting function somewhere or something. Please have a look at my jsFiddle and feel free to update it.
My add-node-back function
/* step 3: node B reappears with links */
function step3() {
var nB = {id: 'bbb'};
nodes.push(nB);
/* find exiting nodes for links */
var nA = nodes.filter(function(n) { return n.id === 'aaa'; })[0];
var nC = nodes.filter(function(n) { return n.id === 'ccc'; })[0];
var lAB = {source: nA, target: nB};
var lBC = {source: nB, target: nC};
links.push(lAB);
links.push(lBC);
recalc();
}
Thanks
The problem is the way you're removing the nodes and links. The following lines create new nodes and links arrays, shadowing the previous definitions:
nodes = nodes.filter(function(n) { return n.id !== 'bbb'; });
links = links.filter(function(l) { return (l.source.id !== 'bbb' && l.target.id !== 'bbb'); });
The old, now shadowed definitions are still used by the force layout internally. That is, positions for the (removed) node bbb are still being updated. You just can't see that because the overwritten definitions are used in the tick handler function.
Now when you add a new node and new links, the data structures used internally by the force layout (the old nodes and links) aren't updated, only the new ones, used by the tick handler function are. This means that while the new node is drawn, the force layout doesn't know about it and hence doesn't compute coordinates for it.
There are two ways of fixing this. As pointed out in the other answer, you can simply reassign nodes (and also links!) to the force layout when they change:
force.nodes(nodes);
force.links(links);
The disadvantage of this approach is that you lose the internal state of the force layout. This matters less in your particular case where the layout is fairly settled when you make the changes, but if you do this just after starting when the forces are still quite strong, you may experience some "jumpiness".
The alternative is to modify the data structures that are used by the force layout directly instead of reassigning:
function step2() {
links.splice(0, 1);
links.splice(1, 1);
nodes.splice(1, 1);
recalc();
}
Complete example here. I've hardcoded the indices of nodes and links to remove here to simplify, but you can obviously also compute them dynamically, as I've done in this demo.
I played with your fiddle. It appears that the newly added node has no x and y coordinates. That attributes are generated by the force layout. So I assigned the nodes to the force layout again. It fixes your problem. See here: JFiddle
/* step 3: node B reappears with links */
function step3() {
var nB = {id: 'bbb'};
nodes.push(nB);
/* find exiting nodes for links */
var nA = nodes.filter(function(n) { return n.id === 'aaa'; })[0];
var nC = nodes.filter(function(n) { return n.id === 'ccc'; })[0];
var lAB = {source: nA, target: nB};
var lBC = {source: nB, target: nC};
links.push(lAB);
links.push(lBC);
// I added this line
force.nodes(nodes);
recalc();
}
I am in a bit of a bind and need some with help with linking my svg elements with URL's contained in an CSV file. I have a symbols map with over 100 symbols. The symbols are based on coordinates pulled from longitude and latitude in a CSV file which also contains the links that I would like each unique symbol to link to. I know there is an easy way to do this, pretty sure I'm overlooking the solution.
My CSV file is as follows:
name,longitude,latitude,city,state,url
College of Charleston,803,342,Charleston,SC,http://sitename.com/colleges/college-of-charleston/
etc...
My symbols are generated using D3 and placed on top of my SVG map. I am also using D3 to wrap the symbols in anchor tags. I simply want these anchor tags to link to the appropriate url that correlates with the latitude and longitudes of that particular symbol.
/* Start SVG */
var width = 960,
height = 640.4,
positions = [],
centered;
var bodyNode = d3.select('#Map').node();
var list = $('.school-list').each(function(i){});
var svg = d3.select("#Map");
var contain = d3.select("#map-contain");
var circles = svg.append("svg:g")
.attr("id", "circles");
var g = d3.selectAll("g");
// var locationBySchools = {};
d3.csv("http://sitename.com/wp-content/themes/vibe/homepage/schools.csv",function(schools){
schools = schools.filter(function(schools){
var location = [+schools.longitude, +schools.latitude];
// locationBySchools[schools.loc] = location;
positions.push(location);
return true;
});
circles.selectAll("circles")
.data(schools)
.enter().append("svg:a")
.attr("xlink:href", function(d) { })
.append("svg:circle")
.attr("cx", function(d,i) {return positions[i][0]})
.attr("cy", function(d,i) {return positions[i][1]})
.attr("r", function(d,i) {return 6})
.attr("i", function(d,i) {return i})
.attr("class", "symbol")
Really stuck with this one...Any ideas?
The short answer is that you should simply return the url property from your data when you are assigning the xlink:href attribute:
.attr("xlink:href", function(d) { return d.url; })
However, there are a couple other issues with the code you posted.
Issue 1. circles.selectAll('circles')
This starts with a the selection of your g element, and within it, selects all elements with the tag-name circles. The problem is that circles is not a valid svg tag. This just creates an empty selection, which is okay in this case because the selection is only being used to create new elements. But, it's a bad habit to make dummy selections like this, and it can be confusing to others trying to understand your code. Instead, you should decide on a class name to give to each of the new link elements, and use that class name to make your selection. For example, if you decide to give them a class of link you would want to do the following:
First create a selection for all of the elements with class="link":
circles.selectAll('.link')
This selection will initially be empty, but when you use .data() to bind your data to it, it will be given an enter selection which you can use to create the new elements. Then you can add the class of link to the newly created elements:
.data(schools).enter().append('svg:a')
.attr('class', 'link')
Issue 2. .attr("i", function(d,i) {return i})
This one's pretty straightforward, there is no such attribute as i on svg elements. If you want to store arbitrary data on an element to be able to access it later, you can use a data attribute. In this case you might want to use something nice and semantic like data-index.
Issue 3. positions.push(location)
This is a big one. I would not recommend that you make a separate array to store the altered values from your dataset. You can use an accessor function in your d3.csv() function call, and clean up the incoming data that way. It will save you from having to maintain consistent data across two separate arrays. The accessor function will iterate over the dataset, taking as input the current datum, and should return an object representing the adjusted datum that will be used. This is a good spot to use your unary operator to coerce your latitude and longitude:
function accessor(d) {
return {
name: d.name,
longitude: +d.longitude,
latitude: +d.latitude,
city: d.city,
state: d.state,
url: d.url
};
}
There are two different ways to hook the accessor function into your d3.csv() call:
Method 1: Add a middle parameter to d3.csv() so that the parameters are (<url>, <accessor>, <callback>):
d3.csv('path/to/schools.csv', accessor, function(schools) {
// ...etc...
});
Method 2: Use the .row() method of d3.csv()
d3.csv('path/to/schools.csv')
.row(accessor)
.get(function(schools) {
// ...etc...
});
Now when you want to access the latitude and longitude in your preferred format, you can get them right from the bound data, instead of from an outside source. This keeps everything clean and consistent.
Putting all of this together, you get the following:
d3.csv('http://sitename.com/wp-content/themes/vibe/homepage/schools.csv')
// provide the accessor function
.row(function accessor(d) {
return {
name: d.name,
longitude: +d.longitude,
latitude: +d.latitude,
city: d.city,
state: d.state,
url: d.url
};
})
// provide a callback function
.get(function callback(schools) {
circles.selectAll('.link')
.data(schools)
.enter().append('svg:a')
.attr('class', 'link')
// point the link to the url from the data
.attr('xlink:href', function(d) { return d.url; })
.append('svg:circle')
.attr('class', 'symbol')
// now we can just use longitude and latitude
// since we cleaned them up in the accessor fn
.attr('cx', function(d) { return d.longitude; })
.attr('cy', function(d) { return d.latitude; })
// constants can be assigned directly
.attr('r', 6)
.attr('data-index', function(d,i) { return i; });
});
(D3 beginner here)
I have the following snippet:
// myShape (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
myShape = myShape.data(nodes, function(d) { return d.nodeId; });
// update existing nodes (reflexive & selected visual states)
myShape.selectAll('circle')
.style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.nodeType)).brighter().toString() : colors(d.nodeType); })
.classed('reflexive', function(d) { return d.reflexive; });
// add new nodes
var g = myShape.enter().append('svg:g');
g.append('svg:circle')
.attr('r', 12)
But I would like to make this more flexible: instead of using only circles, I would like to use circles and polygons. This will be selected in a property in d:
var d = [
{ nodeId: 1, nodeType : 'type1' , shape: 'circle' },
{ nodeId: 2, nodeType : 'type2' , shape: 'triangle' },
];
Which means that, depending on d.shape. I must set 'svg:circle' or 'svg:polygon', and then set the radius (for the circle) or the points (for the polygons). I have tried to set the svg shape like this:
g.append(function (d) {
if (d.shape === 'circle' ) { return 'svg:circle'; }
else { return 'svg:polygon'; } } )
But this is not working: I am getting a:
Uncaught Error: NotFoundError: DOM Exception 8
It seems append does not accept a function? How can I set the svg shape on a node-by-node basis?
EDIT
I have prepared this jsbin to show what I want to do.
In recent versions of D3, you can append elements that are the results of function calls. That is, instead of passing a static name, you can pass a function that evaluates to the element to add.
It doesn't quite work the way you're using it -- it's not enough to return the name of the element from the function. Instead of
g.append(function (d) { return svgShape(d); })
where svgShape returns a string, you need to do something like
g.append(function(d) {
return document.createElementNS("http://www.w3.org/2000/svg", svgShape(d));
})
and the corresponding shape will be created. I've updated your jsbin here to demonstrate.
In your case, it might be easier to always append a path and vary the line generator, i.e. the d attribute value. That is, for a circle you would pass in a function that returns a circular path, for a polygon a function that returns the particular polygon path etc.