d3.js - Autoscroll for pie chart legend - d3.js

Pie chart with long legend
I have a d3.js pie chart which uses this code:
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = (-2 * legendRectSize) - 27;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
})
.on("click", function(d,i){
console.log(d);
console.log(i);
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return d; });
The result contains a lot of items, the pie charts looks OK, but the legend is too long. I now want to make the legend elements autoscroll in the center of the pie chart so that the legend does not overlay with the pie chart itself.
How can this be done?
As an alternative, how do you limit the legend to just 10 items? (without limiting the pie chart results to 10 items)

To make a scrollable container, you'll have to create a separate svg for the legends itself and contain it within a div and position the div to the coordinates you need.
(Of course, you'll have to add CSS to the div i.e. max-height to be pie-chart height and overflow: auto; to have the feature of auto-scrolling)
Let me know if you need help with that.
For the alternative, you can just do this without altering the original data:
var legend = svg.selectAll('.legend')
.data(color.domain().slice(0, 10))

Related

Make donut chart clickable

Can anybody help me regarding how I can make my following donut chart Clickable? I am just creat a donut chart from some dummy data and want each portion of the donut to be clickable. I am quite new in D3 and finding it hard to incorporate the click function in the donut chart.
I am finding it difficult to make each portion of the donut chart clickable in d3.js. Any help is appreciated. I have added my code snippet here.
function myFunction(width,height,margin,datax,dis,hg) {
var radius = Math.min(width, height) / 2 - margin
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", hg)
.append("g")
.attr("transform", "translate(" + width / dis + "," + height / 2 + ")");
var color = d3.scaleOrdinal()
.domain(Object.keys(data))
.range(d3.schemeDark2);
var pie = d3.pie()
.sort(null)
.value(function(d) {return d.value; })
var data_ready = pie(d3.entries(data))
var arc = d3.arc()
.innerRadius(radius * 0.5)
.outerRadius(radius * 0.8)
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d){ return(color(d.data.key)) })
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 0.7)
; }
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
You just need to select all path and bind event click on it
svg.selectAll('path')
.on('click', (d, i, n) => {
console.log(d, i, n)
})

Double bar chart creation

I want to create a bar chart like this:
There are two chart bars one below the other, the first one grows upwards while the second one grows downwards.
They have different scales and data.
This is what I created:
var doublebarSvg1 = d3.select('#doublebar')
.append('svg')
.attr('class', 'doublebarSvg1')
.attr('width', 700)
.attr('height', 400);
var doublebarSvg2 = d3.select('#doublebar')
.append('svg')
.attr('class', 'doublebarSvg2')
.attr('width', 700)
.attr('height', 400);
var margin = {top: 0, right: 0, bottom: 0, left: 50};
var width = doublebarSvg1.attr('width') - margin.left - margin.right;
var height = doublebarSvg1.attr('height') - margin.top - margin.bottom;
var x = d3.scaleBand()
.rangeRound([0, width])
.padding(0.1)
.domain(years);
var y1 = d3.scaleLinear()
.rangeRound([height, 0])
.domain([0, 100]);
var y2 = d3.scaleSqrt()
.rangeRound([height, 0])
.domain([813, 0.1]); // max value 812.05 but domain is [0, 100000]
var doublebarSvgG1 = doublebarSvg1.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var doublebarSvgG2 = doublebarSvg2.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
////////////////////////////////////////////////////////////////////////
// Tooltip.
////////////////////////////////////////////////////////////////////////
var svgTip = doublebarSvg1.append('svg').attr('id', 'tooltip');
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-5, 0])
.html(function(d) {
return '<div><span>Country:</span> <span style=\'color:white\'>' + d.country + '</span></div>' +
'<div><span>Perc:</span> <span style=\'color:white\'>' + d.perc + '%</span></div>' +
'<div><span>Rate:</span> <span style=\'color:white\'>' + d.rate + '%</span></div>';
});
svgTip.call(tip);
////////////////////////////////////////////////////////////////////////
// Draw a single double bar
////////////////////////////////////////////////////////////////////////
makeDoublebar1();
function makeDoublebar1() {
// define the axes
var xAxis = d3.axisBottom(x);
var yAxis1 = d3.axisLeft(y1);
// create x axis
doublebarSvgG1.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
// create y axis
doublebarSvgG1.append('g')
.attr('class', 'y axis')
.call(yAxis1)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
// create bar rect
doublebarSvgG1.selectAll('.bar')
.data(testData1) //.data(covFiltered)
.enter().append('rect')
.attr('fill', 'steelblue')
.attr('class', 'bar')
.attr('x', function(d) {
return x(d.year);
})
.attr('y', function(d) {
if(isNaN(d.perc)) {
d.perc = 0;
}
return y1(d.perc);
})
.attr('width', x.bandwidth())
.attr('height', function(d) {
if(isNaN(d.perc)) {
d.perc = 0;
}
return height - y1(d.perc);
})
.on('mouseover', function(d) {
d3.select(this).attr('fill', 'darkblue');
tip.show(d);
})
.on('mouseout', function(d) {
d3.select(this).attr('fill', 'steelblue');
tip.hide(d);
});
}
////////////////////////////////////////////////////////////////////////
// Draw a single double bar
////////////////////////////////////////////////////////////////////////
makeDoublebar2();
function makeDoublebar2() {
// define the axes
var xAxis = d3.axisBottom(x);
var yAxis2 = d3.axisLeft(y2);
// create x axis
doublebarSvgG2.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, 0)')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
// create y axis
doublebarSvgG2.append('g')
.attr('class', 'y axis')
.call(yAxis2)
.append('text')
.style('text-anchor', 'end');
// create bar rect
doublebarSvgG2.selectAll('.bar')
.data(testData2)
.enter().append('rect')
.attr('fill', 'tomato')
.attr('class', 'bar')
.attr('x', function(d) { // left start point
return x(d.year);
})
.attr('y', function(d) { // top start point
if(isNaN(d.rate)) {
d.rate = 0;
}
return 0;
})
.attr('width', x.bandwidth())
.attr('height', function(d) {
if(isNaN(d.rate)) {
d.perc = 0;
}
return y2(d.rate);
})
.on('mouseover', function(d) {
d3.select(this).attr('fill', 'red');
tip.show(d);
})
.on('mouseout', function(d) {
d3.select(this).attr('fill', 'tomato');
tip.hide(d);
});
}
PLUNKER here.
There are some problem:
if I replace .axis {display: initial;} with .axis {display: none;}, all the axis disappear but I want the horizontal line between the two chart
I would like there to be only one tooltip, which when the user hovers over any bar, comes out with a tooltip that shows both perc and rate value.
And, more importantly, is this the smartest way to create a chart like that?
Regarding the axis, since you want to keep the horizontal line, just hide the ticks and the texts:
.x.axis text,.x.axis line {
opacity: 0;
}
The tooltip problem is a bit more complex. The issue is that you're binding different data arrays to each set of bars.
Because of that, the best idea is finding the desired object in each array when you hover over a given year and getting the respective properties:
var thisPerc = testData1.find(function(e){return e.year === d.year}).perc;
var thisRate = testData2.find(function(e){return e.year === d.year}).rate;
Then you use those properties for setting the tooltip's text.
Here is the updated Plunker: http://plnkr.co/edit/tfB4TpkETgzp5GF1677p?p=preview
Finally, for your last question ("And, more importantly, is this the smartest way to create a chart like that?"), the answer is no. There are a lot of things that can (and must) be changed here, but this involves a lot of refactoring and it's arguably off topic at Stack Overflow. However, this is an adequate question for Code Review. But please read their docs first for keeping your question on topic, asking there is not the same as asking here.

Bar height taking up entire height of viewBox responsive SVG

I'm still fairly new to D3.js. Right now, I'm working on making a responsive bar chart. I'm using the viewbox to make it responsive in a div and using the DOM to set the height and width to the offsetWidth/Height of the div.
But the bars seem to be incorrect in height when set to the yScale.
Here's a screenshot of how they end up appearing:
I'm also being thrown this error in the console:
"d3.min.js:2 Error: attribute height: A negative value is not valid. ("-78.69462281846592")"
I believe the error has to do with this part of the code where I set the height attr to the height subtracted by the yScale data value:
var bars = svg.selectAll('.bar')
.data(data)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', function(d) {
return xScale(d.Year);
})
.attr('y', function(d) {
return yScale(d.Total);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return height - yScale(d.Total);
});
Here's the entire function that's drawing the bar chart:
<style>
#barchart {
width: 75vw;
height: 50vh;
}
</style>
function draw(data) {
var width = document.getElementById('barchart').offsetWidth,
height = document.getElementById('barchart').offsetHeight;
var svg = d3.select('#barchart')
.append('svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', '0 0 ' + width + ' ' + height)
.append('g');
var xScale = d3.scaleBand()
.range([0, width])
.padding(0.4);
var yScale = d3.scaleLinear()
.range([height, 0]);
xScale.domain(data.map(function(d) {
return d.Year;
}));
yScale.domain(data.map(function(d) {
return d.Total;
}));
var x_xaxis = svg.append('g')
.attr('transform', 'translate(' + 0 + ',' + 370 + ')')
.call(d3.axisBottom(xScale));
var y_axis = svg.append('g')
.attr('transform', 'translate(' + 20 + ',' + 0 + ')')
.call(d3.axisLeft(yScale)
.ticks(100));
var bars = svg.selectAll('.bar')
.data(data)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', function(d) {
return xScale(d.Year);
})
.attr('y', function(d) {
return yScale(d.Total);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return height - yScale(d.Total);
});
}
Without seeing your data it's tough to give a full answer but I'm certain the problem is that your yScale domain is being set incorrectly. Its linear so it needs an array of [minValue, maxValue], you are giving it an array of all data values. Try setting it with extent:
yScale.domain(d3.extent(data, function(d) {
return d.Total;
}));
Looking through this you want to start at zero, so:
yScale.domain([0, d3.max(data, function(d) {
return d.Total;
})]);
Also, you need to define the margins a bit more concretely. Take a look at these modifications.

d3, pie chart, labels outside not working properly

I have a problem with my pie chart, as the labels are not showing on the left side. Does anyone know the issue?
http://codepen.io/user1010/pen/BQPvLL
Maybe the problem is in here:
var text=svg.selectAll('.legend')
.data(pie(dataset))
.enter()
.append("text")
.attr('class','legend')
.attr("transform", function(d) { return "translate(" + labelArc.centroid(d) + ")"; })
.attr("dy", "20px")
You could try the following changes (applied to your original code: http://codepen.io/anon/pen/mOjaYW)
Increase the radius of labelArc
var labelArc = d3.svg.arc()
.outerRadius(radius + 30)
.innerRadius(radius + 30);
Move the main chart svg more to the right
transform: 'translate(' + w/1.5 +',' + h/2 + ')'
Align the label in the middle
.attr("text-anchor", "middle")
Adjust the size of the widget class
width:500px

How to select only x rows from a csv file for a pie chart

I have a CSV file containing a hundreds of lines here's a sample :
city.csv:
City,JanTemp,Lat,Long
Indianapolis IN,21,39.8,86.9
Des_Moines IA,11,41.8,93.6
Wichita KS,22,38.1,97.6
Louisville KY,27,39,86.5
New_Orleans LA,45,30.8,90.2
Portland ME,12,44.2,70.5
Baltimore MD,25,39.7,77.3
Boston MA,23,42.7,71.4
Detroit MI,21,43.1,83.9
Minneapolis MN,2,45.9,93.9
St_Louis MO,24,39.3,90.5
Helena MT,8,47.1,112.4
Omaha NE,13,41.9,96.1
Concord NH,11,43.5,71.9
Atlantic_City NJ,27,39.8,75.3
Albuquerque NM,24,35.1,106.7
Albany NY,14,42.6,73.7
New_York NY,27,40.8,74.6
What I want to do is create a pie chart representing JanTemp for every 10 rows.
Here's my initial code to create a pie chart for all the rows :
script:
<script>
var width = 500;
var height = 500;
var radius = Math.min(width, height) / 2;
var donutWidth = 120;
var legendRectSize = 18;
var legendSpacing = 4;
var color = d3.scale.category20();
var svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
var arc = d3.svg.arc()
.innerRadius(radius - donutWidth)
.outerRadius(radius);
var pie = d3.layout.pie()
.value(function(d) { return d.JanTemp; })
.sort(null);
d3.csv('city.csv', function(error, dataset) {
dataset.forEach(function(d) {
d.JanTemp = +d.JanTemp;
});
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.City);
});
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = -2 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return d; });
});
</script>
The code is working yet the visualization is bad.
The question is : How can I create a pie chart for every 10 rows in the csv file ? (Where also, can I add the property to only get rows by 10 ?) Is it even possible ?
You have two ways to do this, the first is to simply repeat what you have already and create several SVGs, one for each piechart.
The second is a bit more elegant, and involves a single SVG controlled by D3.
You'll first need to reorder your data into chunks of 10:
function( alldata ) {
var dataDivide = [], i, chunk = 10;
for (i=0; i<alldata.length; i+=chunk)
{
dataDivide.push(alldata.slice(i, i+chunk));
}
}
You can now use D3 to divide up your SVG and then set the chunks to be your data for each piechart:
var svg.selectAll("g")
.data( dataDivide )
.enter()
.append("g")
// position the g, etc.
.selectAll('path')
.data( function(d) {
return pie(d); // d is a chunk
})
.enter()
.append('path')
// etc.

Resources