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.
Related
I'm working on a bar chart that updates its data based on the mouseover of another element. When the chart updates, if there are less bars in the new chart, the chart permanently has fewer bars and changing the data back does not add them back in. I've added a gif to show this - when it gets down to 3 bars, they never come back.
Here's my code:
var scatter_versus_dataset; // the main set
var scatter_versus_dataset_filtered;
// set versus y scale
scatter_versus_y = d3.scaleBand().range([0, SCATTER_VERSUS_HEIGHT])
// set versus x scale
scatter_versus_x_fatal = d3.scaleLinear().range([0, SCATTER_VERSUS_WIDTH / 3]);
scatter_versus_x_nonfatal = d3.scaleLinear().range([-1 * SCATTER_VERSUS_WIDTH / 3, 0 ])
// set the versus colors
scatter_versus_z = d3.scaleOrdinal().range(STACK_COLOURS);
...
function updateScatterVersus(code){
// filter the set
scatter_versus_dataset_filtered = scatter_versus_dataset.filter(function (d) { return (d.majorOccCodeGroup == code) })
scatter_versus_y.domain(scatter_versus_dataset_filtered.map(function (d) { return d.occupation; })).padding(BAR_PADDING);
scatter_versus_x_fatal.domain([0, d3.max(scatter_versus_dataset_filtered, function (d) { return d.f_total_rate; })]).nice();
scatter_versus_x_nonfatal.domain([d3.min(scatter_versus_dataset_filtered, function (d) { return +-1 * d.nf_total_rate; }), 0]).nice();
var bars = d3.selectAll("#scatter_versus_fatal_rect")
.data(scatter_versus_dataset_filtered)
bars.exit()
.remove()
bars.transition()
.duration(600)
.attr("y", function (d) {
return scatter_versus_y(d.occupation);
})
.attr("x", function (d) {
return scatter_versus_x_fatal(0) + SCATTER_VERSUS_GAP_HALF;
})
.attr("width", function (d) {
return scatter_versus_x_fatal(d.f_total_rate);
})
.attr("height", scatter_versus_y.bandwidth())
bars.enter()
.append("rect")
.attr('id', 'scatter_versus_fatal_rect')
.classed("bar", true)
.attr("y", function (d) {
return scatter_versus_y(d.occupation);
})
.attr("x", function (d) {
return scatter_versus_x_fatal(0) + SCATTER_VERSUS_GAP_HALF;
})
.attr("width", function (d) {
return scatter_versus_x_fatal(d.f_total_rate);
})
.attr("height", scatter_versus_y.bandwidth())
}
The process for redrawing the other side of the chart is exactly the same. The problem is still there if i only draw one of the sides.
The data is just from a csv, and I don't think it's the problem - the filtered set has the right number of entries and it's fine in other charts. It's probably something to do with the removal and redrawing but I can't find many examples of this. Or perhaps a key? I can upload some data if needed but it's a pretty big CSV.
id in HTML is unique, only 1 tag should have it.
Select the div for the bars, then selectAll tags with class is bar and bind data.
Remove the id you add to the rects.
var bars = d3.select("#scatter_versus_fatal_rect")
.selectAll(".bar")
.data(scatter_versus_dataset_filtered);
bars.enter()
.append("rect")
// .attr('id', 'scatter_versus_fatal_rect')
.classed("bar", true)
......
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>
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 am using the D3 library to create a Zoomable Treemap for my application data using Javascript and JSON. I see online that many times d3 category for Color is being used to determine the colors of each section. However, I wish to color the sections of treemap using my application logic. Like below:
If conditionA
color = red
If conditionB
color=green
....
Is there any way to achieve this.. Check the values of my JSON Object and set the color of a section only on the basis of some conditions; and have all other sections set to a default color?
Your question is a little vague (next time include some code!), but in general, say you have data like this:
var data = [{
conditionA: true,
conditionB: false
}, {
conditionA: false,
conditionB: true
}, {
conditionA: false,
conditionB: false
}];
then it's as simple as:
svg.selectAll('.SomeCircles')
.data(data)
.enter()
.append('circle')
.attr('r', 20)
.attr('cx', function(d, i) {
return i * 25 + 25;
})
.attr('cy', function(d, i) {
return i * 25 + 25;
})
.attr('class', 'SomeCircles')
.style('fill', function(d) { //<-- filling based on an attribute of my data
if (d.conditionA) {
return 'red';
} else if (d.conditionB) {
return 'green';
} else {
return 'blue';
}
});
Here is an example.
I'm trying to create multiple lines on a line graph one at a time. I've created an object array of about 100 lines in the below format:
var allLines = [{type: "linear", values: [1000, 2000, 3000]}, {}, ... {}];
var line = d3.svg.line()
.defined(function (d) {
return d != null;
})
.x(function (d, i) {
return x(new Date(minYear + i, 1, 1));
})
.y(function (d) {
return y(d);
});
Now I want to draw each line, one at a time with a delay of about 250 milliseconds between each line. I've tried the below approach which I thought would work, but I must be missing something because it just waits 250ms and then draws all the lines.
svg.append('g')
.attr('class', 'lineGroup')
.selectAll('path')
.data(allLines)
.enter()
.append('path')
.attr('class', function (d) {
return d.type;
})
.style('visibility', 'hidden')
.attr('d', function (d) {
return line(d.values);
});
function foo(transition) {
transition
.style('visibility', 'visible');
}
d3.select('.lineGroup').selectAll('path').transition().delay(250).call(foo);
Your basic approach is right, you just need to adjust the delay dynamically such that the later lines are drawn later. At the moment, the delay applies to all lines. To make it dynamic, you can use something like
svg.append("g")
// etc
.transition()
.delay(function(d, i) { return i * 250; })
.style('visibility', 'visible');
You can also do everything in one call, no need for a separate one after creating the lines.