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. :)
I am trying to do something similar to this question d3.js how to dynamically add nodes to a tree. However, I'm finding it really difficult to get any kind of solution working in v4 of d3.js.
I am building a tree from some preloaded json, and then i want to be able to add child nodes onto the tree when the user clicks on a node. The process of adding child nodes will involve a call to a REST service, which will return some json. Most of my code is based heavily on http://bl.ocks.org/mbostock/4339083 which is an expandable/collapsible tidy tree using v3 and https://bl.ocks.org/mbostock/9d0899acb5d3b8d839d9d613a9e1fe04 which is a non expandable/collapsible tidy tree using v4.
My code is here https://jsfiddle.net/rkian/3e73Lz6d/. I can draw the tree out no problem but I'm really having trouble working out how to add nodes to the tree. I suspect this:
function click(d) {
if (!d.children && !d._children) {
var jsonChildren = $.parseJSON('...');
d.data.children = jsonChildren;
d.data._children = jsonChildren;
updateTree(d);
}
is key to it all but I'm just not able to work out how to implement it.
There was something odd with the lineage variable (flat array to be turned into hierarchical structure) which I was trying to build my tree up from, when I turned it into a tree it changed the objects within it. I solved this by making a deep copy of the lineage. All of my code is here, I hope it helps someone:
var lineage = $.parseJSON('[{"id":739,"name":"Life","parent":null,"rank":9,"child_count":1},{"id":1,"name":"Eukaryota","parent":739,"rank":1,"child_count":3},{"id":43,"name":"Animalia","parent":1,"rank":2,"child_count":9},{"id":2,"name":"Archaeplastida","parent":1,"rank":2,"child_count":1},{"id":740,"name":"Plantae","parent":1,"rank":2,"child_count":0},{"id":417,"name":"Annelida","parent":43,"rank":3,"child_count":1},{"id":336,"name":"Arthropoda","parent":43,"rank":3,"child_count":6},{"id":228,"name":"Chordata","parent":43,"rank":3,"child_count":3},{"id":222,"name":"Cnidaria","parent":43,"rank":3,"child_count":4},{"id":328,"name":"Mollusca","parent":43,"rank":3,"child_count":1},{"id":548,"name":"Myzozoa","parent":43,"rank":3,"child_count":1},{"id":604,"name":"Nematoda","parent":43,"rank":3,"child_count":1},{"id":467,"name":"Platyhelminthes","parent":43,"rank":3,"child_count":2},{"id":275,"name":"Porifera","parent":43,"rank":3,"child_count":2},{"id":418,"name":"Polychaeta","parent":417,"rank":4,"child_count":3},{"id":419,"name":"Spionida","parent":418,"rank":5,"child_count":1},{"id":452,"name":"Sabellida","parent":418,"rank":5,"child_count":1},{"id":448,"name":"Terebellida","parent":418,"rank":5,"child_count":1},{"id":420,"name":"Spionidae","parent":419,"rank":6,"child_count":2},{"id":699,"name":"Boccardia","parent":420,"rank":7,"child_count":1},{"id":421,"name":"Polydora","parent":420,"rank":7,"child_count":1},{"id":700,"name":"Boccardia proboscidea","parent":699,"rank":8,"child_count":0}]');
$(document).ready(function() {
// WTF js.
var lineage_flat;
// Color manipulation functions and settings for the tree
function shadeColor2(color, percent) {
var f=parseInt(color.slice(1),16),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=f>>16,G=f>>8&0x00FF,B=f&0x0000FF;
return "#"+(0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1);
}
function blendColors(c0, c1, p) {
var f=parseInt(c0.slice(1),16),t=parseInt(c1.slice(1),16),R1=f>>16,G1=f>>8&0x00FF,B1=f&0x0000FF,R2=t>>16,G2=t>>8&0x00FF,B2=t&0x0000FF;
return "#"+(0x1000000+(Math.round((R2-R1)*p)+R1)*0x10000+(Math.round((G2-G1)*p)+G1)*0x100+(Math.round((B2-B1)*p)+B1)).toString(16).slice(1);
}
// Color variables
var main_color = '#FFFFFF';
var secondary_color = '#FFFFFF';
var line_color = '#FFF793';
var max_rank = lineage[lineage.length - 1]['rank'];
// Used to assign colors to ranks
function get_rank_colour(d) {
rank = d.data.data.rank;
if(rank == 9) {
return main_color;
}
else {
return blendColors(main_color, secondary_color, (d.data.rank / max_rank))
}
}
// Draws a curve between two points
function connector(d) {
return 'M' + d.x + ',' + (d.y - 18) +
"C" + (d.x + d.parent.x) / 2 + "," + (d.y - 25) +
" " + (d.x + d.parent.x) / 2 + "," + (d.parent.y + 25) +
" " + d.parent.x + "," + (d.parent.y + 17);
};
// Transition vars
var duration = 500;
// Get the width of the container element for the tree
width = $('#svgcontainer').width();
// Calculate the height required based on the number of node levels on the ancestry tree
height = lineage[lineage.length - 1]['rank']* 150;
// Set the svg element's height
$('#lifetree').attr('height', height + 'px');
// Select the svg element in the DOM
var svg = d3.select("svg")
// Insert a group container and move it 40 px to the right (to pad the tree contents in the svg container)
var g = svg.append("g").attr("transform", "translate(40,40)");
// Create and return a d3 tree object of the correct width and height, run the root hierarchy element through it
var tree = d3.tree().size([width-200, height - 160]);
var root = getTreeData(lineage);
updateTree(root);
// Get the data for the tree
function getTreeData(json) {
// Save the flat lineage, we have to do this weird parse thing to make a deep copy
lineage_flat = JSON.parse(JSON.stringify(json));
// This seems to unflatten arrays of objects with parentIds and parents. Wish I'd known about it sooner.
var dataTree = d3.stratify()
.id(function(d){ return d.id; })
.parentId(function(d){ return d.parent; })
(JSON.parse(JSON.stringify(json)));
// D3 requires a hierarchy object which then gets made into a tree
var root = d3.hierarchy(dataTree);
tree(root);
// Normalize for fixed-depth, also we do some fancy transitions so save a copy of original xys
root.each(function(d) { d.y = d.depth * 100; d.x0 = d.x; d.y0 = d.y; });
return root;
}
function drawElements(node) {
// Add circles above each node
node.append("circle")
.attr("r", 2)
.attr("transform", function(d) { return "translate(0,-18)"; })
.attr("class", "upper-circle")
.style("stroke", get_rank_colour)
.style("fill", get_rank_colour);
// Add the circles below each node
node.append("circle")
.attr("r", 4)
.attr("transform", function(d) { return "translate(0,16)"; })
.attr("class", "lower-circle")
.style("stroke", "#000000")
.style("fill", function(d) {
return d.data.data.child_count > 0 ? "#FFFFFF" : "#000000";
});
// Add text
node.append("text")
.attr("dy", 3)
.style("fill", '#FFFFFF')
.style("text-anchor", "middle")
.text(function(d) {
return d.data.data.name;
if(d.data.rank == max_rank || d.data.name == "Life") {
return d.data.name;
}
else if(d.children) {
return d.data.name + ' (' + d.children.length + ")";
}
else {
return d.data.name + ' (' + d.data.count + ")";
}
})
.each(function(d) {
d.textwidth = this.getBBox().width;
d.textheight = this.getBBox().height;
});
// Add clickable background rectangle so it looks nicer
node.insert("rect",":first-child")
.style("fill", '#000000')
.style("fill-opacity", function(d) {
if(d.children || d.data.data.rank == max_rank) { return 0.5; }
else { return 0.2; }
}
)
.attr('height', function(d) { return d.textheight + 10; })
.attr('width', function(d) { return d.textwidth + 10; })
.attr("transform", function(d) {
if(d.data.data.rank == 9) {
return "translate(-" + ((d.textwidth + 10) / 2) + ",-" + ((d.textheight + 30) / 2) + ")";
}
return "translate(-" + ((d.textwidth + 10) / 2) + ",-" + ((d.textheight + 15) / 2) + ")";
})
.attr('rx', 10)
.attr('ry', 10);
}
function updateTree(source, shallowestDepth = 0) {
/*
* Nodes
*/
// Data join with source data, keeping ids so it knows about the same nodes
var node = g.selectAll(".node")
.data(source.descendants() , function(d) { return d.data.id; });
// Data enter, this starts doing things to all the new nodes
var nodeEnter = node.enter()
.append("g")
.attr("class", function(d) { return "rank-" + d.data.data.rank + " node" + (d.children ? " node--internal" : " node--leaf"); })
.attr("transform", function(d) {
if(d.parent != null) {
return "translate(" + d.parent.x + "," + d.parent.y + ")";
}
return "translate(" + d.x + "," + d.y + ")";
})
.on("click", click);
// Add text + bg + circles to the nodes
drawElements(nodeEnter);
// Add pretty hover class for each taxon node
$('g').hover(function() {
$(this).children('rect').addClass('recthover');
}, function() {
$(this).children('rect').removeClass('recthover');
});
// Transition nodes to their new position.
var nodeMerge = node.merge(nodeEnter).transition()
.duration(duration)
.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
nodeMerge.select('rect', ':first-child').style("fill-opacity", function(d) {
if(d.children || d.data.data.rank == max_rank) { return 0.5; }
else { return 0.2; }
});
// Get the old elements for removal
var oldNode = node.exit();
// Find the shallowest depth in the old element, that's the parent
oldNode.each(function(d) {
var shallowestParent = d;
do { shallowestParent = shallowestParent.parent; }
while(shallowestParent.depth > shallowestDepth);
d.shallowestParentX = shallowestParent.x;
d.shallowestParentY = shallowestParent.y;
});
// Transition the old nodes out
var transitionedNodes = oldNode.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.shallowestParentX + "," + d.shallowestParentY + ")";
});
oldNode.selectAll('rect').transition()
.style("fill-opacity", 0)
.duration(duration/2)
oldNode.selectAll('text').transition()
.style("fill-opacity", 0)
.duration(duration/2)
oldNode.selectAll('circle').transition()
.style("fill-opacity", 0)
.duration(duration/3)
transitionedNodes.remove();
/*
* Links
*/
var link = g.selectAll(".link")
.data(source.descendants().slice(1).reverse(), function(d) { return d.data.id; })
// Draw the links between nodes
var linkEnter = link.enter()
.insert("path",":first-child")
.attr("class", "link")
.style("stroke", function(d) {
if(d.children) {
return line_color;
}
return blendColors(main_color, secondary_color, (d.data.data.rank / max_rank))
})
.attr("d", function (d) {
var o = {x: d.parent.x0, y: d.parent.y0, parent: {x: d.parent.x0, y: d.parent.y0}};
return connector(o);
});
// Transition links to their new position.
var linkMerge = link.merge(linkEnter).transition()
.duration(0)
.attr('d', connector);
// Style the links
linkMerge.style("stroke", function(d) {
if(d.children) {
return line_color;
}
return blendColors(main_color, secondary_color, (d.data.data.rank / max_rank))
})
// Transition the old links out
var oldLink = link.exit();
oldLink.transition()
.duration(duration/2)
.attr("d", function(d) {
var o = {x: d.x, y: d.y, parent: {x: d.x, y: d.y}};
return connector(o);
})
.remove();
}
// Toggle children on click.
function click(d) {
// If the node does not have any pre-loaded children
if (!d.children && !d._children) {
var jsonPath = '/taxa/api/children/' + d.data.id;
// Get the JSON lineage for it
d3.json(jsonPath, function(error, json) {
// Get the children
children = json['children'];
// Make a new lineage array, can't use the one previously stored because
// of javascript variables mutability being weird
new_lineage = [];
lineage_flat.forEach(function(node, i) {
recalcIndex = lineage.indexOf(node);
// The node.rank 9 is in there because Life for some reason has rank 9
// Basically we want to exclude all nodes of a lower rank than the one clicked
if(node.rank > d.data.data.rank && node.rank != 9) {
// console.log(node); - we don't want these nodes
}
else { new_lineage.push(node); }
});
// Append the children to the new lineage
children.forEach(function(child) {
new_lineage.push(child);
});
// Javascript is weird. We need a deep copy of new_lineage
temp = JSON.parse(JSON.stringify(new_lineage));
new_lineage = JSON.parse(JSON.stringify(temp));
// Turn it into a tree and update our svg
root = getTreeData(new_lineage);
updateTree(root, d.depth);
});
}
}
});
.link {
fill: none;
stroke: orange;
stroke-width: 1.5px;
/* Transition. */
-o-transition:.5s;
-ms-transition:.5s;
-moz-transition:.5s;
-webkit-transition:.5s;
/* ...and now for the proper property */
transition:.5s;
}
.link:hover {
fill: none;
stroke: orange;
stroke-width: 3px;
}
.link.warning{
stroke: orange;
}
.lower-circle, .upper-circle {
z-index: 1;
stroke-width: 2px;
}
path.link {
stroke-width: 3px;
}
.rank-9 .upper-circle {
display: none;
}
.rank-9 text {
font-size: 3em;
font-weight: bold;
}
.rank-1 {
font-size: 1.5em;
font-weight: bold;
}
.rank-2 {
font-size: 1em;
font-weight: bold;
margin-right: 20px;
margin-left: 20px;
}
svg text {
}
.rank-3 {
font-size: 1em;
}
#triangles {
/*position: relative;
top: -20px;
margin-top: -50px;
padding-top: 80px;
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);*/
}
#svgcontainer {
position: relative;
padding-top: 80px;
/*-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);*/
}
#svgparent-disablethis {
background: transparent url('static/img/carousel/1.jpg' 0 0 no-repeat );
background: -moz-linear-gradient(top, rgba(255,255,255,0) 100%, rgba(130,91,0,1) 0%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0)), color-stop(100%,rgba(130,91,0,1))); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* IE10+ */
background: linear-gradient(to bottom, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#825b00',GradientType=0 ); /* IE6-9 */
}
svg { width: 100%; height: auto; }
rect {
/* Transition. */
-o-transition:.2s;
-ms-transition:.2s;
-moz-transition:.2s;
-webkit-transition:.2s;
/* ...and now for the proper property */
transition:.2s;
}
text {
cursor: pointer;
}
.lower-circle {
cursor: pointer;
}
.lower-circle:hover {
fill: #FFF !important;
}
.recthover {
fill: #FFEC1C !important;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="svgparent">
<div id="svgcontainer">
<svg id='lifetree'></svg>
</div>
</div>
Using d3.behavior.drag(), is there a way to enable a dragged image to be visible outside of its parent svg element borders.
In my app, I have a top layout based on an HTML grid (using flexBox) and several D3.js graphs located in each grid cell. Each graph is built with an SVG element and its childrens.
I need a drag and drop feature to enable copy/move of elements between these graphs. As now, the feature is working except that the drag image disappears when I cross the border of the source graph.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
float: left;
border-bottom: solid 1px #ccc;
border-right: solid 1px #ccc;
margin-right: -1px;
margin-bottom: -1px;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 240,
height = 125,
radius = 20;
var overSVG;
var drag = d3.behavior.drag()
.origin(function (d) {
return d;
})
.on("drag", dragmove)
.on("dragend", dragend);
var svg = d3.select("body").append("div").selectAll("svg")
.data(d3.range(2).map(function (v, i) {
return {
svgElement: i,
x : width / 2,
y : height / 2
};
}))
.enter().append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", function (d, i) {
return "svg_" + i
})
.on("mouseover", over)
.on("mouseout", out);
svg.append("circle")
.attr("r", radius)
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
.attr("svgElement", function (d, i) {
return i;
})
.call(drag);
function over(d, i) {
overSVG = d;
var selectedNodeId = "#svg_" + i;
d3.select(selectedNodeId)
.attr("fill", 'red');
}
function out(d, i) {
overSVG = "";
var selectedNodeId = "#svg_" + i;
d3.select(selectedNodeId)
.attr("fill", 'blue');
}
function dragmove(d) {
d3.select(this)
.attr("cx", d.x = d3.event.x)
.attr("cy", d.y = d3.event.y);
console.log("drag move", this, d3.event.x, ' ', d3.event.y);
}
function dragend(d) {
console.log("==>drag Ended :");
console.log(" dragged circle", this);
console.log(" from svg ", d);
console.log(" to", overSVG);
}
</script>
By default the svg elements have an overflow attribute set to hidden.
You can try setting the overflow attribute to visible
svg {
overflow: visible;
}
Again, without seeing a working example, it's hard to tell if this will work for you, but maybe it can help.
I am trying to create VIEWS for my TAXONOMIES like this - Collapsible Tree (http://bl.ocks.org/mbostock/4339083).
I installed D3 modules, imported all the D3 libraries and then I started to create a custom library. I created d3.[mynewlibrary].libraries.info file, [mynewlibrary].css and [mynewlibrary].js and uploaded under d3.[mynewlibrary] folder. However, I went back to VIEWS and couldn't be able to select [mynewlibrary].
I wonder what would be the best way to validate my codes written in my custom library and if the VIEWS will automatically add the new custom library for selection.
I am new to D3. Can anyone provide some help?
Here below are my js and css codes. Thanks!
/**
*#file
*Javascript for D3 Collapsible Tree Library.
*/
(function($) {
Drupal.d3.collapsibletree = function (select, setting) {
var vis = d3.select("#viz").append("svg:svg")
.attr("width", 400)
.attr("height", 300)
.append("svg:g")
.attr("transform", "translate(40, 0)"); // shift everything to the right
// Create a tree "canvas"
var tree = d3.layout.tree()
.size([300,150]);
var diagonal = d3.svg.diagonal()
// change x and y (for the left to right tree)
.projection(function(d) { return [d.y, d.x]; });
// Preparing the data for the tree layout, convert data into an array of nodes
var nodes = tree.nodes(treeData);
// Create an array with all the links
var links = tree.links(nodes);
console.log(treeData)
console.log(nodes)
console.log(links)
var link = vis.selectAll("pathlink")
.data(links)
.enter().append("svg:path")
.attr("class", "link")
.attr("d", diagonal)
var node = vis.selectAll("g.node")
.data(nodes)
.enter().append("svg:g")
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
// Add the dot at every node
node.append("svg:circle")
.attr("r", 3.5);
// place the name atribute left or right depending if children
node.append("svg:text")
.attr("dx", function(d) { return d.children ? -8 : 8; })
.attr("dy", 3)
.attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
.text(function(d) { return d.name; })
}
}
})(jQuery);
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 10px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
Hmm...
Without seeing your other files (.info), I can't be sure. But even if some of the JS is broken, this visualization should show up as an option in Views.
Are all your dependencies and versions correctly called in the .info file?
name = Collapsible Tree
description = D3 example Collapsible Tree library
files[js][] = collapsibletree.js
files[css][] = collapsibletree.css
version = 0.1
dependencies[] = d3.extend
dependencies[] = d3.tooltip
views[version] = 3.0
views[fields][rows][__data_type] = 2dnnv
views[fields][x_label][type] = string
views[settings] = views-settings.php