I want to link x and y attributes of nodes with my Ember.js Model.
var nodes = this.get('store').findAll('node').then(function (nodes) {
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
.attr("r", 7)
.style("fill", function (d) {
return fill(1);
})
.style("stroke", function (d, i) {
return d3.rgb(fill(i)).darker(2);
});
});
But I'm getting Uncaught TypeError: Cannot read property 'x' of undefined error in the console. I have x and y value defined in the model. Maybe I'm doing it completely wrong.
Basically all I want to do is use the x and y values from the model in the d3 plot.
DS.Store.findAll returns a promise, you will have to wait for it to resolve before using it.
this.get('store')
.findAll('node')
.then(nodes => {
let node = svg.selectAll(".node") // rest of your code
})
You will also need to use Ember.get to access the properties defined on the model
Suppose d3 chart required format is [{'x':'xvalue','y':'yvalue'}]. then you need transform nodes result to new array of objects..
var d3ExpectedFormat = nodes.map(function(node){
return {'x':node.get('x'),'y':node.get('y')};
});
instead of .data(nodes) try .data(d3ExpectedFormat)
Related
I have the following code, where I read a file, and then use d3.map to get the unique user_id values to create column labels of a heat map:
d3.tsv("data_heatmap.tsv", function(d) {
console.log(d.user_id);
return {
col : +d.u_id,
row : +d.d_id,
value : +d.count,
user_id: +d.user_id,
};
var colLabels = svg.append("g").selectAll(".colLabelg")
.data(d3.map(data, function(d) {return d.user_id;}.keys())
.enter().append("text")
.text(function(d) {return d;}
)
}
This code works fine, but I need to use the col variable to change the positions of the .colLabelg elements created. I tried the following but it does not work:
var colLabels = svg.append("g").selectAll(".colLabelg")
.data(d3.map(data, function(d) {return d.user_id;}.keys())
.enter().append("text")
.text(function(d) {return d;}
).data(d3.map(data, function(d){return d.u_id;}).keys())
.enter().attr("x", 0).attr("y", function(d) {
return d * cellSize;
})
But, it gives an error:
What would be the solution to this problem?
For example, I need to calculate a Math.sqrt of my data for each attr, how can I calculate only one time the Math.sqrt(d)?
var circle = svgContainer.data(dataJson).append("ellipse")
.attr("cx", function(d) {
return Math.sqrt(d) + 1
})
.attr("cy", function(d) {
return Math.sqrt(d) + 2
})
.attr("rx", function(d) {
return Math.sqrt(d) + 3
})
.attr("ry", function(d) {
return Math.sqrt(d) + 4
});
Has any elegant/performative mode? I'm thinking this way:
var aux;
var circle = svgContainer.data(dataJson).append("ellipse")
.attr("cx", function(d) {
aux = Math.sqrt(d);
return aux + 1
})
.attr("cy", function(d) {
return aux + 2
})
.attr("rx", function(d) {
return aux + 3
})
.attr("ry", function(d) {
return aux + 4
});
An underestimated feature of D3 is the concept of local variables which were introduced with version 4. These variables allow you to store information on a node (that is the reason why it is called local) independent of the data which might have been bound to that node. You don't have to bloat your data to store additional information.
D3 locals allow you to define local state independent of data.
Probably the major advantage of using local variables over other approaches is the fact that it smoothly fits into the classic D3 approach; there is no need to introduce another loop whereby keeping the code clean.
Using local variables to just store a pre-calculated value is probably the simplest use case one can imagine. On the other hand, it perfectly illustrates what D3's local variables are all about: Store some complex information, which might require heavy lifting to create, locally on a node, and retrieve it for later use further on in your code.
Shamelessly copying over and adapting the code from Gerardo's answer the solution can be implemented like this:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100);
var roots = d3.local(); // This is the instance where our square roots will be stored
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.attr("cx", function(d) {
return roots.set(this, Math.sqrt(d)) * 3; // Calculate and store the square root
})
.attr("cy", function(d) {
return roots.get(this) * 3; // Retrieve the previously stored root
})
.attr("rx", function(d) {
return roots.get(this) + 3; // Retrieve the previously stored root
})
.attr("ry", function(d) {
return roots.get(this) + 4; // Retrieve the previously stored root
});
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
Probably, the most idiomatic way for doing this in D3 is using selection.each, which:
Invokes the specified function for each selected element, in order, being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element (nodes[i]).
So, in your case:
circle.each(function(d){
//calculates the value just once for each datum:
var squareRoot = Math.sqrt(d)
//now use that value in the DOM element, which is 'this':
d3.select(this).attr("cx", squareRoot)
.attr("cy", squareRoot)
//etc...
});
Here is a demo:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100);
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.each(function(d) {
var squareRoot = Math.sqrt(d);
d3.select(this)
.attr("cx", function(d) {
return squareRoot * 3
})
.attr("cy", function(d) {
return squareRoot * 3
})
.attr("rx", function(d) {
return squareRoot + 3
})
.attr("ry", function(d) {
return squareRoot + 4
});
})
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
Another common approach in D3 codes is setting a new data property in the first attr method, and retrieving it latter:
.attr("cx", function(d) {
//set a new property here
d.squareRoot = Math.sqrt(d.value);
return d.squareRoot * 3
})
.attr("cy", function(d) {
//retrieve it here
return d.squareRoot * 3
})
//etc...
That way you also perform the calculation only once per element.
Here is the demo:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100).map(function(d) {
return {
value: d
}
});
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.attr("cx", function(d) {
d.squareRoot = Math.sqrt(d.value);
return d.squareRoot * 3
})
.attr("cy", function(d) {
return d.squareRoot * 3
})
.attr("rx", function(d) {
return d.squareRoot + 3
})
.attr("ry", function(d) {
return d.squareRoot + 4
});
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: by the way, your solution with var aux will not work. Try it and you'll see.
UPDATE: So, I was able to get the data into my cx/cy data properly (spits out the correct values), but the element gives me an error of NaN.
So I get 3 circle elements with only the radius defined.
Original post:
I have a question about rendering dots in on a d3 line chart. I will try to give every part of the relevant code (and the data structure, which is slightly more complicated).
So, currently, 1 black dot renders in the top left corner of my chart, but nowhere else. It looks like I am getting 3 constants when I console.log the cx and cy return functions. What am I doing wrong?
Cities is currently an array that returns 3 objects.
Each object has the following structure:
Object {
name: 'string',
values: array[objects]
}
values array is as follows:
objects {
Index: number,
Time: date in a particular format
}
Okay. Relevant code time:
var points = svg.selectAll('dot')
.data(cities);
console.log('Points is :', points)
points
.enter().append('circle')
// .data(function(d) {console.log("data d is :", d); return d})
.data(cities)
.attr('cx', function(d) {
return x(new Date(d.values.forEach(function(c) {
console.log("cx is: ", c.Time);
return c.Time;
})))})
.attr('cy', function(d) {
return y(d.values.forEach(function(c) {
console.log("cy is: ", c.Index)
return c.Index;
}))
})
.attr('r', dotRadius());
points.exit().remove();
// points.attr('class', function(d,i) { return 'point point-' + i });
d3.transition(points)
.attr('cx', function(d) {
return x(new Date(d.values.forEach(function(c) {
console.log("cx is: ", c.Time);
return c.Time;
})))})
.attr('cy', function(d) {
return y(d.values.forEach(function(c) {
console.log("cy is: ", c.Index)
return c.Index;
}))
})
.attr('r', dotRadius())
You need a nested selection here.
This:
.attr('cx', function(d) {
return x(new Date(d.values.forEach(function(c) {
console.log("cx is: ", c.Time);
return c.Time;
})))})
is totally invalid. One, attr is expecting a single value to set, you are trying to get it to process an array of values. Two, forEach by design only returns undefined. Just not going to work.
You should be doing something like this:
var g = svg.selectAll(".groupOfPoint") //<-- top level selection
.data(cities)
.enter().append("g")
.attr("class", "groupOfPoint");
g.selectAll(".point") //<-- this is the nested selection
.data(function(d){
return d.values; //<-- we are doing a circle for each value
})
.enter().append("circle")
.attr("class", "point")
.attr('cx', function(d){
return x(d.Time);
})
.attr('cy', function(d){
return y(d.Index);
})
.attr('r', 5)
.style('fill', function(d,i,j){
return color(j);
});
Since you seem to be building off of this example, I've modified it here to be a scatter plot instead of line.
I'm having a bit of trouble getting a something to work with D3.js. Namely, I'm trying to make a tree of nodes, using the basic code from http://bl.ocks.org/mbostock/1021953.
I switched it to load the data inline, as opposed to loading from file, because I'm using it with a Rails application and don't want to have repetitive information. I switched the line so that you could see the format of my data.
Anyways, here's the bulk of my code:
<%= javascript_tag do %>
var nodes = [{"title":"Duncan's Death","id":"265"},{"title":"Nature Thrown Off","id":"266"},{"title":"Cows Dead","id":"267"},{"title":"Weather Bad","id":"268"},{"title":"Lighting kills man","id":"269"},{"title":"Macbeth's Rise","id":"270"}];
var links = [{"source":"265","target":"266","weight":"1"},{"source":"266","target":"267","weight":"1"},{"source":"266","target":"268","weight":"1"},{"source":"268","target":"269","weight":"1"}];
var firstelement = +links[0].source;
links.forEach(function(l) {
l.source = +l.source;
l.source = l.source-firstelement;
l.target = +l.target
l.target = l.target-firstelement;
});
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-1000)
.linkDistance(300)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
force
.nodes(nodes)
.links(links)
.start();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.weight); });
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", "circle_node")
.attr("r", 50)
.style("fill", function(d) { return color(d.id); })
node.append("title")
.text(function(d) { return d.title; });
node.append("text")
.attr("x", function(d) { return d.x; } )
.attr("y", function(d) { return d.y; })
.text(function(d) { return d.title; });
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
});
<% end %>
This seems like it should work to me, but I can seem to manage it. The links work, but the nodes all remain in the top left corner. I've tried just entering the circles directly and appending the text to them (staying close to the source code I listed above,) but while the circles behave properly, it doesn't display the text. I'd like the title to be centered in the nodes.
More generally, I'm kind of confused by how this is working. What does "d" refer to within lines like
function(d) { return d.source.x; }
It seems to be declaring a function and calling it simultaneously. I know that it doesn't have to be specifically the character "d," (for instance, switching the "d" to an "a" seems to make no difference as long as it's done both in the declaration and within the function.) But what is it referring to? The data entered into the object that's being modified? For instance, if I wanted to print that out, (outside of the attribute,) how would I do it?
Sorry, I'm new to D3 (and fairly new to JavaScript in general,) so I have a feeling the answer is obvious, but I've been looking it up and through tutorials and I'm still lost. Thanks in advance.
First, there's a simple problem with your code that is causing all your nodes to stay in the top left corner. You are trying to position each node using the code:
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
However, node is a selection of gs which do not take x and y attributes. Instead, you can move each node using translate transform, e.g.
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
Making this change should allow the nodes to move around.
Next, moving to your question about "d", I think the first thing you need to understand is what you can do with a selection of elements in D3. From the docs: a selection (such as nodes) "is an array of elements pulled from the current document." Once you have a selection of elements, you can apply operators to change the attributes or style of the elements. You can also bind data to each element.
In your case, you are binding data to a selection of gs (nodes):
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
You are then using attr to change the position of each node. However, instead of setting the x and y attributes of each element to the same value, you are passing attr an anonymous function that will return a (presumably different) position for each node:
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
This behavior is also explained in the docs for attr:
Attribute values and such are specified as either constants or
functions; the latter are evaluated for each element.
Thus, d represents an individual element (Object) in nodes.
So going back to your code, on each tick two things are happening:
The position of each node (data) is being recalculated by force.
Each corresponding element is then being moved to its new location by the anonymous function you pass to force.on.
I am trying to understand the D3.js code for this example and am confused by this code:
var circle = interpolation.selectAll("circle")
.data(Object);
circle.enter().append("circle")
.attr("r", 4)
.attr("fill","yellow");
circle
.attr("cx", function y(d) { console.log(d.attr("class")); return d.x; })
.attr("cy", function(d) { return d.y; });
What does the second line of this code actually do? What data does it bind to?
The data bound in the element above that is given by the function getLevels(d, t), where d is a number of range 2 - 4 and t is a number derived from the current time.
This only ever returns an array of arrays. Because an array is already of type Object, Calling Object() on an Array returns the original array.. Therefore, from what I can see, the author is simply using Object as a kind of identity function, similar to:
var identity = function(d){
return d;
}
var circle = interpolation.selectAll("circle")
.data(identity);
circle.enter().append("circle")
.attr("r", 4)
.attr("fill","yellow");
circle
.attr("cx", function y(d) { console.log(d.attr("class")); return d.x; })
.attr("cy", function(d) { return d.y; });