I'm having troubles in understanding how to get each D3 object in a selection to apply a transition.
Consider the follwoing code (here on jsfiddle):
var svg = d3.select('svg');
var dataSet = [10, 20, 30, 40];
var circle = svg.selectAll('circle')
.data(dataSet)
.enter()
.append('circle')
.attr("r",function(d){ return d })
.attr("cx", function(d, i){ return i * 100 + Math.random()*50 })
.attr("cy",50)
.attr("fill",'red')
;
circle.each(function(d,i) {
this
.transition()
.duration(1000)
.attr("cx",this.cx+100);
})
My use of this is wrong. I've also tried with d3.select(this) but I get the dom object corresponding to D3 object.
I'm unable to get the D3 object to apply transition.
The missing part is that you can supply a function to .attr('cx', function (d,i) { ... }) when using a transition, and inside that function you can access the cx attribute using this.getAttribute('cx').
Of course, you also want to make sure to turn it into a number using parseInt(), otherwise it will do string concatenation (because JS, sigh).
So change your final line to:
circle.transition().duration(1000).attr('cx', function(d, i) {
return parseInt(this.getAttribute('cx')) + 100;
});
Related
I am trying to write a transitioning bar graph that uses two CVS files. I know that both of the files are loading properly because it shows in the console that the first one loads with the page and the second one loads when you click the update button.
The only thing that I have really thought of trying was changing the svg select to group instead of selecting all rectangles incase there was something screwed up there.
This block is creating the svg element, bringing in the first CSV file, and appending the rectangles onto the chart. My only thought for what the problem could be is that it is inside a function, but if I take it out of the function how do I bind the data to them?
//Creating SVG Element
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3.select('#chart')
.append('svg')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand().range([chart_pad_x, chart_w -
chart_pad_x]).padding(0.2);
var y_scale = d3.scaleLinear().range([chart_pad_y, chart_h -
chart_pad_y]);
//Data-------------------------------------------------------------------
d3.csv('data.csv').then(function(data){
console.log(data);
generate(data); });
function generate(data){
//Scale domains
x_scale.domain(d3.extent(data, function(d){ return d }));
y_scale.domain([0, d3.max(data, function(d){ return d })]);
//Create Bars
svg.select('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i){
return x_scale(i);
})
.attr('y', function(d){
return y_scale(d);
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d){
return y_scale(d);
})
.attr('transform',
"translate(0,0)")
.attr('fill', '#03658C')
'''
The results I have experienced is a blank window with just the update button. As previously stated I know that the data is being generated because I can see it in the console.
Try using the following:
svg.selectAll('rect')
.data(data)
If you use svg.select this will only make the data binding with the first element found.
d3.select(selector): Selects the first element that matches the specified selector string. If no elements match the selector, returns an empty selection. If multiple elements match the selector, only the first matching element (in document order) will be selected. For example, to select the first anchor element:
This should be clear if you inspect the DOM nodes.
To fix the issue lets change some things in your code:
Lets create a dummy fetch function:
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
You are also using a scaleBand with an incomplete domain by using the extent function:
d3.extent(): Returns the minimum and maximum value in the given iterable using natural order. If the iterable contains no comparable values, returns [undefined, undefined]. An optional accessor function may be specified, which is equivalent to calling Array.from before computing the extent.
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
console.log(x_scale.domain()) // [min, max]
The scaleBand needs the whole domain to be mapped
Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
If we continue using that scale we will be only to get two values for our x scale. Lets fix that with the correct domain:
x_scale.domain(data);
Lastly use the selectAll to create the data bind:
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
This should do the trick.
Complete code:
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3
.select('#chart')
.append('svg')
.style('background', '#f9f9f9')
.style('border', '1px solid #cacaca')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand()
.range([chart_pad_x, chart_w - chart_pad_x])
.padding(0.2);
var y_scale = d3.scaleLinear()
.range([chart_pad_y, chart_h - chart_pad_y]);
//Data-------------------------------------------------------------------
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
function generate(data) {
console.log(d3.extent(data, function(d) { return d }));
//Scale domains
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
// Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
x_scale.domain(data);
y_scale.domain([0, d3.max(data, function(d) {
return d
})]);
//Create Bars
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
}
Working jsfiddle here
I am learning D3.js and curious on the chaining of methods
This script works:
var data = [32, 57, 112, 250]
var svg = d3.select("svg")
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30 })
.attr("r", function(d) { return Math.sqrt(d); })
But this script results in nothing:
var data = [32, 57, 112, 250]
var circles = d3.select("svg").selectAll("circle");
circles.data(data);
var circlesEnter = circles
.enter()
.append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30})
.attr("r", function (d) { return Math.sqrt(d)})
I don't see the different effects on these two different approaches. Can anyone tell me the difference between these?
Thanks in advance!
The issue is that selection.data() doesn't modify an existing selection, it returns a new selection:
[selection.data] Binds the specified array of data with the selected elements,
returning a new selection that represents the update selection: the
elements successfully bound to data. Also defines the enter and exit
selections on the returned selection, which can be used to add or
remove elements to correspond to the new data. (from the docs)
Also,
Selections are immutable. All selection methods that affect which
elements are selected (or their order) return a new selection rather
than modifying the current selection. However, note that elements are
necessarily mutable, as selections drive transformations of the
document! (link)
As is, circles contains an empty selection of circles (size: 0) with no associated data array. Because it is immutable, calling circles.data(data) won't change that selection, and circles.enter() will remain empty. Meanwhile the selection created by circles.data() is lost as it isn't assigned to a variable.
We can chain methods together as in the first code block of yours because the returned selection in the chain is a new selection when using .data(), .enter(), or selectAll(). Each method in the method chain uses the selection returned by the previous line, which is the correct one.
In order to break .data() from the chain, we would need to create a new intermediate selection with selection.data() to access the enter selection:
var circles = d3.select("svg").selectAll("circle");
var circlesData = circles.data(data);
var circlesEnter = circlesData
.enter()
...
var data = [32, 57, 112, 250]
var circles = d3.select("svg").selectAll("circle");
var circlesData = circles.data(data);
var circlesEnter = circlesData
.enter()
.append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30})
.attr("r", function (d) { return Math.sqrt(d)})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
But this would be a bit of an odd approach.
I have a scatter plot and a table. Each circle in the scatter plot has a corresponding row in the table. When I apply classes to the circles for CSS purposes, I also want to have that same class be assigned to the corresponding table row. They have the same data value, but are appended to separate elements.
Here is my circle class event:
my_circles.each(function(d,i) {
if (my_bool===true) {
d3.select(this).classed('selected',true);
//d3.selectAll('tr').filter(d===???)
}
});
I was trying to use a filter to select only the table rows of matching d value, but it didn't quite work out, I didn't know how to finish the line. Which got me thinking, maybe there is a better way, like the post title, assign classes to all elements bound to the same data.
If you have another solution aside from any of my ideas, that would be fine too.
Probably the easiest solution will be to check in the .classed() method for the tr selection, if the data bound to that tr matches the one for the selected circle.
my_circles.each(function(d,i) {
if (my_bool===true) {
d3.select(this).classed("selected",true);
d3.selectAll('tr')
.classed("selected", trData => d === trData); // Set class if data matches
}
});
This, however, is a bit clumsy and may be time-consuming because it will iterate over all trs each time this code is called. In case this is in an outer loop for handling multiple selected circles—as mentioned in your comment—things will get even worse.
D3 v4
For a slim approach I would prefer using D3's local variables, which are new to v4, to store the references between circles and table rows. This will require just a one-time setup which will depend on the rest of your code, but might go somewhat along the following lines:
// One-time setup
var tableRows = d3.local();
my_circles.each(function(d) {
var row = d3.selectAll("tr").filter(trData => d === trData);
tableRows.set(this, row); // Store row reference for this circle
});
This creates a new local variable tableRows which is used to store the reference to the corresponding table row for each circle. Later on you are then able to retrieve the reference to the row without the need for further iterations.
my_circles.each(function(d,i) {
if (my_bool===true) {
d3.select(this).classed('selected',true);
tableRows.get(this).classed("selected", true); // Use local variable to get row
}
});
D3 v3
If you are not yet using D3 there are, of course, other ways to achieve the same thing. Personally, I would prefer using a WeakMap to store the references. Because the API of the WeakMap also features get and set methods similar to d3.local, all you need to do is to change the line creating the local reference store while keeping the rest of the above code as is:
// var tableRows = d3.local();
var tableRows = new WeakMap(); // use a WeakMap to hold the references
You can use dataIndex for this purpose. Here is a code snippet for the same.
var data = ["A", "B", "C"];
var color = d3.scale.category10();
var container = d3.select("body")
.append("svg")
.attr("height", 500)
.attr("width", 500);
var my_circles = container.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("name", function(d, i) {
return "circle" + i
})
.attr("r", 10)
.attr("cx", function(d, i) {
return (i + 1) * 50
})
.attr("cy", function(d, i) {
return (i + 1) * 50
})
.style("fill", function(d, i) {
return color(i)
});
container.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("name", function(d, i) {
return "rect" + i
})
.attr("width", 15)
.attr("height", 15)
.attr("x", function(d, i) {
return i * 50 + 200
})
.attr("y", function(d, i) {
return (i + 1) * 50
})
.style("fill", function(d, i) {
return color(i)
});
my_circles.each(function(d, i) {
d3.select(this).classed("selected" + i, true);
container.selectAll("[name=rect" + i + "]").classed("selected" + i, true);
});
svg {
border: 1px solid black;
background: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I tried to set numbers as translate attributes of groups.
But, to set the numbers, I need to access data.
I found it's impossible to access data with function(d){}.
How to access data in .attr()?
var xCol = 'month'
var wraps = g.selectAll('.wrap').data(data);
wraps.enter().append('g')
.attr('class', 'wrap')
.attr('transform', 'translate('+function(d){return xScale(d[xCol])}()+', '+ (-margin.top)+')')
>>>index.js:132 Uncaught TypeError: Cannot read property 'month' of undefined
And I want to make several groups .wrap and draw bars in each groups
But, I have no idea to forward data to child elements.
var bars = wraps.selectAll('.bar').data(function(d){return d});
bars.enter().append('rect')
.attr('class', 'bar')
.attr('x', function(d){return xScale(d.d[xCol])})
...
I think you may just have the idea right but missing one small thing. The second argument to the attr function should be another function. Like below.
var svg = d3.select('svg');
var dataSet = [10, 20, 30, 40];
var circle = svg.selectAll('circle')
.data(dataSet)
.enter()
.append('circle')
.attr('r', function(d) {return d;})
.attr('cx', function(d,i) {return i * 100 + 50;})
.attr('cy', 50)
.attr('fill', 'red')
So the function then returns the result. This fiddle shows it in action
http://jsfiddle.net/bdkxgph5/1/
So in your case replace you attr call with
.attr('transform', function(d) { return 'translate('+xScale(d[xCol])+', '+ (-margin.top)+')'})
I'm new to D3, and spent already a few hours to find out anything about dealing with structured data, but without positive result.
I want to create a bar chart using data structure below.
Bars are drawn (horizontally), but only for user "jim".
var data = [{"user":"jim","scores":[40,20,30,24,18,40]},
{"user":"ray","scores":[24,20,30,41,12,34]}];
var chart = d3.select("div#charts").append("svg")
.data(data)
.attr("class","chart")
.attr("width",800)
.attr("height",350);
chart.selectAll("rect")
.data(function(d){return d3.values(d.scores);})
.enter().append("rect")
.attr("y", function(d,i){return i * 20;})
.attr("width",function(d){return d;})
.attr("height", 20);
Could anyone point what I did wrong?
When you join data to a selection via selection.data, the number of elements in your data array should match the number of elements in the selection. Your data array has two elements (for Jim and Ray), but the selection you are binding it to only has one SVG element. Are you trying to create multiple SVG elements, or put the score rects for both Jim and Ray in the same SVG element?
If you want to bind both data elements to the singular SVG element, you can wrap the data in another array:
var chart = d3.select("#charts").append("svg")
.data([data])
.attr("class", "chart")
…
Alternatively, use selection.datum, which binds data directly without computing a join:
var chart = d3.select("#charts").append("svg")
.datum(data)
.attr("class", "chart")
…
If you want to create multiple SVG elements for each person, then you'll need a data-join:
var chart = d3.select("#charts").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "chart")
…
A second problem is that you shouldn't use d3.values with an array; that function is for extracting the values of an object. Assuming you wanted one SVG element per person (so, two in this example), then the data for the rect is simply that person's associated scores:
var rect = chart.selectAll("rect")
.data(function(d) { return d.scores; })
.enter().append("rect")
…
If you haven't already, I recommend reading these tutorials:
Thinking with Joins
Nested Selections
This may clarify the nested aspect, in addition to mbostock's fine answer.
Your data has 2 degrees of nesting. You have an array of 2 objects, each has an array of ints. If you want your final image to reflect these differences, you need to do a join for each.
Here's one solution: Each user is represented by a group g element, with each score represented by a rect. You can do this a couple of ways: Either use datum on the svg, then an identity function on each g, or you can directly join the data on the g. Using data on the g is more typical, but here are both ways:
Using datum on the svg:
var chart = d3.select('body').append('svg')
.datum(data) // <---- datum
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(function(d){ return d; }) // <----- identity function
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Using data on the group (g) element:
var chart = d3.select('body').append('svg')
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(data) // <--- attach directly to the g
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Again, you don't have to create these g elements, but by doing so I can now represent the user scores differently (they have different y from the transform) and I can also give them different styles, like this:
.jim {
fill: red;
}
.ray {
fill: blue;
}