I've prepared a small fiddle to illustrate the problem here
I'm having an issue using d3's exit function to remove elements from the dom.
Say I have an array of 10 elements:
var data = [1 ,4, 5, 6, 24, 8, 12, 1, 1, 20]
I use this data to create a simple horizontal bar chart using d3
d3.selectAll('rect')
.data(data)
.enter()
.attr("class", "rectangle")
.attr("stroke", "black")
.attr("stroke-width","1px")
.attr("fill","none")
.attr("x", 0)
.attr("y", function(d, i) { return 25 * i; } )
.attr("width", function(d) { return 22 * d; } )
.attr("height", "20");
Now after a short delay I'd like to prune my dataset so all that I have left is
var truncatedData = [4,5]
d3.selectAll('rect')
.data(truncatedData )
.exit()
.transition()
.delay(3000)
.remove();
Data is removed successfully but it still shows the first two elements 1,4 instead of 4,5.
How can I remove all but [4,5] from the dom?
By default, d3 matches elements given to .data() by their index and not value. That is, giving it a two-element array in the second call means that the first two elements from the first call (indices 0 and 1) are retained.
To fix this issue in your case, give a function to match elements to the second call to .data(), i.e.
.data([5, 6], function(d) { return(d); })
Fixed jsfiddle here.
Related
I adding two legends to my bar chart but I don't know why there is a extra legend appear. I don't know which part of my code is wrong since I only define two legend in my code.
var color_hash = { 0 : ["Male", "blue"],
1 : ["Female", "pink"]}
var legend = svg.append("g")
.attr("class", "legend")
.attr("x", width - 65)
.attr("y", 25)
.attr("height", 100)
.attr("width", 100);
legend.selectAll('g').data(data)
.enter()
.append('g')
.each(function(d, i) {
var g = d3.select(this);
g.append("rect")
.attr("x", width - 65)
.attr("y", i*25)
.attr("width", 10)
.attr("height", 10)
.style("fill", color_hash[String(i)][1]);
g.append("text")
.attr("x", width - 50)
.attr("y", i * 25 + 8)
.attr("height", 30)
.attr("width", 100)
.style("fill", color_hash[String(i)[1]])
.text(color_hash[String(i)][0]);
});
the black rectangle is the extra one:
With the enter/update/exit cycle in D3, you generally want to have a data array that contains one item for every element you want drawn. You have:
a color has object color_hash, this is what you really want to use to draw the legend, and
some data array data, though we don't know what is inside of this.
We are using data to visualize color_hash, this is not ideal.
For one, you only want to plot 2 elements, I can tell you that the length of data is at least 3:
You create an empty g with:
var legend = svg.append("g")
Then you select child g elements of that:
legend.selectAll('g')
Since there are none, this is an empty selection. Then you assign data to this selection and enter new HTML/SVG elements:
legend.selectAll('g')
.data(data)
.enter()
.append('g')
Since legend is an empty selection, the enter selection will create one HTML/SVG element for each item in the data array. After entering (and/or exiting), the number of HTML/SVG elements should be equal to the number of items in the data array. So, data must have at least 3 items in it (it could have more if additional elements are created, but they fall outside of the SVG/container bounds. This also explains why the third box has no color or text: the color hash has no values with key 2 or greater).
D3 is creates elements from data, generally in a one to one relationship between elements and items. To create our legend, the data array should be what we want to plot. As a consequence, we need to convert the color hash to an array:
var legendData = [
{name: "A", color:"crimson"},
{name: "B", color:"steelblue"}
];
Now we just supply that to selection.data()
And, since we are now binding the data we want to draw to the legend entries, we can also simplify the code, instead of:
.style("fill", color_hash[String(i)][1]);
and
.text(color_hash[String(i)][0]);
We can just use:
.style("fill",d.color);
and
.text(d.name);
This gives us:
var color_hash = { 0 : ["Male", "blue"],
1 : ["Female", "pink"]}
var width = 300;
var height = 200;
var svg = d3.select("svg")
.attr("width",width)
.attr("height",height);
var legendData = [
{name:"A",color:"crimson"},
{name:"B",color:"steelblue"}
]
var legend = svg.append("g")
legend.selectAll('g')
.data(legendData)
.enter()
.append('g')
.each(function(d, i) {
var g = d3.select(this);
g.append("rect")
.attr("x", width - 65)
.attr("y", i*25+25)
.attr("width", 10)
.attr("height", 10)
.style("fill", d.color);
g.append("text")
.attr("x", width - 50)
.attr("y", i * 25 + 33)
.attr("height", 30)
.attr("width", 100)
.style("fill", d.color)
.text(d.name);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I'm just focusing on the enter cycle here: there could be further revisions to placement and a different approach to nested appends can offer benefits compared to appending children with .each
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'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;
});
I have a 1000(rows)x100(columns) Matrix in a TSV file where each cell is an integer. I want to do an Scatterplot of the data, the X axis will be the rows (1000) and the columns the Y axis. Each value will be represented as a circle that will be bigger if the value is bigger.
At first I have tried to load the data with D3.js:
d3.tsv(Data_url, function(matrix_data) {
console.log((matrix_data));
}
And I'm getting just an unidimensional array of 1000 objects, I don't know why.
Furthermore I want to paint these data as explained before, so I need the row and column number, because they are data indeed. I mean, de 0 to 100 columns, are percent, and the 0 to 1000 rows are length so I will need something like:
.attr("cx", function (d) { return x(row_number); })
.attr("cy", function (d) { return y(column_number); })
.attr("r", function (d) { return r(d); });
But I can't find something to get the row_number and the column_number.
I have done another approach using 'Papaparse' to read the data and it works fine. Even using JSON this way:
matrix = JSON.parse(JSON.stringify(matrix_data));
I just want to understand how it should be done in D3.
Thanks in advance =)
Given matrix-like data of:
18 12 14 15 17 14 15 16 16 15 15 14
11 13 15 16 14 14 15 16 16 16 10 18
...
Here's a quick way to plot it:
// grad the data as text
d3.text("data.tsv", function(text) {
// parse the data, this will produce an array of arrays
// where the outer array is each row, the inner each column
var data = d3.tsv.parseRows(text);
// set your domains to be the lengths of your data with some padding
x.domain([-0.5, data.length + 0.5]);
y.domain([-0.5, data[0].length + 0.5]);
// we are going to use a nested selection
// the outer represents a row and is a svg g
var rows = svg.selectAll(".row")
.data(data)
.enter()
.append('g')
.attr('class', 'row');
// the inner selection is a col and contains the points
// which are circles
rows.selectAll('.point')
.data(function(d){
return d; //<-- return each point
})
.enter()
.append('circle')
.attr('class', 'point')
.attr('cx', function(d,i,j){
return x(j); //<-- the 'j' is the index of the row
})
.attr('cy', function(d,i,j){
return y(i); //<-- the 'i' is the index of the column
})
.attr('r', function(d,i,j){
return d; //<-- the d is the value in the matrix
})
.style('fill', 'steelblue');
Full working example is here.
I am just starting out with javascript and have a problem understanding this piece of (svg)code below which uses javascript for defining it's x co-ordinate and radius. I understand how data is bound etc. But my question is - For the function which takes two arguments : d and i, where is it defined that the first argument to the function is the dataset and the second is a counter for the circle, ie 0 for the first circle, 1 for the second and so on.
var dataset = [ 5, 10, 15, 20, 25 ];
var circles = svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
circles.attr("cx", function(d, i) {
return (i * 50) + 25;
})
.attr("cy", h/2)
.attr("r", function(d) {
return d;
});
Thanks.
This is d3 and so the d3 documentation defines what the function expects
If value is a constant, then all elements are given the same attribute value; otherwise, if value is a function, then the function is evaluated for each selected element (in order), being passed the current datum d and the current index i, with the this context as the current DOM element.