I'm quite new to D3 and been working through trying to figure everything out. I'm trying to configure this example here to update with new data and transition appropriately.
Here is the code pen I have configured (click the submit to update) http://codepen.io/anon/pen/pbjLRW?editors=1010
From what I can gather, using some variation of .exit() is required for a clean data transition, but after reading some tutorials I'm still finding it difficult to know how it works. I have seen examples where simply removing the containers before calling the draw function works, but in my limited experience it can cause a flicker when changing data so I'm not sure if it's best practice?
Now, I'm not sure why the data is not updating correctly in my codepen, but my main concern is trying to get the transition right. Ideally I would like to know how I could just move the needle when changing data, so It would go from 90 > 40 for example, instead of 90 > 0 > 40.
However, I will definitely settle for figuring out why it doesn't redraw itself in the same location once clicking submit in the linked codepen.
Here is my update function;
function updateGuage() {
d3.selectAll("text").remove()
d3.selectAll('.needle').remove()
chart.remove()
name = "qwerty";
value = "25";
drawGuage();
}
initial draw;
function drawGuage() {
percToDeg = function(perc) {
return perc * 360;
};
percToRad = function(perc) {
return degToRad(percToDeg(perc));
};
degToRad = function(deg) {
return deg * Math.PI / 180;
};
// Create SVG element
svg = el.append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom);
// Add layer for the panel
chart = svg.append('g').attr('transform', "translate(" + ((width + margin.left) / 2) + ", " + ((height + margin.top) / 2) + ")");
chart.append('path').attr('class', "arc chart-first");
chart.append('path').attr('class', "arc chart-second");
chart.append('path').attr('class', "arc chart-third");
arc3 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
arc2 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
arc1 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
repaintGauge = function() {
perc = 0.5;
var next_start = totalPercent;
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 3);
next_start += perc / 3;
arc1.startAngle(arcStartRad).endAngle(arcEndRad);
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 3);
next_start += perc / 3;
arc2.startAngle(arcStartRad + padRad).endAngle(arcEndRad);
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 3);
arc3.startAngle(arcStartRad + padRad).endAngle(arcEndRad);
chart.select(".chart-first").attr('d', arc1);
chart.select(".chart-second").attr('d', arc2);
chart.select(".chart-third").attr('d', arc3);
}
/////////
var texts = svg.selectAll("text")
.data(dataset)
.enter();
texts.append("text")
.text(function() {
return dataset[0].metric;
})
.attr('id', "Name")
.attr('transform', "translate(" + ((width + margin.left) / 6) + ", " + ((height + margin.top) / 1.5) + ")")
.attr("font-size", 25)
.style("fill", "#000000");
var trX = 180 - 210 * Math.cos(percToRad(percent / 2));
var trY = 195 - 210 * Math.sin(percToRad(percent / 2));
// (180, 195) are the coordinates of the center of the gauge.
displayValue = function() {
texts.append("text")
.text(function() {
return dataset[0].value;
})
.attr('id', "Value")
.attr('transform', "translate(" + trX + ", " + trY + ")")
.attr("font-size", 18)
.style("fill", '#000000');
}
texts.append("text")
.text(function() {
return 0;
})
.attr('id', 'scale0')
.attr('transform', "translate(" + ((width + margin.left) / 100) + ", " + ((height + margin.top) / 2) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return gaugeMaxValue / 2;
})
.attr('id', 'scale10')
.attr('transform', "translate(" + ((width + margin.left) / 2.15) + ", " + ((height + margin.top) / 30) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return gaugeMaxValue;
})
.attr('id', 'scale20')
.attr('transform', "translate(" + ((width + margin.left) / 1.03) + ", " + ((height + margin.top) / 2) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
var Needle = (function() {
//Helper function that returns the `d` value for moving the needle
var recalcPointerPos = function(perc) {
var centerX, centerY, leftX, leftY, rightX, rightY, thetaRad, topX, topY;
thetaRad = percToRad(perc / 2);
centerX = 0;
centerY = 0;
topX = centerX - this.len * Math.cos(thetaRad);
topY = centerY - this.len * Math.sin(thetaRad);
leftX = centerX - this.radius * Math.cos(thetaRad - Math.PI / 2);
leftY = centerY - this.radius * Math.sin(thetaRad - Math.PI / 2);
rightX = centerX - this.radius * Math.cos(thetaRad + Math.PI / 2);
rightY = centerY - this.radius * Math.sin(thetaRad + Math.PI / 2);
return "M " + leftX + " " + leftY + " L " + topX + " " + topY + " L " + rightX + " " + rightY;
};
function Needle(el) {
this.el = el;
this.len = width / 2.5;
this.radius = this.len / 8;
}
Needle.prototype.render = function() {
this.el.append('circle').attr('class', 'needle-center').attr('cx', 0).attr('cy', 0).attr('r', this.radius);
return this.el.append('path').attr('class', 'needle').attr('id', 'client-needle').attr('d', recalcPointerPos.call(this, 0));
};
Needle.prototype.moveTo = function(perc) {
var self,
oldValue = this.perc || 0;
this.perc = perc;
self = this;
// Reset pointer position
this.el.transition().delay(100).ease('quad').duration(200).select('.needle').tween('reset-progress', function() {
return function(percentOfPercent) {
var progress = (1 - percentOfPercent) * oldValue;
repaintGauge(progress);
return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
};
});
this.el.transition().delay(300).ease('bounce').duration(1500).select('.needle').tween('progress', function() {
return function(percentOfPercent) {
var progress = percentOfPercent * perc;
repaintGauge(progress);
return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
};
});
};
return Needle;
})();
needle = new Needle(chart);
needle.render();
needle.moveTo(percent);
setTimeout(displayValue, 1350);
}
Any help/advice is much appreciated,
Thanks
What you wanna check out is How selections work written by Mike Bostock. After reading this article, everything around enter, update and exit selections will become clearer.
In a nutshell:
selectAll('li')
data([...])
__data__
property that allows D3 to bind a data item to an element.enter()
. This is every data element that has not yet been bound to the selected DOM elements. Typically you use the enter selection to create new elements, e.g. through append()
exit()
you receive the exit selection. These are all already existing DOM elements which no longer have an associated data item after the join. Typically you use the exit selection to remove DOM elements with remove()
data()
. You will want to store the update selection in a variable, so you have access to it even after calling enter()
or exit()
. In d3v3, when you've already added elements via the enter selection, the update selection includes those newly created DOM elements as well. It's crucial to know that the update selection changes after you've created new elements.
However, this is no longer true when using d3v4. The change log says
"In addition, selection.append no longer merges entering nodes into the update selection; use selection.merge to combine enter and update after a data join."
It is important to know that there are three different operations you can perform after binding data. Handle additions, deletions and modify things that did not change (or have been added just before).
Here is an example creating and manipulating a simple list: http://jsbin.com/sekuhamico/edit?html,css,js,output
var update = () => {
// bind data to list elements
// think of listWithData as a virtual representation of
// the array of list items you will later see in the
// DOM. d3.js does not handle the mapping from this
// virtual structure to the DOM for you. It is your task
// to define what is to happen with elements that are
// added, removed or updated.
var listWithData = ul.selectAll('li').data(listItems);
// handle additions
// by calling enter() on our virtual list, you get the
// subset of entries which need to be added to the DOM
// as their are not yet present there.
listWithData.enter().append('li').text(i => i.text).on('click', i => toggle(i));
// handle removal
// by calling exit() on our virtual list, you get the
// subset of entries which need to be removed from the
// DOM as they are not longer present in the virtual list.
listWithData.exit().remove();
// update existing
// acting directly on the virtual list will update any
// elements currently present in the DOM. If you would
// execute this line before calling exit(), you would
// also manipulate those items to be removed. If you
// would even call it before calling enter() you would
// miss on updating the newly added element.
listWithData.attr('class', i => i.active ? 'active' : '');
};
Be aware that in reality you probably need to add some sort of id to your items. To ensure the right items are removed and you do not get ordering issues.
The update function knows nothing about what, or even if anything has changed. It does not know, nor care, if there are new data elements or if old ones have been removed. But both things could happen. Therefore we handle both cases by calling enter()
and exit()
respectively. The d3 functions enter()
and exit()
provides us with the subsets of list elements that should be added or removed. Finally we need to take care of changes in the existing data.
var listItems = [{ text: 1, active: false}, { text: 2, active: true}];
var ul = d3.select('#id').append('ul');
var update = () => {
var listWithData = ul.selectAll('li').data(listItems);
// add new
listWithData.enter().append('li').text(i => i.text).on('click', i => toggle(i));
// remove old
listWithData.exit().remove();
// update existing
listWithData.attr('class', i => i.active ? 'active' : '');
};
update();
$('#add').click(() => {
listItems.push({
text: listItems.length+1,
active: false
});
update();
});
var toggle = (i) => {
i.active = !i.active;
update();
};
li.active {
background-color:lightblue;
}
li {
padding: 5px;
}
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div id="id"></div>
<button id="add">Add</button>
</body>
</html>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With