Related
I am trying to create two heatmaps showing different data updated by a common date drop down. I am using heatmap with data update and creating two separate svgs to update when a new date field is selected in the dropdown. I was able to follow some of the SO answers to create two plots in the same page, but I am totally clueless as to have just one drop down of the locations update both charts simultaneously. Any pointers on how to achieve this would be greatly appreciated. I have included the code I have been working with and the json data. I have the data in two different files for now. Would be even better if it can be just from one file to make it easier to read the location dropdown value.
var dataset;
var dataset2;
var days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
times = d3.range(24);
var margin = {top:40, right:50, bottom:70, left:50};
// calculate width and height based on window size
var w = Math.max(Math.min(window.innerWidth, 1000), 500) - margin.left - margin.right - 20,
gridSize = Math.floor(w / times.length),
h = gridSize * (days.length+2);
//reset the overall font size
var newFontSize = w * 62.5 / 900;
d3.select("html").style("font-size", newFontSize + "%");
// svg container
var svg = d3.select("#heatmap")
.append("svg")
.attr("width", w + margin.top + margin.bottom)
.attr("height", h + margin.left + margin.right)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// svg container
var svg2 = d3.select("#heatmap2")
.append("svg")
.attr("width", w + margin.top + margin.bottom)
.attr("height", h + margin.left + margin.right)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// linear colour scale
var colours = d3.scaleLinear()
.domain(d3.range(1, 11, 1))
.range(["#87cefa", "#86c6ef", "#85bde4", "#83b7d9", "#82afce", "#80a6c2", "#7e9fb8", "#7995aa", "#758b9e", "#708090"]);
var dayLabels = svg.selectAll(".dayLabel")
.data(days)
.enter()
.append("text")
.text(function(d) { return d; })
.attr("x", 0)
.attr("y", function(d, i) { return i * gridSize; })
.style("text-anchor", "end")
.attr("transform", "translate(-6," + gridSize / 1.5 + ")")
var dayLabels = svg2.selectAll(".dayLabel")
.data(days)
.enter()
.append("text")
.text(function(d) { return d; })
.attr("x", 0)
.attr("y", function(d, i) { return i * gridSize; })
.style("text-anchor", "end")
.attr("transform", "translate(-6," + gridSize / 1.5 + ")")
var timeLabels = svg.selectAll(".timeLabel")
.data(times)
.enter()
.append("text")
.text(function(d) { return d; })
.attr("x", function(d, i) { return i * gridSize; })
.attr("y", 0)
.style("text-anchor", "middle")
.attr("transform", "translate(" + gridSize / 2 + ", -6)");
var timeLabels = svg2.selectAll(".timeLabel")
.data(times)
.enter()
.append("text")
.text(function(d) { return d; })
.attr("x", function(d, i) { return i * gridSize; })
.attr("y", 0)
.style("text-anchor", "middle")
.attr("transform", "translate(" + gridSize / 2 + ", -6)");
// load data heatmap 1
d3.json("test.json", function(error, data) {
data.forEach(function(d) {
d.day = +d.day;
d.hour = +d.hour;
d.value = +d.value;
});
dataset = data;
// group data by location
var nest = d3.nest()
.key(function(d) { return d.location; })
.entries(dataset);
// array of locations in the data
var locations = nest.map(function(d) { return d.key; });
var currentLocationIndex = 0;
// create location dropdown menu
var locationMenu = d3.select("#locationDropdown1");
locationMenu
.append("select")
.attr("id", "locationMenu")
.selectAll("option")
.data(locations)
.enter()
.append("option")
.attr("value", function(d, i) { return i; })
.text(function(d) { return d; });
// function to create the initial heatmap
var drawHeatmap = function(location) {
// filter the data to return object of location of interest
var selectLocation = nest.find(function(d) {
return d.key == location;
});
var heatmap = svg.selectAll(".hour")
.data(selectLocation.values)
.enter()
.append("rect")
.attr("x", function(d) { return (d.hour-1) * gridSize; })
.attr("y", function(d) { return (d.day-1) * gridSize; })
.attr("class", "hour bordered")
.attr("width", gridSize)
.attr("height", gridSize)
.style("stroke", "white")
.style("stroke-opacity", 0.6)
.style("fill", function(d) { return colours(d.value); })
}
drawHeatmap(locations[currentLocationIndex]);
var updateHeatmap = function(location) {
// filter data to return object of location of interest
var selectLocation = nest.find(function(d) {
return d.key == location;
});
// update the data and redraw heatmap
var heatmap = svg.selectAll(".hour")
.data(selectLocation.values)
.transition()
.duration(500)
.style("fill", function(d) { return colours(d.value); })
}
// run update function when dropdown selection changes
locationMenu.on("change", function() {
// find which location was selected from the dropdown
var selectedLocation = d3.select(this)
.select("select")
.property("value");
currentLocationIndex = +selectedLocation;
// run update function with selected location
updateHeatmap(locations[currentLocationIndex]);
});
})
// load data heatmap 2
d3.json("test2.json", function(error, data2) {
data2.forEach(function(d2) {
d2.day = +d2.day;
d2.hour = +d2.hour;
d2.value = +d2.value;
});
dataset2 = data2;
// group data by location
var nest2 = d3.nest()
.key(function(d2) { return d2.location; })
.entries(dataset2);
// array of locations in the data
var locations2 = nest2.map(function(d2) { return d2.key; });
var currentLocationIndex2 = 0;
// create location dropdown menu
var locationMenu2 = d3.select("#locationDropdown2");
locationMenu2
.append("select")
.attr("id", "locationMenu")
.selectAll("option")
.data(locations2)
.enter()
.append("option")
.attr("value", function(d2, i2) { return i2; })
.text(function(d2) { return d2; });
// function to create the initial heatmap
var drawHeatmap2 = function(location2) {
// filter the data to return object of location of interest
var selectLocation2 = nest2.find(function(d2) {
return d2.key == location2;
});
var heatmap2 = svg2.selectAll(".hour")
.data(selectLocation2.values)
.enter()
.append("rect")
.attr("x", function(d2) { return (d2.hour-1) * gridSize; })
.attr("y", function(d2) { return (d2.day-1) * gridSize; })
.attr("class", "hour bordered")
.attr("width", gridSize)
.attr("height", gridSize)
.style("stroke", "white")
.style("stroke-opacity", 0.6)
.style("fill", function(d2) { return colours(d2.value); })
}
drawHeatmap2(locations2[currentLocationIndex2]);
var updateHeatmap2 = function(location2) {
console.log("currentLocationIndex: " + currentLocationIndex2)
// filter data to return object of location of interest
var selectLocation2 = nest2.find(function(d2) {
return d2.key == location2;
});
// update the data and redraw heatmap
var heatmap2 = svg2.selectAll(".hour")
.data(selectLocation2.values)
.transition()
.duration(500)
.style("fill", function(d2) { return colours(d2.value); })
}
// run update function when dropdown selection changes
locationMenu2.on("change", function() {
// find which location was selected from the dropdown
var selectedLocation2 = d3.select(this)
.select("select")
.property("value");
currentLocationIndex2 = +selectedLocation2;
// run update function with selected location
updateHeatmap2(locations2[currentLocationIndex2]);
});
})
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
html {
font-size: 62.5%;
}
body {
margin-top: 30px;
font-size: 1.4rem;
font-family: 'Source Sans Pro', sans-serif;
font-weight: 400;
fill: #696969;
text-align: center;
}
.timeLabel, .dayLabel {
font-size: 1.6rem;
fill: #AAAAAA;
font-weight: 300;
}
#nav-container {
display: flex;
justify-content: center;
cursor: pointer;
}
</style>
</head>
<body>
<div id="nav-container">
<div id="locationDropdown1"></div>
</div>
<div id="heatmap"></div>
<div id="nav-container">
<div id="locationDropdown2"></div>
</div>
<div id="heatmap2"></div>
JSON sample is the same as the link above .. just with some values modified to show differentiation. (Not sure how to include a big json file on here). Thanks again.
I am new using d3.js. I like to represent a multiline chart. In the x axis the years, in the y axis the quantity of patents every country has. Every line should be a different country.
I have a csv file with the following information:
PATENT,GYEAR,GDATE,APPYEAR,COUNTRY
3070801,1963,1096,,"BE"
3070802,1963,1096,,"US"
3070802,1963,1096,,"US"
3070802,1963,1096,,"US"
3070802,1963,1096,,"US"
3070803,1964,1096,,"US"
3070804,1964,1096,,"US"
3070801,1964,1096,,"BE"
3070801,1964,1096,,"BE"
3070801,1964,1096,,"BE"
3070801,1964,1096,,"BE"
3070801,1964,1096,,"BE"
3070801,1964,1096,,"BE"
...
I use a nested structure like the following:
var countries = d3.nest()
.key(function(d) { return d.COUNTRY; })
.key(function(d) { return d.GYEAR; })
.rollup(function(v) { return { "total": v.length} })
.map(data);
This structure give me the information in the following way:
BE: Object
1963: Object
total: 1
1964: Object
total: 6
US: Object
1963: Object
total: 4
1964: Object
total: 2
My script:
<script>
var margin = {top: 20, right: 80, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var parseDate = d3.time.format("%Y").parse;
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var line = d3.svg.line()
.interpolate("basis")
.x(function(d) {...}) //not so sure how to obtain the x and y
.y(function(d) {... });
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data3", function(error, data) {
if (error) throw error;
color.domain(d3.keys(data[0]).filter(function (key) {
return key !== "GYEAR";
}));
data.forEach(function(d) {
d.GYEAR = parseDate(String(d.GYEAR));
});
var countries = d3.nest()
.key(function(d) { return d.COUNTRY; })
.key(function(d) { return d.GYEAR; })
.rollup(function(v) { return { "total": v.length} })
.map(data);
x.domain(d3.extent(data, function (d) {
return d.GYEAR;
}));
y.domain([0,10]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.style({'stroke': 'Black', 'fill': 'none', 'stroke-width': '1.5px'})
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.style({'stroke': 'Black', 'fill': 'none', 'stroke-width': '1.5px'})
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Cantidad");
var city = svg.selectAll(".country")
.data(countries)
.enter().append("g")
.attr("class", "country");
//How to append path?
city.append("path")
.attr("class", "line")
.attr("d", function(d) {...})
.style("stroke", function(d) {...});
});
</script>
How can I construct my var line? How can I append the path?
Thanks in advance.
For generating line function you can do:
var line = d3.svg.line()
.interpolate("basis")
.x(function(d) {console.log(x(d.year));return x(d.year)}) // x will be year
.y(function(d) {console.log(y(d.total));return y(d.total)}); //y will be total
For rollup function get all the data:
var countries = d3.nest()
.key(function(d) { return d.COUNTRY; })
.key(function(d) { return d.GYEAR.getFullYear(); })
.rollup(function(v){
var pat = 0;
var yr = 0;
var country = ""
v.forEach(function(r){
pat += parseInt(r.PATENT);
yr = r.GYEAR;
country = r.COUNTRY
});
//store all the info like patent year country
return { "total": v.length, patent:pat, year:yr, country: country}
})
.map(data);
For setting values for Line:
var city = svg.selectAll(".country")
.data([countries.BE, countries.US])//this will genrate 2 lines for the two datset.
.enter().append("g")
.attr("class", "country");
//How to append path?
city.append("path")
.attr("class", "line")
.attr("d", function(d) {
var d_array = []
//for all the years make it an array
for (key in d) {
d_array.push(d[key])
}
return line(d_array);
})
.style("stroke", function(d) {
var country = "";
//the first object will tell the country
for (key in d) {
country = d[key].country;
break;
}
return color(country)
});
EDIT
For putting the countries in general form:
Instead of doing this:
.data([countries.BE, countries.US]),
Do this to get the array:
var country_data = [];
for (key in countries) {
country_data.push(countries[key])
}
and later do
.data(country_data),
Working code here
Cleaner code here
Hope this helps!
I'm building a grouped bar chart by nesting a .csv file. The chart will also be viewable as a line chart, so I want a nesting structure that suits the line object. My original .csv looks like this:
Month,Actual,Forecast,Budget
Jul-14,200000,-,74073.86651
Aug-14,198426.57,-,155530.2499
Sep-14,290681.62,-,220881.4631
Oct-14,362974.9,-,314506.6437
Nov-14,397662.09,-,382407.67
Dec-14,512434.27,-,442192.1932
Jan-15,511470.25,511470.25,495847.6137
Feb-15,-,536472.5467,520849.9105
Mar-15,-,612579.9047,596957.2684
Apr-15,-,680936.5086,465313.8723
May-15,-,755526.7173,739904.081
Jun-15,-,811512.772,895890.1357
and my nesting is like this:
d3.csv("data/net.csv", function(error, data) {
if (error) throw error;
var headers = d3.keys(data[0]).filter(function(head) {
return head != "Month";
});
data.forEach(function(d) {
d.month = parseDate(d.Month);
});
var categories = headers.map(function(name) {
return {
name: name, // "name": the csv headers except month
values: data.map(function(d) {
return {
date: d.month,
rate: +(d[name]),
};
}),
};
});
The code to build my chart is:
var bars = svg.selectAll(".barGroup")
.data(data) // Select nested data and append to new svg group elements
.enter()
.append("g")
.attr("class", "barGroup")
.attr("transform", function (d) { return "translate(" + xScale(d.month) + ",0)"; });
bars.selectAll("rect")
.data(categories)
.enter()
.append("rect")
.attr("width", barWidth)
.attr("x", function (d, i) { if (i < 2) {return 0;} else {return xScale.rangeBand() / 2;}})
.attr("y", function (d) { return yScale(d.rate); })
.attr("height", function (d) { return h - yScale(d.rate); })
.attr("class", function (d) { return lineClass(d.name); });
The g elements are fine and the individual bars are being mapped to them, with the x value and class applied correctly.
My problem comes in accessing the data for 'rate' for the height and y value of the bars. In the form above it gives a NaN. I've also tried using the category data to append g elements and then appending the rects with:
.data(function(d) { return d.values })
This allows me to access the rate data, but maps all 36 bars to each of the rangeBands.
It also works fine in a flatter data structure, but I can't seem to use it when it's nested two levels down, despite looking through a great many examples and SO questions.
How do I access the rate data?
In response to Cyril's request, here's the full code:
var margin = {top: 20, right: 18, bottom: 80, left: 50},
w = parseInt(d3.select("#bill").style("width")) - margin.left - margin.right,
h = parseInt(d3.select("#bill").style("height")) - margin.top - margin.bottom;
var customTimeFormat = d3.time.format.multi([
[".%L", function(d) { return d.getMilliseconds(); }],
[":%S", function(d) { return d.getSeconds(); }],
["%I:%M", function(d) { return d.getMinutes(); }],
["%I %p", function(d) { return d.getHours(); }],
["%a %d", function(d) { return d.getDay() && d.getDate() != 1; }],
["%b %d", function(d) { return d.getDate() != 1; }],
["%b", function(d) { return d.getMonth(); }],
["%Y", function() { return true; }]
]);
var parseDate = d3.time.format("%b-%y").parse;
var displayDate = d3.time.format("%b %Y");
var xScale = d3.scale.ordinal()
.rangeRoundBands([0, w], .1);
var xScale1 = d3.scale.linear()
.domain([0, 2]);
var yScale = d3.scale.linear()
.range([h, 0])
.nice();
var xAxis = d3.svg.axis()
.scale(xScale)
.tickFormat(customTimeFormat)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.innerTickSize(-w)
.outerTickSize(0);
var svg = d3.select("#svgCont")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var thous = d3.format(",.0f")
var lineClass = d3.scale.ordinal().range(["actual", "forecast", "budget"]);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<p id='date'>" + displayDate(d.date) + "</p><p id='value'>$" + thous(d.rate);
})
d3.csv("data/net.csv", function(error, data) {
if (error) throw error;
var headers = d3.keys(data[0]).filter(function(head) {
return head != "Month";
});
data.forEach(function(d) {
d.month = parseDate(d.Month);
});
var categories = headers.map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
date: d.month,
rate: +(d[name]),
};
}),
};
});
var min = d3.min(categories, function(d) {
return d3.min(d.values, function(d) {
return d.rate;
});
});
var max = d3.max(categories, function(d) {
return d3.max(d.values, function(d) {
return d.rate;
});
});
var minY = min < 0 ? min * 1.2 : min * 0.8;
xScale.domain(data.map(function(d) { return d.month; }));
yScale.domain([minY, (max * 1.1)]);
var barWidth = headers.length > 2 ? xScale.rangeBand() / 2 : xScale.rangeBand() ;
svg.call(tip);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
var bars = svg.selectAll(".barGroup")
.data(data)
.enter()
.append("g")
.attr("class", "barGroup")
.attr("transform", function (d) { return "translate(" + xScale(d.month) + ",0)"; });
bars.selectAll("rect")
.data(categories)
.enter()
.append("rect")
.attr("width", barWidth)
.attr("x", function (d, i) { if (i < 2) {return 0;} else {return xScale.rangeBand() / 2;}})
.attr("y", function (d) { return yScale(d.rate); })
.attr("height", function (d) { return h - yScale(d.rate); })
.attr("class", function (d) { return lineClass(d.name) + " bar"; });
var legend = svg.selectAll(".legend")
.data(headers)
.enter()
.append("g")
.attr("class", "legend");
legend.append("line")
.attr("class", function(d) { return lineClass(d); })
.attr("x1", 0)
.attr("x2", 40)
.attr("y1", function(d, i) { return (h + 30) + (i *14); })
.attr("y2", function(d, i) { return (h + 30) + (i *14); });
legend.append("text")
.attr("x", 50)
.attr("y", function(d, i) { return (h + 32) + (i *14); })
.text(function(d) { return d; });
svg.selectAll(".bar")
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
});
Update 18 Feb '16.
It seems I haven't explained what I was trying to do sufficiently well. The line and bar versions of the chart will be seen separately, i.e. users can see either one according to input to a select element. Also note that I don't have control over how the data comes in initially.
I have a version of exactly how it should work here.
This question was raised when I was still working through it, but I never solved the issue – I used a workaround of doing two separate nests of the data.
Link to jsfiddle:
https://jsfiddle.net/sladav/rLh4qwyf/1/
I think the root of the issue is that you want to use two variables that do not explicitly exist in your original data set: (1) Category and (2) Rate.
Your data is formatted in a wide format in that each category gets its own variable and the value for rate exists at the crossroads of month and one of the given categories. I think the way you're nesting ultimately is or at least should address this, but it is unclear to me if or where something gets lost in translation. Conceptually, I think it makes more sense to start with an organization that matches what you are trying to accomplish. I reformatted the original data and approached it again - on a conceptual level the nesting seems straightforward and simple...
NEW COLUMNS:
Month: Time Variable; mapped to X axis
Category: Categorical values [Actual, Forecast, Budget]; used to group/color
Rate: Numerical value; mapped to Y axis
Reorganized CSV (dropped NULLs):
Month,Category,Rate
Jul-14,Actual,200000
Aug-14,Actual,198426.57
Sep-14,Actual,290681.62
Oct-14,Actual,362974.9
Nov-14,Actual,397662.09
Dec-14,Actual,512434.27
Jan-15,Actual,511470.25
Jan-15,Forecast,511470.25
Feb-15,Forecast,536472.5467
Mar-15,Forecast,612579.9047
Apr-15,Forecast,680936.5086
May-15,Forecast,755526.7173
Jun-15,Forecast,811512.772
Jul-14,Budget,74073.86651
Aug-14,Budget,155530.2499
Sep-14,Budget,220881.4631
Oct-14,Budget,314506.6437
Nov-14,Budget,382407.67
Dec-14,Budget,442192.1932
Jan-15,Budget,495847.6137
Feb-15,Budget,520849.9105
Mar-15,Budget,596957.2684
Apr-15,Budget,465313.8723
May-15,Budget,739904.081
Jun-15,Budget,895890.1357
With your newly formatted data, you start by using d3.nest to GROUP your data explicitly with the CATEGORY variable. Now your data exists in two tiers. The first tier has three groups (one for each category). The second tier contains the RATE data for each line/set of bars. You have to nest your data selections as well - the first layer is used to draw the lines, the second layer for the bars.
Nesting your data:
var nestedData = d3.nest()
.key(function(d) { return d.Category;})
.entries(data)
Create svg groups for your grouped, 1st-tier data:
d3.select(".plot-space").selectAll(".g-category")
.data(nestedData)
.enter().append("g")
.attr("class", "g-category")
Use this data to add your lines/paths:
d3.selectAll(".g-category").append("path")
.attr("class", "line")
.attr("d", function(d){ return lineFunction(d.values);})
.style("stroke", function(d) {return color(d.key);})
Finally, "step into" 2nd-tier to add bars/rect:
d3.selectAll(".g-category").selectAll(".bars")
.data(function(d) {return d.values;})
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {return x(d.Month);})
.attr("y", function(d) {return y(d.Rate);})
.attr("width", 20)
.attr("height", function(d) {return height - y(d.Rate)})
.attr("fill", function(d) {return color(d.Category)})
This is a straightforward approach (to me at least), in that you take it one category at a time, using the grouped data to draw a line, then individual data points to draw the bars.
LAZY EDIT:
To get category bars side by side
Create ordinal scale mapping category to [1,nCategories]. Use this to dynamically offset bars with something like
translate( newScale(category)*barWidth )
To show either bars or lines (not both)
Create a function that selects bars/lines and transitions/toggles their visibility/opacity. Run when your drop-down input changes and with the drop-down input as input to the function.
The problem, I belive, is that you are binding the categories array to the bars selection, like this:
bars.selectAll("rect").data(categories)
As far as I can see (whithout a running demo) categories is an array with only four values (one for each category).
You have to go one step 'deeper' in your nested data structure.
To draw a set of bars for each category you would need to iterate over categories and bind the values array that contains the actual values to the selection.
Something like:
categories.each(function (category) {
var klass = category.name;
bars.selectAll("rect ." + klass)
.data(category.values)
.enter()
.append("rect")
.attr("class", klass)
.attr("width", barWidth)
.attr("x", function (d, i) { /* omitted */})
.attr("y", function (d) { return yScale(d.rate); })
.attr("height", function (d) { return h - yScale(d.rate); });
});
---- Edit
Instead of the above code, think about drawing the bars just like you do with the lines. Like this:
var bars = svg.selectAll(".barGroup")
.data(categories)
.enter()
.append("g")
.attr("class", function (d) { return lineClass(d.name) + "Bar barGroup"; })
.attr("transform", function (d, i) {
var x = i > 1 ? xScale.rangeBand() / 2 : 0;
return "translate(" + x + ",0)";
})
.selectAll('rect')
.data(function (d) { return d.values; })
.enter()
.append("rect")
.attr("class", "bar")
.attr("width", barWidth)
.attr("x", function (d, i) { return xScale(d.date); })
.attr("y", function (d, i) { return yScale(d.rate); })
.attr("height", function (d) { return h - yScale(d.rate); });
I'm trying to create a sankey chart using the d3 sankey plugin with dynamic shipping data. It works great most of the time except when I get a data set like this:
[{"DeparturePort":"CHARLESTON","ArrivalPort":"BREMERHAVEN","volume":5625.74},{"DeparturePort":"CHARLESTON","ArrivalPort":"ITAPOA","volume":2340},{"DeparturePort":"PT EVERGLADES","ArrivalPort":"PT AU PRINCE","volume":41.02},{"DeparturePort":"BREMERHAVEN","ArrivalPort":"CHARLESTON","volume":28}]
The key to my issue is the first and last entry in the data set. It seems that having opposite directions in the same sankey chart sends the javascript into an infinite loop and kills the browser. Any ideas on how to prevent this from happening?
Here's my chart code where raw would be the object above:
var data = raw;
var units = "Volume";
var margin = { top: 100, right: 0, bottom: 30, left: 0 },
width = $("#"+divID).width() - margin.left - margin.right,
height = divID == "enlargeChart" ? 800 - margin.top - margin.bottom : 600 - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"), // zero decimal places
format = function (d) { return ""; },
color = d3.scale.ordinal()
.range(["#0077c0", "#FF6600"]);
// append the svg canvas to the page
var svg = d3.select("#"+divID).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("font-size", "12px")
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Set the sankey diagram properties
var sankey = d3.sankey(width)
.nodeWidth(10)
.nodePadding(10)
.size([width, height]);
var path = sankey.link();
// load the data (using the timelyportfolio csv method)
//d3.csv("sankey.csv", function (error, data) {
//set up graph in same style as original example but empty
graph = { "nodes": [], "links": [] };
var checklist = [];
data.forEach(function (d) {
if ($.inArray(d.DeparturePort, checklist) == -1) {
checklist.push(d.DeparturePort)
graph.nodes.push({ "name": d.DeparturePort });
}
if ($.inArray(d.ArrivalPort, checklist) == -1) {
checklist.push(d.ArrivalPort)
graph.nodes.push({ "name": d.ArrivalPort });
}
graph.links.push({
"source": d.DeparturePort,
"target": d.ArrivalPort,
"value": +d.volume
});
});
// return only the distinct / unique nodes
graph.nodes = d3.keys(d3.nest()
.key(function (d) { return d.name; })
.map(graph.nodes));
// loop through each link replacing the text with its index from node
graph.links.forEach(function (d, i) {
graph.links[i].source = graph.nodes.indexOf(graph.links[i].source);
graph.links[i].target = graph.nodes.indexOf(graph.links[i].target);
});
//now loop through each nodes to make nodes an array of objects
// rather than an array of strings
graph.nodes.forEach(function (d, i) {
graph.nodes[i] = { "name": d };
});
sankey
.nodes(graph.nodes)
.links(graph.links)
.layout(32);
// add in the links
var link = svg.append("g").selectAll(".link")
.data(graph.links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("stroke-width", function (d) { return Math.max(1, d.dy); })
.sort(function (a, b) { setTimeout(function () { return b.dy - a.dy; }, 10);});
// add the link titles
link.append("title")
.text(function (d) {
return d.source.name + " → " +
d.target.name + "\n" + d.value.toFixed(0) + " TEU";
});
$("#" + divID + " .loading").addClass("hide");
// add in the nodes
var node = svg.append("g").selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) {
//setTimeout(function () {
return "translate(" + d.x + "," + d.y + ")";
//}, 10);
})
.call(d3.behavior.drag()
.origin(function (d) { return d; })
.on("dragstart", function () {
this.parentNode.appendChild(this);
})
.on("drag", dragmove));
// add the rectangles for the nodes
node.append("rect")
.attr("height", function (d) { if (d.dy < 0) { d.dy = (d.dy * -1); } return d.dy; })
.attr("width", sankey.nodeWidth())
.style("fill", function (d) {
return d.color = color(d.name);
})
.style("stroke", function (d) {
return d3.rgb(d.color);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
// add in the title for the nodes
node.append("text")
.attr("x", -6)
.attr("y", function (d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.style("stroke", function (d) { return "#000000" })
.attr("transform", null)
.text(function (d) { return d.name; })
.filter(function (d) { return d.x < width / 2; })
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
// the function for moving the nodes
function dragmove(d) {
d3.select(this).attr("transform",
"translate(" + d.x + "," + (
d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))
) + ")");
sankey.relayout();
link.attr("d", path);
}
}, 0)
It's an issue with the sankey.js script.
See this commit (on a fork of sankey.js) which fixed it:
https://github.com/soxofaan/d3-plugin-captain-sankey/commit/0edba18918aac3e9afadffd4a169c47f88a98f81
while (remainingNodes.length) {
becomes:
while (remainingNodes.length && x < nodes.length) {
That should prevent the endless loop.
I have a graph that correctly plots several lines based on a sensor's serial number. The problem is that I need it to transition to a new dataset and the data will overlap. Here's what I have so far......
var thresholdTemp = 72;
var minutes = 5;
var transInterval;
//Main function to create a graph plot
function plot(date1, date2, interval) {
var data;
//If we define a date search parameter then we don't want to have it load interactive
if (date1 == undefined && date2==undefined) {
data = loadMinutesJSON(minutes);
} else {
data = searchJSON(date1, date2, interval);
}
var margin = {top: 20, right: 80, bottom: 60, left: 50},
width = 960 - margin.left - margin.right ,
height = 500 - margin.top - margin.bottom;
//Re-order the data to be more usable by the rest of the script
var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse;
//Set the X domain to exist within the date and times given
var x = d3.time.scale().domain([getMinDate(data), getMaxDate(data)]).range([0, (width)]);
//Y Axis scale set to start at 45 degrees and go up to 30 degrees over highest temp
var y = d3.scale.linear()
.domain([
45,
getMaxTemp(data) + 10
])
.range([height, 0]);
//Set up the line colors based on serial number, this generates a color based on an ordinal value
var color = d3.scale.category20().domain(d3.keys(data).filter(function(key) { return key;}));
//.domain(d3.keys(data[0]).filter(function(key) { return key === 'serial';}));
//Define where the X axis is
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat(d3.time.format("%b %d %H:%M:%S"));
//Define where the Y axis is
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
//When called creates a line with the given datapoints
var line = d3.svg.line()
.interpolate("basis")
.x(function(d) { return x(d.date);})
.y(function(d) { return y(d.reading); });
//An extra line to define a maximum temperature threshold
var threshold = d3.svg.line()
.x(function(d) { return x(d.date);})
.y(function(d) { return y(thresholdTemp); });
//Append the SVG to the HTML element
var svg = d3.select("#plot").append("svg")
.attr("width", (width + margin.left + margin.right))
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Define the clipping boundaries
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", (width + margin.left + margin.right))
.attr("height", height + margin.top + margin.bottom);
//Add the X axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform" , function (d) {return "rotate(-35)"});
//Add the Y axis and a label denoting it as temperature in F
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Temperature (F)");
//Create the lines based on serial number
var serial = svg.selectAll(".serial")
.data(data);
var serials = serial.enter().append("g")
.attr("class", "serial");
//Add the extra line for a threshold and align it with the current time
var threshElement = svg.selectAll(".thresh")
.data(data)
.enter().append("g")
.attr("class", ".thresh");
//Add the path to draw lines and clipping so they stay within bounds
var path = serial.append("path")
.attr("clip-path", "url(#clip)")
.attr("class", "line")
.attr("d", function (d) {return line(d.values);})
.style("stroke", function(d,i) {return color(i);});
//Custom path to add a line showing the temperature threshold
var threshpath = threshElement.append("path")
.attr("clip-path", "url(#clip)")
.attr("class", "line")
.attr("d", function(d) { return threshold(d.values);})
.style("stroke", "red");
//Add a label to the end of the threshold line denoting it as the threshold
threshElement.append("text")
.attr("transform", "translate(" + x(getMaxDate(data)) + "," + y(thresholdTemp) + ")")
.attr("x", 3)
.attr("dy", ".35em")
.text("Threshold");
serial.exit().remove();
//Add the legend
//plotLegend(data);
//Load in the new data and add it to the SVG
function transition() {
data = loadMinutesJSON(minutes);
x.domain([getMinDate(data), getMaxDate(data)]);
y.domain([45, getMaxTemp(data) + 10]);
d3.select(".x.axis")
.transition()
.duration(500).call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform" , function (d) {return "rotate(-35)"});
d3.select(".y.axis").transition().duration(500).call(yAxis);
serial.data(data).enter().append("g").attr("class", "serial");
d3.selectAll("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values);})
.style("stroke", function(d,i) { return color(i);});
}
if(date1 == undefined && date2 == undefined) {
//Set the transition loop to run every 30 seconds
transInterval = setInterval(transition,30000);
}
}
//Set the new time in minutes and re-draw the graph
function setMinutes(newminutes) {
if (transInterval) {
clearInterval(transInterval);
}
d3.selectAll("svg").remove();
console.log("Setting new minutes " + newminutes);
minutes=newminutes;
plot();
}
//Search the database for data between 2 dates and create a new plot of it
function searchDB(date1, date2, intervalInMinutes) {
if (transInterval) {
clearInterval(transInterval);
}
d3.selectAll("svg").remove();
plot(date1, date2, intervalInMinutes);
console.log("Processing Search ");
}
//Quick function to determine the maximum date in a dataset
function getMaxDate(data) {
var arr = [];
for (x in data) {
arr.push(d3.max(data[x].values, function(d) { return d.date;}));
}
return d3.max(arr);
//return d3.max(data[0].values, function(d) { return d.date;});
}
//Calculate the minimum data
function getMinDate(data) {
var arr = [];
for (x in data) {
arr.push(d3.min(data[x].values, function(d) { return d.date;}));
}
return d3.min(arr);
//return d3.min(data[0].values, function(d) { return d.date;});
}
//Calculate the upper maximum temperature
function getMaxTemp(data) {
var arr = [];
for (x in data) {
arr.push(d3.max(data[x].values, function(d) { return d.reading;}));
}
return d3.max(arr);
//return d3.max(data, function(d) { return d3.max(data.values, function(d) {return d.reading})});
}
Here's examples of the data:
First
[{"serial":"2D0008017075F210","values":[{"date":"2013-08-23T20:43:46.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:44:16.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:44:46.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:16.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:47.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:17.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:47.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:17.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:47.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:48:17.000Z","reading":76.1,"elevation":null,"room":null,"system":null}]},{"serial":"1D00080170496D10","values":[{"date":"2013-08-23T20:43:46.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:44:16.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:44:46.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:16.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:47.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:17.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:47.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:17.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:47.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:48:17.000Z","reading":73.4,"elevation":null,"room":null,"system":null}]},{"serial":"380008017037ED10","values":[{"date":"2013-08-23T20:43:46.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:44:16.000Z","reading":75.2,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:44:46.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:16.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:47.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:17.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:47.000Z","reading":75.2,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:17.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:47.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:48:17.000Z","reading":74.3,"elevation":null,"room":null,"system":null}]}]
And then transitioning to:
[{"serial":"2D0008017075F210","values":[{"date":"2013-08-23T20:44:46.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:16.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:47.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:17.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:47.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:17.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:47.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:48:17.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:48:47.000Z","reading":76.1,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:49:17.000Z","reading":76.1,"elevation":null,"room":null,"system":null}]},{"serial":"1D00080170496D10","values":[{"date":"2013-08-23T20:44:46.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:16.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:47.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:17.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:47.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:17.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:47.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:48:17.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:48:47.000Z","reading":73.4,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:49:17.000Z","reading":73.4,"elevation":null,"room":null,"system":null}]},{"serial":"380008017037ED10","values":[{"date":"2013-08-23T20:44:46.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:16.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:45:47.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:17.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:46:47.000Z","reading":75.2,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:17.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:47:47.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:48:17.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:48:47.000Z","reading":74.3,"elevation":null,"room":null,"system":null},{"date":"2013-08-23T20:49:17.000Z","reading":74.3,"elevation":null,"room":null,"system":null}]}]
UPDATE:
I've modified the code quite a bit, but the jsfiddle is here http://jsfiddle.net/8cguF/1/
I'm not sure why the fiddle doesn't work, I tested it on my webserver and it works fine. But what I'm trying to do is construct a graph with the data1 variable in there and then transition it to the data2 variable.