I'm trying to understand how to use with dimplejs but the result is not what i ment.
JSFiddleCode
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://dimplejs.org/dist/dimple.v2.0.0.min.js"></script>
<script type="text/javascript">
var svg = dimple.newSvg("#chartContainer", 590, 400);
d3.csv("carsData.csv", function (data) {
// change string (from CSV) into number format
data.forEach(function(d) {
if(d["Sports Car"]==1)
d.Category = "Sports Car";
else if(d["SUV"]==1)
d.Category = "SUV";
else
d.Category = "Other";
d.HP = +d.HP;
d["Engine Size (l)"] = +d["Engine Size (l)"];
});
// Latest period only
//dimple.filterData(data, "Date", "01/12/2012");
// Create the chart
var myChart = new dimple.chart(svg, data);
myChart.setBounds(60, 30, 420, 330)
// Create a standard bubble of SKUs by Price and Sales Value
// We are coloring by Owner as that will be the key in the legend
myChart.addMeasureAxis("x", "HP");
myChart.addMeasureAxis("y", "Engine Size (l)");
myChart.addSeries("Category", dimple.plot.bubble);
var myLegend = myChart.addLegend(530, 100, 60, 300, "Right");
myChart.draw();
// This is a critical step. By doing this we orphan the legend. This
// means it will not respond to graph updates. Without this the legend
// will redraw when the chart refreshes removing the unchecked item and
// also dropping the events we define below.
myChart.legends = [];
// This block simply adds the legend title. I put it into a d3 data
// object to split it onto 2 lines. This technique works with any
// number of lines, it isn't dimple specific.
svg.selectAll("title_text")
.data(["Click legend to","show/hide owners:"])
.enter()
.append("text")
.attr("x", 499)
.attr("y", function (d, i) { return 90 + i * 14; })
.style("font-family", "sans-serif")
.style("font-size", "10px")
.style("color", "Black")
.text(function (d) { return d; });
// Get a unique list of Owner values to use when filtering
var filterValues = dimple.getUniqueValues(data, "Category");
// Get all the rectangles from our now orphaned legend
myLegend.shapes.selectAll("rect")
// Add a click event to each rectangle
.on("click", function (e) {
// This indicates whether the item is already visible or not
var hide = false;
var newFilters = [];
// If the filters contain the clicked shape hide it
filterValues.forEach(function (f) {
if (f === e.aggField.slice(-1)[0]) {
hide = true;
} else {
newFilters.push(f);
}
});
// Hide the shape or show it
if (hide) {
d3.select(this).style("opacity", 0.2);
} else {
newFilters.push(e.aggField.slice(-1)[0]);
d3.select(this).style("opacity", 0.8);
}
// Update the filters
filterValues = newFilters;
// Filter the data
myChart.data = dimple.filterData(data, "Category", filterValues);
// Passing a duration parameter makes the chart animate. Without
// it there is no transition
myChart.draw(800);
});
});
the scatterplot result is only 3 and i dont know why.
the x is the HP and the y is horse power.
more questions:
1. how can i change the axis unit.
2. how can i control the size of each bubble.
3. how to fix the wrong results.
heres the result picture:
The csv file has 480 rows.
maybe the addseries is wrong (i dont know what it is)?
Dimple aggregates the data for you based on the first parameter of the addSeries method. You have passed "Category" which has 3 values and therefore creates 3 bubbles with summed values. If instead you want a bubble per vehicle coloured by category you could try:
myChart.addSeries(["Vehicle Name", "Category"], dimple.plot.bubble);
To change the axis unit you can use axis.tickFormat though the change above will reduce scale so you might find you don't need to.
To control bubble size based on values in your data you need to add a "z" axis. See this example.
If you want to just set a different marker size for your scatter plot you can do so after the draw method has been called with the following:
var mySeries = myChart.addSeries("Category", dimple.plot.bubble);
var myLegend = myChart.addLegend(530, 100, 60, 300, "Right");
myChart.draw();
// Set the bubble to 3 pixel radius
mySeries.shapes.selectAll("circle").attr("r", 3);
NB. A built in property for this is going to be included in the next release.
Related
I have built a pie/doughnut chart using D3js (v4) as an Ember component and I am trying to have segments with specific labels be filled with a specific color but it is proving difficult.
To color the charts I have the following code:
marc = arc().outerRadius(radius - 10).innerRadius(radius - donutwidth),
color = scaleOrdinal().range(['#49b6d6', '#f59c1a', '#ff5b57', '#00acac',]),
gEnter.append("path")
.attr("d", marc)
.attr("fill", (d, i) => {
return color(i);
})
The above works fine and fills the arcs with the selected colors but not the color I want per arc. The index of the array is consistent so I tried to simply re-arrange the order of the colors with no effect.
I also tried using an if statement based on the index like:
gEnter.append("path")
.attr("d", marc)
.attr("fill", (d, i) => {
if (i === 0 { return color([0]) }
})
This does fill in the segment which is index 0 but not with the selected color from the list. Changing the number in color([0]) actually produces no change at all. This is also true if I try to use a conditional based on the string of the Label instead of the index of the array.
EDIT
As part of the Ember Computed Property that formats data for the chart, the data is re-ordered so that each label is presented in the same order every time. THe computed property is as follows:
//takes the ember model 'referralsource' and groups it as needed
sourceData: groupBy('referralsource', 'label'),
//ember computed property that provides data to chart
pieData: Ember.computed('sourceData', function() {
let objs = this.get('sourceData')
let sortedObjs = _.sortBy(objs, 'value')
let newArray = []
sortedObjs.forEach(function(x) {
let newLabel = x.value
let count = x.items.length
let newData = {
label: newLabel,
count: count
}
newArray.push(newData)
})
return newArray
}),
in your first example, try changing this:
color = scaleOrdinal().range(['#49b6d6', '#f59c1a', '#ff5b57', '#00acac',]),
for this:
color = scaleOrdinal().range([0,4]),
color.domain(['#49b6d6', '#f59c1a', '#ff5b57', '#00acac']),
the range you use to indicate the size of your scale (in this case 4 because you put 4 colors) and the domain specifies what things are in each position of that scale
If your labels are the same each time (or draw from the same pool of options), you can specify a specific domain. In an ordinal scale, the domain :
sets the domain to the specified array of values. The first element in
domain will be mapped to the first element in the range, the second
domain value to the second range value, and so on (from the API documentation).
By setting the domain equal to an array that contains each possible label option, you can easily assign a color to each label. The example below has five possible labels, the first row uses the opposite data array order as the second row, the third row uses a random order with duplicates. All three rows associate each datum with a specific color consistently:
var labels = ["redData","blueData","orangeData","pinkData","greenData"];
var colors = ["crimson","steelblue","orange","lightsalmon","lawngreen"];
var scale = d3.scaleOrdinal()
.domain(labels) // input values
.range(colors); // output values
var svg = d3.select("svg");
// initial order
svg.selectAll(null)
.data(labels)
.enter()
.append("circle")
.attr("cy",40)
.attr("cx", function(d,i) { return i * 40+ 20; })
.attr("r",15)
.attr("fill",function(d) { return scale(d); });
// reverse order
svg.selectAll(null)
.data(labels.reverse())
.enter()
.append("circle")
.attr("cy",80)
.attr("cx", function(d,i) { return i * 40+ 20; })
.attr("r",15)
.attr("fill",function(d) { return scale(d); });
// random labels
svg.selectAll(null)
.data(["blueData","blueData","redData","orangeData","blueData"])
.enter()
.append("circle")
.attr("cy",120)
.attr("cx", function(d,i) { return i * 40+ 20; })
.attr("r",15)
.attr("fill",function(d) { return scale(d); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="600" height="400"></svg>
I'm trying to make a pie chart with d3.js that looks like this:
Note that the labels are placed along the edges of the pie chart. Initially, I am able to draw the pie charts and properly place the text nodes (the fiddle only displays one pie chart; assume, however, that they all have data that works and is appropriate, as this one does). However, when I go to adjust the data, I can't seem to .attr(translate, transform) them to the correct region along the edge of the pie chart (or do anything to them, for that matter):
changeFunctions[i] = function (data, i) {
path.data(pie(data))
.transition()
.duration(750)
.attrTween("d", arcTween);
text.each(function (d, num) {
d3.select(this)
.text(function (t) {
return t.data.name+". "+(100 * t.data.votes/totalVotes).toFixed(0) + "%";
})
/*
.attr("transform", function (d) {
//console.log("the d", d)
var c = arc.centroid(d),
x = c[0], y = c[1],
h = Math.sqrt(x * x + y * y);
return "translate(" + (x/h * 100) + ',' + (y/h * 100) + ")";
})*/
.attr("opacity", function (t) {
return t.data.votes == 0 ? 0 : 1;
});
})
}
I have omitted the general code to draw the pie chart; it's in the jsfiddle. Basically, I draw each of the pie charts in a for loop and store this function, changeFunctions[i], in a closure, so that I have access to variables like path and text.
The path.data part of this function works; the pie chart properly adjusts its wedges. The text.each part, however, does not.
How should I go about making the text nodes update both their values and locations?
fiddle
When updating the text elements, you also need to update the data that's bound to them, else nothing will happen. When you create the elements, you're binding the data to the g element that contains the arc segment and text. By then appending path and text, the data is "inherited" to those elements. You're exploiting this fact by referencing d when setting attributes for those elements.
Probably the best way to make it work is to use the same pattern on update. That is, instead of updating only the data bound to the path elements as you're doing at the moment, update the data for the g elements. Then you can .select() the descendant path and text elements, which will again inherit the new data to them. Then you can set the attributes in the usual manner.
This requires a few changes to your code. In particular, there should be a variable for the g element selection and not just for the paths to make things easier:
var g = svg.selectAll("g.arc")
.data(pie(data));
g.enter()
.append("g").attr("class", "arc");
var path = g.append("path");
The changeFunction code changes as follows:
var gnew = g.data(pie(data));
gnew.select("path")
.transition()
.duration(750)
.attrTween("d", arcTween);
Now, to update the text, you just need to select it and reset the attributes:
gnew.select("text")
.attr("transform", function(d) { ... });
Complete demo here.
I'd like to represent the difference between the current data set and the previous data set, as calculated by the client.
Imagine I already have three circles, bound to the data [1, 2, 3]. Now I'd like to update the data and do something based on the difference between the new values and the old?
var new_data = [2, 2, 2]; // This is the new data I'd like to compare with the old
svg.selectAll("circle").data(new_data)
.transition().duration(2000)
.attr("fill", "red") // e.g. I'd like to colour the circles red if the change
// is negative, blue if positive, black if no change.
.attr("r", function(d) { return d * 10; });
Here's a JSFiddle with the above code set into an example.
You have two options for saving the old data attached to an element in order to identify changes after a new data join.
The first option, as you suggested, is to use data attributes. This SO Q&A describes that approach. Things to consider:
all your data values will get coerced to strings
you'll need a separate method call/attribute for each aspect of the data
you're manipulating the DOM, so it could slow things down if you've got a lot of elements or lot of data for each
the data is now part of the DOM, so can be saved with the image or accessed by other scripts
The second option is to store the data as a Javascript property of the DOM object for the element, in the same way that d3 stores the active data as the __data__ property. I've discussed this method in this forum post.
The general approach:
selection = selection.property(" __oldData__", function(d){ return d; } );
//store the old data as a property of the node
.data(newData, dataKeyFunction);
//over-write the default data property with new data
//and store the new data-joined selection in your variable
selection.enter() /*etc*/;
selection.attr("fill", function(d) {
// Within any d3 callback function,
// you can now compare `d` (the new data object)
// with `this.__oldData__` (the old data object).
// Just remember to check whether `this.__oldData__` exists
// to account for the just-entered elements.
if (this.__oldData__) { //old data exists
var dif = d.value - this.__oldData__.value;
return (dif) ? //is dif non-zero?
( (dif > 0)? "blue" : "red" ) :
"black" ;
} else {
return "green"; //value for new data
}
});
selection.property("__oldData__", null);
//delete the old data once it's no longer needed
//(not required, but a good idea if it's using up a lot of memory)
You can of course use any name for the old data property, it's just convention to throw a lot of "_" characters around it to avoid messing up any of the browser's native DOM properties.
As of D3 v4 you can use the built-in support for local variables. The internal implementation is basically the same as suggested by AmeliaBR's answer, but it frees you from having to do the storing of old data on your own. When using d3.local() you can set a value scoped to a specific DOM node, hence the name local variable. In below snippet this is done for each circle by the line
.each(function(d) { previousData.set(this, d) }); // Store previous data locally...
You can later on retrieve that value for any particular node it was stored upon:
.attr("fill", function(d) {
var diff = previousData.get(this) - d; // Retrieve previously stored data.
return diff < 0 ? "red" : diff > 0 ? "blue" : "black";
})
This full code might look something like this:
var old_data = [1, 2, 3]; // When the data gets updated I'd like to 'remember' these values
// Create a local variable for storing previous data.
var previousData = d3.local();
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 200);
var p = d3.select("body")
.append("p")
.text("Old data. Click on the circles to update the data.");
var circle = svg.selectAll("circle")
.data(old_data)
.enter().append("circle")
.attr("fill", "black")
.attr("r", function(d) { return d * 10; })
.attr("cx", function(d){ return d * 40; })
.attr("cy", function(d){ return d * 40; })
.each(function(d) { previousData.set(this, d) }); // Store previous data locally on each node
svg.on("click", function(d) {
p.text("Updated data.");
var new_data = [2, 2, 2]; // This is the new data I'd like to compare with the old
circle.data(new_data)
.transition().duration(2000)
.attr("fill", function(d) {
var diff = previousData.get(this) - d; // Retrieve previously stored data.
return diff < 0 ? "red" : diff > 0 ? "blue" : "black";
})
.attr("r", function(d) { return d * 10; });
});
<script src="https://d3js.org/d3.v4.js"></script>
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 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.