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; });
Related
I have a scatter plot which uses three different CSV's. The following is the Javascript that controls the dots used for one of them:
svg.append('g')
.selectAll("dot")
.data(files[2])
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.Index); } )
.attr("cy", function (d) { return y(d.Value); } )
.attr("r", 4)
.style("fill", "#d3d3d3")
Currently, the dots from this CSV are light grey. However, how could I make the first point listed in this CSV, or any other I want, a different color? Thanks in advance.
Welcome to StackOverflow!
In order to achieve what you want, you could use a callback in the fill style, just like you are doing in cx and cy, but adding a second parameter i, this callback's second parameter is the index (zero based), then you can conditionally return one value or another. Something like this:
svg.append('g')
.selectAll("dot")
.data(files[2])
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.Index); } )
.attr("cy", function (d) { return y(d.Value); } )
.attr("r", 4)
.style("fill", function (d, i) {
if(i === 0) {
return "#f05050"
} else {
return "#d3d3d3"
}
})
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.
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)
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 trying to do something like this: http://bost.ocks.org/mike/nations/
However instead of the transitions on mouseover I want the transitions to display when I click on a button for each year in the timeline.
Some example data in a csv file:
time,name,xAxis,yAxis,radius,color
1990,America,10,20.2,30,black
1990,China,50,50,50,yellow
2000,Singapore,20,30,20,red
2010,China,60,50,50,yellow
2020,America,20,30,40,black
2020,Malaysia,60,5,10,orange
I'm new to javascript and d3 and am having trouble with the transitions. I want the circles to be unique to each name (America, China, Singapore, Malaysia) so that I will only have one circle per name. Currently new circles add when I click on the respective timeline buttons, but don't transit to new positions or exit.
Read data using d3.csv:
d3.csv("data.csv", function(dataset) {
var years = [];
data=dataset;
//create a button for each year in the timeline
dataset.forEach(function(d){
console.log(d.time);
//if not existing button for timeline
if($.inArray(d.time, years) == -1)
{
var button = document.createElement("button");
button.setAttribute("type", "button");
button.setAttribute("class", "btn btn-default");
button.setAttribute('onclick', 'update("'+d.time+'")');
var t = document.createTextNode(d.time);
button.appendChild(t);
$("#timeline").append(button);
years.push(d.time);
}
})
//create circles for the first year
svg.selectAll("circles")
.data(dataset.filter(function(d) { return d.time == d3.min(years);}, function(d) { return d.name; }))
.enter()
.append("circle")
//.filter(function(d){ return d.time == d3.min(years); })
.attr("cx", function (d) { return d.xAxis *10; })
.attr("cy", function (d) { return d.yAxis; })
.style("fill", function(d) { return d.color; })
.transition()
.duration(800)
.attr("r", function(d) { return d.radius});
});
My update function:
function update(year){
var circle = svg.selectAll("circles")
.data(data.filter(function(d){return d.time == year;}), function(d) { return d.name; });
//update
circle.attr("class", "update")
.filter(function(d){ return d.time == year; })
.transition()
.duration(800)
.attr("cx", function (d) { return d.xAxis *10; })
.attr("cy", function (d) { return d.yAxis; })
.attr("r", function(d) { return d.radius});
//enter
circle.enter().append("circle")
.filter(function(d){ return d.time == year; })
.attr("cx", function (d) { return d.xAxis *10; })
.attr("cy", function (d) { return d.yAxis; })
.style("fill", function(d) { return d.color; })
.attr("r", function(d) { return d.radius});
//exit
circle.exit()
.remove();
}
Can someone point me in the right direction? Thanks.
svg.selectAll("circles") is invalid and should become svg.selectAll("circle") (singularize "circles").
As you have it currently, with "circles", it yields an empty selection, so d3 assumes all your data is bound to non-existent circles, and therefore the .enter() selection is always full (rather than being full only at the first render).
Next, in the section labled //update, you shouldn't need to do any filtering. The .data() binding you're doing to a filtered array should take care of this for you.
Also, the section labeled //create circles for the first year is unnecessary, and probably should be removed to eliminate side effect bugs. The update() function, assuming it's working fine, should take care of this for you.