Skip to content

Instantly share code, notes, and snippets.

@vdepoorter
Last active July 5, 2017 07:30
Show Gist options
  • Save vdepoorter/e13d50c995c29bc15511e12a8061a6ae to your computer and use it in GitHub Desktop.
Save vdepoorter/e13d50c995c29bc15511e12a8061a6ae to your computer and use it in GitHub Desktop.
Reusable Line Chart v2
license: mit

An implementation of a reusable responsive multiline chart. Based on the concept outlined in Mike Bostocks blog post Towards Reusable Charts.

Features:

  • Reusable modular design
  • Responsive design, chart size adjusts with screen size
  • Customizable options
    • Chart Size
    • Margins
    • Div Selector
    • Chart Colors
    • Axis labels
  • Toggleable series (click on the legend to toggle series)

Previous version: Reusable Responsive Multiline Chart

forked from asielen's block: Reusable Line Chart v2

forked from anonymous's block: Reusable Line Chart v2

forked from vdepoorter's block: Reusable Line Chart v2

forked from vdepoorter's block: Reusable Line Chart v2

forked from vdepoorter's block: Reusable Line Chart v2

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" type="text/css" href="multiline.css">
<script src="http://d3js.org/d3.v3.js" charset="utf-8"></script>
<!--<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>-->
</head>
<body>
<div class="chart-wrapper" id="chart-line1"></div>
<script src="multiline.js" charset="utf-8"></script>
<script type="text/javascript">
function httpGetAsync(theUrl, callback)
{
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
callback(xmlHttp.responseText);
}
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.send(null);
}
//d3.csv = httpGetAsync("http://forecasts.proaanalytics.com/GetForecast/testkey1/2");
d3.csv("multiline_data.csv", function(error, data) {
console.log(data);
data.forEach(function (d) {
d.year = +d.year;
d.variableA = +d.variableA/1000;
d.variableB = +d.variableB/1000;
d.variableC = +d.variableC/1000;
d.variableD = +d.variableD/1000;
d.variableA_90 = +d.variableA*0.8;
d.variableA_10 = +d.variableA*1.1;
d.variableB_90 = +d.variableB*0.9;
d.variableB_10 = +d.variableB*1.1;
d.variableC_90 = +d.variableC*0.9;
d.variableC_10 = +d.variableC*1.1;
d.variableD_90 = +d.variableD*0.9;
d.variableD_10 = +d.variableD*1.1;
});
var chart = makeLineChart(data, 'year', {
'NSW': {best_guess: 'variableA', p90: 'variableA_90', p10: 'variableA_10'},
'SA': {best_guess: 'variableB', p90: 'variableB_90', p10: 'variableB_10'},
'VIC': {best_guess: 'variableC', p90: 'variableC_90', p10: 'variableC_10'},
'QLD': {best_guess: 'variableD', p90: 'variableD_90', p10: 'variableD_10'}
});
chart.bind({selector:"#chart-line1",chartSize:{height:400, width:700}, axisLabels: {xAxis:'Forecast horizon (h)', yAxis: 'Generation (kW)'}});
chart.render();
});
</script>
</body>
</html>
.chart-wrapper {
max-width: 650px;
min-width: 304px;
margin: 0 auto;
background-color: #FAF7F7;
}
.chart-wrapper .inner-wrapper {
position: relative;
padding-bottom: 50%;
width: 100%;
}
.chart-wrapper .outer-box {
position: absolute;
top: 0; bottom: 0; left: 0; right: 0;
}
.chart-wrapper .inner-box {
width: 100%;
height: 100%;
}
.chart-wrapper text {
font-family: sans-serif;
font-size: 11px;
}
.chart-wrapper p {
font-size: 16px;
margin-top:5px;
margin-bottom: 40px;
}
.chart-wrapper .axis path,
.chart-wrapper .axis line {
fill: none;
stroke: #1F1F2E;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
}
.chart-wrapper .axis path {
stroke-width: 2px;
}
.chart-wrapper .line {
fill: none;
stroke: steelblue;
stroke-width: 5px;
}
.chart-wrapper .legend {
min-width: 200px;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
font-size: 16px;
padding: 10px 40px;
}
.chart-wrapper .legend > div {
margin: 0px 25px 10px 0px;
flex-grow: 0;
cursor: pointer;
}
.chart-wrapper .legend .series-marker:hover {
opacity: 0.8;
}
.chart-wrapper .legend p {
display:inline;
font-size: 0.8em;
font-family: sans-serif;
font-weight: 600;
}
.chart-wrapper .legend .series-marker {
height: 1em;
width: 1em;
border-radius: 35%;
background-color: crimson;
display: inline-block;
margin-right: 4px;
margin-bottom: -0.16rem;
}
.chart-wrapper .overlay {
fill: none;
pointer-events: all;
}
.chart-wrapper .tooltip circle {
fill: black;
stroke: crimson;
stroke-width: 2px;
fill-opacity: 25%;
}
.chart-wrapper .tooltip rect {
fill: #ecf0f4;
opacity: 0.7;
border-radius: 2px;
}
.chart-wrapper .tooltip text {
font-size: 14px;
}
.chart-wrapper .tooltip .line {
stroke: steelblue;
stroke-dasharray: 2,5;
stroke-width: 2;
opacity: 0.5;
}
@media (max-width:500px){
.chart-wrapper .line {stroke-width: 3px;}
.chart-wrapper .legend {font-size: 14px;}
}
function makeLineChart(dataset, xName, yNames) {
/*
* dataset = the csv file
* xName = the name of the column to use as the x axes
* yNames = the columns to use for y values
*
* */
var chart = {};
chart.data = dataset;
chart.xName = xName;
chart.yNames = yNames;
chart.groupObjs = {}; //The data organized by grouping and sorted as well as any metadata for the groups
chart.objs = {mainDiv: null, chartDiv: null, g: null, xAxis: null, yAxis: null, tooltip:null, legend:null, text_box:null};
var colorFunct = d3.scale.category10();
function updateColorFunction(colorOptions) {
/*
* Takes either a list of colors, a function or an object with the mapping already in place
* */
if (typeof colorOptions == 'function') {
return colorOptions
} else if (Array.isArray(colorOptions)) {
// If an array is provided, map it to the domain
var colorMap = {}, cColor = 0;
for (var cName in chart.groupObjs) {
colorMap[cName] = colorOptions[cColor];
cColor = (cColor + 1) % colorOptions.length;
}
return function (group) {
return colorMap[group];
}
} else if (typeof colorOptions == 'object') {
// if an object is provided, assume it maps to the colors
return function (group) {
return colorOptions[group];
}
}
}
//Formatter functions for the axes
chart.formatAsNumber = d3.format(".0f");
chart.formatAsDecimal = d3.format(".2f");
chart.formatAsCurrency = d3.format("$.2f");
chart.formatAsFloat = function(d) {if(d%1!==0){return d3.format(".2f")(d);}else{return d3.format(".2f")(d);}};
chart.formatAsYear = d3.format("");
chart.xFormatter = chart.formatAsNumber;
chart.yFormatter = chart.formatAsFloat;
function getYFuncts() {
// Return a list of all *visible* y functions
var yFuncts = [];
for (var yName in chart.groupObjs) {
currentGroup = chart.groupObjs[yName];
if (currentGroup.visible == true) {
yFuncts.push(currentGroup.p10Funct);
}
}
return yFuncts
}
function getYMax () {
// Get the max y value of all *visible* y lines
return d3.max(getYFuncts().map(function(fn){
return d3.max(chart.data, fn);
}))
}
function prepareData() {
chart.fc_base_time = new Date();
chart.fc_time = function(h) {
var fc_time = new Date();
fc_time.setTime(chart.fc_base_time.getTime() + (h*60*60*1000));
return fc_time;
}
chart.xFunct = function(d){return d[xName]};
chart.bisectYear = d3.bisector(chart.xFunct).left;
var yName, cY;
for (yName in chart.yNames) {
chart.groupObjs[yName] = {yFunct:null, p90Funct:null, p10Funct:null, visible:null, objs:{}};
}
// For each yName argument, create a yFunction
function getYFn(column) {
return function (d) {
return d[column];
};
}
// Object instead of array
chart.yFuncts = [];
for (yName in chart.yNames) {
cY = chart.groupObjs[yName];
cY.visible = true;
cY.yFunct = getYFn(chart.yNames[yName].best_guess);
cY.p90Funct = getYFn(chart.yNames[yName].p90);
cY.p10Funct = getYFn(chart.yNames[yName].p10);
}
}
prepareData();
chart.update = function () {
chart.width = parseInt(chart.objs.chartDiv.style("width"), 10) - (chart.margin.left + chart.margin.right);
chart.height = parseInt(chart.objs.chartDiv.style("height"), 10) - (chart.margin.top + chart.margin.bottom);
/* Update the range of the scale with new width/height */
chart.xScale.range([0, chart.width]);
chart.yScale.range([chart.height, 0]).domain([0, getYMax()]);
if (!chart.objs.g) {return false;}
/* Else Update the axis with the new scale */
chart.objs.axes.g.select('.x.axis').attr("transform", "translate(0," + chart.height + ")").call(chart.objs.xAxis);
chart.objs.axes.g.select('.x.axis .label').attr("x", chart.width / 2);
chart.objs.axes.g.select('.y.axis').call(chart.objs.yAxis);
chart.objs.axes.g.select('.y.axis .label').attr("x", -chart.height / 2);
/* Force D3 to recalculate and update the line */
for (var yName in chart.groupObjs) {
cY = chart.groupObjs[yName];
if (cY.visible==true) {
cY.objs.line.g.attr("d", cY.objs.line.series).style("display",null);
cY.objs.area.g.attr("d", cY.objs.area.series).style("display",null);
cY.objs.tooltip.style("display",null);}
else {
cY.objs.line.g.style("display","none");
cY.objs.area.g.style("display","none");
cY.objs.tooltip.style("display","none");
}
}
chart.objs.tooltip.select('.line').attr("y2", chart.height);
chart.objs.chartDiv.select('svg').attr("width", chart.width + (chart.margin.left + chart.margin.right)).attr("height", chart.height + (chart.margin.top + chart.margin.bottom));
chart.objs.g.select(".overlay").attr("width", chart.width).attr("height", chart.height);
return chart;
};
chart.bind = function (bindOptions) {
function getOptions() {
if (!bindOptions) throw "Missing Bind Options";
if (bindOptions.selector) {
chart.objs.mainDiv = d3.select(bindOptions.selector);
// Capture the inner div for the chart (where the chart actually is)
chart.selector = bindOptions.selector + " .inner-box";
} else {throw "No Selector Provided"}
if (bindOptions.margin) {
chart.margin = margin;
} else {
chart.margin = {top: 15, right: 60, bottom: 50, left: 60};
}
if (bindOptions.chartSize) {
chart.divWidth = bindOptions.chartSize.width;
chart.divHeight = bindOptions.chartSize.height;
} else {
chart.divWidth = 800;
chart.divHeight = 400;
}
chart.width = chart.divWidth - chart.margin.left - chart.margin.right;
chart.height = chart.divHeight - chart.margin.top - chart.margin.bottom;
if (bindOptions.axisLabels) {
chart.xAxisLable = bindOptions.axisLabels.xAxis;
chart.yAxisLable = bindOptions.axisLabels.yAxis;
} else {
chart.xAxisLable = chart.xName;
chart.yAxisLable = chart.yNames[0];
}
if (bindOptions.colors) {
colorFunct = updateColorFunction(bindOptions.colors);
}
}
getOptions();
chart.xScale = d3.scale.linear().range([0, chart.width]).domain(d3.extent(chart.data, chart.xFunct));
chart.yScale = d3.scale.linear().range([chart.height, 0]).domain([0, getYMax()]);
//Create axis
chart.objs.xAxis = d3.svg.axis()
.scale(chart.xScale)
.orient("bottom")
.tickFormat(chart.xFormatter)
.tickSize(6)
.ticks(20); // number of tick labels
//.attr("font-size", "10px");
//."font-family"."("Verdana");
chart.objs.yAxis = d3.svg.axis()
.scale(chart.yScale)
.orient("left")
.tickFormat(chart.yFormatter);
// Build line building functions
function getYScaleFn(yName) {
return function (d) {
return chart.yScale(chart.groupObjs[yName].yFunct(d));
};
}
function getYScaleFn_p10(yName) {
return function (d) {
return chart.yScale(chart.groupObjs[yName].p10Funct(d));
};
}
function getYScaleFn_p90(yName) {
return function (d) {
return chart.yScale(chart.groupObjs[yName].p90Funct(d));
};
}
// Create lines (as series)
for (var yName in yNames) {
var cY = chart.groupObjs[yName];
cY.objs.line = {g:null, series:null};
cY.objs.area = {g:null, series:null};
cY.objs.line.series = d3.svg.line()
.interpolate("linear")
.x(function (d) {return chart.xScale(chart.xFunct(d));})
.y(getYScaleFn(yName));
cY.objs.area.series = d3.svg.area()
.x(function (d) {return chart.xScale(chart.xFunct(d));})
.y0(getYScaleFn_p10(yName))
.y1(getYScaleFn_p90(yName));
}
chart.objs.mainDiv.style("max-width", chart.divWidth + "px");
// Add all the divs to make it centered and responsive
chart.objs.mainDiv.append("div")
.attr("class", "inner-wrapper")
.style("padding-bottom", (chart.divHeight / chart.divWidth) * 100 + "%")
.append("div").attr("class", "outer-box")
.append("div").attr("class", "inner-box");
chart.objs.chartDiv = d3.select(chart.selector);
d3.select(window).on('resize.' + chart.selector, chart.update);
// Create the svg
chart.objs.g = chart.objs.chartDiv.append("svg")
.attr("class", "chart-area")
.attr("width", chart.width + (chart.margin.left + chart.margin.right))
.attr("height", chart.height + (chart.margin.top + chart.margin.bottom))
.append("g")
.attr("transform", "translate(" + chart.margin.left + "," + chart.margin.top + ")");
chart.objs.axes = {};
chart.objs.axes.g = chart.objs.g.append("g").attr("class", "axis");
// Show axis
chart.objs.axes.x = chart.objs.axes.g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + chart.height + ")")
.call(chart.objs.xAxis)
.append("text")
.attr("class", "label")
.attr("x", chart.width / 2)
.attr("y", 40)
.style("text-anchor", "middle")
.style("font-size","15px")
.style("font-family","Verdana")
.text(chart.xAxisLable);
chart.objs.axes.y = chart.objs.axes.g.append("g")
.attr("class", "y axis")
.call(chart.objs.yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", -50)
.attr("x", -chart.height / 2)
.attr("dy", ".71em")
.style("text-anchor", "middle")
.style("font-size","15px")
.style("font-family","Verdana")
.text(chart.yAxisLable);
return chart;
};
chart.render = function () {
var yName,
cY=null;
chart.objs.legend = chart.objs.mainDiv.append('div').attr("class", "legend");
// //chart.objs.mainDiv.append('div').attr("class","text")
// chart.objs.legend.append("class","text")
// //.attr("x", 30)
// //.attr("y", 470)
// .attr("text-anchor", "middle")
// .style("font-size", "15px")
// .style("font-family", "Verdana")
// //.style("text-decoration", "underline")
// .text(" \u00A9 Forecast base time: " + (chart.fc_base_time
// .toISOString()
// .slice(0, 19)
// .replace("T", " ")
// .replace("-", "/")
// .replace("-", "/"))
// + " AEST ")
function toggleSeries(yName) {
cY = chart.groupObjs[yName];
cY.visible = !cY.visible;
if (cY.visible==false) {cY.objs.legend.div.style("opacity","0.3")} else {cY.objs.legend.div.style("opacity","1")}
chart.update()
}
function getToggleFn(series) {
return function () {
return toggleSeries(series);
};
}
for (yName in chart.groupObjs) {
cY = chart.groupObjs[yName];
cY.objs.g = chart.objs.g.append("g");
cY.objs.line.g = cY.objs.g.append("path")
.datum(chart.data)
.attr("class", "line")
.attr("d", cY.objs.line.series)
.style("stroke", colorFunct(yName))
.attr("data-series", yName)
.on("mouseover", function () {
tooltip.style("display", null);
}).on("mouseout", function () {
tooltip.transition().delay(700).style("display", "none");
}).on("mousemove", mouseHover);
cY.objs.area.g = cY.objs.g.append("path")
.datum(chart.data)
.attr("class", "area")
.attr("d", cY.objs.area.series)
.attr("fill",colorFunct(yName))
.attr("fill-opacity","0.4")
//.style("stroke", colorFunct(yName))
// .attr("data-series", yName)
cY.objs.legend = {};
cY.objs.legend.div = chart.objs.legend.append('div').on("click",getToggleFn(yName));
cY.objs.legend.icon = cY.objs.legend.div.append('div')
.attr("class", "series-marker")
.style("background-color", colorFunct(yName));
cY.objs.legend.text = cY.objs.legend.div.append('p').text(yName);
}
//chart.objs.mainDiv.append('div').attr("class","text")
chart.objs.legend.append("class","text")
//.attr("x", 30)
//.attr("y", 470)
.attr("text-anchor", "middle")
.style("font-size", "15px")
.style("font-family", "Verdana")
//.style("text-decoration", "underline")
.text(" Forecast base time: " + (chart.fc_base_time
.toISOString()
.slice(0, 19)
.replace("T", " ")
.replace("-", "/")
.replace("-", "/"))
+ " AEST \u00A9 Proa Analytics")
chart.objs.legend.append("class","text")
//.attr("x", 30)
//.attr("y", 470)
.attr("text-anchor", "middle")
.style("font-size", "15px")
.style("font-family", "Verdana")
//.style("text-decoration", "underline")
.text(" ")
//Draw tooltips
//Themust be a better way so we don't need a second loop. Issue is draw order so tool tips are on top
chart.objs.tooltip = chart.objs.g.append("g").attr("class", "tooltip").style("display", "none");
// Year label
chart.objs.tooltip.append("text").attr("class", "year").attr("x", 9).attr("y", 7);
// Focus line
chart.objs.tooltip.append("line").attr("class", "line").attr("y1", 0).attr("y2", chart.height);
for (yName in chart.groupObjs) {
cY = chart.groupObjs[yName];
//Add tooltip elements
var tooltip = chart.objs.tooltip.append("g");
cY.objs.circle = tooltip.append("circle").attr("r", 3);
cY.objs.rect = tooltip.append("rect").attr("x", 8).attr("y","-5").attr("width",22).attr("height",'0.75em');
cY.objs.text = tooltip.append("text").attr("x", 9).attr("dy", ".35em").attr("class","value");
cY.objs.tooltip = tooltip;
}
// Overlay to capture hover
chart.objs.g.append("rect")
.attr("class", "overlay")
.attr("width", chart.width)
.attr("height", chart.height)
.on("mouseover", function () {
chart.objs.tooltip.style("display", null);
}).on("mouseout", function () {
chart.objs.tooltip.style("display", "none");
}).on("mousemove", mouseHover);
return chart;
function mouseHover() {
var x0 = chart.xScale.invert(d3.mouse(this)[0]), i = chart.bisectYear(dataset, x0, 1), d0 = chart.data[i - 1], d1 = chart.data[i];
try {
var d = x0 - chart.xFunct(d0) > chart.xFunct(d1) - x0 ? d1 : d0;
} catch (e) { return;}
var minY = chart.height;
var yName, cY;
for (yName in chart.groupObjs) {
cY = chart.groupObjs[yName];
if (cY.visible==false) {continue}
//Move the tooltip
cY.objs.tooltip.attr("transform", "translate(" + chart.xScale(chart.xFunct(d)) + "," + chart.yScale(cY.yFunct(d)) + ")");
//Change the text
cY.objs.tooltip.select("text").text(chart.yFormatter(cY.yFunct(d)));
minY = Math.min(minY, chart.yScale(cY.yFunct(d)));
}
chart.objs.tooltip.select(".tooltip .line").attr("transform", "translate(" + chart.xScale(chart.xFunct(d)) + ")").attr("y1", minY);
chart.objs.tooltip.select(".tooltip .year")
.text("Forecast time: "
+ chart.fc_time(chart.xFunct(d))
.toISOString()
.slice(0, 19)
.replace("T", " ")
.replace("-", "/")
.replace("-", "/")
+ " AEST " ).style("font-size", "10px")
.style("font-family", "Verdana");
}
};
return chart;
}
year variableA variableB variableC variableD
0.166666667 1342 1207.8 1476.2 2688
0.333333333 1593 1433.7 1752.3 2708
0.5 1675 1507.5 1842.5 2751
0.666666667 1701 1530.9 1871.1 2807
0.833333333 1743 1568.7 1917.3 2862
1 1769 1592.1 1945.9 2902
1.166666667 1820 1638 2002 2919
1.333333333 1744 1569.6 1918.4 2930
1.5 1773 1595.7 1950.3 2938
1.666666667 1787 1608.3 1965.7 2940
1.833333333 1792 1612.8 1971.2 2938
2 1748 1573.2 1922.8 2921
2.166666667 1720 1548 1892 2888
2.333333333 1640 1476 1804 2849
2.5 1592 1432.8 1751.2 2805
2.666666667 1526 1373.4 1678.6 2750
2.833333333 1403 1262.7 1543.3 2695
3 1493 1343.7 1642.3 2606
3.166666667 1338 1204.2 1471.8 2525
3.333333333 1242 1117.8 1366.2 2438
3.5 1166 1049.4 1282.6 2344
3.666666667 1039 935.1 1142.9 2245
3.833333333 938 844.2 1031.8 2146
4 769 692.1 845.9 2044
4.166666667 657 591.3 722.7 1925
4.333333333 506 455.4 556.6 1816
4.5 415 373.5 456.5 1705
4.666666667 345 310.5 379.5 1593
4.833333333 306 275.4 336.6 1479
5 191 171.9 210.1 1371
5.166666667 111 99.9 122.1 1269
5.333333333 75 67.5 82.5 1165
5.5 90 81 99 1059
5.666666667 0 0 0 953
5.833333333 0 0 0 815
6 0 0 0 704
6.166666667 0 0 0 526
6.333333333 0 0 0 428
6.5 0 0 0 335
6.666666667 0 0 0 159
6.833333333 0 0 0 107
7 0 0 0 48
7.166666667 0 0 0 52
7.333333333 0 0 0 45
7.5 0 0 0 0
7.666666667 0 0 0 0
7.833333333 0 0 0 0
8 0 0 0 0
8.166666667 0 0 0 0
8.333333333 0 0 0 0
8.5 0 0 0 0
8.666666667 0 0 0 0
8.833333333 0 0 0 0
9 0 0 0 0
9.166666667 0 0 0 0
9.333333333 0 0 0 0
9.5 0 0 0 0
9.666666667 0 0 0 0
9.833333333 0 0 0 0
10 0 0 0 0
10.16666667 0 0 0 0
10.33333333 0 0 0 0
10.5 0 0 0 0
10.66666667 0 0 0 0
10.83333333 0 0 0 0
11 0 0 0 0
11.16666667 0 0 0 0
11.33333333 0 0 0 0
11.5 0 0 0 0
11.66666667 0 0 0 0
11.83333333 0 0 0 0
12 0 0 0 0
12.16666667 0 0 0 0
12.33333333 0 0 0 0
12.5 0 0 0 0
12.66666667 0 0 0 0
12.83333333 0 0 0 0
13 0 0 0 0
13.16666667 0 0 0 0
13.33333333 0 0 0 0
13.5 0 0 0 0
13.66666667 0 0 0 0
13.83333333 0 0 0 0
14 0 0 0 0
14.16666667 0 0 0 0
14.33333333 0 0 0 0
14.5 0 0 0 0
14.66666667 0 0 0 0
14.83333333 0 0 0 0
15 0 0 0 0
15.16666667 0 0 0 0
15.33333333 0 0 0 0
15.5 0 0 0 0
15.66666667 0 0 0 0
15.83333333 0 0 0 0
16 0 0 0 0
16.16666667 0 0 0 0
16.33333333 0 0 0 0
16.5 0 0 0 0
16.66666667 0 0 0 0
16.83333333 0 0 0 0
17 0 0 0 0
17.16666667 0 0 0 0
17.33333333 0 0 0 0
17.5 0 0 0 0
17.66666667 0 0 0 0
17.83333333 0 0 0 0
18 0 0 0 0
18.16666667 0 0 0 0
18.33333333 0 0 0 0
18.5 0 0 0 0
18.66666667 0 0 0 0
18.83333333 0 0 0 0
19 0 0 0 0
19.16666667 41 36.9 45.1 0
19.33333333 55 49.5 60.5 0
19.5 68 61.2 74.8 0
19.66666667 118 106.2 129.8 0
19.83333333 143 128.7 157.3 0
20 148 133.2 162.8 0
20.16666667 162 145.8 178.2 0
20.33333333 218 196.2 239.8 0
20.5 596 536.4 655.6 0
20.66666667 653 587.7 718.3 0
20.83333333 404 363.6 444.4 0
21 983 884.7 1081.3 0
21.16666667 1072 964.8 1179.2 31
21.33333333 1010 909 1111 73
21.5 1781 1602.9 1959.1 158
21.66666667 1896 1706.4 2085.6 274
21.83333333 2005 1804.5 2205.5 349
22 2109 1898.1 2319.9 535
22.16666667 2203 1982.7 2423.3 725
22.33333333 2285 2056.5 2513.5 960
22.5 2361 2124.9 2597.1 1232
22.66666667 2430 2187 2673 1448
22.83333333 2493 2243.7 2742.3 1666
23 2548 2293.2 2802.8 1836
23.16666667 2597 2337.3 2856.7 1981
23.33333333 2640 2376 2904 2137
23.5 2676 2408.4 2943.6 2274
23.66666667 2705 2434.5 2975.5 2409
23.83333333 2727 2454.3 2999.7 2537
24 2742 2467.8 3016.2 2637
24.16666667 2750 2475 3025 2709
24.33333333 2748 2473.2 3022.8 2774
24.5 2740 2466 3014 2836
24.66666667 2726 2453.4 2998.6 2888
24.83333333 2704 2433.6 2974.4 2935
25 2677 2409.3 2944.7 2974
25.16666667 2640 2376 2904 3005
25.33333333 2469 2222.1 2715.9 3030
25.5 2420 2178 2662 3049
25.66666667 2365 2128.5 2601.5 3062
25.83333333 2243 2018.7 2467.3 3069
26 2178 1960.2 2395.8 3068
26.16666667 2080 1872 2288 3058
26.33333333 1968 1771.2 2164.8 3042
26.5 1885 1696.5 2073.5 3020
26.66666667 1717 1545.3 1888.7 2992
26.83333333 1629 1466.1 1791.9 2958
27 1537 1383.3 1690.7 2916
27.16666667 1369 1232.1 1505.9 2865
27.33333333 1301 1170.9 1431.1 2813
27.5 1197 1077.3 1316.7 2751
27.66666667 1067 960.3 1173.7 2684
27.83333333 960 864 1056 2610
28 768 691.2 844.8 2525
28.16666667 654 588.6 719.4 2433
28.33333333 488 439.2 536.8 2336
28.5 391 351.9 430.1 2233
28.66666667 326 293.4 358.6 2126
28.83333333 237 213.3 260.7 2013
29 133 119.7 146.3 1889
29.16666667 72 64.8 79.2 1752
29.33333333 45 40.5 49.5 1612
29.5 59 53.1 64.9 1468
29.66666667 0 0 0 1324
29.83333333 0 0 0 1180
30 0 0 0 1011
30.16666667 0 0 0 722
30.33333333 0 0 0 565
30.5 0 0 0 423
30.66666667 0 0 0 117
30.83333333 0 0 0 83
31 0 0 0 44
31.16666667 0 0 0 51
31.33333333 0 0 0 46
31.5 0 0 0 0
31.66666667 0 0 0 0
31.83333333 0 0 0 0
32 0 0 0 0
32.16666667 0 0 0 0
32.33333333 0 0 0 0
32.5 0 0 0 0
32.66666667 0 0 0 0
32.83333333 0 0 0 0
33 0 0 0 0
33.16666667 0 0 0 0
33.33333333 0 0 0 0
33.5 0 0 0 0
33.66666667 0 0 0 0
33.83333333 0 0 0 0
34 0 0 0 0
34.16666667 0 0 0 0
34.33333333 0 0 0 0
34.5 0 0 0 0
34.66666667 0 0 0 0
34.83333333 0 0 0 0
35 0 0 0 0
35.16666667 0 0 0 0
35.33333333 0 0 0 0
35.5 0 0 0 0
35.66666667 0 0 0 0
35.83333333 0 0 0 0
36 0 0 0 0
36.16666667 0 0 0 0
36.33333333 0 0 0 0
36.5 0 0 0 0
36.66666667 0 0 0 0
36.83333333 0 0 0 0
37 0 0 0 0
37.16666667 0 0 0 0
37.33333333 0 0 0 0
37.5 0 0 0 0
37.66666667 0 0 0 0
37.83333333 0 0 0 0
38 0 0 0 0
38.16666667 0 0 0 0
38.33333333 0 0 0 0
38.5 0 0 0 0
38.66666667 0 0 0 0
38.83333333 0 0 0 0
39 0 0 0 0
39.16666667 0 0 0 0
39.33333333 0 0 0 0
39.5 0 0 0 0
39.66666667 0 0 0 0
39.83333333 0 0 0 0
40 0 0 0 0
40.16666667 0 0 0 0
40.33333333 0 0 0 0
40.5 0 0 0 0
40.66666667 0 0 0 0
40.83333333 0 0 0 0
41 0 0 0 0
41.16666667 0 0 0 0
41.33333333 0 0 0 0
41.5 0 0 0 0
41.66666667 0 0 0 0
41.83333333 0 0 0 0
42 0 0 0 0
42.16666667 0 0 0 0
42.33333333 0 0 0 0
42.5 0 0 0 0
42.66666667 0 0 0 0
42.83333333 0 0 0 0
43 0 0 0 0
43.16666667 38 34.2 41.8 0
43.33333333 50 45 55 0
43.5 66 59.4 72.6 0
43.66666667 112 100.8 123.2 0
43.83333333 136 122.4 149.6 0
44 135 121.5 148.5 0
44.16666667 146 131.4 160.6 0
44.33333333 166 149.4 182.6 0
44.5 565 508.5 621.5 0
44.66666667 642 577.8 706.2 0
44.83333333 385 346.5 423.5 0
45 975 877.5 1072.5 30
45.16666667 1065 958.5 1171.5 44
45.33333333 1001 900.9 1101.1 215
45.5 1793 1613.7 1972.3 362
45.66666667 1912 1720.8 2103.2 598
45.83333333 2025 1822.5 2227.5 592
46 2133 1919.7 2346.3 949
46.16666667 2229 2006.1 2451.9 1096
46.33333333 2314 2082.6 2545.4 1380
46.5 2392 2152.8 2631.2 1609
46.66666667 2463 2216.7 2709.3 1779
46.83333333 2527 2274.3 2779.7 1952
47 2585 2326.5 2843.5 2103
47.16666667 2636 2372.4 2899.6 2227
47.33333333 2682 2413.8 2950.2 2343
47.5 2720 2448 2992 2453
47.66666667 2751 2475.9 3026.1 2555
47.83333333 2775 2497.5 3052.5 2651
48 2793 2513.7 3072.3 2734
48.16666667 2802 2521.8 3082.2 2804
48.33333333 2800 2520 3080 2867
48.5 2792 2512.8 3071.2 2925
48.66666667 2777 2499.3 3054.7 2975
48.83333333 2755 2479.5 3030.5 3018
49 2727 2454.3 2999.7 3053
49.16666667 2557 2301.3 2812.7 3077
49.33333333 2515 2263.5 2766.5 3095
49.5 2467 2220.3 2713.7 3107
49.66666667 2412 2170.8 2653.2 3113
49.83333333 2288 2059.2 2516.8 3112
50 2223 2000.7 2445.3 3102
50.16666667 2124 1911.6 2336.4 3081
50.33333333 2011 1809.9 2212.1 3053
50.5 1930 1737 2123 3019
50.66666667 1758 1582.2 1933.8 2980
50.83333333 1670 1503 1837 2935
51 1497 1347.3 1646.7 2881
51.16666667 1403 1262.7 1543.3 2816
51.33333333 1340 1206 1474 2746
51.5 1233 1109.7 1356.3 2670
51.66666667 1099 989.1 1208.9 2590
51.83333333 989 890.1 1087.9 2505
52 786 707.4 864.6 2399
52.16666667 673 605.7 740.3 2258
52.33333333 504 453.6 554.4 2128
52.5 408 367.2 448.8 1993
52.66666667 322 289.8 354.2 1854
52.83333333 279 251.1 306.9 1723
53 163 146.7 179.3 1582
53.16666667 95 85.5 104.5 1395
53.33333333 65 58.5 71.5 1215
53.5 83 74.7 91.3 1043
53.66666667 0 0 0 826
53.83333333 0 0 0 687
54 0 0 0 559
54.16666667 0 0 0 425
54.33333333 0 0 0 328
54.5 0 0 0 242
54.66666667 0 0 0 144
54.83333333 0 0 0 93
55 0 0 0 49
55.16666667 0 0 0 29
55.33333333 0 0 0 26
55.5 0 0 0 0
55.66666667 0 0 0 0
55.83333333 0 0 0 0
56 0 0 0 0
56.16666667 0 0 0 0
56.33333333 0 0 0 0
56.5 0 0 0 0
56.66666667 0 0 0 0
56.83333333 0 0 0 0
57 0 0 0 0
57.16666667 0 0 0 0
57.33333333 0 0 0 0
57.5 0 0 0 0
57.66666667 0 0 0 0
57.83333333 0 0 0 0
58 0 0 0 0
58.16666667 0 0 0 0
58.33333333 0 0 0 0
58.5 0 0 0 0
58.66666667 0 0 0 0
58.83333333 0 0 0 0
59 0 0 0 0
59.16666667 0 0 0 0
59.33333333 0 0 0 0
59.5 0 0 0 0
59.66666667 0 0 0 0
59.83333333 0 0 0 0
60 0 0 0 0
60.16666667 0 0 0 0
60.33333333 0 0 0 0
60.5 0 0 0 0
60.66666667 0 0 0 0
60.83333333 0 0 0 0
61 0 0 0 0
61.16666667 0 0 0 0
61.33333333 0 0 0 0
61.5 0 0 0 0
61.66666667 0 0 0 0
61.83333333 0 0 0 0
62 0 0 0 0
62.16666667 0 0 0 0
62.33333333 0 0 0 0
62.5 0 0 0 0
62.66666667 0 0 0 0
62.83333333 0 0 0 0
63 0 0 0 0
63.16666667 0 0 0 0
63.33333333 0 0 0 0
63.5 0 0 0 0
63.66666667 0 0 0 0
63.83333333 0 0 0 0
64 0 0 0 0
64.16666667 0 0 0 0
64.33333333 0 0 0 0
64.5 0 0 0 0
64.66666667 0 0 0 0
64.83333333 0 0 0 0
65 0 0 0 0
65.16666667 0 0 0 0
65.33333333 0 0 0 0
65.5 0 0 0 0
65.66666667 0 0 0 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment