|
"use strict"; |
|
|
|
// date helpers |
|
var startDate = new Date(2000, 0, 1) |
|
var interval = 1 |
|
|
|
// since data comes in without date information -> need start date and intervals |
|
var getDateByWeeksSinceStart = function(months) { |
|
var dat = new Date(startDate) |
|
dat.setMonth(dat.getMonth() + interval * months) |
|
return dat |
|
} |
|
|
|
// general parameters |
|
var margin = { |
|
top: 100, |
|
right: 50, |
|
bottom: 400, |
|
left: 100 |
|
}, |
|
mainChartWidth = 1000, |
|
width = 1400 - margin.left - margin.right, |
|
height = 1200 - margin.top - margin.bottom; |
|
|
|
var chartHeight = 150; |
|
var chartSep = 30; |
|
|
|
// dataset information |
|
var dataSetHelpers = { |
|
'guns': { |
|
'title': 'Monthly Gun Sales, United States', |
|
'startDate': new Date(2000, 0, 1), |
|
} |
|
} |
|
|
|
// chart specific paramenters |
|
var params = { |
|
'original': { |
|
yChartOffset: 0, |
|
name: 'Original Timeseries', |
|
color: 'black' |
|
}, |
|
'trend': { |
|
yChartOffset: 200, |
|
name: 'Trend', |
|
color: '#103392' |
|
}, |
|
'seasonal': { |
|
yChartOffset: 400, |
|
name: 'Seasonality', |
|
color: '#0f3431' |
|
}, |
|
'remainder': { |
|
yChartOffset: 600, |
|
name: 'Remainder (variation from seasonal/trend patterns)', |
|
color: '#793131' |
|
} |
|
} |
|
|
|
// formatting helpers |
|
var formatDate = d3.time.format("%d-%b-%y"); |
|
var monthFormat = function(month) { |
|
var monthLookup = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] |
|
return monthLookup[month] |
|
} |
|
|
|
var x = d3.time.scale() |
|
.range([0, mainChartWidth]); |
|
|
|
var getXAxis = function(scale) { |
|
return d3.svg.axis() |
|
.scale(scale) |
|
.orient("bottom"); |
|
} |
|
|
|
var getYAxis = function(scale) { |
|
return d3.svg.axis() |
|
.scale(scale) |
|
.orient("left") |
|
.tickFormat(d3.format(".2s")); |
|
} |
|
|
|
var getLine = function(className, yScale) { |
|
return d3.svg.line().x(function(d) { |
|
return x(d.date) |
|
}).y(function(d) { |
|
return yScale(d[className]) |
|
}) |
|
} |
|
|
|
var getAnnualLine = function(className, xScale, yScale) { |
|
return d3.svg.line().x(function(d) { |
|
return xScale(d.date.getMonth()) |
|
}).y(function(d) { |
|
return yScale(d[className]) |
|
}) |
|
} |
|
|
|
// svg setup |
|
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("stloutput.csv", function(error, data) { |
|
if (error) throw error; |
|
|
|
// manage data |
|
data.forEach(function(data, i) { |
|
data.remainder = +data.remainder; |
|
data.seasonal = +data.seasonal; |
|
data.trend = +data.trend |
|
data.original = data.remainder + data.seasonal + data.trend; |
|
data.date = getDateByWeeksSinceStart(i) |
|
}) |
|
|
|
// nest data by year for annual charts |
|
var annualNested = d3.nest() |
|
.key(function(d) { |
|
return d.date.getFullYear() |
|
}) |
|
.entries(data) |
|
|
|
// date min to date max |
|
x.domain(d3.extent(data, function(d) { |
|
return d.date; |
|
})); |
|
|
|
// 0 to max for y: note - assume min value of 0 for time series |
|
var domainExtent = [0, d3.max(data, function(d) { |
|
return d.original; |
|
})]; |
|
|
|
// shift it so that 0 is in the middle, for seasonal & remainder |
|
var midDomainExtent = [-domainExtent[1] / 2, domainExtent[1] / 2] |
|
|
|
var drawChart = function(svg, className) { |
|
var chartParams = params[className]; |
|
// everything |
|
var chart = svg.append("g").attr("class", className) |
|
|
|
var minValue = d3.min(data, function(d) { |
|
return d[className] |
|
}) |
|
|
|
var yDomain = minValue > 0 ? domainExtent : midDomainExtent |
|
|
|
var yFunction = d3.scale.linear() |
|
.range([chartHeight + chartParams.yChartOffset, chartParams.yChartOffset]) |
|
.domain(yDomain); |
|
|
|
chart.append("g").attr("class", "x axis") |
|
.attr("transform", "translate(0," + yFunction(0) + ")") |
|
.call(getXAxis(x)); |
|
|
|
chart.append("g").attr("class", "y axis") |
|
.call(getYAxis(yFunction)) |
|
.append("text") |
|
.attr("transform", "translate(" + 4 + "," + yFunction.range()[1] + ")") |
|
.attr("dy", ".71em") |
|
.style("text-anchor", "start") |
|
.text(params[className].name); |
|
|
|
chart.append("g").attr("class", "fullPaths") |
|
.append("path") |
|
.datum(data) |
|
.attr("class", "line") |
|
.attr("stroke", params[className].color) |
|
.attr("d", getLine(className, yFunction)) |
|
} |
|
|
|
var drawRemainder = function(svg, className) { |
|
var chartParams = params[className]; |
|
// everything |
|
var chart = svg.append("g").attr("class", className) |
|
|
|
var minValue = d3.min(data, function(d) { |
|
return d[className] |
|
}) |
|
|
|
var yDomain = minValue > 0 ? domainExtent : midDomainExtent |
|
|
|
var yFunction = d3.scale.linear() |
|
.range([chartHeight + chartParams.yChartOffset, chartParams.yChartOffset]) |
|
.domain(yDomain); |
|
|
|
chart.append("g").attr("class", "fullPaths") |
|
.selectAll(".remainderBars") |
|
.data(data) |
|
.enter() |
|
.append("line") |
|
.attr("stroke", params[className].color) |
|
.attr({ |
|
"x1": function(d) { |
|
return x(d.date); |
|
}, |
|
"x2": function(d) { |
|
return x(d.date); |
|
}, |
|
"y1": function(d) { |
|
return yFunction(0); |
|
}, |
|
"y2": function(d) { |
|
return yFunction(d[className]); |
|
}, |
|
"stroke-width": 3, |
|
"opacity": .5 |
|
}) |
|
} |
|
|
|
|
|
var drawOverlayChart = function(svg, className) { |
|
var chartParams = params[className]; |
|
// everything |
|
var chart = svg.append("g").attr("class", className) |
|
|
|
var minValue = d3.min(data, function(d) { |
|
return d[className] |
|
}) |
|
|
|
var oneYearWidth = (x(startDate.getTime() + 24 * 60 * 60 * 1000 * 365) - x(startDate)) * 2; |
|
console.log(oneYearWidth) |
|
var xOneYear = d3.scale.linear().domain([0, 11]).range([mainChartWidth + chartSep, mainChartWidth + chartSep + oneYearWidth]) |
|
var colorByYear = d3.scale.linear() |
|
.domain([x.domain()[0].getFullYear(), x.domain()[1].getFullYear()]) |
|
.range([-5, 0]) |
|
|
|
var yDomain = minValue > 0 ? domainExtent : midDomainExtent |
|
|
|
var yFunction = d3.scale.linear() |
|
.range([chartHeight + chartParams.yChartOffset, chartParams.yChartOffset]) |
|
.domain(yDomain); |
|
|
|
console.log(yDomain) |
|
|
|
chart.append("g").attr("class", "x axis") |
|
.attr("transform", "translate(0," + yFunction(0) + ")") |
|
.call(getXAxis(xOneYear).tickFormat(monthFormat).tickValues([0, 3, 6, 9])); |
|
|
|
|
|
chart.append("g").attr("class", "y axis") |
|
.attr("transform", "translate(" + (mainChartWidth + chartSep) + ",0)") |
|
.call(getYAxis(yFunction).tickFormat("")) |
|
|
|
|
|
chart.append("g").attr("class", "data") |
|
.selectAll(".annualPaths") |
|
.data(annualNested) |
|
.enter() |
|
.append("path") |
|
.datum(function(d) { |
|
return d.values |
|
}) |
|
.attr("stroke", function(d) { |
|
return d3.hcl(params[className].color).darker(colorByYear(d[0].date.getFullYear())) |
|
}) |
|
.attr("class", "annualPaths") |
|
.attr("d", getAnnualLine(className, xOneYear, yFunction)) |
|
} |
|
|
|
// draw them! |
|
drawChart(svg, 'original') |
|
drawChart(svg, 'trend') |
|
drawChart(svg, 'seasonal') |
|
drawChart(svg, 'remainder') |
|
drawRemainder(svg, 'remainder') |
|
|
|
drawOverlayChart(svg, 'original') |
|
drawOverlayChart(svg, 'trend') |
|
drawOverlayChart(svg, 'seasonal') |
|
drawOverlayChart(svg, 'remainder') |
|
|
|
}); |