I am a beginner on D3.js and hit the wall on the following:
I wish to display the score given by an attendee of an event over time. Then as the attendee can give also comments, I would like to place a circle on the curve of the score in the same colour as the scoring.
I succeeded to do this for a single user.
The code is on JS Fiddle http://jsfiddle.net/roestigraben/8s1t8hb3/
Then, trying to extend this to multiple attendees, I run into problems.
The JSFiddle http://jsfiddle.net/roestigraben/Lk2kf1gh/
This code displays nicely the score data for the 3 attendees simulated. However the circles to display the possible comments (there is only one in the data set) from the attendees do not work
I try to filter the attendees array
svg.selectAll("circle")
.data(data.filter(function(d, i){ if(d.comment){return d}; })) // condition here
.enter().append("circle")
.attr("class", "dotLarge")
.attr({r: 5})
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.status); })
I think I need to go deeper into the nesting, but ....my ignorance.
Thanks a lot
Peter
The code where you're displaying your circles doesn't even come close to matching the format of your data. I don't know that you're having a problem with the nest, but you probably want a slightly different data structure when it comes to graphing your comments.
I have updated your fiddle here: http://jsfiddle.net/Lk2kf1gh/7/
The important bit is:
var comments = attendees.map(function(d) {
return {
name: d.name,
comments: d.values.filter(function(e) {
return e.comment != undefined;
})
};
});
//generation of the circles to indicate a comment
// needs to be done with a filter
svg.selectAll("g.comments")
.data(comments)
.enter()
.append("g")
.attr("class", function(d) { return "comments " + d.name })
.selectAll("circle.comment")
.data(function(d) {
return d.comments;
})
.enter()
.append("circle")
.attr("class", "dotLarge comment")
.attr("r", 5)
.attr("cx", function(e) { return x(e.time); })
.attr("cy", function(e) { return y(e.status); });
The first part of this code creates a new data structure that is more focused on the comment information. The second part creates the circles.
Note: I've grouped each person into their own g element and then created a circle for each of their comments. This makes use of d3 nested selections and data binding. Hopefully you can follow what is happening.
I've also added a few more comments to your data for testing. I didn't bother to fix any of the cosmetic issues, I'll leave that up to you.
Related
var IndChart = dc.geoChoroplethChart("#india-chart");
var states = data.dimension(function (d) {
return d["state_name"];
});
var stateRaisedSum = states.group().reduceSum(function (d) {
return d["popolation"];
});
IndChart
.width(700)
.height(500)
.dimension(states)
.group(stateRaisedSum)
.colors(d3.scale.ordinal().domain().range(["#27AE60", "#F1C40F", "#F39C12","#CB4335"]))
.overlayGeoJson(statesJson.features, "state", function (d) { //console.log(d.properties.name);
return d.id;
})
.projection(d3.geo.mercator().center([95, 22]).scale(940))
.renderLabel(true)
.title(function (d) { console.log(d); return d.key + " : " + d.value ;
})
.label(function (d) { console.log(d);}) ;
wanted to add Label or custom value(25%, added in Map chart screen-shots) in map chart for each path using dc.js.
In the comments above, you found or created a working example that answers your original question. Then you asked how to make it work for two charts on the same page.
This is just a matter of getting the selectors right, and also understanding how dc.js renders and redraws work.
First off, that example does
var labelG = d3.select("svg")
which will always select the first svg element on the page. You could fix this by making the selector more specific, i.e. #us-chart svg and #us-chart2 svg, but I prefer to use the chart.select() function, which selects within the DOM tree of the specific chart.
Next, it's important to remember that when you render a chart, it will remove everything and start from scratch. This example calls dc.renderAll() twice, so any modifications made to the first chart will be lost on the second render.
In contrast, a redraw happens when any filter is changed, and it incrementally changes the chart, keeping the previous content.
I prefer to listen to dc.js chart events and make my modifications then. That way, every time the chart is rendered or redrawn, modifications can be made.
In particular, I try to use the pretransition event whenever possible for modifying charts. This happens right after drawing, so you have a chance to change things without any glitches or pauses.
Always add event listeners before rendering the chart.
Adding (the same) handler for both charts and then rendering, looks like this:
usChart.on('pretransition', function(chart) {
var project = d3.geo.albersUsa();
var labelG = chart.select("svg")
.selectAll('g.Title')
.data([0])
.enter()
.append("svg:g")
.attr("id", "labelG")
.attr("class", "Title");
labelG.selectAll("text")
.data(labels.features)
.enter().append("svg:text")
.text(function(d){return d.properties.name;})
.attr("x", function(d){return project(d.geometry.coordinates)[0];})
.attr("y", function(d){return project(d.geometry.coordinates)[1];})
.attr("dx", "-1em");
});
usChart2.on('pretransition', function(chart) {
var project = d3.geo.albersUsa();
var labelG = chart.select("svg")
.selectAll('g.Title')
.data([0])
.enter()
.append("svg:g")
.attr("id", "labelG")
.attr("class", "Title");
labelG.selectAll("text")
.data(labels.features)
.enter().append("svg:text")
.text(function(d){return d.properties.name;})
.attr("x", function(d){return project(d.geometry.coordinates)[0];})
.attr("y", function(d){return project(d.geometry.coordinates)[1];})
.attr("dx", "-1em");
});
dc.renderAll();
I used one more trick there: since pretransition happens for both renders and redraws, but we only want to add these labels once, I use this pattern:
.selectAll('g.Title')
.data([0])
.enter()
.append("svg:g")
.attr("class", "Title");
This is the simplest data binding there is: it says we want one g.Title and its data is just the value 0. Since we give the g element the Title class, this ensures that we'll add this element just once.
Finally, the result of this expression is an enter selection, so we will only add text elements when the Title layer is new.
Fork of your fiddle.
This question has been asked before but I don't think the given solution is the cleanest way to do it so I'm hoping someone may have figured it out since then. I am generating multiple pie charts using d3.js and am dynamically updating them through SQL queries. This is my update function:
function updateCharts()
{
var updatedDataSet = getDataSet();
// Create a pie layout and bind the new data to it
var layout = d3.layout.pie()
.value(function(d, i) { return d[i].count; })
.sort(null);
// Select the pie chart
var pieChartSVG = d3.selectAll("#pie");
// Select each slice of the pie chart
var arcsUpdate = pieChartSVG.selectAll("g.slice")
.data(layout([updatedDataSet]))
.enter();
// Apply transitions to the pie chart to reflect the new dataset
arcsUpdate.select("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.transition()
.duration(1000)
.attrTween("d", arcTween);
}
But it doesn't work. If I take the .enter() out of arcsUpdate then it works but applies the same changes(data and tweens) to each chart. I could get around this by doing a foreach() on the elements returned from pieChartSVG but I can't think of a way of doing that other than the one described in the other question.
I have had to use the solution from the other question as I have to move forward but it's not a "clean" solution so I'd love to know if anybody is aware of a better way to handle it.
I thought you need take the .enter() out of arcsUpdate just like
var arcsUpdate = pieChartSVG.selectAll("path")
.data(layout([updatedDataSet]));
// Apply transitions to the pie chart to reflect the new dataset
arcsUpdate.enter()
.append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.transition()
.duration(1000)
.attrTween("d", arcTween);
This is the correct way.
And if it applies the same changes(data and tweens) to each chart. Please check out they are binding same updateDataSet or not.
I'm working through re-creating this population pyramid by Mike Bostock. I have a basic question about how .data() works.
I have data in a CSV file like so:
So, I have the number of men and women in each region (dodoma, arusha, etc.). Following Mike's example, I want to create a bar chart where men and women occupy different rects, which overlap with 80% opacity. I want region to be my x-axis, and number of people to be my y-axis. Here are my x- and y-axis scales:
var x = d3.scale.ordinal()
.rangeRoundBands([0, w], 0.05);
var y = d3.scale.linear()
.range([0, h-padding]);
And here's my attempt at grouping men+women over each region:
var regions = svg.append("g")
.classed("regions", true);
var region = regions.selectAll(".region")
.data(dataset)
.enter()
.append("g")
.attr("class", "region")
.attr("transform", function(region) { return "translate(" + x(region) + ",0)"; });
region.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i) {
return x(i);
})
.attr("y", function(d) { return h - y(d.people_0); })
.attr("width", barWidth)
.attr("height", function(d) { return y(d.people_0); });
I end up with this:
That is, I end up with 50 g elements which have all the data, and are just creating the full bar chart for each gender-region pair (men from Dodoma, women from Dodoma; men from Tanga, women from Tanga, etc.). The full chart is thus being redrawn 50 times. I have 25 regions, so I want 25 g elements with only the men/women numbers for that region.
I think this could all be tied up in .data(). But I don't know how to tell d3 that I want to group my observations by region, and end up with 2 rects per g.
Here's my JSFiddle (though I don't know how to load my external data - so nothing shows up; any hints on that would be appreciated).
Edited to add:
I've tried using .nest, given Benjamin's comment. So I've got:
dataset = d3.nest()
.key(function(d) { return d.round; })
.key(function(d) { return d.region; })
.rollup(function(v) { return v.map(function(d) { return d.people_0; }); })
.map(dataset);
And am now calling my rects like so:
region.selectAll("rect")
.data(function(region) { return data[round][region]; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return x(i);
})
.attr("y", function(d) { return h - y(d.people_0); })
.attr("width", barWidth)
.attr("height", function(d) { return y(d.people_0); });
No luck. FWIW, console.log(dataset) shows me that the data has, indeed, been rolled up. But I'm still having trouble calling it and associating it with my g.region elements.
Could someone explain what data[round][region] does? I don't understand how d3 is interpreting this.
There are several ways to do this. My approach would be to create a two-level nest for the data, by region and then by sex:
dataset = d3.nest()
.key(function(d) {
return d.region;
})
.sortKeys(d3.ascending)
.key(function(d) {
return d.sex;
})
.entries(dataset);
This will give you a structure that's easy to use later, which will look something like this:
key: "dodoma"
values:
key: "female"
values:
people_0: "43"
region: "dodoma"
sex: "female"
key: "male
values:
people_0: "77"
region: "dodoma"
sex: "male"
key: "killimanj"
values: ...
that's quite verbose, and could be simplified a little (eg using .map). This is a great nest tutorial page for more info.
From then, only a few changes are needed to draw your rects. See this plunk: http://embed.plnkr.co/PPLbQER4FERA6D7T4m9P/preview.
Plunker (http://plnkr.co/) is a good alternative to jsfiddle if you want to link external data by the way.
A few of the changes diverge from Mike's original example, including using class="male" \ "female" in the CSS to get blue \ pink colours, rather than using rect:first-child
http://bost.ocks.org/mike/selection/ explains how data works in d3. if you call it once, it will 'bind' each entry of data to the element you specify.
In your case you have to nest() by region and you get a array of objects that looks like:
['regionA' : [{region, sex, people}, {region, sex, people}], 'regionB' : [{region, sex, people}, {region, sex, people}]
you then do a double iteration of the data. so you first call .data() (its a array of regions this time, ie regionA) and bind it on a group called region
then bind data() again (this time its the array of objects for one region ie [{region, sex, people}, {region, sex, people}]) on rectangles.
I thought I understood the D3 enter/update/exit, but I'm having issues implementing an example. The code in http://jsfiddle.net/eamonnmag/47TtN/ illustrates what I'm doing. As you'll see, at each 5 second interval, I increase the rating of an item, and update the display again. This works, in some way. The issue is in only updating what has changed - D3 is updating everything in this case. The enter and exit methods, displayed in the console output that nothing has changed, which makes my think that it's treating each array as a completely new instance.
My understanding of the selectAll() and data() calls was that it would 'bind' all data to a map called 'chocolates' somewhere behind the scenes, then do some logic to detect what was different.
var chocolate = svg.selectAll("chocolates").data(data);
In this case, that is not what's happening. This is the update code. Any pointers to what I've missed are most appreciated!
function update(data){
var chocolate = svg.selectAll("chocolates").data(data);
var chocolateEnter = chocolate.enter().append("g").attr("class", "node");
chocolateEnter.append("circle")
.attr("r", 5)
.attr("class","dot")
.attr("cx", function(d) {return x(d.price)})
.attr("cy", function(d) {
//put the item off screen, to the bottom. The data item will slide up.
return height+100;})
.style("fill", function(d){ return colors(d.manufacturer); });
chocolateEnter
.append("text")
.text(function(d) {
return d.name;})
.attr("x", function(d) {return x(d.price) -10})
.attr("y", function(d){return y(d.rating+step)-10});
chocolateEnter.on("mouseover", function(d) {
d3.select(this).style("opacity", 1);
}).on("mouseout", function(d) {
d3.select(this).style("opacity", .7);
})
chocolate.selectAll('circle')
.transition().duration(500)
.attr('cy', function(d) {return y(d.rating+step)});
var chocolateExit = chocolate.exit().remove();
chocolateExit.selectAll('circle')
.attr('r', 0);
}
setInterval(function() {
chocolates[3].rating = Math.min(chocolates[3].rating+1, 5);
update(chocolates);
}, 5000);
Easy as apple pie!
Why are you doing svg.selectAll("chocolates")? There is no HTML element in your DOM called chocolates.
You need to change that to svg.selectAll(".node"). That will fix the problem.
There are a couple of issues in your code. First, the logic to detect what's different is, by default, to use the index of the item. That is, the first data item is matched to the first DOM element, and so on. This works in your case, but will break if you ever pass in partial data. I would suggest using the second argument to .data() to tell it how to match:
var chocolate = svg.selectAll("g.node").data(data, function(d) { return d.name; });
Second, as the other poster has pointed out, selecting "chocolate" will select nothing, as there are no such DOM elements. Just select the actual elements instead.
Finally, since you're adding g elements for the data items, you might as well use them. What I mean is that currently, you're treating the circles and text separately and have to update both of them. You can however just put everything underneath g elements. Then you have to update only those, which simplifies your code.
I've made all the above changes in your modified fiddle here.
I'm using D3.js to build a circular heat chart, and I want to add events so that when I mouseover any section of the chart, all the elements at the same angle also highlight. (Like the mouseover events on this Guardian visualisation.)
At the moment, I'm doing this by explicitly adding data attributes to the HTML for every path element:
g.selectAll("path").data(data)
.enter().append("path")
.attr("d", d3.svg.arc().innerRadius(ir).outerRadius(or).startAngle(sa).endAngle(ea))
.attr("data-pathnumber", function(d) { return d.pathNumber });
And then my mouseover event selects by data attribute:
d3.selectAll("#chart4 path").on('mouseover', function() {
var d = d3.select(this).data()[0];
d3.selectAll('path[data-pathnumber="' + d.pathNumber + '"]').attr('fill', 'black');
});
However, is this actually the correct way to do things in D3? It feels to me like there "ought" to be a way to select the path based only on the data stored in the DOM, not on explicit data attributes.
If you store the reference to your paths, you can use selection.filter here:
var paths = g.selectAll("path").data(data)
.enter().append("path")
.attr("d", d3.svg.arc().innerRadius(ir).outerRadius(or).startAngle(sa).endAngle(ea))
;
Mouseover:
d3.selectAll("#chart4 path").on('mouseover', function(thisData) {
paths
.filter(function (d) { return d.pathNumber === thisData.pathNumber; })
.attr('fill', 'black');
});