In d3.js v4, nested selections don't appear to be working as they had in the past.
This works (in v3):
var data = [["1-a", "1-b"], ["2-a", "2-b"]];
var tbody = d3.select("tbody");
var row = tbody.selectAll("tr").data(data);
row.exit().remove();
row.enter().append("tr");
var cell = row.selectAll("td").data(function(d){ return d;});
cell.exit().remove();
cell.enter().append("td");
cell.text(function(d){ return d; });
https://jsfiddle.net/nwozjscs/
But not in v4: https://jsfiddle.net/nwozjscs/1/
My sense is that this has something to do with the merge(...) changes, but I haven't been able to find an example of the proper way to write a nested selection in v4.
I think I figured it out. It appears to work correctly if you merge the enter and update selections into a single selection before joining the next layer of data. This way any new data as well as any existing data at the top level will be correctly taken into account at the next level down.
This makes total sense if you think about it. I think I was just too used to the magic of v3 to see the obvious.
Please comment if there is a better way to do this!
https://jsfiddle.net/nwozjscs/2/
function render(data){
var tbody = d3.select("tbody");
var row = tbody.selectAll("tr").data(data);
var rowenter = row.enter().append("tr");
var cell = row.merge(rowenter)
.selectAll("td").data(function(d){ return d;});
cell.enter().append("td").text(function(d){ return d; });
}
render([["1-a", "1-b"], ["2-a", "2-b"]]);
setTimeout(function(){
render([["1-a", "1-b", "1-c"], ["2-a", "2-b", "2-c"], ["3-a", "3-b", "3-c"]]);
}, 2000);
Related
I am trying to make amCharts V4 to show tooltip for the data item most close to the cursor.
The problem is that my chart is mostly oriented vertically. I was able to break amCharts V4 sample by updating the sample data to get a vertical chart: https://codepen.io/fvnever/pen/jOwyQEE
Here, the data tooltips aren't changing as I move my cursor over the Y axis, and I want them to be updated. I.e. on the following image, it should show the tooltip closest to the cursor from series 1, and it instead shows some item from the beginning of said series.
I've tried various suggestions from the documentation: setting and not setting chart.cursor.xAxis, chart.cursor.yAxis, chart.cursor.snapToSeries. Nothing seems to work.
There's no such feature out of the box, but it's possible to implement one. Two steps:
Make sure to disable the default tooltip behavior:
chart.cursor.snapToSeries = [];
Now, handle the 'cursorpositionchanged' event and control the tooltip there, something like the following.
Please note that this examples is very inefficient (it will iterate over all the chart nodes on every mouse move), so for any practical use, it would be better to store the data points in some sort of precalculated k-d tree.
chart.cursor.events.on('cursorpositionchanged', (e) => {
const cursor = e.target;
const cursorPoint = cursor.point;
let closestSeries = null;
let closestItem = null;
let minimalDistance = null;
for (const series of chart.series) {
for (const dataItem of series.dataItems) {
const dataPoint = dataItem.point;
const distance = Math.sqrt(Math.pow(cursorPoint.x - dataPoint.x, 2) + Math.pow(cursorPoint.y - dataPoint.y, 2));
if (minimalDistance === null || distance < minimalDistance) {
minimalDistance = distance;
closestItem = dataItem;
closestSeries = series;
}
}
}
for (const series of chart.series)
series.tooltip.disabled = series !== closestSeries;
if (closestItem)
closestSeries.showTooltipAtDataItem(closestItem);
});
Full example for testing: https://codepen.io/fvnever/pen/yLXbEKG
I am trying to update a stacked bar chart with transitions as the underlying data is changed. It calls the same "render" function each time and works well when no transitions are involved. However, I would like to animate the changes in values, transitioning from its current state to the next.
I have somewhat solved the problem, but feel like my solution is clunky - hoping there is a better way to do this for stacked bar charts.
My approach has been to do the following:
Load the data
Load the initial conditions (req. for transitions)
Load the final conditions (within a transition)
Copy the current data into another array: prevData
Reload data after interval
Using the above approach, if prevData has values, then use these to set the initial conditions. My problems is that finding and setting the initial conditions feels really clunky:
if (prevData.length > 0) {
//get the parent key so we know who's data we are now updating
var devKey = d3.select(this.parentNode).datum().key;
//find the data associated with its PREVIOUS value
var seriesData = seriesPrevData.find(function (s) { return (s.key == devKey); })
if (seriesData != null) {
//now find the date we are currently looking at
var day = seriesData.find(function (element) { return (element.data.Date.getTime() == d.data.Date.getTime()); });
if (day != null) {
//now set the value appropriately
//console.debug("prev height:" + devKey + ":" + day[1]);
return (y(day[0]) - y(day[1]));
}
}
}
All I'm doing, is finding the correct key array (created by d3.stack()), then trying to find the appropriate previous data entry (if it exists). However, searching parent nodes, and searching through arrays to find the required key and the appropriate data element feels very long-winded.
So, my question is, is there a better way to do this? or parts of this?
Find the previously bound data values associated with this element or the current values before it is changed within a function.
Better way to find the current key being updated rather than using: d3.select(this.parentNode)... ? I've tried passing key values but don't seem to be getting it right. The best I have achieved, is passing a key function to the parent, and looking for it the way described above.
Sorry for the long post, I just spent a whole day working out my solution, frustrated by the fact that all I really needed, was the previous values of an item. Having to do all these "gymnastics" to get what I needed seems very "un" D3.js like :-)
Thanks
Following is a simple example for an animated bar chart. It'll iterate over two different versions of the dataset to show how one can handle changes in the underlying data very easily with d3. There is no need (in this example) for any manual data preparation for the transition/animation.
var data = [
[1, 2, 3, 4, 5],
[1, 6, 5, 3]
];
var c = d3.select('#canvas');
var currentDataIndex = -1;
function updateData() {
// change the current data
currentDataIndex = ++currentDataIndex % data.length;
console.info('updating data, index:', currentDataIndex);
var currentData = data[currentDataIndex];
// get our elements and bind the current data to it
var rects = c.selectAll('div.rect').data(currentData);
// remove old items
rects.exit()
.transition()
.style('opacity', 0)
.remove();
// add new items and define their appearance
rects.enter()
.append('div')
.attr('class', 'rect')
.style('width', '0px');
// change new and existing items
rects
// will transition from the previous width to the current one
// for new items, they will transition from 0px to the current value
.transition()
.duration('1000')
.ease('circle')
.style('width', function (d) { return d * 50 + 'px'; });
}
// initially set the data
updateData();
// keep changing the data every 2 seconds
window.setInterval(updateData, 2000);
div.rect {
height: 40px;
background-color: red;
}
div#canvas {
padding: 20px;
border: 1px solid #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="canvas">
</div>
Using the d3js join model, is it possible to do conditional rendering based on the data content?
I want to do something like this:
var nodes = svg.selectAll('.node').data(nodes);
var node = nodes.enter().insert('svg:g').attr('class', 'node');
// if node.hasDuration {
node.insert('svg:rect');
//} else {
node.insert('svg:circle');
//}
nodes.exit().remove();
There doesn't seem to be a way using the join model (enter/exit) to have conditional rendering. I can brute force it with selection.each() but that seems to defeat the purpose of the selection model.
You could use a filter:
var nodes = svg.selectAll('.node').data(nodes);
nodes.enter()
.insert('svg:g')
.attr('class', 'node');
nodes.filter(function(d,i){
return d.hasDuration;
}).append('svg:rect');
nodes.filter(function(d,i){
return !d.hasDuration;
}).append('svg:circle');
Example here.
I'm trying to draw an area chart using dc.js, and the end date (i.e. far right) of the chart is based on the current date, not the last date in the dataset. In cases where there's a date gap between data points, I want the area to extend from one point to the next, not draw at 0.
Given this data:
var data = [
{domain: "foo.com", project: "pdp", repo: "myrepo", commit_date: "6/1/2014", lines_added: 100, lines_deleted: 50},
{domain: "foo.com", project: "pdp", repo: "myrepo", commit_date: "7/1/2014", lines_added: 100, lines_deleted: 50}
];
var ndx = crossfilter(data);
The chart's line/area currently ends at the "7/1/2014" data point, but I want it to stretch the entire length of the chart.
The relevant code for drawing the chart is:
var dateDim = ndx.dimension(function(d) {return d.commit_date;});
var minDate = dateDim.bottom(1)[0].commit_date;
var maxDate = new Date();
var domainGroup = dateDim.group().reduceSum(function(d) {return d.cumulative_lines;});
unshippedlineChart
.width(500).height(200)
.dimension(dateDim)
.group(domainGroup)
.renderArea(true)
.x(d3.time.scale().domain([minDate,maxDate]))
.brushOn(false)
.interpolate('step-after')
.yAxisLabel("Unshipped Value");
Full example is at http://jsfiddle.net/xayhkcvn/1/
You didn't actually ask a question :-), but I think you may be looking for ways to prefilter your data so that it gets extended to today, and to remove any zeros.
This stuff isn't built into dc.js, but there is some example code in the FAQ which may help. Specifically, there is a function remove_empty_bins which adapts a group to remove any zeros.
You could similarly define a function to add a final point (untested):
function duplicate_final_bin(source_group, key) {
return {
all:function () {
var ret = Array.prototype.slice.call(source_group.all()); // copy array
if(!ret.length) return ret;
ret.push({key: key, value: ret[ret.length-1].value});
return ret;
}
};
}
You can compose this with remove_empty_bins:
var super_group = duplicate_final_bin(remove_empty_bins(domainGroup), maxDate);
The idea is to create a wrapper object which dynamically adds or remove stuff from the (always changing) source_group.all() on demand. dc.js will call group.all() whenever it is redrawing, and these wrappers intercept that call and adapt the data the crossfilter group returns.
i am trying to update a line graph and it is not throwing any error but it is also not updating the graph.
i am deleting a point and adding a new one with an incremented rate and incremented created_at date by a second(trying to follow http://bl.ocks.org/benjchristensen/1148374)
function redrawWithoutAnimation() {
for (var i in chart_data) {
linedata = chart_data[i];
//delete first element of array
linedata.points.reverse().shift();
//create a new point
rate = linedata.points[0].rate + 1;
created_at = linedata.points[0].created_at + 6000;
new_point = {};
new_point.rate = rate;
new_point.created_at = created_at;
linedata.points.push(new_point);
console.log(linedata);
}
// static update without animation
svg.selectAll("path")
.data([linedata.points]); // set the new data
line(linedata.points); // apply the new data values
}
redrawWithoutAnimation();
setInterval(function () {
redrawWithoutAnimation();
}, 8000);
here is my code
http://jsfiddle.net/yr2Nw/8/
Working fiddle: http://jsfiddle.net/reblace/GsaGb/1
There's a few issues here...
First, you were updating all the chart_data in the for loop, but outside the loop, you were only trying to update the line still stored in the linedata variable after loop execution. You should try to avoid having variables with greater scope than they need. It can lead to bugs like this one:
svg.selectAll("path").data([linedata.points]);
line(linedata.points);
You should instead use D3's data joining to rejoin the new data to all the paths at once declaratively like so:
linesGroup.selectAll("path")
.data(chart_data)
.attr("d", function(d){ return line(d.points); });
What that code's doing is it's selecting the paths and then joining each of them to the chart_data elements and then binding the appropriate line generator to the "d" attribute for the appropriate path.
Then, you need to update your x axis and y axis otherwise the plot will just shoot off the drawn area. This code is updating the domains and then rebinding the axes to the dom elements so they redraw:
xAxis.scale().domain([
d3.min(chart_data, function (c) { return d3.min(c.points, function (v) { return v.created_at; }); }),
d3.max(chart_data, function (c) { return d3.max(c.points, function (v) { return v.created_at; }); })
]);
yAxis.scale().domain([
0,
d3.max(chart_data, function (c) { return d3.max(c.points, function (v) { return v.rate; }); })
]);
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
There were a few other bugs I fixed them in the Fiddle. For example, you need to calculate the time for the new point based on the last element in the array, not the first, otherwise the line can't interpolate properly since its no longer a continuous function... and this is a bit more concise way to do your line updates:
for (var i=0; i<chart_data.length; i++) {
linedata = chart_data[i];
//delete first element of array
var removedPoint = linedata.points.shift();
//create a new point
var lastpoint = linedata.points[linedata.points.length-1];
var new_point = {
rate: removedPoint.rate,
created_at: lastpoint.created_at + 6000
};
linedata.points.push(new_point);
}
Also note that you shouldn't use the for(var in) loop for Arrays, that's for iterating over the properties in an object.
There's still some issues, but I think this should help get you over the hurdle you were stuck on. Anyways, it looks cool in action!
Fine fenac.. You facing so many problems since your data is not in good format for your requirements..
as per http://bl.ocks.org/benjchristensen/1148374 The x-axis data must be (data[] (data array))
Your data is something like this
[objects,object,object] where each object holds one element of xaxis value.. so the pushing and shifting is not possible..
try to change the format of the data (linedata.points) to an array (data[]) and try it out sure it works..
You just need to put all the values in linedata.points into an array data[] and use this data[] to animate your line..
Since yours the multiline.. you need to create 2D array and must pass them accordingly...
Cheers..
I updated your jsfiddle
setInterval(function () {
console.log(linedata.points);
var v = linedata.points.shift(); // remove the first element of the array
linedata.points.push(v); // add a new element to the array (we're just taking the number we just shifted off the front and appending to the end)
redrawWithoutAnimation();
}, 3000);
http://jsfiddle.net/yr2Nw/9/
But still it wont works till you do that work...
Personal Suggestion: First Try with single line graph then go with looping for multiline...