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>
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 get to grips with d3 by creating a bar chart of house prices based on whether the property address ends with 'Street', 'Road', 'Way' etc.
However, I'd also like the view of the data to change based on a column of data for local neighbourhoods.
It's the second query on this topic. Here's the previous query - How to extract nominal labels for d3 chart
You can see the structure of the data extracted through Pandas' to_json function here: http://plnkr.co/edit/He3yPUfuE8k7hvIkupjS?p=preview
I've used a nest function to key the data on the the local areas, but can't work out how to plug in a d3.filter method to restrict the data to a selected area.
I've got a function which creates a select button based on the keys:
var options = dropDown.selectAll("option")
.data(nested_data)
.enter()
.append("option");
options.text(function (d) { return d.key; })
.attr("value", function (d) { return d.key; });
But what I can't work out is how to plug the value from this selection into the plotting part of the d3 script.
d3.select("svg")
.selectAll("circle")
.data(nested_data)
.enter()
.append("circle")
d3.selectAll("circle")
.attr("cx", function(d) {
console.log(d["street_name"]);
return street_scale(d["street_name"]);
})
.attr("cy", function(d) {
return price_scale(d["value"]);
})
.attr("r", 5)
.attr("fill", "steelblue");
And while I know I need an update function to continue to change the chart as users select between, I've not found an example that I can adapt.
Thank you in advance for your patience - I'm very new to d3 and a Javascript noob.
Think about the data structure you want in the end. Since d3 likes arrays of objects and you want to filter by district, I'm picturing this:
var data = {
district1: [
{
street_split: 'a',
value: 1
},{
street_split: 'b',
value: 2
}
],
district2: [
{
street_split: 'a',
value: 1
},{
street_split: 'b',
value: 2
}
],
etc...
Your filter then simply becomes:
data[someDistrict]
So, how do we get your data in this format. I'd do everything in one loop, the data, the extents, the labels, etc...:
var orgData = {}, // our final data like above
street_labels = d3.set(), // set of streets
districts = d3.set(), // set of districts
minVal = 1e99, // min of values
maxVal = -1e99; //max of values
data.forEach(function(d){
d.value = +d.value; // convert to numeric
street_labels.add(d.street_split); // set of street_labels
districts.add(d.district); // set of districts
if (d.value < minVal) minVal = d.value; // new min?
if (d.value > maxVal) maxVal = d.value; //new max?
if (!orgData[d.district]){ // we want a associate array with keys of districts
orgData[d.district] = []; // and values that are arrays of object
}
orgData[d.district].push({ // those objects are street_split and value
street_split: d.street_split,
value: d.value
});
});
Now how do we update on a different select? That simply becomes:
dropDown.on("change", function() {
d3.selectAll("circle")
.data(orgData[this.value]) // new data
.attr("cx", function(d) { // update attributes
return street_scale(d.street_split);
})
.attr("cy", function(d) {
return price_scale(d.value);
});
});
Here's my working code.
As project to get to know d3.js, I’m displaying tweets on a map in real-time. Everything has worked this far, and I’m very, very pleased with the library.
On the page, I’m trying to list all languages. On hover, I want all tweets of that language to pop up. All of this is done, except some items in the list pops up the tweets of another language. A wrong one, I might add.
This is how I project the dots on the map:
points.selectAll('circle')
.data(tweets)
.enter()
.append('circle')
// Attach node to tweet, so I can use refer to the nodes later
.each(function(d) {
d.node = this;
})
.attr('r', 1);
This is how I create the list:
var data = d3.nest()
// Group by language code
.key(function(d) { return d.lang.code; })
.entries(tweets)
// Sort list by amount of tweets in that language
.sort(function(a, b) {
return b.values.length - a.values.length;
});
var items = languages_dom
// Add items
.selectAll('li')
.data(data)
.enter()
.append('li');
// Used for debugging
.attr('data-lang', function(d) {
return d.key; // Group key = language code
})
// Set text
.text(function(d) {
var dt = d.values[0];
return dt.lang.name;
})
// Mouseover handler
.on('mouseover', function(d) {
// Compare attribute with
// These values are actually different
var attr = d3.select(this).attr('data-lang');
console.log(attr, d.key);
// Pop up each node
d.values.forEach(function(d) {
d = d3.select(d.node);
d.transition()
.duration(200)
.attr('opacity', 0.5)
.attr('r', 8);
});
});
Note that the script above is run several times. d.key refers to another value later in the chain, while I’m not modifying data in that chain.
Edit 22:08
Things seems to work fine when I’m not sorting the data. At least it’s a lead.
As noted in the comments, you're overwriting d in your forEach function. Instead, you should try something like
d.values.forEach(function(p) {
d3.select(p.node)
.transition()
.duration(200)
.attr('opacity', 0.5)
.attr('r', 8);
});
Notice the forEach variable is named p instead of d.
As the data changed, the old data seems to be kept somehow.
Either way, I simply deleted the list before applying the new data:
languages_dom
.selectAll('li')
.remove();
Can’t say this is graceful, nor performant, but it gets the job done :)
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 using d3.js to create a large number of svg:ellipse elements (~5000). After the initial rendering some of the data items can be updated via the backend (I'll know which ones) and I want to change the color of those ellipses (for example).
Is there a fast way to recover the DOM element or elements associated with a data item or items? Other than the obvious technique if recomputing a join over the full set of DOM elements with the subset of data?
var myData = [{ id: 'item1'}, { id: 'item2' }, ... { id: 'item5000' }];
var create = d3.selectAll('ellipse).data(myData, function(d) { return d.id; });
create.enter().append('ellipse').each(function(d) {
// initialize ellipse
});
// later on
// this works, but it seems like it would have to iterate over all 5000 elements
var subset = myData.slice(1200, 1210); // just an example
var updateElements = d3.selectAll('ellipse').data(subset, function(d) { return d.id; });
updateElements.each(function(d) {
// this was O(5000) to do the join, I _think_
// change color or otherwise update
});
I'm rendering updates multiple times per second (as fast as possible, really) and it seems like O(5000) to update a handful of elements is a lot.
I was thinking of something like this:
create.enter().append('ellipse').each(function(d) {
d.__dom = this;
// continue with initialization
});
// later on
// pull the dom nodes back out
var subset = myData.slice(1200, 1210).map(function(d) { return d.__dom; });
d3.selectAll(subset).each(function(d) {
// now it should be O(subset.length)
});
This works. But it seems like this would be a common pattern, so I'm wondering if there is a standard way to solve this problem? I actually want to use my data in multiple renderings, so I would need to be more clever so they don't trip over each other.
Basically, I know that d3 provides a map from DOM -> data via domElement.__data__. Is there a fast and easy way to compute the reverse map, other than caching the values myself manually?
I need to get from data -> DOM.
As long as you keep the d3 selection reference alive (create in your example), D3 is using a map to map the data keys to DOM nodes in the update so it's actually O(log n).
We can do some testing with the D3 update /data operator method vs a loop method over the subset:
var d3UpdateMethod = function() {
svg.selectAll("ellipse").data(subset, keyFunc)
.attr("style", "fill:green");
}
var loopMethod = function() {
for (var i=0; i < subset.length; i++) {
svg.selectAll(".di" + i)
.attr("style", "fill:green");
}
}
var timedTest = function(f) {
var sumTime=0;
for (var i=0; i < 10; i++) {
var startTime = Date.now();
f();
sumTime += (Date.now() - startTime);
}
return sumTime / 10;
};
var nextY = 100;
var log = function(text) {
svg.append("text")
.attr("x", width/2)
.attr("y", nextY+=100)
.attr("text-anchor", "middle")
.attr("style", "fill:red")
.text(text);
};
log("d3UpdateMethod time:" + timedTest(d3UpdateMethod));
log("loopMethod time:" + timedTest(loopMethod));
I also created a fiddle to demonstrate what I understand you're trying to do here.
Another method to make it easy to track the nodes that are in your subset is by adding a CSS class to the subset. For example:
var ellipse = svg.selectAll("ellipse").data(data, keyFunc).enter()
.append("ellipse")
.attr("class", function (d) {
var cl = "di" + d.i;
if (d.i % 10 == 0)
cl+= " subset"; //<< add css class for those nodes to be updated later
return cl;
})
...
Note how the "subset" class would be added only to those nodes that you know are in your subset to be updated later. You can then select them later for an update with the following:
svg.selectAll("ellipse.subset").attr("style", "fill:yellow");
I updated the fiddle to include this test too and it's nearly as fast as the directMethod.