I am building a web application to display different trends and stats between countries in the world. With d3, I am able to load the topojson file and project the world map.
var countryStatistics = [];
var pathList = [];
function visualize(){
var margin = {top: 100, left: 100, right: 100, bottom:100},
height = 800 - margin.top - margin.bottom,
width = 1200 - margin.left - margin.right;
//create svg
var svg = d3.select("#map")
.append("svg")
.attr("height", height + margin.top + margin.bottom)
.attr("width", width + margin.left + margin.right)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//load topojson file
d3.queue()
.defer(d3.json, "world110m.json")
.await(ready)
var projection = d3.geoMercator()
.translate([ width / 2, height / 2 ])
.scale(180)
//pass path lines to projection
var path = d3.geoPath()
.projection(projection);
function ready (error, data){
console.log(data);
//we pull the countries data out of the loaded json object
countries = topojson.feature(data, data.objects.countries).features
//select the country data, draw the lines, call mouseover event to change fill color
svg.selectAll(".country")
.data(countries)
.enter().append("path")
.attr("class", "country")
.attr("d", path)
.on('mouseover', function(d) {
d3.select(this).classed("hovered", true)
//this function matches the id property in topojson country, to an id in (see below)
let country = matchPath(this.__data__.id);
console.log(country)
})
.on('mouseout', function(d) {
d3.select(this).classed("hovered", false)
})
//here I push the country data into a global array just to have access and experimentalism.
for (var i = 0; i < countries.length; i++) {
pathList.push(countries[i]);
}
}
};
The matchPath() function is used to allow me to match the path data to countryStatistics for display when a certain country is mouseovered.
function matchPath(pathId){
//to see id property of country currently being hovered over
console.log("pathID:" + pathId)
//loop through all countryStatistics and return the country with matching id number
for(var i = 0; i < countryStatistics.length; i++){
if(pathId == countryStatistics[i].idTopo){
return countryStatistics[i];
}
}
}
The Problem: This works, but only in one direction. I can reach my statistical data from each topojson path... but I can't reach and manipulate individual paths based on the data.
What I would like to happen, is to have a button that can select a certain property from countryStatistics, and build a domain/range scale and set a color gradient based on the data values. The step I am stuck on is getting the stat data and path data interfacing.
Two potential solutions I see,
1:There is a way to connect the topo path data to the statistical data during the render, I could call a function to redraw sgv...
2:I build a new object that contains all of the path data and statistical data. In this case can I just pull out the topojson.objects.countries data and ignore the rest?
How should I achieve this? Any pointers, next step will be appreciated.
(where I am at with this project... http://conspiracytime.com/globeApp )
TopoJSON is a really powerfull tool. It has its own CLI (command line interface) to generate your own TopoJSON files.
That CLI allows you to create a unique file with the topology and the data you want to merge with.
Even though it is in its v3.0.2 the first versión looks the clear one to me. This is an example of how you can merge a csv file with a json through a common id attribute.
# Simplified versión from https://bl.ocks.org/luissevillano/c7690adccf39bafe583f72b044e407e8
# note is using TopoJSON CLI v1
topojson \
-e data.csv \
--id-property CUSEC,cod \
-p population=+t1_1,area=+Shape_area \
-o cv.json \
-- cv/shapes/census.json
There is a data.csv file with a cod column and census.json file with a property named CUSEC.
- Using the --id-property you can specify which attributes will be used in the merge.
- With the property -p you can create new properties on the fly.
This is the solid solution where you use one unique file (with one unique request) with the whole data. This best scenario is not always possible so another solution could be the next one.
Getting back to JavaScript, you can create a new variable accessible through the attribute in common the following way. Having your data the format:
// countryStatistics
{
"idTopo": "004",
"country": "Afghanistan",
"countryCode": "afg",
// ..
},
And your TopoJSON file the structure:
{"type":"Polygon","arcs":[[0,1,2,3,4,5]],"id":"004"},
{"type":"MultiPolygon","arcs":[[[6,7,8,9]],[[10,11,12]]],"id":"024"} // ...
A common solution to this type of situation is to create an Array variable accessible by that idTopo:
var dataById = [];
countryStatistics.forEach(function(d) {
dataById[d.idTopo] = d;
});
Then, that variable will have the next structure:
[
004:{
"idTopo": "004",
"country": "Afghanistan",
"countryCode": "afg",
//...
},
008: {
//...
}
]
From here, you can access the properties through its idTopo attribute, like this:
dataById['004'] // {"idTopo":"004","country":"Afghanistan","countryCode":"afg" ...}
You can decide to iterate through the Topo data and add these properties to each feature:
var countries = topojson
.feature(data, data.objects.countries)
.features.map(function(d) {
d.properties = dataById[d.id];
return d
});
Or use this array whenever you need it
// ...
.on("mouseover", function(d) {
d3.select(this).classed("hovered", true);
console.log(dataById[d.id]);
});
Related
I'm working on creating a graph that will show unemployment rates by county in the United States. It's based on Mike Bostock's County-level map here, and it looks like this:
It works by pulling data from a CSV, assigning that data to a Javascript Map, and using that to color the various counties with a d3 color scale. My CSV data looks like this:
county,area,rate2007,rate2008,rate2009,rate2010,rate2011,rate2012,rate2013,rate2014,rate2015,rate2016
01001,Autauga County,3.3,5.1,9.7,8.9,8.4,6.9,6.2,5.9,5.3,5.3
01003,Baldwin County,3.1,4.6,9.8,10.0,9.0,7.5,6.6,6.1,5.6,5.4
01005,Barbour County,6.3,8.8,14.3,12.3,11.5,11.5,10.2,10.6,8.8,8.6
01007,Bibb County,4.1,5.8,13.3,11.4,10.5,8.5,7.9,7.2,6.7,6.6
01009,Blount County,3.2,4.7,10.0,9.8,8.7,6.9,6.3,6.1,5.4,5.5
etc...
Here's my issue: I want to be able to change this data over time, and to do so must be able to include multiple values in my Map. However, I cannot get this to work. In attempting to "upgrade" my map, I tweak the code slightly to use an array as the value in the key-value pair in the Map:
d3.queue()
.defer(d3.json, "https://d3js.org/us-10m.v1.json")
.defer(d3.csv, "unemploymentEdited.csv", function(d) {
unemployment.set(d.county, [+d.rate2007, +d.rate2008]);
}) // I use an array here to include multiple values.
.await(ready);
When I log this to the console, my data is indeed formatted as I would expect:
However, I cannot use these values to color my paths correctly. When I use a relatively simple function to access the data from the Map, it logs this error to the console: "TypeError: unemployment.get(...) is undefined."
Even more strangely, when I log this same thing to the console outside of my "fill" attribute, I get the number I would expect, meaning I get the values inside my Map. Like if I write console.log(unemployment.get(d.id)[0]) I get 4.6, or whatever the value is. What am I doing wrong? Why does the "get" function return undefined, even though I can see that the data structure is not?
Here's my full code:
var width = 960;
var height = 600;
var margin = {top: 50, bottom: 0, left: 0, right: 0}
var path = d3.geoPath(); // Geopath generator
var unemployment = new Map();
var color = d3.scaleSequential()
.domain([0.0,30.0])
.interpolator(d3.interpolateInferno);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("background-color","lightgrey")
d3.queue()
.defer(d3.json, "https://d3js.org/us-10m.v1.json")
.defer(d3.csv, "unemploymentEdited.csv", function(d){
unemployment.set(d.county, [+d.rate2007,+d.rate2008])
})
.await(ready);
function ready(error, us) {
console.log(unemployment)
svg.append("g")
.attr("class", "counties")
.attr("transform",`translate(0,${margin.top})`)
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("fill", function(d){
return color(unemployment.get(d.id)[0])
})
.attr("d", path)
}
Well, your d3.map() is correct, that's not the problem.
The problem is that your CSV doesn't have data for all counties. Therefore, sometimes, this...
unemployment.get(d.id)
... is undefined, which explains your error:
unemployment.get(d.id)[0] --> Cannot read property '0' of undefined
Solution
Check for undefined:
.attr("fill", function(d){
return unemployment.get(d.id) ? color(unemployment.get(d.id)[0]) : "#ccc";
})
Here, if unemployment.get(d.id) is undefined, the function will return #ccc. Change this according to the colour you want.
Here is the updated bl.ocks: https://bl.ocks.org/anonymous/3adc7518e4423d816f6b6842ef48d27f/414e4b738dd4c2d8e91a9ca2fb8511dcec747b9c
I am currently creating a barchart that gets data from a d3 histogram.
I create the histogram like this:
var binningThresholds = d3.range(getMinValue(rows, 'Model year'), getMaxValue(rows, 'Model year') + 2)
var histogram = d3.layout.histogram()
.value(getModelYear)
.bins(binningThresholds)
(rows);
I get rows from a csv input. I do a lot of stuff with the input already, so that works fine.
The histogram now is an array of an array of objects.
When I now want to use this histogram and add a bar to a chart for every index in the array, no error shows up and d3 does not append any g, rect or text tags to the DOM.
var histogramBars = histogramSvg.selectAll('g')
.data(histogram)
.enter()
.append('g');
histogramBars.append('rect');
histogramBars.append('text')
.text(function(d) {
return d.length;
})
I really don't know why, does anyone see a mistake?
Don't use .selectAll('g').data..., my guess is that there's other g elements in your visualization and you are selecting things you don't mean to. Instead do something like:
var histogramBars = histogramSvg.selectAll('.myUniqueClass')
.data(histogram)
.enter()
.append('g')
.attr('class', 'myUniqueClass');
I have been following the guide for choropleth using D3 from this link.
http://synthesis.sbecker.net/articles/2012/07/18/learning-d3-part-7-choropleth-maps
instead of unemployment, I have a json file that lists the number of automobile crashes per county per state. The format of this json file is
{
"id":1001,
"crashTotal":2
},
And this is for each of the elements in the json file; one for each county. The ID is the State+County FIPS Code and the crashTotal is its namesake.
I have been following the example code closely and have come upon the quantize function
// quantize function takes a data point and returns a number
// between 0 and 8, to indicate intensity, the prepends a 'q'
// and appends '-9'
function quantize(d) {
return "q" + Math.min(8, ~~(data[d.id] * 9 / 12)) + "-9";
}
For me, data is a variable set equal to the crashes.json file. I'm confused as to why I cannot use the crashTotal values from my data to use according to the quantize function.
When I try to use the following code
~~data[d.id] or +data[d.id]
I get 0 or NaN. Why is this? I'm fairly new to using d3 so I'm not sure how this is meant to work. Thanks.
My code is quite close to the example code, but with my own US country and state JSON files converted from the census shapefiles. Can someone help?
EDIT: I'd figure I explain the issue a little bit more. Its not a problem between using a quantize function or d3 scale quantize, but rather how to access my data to color each county. As stated, my data file is a JSON in the format above. The following is how I set the data and how I call quantize
d3.json("Crashes.json", function(crashes) {
max = +crashes[0].crashTotal;
min = +crashes[0].crashTotal;
maxFIPS = +crashes[0].id;
minFIPS = +crashes[0].id;
for(i = 0; i < crashes.length; i++) {
if(+crashes[i].crashTotal > max) {
maxFIPS = +crashes[i].id;
max = +crashes[i].crashTotal;
}
if(+crashes[i].crashTotal < min) {
minFIPS = +crashes[i].id;
min = +crashes[i].crashTotal;
}
}
data=crashes;
//for(i = 0; i < data.length; i++) {
// document.writeln(data[i].id + " " + data[i].crashTotal);
// }
counties.selectAll("path")
.attr("class", quantize);
//.text(function (d){return "" + d.value;});
//console.log("maxFIPS:" + maxFIPS + " minFIPS:" + minFIPS + "\n" + "max:" + max + " min:" + min);
});
function quantize(d) {
return "q" + Math.min(8, ~~data[d.id]) + "-9";
}
If I were to replace data[d.id] in the quantize function above, it would actually color based on the color scheme specified in the bracket or CSS document. How would I get this to use the CrashTotal numbers from my data?
EDIT[3-6-2014]
Following the answer from Amelia, I now have the following code bracket.
d3.json("Crashes.json", function(crashes) {
crashDataMap = d3.map();
crashes.forEach(function(d) {crashDataMap.set(d.id, d);});
data = crashDataMap.values();
quantize = d3.scale.quantize()
.domain(d3.extent(data, function(d) {return d.crashTotal;}))
.range(d3.range(9).map(function(i) {return "q" + i + "-9"}));
//min = d3.min(crashDataMap.values(), function(d) {return d.crashTotal;});
//max = d3.max(crashDataMap.values(), function(d) {return d.crashTotal;});
//console.log(quantize(crashDataMap.get(6037).crashTotal));
counties.selectAll("path")
.attr("class", function(d) {return quantize(crashDataMap.get(d.id).crashTotal);});
});
This should get me the correct coloring for my map, but my map stays white. I can confirm that by testing out quantize, I get the correct class name from my CSS file.
console.log(quantize(crashDataMap.get(1001).crashTotal)); //returns q0-9
More help is appreciated. Thanks.
EDIT2[3-6-2014] I decided to just post the entire code I have here, hoping someone could make sense out of the madness of why this doesn't work
//CSS or <style></style> bracket
svg {
background: white;
}
path {
fill: none;
stroke: #000;
stroke-width: 0.1px;
}
#counties path{
stroke: #000;
stroke-width: 0.25px;
}
#states path{
fill: none;
stroke: #000;
stroke-width: 0.5px;
}
.Blues .q0-9{fill:rgb(247,251,255)}
.Blues .q1-9{fill:rgb(222,235,247)}
.Blues .q2-9{fill:rgb(198,219,239)}
.Blues .q3-9{fill:rgb(158,202,225)}
.Blues .q4-9{fill:rgb(107,174,214)}
.Blues .q5-9{fill:rgb(66,146,198)}
.Blues .q6-9{fill:rgb(33,113,181)}
.Blues .q7-9{fill:rgb(8,81,156)}
.Blues .q8-9{fill:rgb(8,48,107)}
//Crashes.js file
var width = 960
var height = 500;
var data;
var crashDataMap;
var quantize;
var path = d3.geo.path();
var zoom = d3.behavior.zoom()
.on("zoom", zoomed);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom)
.append("g");
var counties = svg.append("g")
.attr("id", "counties")
.attr("class", "Blues");
var states = svg.append("g")
.attr("id", "states");
d3.json("county.json", function(county) {
var countyFeatures = topojson.feature(county, county.objects.county);
counties.selectAll("path")
.data(countyFeatures.features)
.enter().append("path")
.attr("d", path);
});
d3.json("state.json", function(state) {
var stateFeatures = topojson.feature(state, state.objects.state);
states.selectAll("path")
.data(stateFeatures.features)
.enter().append("path")
.attr("d", path);
});
d3.json("Crashes.json", function(crashes) {
crashDataMap = d3.map();
crashes.forEach(function(d) {crashDataMap.set(d.id, d);});
data = crashDataMap.values();
quantize = d3.scale.quantize()
.domain(d3.extent(data, function(d) {return d.crashTotal;}))
.range(d3.range(9).map(function(i) {return "q" + i + "-9"}));
/*
for(i = 0; i < data.length; i++) {
console.log(quantize(crashDataMap.get(data[i].id).crashTotal));
}
*/
counties.selectAll("path")
.attr("class", function(d) {return quantize(crashDataMap.get(d.id).crashTotal);});
});
function zoomed() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
};
Take a look at where I generated the paths for counties. After .enter().append("path")
statement, if I were to enter the code .attr("class", "q8-9) It would color every county to the scheme defined as q8-9.
If I were to call counties.selectAll("path").attr("class", "q8-9") anywhere outside of the code bracket, nothing happens; the map stays white. This is bugging me as I clearly have no idea why this can happen. I can verify that the path elements are there for both county and state.
To explain what's going on in the original code:
The tutorial you linked to uses two data files, one for the maps and one for the data values. Unfortunately, it doesn't seem to include links to the actual data files used, but their both JSON. The counties have an 'id' property and that property seems to be used as the keys in the second JSON data file. I.e., that second file (data) must be of the form:
{
"1001": ".097",
"1003": ".091",
"1005": ".134",
/*...*/
}
This is different from the data structure used in the very similar Mike Bostock example, which uses a .tsv file for the unemployment data, which is then used to generate a d3.map hashmap data dictionary.
var rateById = d3.map();
queue.defer(d3.tsv, "unemployment.tsv", function(d) { rateById.set(d.id, +d.rate); })
//this is equivalent to
/*
d3.tsv("unemployment.tsv",
function(d) { rateById.set(d.id, +d.rate); },
readyFunction );
*/
//except that the readyFunction is only run when *both* data files are loaded.
//When two functions are given as parameters to d3.tsv,
//the first one is called on each row of the data.
//In this case, it adds the id and rate as a key:value pair to the hashmap
Both of these examples end up with a data structure where the id values are keys that can be used to grab the appropriate data value. In contrast, your data are in an unkeyed array, with your id values as just another property, not as a key. That is why data[d.id] was returning an error for you -- instead of grabbing a data number that matches that id, it's grabbing an element of your array at the index equivalent the id number. That either returns an object, which becomes NaN when converted to a number, or undefined, which becomes zero.
In either example, once they have the number, they then want to convert it to an integer from 0 to 8 in order to assign one of the ColorBrewer class names to the path. The Scott Becker tutorial uses a somewhat arbitrary calculation for this, the Mike Bostock example uses a quantize scale with a hard-coded domain. You say you want to figure out a domain based on your data.
To help you figure out what you need to do:
Your first step is to get your crash data into a structure where you can easily grab a data element based on its id value.
One option would be to create a d3.map object (var crashDataMap = d3.map();) and then use a forEach call on your existing data array to add each object to the map using map.set(key, value) with its id as the key.
crashDataArray.forEach( function(d){ crashDataMap.set( d.id, d) });
Then when you are setting the class on your shapes, you can use crashDataMap.get(d.id) to grab the crash data that matches the shape's id, and you can extract the correct number from that.
For dividing your data into categories, you probably want to use a quantize scale similar to Mike Bostock's example. On your original data array, you can use d3.extent with an appropriate accessor function to grab the crash totals from each entry and find the max and min for setting the domain.
I'm having trouble working with json data in D3. The file is read properly, judging from the fact that it appears when I console.log, and seems to be formatted right based on the way all the examples I found. But, when I try to do a nested selection using .data(function(json_data){return json_data.accessibility;}) I get: "cannot read property 'length' of undefined".
My function:
//load scenario json data
d3.json("./SupportTool/scenario1result.json", function(error, json_data){
if(error) {return console.warn(error)};
console.log(json_data); //works
// add main SVG block
var svg = d3.select(d3id)
.append('svg')
.attr('width', 300)
.attr('height', 75)
.attr('id', 'svgblock');
// add an SVG group element for each scenario
var series = svg.selectAll('g.series')
.data(d3.keys(json_data))
.enter()
.append('g')
.attr('class', 'series');
var circles = series.selectAll("circle")
.data(function(json_data){return json_data.accessibility;})
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", 20)
.attr("cy", 20)
.attr("r", 20)
.style("color","blue"); });
My json data:
{
"meta":[{"sc":"1"},{"stratid":"1"}],
"accessibility":[
{"pop400":"77"},{"pop800":"86"},{"jobs800":"78"},{"pop400tr":"41"},{"pop800tr":"69"},{"jobs800tr":"80"}
],
"housing":[
{"newcom":"0"},{"redev":"100"},
{"apt5":"6"},{"apt4":"65"},{"twn":"14"},{"sglf":"15"},
{"urb":"0"},{"urbhec":"0"}
],
"transport":[
{"walk":"55"},{"transit":"18"},{"auto":"27"},
{"vkt":"11000"},
{"kmtr":"502"},{"form":"grid"},
{"lanekm":"3250"},
{"ghgtr":"67"},{"ghgres":"75"}
],
"costs":[
{"roadcapbils":null,"roadcap":null},
{"transitcapbils":null,"transitcap":null},
{"watercapbils":null,"watercap":null},
{"firecapbils":null,"firecap":null},
{"reccapbils":null,"reccap":null},
{"educapbils":null,"educap":null}
],
"opcosts":[
{"roadopbils":null,"roadop":null},
{"transitoppbils":null,"transitop":null},
{"wateropbils":null,"waterop":null},
{"fireopbils":null,"fireop":null},
{"parksopbils":null,"parksop":null}
] }
The issue you are having comes from the fact that you are binding data to circles using a function of the data already bound to series:
var circles = series.selectAll("circle")
series already has data bound to it from .data(d3.keys(json_data)). Thus, when you log the objects being passed one at a time to the .data() for circles, you just get the keys of json_data, i.e.
["meta", "accessibility", "housing", "transport", "costs", "opcosts"]
Since this is a list of Strings, they do not have any property called accessibility, hence your error.
My guess is that you are trying to append circles for each item in json_data.accessibility, which your code will do if you just replace that line with
.data(json_data.accessibility)
which will pass
[{"pop400":"77"},{"pop800":"86"},{"jobs800":"78"},{"pop400tr":"41"},{"pop800tr":"69"},{"jobs800tr":"80"}]
to data. This code works on my machine, and draws six circles on the page.
One final note is that you should be careful with your variable names. In the function you pass to data, you are redefining json_data as a local variable in that function, which means you can't access your actual JSON data in that function.
I'm working on a simple d3 example where I use d3 to place some new divs on a page, add attributes, and add data-driven styles. The part that is tripping me up is when I want to use d3 to update some styles using new data. I've pasted the code from a jsFiddle ( http://jsfiddle.net/MzPUg/15/ ) below.
In the step that originally creates the divs, I use a key function to add indexes to the elements and in the update step (the part that isn't working) I also use a key function. But what isn't clear from the d3 documentation is how the actual data join works (e.g. where are indexes stored in the DOM elements? what if there are duplicate indexes?, etc.).
So, there are obvious gaps in my knowledge, but keeping it simple here can anyone shed light on why this example is not working? Any additional info on the precise nature of data joins in d3 would be frosting on the cake. (I've already seen http://bost.ocks.org/mike/join/.)
//add a container div to the body and add a class
var thediv = d3.select("body").append("div").attr("class","bluediv");
//add six medium-sized divs to the container div
//note that a key index function is provided to the data method here
//where do the resulting index value get stored?
var mediumdivs = thediv.selectAll("div")
.data([10,50,90,130,170,210],function(d){return d})
.enter().append("div")
.style("top",function(d){return d + "px"})
.style("left",function(d){return d + "px"})
.attr("class","meddiv")
//UPDATE STEP - NOT WORKING
//Attempt to update the position of two divs
var newdata = [{newval:30,oldval:10},{newval:80,oldval:50}]
var mediumUpdate = mediumdivs.data(newdata,function(d){return d.oldval})
.style("left",function(d){return d.newval + "px"})
As far as I know, you do not update the elements that already exist. Instead, you tell D3 which elements to draw and it determines what to remove or update on the screen.
I updated your JSFiddle with working code. I have also added the code below.
//add a container div to the body and add a class
var thediv = d3.select("body").append("div").attr("class", "bluediv");
function update(data) {
var mediumdivs = thediv.selectAll("div").data(data, function(d) {
return d;
});
// Tell D3 to add a div for each data point.
mediumdivs.enter().append("div").style("top", function(d) {
return d + "px";
}).style("left", function(d) {
return d + "px";
}).attr("class", "meddiv")
// Add an id element to allow you to find this div outside of D3.
.attr("id", function(d) {
return d;
});
// Tell D3 to remove all divs that no longer point to existing data.
mediumdivs.exit().remove();
}
// Draw the scene for the initial data array at the top.
update([10, 50, 90, 130, 170, 210]);
// Draw the scene with the updated array.
update([30, 80, 90, 130, 170, 210]);
I am not sure of D3's inner workings of how it stores indexes, but you can add an id attribute to the divs you create to create unique indexes for yourself.
In the above answer an update step is needed for transition of divs with the same key. illustrative jsfiddle showing what happens with/without update function.
Update function is just selection.stuff, rather than selection.enter().stuff :
//add a container div to the body and add a class
var updateDiv = d3.select("#updateDiv").attr("class", "bluediv");
var noUpdateDiv = d3.select("#noUpdateDiv").attr("class", "bluediv");
function update(selection,data,zeroTop,withUpdate) {
//add six medium-sized divs to the container div
//note that a key index function is provided to the data method here
//where do the resulting index value get stored?
var mediumdivs = selection.selectAll("div").data(data, function(d) {
return d;
});
if(withUpdate){
mediumdivs.style("top", function(d) {
if(zeroTop){
return 0
}else{
return d + "px";
}
}).style("left", function(d) {
return d + "px";
}).attr("class", "meddiv");
}
mediumdivs.enter().append("div").style("top", function(d) {
if(zeroTop){
return 0
}else{
return d + "px";
}
}).style("left", function(d) {
return d + "px";
}).attr("class", "meddiv");
mediumdivs.exit().remove();
}
//with the update function we maintain 3 of the old divs, and move them to the top
update(updateDiv,[10, 50, 90, 130, 170, 210],false,true);
update(updateDiv,[10,50,90],true,true);
//without the update function divs are maintained, but not transitioned
update(noUpdateDiv,[10, 50, 90, 130, 170, 210],false,false);
update(noUpdateDiv,[10,50,90],true,false);
The other answers given so far use the strategy of removing and recreating divs. This isn't necessary. The problem with Al R.'s original code was just in the way it used the data key. The same data key function is used both for the old data and for the data that's newly passed in. Since in Al R.'s example, the old data was a simple array of numbers, and the new data was an array of objects with properties, no data was selected in the mediumUpdate line.
Here's one way to make the selection work:
var newdata = [10, 50];
var newdatamap = {10:30, 50:80};
var mediumUpdate = mediumdivs.data(newdata, function(d){return d;})
.style("left",function(d){return newdatamap[d] + "px";});
Here's a jsfiddle, which also changes the color of the selected divs to make the effect obvious.