I'm trying to make a plot that shows density estimations for two different distributions simultaneously, like this:
The data is in two columns of a CSV file. I've modified code from Mike Bostock's block on kernel density estimation, and have managed to make a plot that does what I want, but only if I manually specify the two separate density plots -- see this JSFiddle, particularly beginning at line 66:
svg.append("path")
.datum(kde(cola))
.attr("class", "area")
.attr("d", area)
.style("fill", "#a6cee3");
svg.append("path")
.datum(kde(colb))
.attr("class", "area")
.attr("d", area)
.style("fill", "#b2df8a");
I've tried various incantations with map() to try to get the data into a single object that can be used to set the color of each density area according to the color domain, e.g.:
var columns = color.domain().map(function(column) {
return {
column: column,
values: data.map(function(d) {
return {kde: kde(d[column])};
})
};
});
I don't have a great grasp of what map() does, but this definitely does not work.
How can I structure my data to make this plot in a less brittle way?
To make this generic and remove column dependency first prepare your data:
var newData = {};
// Columns should be numeric
data.forEach(function(d) {
//iterate over all the keys
d3.keys(d).forEach(function(col){
if (!newData[col])
newData[col] = [];//create an array if not present.
newData[col].push(+d[col])
});
});
Now newData will hold the data like this
{
a:[123, 23, 45 ...],
b: [34,567, 45, ...]
}
Next make the color domain like this:
var color = d3.scale.category10()
.domain(d3.keys(newData))//this will return the columns
.range(["#a6cee3", "#b2df8a"]);
Finally make your area chart like this:
d3.keys(newData).forEach(function(d){
svg.append("path")
.datum(kde(newData[d]))
.attr("class", "area")
.attr("d", area)
.style("fill", color(d));
})
So now the code will have no dependency over the column names and its generic.
working code here
Related
I created an heatmap using this example and this data:
NAME,YEAR,M1,M2
A,2000,20,5
B,2000,30,1
C,2000,,10
D,2000,,88
E,2000,,21
F,2000,84,3
G,2000,,64
A,2001,44,48
B,2001,15,51
C,2001,20,5
D,2001,95,2
E,2001,82,9
F,2001,,77
G,2001,3,80
A,2002,8,99
B,2002,92,52
C,2002,62,
D,2002,41,
E,2002,66,
F,2002,21,21
G,2002,62,4
A,2003,2,5
B,2003,89,78
C,2003,9,
D,2003,7,9
E,2003,2,45
F,2003,92,58
G,2003,2,14
A,2004,2,55
B,2004,89,58
C,2004,9,55
D,2004,7,59
E,2004,2,70
F,2004,92,
G,2004,2,
Now I would like to add to the right of the heatmap a sparkline for each row, so there must be a sparkline associated with A, to B, etc.
And I wish they were positioned right next to each other.
To make the sparklines I saw this example.
This is the result: PLUNKER.
As you can see, I can't get the data correctly from the data.csv file to create the sparklines. Also I don't know how to place them in the correct position.
I tried this way but without success.
var sparkSvg = d3.select("#container-sparkline")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.data(dataNest)
.enter()
.append("path")
.attr("class", "sparkline-path")
.attr("d", function(d) {
console.log("i");
console.log(d);
});
Also I'm not sure using two div is the correct way to put a chart near another chart.
Anyone would know how to help me?
Approach:
I've created a sparkline for every name in data set with values on x axis as a year and y as a m2 value from data set. For the demo purposes I've hardcoded number of years to 5 so x axis have only 5 values, but that can be computed with some additional script based on input data.
I've also added tome padding for sparkline container so they're aligned with the heatmap
Code:
As you can see in the plunker I've introduced a function to group data by name, so for each name we have an array with objects:
var groupBy = function(array, key) {
return array.reduce(function(a, v) {
(a[v[key]] = a[v[key]] || []).push(v);
return a;
}, {});
};
// data grouped by name
var groupedData = groupBy(data, 'name');
Since we assumed for demo purposes that X axis has fixed number of values we need to find max value for Y axis to properly scale charts. To do that I reduce array of values to get only m2 values and find a max number whthin that array:
var maxYvalue = Math.max(...data.map(function(d){return Number(d.m2)}));
Now we can create scales for the sparklines
var x = d3.scaleLinear().domain([0, 4]).range([0, 60]);
var y = d3.scaleLinear().domain([0, maxYvalue]).range([2, itemSize-2 ]);
I'm assuming that chart have width of 60px and height of itemSize, I also introduce 2px of vertical padding so its easier to read those sparklines being on next to each-other.
Now we can define d3.line(as you already did in your plunker) which we'll use fro rendering sparklines .
var line = d3.line()
.x(function(d, i) { return x(i); })
.y(function(d) { return y(d); })
And last step is to render sparklines inside "#container-sparkline" container. To do that we can iterate over every array in groupedData and render sparkline for each name:
// for each name render sparkline
Object.keys(groupedData).forEach(function(key){
const sparkData = groupedData[key].map(function(datum){
return Number(datum['m2']);
})
var sparkSvg = d3.select("#container-sparkline")
.append("svg")
.attr("width", "100%")
.attr("height", itemSize-1)
.append("path")
.attr("class", "sparkline-path")
.attr("d", line(sparkData));
})
I've also slightly changed styles for #container-sparkline and added borders for sparkline svg's. I hope this is what you've asked for.
Here you can find your plunker with my changes
http://plnkr.co/edit/9vUFI76Ghieq4yZID5B7?p=preview
Update
I have made some progress and updated my question in light of my new understanding of topojson's functions.
To achieve my previous goal I used a method similar to this example. Namely, I hard-coded the FIPS id's of the counties I wished to merge into a set then created a FeatureCollection to use for creating my paths. Here is one example for northern Texas:
var set1 = d3.set([
48111, 48421, 48195, 48357, 48295, 48205, 48341, 48233, 48393, 48211,
48359, 48375, 48065, 48179, 48483, 48485, 48077, 48337, 48237, 48009,
48503, 48023, 48269, 48125, 48107, 48303, 48219, 48079, 48501, 48445,
48305, 48169,
48117, 48381, 48011, 48129, 48087, 48369, 48069, 48437, 48045, 48191,
48075, 48017, 48279, 48189, 48153, 48345, 48101, 48155, 48197, 48487
]);
var region1 = {type: "FeatureCollection", features: counties.filter(function(d) {return set1.has(d.id); })};
countiesG.append("path")
.datum(region1)
.attr("class", "region")
.attr("d", path);
countiesG.append("path")
.datum(topojson.merge(us, us.objects.counties.geometries.filter(function(d) {return set1.has(d.id); })))
.attr("class", "region-boundary")
.attr("d", path);
var set2 = d3.set([
17035, 17032, 17038, 17083, 17022, 17062
]);
var region2 = {type: "FeatureCollection", features: counties.filter(function(d) {return set2.has(d.id); })};
countiesG.append("path")
.datum(region2)
.attr("class", "region")
.attr("d", path);
countiesG.append("path")
.datum(topojson.merge(us, us.objects.counties.geometries.filter(function(d) {return set2.has(d.id); })))
.attr("class", "region-boundary")
.attr("d", path);
I will just provide two region's code above for reference. In reality, I repeated this process many times to create all of my desired merged conglomerate regions. Hard-coding all of this data was arduous and makes my code less readable. My hope is that the hard work is behind me, and I can somehow use these new merged multi-polygon regions in the future.
My question is: Now that I have hard-coded every region, can topojson somehow save the coordinates of these new merged multipolygons into the original us.json file or even a new json file? That way when I parse the json's coordinates it will have all of my custom merged regions -- which would allow me to delete all my hard-coded regions. I'm open to other approaches as well, as long as I can achieve my goal of exporting the merged multipolygon boundary coordinates.
I'm really new to coding, and also to asking questions about coding. So let me know if my explanation is overly complex, or if you need more context on anything, etc.
I am creating an interactive map of migration flows on the Mediterranean Sea. The flows show origin and destination regions of the migrant flows, as well as the total number of migrants, for Italy and Greece. Flows should be displayed in a Sankey diagram like manner. Because I am displaying the flows on a map and not in a diagram fashion, I am not using D3’s Sankey plugin, but creating my own paths.
My flow map, as of now (curved flows are on top of each other, should line up next to each other)
For generating my flows I have four points:
2 points for the straight middle part of the flow (country total)
1 point each for the curved outer parts (origin and destination region), using the two points of the straight middle part as starting points
The straight middle and both curved outer parts are each generated independently from their own data source. Flow lines are updated by changing the data source and calling the function again. The flow lines are generated using the SVG path mini-language. In order for the curved outer parts of the flows to show correctly, I need them to be lined up next to each other. To line them up correctly, I need to shift their starting points. The distance of the shift for each path element is determined by the width of the path elements before it. So, grouping by country, each path element i needs to know the sum of the width of the elements 0-i in the same group.
After grouping my data with d3.nest(), which would allow me to iterate over each group, I am not able to bind the data correctly to the path elements
I also can't figure out a loop function that adds up values for all elements 0-i. Any help here? (Sorry if this is kind of unrelated to the issue of binding nested data)
Here is a working function for the curved paths, working for unnested data:
function lineFlow(data, flowSubGroup, flowDir) {
var flowSelect = svg.select(".flowGroup").select(flowSubGroup).selectAll("path");
var flow = flowSelect.data(data);
var flowDirection = flowDir;
flow.enter()
.append("path").append("title");
flow
.attr("stroke", "purple")
.attr("stroke-linecap", "butt")
.attr("fill", "none")
.attr("opacity", 0.75)
.transition()
.duration(transitionDur)
.ease(d3.easeCubic)
.attr("d", function(d) {
var
slope = (d.cy2-d.cy1)/(d.cx2-d.cx1),
dist = (Math.sqrt(Math.pow((d.rx2-d.rx1),2)+Math.pow((d.ry2-d.ry1),2)))*0.5,
ctrlx = d.rx1 + Math.sqrt((Math.pow(dist,2))/(1+Math.pow(slope,2)))*flowDirection,
ctrly = slope*(ctrlx-d.rx1)+d.ry1;
return "M"+d.rx1+","+d.ry1+"Q"+ctrlx+","+ctrly+","+d.rx2+","+d.ry2})
.attr("stroke-width", function(d) {return (d.totalmig)/flowScale});
flowSelect
.select("title")
.text(function(d) {
return d.region + "\n"
+ "Number of migrants: " + addSpaces(d.totalmig)});
};
I tried adapting the code to work with data grouped by country:
function lineFlowNested(data, flowSubGroup, flowDir) {
var g=svg.select(".flowGroup").select(flowSubGroup).append("g").data(data).enter();
var gflowSelect=g.selectAll("path");
var gflow=gflowSelect.data (function(d) {return d.values});
gflow.enter()
.append("path");
gflow.attr("stroke", "purple")
.attr("stroke-linecap", "butt")
.attr("fill", "none")
.attr("opacity", 0.75)
// .transition()
// .duration(transitionDur)
// .ease(d3.easeCubic)
.attr("d", function(d) {
var
slope = (d.cy2-d.cy1)/(d.cx2-d.cx1),
dist = (Math.sqrt(Math.pow((d.rx2-d.rx1),2)+Math.pow((d.ry2-d.ry1),2)))*0.5,
ctrlx = d.rx1 - Math.sqrt((Math.pow(dist,2))/(1+Math.pow(slope,2)))*flowDirection,
ctrly = slope*(ctrlx-d.rx1)+d.ry1;
return "M"+d.rx1+","+d.ry1+"Q"+ctrlx+","+ctrly+","+d.rx2+","+d.ry2})
.attr("stroke-width", function(d) {return (d.totalmig)/flowScale});
};
which isn't working. What am I doing wrong? Thanks for any hints!
I'm basically using a modified version of : http://dimplejs.org/advanced_examples_viewer.html?id=advanced_bar_labels .
I'd like to be able to add for each value a border on the left as high as the value (with a specific color for that border).
I'm not really sure where to start for adding that.
Any ideas?
Thanks.
More details : This is what I'd like to obtain : https://dl.dropboxusercontent.com/u/2227188/Image%202.png - the border on the left is the issue. (jsfiddle.net/mkzTk/5/ this what I currently have which is pretty much what's in the example - I don't know where to start really for adding a border)
You could append a rectangle after drawing for each element of the series as follows:
mySeries.afterDraw = function (s, d) {
var shape = d3.select(s);
svg.append("rect")
.attr("x", shape.attr("x"))
.attr("y", shape.attr("y"))
.attr("height", shape.attr("height"))
.attr("width", "10px")
.style("fill", shape.style("stroke"))
.style("pointer-events", "none");
};
The example you mention already uses the afterDraw function so just add the contents above to the existing method for labelling.
It looks nice, here's an example:
http://jsbin.com/lorin/9/edit?js,output#J:L20
I would set up each bar + edge pair as its own group based on a certain data point, and then append two rect elements to that group. Differences in color can be used to give them their distinctive colors.
Your code would look something like this:
var monthBars = d3.selectAll('.monthBar') //These will be for each chart
.data(allMyData, idFunction) //Assign and key your data
.enter()
.append('g')
.classed('monthBar', true);
.each(function(d){
var taskGroups = d3.select(this).selectAll('.taskGroup')
.data(d.dataForThisMonth, taskIdFn)
.enter()
.append('g')
.classed('.taskGroup', true);
.attr('transform', ...) //Define the x and y positioning for the group
taskGroups.append('rect')
//Make this the 'body' rect with the text in it
taskGroups.append('rect')
//Make this the edge rect
})
I want to make a bar chart in d3.js that has both positive and negative bars for each item or row, like this:
it's somewhat like the google finance "Sector Summary" chart (http://google.com/finance)
Can anyone point me to a d3.js example of this kind of chart? I am aware of the "bar chart with negative values" example here: http://bl.ocks.org/mbostock/2368837
If there is a relatively easy way to explain how to modify that example to accomplish what I want, that could work too.
Thanks!
Here is what I could do:
The basis is to have two values per item. To simplify things we can say that all values have to be positive, the first one will be displayed on the right, the second one on the left.
From the example you provided, we will just plot the second value we add:
data = [
{name: "A",value: 1,value2: 2},
...
]
We will also fix the domain (you can write a function to do it nicely afterwards):
x.domain([-10,10])
Finally, we will draw the second bars (on the left):
svg.selectAll(".bar2")
.data(data)
.enter().append("rect")
.attr("class", "bar2")
.attr("x", function (d) {
return x(Math.min(0, -d.value2));
})
.attr("y", function (d) {
return y(d.name);
})
.attr("width", function (d) {
return Math.abs(x(-d.value2) - x(0));
})
.attr("height", y.rangeBand());
This code is just copy paste from the example where I replaced d.value by -d.value1 and .bar by .bar2 for the class.
You will also have to modify the x-axis format for having "75, 50, 25, 0, 25, 50, 75".
jsFiddle: http://jsfiddle.net/chrisJamesC/vgZ6E/