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');
});
Related
I try to add labels to map done with d3-geomap, but can't make it work.
The choropleth map itself gets painted correctly, but adding the labels doesn't work out right. The labels show up on the wrong position.
After painting the map I loaded again the topojson file again and then add text blocks like that:
d3.json("https://d3-geomap.github.io/d3-geomap/topojson/countries/ESP.json").then(function(es) {
let path = d3.geoPath().projection(d3.geoMercator());
svg.select("svg").append("g")
.selectAll("labels")
.data(topojson.feature(es, es.objects.units).features)
.enter().append("text")
.attr("class", "place-label")
.attr("x", function(d) { return path.centroid(d)[0]; })
.attr("y", function(d) { return path.centroid(d)[1]; })
.attr("text-anchor","middle")
.text(function(d) { return d.properties.name; });
});
The problem here is that I can't figure out the correct position of the labels. I also tried to apply the same transform as to the polygons, but then have all the same y position.
Here is the example on bl.ocks.
I made some changes to your code and published it in this gist. When testing it locally, the map displayed like the image below. At this size, labels don't work well, but if you resize the map and/or show fewer labels it should be okay.
Some info on the changes. Whenever you want to draw something on top of a map with d3-geomap, it should go in the postUpdate function. This way the map is already rendered and its SVG elements, the geo data and the path object are accessible via the map object you created. No need to load the Topojson file a second time. The function passed to postUpdate looks like follows:
function drawLabels() {
map.svg.append("g").attr('class', 'zoom')
.selectAll("text")
.data(topojson.feature(map.geo, map.geo.objects.units).features)
.enter().append("text")
.attr("class", "place-label")
.attr("x", function(d) { return map.path.centroid(d)[0]; })
.attr("y", function(d) { return map.path.centroid(d)[1]; })
.attr("text-anchor","middle")
.text(function(d) { return d.properties.name; })
.on('click', map.clicked.bind(map));
}
This page of the documentation shows the available map attributes and accessor functions. Hope this helps.
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.
I want to draw a pie chart for every point on the map instead of a circle.
The map and the points are displaying well but the pie chart is not showing over the map points. There is no error also. I can see the added pie chart code inside map also.
Below is the code snippet .
var w = 600;
var h = 600;
var bounds = [[78,30], [87, 8]]; // rough extents of India
var proj = d3.geo.mercator()
.scale(800)
.translate([w/2,h/2])
.rotate([(bounds[0][0] + bounds[1][0]) / -2,
(bounds[0][1] + bounds[1][1]) / -2]); // rotate the project to bring India into view.
var path = d3.geo.path().projection(proj);
var map = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
var india = map.append("svg:g")
.attr("id", "india");
var gDataPoints = map.append("g"); // appended second
d3.json("data/states.json", function(json) {
india.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("data/water.csv", function(csv) {
console.log(JSON.stringify(csv))
gDataPoints.selectAll("circle")
.data(csv)
.enter()
.append("circle")
.attr("id", function (d,i) {
return "chart"+i;
})
.attr("cx", function (d) {
return proj([d.lon, d.lat])[0];
})
.attr("cy", function (d) {
return proj([d.lon, d.lat])[1];
})
.attr("r", function (d) {
return 3;
})
.each(function (d,i) {
barchart("chart"+i);
})
.style("fill", "red")
//.style("opacity", 1);
});
function barchart(id){
var data=[15,30,35,20];
var radius=30;
var color=d3.scale.category10()
var svg1=d3.select("#"+id)
.append("svg").attr('width',100).attr('height',100);
var group=svg1.append('g').attr("transform","translate(" + radius + "," + radius + ")");
var arc=d3.svg.arc()
.innerRadius('0')
.outerRadius(radius);
var pie=d3.layout.pie()
.value(function(d){
return d;
});
var arcs=group.selectAll(".arc")
.data(pie(data))
.enter()
.append('g')
.attr('class','arc')
arcs.append('path')
.attr('d',arc)
.attr("fill",function(d,i){
return color(d.data);
//return colors[i]
});
}
water.csv:
lon,lat,quality,complaints
80.06,20.07,4,17
72.822,18.968,2,62
77.216,28.613,5,49
92.79,87.208,4,3
87.208,21.813,1,12
77.589,12.987,2,54
16.320,75.724,4,7
In testing your code I was unable to see the pie charts rendering, at all. But, I believe I still have a solution for you.
You do not need a separate pie chart function to call on each point. I'm sure that there are a diversity of opinions on this, but d3 questions on Stack Overflow often invoke extra functions that lengthen code while under-utilizing d3's strengths and built in functionality.
Why do I feel this way in this case? It is hard to preserve the link between data bound to svg objects and your pie chart function, which is why you have to pass the id of the point to your function. This will be compounded if you want to have pie chart data in your csv itself.
With d3's databinding and selections, you can do everything you need with much simpler code. It took me some time to get the hang of how to do this, but it does make life easier once you get the hang of it.
Note: I apologize, I ported the code you've posted to d3v4, but I've included a link to the d3v3 code below, as well as d3v4, though in the snippets the only apparent change may be from color(i) to color[i]
In this case, rather than calling a function to append pie charts to each circle element with selection.each(), we can append a g element instead and then append elements directly to each g with selections.
Also, to make life easier, if we initially append each g element with a transform, we can use relative measurements to place items in each g, rather than finding out the absolute svg coordinates we would need otherwise.
d3.csv("water.csv", function(error, water) {
// Append one g element for each row in the csv and bind data to it:
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("id", function (d,i) { return "chart"+i; })
.append("g").attr("class","pies");
// Add a circle to it if needed
points.append("circle")
.attr("r", 3)
.style("fill", "red");
// Select each g element we created, and fill it with pie chart:
var pies = points.selectAll(".pies")
.data(pie([0,15,30,35,20]))
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
Now, what if we wanted to show data from the csv for each pie chart, and perhaps add a label. This is now done quite easily. In the csv, if there was a column labelled data, with values separated by a dash, and a column named label, we could easily adjust our code to show this new data:
d3.csv("water.csv", function(error, water) {
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("class","pies")
points.append("text")
.attr("y", -radius - 5)
.text(function(d) { return d.label })
.style('text-anchor','middle');
var pies = points.selectAll(".pies")
.data(function(d) { return pie(d.data.split(['-'])); })
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
The data we want to display is already bound to the initial g that we created for each row in the csv. Now all we have to do is append the elements we want to display and choose what properties of the bound data we want to show.
The result in this case looks like:
I've posted examples in v3 and v4 to show a potential implementation that follows the above approach for the pie charts:
With one static data array for all pie charts as in the example: v4 and v3
And by pulling data from the csv to display: v4 and v3
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 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.