Related
I have been working with dc.js for a year now. Recently I have been tasked to implement a pie chart as below:
I want to replace the text labels in the pie chart slices with appropriate images.
I saw this implemented in pure d3.js. Can someone help me translate the implementation to dc.js?
http://jsfiddle.net/LLwr4q7s/
pie
.width(600)
.height(500)
.radius(200)
.innerRadius(120)
.dimension(disastersDimension)
.group(disastersGroup)
.on("filtered", function (chart, filter) {
var sel = filter;
let percentage = 0,
value = 0;
let disastersBuffer = [];
totalAmount = 0;
pie.selectAll("text.pie-slice").text((d) => {
percentage = dc.utils.printSingleValue(
((d.endAngle - d.startAngle) / (2 * Math.PI)) * 100
);
disastersBuffer.push({ ...d.data, percentage });
totalAmount += parseFloat(d.data.value);
});
filterPiechart(sel, percentage, totalAmount, disastersBuffer, value);
})
.on("renderlet", (chart) => {
if (!chart.selectAll("g.selected")._groups[0].length) {
chart.filter(null);
filterPiechart("", 100, totalAmount, [], 0);
}
var arc = chart.radius(250).innerRadius(100);
console.log(arc);
var g = chart.selectAll(".pie-slice");
chart
.selectAll(".pie-slice")
.append("image")
.attr("xlink:href", "img/disasters/Floods.png")
.attr("transform", "translate(-10,10) rotate(315)")
.attr("width", "26px")
.attr("hight", "26px")
.style("background-color", "white")
.attr("x", function (d) {
var bbox = this.parentNode.getBBox();
return bbox.x;
})
.attr("y", function () {
var bbox = this.parentNode.getBBox();
return bbox.y;
});
g.append("g")
.append("svg:image")
.attr("xlink:href", function (d) {
let filteredImage = self.piedata.find(
(i) => i.label == d.data.key
);
let image = filteredImage ? filteredImage.image : "";
return image;
})
.attr("width", 30)
.attr("height", 40)
.attr("x", function (d) {
var bbox = this.parentNode.getBBox();
return bbox.x;
})
.attr("y", function (d) {
var bbox = this.parentNode.getBBox();
return bbox.y;
});
})
.addFilterHandler(function (filters, filter) {
filters.length = 0; // empty the array
filters.push(filter);
return filters;
});
I took the fiddle and added a couple of 'Meteoicons' I found here.
(Of course, those icons are taken as an example and I have no permission to use them commercially)
The icons are stored in a separated <svg> elements. To render an icon, just select its root <g> element and copy its content to another <g> you create in your piechart:
g.append("g")
.attr("transform", d => `translate(${arc.centroid(d)}) scale(0.25)`)
.append('g')
.attr('transform', 'translate(-256,-256)') // The original icons are 256 x 256
.html(d => d3.select(`#meteo-icon-${... some attribute of d ...} > g`).html())
The code is for demonstration purposes only, you will need to modify it for your needs.
See the result in the snippet below:
var width = 550,
height = 550,
radius = 250,
colors = d3.scale.ordinal()
.range(['#336699 ','#336699 ','#ACD1E9','#ACD1E9','#ACD1E9']);
var image_width=40,
image_height=40;
var piedata = [
{
label: "test",
image: "http://placeimg.com/40/40/any",
value: 50
},
{
label: "",
image: "http://placeimg.com/42/42/any",
value: 50
},
{
label: "Jonathan",
image: "http://placeimg.com/44/44/any",
value: 50
},
{
label: "Lorenzo",
image: "http://placeimg.com/46/46/any",
value: 50
},
{
label: "Hillary",
image: "http://placeimg.com/38/38/any",
value: 50
}
]
var pie = d3.layout.pie()
.value(function(d) {
return d.value;
})
var arc = d3.svg.arc()
.outerRadius(250)
.innerRadius(100)
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate('+(width-radius)+','+(height-radius)+')');
var g = svg.selectAll(".arc")
.data(pie(piedata))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d,i) { return colors(i); });
g.append("g")
.attr("transform", d => `translate(${arc.centroid(d)}) scale(0.25)`)
.append('g')
.attr('transform', 'translate(-256,-256)')
.html(() => d3.select(`#meteo-icon-${Math.random() < 0.5 ? 1 : 2} > g`).html())
.selectAll('path')
.style('fill', 'orange');
path {
stroke: #fff;
fill-rule: evenodd;
}
text {
font-family: Arial, sans-serif;
font-size: 12px;
}
.meteo-icon {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<svg id="meteo-icon-1" class="meteo-icon" width="24" height="24" viewBox="0 0 512 512">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#1D1D1B" d="M177.615,288c7.438-36.521,39.688-64,78.396-64
c38.709,0,70.958,27.479,78.376,64h32c-7.834-54.125-54.084-96-110.376-96c-56.271,0-102.541,41.875-110.375,96H177.615z
M256.011,160c8.833,0,16-7.167,16-16v-32c0-8.833-7.167-16-16-16c-8.832,0-16,7.167-16,16v32
C240.011,152.833,247.179,160,256.011,160z M403.073,156.917c-6.249-6.25-16.374-6.25-22.625,0l-22.625,22.625
c-6.249,6.25-6.249,16.375,0,22.625c6.251,6.25,16.376,6.25,22.625,0l22.625-22.625
C409.323,173.292,409.323,163.167,403.073,156.917z M154.177,179.542l-22.625-22.625c-6.249-6.25-16.373-6.25-22.625,0
c-6.249,6.25-6.249,16.375,0,22.625l22.625,22.625c6.252,6.25,16.376,6.25,22.625,0
C160.429,195.917,160.429,185.792,154.177,179.542z M352.011,320h-192c-8.832,0-16,7.167-16,16s7.168,16,16,16h192
c8.833,0,16-7.167,16-16S360.844,320,352.011,320z M320.011,384h-128c-8.832,0-16,7.167-16,16s7.168,16,16,16h128
c8.833,0,16-7.167,16-16S328.844,384,320.011,384z"/>
</g>
</svg>
<svg id="meteo-icon-2" class="meteo-icon" width="24" height="24" viewBox="0 0 512 512">
<g>
<path fill="#1D1D1B" d="M349.852,343.15c-49.876,49.916-131.083,49.916-181,0c-49.916-49.917-49.916-131.125,0-181.021
c13.209-13.187,29.312-23.25,47.832-29.812c5.834-2.042,12.293-0.562,16.625,3.792c4.376,4.375,5.855,10.833,3.793,16.625
c-12.542,35.375-4,73.666,22.249,99.917c26.209,26.228,64.501,34.75,99.917,22.25c5.792-2.062,12.271-0.583,16.625,3.792
c4.376,4.333,5.834,10.812,3.771,16.625C373.143,313.838,363.06,329.941,349.852,343.15z M191.477,184.754
c-37.438,37.438-37.438,98.354,0,135.771c40,40.021,108.125,36.417,143-8.167c-35.959,2.25-71.375-10.729-97.75-37.084
c-26.375-26.354-39.333-61.771-37.084-97.729C196.769,179.796,194.039,182.192,191.477,184.754z"/>
</g>
</svg>
First, apologies for an incomplete example, but I ran out of time and I think this shows the principles.
I agree with #MichaelRovinsky that SVG icons would be better than images, but I couldn't find a CDN for SVG icons that would be suitable for the example, and I think the principles are exactly the same, since you could embed SVGs as image just as well.
Using placeimg.com for this purpose leads to weird results because the same URL will yield different results when read twice, so e.g. two slices may end up with the same image, and images change when the chart redraws.
Luckily these are both beside the point of customizing dc.js!
Adding things to dc.js pie slices
It would be nice if dc.js used an svg g group element to put the text in. Then we could just add to it and the position would be correct.
Instead, we have to add our image element and read the corresponding data from the pie label to get the placement:
chart.on('pretransition', chart => {
let labelg = chart.select('g.pie-label-group');
let data = labelg.selectAll('text.pie-label').data();
console.log('data', data);
Then we can add image elements in the same layer/g:
let pieImage = labelg.selectAll('image.pie-image');
let arcs = chart._buildArcs();
pieImage.data(data)
.join(
enter => enter.append('image')
.attr('class', 'pie-image')
.attr('x', -19)
.attr('y', -19))
.attr('href', d => images[d.data.key === 'Others' ? 'Others' : d.data.key.slice(4)])
.attr('transform', d => chart._labelPosition(d, arcs));
});
Notice that the attributes which only need to be set once (on enter) are inside the join call, and the attributes which need to be set every redraw (on update) are outside the join call.
x and y are negative one half the image size to center the images.
I used an object to store the URLs but you could use whatever.
Demo fiddle
Limitation
As with any customization of the pie chart, this doesn't account for animations well. The images will move before the animation is complete. If you care, I think I wrote an answer some years ago which dealt with this properly. I can probably dig it up but it was quite complicated and IMHO not worth it.
I have a Choropleth map where the tooltip is working for most of it, but the central states are now showing the tooltip...in face, they are not even running the mouseout callback function at all (tested with a console.log command).
At first I was using d3-tip, and that wasn't working, and it was the first time attempting it, so I thought I might be doing something wrong, so I opted to implement a standard div that toggles between display: none and display: block and when it still wasn't working, I threw in a console.log command to see if the callback function was running at all, and it's not. It's mostly an issue with Kansas, but some of the counties in the surrounding states are having problems too. and I know it's not an issue with the data set, because the example given, which pulls from the same data set is working fine.
Here is the css for the tooltip:
#tooltip{
display: none;
background-color: rgba(32,32,32,1);
position: absolute;
border-radius: 10px;
padding: 10px;
width: 200px;
height: 40px;
color: white
}
and the JS code:
$(function(){
//svg setup
const svgPadding = 60;
const svgWidth = 1000;
const svgHeight = 600;
var svg = d3.select('body')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight)
.attr('id', 'map');
function createChart(topData, eduData){
//scales
var colorScale = d3.scaleSequential(d3.interpolateBlues);
var unitScale = d3.scaleLinear()
.domain(d3.extent(eduData.map(e => e.bachelorsOrHigher)))
.range([0,1])
//map
var path = d3.geoPath();
svg.selectAll('.county')
.data(topojson.feature(topData, topData.objects.counties).features)
.enter()
.append('path')
.attr('class', 'county')
.attr('d', path)
.attr('data-fips', d=>d.id)
.attr('eduIndex', d => eduData.map(e => e.fips).indexOf(d.id))
.attr('data-education', function(){
var index = d3.select(this).attr('eduIndex');
if (index == -1)return 0;
return eduData[
d3.select(this).
attr('eduIndex')
]
.bachelorsOrHigher
})
.attr('fill', function(){
var value = d3.select(this).attr('data-education');
return colorScale(unitScale(value));
})
.attr('stroke', function(){
return d3.select(this).attr('fill');
})
.on('mouseover', function(d){
var index = d3.select(this).attr('eduIndex');
var education = d3.select(this).attr('data-education');
var county = index == -1 ? 'unknown' : eduData[index].area_name;
console.log(county)
var tooltip = d3.select('#tooltip')
.style('left', d3.event.pageX + 10 + 'px')
.style('top', d3.event.pageY + 10 + 'px')
.style('display', 'block')
.attr('data-education', education)
.html(`${county}: ${education}`)
})
.on('mouseout', ()=>d3.select('#tooltip').style('display', 'none'));
svg.append('path')
.datum(topojson.mesh(topData, topData.objects.states, (a,b)=>a.id!=b.id))
.attr('d', path)
.attr('fill', 'rgba(0,0,0,0)')
.attr('stroke', 'black')
.attr('stroke-width', 0.4)
//legend scale
const legendWidth = 0.5 * svgWidth;
const legendHeight = 30;
const numCells = 1000;
const cellWidth = legendWidth/numCells;
const legendUnitScale = d3.scaleLinear()
.domain([0, legendWidth])
.range([0,1]);
//legend
var legend = svg.append('svg')
.attr('id', 'legend')
.attr('width', legendWidth)
.attr('height', legendHeight)
.attr('x', 0.5 * svgWidth)
.attr('y', 0)
for (let i = 0; i < numCells; i++){
legend.append('rect')
.attr('x', i * cellWidth)
.attr('width', cellWidth)
.attr('height', legendHeight - 10)
.attr('fill', colorScale(legendUnitScale(i*cellWidth)))
}
}
//json requests
d3.json('https://raw.githubusercontent.com/no-stack-dub-sack/testable-projects-fcc/master/src/data/choropleth_map/counties.json')
.then(function(topData){
d3.json('https://raw.githubusercontent.com/no-stack-dub-sack/testable-projects-fcc/master/src/data/choropleth_map/for_user_education.json')
.then(function(eduData){
createChart(topData, eduData);
});
});
});
The issue is that you are applying a fill to the state mesh. Let's change the fill from rgba(0,0,0,0) to rgba(10,10,10,0.1):
It should be clear now why the mouse interaction doesn't work in certain areas: the mesh is filled over top of it. Regardless of the fact you can't see the mesh due to it having 0 opacity, it still intercepts the mouse events.
The mesh is meant only to represent the borders: it is a collection of geojson lineStrings (see here too). The mesh is not intended to be filled, it only should have a stroke.
If you change the mesh fill to none, or the pointer events of the mesh to none, then the map will work as expected.
I have a donut chart that recalculates the percentages and sizes of each category when a label/category is disabled in the legend. What I would like to do is recalculate the total count excluding the disabled categories. I'd like to do this everytime a user enables or disables a category in the legend.
Code below:
// define data
var dataset = [
{label: "Venue", count: 16107},
{label: "Photographer", count: 2783},
{label: "Wedding/Event Planner", count: 2037},
{label: "Reception Band", count: 4156},
{label: "Reception DJ", count: 1245},
{label: "Florist/Decor", count: 2534},
{label: "Videographer", count: 1995},
{label: "Wedding Dress", count: 1564},
{label: "Groom's Attire", count: 280},
{label: "Wedding Cake", count: 582},
{label: "Ceremony Site", count: 2197},
{label: "Ceremony Musicians", count: 755},
{label: "Invitations", count: 2534},
{label: "Transportation", count: 1995},
{label: "Favors", count: 1564},
{label: "Rehearsal Dinner", count: 280},
{label: "Engagement Ring", count: 582},
{label: "Officiant", count: 2197}
];
// chart dimensions
var width = 800;
var height = 800;
// a circle chart needs a radius
var radius = Math.min(width, height) / 2;
var donutWidth = 100; // size of donut hole. not needed if doing pie chart
// legend dimensions
var legendRectSize = 25; // defines the size of the colored squares in legend
var legendSpacing = 6; // defines spacing between squares
// define color scale
var color = d3.scaleOrdinal(d3.schemeCategory20b);
// more color scales: https://bl.ocks.org/pstuffa/3393ff2711a53975040077b7453781a9
// calculate new total
var total = d3.sum(dataset, d => d.count);
// define new total section
var newTotal = d3.select('new-total-holder')
.append('span')
.attr('class', 'newTotal')
var svg = d3.select('#chart') // select element in the DOM with id 'chart'
.append('svg') // append an svg element to the element we've selected
.attr('width', width) // set the width of the svg element we just added
.attr('height', height) // set the height of the svg element we just added
.append('g') // append 'g' element to the svg element
.attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')'); // our reference is now to the 'g' element. centerting the 'g' element to the svg element
var arc = d3.arc()
.innerRadius(radius - donutWidth) // radius - donutWidth = size of donut hole. use 0 for pie chart
.outerRadius(radius); // size of overall chart
var pie = d3.pie() // start and end angles of the segments
.value(function(d) { return d.count; }) // how to extract the numerical data from each entry in our dataset
.sort(null); // by default, data sorts in oescending value. this will mess with our animation so we set it to null
// define tooltip
var tooltip = d3.select('#chart') // select element in the DOM with id 'chart'
.append('div') // append a div element to the element we've selected
.attr('class', 'tooltip'); // add class 'tooltip' on the divs we just selected
tooltip.append('div') // add divs to the tooltip defined above
.attr('class', 'label'); // add class 'label' on the selection
tooltip.append('div') // add divs to the tooltip defined above
.attr('class', 'count'); // add class 'count' on the selection
tooltip.append('div') // add divs to the tooltip defined above
.attr('class', 'percent'); // add class 'percent' on the selection
// Confused? see below:
// <div id="chart">
// <div class="tooltip">
// <div class="label">
// </div>
// <div class="count">
// </div>
// <div class="percent">
// </div>
// </div>
// </div>
dataset.forEach(function(d) {
d.count = +d.count; // calculate count as we iterate through the data
d.enabled = true; // add enabled property to track which entries are checked
});
// creating the chart
var path = svg.selectAll('path') // select all path elements inside the svg. specifically the 'g' element. they don't exist yet but they will be created below
.data(pie(dataset)) //associate dataset wit he path elements we're about to create. must pass through the pie function. it magically knows how to extract values and bakes it into the pie
.enter() //creates placeholder nodes for each of the values
.append('path') // replace placeholders with path elements
.attr('d', arc) // define d attribute with arc function above
.attr('fill', function(d) { return color(d.data.label); }) // use color scale to define fill of each label in dataset
.each(function(d) { this._current - d; }); // creates a smooth animation for each track
// mouse event handlers are attached to path so they need to come after its definition
path.on('mouseover', function(d) { // when mouse enters div
var total = d3.sum(dataset.map(function(d) { // calculate the total number of tickets in the dataset
return (d.enabled) ? d.count : 0; // checking to see if the entry is enabled. if it isn't, we return 0 and cause other percentages to increase
}));
var percent = Math.round(1000 * d.data.count / total) / 10; // calculate percent
tooltip.select('.label').html(d.data.label); // set current label
tooltip.select('.count').html('$' + d.data.count); // set current count
tooltip.select('.percent').html(percent + '%'); // set percent calculated above
tooltip.style('display', 'block'); // set display
});
path.on('mouseout', function() { // when mouse leaves div
tooltip.style('display', 'none'); // hide tooltip for that element
});
path.on('mousemove', function(d) { // when mouse moves
tooltip.style('top', (d3.event.layerY + 10) + 'px') // always 10px below the cursor
.style('left', (d3.event.layerX + 10) + 'px'); // always 10px to the right of the mouse
});
// define legend
var legend = svg.selectAll('.legend') // selecting elements with class 'legend'
.data(color.domain()) // refers to an array of labels from our dataset
.enter() // creates placeholder
.append('g') // replace placeholders with g elements
.attr('class', 'legend') // each g is given a legend class
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing; // height of element is the height of the colored square plus the spacing
var offset = height * color.domain().length / 2; // vertical offset of the entire legend = height of a single element & half the total number of elements
var horz = -2 * legendRectSize; // the legend is shifted to the left to make room for the text
var vert = i * height - offset; // the top of the element is hifted up or down from the center using the offset defiend earlier and the index of the current element 'i'
return 'translate(' + horz + ',' + vert + ')'; //return translation
});
// adding colored squares to legend
legend.append('rect') // append rectangle squares to legend
.attr('width', legendRectSize) // width of rect size is defined above
.attr('height', legendRectSize) // height of rect size is defined above
.style('fill', color) // each fill is passed a color
.style('stroke', color) // each stroke is passed a color
.on('click', function(label) {
var rect = d3.select(this); // this refers to the colored squared just clicked
var enabled = true; // set enabled true to default
var totalEnabled = d3.sum(dataset.map(function(d) { // can't disable all options
return (d.enabled) ? 1 : 0; // return 1 for each enabled entry. and summing it up
}));
if (rect.attr('class') === 'disabled') { // if class is disabled
rect.attr('class', ''); // remove class disabled
} else { // else
if (totalEnabled < 2) return; // if less than two labels are flagged, exit
rect.attr('class', 'disabled'); // otherwise flag the square disabled
enabled = false; // set enabled to false
}
pie.value(function(d) {
if (d.label === label) d.enabled = enabled; // if entry label matches legend label
return (d.enabled) ? d.count : 0; // update enabled property and return count or 0 based on the entry's status
});
path = path.data(pie(dataset)); // update pie with new data
path.transition() // transition of redrawn pie
.duration(750) //
.attrTween('d', function(d) { // 'd' specifies the d attribute that we'll be animating
var interpolate = d3.interpolate(this._current, d); // this = current path element
this._current = interpolate(0); // interpolate between current value and the new value of 'd'
return function(t) {
return arc(interpolate(t));
};
});
// calculate new total
var newTotalCalc = d3.sum(dataset, d => d.count)
console.log(newTotalCalc);
// append newTotalCalc to newTotal which is defined above
newTotal.append("text")
.text(newTotalCalc);
});
// adding text to legend
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return d; }); // return label
body {
font-family: 'Open Sans Condensed', sans-serif;
}
.title-holder {
text-align: center;
}
.title {
font-family: 'Pacifico', cursive;
}
.font {
font-size: 20px;
}
/* legend */
.legend {
font-size: 14px;
}
rect {
cursor: pointer;
stroke-width: 2;
}
rect.disabled {
fill: transparent !important;
}
/* chart */
#chart {
height: 800px;
margin: 0 auto;
position: relative;
display: block;
width: 800px;
}
/* tooltip */
.tooltip {
background: #eee;
box-shadow: 0 0 5px #999999;
color: #333;
display: none;
font-size: 18px;
left: 130px;
padding: 10px;
position: absolute;
text-align: center;
top: 95px;
width: 80px;
z-index: 10;
}
.footer {
padding-top: 50px;
text-align: center;
list-style-type: none;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3.js Donut Chart</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300|Pacifico" rel="stylesheet">
<link href="styles.css" rel="stylesheet">
</head>
<body>
<div class="title-holder">
<h1 class="title">2016 Average Wedding Budget Breakdown</h1>
<p class="font">Uncheck categories to recalculate.</p>
<p class="font new-total-holder">New Total:</p>
</div>
<div id="chart"></div>
<footer>
Data Source</li>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
<script src="script.js"></script> <!-- remove if no javascript -->
</body>
</html>
Here's one approach to do that:
As you're toggling the enabled flag based on legend click, the new count can be calculated based on this flag as follows:
var newTotalCalc = d3.sum(dataset.filter(function(d) { return d.enabled;}), d => d.count)
Other minor changes:
You missed the dot operator here:
var newTotal = d3.select('.new-total-holder')
Instead of adding another text element in the new total <span>, I'm just changing the text.
newTotal.text(newTotalCalc);
Using the above, here's a code snippet:
// define data
var dataset = [
{label: "Venue", count: 16107},
{label: "Photographer", count: 2783},
{label: "Wedding/Event Planner", count: 2037},
{label: "Reception Band", count: 4156},
{label: "Reception DJ", count: 1245},
{label: "Florist/Decor", count: 2534},
{label: "Videographer", count: 1995},
{label: "Wedding Dress", count: 1564},
{label: "Groom's Attire", count: 280},
{label: "Wedding Cake", count: 582},
{label: "Ceremony Site", count: 2197},
{label: "Ceremony Musicians", count: 755},
{label: "Invitations", count: 2534},
{label: "Transportation", count: 1995},
{label: "Favors", count: 1564},
{label: "Rehearsal Dinner", count: 280},
{label: "Engagement Ring", count: 582},
{label: "Officiant", count: 2197}
];
// chart dimensions
var width = 800;
var height = 800;
// a circle chart needs a radius
var radius = Math.min(width, height) / 2;
var donutWidth = 100; // size of donut hole. not needed if doing pie chart
// legend dimensions
var legendRectSize = 25; // defines the size of the colored squares in legend
var legendSpacing = 6; // defines spacing between squares
// define color scale
var color = d3.scaleOrdinal(d3.schemeCategory20b);
// more color scales: https://bl.ocks.org/pstuffa/3393ff2711a53975040077b7453781a9
// calculate new total
var total = d3.sum(dataset, d => d.count);
// define new total section
var newTotal = d3.select('.new-total-holder')
.append('span')
.attr('class', 'newTotal').text(total);
var svg = d3.select('#chart') // select element in the DOM with id 'chart'
.append('svg') // append an svg element to the element we've selected
.attr('width', width) // set the width of the svg element we just added
.attr('height', height) // set the height of the svg element we just added
.append('g') // append 'g' element to the svg element
.attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')'); // our reference is now to the 'g' element. centerting the 'g' element to the svg element
var arc = d3.arc()
.innerRadius(radius - donutWidth) // radius - donutWidth = size of donut hole. use 0 for pie chart
.outerRadius(radius); // size of overall chart
var pie = d3.pie() // start and end angles of the segments
.value(function(d) { return d.count; }) // how to extract the numerical data from each entry in our dataset
.sort(null); // by default, data sorts in oescending value. this will mess with our animation so we set it to null
// define tooltip
var tooltip = d3.select('#chart') // select element in the DOM with id 'chart'
.append('div') // append a div element to the element we've selected
.attr('class', 'tooltip'); // add class 'tooltip' on the divs we just selected
tooltip.append('div') // add divs to the tooltip defined above
.attr('class', 'label'); // add class 'label' on the selection
tooltip.append('div') // add divs to the tooltip defined above
.attr('class', 'count'); // add class 'count' on the selection
tooltip.append('div') // add divs to the tooltip defined above
.attr('class', 'percent'); // add class 'percent' on the selection
// Confused? see below:
// <div id="chart">
// <div class="tooltip">
// <div class="label">
// </div>
// <div class="count">
// </div>
// <div class="percent">
// </div>
// </div>
// </div>
dataset.forEach(function(d) {
d.count = +d.count; // calculate count as we iterate through the data
d.enabled = true; // add enabled property to track which entries are checked
});
// creating the chart
var path = svg.selectAll('path') // select all path elements inside the svg. specifically the 'g' element. they don't exist yet but they will be created below
.data(pie(dataset)) //associate dataset wit he path elements we're about to create. must pass through the pie function. it magically knows how to extract values and bakes it into the pie
.enter() //creates placeholder nodes for each of the values
.append('path') // replace placeholders with path elements
.attr('d', arc) // define d attribute with arc function above
.attr('fill', function(d) { return color(d.data.label); }) // use color scale to define fill of each label in dataset
.each(function(d) { this._current - d; }); // creates a smooth animation for each track
// mouse event handlers are attached to path so they need to come after its definition
path.on('mouseover', function(d) { // when mouse enters div
var total = d3.sum(dataset.map(function(d) { // calculate the total number of tickets in the dataset
return (d.enabled) ? d.count : 0; // checking to see if the entry is enabled. if it isn't, we return 0 and cause other percentages to increase
}));
var percent = Math.round(1000 * d.data.count / total) / 10; // calculate percent
tooltip.select('.label').html(d.data.label); // set current label
tooltip.select('.count').html('$' + d.data.count); // set current count
tooltip.select('.percent').html(percent + '%'); // set percent calculated above
tooltip.style('display', 'block'); // set display
});
path.on('mouseout', function() { // when mouse leaves div
tooltip.style('display', 'none'); // hide tooltip for that element
});
path.on('mousemove', function(d) { // when mouse moves
tooltip.style('top', (d3.event.layerY + 10) + 'px') // always 10px below the cursor
.style('left', (d3.event.layerX + 10) + 'px'); // always 10px to the right of the mouse
});
// define legend
var legend = svg.selectAll('.legend') // selecting elements with class 'legend'
.data(color.domain()) // refers to an array of labels from our dataset
.enter() // creates placeholder
.append('g') // replace placeholders with g elements
.attr('class', 'legend') // each g is given a legend class
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing; // height of element is the height of the colored square plus the spacing
var offset = height * color.domain().length / 2; // vertical offset of the entire legend = height of a single element & half the total number of elements
var horz = -2 * legendRectSize; // the legend is shifted to the left to make room for the text
var vert = i * height - offset; // the top of the element is hifted up or down from the center using the offset defiend earlier and the index of the current element 'i'
return 'translate(' + horz + ',' + vert + ')'; //return translation
});
// adding colored squares to legend
legend.append('rect') // append rectangle squares to legend
.attr('width', legendRectSize) // width of rect size is defined above
.attr('height', legendRectSize) // height of rect size is defined above
.style('fill', color) // each fill is passed a color
.style('stroke', color) // each stroke is passed a color
.on('click', function(label) {
var rect = d3.select(this); // this refers to the colored squared just clicked
var enabled = true; // set enabled true to default
var totalEnabled = d3.sum(dataset.map(function(d) { // can't disable all options
return (d.enabled) ? 1 : 0; // return 1 for each enabled entry. and summing it up
}));
if (rect.attr('class') === 'disabled') { // if class is disabled
rect.attr('class', ''); // remove class disabled
} else { // else
if (totalEnabled < 2) return; // if less than two labels are flagged, exit
rect.attr('class', 'disabled'); // otherwise flag the square disabled
enabled = false; // set enabled to false
}
pie.value(function(d) {
if (d.label === label) d.enabled = enabled; // if entry label matches legend label
return (d.enabled) ? d.count : 0; // update enabled property and return count or 0 based on the entry's status
});
path = path.data(pie(dataset)); // update pie with new data
path.transition() // transition of redrawn pie
.duration(750) //
.attrTween('d', function(d) { // 'd' specifies the d attribute that we'll be animating
var interpolate = d3.interpolate(this._current, d); // this = current path element
this._current = interpolate(0); // interpolate between current value and the new value of 'd'
return function(t) {
return arc(interpolate(t));
};
});
// calculate new total
var newTotalCalc = d3.sum(dataset.filter(function(d) { return d.enabled;}), d => d.count)
// console.log(newTotalCalc);
// append newTotalCalc to newTotal which is defined above
newTotal.text(newTotalCalc);
});
// adding text to legend
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return d; }); // return label
body {
font-family: 'Open Sans Condensed', sans-serif;
}
.title-holder {
text-align: center;
}
.title {
font-family: 'Pacifico', cursive;
}
.font {
font-size: 20px;
}
/* legend */
.legend {
font-size: 14px;
}
rect {
cursor: pointer;
stroke-width: 2;
}
rect.disabled {
fill: transparent !important;
}
/* chart */
#chart {
height: 800px;
margin: 0 auto;
position: relative;
display: block;
width: 800px;
}
/* tooltip */
.tooltip {
background: #eee;
box-shadow: 0 0 5px #999999;
color: #333;
display: none;
font-size: 18px;
left: 130px;
padding: 10px;
position: absolute;
text-align: center;
top: 95px;
width: 80px;
z-index: 10;
}
.footer {
padding-top: 50px;
text-align: center;
list-style-type: none;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3.js Donut Chart</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300|Pacifico" rel="stylesheet">
<link href="styles.css" rel="stylesheet">
</head>
<body>
<div class="title-holder">
<h1 class="title">2016 Average Wedding Budget Breakdown</h1>
<p class="font">Uncheck categories to recalculate.</p>
<p class="font new-total-holder">New Total:</p>
</div>
<div id="chart"></div>
<footer>
Data Source</li>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
<script src="script.js"></script> <!-- remove if no javascript -->
</body>
</html>
Hope this helps. :)
The code in the link is for two pie charts. http://jsbin.com/vipodidiyo/edit?html,css,js,outputThey are almost the same just different data and positions. When user mouseover a piece of pie, that piece and tooltip will pup up in that pie chart.
My question is... when user mouseover a piece of pie, how can I make the popups happen in both of the charts. For example, if user mouseover the green part in the first pie chart, the green parts and tooltips in both charts will show up at the same time.
How can I make two charts connected? Does anyone have clue about this? Appreciate!!
Updated
I found a similar question here and try to change the code follow Lars's answer. Interactions that affect multiple separate charts in d3.js?
I assigned a class name 'path', and changed d3.select(this) to d3.select('.path'). But wherever my mouse is, the purple part of first pie chart pops up.
Here is my updated JSbins http://jsbin.com/gozuyekibi/edit?html,css,js,output
var path = svg.selectAll('path').
data(pie(data)).
enter().
append('path').
attr('d', arc).
attr('class','path').
//attr('id','pathid').
attr('fill', function(d,i){
return color(d.data.label);}).
on('mouseover', function(d) {
d3.select('.path').transition()
.duration(1000)
.attr("d", arcHover);
var total = d3.sum(data.map(function(d) {
return d.count;
}));
var percent = Math.round(1000 * d.data.count / total) / 10;
tooltip.select('.label').html(d.data.label);
tooltip.select('.count').html(d.data.count);
tooltip.select('.percent').html(percent + '%');
tooltip.style('display', 'block');
}).
on('mouseout', function() {
d3.select('.path').transition()
.duration(500)
.attr("d", arc);
tooltip.style('display', 'none');
});
First, assign a single class to your tooltips and clean up that css. Next, assign each arc path a class so that your can pair your paths on mouseover. Then generalize your mouseover to operate on both paths:
.on('mouseover', function(d0) {
// apply over to both paths
d3.selectAll('path.' + d0.data.label).transition()
.duration(1000)
.attr("d", arcHover)
// now loop each of them
.each(function(d1) {
// get a sum for the group of paths
var total = d3.sum(this.parentNode.childNodes, function(d2) {
return d2.value;
});
// and a percent
var percent = Math.round(1000 * d1.value / total) / 10;
// find correct tooltip
var tooltip = d3.select(this.ownerSVGElement.parentNode.childNodes[1]);
tooltip.select('.label').html(d1.data.label);
tooltip.select('.count').html(d1.data.count);
tooltip.select('.percent').html(percent + '%');
tooltip.style('display', 'block');
})
})
.on('mouseout', function(d) {
// apply to both paths
d3.selectAll('path.' + d.data.label).transition()
.duration(500)
.attr("d", arc);
// hide all tooltips
d3.selectAll('.tooltip').style('display', 'none');
});
Full code:
<!DOCTYPE html>
<html>
<head>
<style>
#pieChart1 {
height: 360px;
position: relative;
width: 360px;
}
.tooltip {
background: #fdd0a2;
box-shadow: 0 0 5px #999999;
color: #333;
display: none;
left: 300px;
padding: 10px;
position: absolute;
text-align: center;
width: 80px;
z-index: 10;
}
#tooltip1 {
top: 220px;
}
#tooltip2 {
top: 580px;
}
</style>
</head>
<body>
<div id="pieChart1"></div>
<div id="pieChart2"></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var data1 = [{
label: 'Station1',
count: 10
}, {
label: 'Station2',
count: 20
}, {
label: 'Station3',
count: 30
}];
var data2 = [{
label: 'Station1',
count: 15
}, {
label: 'Station2',
count: 80
}, {
label: 'Station3',
count: 20
}];
var drawPieChartFunction = function(data, chartId, tooltipName) {
var margin = {
top: 20,
right: 40,
bottom: 120,
left: 80
},
width = 700 - margin.right - margin.left,
height = 500 - margin.top - margin.bottom;
var radius = Math.min(width, height) / 2;
var donutWidth = 105;
var legendRectSize = 18;
var legendSpacing = 4;
var color = d3.
scale.
ordinal().
range(['#98df8a', '#c5b0d5', '#9edae5']).
domain(d3.keys(data[0]).filter(function(key) {
return key === 'label';
}));
var svg = d3.
select(chartId).
append('svg').
attr({
'width': width + margin.right + margin.left,
'height': height + margin.top + margin.bottom
}).
append('g').
attr('transform', 'translate(' + ((width + margin.right + margin.left) / 2) +
',' + ((height + margin.top + margin.bottom) / 2) + ')');
var arc = d3.svg.arc().
innerRadius(radius - donutWidth).
outerRadius(radius);
var arcHover = d3.svg.arc().
innerRadius(radius - donutWidth).
outerRadius(radius + 10);
var pie = d3.layout.pie().
value(function(d) {
return d.count;
});
var tooltip = d3.select(chartId)
.append('div')
.attr('class', 'tooltip')
.attr('id', tooltipName);
tooltip.append('div')
.attr('class', 'label');
tooltip.append('div')
.attr('class', 'count');
tooltip.append('div')
.attr('class', 'percent');
var path = svg.selectAll('path').
data(pie(data)).
enter().
append('path').
attr('d', arc).
attr('class', function(d) {
return d.data.label;
}).
attr('fill', function(d, i) {
return color(d.data.label);
}).
on('mouseover', function(d0) {
d3.selectAll('path.' + d0.data.label).transition()
.duration(1000)
.attr("d", arcHover)
.each(function(d1) {
var total = d3.sum(this.parentNode.childNodes, function(d2) {
return d2.value;
});
var percent = Math.round(1000 * d1.value / total) / 10;
// find correct tooltip
var tooltip = d3.select(this.ownerSVGElement.parentNode.childNodes[1]);
tooltip.select('.label').html(d1.data.label);
tooltip.select('.count').html(d1.data.count);
tooltip.select('.percent').html(percent + '%');
tooltip.style('display', 'block');
})
}).
on('mouseout', function(d) {
d3.selectAll('path.' + d.data.label).transition()
.duration(500)
.attr("d", arc);
d3.selectAll('.tooltip').style('display', 'none');
});
return path;
};
drawPieChartFunction(data1, '#pieChart1', 'tooltip1');
drawPieChartFunction(data2, '#pieChart2', 'tooltip2');
</script>
</body>
</html>
I'm wrestling with a problem of a brush not being removed correctly on a bar chart. You can see the Bl.ock here and see what's not working correctly.
In short, the brush highlights the bars that have been selected by the brush, as well as snaps to the edge of the rect to make selecting spans of time easier (there's a secondary bug here where the brush snapping isn't quite mapping correctly to the dates -- you'll see this if you try to draw the brush up to the edge of the barchart). Somewhere along the way (maybe with the rect snapping?) the background click-to-remove-brush feature stopped working (it now selects a single year span, although doesn't highlight the rect correctly). To make it easier for users, I wanted to add a button that a user can click to remove the brush when they're done (the resetBrush() function below).
My understanding was the brush selection can be cleared with brush.extent(), but when you clear the extent you then have to redraw the brush. I thought I was doing that correctly, but alas, I'm running into some problem somewhere that I can't seem to track down. Any pointers on where I'm tripping up would be greatly appreciated!
Code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: sans-serif;
color: #000;
text-rendering: optimizeLegibility;
}
.barchart {
z-index: 30;
display: block;
visibility: visible;
position: relative;
padding-top: 15px;
margin-top: 15px;
}
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.resize path {
fill: #666;
fill-opacity: .8;
stroke: #000;
stroke-width: 1.5px;
}
.brush .extent {
stroke: #fff;
stroke-opacity: .6;
stroke-width: 2px;
fill-opacity: .1;
shape-rendering: crispEdges;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
brushYearStart = 1848;
brushYearEnd = 1905;
// Scales
var x = d3.scale.ordinal().rangeRoundBands([0, width - 60], .1);
var y = d3.scale.linear().range([height, 0]);
// Prepare the barchart canvas
var barchart = d3.select("body").append("svg")
.attr("class", "barchart")
.attr("width", "100%")
.attr("height", height + margin.top + margin.bottom)
.attr("y", height - height - 100)
.append("g");
var z = d3.scale.ordinal().range(["steelblue", "indianred"]);
var brushYears = barchart.append("g")
brushYears.append("text")
.attr("id", "brushYears")
.classed("yearText", true)
.text(brushYearStart + " - " + brushYearEnd)
.attr("x", 35)
.attr("y", 12);
d3.csv("years_count.csv", function (error, post) {
// Coercion since CSV is untyped
post.forEach(function (d) {
d["frequency"] = +d["frequency"];
d["frequency_discontinued"] = +d["frequency_discontinued"];
d["year"] = d3.time.format("%Y").parse(d["year"]).getFullYear();
});
var freqs = d3.layout.stack()(["frequency", "frequency_discontinued"].map(function (type) {
return post.map(function (d) {
return {
x: d["year"],
y: +d[type]
};
});
}));
x.domain(freqs[0].map(function (d) {
return d.x;
}));
y.domain([0, d3.max(freqs[freqs.length - 1], function (d) {
return d.y0 + d.y;
})]);
// Axis variables for the bar chart
x_axis = d3.svg.axis().scale(x).tickValues([1850, 1855, 1860, 1865, 1870, 1875, 1880, 1885, 1890, 1895, 1900]).orient("bottom");
y_axis = d3.svg.axis().scale(y).orient("right");
// x axis
barchart.append("g")
.attr("class", "x axis")
.style("fill", "#000")
.attr("transform", "translate(0," + height + ")")
.call(x_axis);
// y axis
barchart.append("g")
.attr("class", "y axis")
.style("fill", "#000")
.attr("transform", "translate(" + (width - 85) + ",0)")
.call(y_axis);
// Add a group for each cause.
var freq = barchart.selectAll("g.freq")
.data(freqs)
.enter().append("g")
.attr("class", "freq")
.style("fill", function (d, i) {
return z(i);
})
.style("stroke", "#CCE5E5");
// Add a rect for each date.
rect = freq.selectAll("rect")
.data(Object)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function (d) {
return x(d.x);
})
.attr("y", function (d) {
return y(d.y0) + y(d.y) - height;
})
.attr("height", function (d) {
return height - y(d.y);
})
.attr("width", x.rangeBand())
.attr("id", function (d) {
return d["year"];
});
// Draw the brush
brush = d3.svg.brush()
.x(x)
.on("brush", brushmove)
.on("brushend", brushend);
var arc = d3.svg.arc()
.outerRadius(height / 15)
.startAngle(0)
.endAngle(function(d, i) { return i ? -Math.PI : Math.PI; });
brushg = barchart.append("g")
.attr("class", "brush")
.call(brush);
brushg.selectAll(".resize").append("path")
.attr("transform", "translate(0," + height / 2 + ")")
.attr("d", arc);
brushg.selectAll("rect")
.attr("height", height);
});
// ****************************************
// Brush functions
// ****************************************
function brushmove() {
y.domain(x.range()).range(x.domain()).clamp(true);
b = brush.extent();
brushYearStart = Math.ceil(y(b[0]));
brushYearEnd = Math.ceil(y(b[1]));
// Snap to rect edge
d3.select("g.brush").call(brush.extent([y.invert(brushYearStart), y.invert(brushYearEnd)]));
// Fade all years in the histogram not within the brush
d3.selectAll("rect.bar").style("opacity", function (d, i) {
return d.x >= brushYearStart && d.x < brushYearEnd ? "1" : ".4"
});
}
function brushend() {
// Additional calculations happen here...
// filterPoints();
// colorPoints();
// styleOpacity();
// Update start and end years in upper right-hand corner of the map
d3.select("#brushYears").text(brushYearStart + " - " + brushYearEnd);
}
function resetBrush() {
d3.selectAll(".brush").remove();
d3.selectAll("brushg.resize").remove();
brush.clear();
brushg.call(brush);
}
</script>
<div id="resetMap">
<button
id="returnBrush"
class="btn btn-default"
onclick="resetBrush()"/>Remove Brush
</div>
</body>
</html>
If you execute d3.selectAll(".brush").remove(); you remove <g class="brush"></g> and his childs.
This d3.selectAll("brushg.resize").remove(); is a bug. Must to be brushg.selectAll(".resize").remove(); but is the same case that d3.selectAll(".brush").remove();.
You have to do this:
For reset the brush.extent() and fire the brush event.
function resetBrush() {
brush
.clear()
.event(d3.select(".brush"));
}
For reset #brushYears to the initial state
function brushend() {
var localBrushYearStart = (brush.empty()) ? brushYearStart : Math.ceil(y(b[0])),
localBrushYearEnd = (brush.empty()) ? brushYearEnd : Math.ceil(y(b[1]));
// Update start and end years in upper right-hand corner of the map
d3.select("#brushYears").text(localBrushYearStart + " - " + localBrushYearEnd);
}
For reset to initial values on brush event
function brushmove() {
y.domain(x.range()).range(x.domain()).clamp(true);
b = brush.extent();
3.1. To set the localBrushYearStart and localBrushYearEnd variables to initial state on brush.empty() or set to Math.ceil(brush.extent()))
var localBrushYearStart = (brush.empty()) ? brushYearStart : Math.ceil(y(b[0])),
localBrushYearEnd = (brush.empty()) ? brushYearEnd : Math.ceil(y(b[1]));
3.2. To execute brush.extent() on selection, or brush.clear() on brush.empty()
// Snap to rect edge
d3.select("g.brush").call((brush.empty()) ? brush.clear() : brush.extent([y.invert(localBrushYearStart), y.invert(localBrushYearEnd)]));
3.3. To set opacity=1 years on brush.empty() or selection, and opacity=.4 on not selected years
// Fade all years in the histogram not within the brush
d3.selectAll("rect.bar").style("opacity", function(d, i) {
return d.x >= localBrushYearStart && d.x < localBrushYearEnd || brush.empty() ? "1" : ".4";
});
}
Check the corrections on my BL.OCKS
Just do this
function resetBrush() {
d3.select("g.brush").call(brush.extent([0, 0]))
d3.selectAll("rect.bar").style("opacity", "0.4");
//reset year labels at top
}