var impexp = {}; impexp.dataManager = function module() { var exports = {}, // Custom events: dispatch = d3.dispatch('dataReady', 'dataLoading'), data; exports.loadCsvData = function(_file, _cleaningFunc) { var loadCsv = d3.csv(_file); loadCsv.on('progress', function() { dispatch.dataLoading(d3.event.loaded); }); loadCsv.get(function(_error, _response) { // Apply the cleaning function supplied in the _cleaningFunc parameter. _response.forEach(function(d) { _cleaningFunc(d); }); // Assign cleaned response to data. data = _response; dispatch.dataReady(_response); }); }; exports.getCleanedData = function() { return data; }; d3.rebind(exports, dispatch, 'on'); return exports; }; /** * For combining our sets of import and export data into one data structure. * var combiner = impexp.dataCombiner(); * var data = combiner.combine(imports_data, exports_data); */ impexp.dataCombiner = function module() { var exports = {}; /** * Both imports_data and exports_data are like: * [ * {1999: '15', 2000: '25', 2001: '30', 'Country': 'France'} * {1999: '16', 2000: '18', 2001: '22', 'Country': 'UK'} * ] * and it returns this: * { * 'France': [ * { * year: 1999, * imports: 15, * exports: 33 * }... * ], * 'UK': [ * ... * ] * } */ exports.combine = function(imports_data, exports_data) { // The basis for what we'll return: combined_data = keyByCountryWithArrays('imports', imports_data); // So we can access its data more easily: exports_by_country = keyByCountry(exports_data); // Add the export data to the transformed import data: d3.keys(combined_data).forEach(function(country) { if (country in exports_by_country) { combined_data[country].forEach(function(year_data, n) { var year = year_data['year']; if (year in exports_by_country[country]) { combined_data[country][n]['exports'] = exports_by_country[country][year]; }; }); }; }); return combined_data; }; /** * `kind` is one of 'imports' or 'exports'. * `rows` is an array or objects. * * Changes from: * [ * {1999: '15', 2000: '25', 2001: '30', 'Country': 'France'} * {1999: '16', 2000: '18', 2001: '22', 'Country': 'UK'} * ] * * to (if `kind` is 'imports'): * { * 'France': [ * {'year': 1999, 'imports': 15}, * {'year': 2000, 'imports': 25}, ... * ], * 'UK': [ ... * } */ keyByCountryWithArrays = function(kind, rows) { var countries = {}; rows.forEach(function(row) { var years = []; // k will be either a year or 'Country': d3.keys(row).forEach(function(k) { if (k !== 'Country') { var year_data = {year: new Date(+k, 0, 1)}; // Make missing data null, ensure everything else is a number. year_data[kind] = row[k] === '' ? null : +row[k]; // year_data will be like {'year': DateObj, 'imports': 15} years.push(year_data); }; }); countries[row['Country']] = years; }); return countries; }; /** * Takes this: * [ * {1999: '15', 2000: '25', 2001: '30', 'Country': 'France'} * {1999: '16', 2000: '18', 2001: '22', 'Country': 'UK'} * ] * and returns this: * { * 'France': {1999: 15, 2000: 25, 2001: 30}, * 'UK': {1999: 16, 2000: 18, 2001: 22} * } */ keyByCountry = function(rows) { var countries = {}; rows.forEach(function(row) { // Get the country name and remove from the row's data. var country = row['Country']; delete row['Country']; // Make sure all years and values are numeric: var year_data = {}; d3.keys(row).forEach(function(k) { // Make a Date object for 1st Jan for the corresponding year. var year = new Date(+k, 0, 1); // Make missing data null, ensure everything else is a number. year_data[year] = row[k] === '' ? null : +row[k]; }); countries[country] = year_data; }) return countries; }; return exports; }; impexp.chart = function module() { var margin = {top: 20, right: 20, bottom: 30, left: 50}, width = 400, height = 300, inner_width = width - margin.left - margin.right, inner_height = height - margin.top - margin.bottom, xValue = function(d) { return d[0]; }, yValue = function(d) { return d[1]; }, xScale = d3.time.scale(), yScale = d3.scale.linear(), xAxis = d3.svg.axis().scale(xScale).orient('bottom') .tickFormat(d3.time.format('%Y')) .ticks(d3.time.years, 5), yAxis = d3.svg.axis().scale(yScale).orient('left') .tickFormat(function(d){ return d3.format(',')(d / 1000000000); }), // defined() ensures we only draw the lines where there is data. imports_line = d3.svg.line().x(X).y(YImports) .defined(function(d){ return d.imports !== null; }), exports_line = d3.svg.line().x(X).y(YExports) .defined(function(d){ return d.exports !== null; }), // defined() ensures we draw the area only when both lines have data. area = d3.svg.area().x(X).y1(YImports) .defined(function(d){ return d.imports !== null && d.exports !== null; }); var dispatch = d3.dispatch('customHover'); function exports(_selection) { _selection.each(function(data) { inner_width = width - margin.left - margin.right; inner_height = height - margin.top - margin.bottom; // Update scales. // Use min/max of the years from all countries we're displaying. xScale.domain([ d3.min(data, function(country){ return d3.min(country.values, function(v) { return v.year; }) }), d3.max(data, function(country){ return d3.max(country.values, function(v) { return v.year; }) }) ]).range([0, inner_width]); // Use maximum value of all imports or exports from all countries. yScale.domain([ 0, d3.max(data, function(country){ return d3.max(country.values, function(v) { return Math.max(v.imports, v.exports); }) }), ]).range([inner_height, 0]); // Select svg element if it exists. var svg = d3.select(this) .selectAll('svg') .data([data]); // Or create skeletal chart, with no data applied. var gEnter = svg.enter().append('svg').append('g'); //gEnter.append('clipPath') //.attr('id', 'clip-surplus') //.append('path') //.attr('class', 'clip surplus'); //gEnter.append('clipPath') //.attr('id', 'clip-deficit') //.append('path') //.attr('class', 'clip deficit'); //gEnter.append('path') //.attr('class', 'area surplus') //.attr('clip-path', 'url("#clip-surplus")'); //gEnter.append('path') //.attr('class', 'area deficit') //.attr('clip-path', 'url("#clip-deficit")'); gEnter.append('path').attr('class', 'line imports'); gEnter.append('path').attr('class', 'line exports'); gEnter.append('g').attr('class', 'x axis'); gEnter.append('g').attr('class', 'y axis'); // Update outer dimensions. svg.transition() .attr({ width: width, height: height }); // Update inner dimensions. var g = svg.select('g') .attr('transform', 'translate(' + margin.left + ',' + margin.right + ')'); // Update lines/areas. //g.select('.clip.surplus').attr('d', area.y0(0)); //g.select('.clip.deficit').attr('d', area.y0(inner_height)); //g.select('.area.surplus').attr('d', area.y0( //function(d) { return yScale(d['exports']); })); //g.select('.area.deficit').attr('d', area); // Hello - HERE IS WHERE IT'S CURRENTLY FAILING. // The d[0].values is wrong, but it does at least draw the first pair of lines. // Each element of array d is like: // {name: 'United Kingdom', values: [{imports: 3, exports: 4, year: 1960}, {}, {}...]} g.select('.line.imports').attr('d', function(d){ return imports_line(d[0].values); }); g.select('.line.exports').attr('d', function(d){ return exports_line(d[0].values); }); // Update axes. g.select('.x.axis') .attr('transform', 'translate(0,' + yScale.range()[0] + ')') .call(xAxis); g.select('.y.axis') .call(yAxis); }); }; // The x-accessor for the path generator; xScale ∘ xValue. function X(d) { return xScale(d.year); } // The x-accessor for the path generator; yScale ∘ yValue. function YImports(d) { return yScale(d.imports); } function YExports(d) { return yScale(d.exports); } exports.margin = function(_) { if (!arguments.length) return margin; margin = _; return this; }; exports.width = function(_) { if (!arguments.length) return width; width = _; return this; }; exports.height = function(_) { if (!arguments.length) return height; height = _; return this; }; exports.x = function(_) { if (!arguments.length) return xValue; xValue = _; return chart; }; exports.y = function(_) { if (!arguments.length) return yValue; yValue = _; return chart; }; d3.rebind(exports, dispatch, "on"); return exports; }; impexp.controller = function module() { var exports = {}, chart, data, container, default_country = 'United Kingdom', importsDataManager = impexp.dataManager(), exportsDataManager = impexp.dataManager(); /** * Call this to start everything going. */ exports.init = function() { // Could be used to clean the data, but we don't need to. var csvCleaner = function(d){}; // Load both files. // After this you could do importsDataManager.getCleanedData() to see // what was loaded. importsDataManager.loadCsvData('imports.csv', csvCleaner); exportsDataManager.loadCsvData('exports.csv', csvCleaner); // Once the data has loaded, each manager will send 'dataReady' events. // So, once both have happened, we want to draw the chart: var loaded = 0; importsDataManager.on('dataReady', function() { loaded++; if (loaded == 2) { draw_chart(); }; }); exportsDataManager.on('dataReady', function() { loaded++; if (loaded == 2) { draw_chart(); }; }); }; /** * Called once all the CSV data has been loaded. */ var draw_chart = function() { $('#wait').hide(); var combiner = impexp.dataCombiner(); data = combiner.combine(importsDataManager.getCleanedData(), exportsDataManager.getCleanedData()); init_form(); chart = impexp.chart() .width(800).height(400) .margin({top: 50, right: 50, bottom: 50, left: 50}); container = d3.select('#container') .datum([{ name: 'United Kingdom', values: data['United Kingdom'] }, { name: 'United States', values: data['United States'] }]) .call(chart); }; var init_form = function() { // Add all the countries we have data for to the select field. d3.keys(data).forEach(function(country) { var option = $('