Skip to content

Instantly share code, notes, and snippets.

@walkerjeffd
Last active November 17, 2015 02:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save walkerjeffd/9a77fe4663f531c0d8cd to your computer and use it in GitHub Desktop.
Save walkerjeffd/9a77fe4663f531c0d8cd to your computer and use it in GitHub Desktop.
timeseries-uncertainty
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="timechart.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
svg { width: 100%; height: 100%; }
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
fill: steelblue;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
rect.pane {
cursor: move;
fill: none;
pointer-events: all;
}
</style>
</head>
<body>
<div id="controls">
<input id="slider1" type="range" min="0" max="100" value="50">
<input id="slider2" type="range" min="0" max="100" value="50">
</div>
<div id="viz"></div>
<script>
var startDate = new Date("2001-10-01"),
rHour = -0.0406079999999997,
rDay = 0.1;
var days = d3.range(0, 366, 1),
hours = d3.range(0, 24, 1);
var data = compute(+d3.select('#slider1').property('value')/100,
+d3.select('#slider2').property('value')/100);
d3.select('#slider1').on('input', render);
d3.select('#slider2').on('input', render);
function render () {
data = compute(+d3.select('#slider1').property('value')/100,
+d3.select('#slider2').property('value')/100);
console.log(data[0]);
chart.data([{
key: 'obs',
label: 'Obs',
color: 'forestgreen',
opacity: 0.5,
primary: true,
values: data
}]);
d3.select('#viz').call(chart);
}
var chart = timeChart()
.data([{
key: 'obs',
label: 'Obs',
color: 'forestgreen',
opacity: 0.5,
primary: true,
values: data
}]);
d3.select('#viz').call(chart);
function compute(rDay, rHour) {
return _.flatten(days.map(function(d) {
return hours.map(function (h) {
var datetime = d3.time.hour.offset(d3.time.day.offset(startDate, d), h),
value = Math.random()*rDay*Math.sin(2*Math.PI*(d)/365) +
Math.random()*Math.sin(2*Math.PI*(h/24*rHour));
return [datetime, value];
});
}));
}
function timeChart () {
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 450 - margin.top - margin.bottom,
x = d3.time.scale().range([0, width]),
y = d3.scale.linear().range([height, 0]),
xAxis = d3.svg.axis().scale(x).orient('bottom'),
yAxis = d3.svg.axis().scale(y).orient('left'),
xAccessor = function (d) { return d[0]; },
yAccessor = function (d) { return d[1]; },
yLabel = '',
bisectDate = d3.bisector(xAccessor).left,
numberFormat = d3.format('.1f'),
dateFormat = d3.time.format('%Y-%m-%d %H:%M'),
focus,
data,
showBand = true,
svg, g, container,
onZoom = function () {};
var area = d3.svg.area()
.x(function (d) { return x(xAccessor(d)); })
.y0(function (d) { return y(d.max); })
.y1(function (d) { return y(d.min); })
.interpolate('step-after');
var line = d3.svg.line()
.x(function (d) { return x(xAccessor(d)); })
.y(function (d) { return y(yAccessor(d)); })
.interpolate('step-after');
var zoom = d3.behavior.zoom()
.scaleExtent([1, 80])
.on('zoom', zoomed);
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(); }],
['%b %d', function (d) { return d.getDay() && d.getDate() != 1; }],
['%b %d', function (d) { return d.getDate() != 1; }],
['%b %Y', function (d) { return d.getMonth(); }],
['%Y', function () { return true; }]
]);
xAxis.tickFormat(customTimeFormat).ticks(5);
function chart (el) {
if (el.selectAll('svg').empty()) {
svg = el
.append('svg')
.attr('height', height + margin.top + margin.bottom)
.attr('width', width + margin.left + margin.right)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' +
margin.top + ')');
svg.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', height);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')');
svg.append('g')
.attr('class', 'y axis')
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end')
.text(yLabel);
container = svg.append('g')
.attr('class', 'data')
.attr('clip-path', 'url(#clip)');
container.append('g').attr('class', 'areas');
container.append('g').attr('class', 'lines');
container.append('g').attr('class', 'points');
container.append('g').attr('class', 'ref-lines');
focus = svg.append('g')
.attr('class', 'focus')
.style('display', 'none');
focus.append('circle')
.style('fill', 'none')
.style('stroke', 'steelblue')
.attr('r', 4.5);
focus.append('text')
.attr('x', 9)
.attr('dy', '.35em');
svg.append('rect')
.attr('width', width)
.attr('height', height)
.attr('class', 'pane')
.on('mouseover', function () { focus.style('display', null); })
.on('mouseout', function () { focus.style('display', 'none'); })
.on('mousemove', mousemove)
.call(zoom);
}
zoomed();
}
function zoomed() {
if (data) {
var xExtent = data.map(function (d) {
return d3.extent(d.values, xAccessor);
});
xExtent = d3.extent(_.flatten(xExtent));
// clamp x-axis zoom
if (x.domain()[0] < xExtent[0]) {
zoom.translate([zoom.translate()[0] - x(xExtent[0]) + x.range()[0],
zoom.translate()[1]]);
} else if (x.domain()[1] > xExtent[1]) {
zoom.translate([zoom.translate()[0] - x(xExtent[1]) + x.range()[1],
zoom.translate()[1]]);
}
var yExtent = data.map(function (d) {
if (d.showBand) {
var yMin = data.map(function (d) {
return d3.extent(d.values, function (d) { return d.min; });
});
var yMax = data.map(function (d) {
return d3.extent(d.values, function (d) { return d.max; });
});
return [d3.min(_.flatten(yMin)), d3.max(_.flatten(yMax))];
} else {
return d3.extent(d.values, yAccessor);
}
});
yExtent = d3.extent(_.flatten(yExtent));
y.domain(yExtent);
onZoom(x.domain());
svg.select('.x.axis')
.call(xAxis);
svg.select('.y.axis')
.call(yAxis);
var areas = container.select('g.areas')
.selectAll('.area')
.data(data.filter(function (d) { return d.showBand; }));
areas.enter().append('path')
.attr('class', 'area')
.attr('fill', 'gray')
.style('opacity', 0.25);
areas.attr('d', function (d, i) {
return area(d.values, i);
});
areas.exit().remove();
var gLines = container.select('g.lines')
.selectAll('.line')
.data(data);
gLines.enter().append('path')
.attr('class', 'line');
gLines.attr('d', function (d, i) {
return line(d.values, i);
})
.style('stroke', function (d) {
return d.color || 'orangered';
})
.style('opacity', function (d) {
return d.opacity || 0.5;
});
gLines.exit().remove();
var gPoints = container.select('g.points')
.selectAll('.point')
.data(data[0].values);
gPoints.enter().append('circle')
.attr('class', 'point');
gPoints.attr('cx', function (d) { return x(xAccessor(d)); })
.attr('cy', function (d) { return y(yAccessor(d)); })
.attr('r', function (d) { return Math.pow(Math.max(0, -10*yAccessor(d))+1, 2);})
.attr('opacity', function (d) { return (Math.random()+2)*y(yAccessor(d)); })
.attr('fill', 'deepskyblue');
gPoints.exit().remove();
}
}
function mousemove() {
var mouseDate = x.invert(d3.mouse(this)[0]);
// filter series
var filtered = data.filter(function (d) {
return mouseDate >= d.extent[0] && mouseDate <= d.extent[1];
});
if (filtered.length > 0) {
var series = filtered[0];
var i = bisectDate(series.values, mouseDate, 1);
var d;
if (i === 0) {
d = series.values[0];
} else {
var d0 = series.values[i - 1],
d1 = series.values[i];
// d = mouseDate - xAccessor(d0) > xAccessor(d1) - mouseDate ? d1 : d0;
d = d0;
}
focus.attr('transform',
'translate(' + x(mouseDate) + ',' +
y(yAccessor(d)) + ')');
var text = dateFormat(mouseDate) + ' | ' + numberFormat(yAccessor(d));
focus.style('display', null);
focus.select('text').text(text);
var textWidth = focus.node().getBBox().width,
mouseX = d3.mouse(this)[0],
xWidth = x.range()[1];
// flip/flop text side
if (textWidth + mouseX > xWidth) {
focus.select('text')
.attr('text-anchor', 'end')
.attr('x', -9);
} else {
focus.select('text')
.attr('text-anchor', 'start')
.attr('x', 9);
}
} else {
focus.style('display', 'none');
}
}
chart.width = function (_) {
if (!arguments.length) {
return width;
}
width = _ - margin.left - margin.right;
x.range([0, width]);
return chart;
};
chart.height = function (_) {
if (!arguments.length) {
return height;
}
height = _ - margin.top - margin.bottom;
y.range([height, 0]);
return chart;
};
chart.x = function (_) {
if (!arguments.length) {
return xAccessor;
}
xAccessor = _;
bisectDate = d3.bisector(xAccessor).left;
return chart;
};
chart.y = function (_) {
if (!arguments.length) {
return yAccessor;
}
yAccessor = _;
return chart;
};
chart.yLabel = function (_) {
if (!arguments.length) {
return yLabel;
}
yLabel = _;
return chart;
};
chart.data = function (__) {
if (!arguments.length) {
return data;
}
data = __;
var oldTranslate = zoom.translate(),
oldScale = zoom.scale();
data.forEach(function (d) {
d.extent = d3.extent(d.values, xAccessor);
});
x.domain(d3.extent(_.flatten(_.pluck(data, 'extent'))));
zoom
.x(x)
.translate(oldTranslate)
.scale(oldScale);
return chart;
};
chart.onZoom = function (_) {
if (!arguments.length) {
return onZoom;
}
onZoom = _;
return chart;
};
return chart;
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment