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>
Related
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've this code of an interactive table made with d3 and it's running just fine. The only problem is that I want the second and third columns contents to be shown as percentage.
The csv file I'm working with look like this:
CSV
date,kind1,kind2,place
17/03/2014,0.28,0.46,NY
....
I suppose I need to use the map function again but I'm getting confused, any help?
var table = d3.select("body")
.append("table")
.attr("class", "table"),
thead = table.append("thead"),
tbody = table.append("tbody");
d3.csv("data.csv", function(error, data){
var columns = Object.keys(data[0])
var header = thead.append("tr")
.selectAll("th")
.data(columns)
.enter()
.append("th")
.text(function(d){ return d;});
var rows = tbody.selectAll("tr")
.data(data)
.enter()
.append("tr")
.on("mouseover", function(d){
d3.select(this)
.style("background-color", "orange");
})
.on("mouseout", function(d){
d3.select(this)
.style("background-color","transparent");
});
var cells = rows.selectAll("td ")
.data(function(row){
return columns.map(function(d, i){
return {i: d, value: row[d]};
});
})
.enter()
.append("td")
.html(function(d){ return d.value;});`
`
One way to achieve your goal would be to change the callback on the last line from this:
.html(function(d){ return d.value;})
to this:
.html(function(d,i){ if(i == 1 || i == 2) return (d.value*100) + '%'; return d.value; })
This takes advantage of the way d3 calls all functors with both the data and the index. Depending on your data there might be better ways than looking at the index.
Alternatively, you could add the percentage signs up front after reading the data:
data.forEach(function(d) { d[1] = (d[1]*100)+'%'; d[2] = (d[2]*100)+'%'; })
This approach limits your ability to do other calculations with the data at a later time.
I'd recommend changing the last line to:
.html(function(d){ return typeof(d.value)==="number"?(100*d.value).toFixed(1)+"%":d.value;});
This changes all the attributes which are of number type (in your case kind1 and kind2) to a percentage, with decimal precision given in the parameter of the .toFixed() call.
So I have a .csv file having multiple columns of data.
x,y,d1,d2,d3
28,77,1,2,3
27,78,4,5,6
21,79,2,7,9
10,80,5,7,8
I am able to create a scatter plot of d1, d2 & d3 on a single graph but what is required is: first d1 is plotted then it is removed and d2 is plotted & so on.
I know this can be achieved using separate function & setTimeout() for each data set but I have many data sets like this, so writing same code multiple times is not efficient.
Can somebody help me out in this?
EDIT:
So this is a part of code modified according to what #Lars suggested and it is working as I wanted!
var indices= d3.keys(mydata1[0])
.filter(function(d) { return (d !== "xaxis" && d!="yaxis"); }).sort();
indices.forEach(function(d, i) {
setTimeout(function() { update(d); }, 5000 * i);
});
function update(idx) {
p.selectAll("ellipse").remove(); //remove previous plot--to give animation like effect
p.selectAll(".R")
.attr("class", "ellipse")
.data(mydata1)
.enter()
.append("ellipse")
.attr("cx", function(d){ return scaleX(d["xaxis"]);})
.attr("cy", function(d){return scaleY(d["yaxis"]);})
.attr({
"rx": 3,
"ry": 4,
})
.attr("fill", function(d)
{
d[idx]=(d[idx]/2)+32;
for(i=0; i<64; i++)
{
if (d[idx]==0)
return mycolor[0];
else if(d[idx]>i && d[idx]<=(i+1))
return mycolor[i];
else if(d[idx]<0)
return "none";
}
});
}
You have basically two ways of doing this. First, setTimeout to update the part of the data that is referenced. This would look something like the following, assuming data holds your data.
function update(idx) {
svg.selectAll("circle")
.attr("cx", function(d) { return xScale(d[idx]); })
.attr("cy", function(d) { return yScale(d[idx]); });
}
var indices = ["d1", "d2", "d3"];
indices.forEach(function(d, i) {
setTimeout(function() { update(d); }, 1000 * i);
});
Alternatively, you can use D3's .transition() to effect the same thing. This is a bit awkward as it wasn't intended for this kind of thing. The idea is to create a set of dummy elements to drive the transitions.
svg.selectAll("dummy").data(indices).enter().append("dummy")
.transition().duration(1000).delay(function(d, i) { return i * 1000; })
.attr("foo", function(d) { update(d); });
I would recommend going with the approach that uses setTimeout.
I am trying to create several charts at once using d3 - code is here: http://jsfiddle.net/jgilfillan/W85ut/
I have an array bulletDataX of these objects:
function BulletObject(name, actual, target, ranges, bulletWidth) {
this.name = name;
this.actual = actual;
this.target = target;
this.ranges = ranges;
this.maxX = ranges[2];
this.bulletWidth = bulletWidth;
this.scale = d3.scale.linear().domain([0, this.maxX]).range([0, this.bulletWidth]);
this.xAxis = d3.svg.axis().scale(this.scale).orient("bottom");
}
this is the code I am trying to get working...
//axes??? not working
svg.selectAll(".xaxis")
.data(bulletDataX)
.enter()
.append("g")
.attr("id", function(d) { return d.name; })
.attr("class", "x axis")
.attr("transform", function(d, i) { return "translate(0, " + ((bulletHeight + bulletPadding) * i + .25 * bulletHeight).toString() + ")"; })
.call(function(d, i) { return d.xAxis; });
I know I have to fiddle with the transform attribute a bit but I can't even get the axis to display. I think the issue is with .call(function(d, i) { return d.xAxis; }) but I can't figure out how to get it to work. Any ideas would be much appreciated.
The call method doesn't provide you with d, i arguments. Instead, it:
Invokes the specified function once, passing in the current selection
along with any optional arguments.
See the API documentation: selection.call(function[, arguments…])
You can try using each instead. It:
Invokes the specified function for each element in the current
selection, passing in the current datum d and index i, with the this
context of the current DOM element.
See the API documentation: selection.each(function)
Here's a code example:
.each(function (d, i) {
return d.xAxis(d3.select(this));
});