I'm having a problem accessing data that I've nested in D3. I believe I've nested and rolled up the data correctly but subsequently I don't seem to be able to access the new key/value pairs I've created.
The original data is a JSON blob with each test, its date of completion, subject area and score. What I want to acheive is to plot a separate set of average score datapoints on a chart by day for each subject. I've used nest previously to get an overall daily score and have plotted this successfully with the scales referenced in the code below but a two level heirarchy is getting the better of me. When I log the new 'nestedData' object it looks correct to me (i.e. the expected values are all there and grouped as I would expect).
I've worked on a few different versions of this with no success. I'm wondering if I'm trying to do something nest isn't designed for or if it's simply that my array notation is wrong (quite possible!). The code below shows the basic framework I'm using. What I'm missing is a filter to select the appropriate subject and the correct way to then access the average daily score from the rollup.
// Takes original data and nest by date and then subject and rolls up on count of work and mean score
var dataToNest = data;
nestedData = d3.nest()
.key(function(el) {return el.dateCompleted})
.key(function(el) {return el.subject})
.rollup(function(leaves) {
return {"numberCompleted": leaves.length,
"averageScore": d3.mean(leaves, function(d) {return(d.score)})}
})
.entries(dataToNest);
// Format date as JS object
nestedData.forEach(function (el) {
el.key = new Date(el.key);
});
// Sort by date
nestedData.sort(function (a,b) {
return a.key - b.key;
});
// Code for scales etc not included
d3.select("svg")
.selectAll("circle")
.data(nestedData)
.filter(/* Filter by subject */)
.enter()
.append("circle")
.attr("class", "subject")
.attr("r", 5)
.attr("cy", function (d) {/* Get the average score for the selected subject */})
.attr("cx", function(d) {return xScale(d.key)});
Some things I've tried:
For the filter I've attempted to select the value of the second key (the subject key) using various iterations of d.values.key === 'Algebra'
For the average score I've tried accessing using iterations of d.values.values.averageScore as well as a function that iterated through the index of the second array.
My strong suspicion is that this is a problem with my understanding of how arrays are structured and referenced in javascript. I've read all the related posts on this but they seem to be mostly about using nest() rather than accessing the values from within it.
UPDATE
Got my foot in the door with the following:
svg.selectAll('.circle-group')
.data(nestedSubjectData)
.enter().append('g')
.attr('class', 'circle-group')
.attr('transform', function(d, i) {
return 'translate(' + xScale(d.key) + ',0)';
})
.selectAll('circle')
.data(function(d) {return d.values;})
.enter().append('circle')
.attr('class', function(d) {return d.key;})
.attr('cx', 0)
.attr('cy', function(d) { return d.values.averageScore; })
.attr('r', 5);
This adds a circle for the averageScore datapoint in each of the arrays at the second level and applies a class that can be used to differentiate the subjects.
Thanks to Lars and a few related questions on SO I got to the bottom of this. The code is below. Having tried this out a few different ways I changed the nesting order to something more logical. Since date and score are used to set x/y coordinates it was easier to keep them at one level and to set the subject as the first level. This makes it easier to split out each series for styling and other series-level interaction (like just switching an entire subject on or off in the visualisation).
My chart now shows a point for each daily average score within each subject with a line between them. You can remove one or the other and it will still work fine.
I haven't included the data but it's a JSON doc. Similarly I haven't included all my scales or the svg creation.
// Nest data by subject and day and get average overall
var subjectDataToNest = data;
nestedSubjectData = d3.nest()
.key(function(el) {return el.subject}) // Nest by subject first
.key(function(el) {return el.dateAppeared}) // Within each subject array create an array for each day
.sortKeys(d3.ascending) // Sort the daily arrays by date (but this doesn't work reliably)
.rollup(function(leaves) {
return {
"averageScore" : d3.mean(leaves, function(d) {return(d.score)}) // Return the average score
}
})
.entries(subjectDataToNest);
// Draw circles for each subject and each
svg.selectAll('.subject-group')
.data(nestedSubjectData)
.enter().append('g')
// Create a group to contain each circle and (eventually) the path
.attr('class', 'subject-group')
.attr("id", function(d) {return d.key;})
// Change the selection
.selectAll('circle')
// Change the data to return index of nested array
.data(function(d) {return d.values;})
.enter().append('circle')
// Convert string date (which is the key in the nested array) to object and apply scale
.attr('cx', function(d) {return xScale(new Date(d.key));})
.attr('cy', function(d) { return yScale(d.values.averageScore); })
.attr('r', 5);
//Draw line
// 1. d3.svg.line() line generator takes values and retuns x/y c-oords for each datapoint
var subjectPath = d3.svg.line()
.x(function(d) {
return xScale(new Date(d.key));})
.y(function(d) {
return yScale(d.values.averageScore);
})
// 2. Select the subject group and append a path to each
svg.selectAll(".subject-group")
.data(nestedSubjectData)
.append('path')
.attr('class', 'subject-line')
.attr("d", subjectPath)
// Pass the second level of the nested array to subjectPath to generate x/y co-ords for the
.attr("d",function(d) {return subjectPath(d.values);});
Useful Reading:
Nested Selections
jshanley's nested selection JS Bin - Relates to SO Question Accessing Nested Array in a D3 variable
Phoebe Bright's canonical D3 nest examples
Related
I am having d3 read a TSV file. Then I want a particular column of that file to be graphed on a line.
I've tried a lot of variations all with undefined errors.
Below is what I am trying to do in concept:
d3.tsv("file.txt").then( foo => {
var svg = d3.select("graph").append("svg").attr(etc... )
svg.selectAll(".dot")
-----> .data(foo[all indexes]['nameofcolumn'])
.enter().append("circle")
.attr("class", "dot")
.attr("cx", function(d, i) { return xScale(i) })
.attr("cy", function(d) { return yScale(d.y) })
.attr("r", 5)
...etc...
Even if I hardcode foo[1]['columnname'] (which console.log confirms is a number) my graph just falls through. No images, no error.
I think you are getting confused by the argument that .data() takes. You seem to be passing it a single property from a single data point. But, in fact, you should pass it your entire dataset. So in your example, this should just be foo:
// foo: [{ "Date Time": 12:00, T1: 1, T2: 2 }, { "Date Time": 13:00, T1: 3, T2: 3.5 }, ...]
d3.selectAll('.dot')
.data(foo)
.enter()
.append('circle')
...
This code says:
select all of the elements with the dot class (there won't have been any created yet, but that's fine).
pass in an array representing my entire dataset, called foo.
if there is any datapoint in the dataset for which there hasn't been a dom element created yet (all of them at the moment because this is the start), then append a circle. So on enter() d3 will loop through the foo array that you provided to data() and append a circle for each data-point.
The second issue is that you look to be setting the cy attribute incorrectly, because you are passing in d.y to the yScale() function. Instead you need to pass in the correct property from your data.
So in your example, you have T1 and T2 properties in each of your data-points. I'm not sure which is the one you wish to represent on your y axis, but let say it is T1, then the code should read:
...
.attr('cy', function(d) { return yScale(d.T1); })
...
After 6 long hours, I managed to add just a couple of more lines to my example, following my previous post (D3 tooltip show values from nested dataset) concerning the use of tooltips.
Now I am stuck at a different point - I can't make circle points that snap to the line points. Other users have already pointed me to a few directions (thanks #Mark), but still haven't been able to combine everything and make it work as I want it.
I have created one circle for each line with its corresponding line color. When hovering over with the mouse, a tooltip with all the lines' values appears and the circles must be positioned on the lines on the x and y axis.
My problem lies in the following piece of code, located inside the mousemove function, in line #106 of this fiddle edit: updated fiddle (https://jsfiddle.net/2en21Lqh/13/):
d3.selectAll(parent + ' .d3-focuspoint')
.classed("hidden", false)
.attr("cx", xz(lastDate))
.attr("cy", function(c) {
return d3.map(c.values, function(d,i) {
if (lastDate == d.date) {
console.log(d.value);
return d.value;
}
})
});
The circles are already bound to the existing data (two days ago I wouldn't have figured this on my own! - at least I am sligthly improving! :/ ), so I am trying to apply the correct cy value but can't figure out the way to do it. The function inside cy returns an object rather than the d.value I am looking for. What am I doing wrong? I've been trying for hours to find any similar examples but can't find any :/
edit: even pointers to the right direction would help :/
Try this:
var mousemoveFunc = function(d, i) {
var x0 = xz.invert(d3.mouse(this)[0]);
var lastDate,
cys = [], //<-- create array to hold y values for circles
ds = []; //<-- create array to hold tooltip text
dataGroup.forEach(function(e) { //<-- loop the data (I removed the map since we now need to return two things)
var i = bisectDate(e.values, x0, 1),
d0 = e.values[i - 1],
d1 = e.values[i];
var d = x0 - d0.date > d1.date - x0 ? d1 : d0;
lastDate = d.date; //<-- hold onto the date (same for all xs)
cys.push(d.value); //<-- hold onto the y value for all circles
ds.push(e.key + " " + d.value); //<-- make the tooltip line
});
var mouse = d3.mouse(svg.node()).map(function(d) {
return parseInt(d);
});
var left = Math.min(containerwidth, mouse[0]+margin.left+margin.right),
top = Math.min(containerheight, mouse[1]+margin.top+margin.right);
d3.selectAll(parent + ' .d3-focuspoint')
.classed("hidden", false)
.attr("cx", xz(lastDate)) //<-- set x position
.attr("cy", function(d,i) {
return yz(cys[i]); //<-- loop the 3 circles and set y position
});
tooltip
.html(lastDate.toString() + "<br/>" + ds.join("<br/>"))
.classed('hidden', false)
.style('left', left + 'px')
.style('top', top + 'px');
};
Updated fiddle.
I am looking into ways I can display the percent of a slice of pie compared to the current graph in the tooltip.
First I tried looking for something similar to chart.labelType: 'percent' but it looks like there is no such option.
What I am trying to do now is calculate the percentage inside chart.tooltip.contentGenerator according to documentation the function should be passed 5 arguments function (key, x, y, e, series) -> String however I am only receiving the first argument.
I am using angular 1.5.0, d3 3.5.16, nvd3 1.8.2, and angular-nvd3 1.0.5.
What is the best practice for displaying the percentage in the tooltip?
EDIT: You brought up a great point that I didn't account for, that the total (and thus the percentage portion of each pie segment) will change when you remove segments from the pie. Looked into how to account for this, and I discovered you can monitor the chart for a stateChange, and configure what happens when this event is dispatched.
So, what I've done is to update the total when that event is fired by filtering out whatever values are disabled:
chart: {
...,
dispatch: {
stateChange: function(e) {
total = _.reduce(_.filter($scope.data, function(value) {
return !value.disabled;
}), function(result, value, key) {
return result += parseFloat(value.y);
}, 0);
console.log(total);
}
},...
};
I've updated the example pie chart plunker with the new code, but everything else remained the same, except that I added in each key and the updated total into the tooltip so you can verify that it's working. But you'll still initialize the total value when the chart first loads:
var total = _.reduce($scope.data, function(result, value, key) {
return result += parseFloat(value.y);
}, 0);
...And use the chart.tooltip.contentGenerator method to customize the tooltip message, which you can set up to return something like this:
tooltip: {
contentGenerator: function (key, x, y, e, series) {
return '<h4>' + ((key.data.y / total) * 100).toFixed(2) + '% of total</h4>';
}
}
Hope that helps!
I'm a beginner in d3.js and I need help printing out the data from the arrays of arrays.
Every time I try to print out the data inside the nested hard bracket, the text doesn't show up in browser. I feel frustrated because I'm pretty this is easily fixable but I just can't figure it out. Then I figured I don't have the foundation for the d3.js yet. So far I got this:
//Width and height
var w = 500;
var h = 120;
var barPadding = 1;
var dataset =[
[5,23]
[10,23]
];
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[0]; })])
.range([0, w]);
//Create SVG element
d3.select("body").selectAll("p")
.data(dataset)
.enter()
.append("p")
.text(function(d) { return d; });
Also, if you were able to figure this out, then does your method apply for nested brackets that hold more than 2 digits?
You will be able to print as is if you fix the dataset declaration:
var dataset =[[5,23],[10,23]];
It needs a comma between array entries.
Probably trivial, but I seem to have a nested data situation for which I can find no guidance. I have an array representing an time progression (index) for which data is only intermittently available:
elem[..]
elem[55].path[..]
elem[56]
elem[57]
elem[58].path[..]
elem[59]
elem[60].path[..]
elem[..]
Empty data points are null:
elem[count] = null;
..whereas, pending further use, path elements are initialised using:
elem[count].path = [];
A large data set is gathered. In the first of the following two blocks of code, I select the array indexes
classes[chan_index].elem_num_container = "elem_num_container" + "_" + comp_id + "_" + chan_index;
chan_selector.elem_num_container[chan_index] = chan_selector.vis_container[chan_index]
.selectAll(classes[chan_index].elem_num_container)
.data(function(d, i) {
return d;
})
.enter()
.append("svg:g")
.attr("class", function(d, i) {
return classes[chan_index].elem_num_container;
});
Given so many null elements, in the second block (below), the nested d.path understandably provokes a "groupData is undefined" error.
classes[chan_index].path_container = "path_container" + "_" + comp_id + "_" + chan_index;
chan_selector.path_container[chan_index] = chan_selector.elem_num_container[chan_index]
.selectAll(classes[chan_index].path_container)
.data(function(d, i) {
return d.path;
})
.enter()
.append("svg:g")
.attr("class", function(d, i) {
return classes[chan_index].path_container;
});
Basically it comes down to selecting data based on the "d" parameter, followed by a nested selection on "d.value". While preserving the index, I need to side-step the error and select d.path elements, where they exist.
I see plenty nested JSON data examples, but nothing applicable to this case.
Blocked, and grateful for any help..
Thx
From the responses above, elimination of null elements was clearly held to be feasible, throwing doubt on incoming data (checked: all ok) and d3 processing hierarchy (malformed: two processing branches hung on to the same node, one propagating data, the other -the branch in question above- not).
In response, separated the two branches from their parent with new, interstitial svg:g container structures. Result: data on both branches.
Then, as suggested in the responses above, with d.path, I added a filter on the processing branch in question (ie between first and second code blocks in the original question), in the form:
chan_selector.temp[chan_index] = chan_selector.elem_num_container[chan_index]
.filter(function(d) { return typeof(d.path) === 'object'; });
Strangely (given claimed impact of filter on indexing) it works, giving the accustomed d3 eyecandy. :-)
Thanks to Superboggly and Lars Kotthoff for the valuable help and patience.