I am creating a legend (var legend) for my d3js chart. The data is bound to the parent 'g' element, specifically a string (label) that I need to get at in the child 'text' element.
QUESTION: How to assign the parent data from 'g' element to the child text element?
Code:
var legend = svg.selectAll(".legend")
.data(color.domain().slice().reverse()) // color is array of strings with length = 6
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(-20," + i * 20 + ")"; });
legend.data(headers).enter().append("text") // headers is array of strings with length = 6
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return this.parentElement.__data__; }); // not working code here
Thanks! Full code: https://github.com/DeBraid/www.cacheflow.ca/blob/master/styles/js/d3kickchart.js
You're working much harder than you need to doing all this manual bookkeeping. Tying together different representations of the same data is one of D3's strong suits. Generally you want all your records in a single array as denormalized as possible, rather than trying to manually use indexes and multiple lists to correlate information.
On line 21 where you create the data records, simply add an additional property to hold the range label, and use these ranges as the domain of the color axis. (You might also look into d3's nest operator...)
Your legend can then be something as simple as this (jsfiddle):
var categories = ["foo", "bar", "baz", "qux", "zoo", "yaw"];
var color = d3.scale.ordinal()
.domain(categories)
.range(["#98ABC5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c"]);
d3.select("#legend")
.selectAll("div")
.data(categories)
.enter()
.append("div")
.style("background-color", color)
.text(function(d) { return d });
EDIT: The marked correct answer was the way to go. Below is the poor solution I hacked together.
Ended up with a pretty whacky bit of code here, but it works! HT Lars for suggesting adding .select() which was missing before.
legend.selectAll("text .legend")
.data([headers.slice().reverse()]) // data takes an array (headers), wrapped in [], order reversed
.enter().append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d,i,j) { return d[j]; }); // ** key **
Where 'j' is the index of the group containing the current element. Hence, using this.parentNode.data or the like is not needed, since we can access parent 'g' with the above. Not 100% sure on the mechanics of this, but it works! Feel free to elaborate on this methodology if you have cleaner way.
Related
I am trying to upgrade this stackable bar chart to v4.
Everything works except for one thing.
When I filter one category the bars don't drop to the start of the x-axis. I get an error which says:
state.selectAll(...).forEach is not a function
I've tried multiple things but I can't figure this one out.
This is the broken code:
function plotSingle(d) {
class_keep = d.id.split("id").pop();
idx = legendClassArray.indexOf(class_keep);
//erase all but selected bars by setting opacity to 0
d3.selectAll(".bars:not(.class" + class_keep + ")")
.transition()
.duration(1000)
.attr("width", 0) // use because svg has no zindex to hide bars so can't select visible bar underneath
.style("opacity", 0);
//lower the bars to start on x-axis
state.selectAll("rect").forEach(function(d, i) {
//get height and y posn of base bar and selected bar
h_keep = d3.select(d[idx]).attr("height");
y_keep = d3.select(d[idx]).attr("y");
h_base = d3.select(d[0]).attr("height");
y_base = d3.select(d[0]).attr("y");
h_shift = h_keep - h_base;
y_new = y_base - h_shift;
//reposition selected bars
d3.select(d[idx])
.transition()
.ease("bounce")
.duration(1000)
.delay(750)
.attr("y", y_new);
})
}
I find it strange that this works flawlessly in D3 v3, why wouldn't this work in v4?
In d3 v3 selectAll returned an array, in d3 v4 it returns an object.
From the v3 notes:
Selections are arrays of elements—literally (maybe not literally...).
D3 binds additional methods to the array so that you can apply
operators to the selected elements, such as setting an attribute on
all the selected elements.
Where as changes in v4 include:
Selections no longer subclass Array using prototype chain injection;
they are now plain objects, improving performance. The internal fields
(selection._groups, selection._parents) are private; please use the
documented public API to manipulate selections. The new
selection.nodes method generates an array of all nodes in a selection.
If you want to access each node in v4 try:
selection.nodes().forEach( function(d,i) { ... })
But, this is just the node, to get the data you would need to select each node:
var data = [0,1,2];
var svg = d3.select("body").append("svg")
.attr("width",500)
.attr("height",200)
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d,i) { return i * 20 + 50 })
.attr("cy", 50)
.attr("r", 4);
circles.nodes().forEach(function(d,i) {
console.log(d3.select(d).data());
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
But, if you need the data or to modify the selection properties, it could be easier to use selection.each(). d3.each iterates through each element of a d3 selection itself, and allows you to invoke a function for each element in a selection (see API docs here):
var data = [0,1,2];
var svg = d3.select("body").append("svg")
.attr("width",500)
.attr("height",200)
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d,i) { return i * 20 + 50 })
.attr("cy", 50)
.attr("r", 4);
circles.each( function() {
console.log(d3.select(this).data());
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
In v3 of this bar chart, in the forEach loop
`states.selectAll("rect").forEach(function(d,i) {`
d is an array of nodes (the rectangles in each .g).
But, in v4 d3 selections aren't arrays, you can't use a forEach loop in the same way. But you can still get the nodes in it without much modification using selection.nodes() and than get the childNodes to replicate the array in the v3 version:
state.nodes().forEach(function(d, i) {
var nodes = d.childNodes;
Here we go through each element/node in state and get the child rects, returned as an array. Here's an updated fiddle.
I have this d3.js project donut chart. For some reason, I am not able to access the data with in the onmousemove. The i value become zero is all the functions I pass within that event. I want to access the data of the particular slice where the mouse has moved.
How do I resolve this? Someone pls hlp!
Here is my code so far:
piesvg.selectAll("path")
.data(pie(data))
.enter().append("g")
.attr('class', 'slice')
var slice = d3.selectAll('g.slice')
.append('path')
.each(function(d) {
d.outerRadius = outerRadius - 20;
})
.attr("d", arc)
.attr('fill', function(d, i) {
return colorspie(i)
})
.on("mouseover", arcTween(outerRadius, 0))
.on("mouseout", arcTween(outerRadius - 20, 150))
.on("mousemove", function(data){
piesvg.select(".text-tooltip")
.attr("fill", function(d,i){return colorspie(i)})
.text(function(d, i){return d[i].domain + ":" + parseInt(d[i].value * 20)}); //Considers i as 0, so no matter whichever slice the mouse is on, the data of only first one is shown
});
Here is the full code:
https://jsfiddle.net/QuikProBro/xveyLfyd/1/
I dont know how to add external files in js fiddle so it doesn't work....
Here is the .tsv that is missing:
value domain
1.3038675 Cloud
2.2541437 Networking
0.15469614 Security
0.8287293 Storage
0.7292818 Analytics
0.61878455 Intelligence
1.7016574 Infra
0.4088398 Platform
Your piesvg.select is bound to be zero-indexed for i and in all probability undefined for d as it takes those values from a single tooltip element, not the slices. Hard to be 100% sure from the snippet, but I suspect you're wanting to access and use the 'data' and 'i' from the original selectAll on the slices.
.on("mousemove", function(d, i){
piesvg.select(".text-tooltip")
.attr("fill", colorspie(i))
.text(d.data.domain + ":" + parseInt(d.data.value * 20));
});
Edited as pie slices store original data in d.data property ^^^
I have a d3.js problem and have struggled with this for a while and just can not seem to solve it. I believe it is pretty easy, but am missing something very basic.
Specifically, I have the following code, which generates a line and 2 circles for the 1st entry in the JSON - I have 'hardcoded' it for the first entry.
I'd like to now add the 2nd and 3rd entries of the JSON file to the graph and have control over line and circle colors and then generalize the code.
From reading the documentation and StackOverflow, it seems like the proper approach is to use nesting, but I can't seem to make it work?
The code is on jsfiddle at the following URL and the javascript is below.
http://jsfiddle.net/GVmVk/
// INPUT
dataset2 =
[
{
movie : "test",
results :
[
{ week: "20130101", revenue: "60"},
{ week: "20130201", revenue: "80"}
]
},
{
movie : "beta",
results :
[
{ week: "20130101", revenue: "40"},
{ week: "20130201", revenue: "50"}
]
},
{
movie : "gamm",
results :
[
{ week: "20130101", revenue: "10"},
{ week: "20130201", revenue: "20"}
]
}
];
console.log("1");
var parseDate = d3.time.format("%Y%m%d").parse;
var lineFunction = d3.svg.line()
.x(function(d) { return xScale(parseDate(String(d.week))); })
.y(function(d) { return yScale(d.revenue); })
.interpolate("linear");
console.log("2");
//SVG Width and height
var w = 750;
var h = 250;
//X SCALE AND AXIS STUFF
//var xMin = 0;
//var xMax = 1000;
var xScale = d3.time.scale()
.domain([parseDate("20130101"),parseDate("20131231")])
.range([0, w]);
console.log(parseDate("20130101"));
console.log("3");
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
console.log("4S");
//Y SCALE AND AXIS STUFF
var yScale = d3.scale.linear()
.domain([0, 100])
.range([h, 0]);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.ticks(5);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
console.log("4S1");
//CREATE X-AXIS
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - 30) + ")")
.call(xAxis);
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + 25 + ",0)")
.call(yAxis);
svg.selectAll("circle")
.data(dataset2[0].results)
.enter()
.append("circle")
.attr("cx", function(d) {
// console.log(d[0]);
console.log(parseDate(d.week));
return xScale(parseDate(d.week));
})
.attr("cy", function (d) {
return yScale(d.revenue);
})
.attr("r", 3);
//create line
var lineGraph = svg.append("path")
.attr("d", lineFunction(dataset2[0].results))
.attr("class", "line");
The word "nesting" comes up in two contexts in d3 -- creating nested data arrays with d3.nest, and using nested data to create nested selections.
Your data is already in the correct format for a nested selection -- an array of objects, each of which has a sub-array of individual data points. So you don't need to worry about manipulating the data, you just need to go straight to joining your data to your elements in nested d3 selections:
I'm going to take you through it quickly, but the following tutorials will be good reference for the future:
Thinking with Joins
Nested Selections
How Selections Work
On to your example: you have a top-level data structure that is an array of movie objects, each of which contains a sub-array of weekly revenue values. The first thing you need to decide is what type of elements you want associated with each level of data. You're drawing a line and a set of circles for the data in the sub-array, but aren't currently adding anything for the top-level array objects (the movies). You need to add something for them in order for nested selections to work, and it needs to be something that can contain your line and circle. In SVG, that's almost always going to be a <g> (grouping) element.
To efficiently create one <g> element for every object in your data array -- and to attach the data objects to the elements for future reference -- you create an empty selection, join your data to it, then use the enter() method of the data join selection to add elements for each data object that didn't match an element. In this case, since we don't have any elements to start, all the data objects will be in the enter() selection. However, the same pattern also works when updating some of the data.
var movies = svg //start with your svg selection,
//it will become the parent to the entering <g> elements
.selectAll("g.movie") //select all <g> elements with class "movie"
//that are children of the <svg> element
//contained in the `svg` selection
//this selection will currently be empty
.data( dataset2 ); //join the selection to a data array
//each object in the array will be associated with
//an element in the selection, if those elements exist
//This data-joined selection is now saved as `movies`
movies.enter() //create a selection for the data objects that didn't match elements
.append("g") //add a new <g> element for each data object
.attr("class", "movie") //set it's class to match our selection criteria
//for each movie group, we're going to add *one* line (<path> element),
//and then a create subselection for the circles
.append("path") //add a <path> within *each* new movie <g> element
//the path will *inherit* the data from the <g> element
.attr("class", "line"); //set the class for your CSS
var lineGraph = movies.select("path.line")
//All the entered elements are now available within the movies selection
//(along with any existing elements that we were updating).
//Using select("path") selects the first (and only) path within the group
//regardless of whether we just created it or are updating it.
.attr("d", function(d){ return lineFunction(d.results); });
//the "d" attribute of a path describes its shape;
//the lineFunction creates a "d" definition based on a data array.
//If the data object attached to the path had *only* been the d.results array
//we could have just done .attr("d", lineFunction), since d3
//automatically passes the data object to any function given as the value
//of an .attr(name, value) command. Instead, we needed to create an
//anonymous function to accept the data object and extract the sub-array.
var circles = movies.selectAll("circle")
//there will be multiple circles for each movie group, so we need a
//sub-selection, created with `.selectAll`.
//again, this selection will initially be empty.
.data( function(d) {return d.results; });
//for the circles, we define the data as a function
//The function will be called once for each *movie* element,
//and passed the movie element's data object.
//The resulting array will be assigned to circles within that particular
//movie element (or to an `enter()` selection, if the circles don't exist).
circles.enter() //get the data objects that don't have matching <circle> elements
.append("circle") //create a circle for each
//the circles will be added to the appropriate "g.movie"
//because of the nested selection structure
.attr("r", 3); //the radius doesn't depend on the data,
//so you can set it here, when the circle is created,
//the same as you would set a class.
circles //for attributes that depend on the data, they are set on the entire
//selection (including updating elements), after having created the
//newly entered circles.
.attr("cx", function(d) { return xScale( parseDate(d.week) ); })
.attr("cy", function(d) { return yScale( d.revenue ); });
Live version with the rest of your code: http://jsfiddle.net/GVmVk/3/
You'll need to adjust the domain of your x-scale so that the first data points aren't cut off, and you'll need to decide how you want to use your movie title property, but that should get you going.
Yes indeed, nested selection are the way to go for the circles, although you don't need them for the paths:
svg.selectAll("g.circle")
.data(dataset2)
.enter()
.append("g")
.attr("class", "circle")
.selectAll("circle")
.data(function(d) { return d.results; })
.enter()
.append("circle")
.attr("cx", function(d) {
// console.log(d[0]);
console.log(parseDate(d.week));
return xScale(parseDate(d.week));
})
.attr("cy", function (d) {
return yScale(d.revenue);
})
.attr("r", 3);
//create line
var lineGraph = svg.selectAll("path.line")
.data(dataset2).enter().append("path")
.attr("d", function(d) { return lineFunction(d.results); })
.attr("class", "line");
Complete example here.
I give up, I can't figure it out.
I was trying to create a bar chart with 3d.js but I can't get it working. Probably I don't understand it enough to deal with my complicate associative array.
My array has the following structure:
{"January"=>{"fail"=>13, "success"=>6},
"February"=>{"success"=>10, "fail"=>4},
"March"=>{"success"=>9, "fail"=>13},
"April"=>{"success"=>16, "fail"=>5},
"May"=>{"fail"=>52, "success"=>23},
"June"=>{"fail"=>7, "success"=>2},
"July"=>{},
"August"=>{"fail"=>6, "success"=>3},
"September"=>{"success"=>54, "fail"=>59},
"October"=>{"success"=>48, "fail"=>78},
"November"=>{"fail"=>4, "success"=>6},
"December"=>{"fail"=>1, "success"=>0}}`
I got the displaying of the axis working:
The code looks really ugly because I converted the names to a "normal" array:
monthsNames = new Array();
i = 0;
for (key in data) {
monthsNames[i] = key;
i++;
}
x.domain(monthsNames);
y.domain([0, 100]);
But I can't figure it out how to deal with the data.
I tried things like, svg.selectAll(".bar").data(d3.entries(data))
What is a good beginning I guess but I can't get the connection to the axis working.
What I want to create is a bar-chart that has the months as x-axis and every month has two bars (respectively one bar with two colours) - one for success and one for fail.
Can anybody please help me how to handle the data? Thanks in advance!
EDIT:
I cannot figure out how to scale x and y. If I use this code:
var x = d3.scale.ordinal()
.domain(monthsNames)
.range([0, width]);
var y = d3.scale.linear()
.domain([0,100])
.range([0, height]);
nothing is shown up then. If I print out the values that evaluate after using e.g. x(d.key) or x(d.value.fail) they are really strange numbers, sometimes even NaN.
EDIT:
d3.selectAll(".barsuccess")
.on('mouseover', function(d){
svg.append('text')
.attr("x", x(d.key))
.attr("y", y(d.value.success))
.text(d.value.success+"%")
.attr('class','success')
.style("font-size","0.7em")
})
.on('mouseout', function(d){
d3.selectAll(".success").remove()
});
d3.selectAll(".barfail")
.on('mouseover', function(d){
svg.append('text')
.attr("x", x(d.key)+x.rangeBand()/2)
.attr("y", y(d.value.fail))
.text(d.value.fail+"%")
.attr('class','fail')
.style("font-size","0.7em")
})
.on('mouseout', function(d){
d3.selectAll(".fail").remove()
});
Be sure to check out the bar chart tutorials here and here. You have basically all you need already. The connection between the axes and the data are the functions that map input values (e.g. "March") to output coordinates (e.g. 125). You (presumably) created these functions using d3.scale.*. Now all you need to do is use the same functions to map your data to coordinates in the SVG.
The basic structure of the code you need to add looks like
svg.selectAll(".barfail").data(d3.entries(data))
.enter().append("rect")
.attr("class", "barfail")
.attr("x", function(d) { x(d.key) })
.attr("width", 10)
.attr("y", function(d) { y(d.value.fail) })
.attr("height", function(d) { y(d.value.fail) });
and similar for success. If you use the same scale for the x axis for both types of bar, add a constant offset to one of them so the bars don't overlap. Colours etc can be set in the CSS classes.
I have followed the instructions at: http://bost.ocks.org/mike/path/ for creating and animating single graphs with single lines.
And, figured out how to create multiple lines in a graph: Drawing Multiple Lines in D3.js
Main Issue: I am having a hard time transitioning multiple lines after I shift & push in new data into my data array.
I create the N lines with: (time: epoch time, steps forward)
var seriesData = [[{time:1335972631000, value:23}, {time:1335972631500, value:42},...],
[{time:1335972631000, value:45}, {time:1335972631500, value:13},...],
[{time:1335972631000, value:33}, {time:1335972631500, value:23},...}],
[...],[...],...
];
var seriesColors = ['red', 'green', 'blue',...];
line = d3.svg.line()
.interpolate(interpolation)
.x(function(d) { return x(d.time); })
.y(function(d) { return y(d.value); });
graphGroup.selectAll(".line")
.data(seriesData)
.enter().append("path")
.attr("class", "line")
.attr("d", line)
.style('stroke', function(d, i) { return seriesColors[i]; })
.style('stroke-width', 1)
.style('fill', 'none');
And am trying to update N lines with a Javascript setInterval(...) calling a method with:
graph.selectAll("path")
.data(seriesData)
.attr("transform", "translate(" + x(1) + ")")
.attr("d", line)
.transition()
.ease("linear")
.duration(transitionDelay)
.attr("transform", "translate(" + x(0) + ")");
It can draw the initial set perfectly, but as soon as I update the data array, the lines just disappear.
UPDATE 01
I realized that I am using epoch time values in the x (xAxis shows date:time) as my example would probably work if I used the illustrative seriesData above.
The problem was the "transform", "translate" using x(1), x(0) was returning huge numbers, way larger than my graph needed to be transitioned.
I modified the update N lines method (above) to use a manual approach:
New Issue:
Now the graph moves left correctly, but the lines/graph pops back to the right, each setInterval update executes.
It's push/shift'ing the seriesData array correctly but it doesn't keep scrolling to the left to show the new data that IS actually being drawn.
graph.selectAll("path")
.data(seriesData)
.attr("d", line)
.attr("transform", null)
.transition()
.ease("linear")
.duration(2000)
.attr("transform", "translate(-200)");
Another reference that I have used is this: http://bl.ocks.org/1148374
Any thoughts?
One thing that jumps out at me as a possibility for error is the data calls that are used, the initial is
.data(seriesData)
the update uses
.data([seriesData])
which may cause issues, but its hard to tell without seeing the rest of what is going on, can you perhaps put it on jsfiddle?