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
Related
I have this d3 code for drawing the pie chart in d3.js
/** START OF PIE CHART */
var svgCirWidth = 600, svgCirHeight = 300, radius = Math.min(svgCirWidth, svgCirHeight) / 2;
const pieContainer = d3.select("#pieChart")
.append("svg")
.attr("width", svgCirWidth)
.attr("height", svgCirHeight);
//create group element to hold pie chart
var g = pieContainer.append("g")
.attr("transform", "translate(" + 250 + "," + radius + ")");
var color = d3.scaleOrdinal(d3.schemeCategory10);
var pie = d3.pie().value(function (d) {
return d.total_up_percentage;
});
var path = d3.arc()
.outerRadius(radius)
.innerRadius(0);
var arc = g.selectAll("arc")
.data(pie(data))
.enter() //means keeps looping in the data
.append("g");
arc.append("path")
.attr("d", path)
.attr("fill", function (d) {
return color(d.data.total_up_percentage);
})
.append("text")
.text("afdaf");
var label = d3.arc()
.outerRadius(radius)
.innerRadius(0);
arc.append("text")
.attr("transform", (d) => {
return "translate(" + label.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text((d) => {
return d.data.region_iso_code + ":" + d.data.total_up_percentage + "%"
});
and this is the result of my pie
as you can see the text overlaps each other. I was wondering how can i rotate the text so it can be much more easier to read. I've tried editing the transform in the console but it won't work it just makes the text go up or down. Also I was wondering what happened to the color of my pie. It stuck on orange. It says on the documentation i read about this schemeCategory10 is that it is a 10 color code scheme. Yet it won't show the rest of the color. Is there any other way to change color?
When using an ordinal scale you should never rely on the scale's ability to infer the domain from usage: a good practice is always to explicitly set the domain.
By setting the domain you'd quickly see that this is indeed the expected behaviour: all orange slices have the same value, which is 100.
If you want different colors for those same values, use the indices instead:
.attr("fill", function (_, i) {
return color(i);
})
PS: regarding the texts, please avoid asking 2 or more different issues in a single question. Edit your question leaving just 1 issue, you can always post a new question with the other issues.
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 have created a d3 donut chart. Here is my code:
var width = 480;
var height = 480;
var radius = Math.min(width, height) / 2;
var doughnutWidth = 30;
var color = d3.scale.category10();
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(radius - 70);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d[1]; });
var dataset = settings.dataset;
console.log(dataset);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data[0]);
})
I have a simple form on my web page which displays a dropdown menu with several options. Every time a user changes a value on the form a new dataset is sent to my script (settings.dataset) and the donut is redrawn on the page.
Problem is, some of the values from the previous dataset remain in the DOM. In the console output below, you can see that the second dataset only has two elements. The third one is from the previous dataset. This is messing up the chart, as it is displaying a value that shouldn't be there.
My question: how do I clear the old values? I've read up on .exit() and .remove(), but I can't get my head around these methods.
Create one function that (re)draws the pie when it's created and when it's updated.
New data should be added to pie using enter() and old data should be removed using exit().remove()
It is as simple as this:
path.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.attrTween("d", arcTween);
path.exit().remove()
Full working code -> JSFIDDLE
There are two steps to implement the 'redraw' effect you want:
First, I suppose you want the svg canvas to be drawn only once when the page is loaded for the first time, and then update the chart in the svg instead of remove and redraw the svg:
var svg = d3.select("body")
.selectAll("svg")
.data([settings.dataset]);
// put data in an array with only one element
// this ensures there is only one consistent svg on the page when data is updated(when the script gets executed)
svg.enter().append("svg")
Second, understanding enter(), exit(), there are many great tutorials about this. In your case, I would suggest to draw the donut something like this:
var path = svg.selectAll(".donut")
.data(settings.data)
// bind data to elements, now you have elements belong to one of those
// three 'states', enter, exit, or update
// for `enter` selection, append elements
path.enter().append("path").attr("d", arc).attr("fill", "teal")
// for `update` selection, which means they are already binded with data
path.transition().attrTween("d", someFunction) // apply transition here if you want some animation for data change
// for `exit` selection, they shouldn't be on the svg canvas since there is no corresponding data, you can then remove them
path.exit().remove()
//remove and create svg
d3.select("svg").remove();
var svg = d3.select("body").append("svg").attr("width","960").attr("height", "600"),
inner = svg.append("g");
I want to show the city name and population related to the voronoi area hovered over. However, with how I made the voronoi areas, I had to either only send coordinate data and have all of the drawings work, or send more data and none of the voronoi areas are drawn (b/c it can't read the non-coordinate data, and I don't know how to specify within an array or object, at least when creating voronois). I can enter static or irrelevant data for the tooltip (as I did below), but not anything from the actual dataset.
var tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.text("a simple tooltip");
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [w, h]]);
d3.csv("us-cities1.csv", function(d) {
return [projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat])[1]];
}, function(error, rows) {
vertices = rows;
drawV(vertices);
}
);
function polygon(d) {
return "M" + d.join("L") + "Z";
}
function drawV(d) {
svg.append("g")
.selectAll("path")
.data(voronoi(d), polygon)
.enter().append("path")
.attr("class", "test")
.attr("d", polygon)
.attr("id", function(d, i){return i;})
.on("mouseover", function(){return tooltip.style("visibility", "visible");})
.on("mousemove", function(){return tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+10)+"px").text((this).id);})
.on("mouseout", function(){return tooltip.style("visibility", "hidden");});
svg.selectAll("circle")
.data(d)
.enter().append("circle")
.attr("class", "city")
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 2);
}
I've put together an example using your data to demonstrate what Lars mentions. I created a variable for Voronoi like this:
var voronoi = d3.geom.voronoi()
.x(function(d) { return (d.coords[0]); })
.y(function(d) { return (d.coords[1]); });
which was taken from this Bl.ock by Mike. This allows you to specify the array of coordinates and still have them connected to the descriptive data you want to display.
I then created the object to store all the data in a format that could be used in the Voronio polygons using:
cities.forEach(function (d,i) {
var element = {
coords: projection([+d.lon, +d.lat]),
place: d.place,
rank: d.rank,
population: d.population
};
locCities.push(element);
});
I could have specified the translation of the coordinates in the voronio variable and then just used the cities variable, but I didn't.
The title attribute was used for the tooltips, but this can be replaced with something more appropriate such as what you have in your code. The relevant code is :
.append("title") // using titles instead of tooltips
.text(function (d,i) { return d.point.place + " ranked " + d.point.rank; });
There were a few issues with the data. I had to use an older version of d3 (3.1.5) to get the geojson to render correctly. I know there have been a number of chnanges to the AlberUsa projection so beware there is an issue there.
The location of some of the cities seems wrong to me for instance San Fancisco appears somewhere in Florida (this caused me some confusion). So I checked the original csv file and the coordinates seem to be wrong in there and the data is rendering where it should (just not where I'd expect according to the labels).
Now putting it all together you can find it here
I'm trying to highlight some points in a time series modeled using a nvd3.js eventLineChart - more precisely I have a json object with time-stamps and for each time-stamp I would like to add a vertical line at this particular date/time. The highlighted points may not exist in the time-series data source and are global over all groups of the time-series data (like ticks).
Any ideas on how this could be achieved? - I tried adding a standard line to my plot (fixed y1 and y2 and x according to the timestamp of the events i want to highlight) but wasn't able to have the timestamps scaled to the same range as the original time series.
Here are some parts of the model I started to build for that purpose based on nv.models.lineChart. - (just an excerpt of the model as most of the code is just a copy from the lineChart model):
nv.models.eventLineChart = function() {
// adds vertical highlights to line chart
"use strict";
var chartEvents = {}
function chart(selection) {
selection.each(function(data) {
// Setup Scales
x = lines.xScale();
y = lines.yScale();
// Setup containers and skeleton of chart
var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-lineChart').append('g');
var g = wrap.select('g');
gEnter.append('g').attr('class', 'nv-eventLinesWrap');
//------------------------------------------------------------
// Main Chart Component(s)
var eventWrap = wrap
.select('.nv-eventLinesWrap')
.selectAll('.nv-eventLines')
.data(function(d) {return d });
eventWrap
.enter()
.append('g')
.attr('class', 'nv-eventLines');
// chartEvents json ==> [{decsription: "test,"timestamp: 1375031820000}]
var eventLine = eventWrap
.selectAll('.nv-eventLine')
.data(chartEvents, function(d){return (d.timestamp)});
var eventLineEnter = eventLine.enter()
.append('line').attr('class', 'nv-eventLine')
.style('stroke-opacity', 0);
// #todo set ymin and ymax
eventLine
.attr('x1', function(d){ return x(d.timestamp);})
.attr('x2', function(d){ return x(d.timestamp);})
.attr('y1', 300)
.attr('y2', 800)
.style('stroke', function(d,i,j) { return color(d,j) })
.style('stroke-opacity', function(d,i) {
return 1;
});
});
return chart;
}
chart.setChartEvents = function(_) {
if (!arguments.length) return chartEvents;
chartEvents = _;
return chart;
};
return chart;}
This model is called by using:
nv.addGraph(function() {
var nv3dChart = nv.models.eventLineChart().useInteractiveGuideline(true).setChartEvents(json.chartEvents);
// json.chartEvents >> [{decsription: "EventDescription,"timestamp: 1375031820000}]
nv3dChart.xAxis
.showMaxMin(false);
.tickFormat(function(d) { return d3.time.format("%Y-%m-%d")(new Date(d)) });
nv3dChart.yAxis
.axisLabel(widgetConfig.action.data.kpiName)
.tickFormat(d3.format(',.f'));
var ndg = d3.select(renderToElementId+' svg');
ndg.datum([{
values: json.data,
key: widgetConfig.action.data.tagName
}])
.transition().duration(500);
nv.utils.windowResize(nv3dChart.update);
return nv3dChart;})
Which produces currently this svg output (events that should be displayed by vertical lines only)
<g class="nv-eventLines">
<line class="nv-eventLine" x1="1375031820000" x2="1375031820000" y1="300" y2="800" style="stroke: #1f77b4;"></line>
</g>
.. as described I haven't yet figured out a way to implement the scaling of the events x values according to the scale of the line chart
Would greatly appreciate any help regarding this problem
I now manually created all scales for x and y and added them to the nvd3 elements. I'm not particularly happy with that solution as it prevents me from creating a more modular feature for multiple nvd3 charts but it is a starting point.
Here is an outline of my current solution:
nv.models.eventLineChart = function() {
// initialize scales
var y = d3.scale.linear(),
x = d3.scale.linear();
// set scales of lines
lines = nv.models.line()
.yScale(y)
function chart(selection) {
//#todo support for multiple series
// set domain and range for scales
x
.domain(d3.extent(data[0].values, function(d){return d.x}))
.range([0,availableWidth]);
y
.domain(d3.extent(data[0].values, function(d){return d.y}))
.range([0,availableHeight]);
// add container for vertical lines
gEnter.append('g').attr('class', 'nv-eventLinesWrap');
// setup container
var eventWrap = wrap.select('.nv-eventLinesWrap').selectAll('.nv-eventLines')
.data(function(d) {return d });
eventWrap.enter().append('g').attr('class', 'nv-eventLines');
eventWrap.select('.nv-eventLinesWrap').attr('transform', 'translate(0,' + (-margin.top) +')');
var eventLine = eventWrap.selectAll('.nv-eventLine').data(chartEvents, function(d){return (d.timestamp)});
var eventLineEnter = eventLine.enter()
.append('line').attr('class', 'nv-eventLine')
// configure and style vertical lines
//#todo: set color for lines
eventLine
.attr('x1', function(d){ return x(d.timestamp)})
.attr('x2', function(d){ return x(d.timestamp)})
.attr('y1', y.range()[0])
.attr('y2', y.range()[1])
.style('stroke', function(d,i,j) { return "#000"; })
.style('stroke-width',1)
// #todo add vertical lines via transitions, add vLine content to toolbox
}}
Thank you, Lars, for your contributions .. they really helped a lot to understand certain parts in more detail.
If anyone has come up with a better idea to solve this problem I would be very grateful if you could post these suggestions here!