I have a problem with my first attempt at an Exit event. The code below draws one of three circles based on three different JSON files. The default is the medium sized circle. The drop-down menu allows you to select either a Small, Medium, or Large circle to be drawn next.
My problem is that I have not written the Exit mode correctly. If I comment out the exit() then each selection appears on the screen without deleting any of the previous elements, as you would expect. If the exit mode is not commented out then only the default (medium) circle is displayed.
This is likely very obvious to someone with D3JS experience. Please help me to learn where I am going wrong. Thanks so much!
- Tim
<body>
<select id = "opts">
<option value="circle1">Small</option>
<option value="circle2" selected="selected">Medium</option>
<option value="circle3">Large</option>
</select>
<script>
// data sources for small, med, large circles
var circle1 = "/MenuSelections/circle1.JSON";
var circle2 = "/MenuSelections/circle2.JSON";
var circle3 = "/MenuSelections/circle3.JSON";
function drawCirc(newData){
// Read in the JSON data
console.log(newData); // Confirms data source
d3.json(newData, function(dataset) {
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
//ENTER
var circles = svgContainer.selectAll("circle")
.data(dataset.circle)
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", function (d) {
console.log(d)
return d.x_axis; })
.attr("cy", function (d) { return d.y_axis; })
.attr("r", function (d) { return d.radius; })
.style("fill", function(d) { return d.color; });
//EXIT - NOT WORKING. Original remains, no new graph.
// if this section commented out: new circles draw in without remove of prev.
d3.select("body").selectAll("circle")
.data(dataset.circle)
.exit()
.remove();
});
}
drawCirc(circle2); // Initiate with default selection
// handle on click event for the ID (opts) of the menu item
d3.select('#opts')
.on('change', function() {
var newData = eval(d3.select(this).property('value'));
// console.log(newData);
drawCirc(newData); // Call with new selection.
});
The advice from both Lars and Ben got me on the right track, with two key parts to the solution. The first was understanding that exit() acts on the elements, not their data values, and I needed to add a key as suggested above. Once I fixed that I still had a problem because I was defining my svg container WITHIN the drawCirc function, so each new selection resulted in a new SVG 200x200 container.
Good information on Enter, Update, Exit can be found here
http://bost.ocks.org/mike/circles/ and here http://bl.ocks.org/mbostock/3808218
The book "Data Visualization with D3 and AngularJS" Chapter 1, pages 13-16 is also very helpful.
Here is my revised code.
<body>
<select id = "opts">
<option value="circle1">Small</option>
<option value="circle2" selected="selected">Medium</option>
<option value="circle3">Large</option>
</select>
<script>
// var names correspond to the value of each drop down item
var circle1 = "/MenuSelections/circle1.JSON";
var circle2 = "/MenuSelections/circle2.JSON";
var circle3 = "/MenuSelections/circle3.JSON";
var svg = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
function drawCirc(newData){
console.log(newData); // Confirms data source
// Read in the JSON data
d3.json(newData, function(dataset) {
//ENTER
var circle = svg.selectAll("circle")
.data(dataset.circle, function(d) {return d;});
circle.enter().append("circle");
circle.attr("cx", function (d) {
console.log(d)
return d.x_axis; })
.attr("cy", function (d) { return d.y_axis; })
.attr("r", function (d) { return d.radius; })
.style("fill", function(d) { return d.color; });
//EXIT
circle.exit().remove();
});
}
drawCirc(circle2); // Initiate with default selection
// handle on click event for the ID (opts) of the menu item
d3.select('#opts')
.on('change', function() {
var newData = eval(d3.select(this).property('value'));
drawCirc(newData); // Call with new selection.
});
</script>
</body>
Related
I am trying to merge these circles but I keep getting a graph of accumulating circles as opposed to circles moving across the graph?
What am I missing?
I have attached the code below. This function is called updatechart. It corresponds to a slider. So whenever I move the slider across the screen. I corresponding year it lands on is where the updated circle data should move.
var filteredyears = d3.nest()
.key(function(d) {
if(year === d.year){
return d;
}
}).entries(globaldataset);
var circled = svg.selectAll('.countries')
.data(filteredyears[1].values);
var circledEnter = circled.enter()
circled.merge(circledEnter);
circledEnter.append("circle").attr("cx", function(d) {
return xScale(d.gdpPercap);
})
.attr("cy", function(d) {
return yScale(d.lifeExp);
})
.attr('transform', "translate("+[40,30]+")")
.attr( 'r', function(d) {
return rScale(d.population) / 100})
.style("fill", function(d) {
if(d.continent == 'Asia'){
return '#fc5a74';
} else if (d.continent == 'Europe') {
return '#fee633';
} else if (d.continent == 'Africa') {
return '#24d5e8';
} else if (d.continent == 'Americas') {
return '#82e92d';
} else if (d.continent == 'Oceania') {
return '#fc5a74';
}
})
.style("stroke", "black");
circled.exit().remove();
You have a couple of issues using the merge() method, which is indeed quite hard to understand initially.
First, you have to reassign your selection:
circled = circled.merge(circledEnter);
Now, from this point on, apply the changes to circled, not to circledEnter:
circled.attr("//etc...
Besides that, your exit selection won't work, since you're calling it on the merged selection. Put it before the merge.
Finally, append goes to the circledEnter selection, before merging, as well as all attributes that don't change.
Here is a very basic demo showing it:
var svg = d3.select("svg"),
color = d3.scaleOrdinal(d3.schemeCategory10);
render();
function render() {
var data = d3.range(~~(1 + Math.random() * 9));
var circles = svg.selectAll("circle")
.data(data);
circles.exit().remove();
var circlesEnter = circles.enter()
.append("circle")
.attr("r", 5)
.attr("fill", function(d) {
return color(d);
});
circles = circlesEnter.merge(circles);
circles.attr("cx", function() {
return 5 + Math.random() * 290
})
.attr("cy", function() {
return 5 + Math.random() * 140
});
}
d3.interval(render, 1000);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
Does someone know of a way to 'flush' a transition.
I have a transition defined as follows:
this.paths.attr('transform', null)
.transition()
.duration(this.duration)
.ease(d3.easeLinear)
.attr('transform', 'translate(' + this.xScale(translationX) + ', 0)')
I am aware I can do
this.paths.interrupt();
to stop the transition, but that doesn't finish my animation. I would like to be able to 'flush' the transition which would immediately finish the animation.
If I understand correctly (and I might not) there is no out of the box solution for this without going under the hood a bit. However, I believe you could build the functionality in a relatively straightforward manner if selection.interrupt() is of the form you are looking for.
To do so, you'll want to create a new method for d3 selections that access the transition data (located at: selection.node().__transition). The transition data includes the data on the tweens, the timer, and other transition details, but the most simple solution would be to set the duration to zero which will force the transition to end and place it in its end state:
The __transition data variable can have empty slots (of a variable number), which can cause grief in firefox (as far as I'm aware, when using forEach loops), so I've used a keys approach to get the non-empty slot that contains the transition.
d3.selection.prototype.finish = function() {
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].duration = 0;
})
}
If working with delays, you can also trigger the timer callback with something like: if(slots[d]) slots[d].timer._call();, as setting the delay to zero does not affect the transition.
Using this code block you call selection.finish() which will force the transition to its end state, click a circle to invoke the method:
d3.selection.prototype.finish = function() {
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].timer._call();
})
}
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var circle = svg.selectAll("circle")
.data([1,2,3,4,5,6,7,8])
.enter()
.append("circle")
.attr("cx",50)
.attr("cy",function(d) { return d * 50 })
.attr("r",20)
.on("click", function() { d3.select(this).finish() })
circle
.transition()
.delay(function(d) { return d * 500; })
.duration(function(d) { return d* 5000; })
.attr("cx", 460)
.on("end", function() {
d3.select(this).attr("fill","steelblue"); // to visualize end event
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.12.0/d3.min.js"></script>
Of course, if you wanted to keep the method d3-ish, return the selection so you can chain additional methods on after. And for completeness, you'll want to ensure that there is a transition to finish. With these additions, the new method might look something like:
d3.selection.prototype.finish = function() {
// check if there is a transition to finish:
if (this.node().__transition) {
// if there is transition data in any slot in the transition array, call the timer callback:
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].timer._call();
})
}
// return the selection:
return this;
}
Here's a bl.ock of this more complete implementation.
The above is for version 4 and 5 of D3. To replicate this in version 3 is a little more difficult as timers and transitions were reworked a bit for version 4. In version three they are a bit less friendly, but the behavior can be achieved with slight modification. For completeness, here's a block of a d3v3 example.
Andrew's answer is a great one. However, just for the sake of curiosity, I believe it can be done without extending prototypes, using .on("interrupt" as the listener.
Here I'm shamelessly copying Andrew code for the transitions and this answer for getting the target attribute.
selection.on("click", function() {
d3.select(this).interrupt()
})
transition.on("interrupt", function() {
var elem = this;
var targetValue = d3.active(this)
.attrTween("cx")
.call(this)(1);
d3.select(this).attr("cx", targetValue)
})
Here is the demo:
var svg = d3.select("svg")
var circle = svg.selectAll("circle")
.data([1, 2, 3, 4, 5, 6, 7, 8])
.enter()
.append("circle")
.attr("cx", 50)
.attr("cy", function(d) {
return d * 50
})
.attr("r", 20)
.on("click", function() {
d3.select(this).interrupt()
})
circle
.transition()
.delay(function(d) {
return d * 500;
})
.duration(function(d) {
return d * 5000;
})
.attr("cx", 460)
.on("interrupt", function() {
var elem = this;
var targetValue = d3.active(this)
.attrTween("cx")
.call(this)(1);
d3.select(this).attr("cx", targetValue)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="500"></svg>
PS: Unlike Andrew's answer, since I'm using d3.active(node) here, the click only works if the transition had started already.
I'm having trouble understanding when and how to use nested data.
In this example I have a CSV with names ('Name') and locations ('starting point'). By assigning keys to the locations I am able to make a dropdown containing them all, and I would like to use this to filter the names associated with each location.
However I am unable to find the data's values, in this case 'd.Name'
Here inside the update function I have tried to access the 'values' on the data join.
var adventurer = canvas
.selectAll(".adventurer")
.data(function(d) {
return d.values;
})
Ive also tried creating an extra data variable but thats not working for me either.
Sorry I can't make a jsfiddle but here is a plunk
DATA
,,Name,First names,s,r,Nat,born,starting point,starting date,arrival date,days,km,Assist,Support,Style,note,arrival date 2
1,1,KAGGE,Erling,,,Nor,1/15/1963,Berkner Island,11/18/1992,1/7/1993,50,appr. 1300,n,n,solo,first solo unassisted,
2,2,ARNESEN,Liv,f,,Nor,6/1/1953,Hercules Inlet,11/4/1994,12/24/1994,50,1130,n,n,solo,first woman unassisted,
3,3,HAUGE,Odd Harald,,,Nor,1956,Berkner Island,11/4/1994,12/27/1994,54,appr. 1300,n,n,,,
HTML
<div id="menu"></div>
<div id="chart"></div>
SCRIPT
d3.csv("data.csv", function(csv_data) {
var data = d3.nest()
.key(function(d) {
return d['starting point'];})
.sortKeys(d3.ascending)
.entries(csv_data)
console.log(data);
//create dropdown select
var list = d3.select("#menu").append("select")
list.selectAll("option")
.data(data)
.enter().append("option")
.attr("value", function(d) {
return d.key;
})
.text(function(d) {
return d.key;
});
//chart config
var w = 375,
h = 1000;
var canvas = d3.select('#chart')
.append('svg')
.attr('width', w)
.attr('height', h)
.append('g')
.attr('transform', 'translate (0,50)');
//function (bind, add, remove, update)
function updateLegend(data) {
var adventurer = canvas
.selectAll(".adventurer")
.data(function(d) {
return d.values;
})
var adventurerEnter = adventurer
.enter().append("g")
.attr('class', 'adventurer');
adventurerEnter
.append("text")
.attr('class', 'name')
.attr('x', 0);
adventurer.select('.name')
.text(function(d, i) {
return d.Name;
})
.attr('y', function(d, i) {
return i * 30;
});
// remove old elements
adventurer.exit().remove();
};
// generate initial legend
updateLegend(data);
});
// handle on click event
d3.select('#menu')
.on('change', function() {
var data = eval(d3.select(this).property('value'));
console.log(data)
updateLegend(data);
});
You need to display both the locations and the names. You have a nest (in the plunk but not in your question) of location/name, but you also need a distinct list of names, or possibly a list of name/location:
var locations = d3.nest()
.key(function(d) {return d['starting point'];})
.sortKeys(function(a,b){ return a > b && 1 || b > a && -1 || 0})
.key(function(d) {return d.Name;})
.entries(csv_data)
var names = d3.nest()
.key(function(d) {return d.Name;})
.sortKeys(function(a,b){ return a > b && 1 || b > a && -1 || 0})
.key(function(d) {return d['starting point'];})
.entries(csv_data)
Then you have to display your names however you want. Then you need an .on('change', function()...) handler (or on click or whatever fits your needs) that actually filters the names wherever those are displayed.
I also fixed your sorting. d3.ascending is for numbers, not strings.
I´m back with another d3.js problem.
I have an choropleth map and i want a tooltip which shows me the correct value of the community on an mouseover function.
It works nearly perfect, there is only one problem, there is no "update" of the value. on Every county it is the same.
I hope someone has an answer for me. Thank you.
Here is my code snippet:
var feature = group.selectAll("path")
.data(collection.features)
.enter()
.append("path")
//.transition()
//.duration(9000)
.attr("fill",function(d) {
//Get data value
var value = d.properties.EINWOHNER;
//console.log(value);
if (value) {
//If value exists…
return color(value);
}
else {
//If value is undefined…
return "none";
}
})
.on("mouseover", function(d) {
d3.select("#tooltip")
.data(collection.features)
.select("#value")
.text(function(d){
return d.properties.EINWOHNER;
});
//Show the tooltip
d3.select("#tooltip").classed("hidden", false);
})
.on("mouseout", function() {
//Hide the tooltip
d3.select("#tooltip").classed("hidden", true);
});
To set the text, use
.text(d.properties.EINWOHNER);
You're currently getting the value from the data bound to the #value DOM element for each of them. You also don't need to pass in data there -- you already have the current data in d. So the complete code would look like this.
.on("mouseover", function(d) {
d3.select("#tooltip")
.text(d.properties.EINWOHNER);
d3.select("#tooltip").classed("hidden", false);
})
I'm just starting out with D3.js. I've created a simple enough donut chart using this example. My problem is, if I have an array of objects as my data source - data points for ex. would be a1.foo or a1.bar - and I want to switch between them, how would i go about doing this? My current solution looks ugly and it can't be the proper way of doing it - code below.
//Call on window change event
//Based on some parameter, change the data for the document
//vary d.foo to d.bar and so on
var donut = d3.layout.pie().value(function(d){ return d.foo})
arcs = arcs.data(donut(data)); // update the data
Is there a way I can set the value accessor at run time other than defining a new pie function?
Generally to switch the data that is being displayed you would create a redraw() function that would then update the data for the chart. In the redraw you'll need to make sure to handle the three cases - what should be done when data elements are modified, what should be done when new data elements are added, and what should be done when data elements are removed.
It usually looks something like this (this example changes the data set through panning, but it doesn't really matter). See the full code at http://bl.ocks.org/1962173.
function redraw () {
var rects, labels
, minExtent = d3.time.day(brush.extent()[0])
, maxExtent = d3.time.day(brush.extent()[1])
, visItems = items.filter(function (d) { return d.start < maxExtent && d.end > minExtent});
...
// upate the item rects
rects = itemRects.selectAll('rect')
.data(visItems, function (d) { return d.id; }) // update the data
.attr('x', function(d) { return x1(d.start); })
.attr('width', function(d) { return x1(d.end) - x1(d.start); });
rects.enter().append('rect') // draw the new elements
.attr('x', function(d) { return x1(d.start); })
.attr('y', function(d) { return y1(d.lane) + .1 * y1(1) + 0.5; })
.attr('width', function(d) { return x1(d.end) - x1(d.start); })
.attr('height', function(d) { return .8 * y1(1); })
.attr('class', function(d) { return 'mainItem ' + d.class; });
rects.exit().remove(); // remove the old elements
}