|
(function() { |
|
var margin = { |
|
top: 20, |
|
right: 20, |
|
bottom: 30, |
|
left: 50 |
|
}, |
|
width = 960 - margin.left - margin.right, |
|
height = 500 - margin.top - margin.bottom; |
|
|
|
var x = d3.time.scale() |
|
.range([0, width]); |
|
|
|
var y = d3.scale.linear() |
|
.range([height, 0]); |
|
|
|
var xAxis = d3.svg.axis() |
|
.scale(x) |
|
.orient("bottom"); |
|
|
|
var yAxis = d3.svg.axis() |
|
.scale(y) |
|
.orient("left"); |
|
|
|
var line = d3.svg.line() |
|
.x(function(d) { |
|
return x(d.ts); |
|
}) |
|
.y(function(d) { |
|
return y(d.data); |
|
}); |
|
|
|
var svg = d3.select("#timeseries") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
var data = getRandomData(1000); |
|
|
|
x.domain(d3.extent(data, function(d) { |
|
return d.ts; |
|
})); |
|
|
|
y.domain(d3.extent(data, function(d) { |
|
return d.data; |
|
})); |
|
|
|
svg.append("text") |
|
.attr("class", "log") |
|
.attr("dx", 12) |
|
.attr("dy", 12) |
|
.text("data:" + data.length + " downsampled:" + 0); |
|
|
|
svg.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(xAxis); |
|
|
|
svg.append("g") |
|
.attr("class", "y axis") |
|
.call(yAxis); |
|
|
|
svg.append("path") |
|
.datum(data) |
|
.attr("class", "line") |
|
.attr("d", line); |
|
|
|
resize(); |
|
|
|
function getRandomValue(min, max) { |
|
return Math.random() * (max - min) + min; |
|
} |
|
|
|
function getRandomData(count) { |
|
count = count || 0; |
|
for (var data = [], |
|
day = 24 * 3600 * 1000, |
|
date = new Date() * 1, |
|
lognormal = d3.random.logNormal(1, .5), |
|
i = 0; i < count; i++) { |
|
date = date + day; |
|
data.push({ |
|
"ts": date, |
|
"data": lognormal(), |
|
}); |
|
} |
|
|
|
return data; |
|
} |
|
|
|
|
|
function largestTriangleThreeBucket(data, threshold, xProperty, yProperty) { |
|
/** |
|
* This method is adapted from the |
|
* "Largest Triangle Three Bucket" algorithm by Sveinn Steinarsson |
|
* In his 2013 Masters Thesis - "Downsampling Time Series for Visual Representation" |
|
* http://skemman.is/handle/1946/15343 |
|
* |
|
* The MIT License |
|
* |
|
* Copyright (c) 2013 by Sveinn Steinarsson |
|
* |
|
* Permission is hereby granted, free of charge, to any person obtaining a copy |
|
* of this software and associated documentation files (the "Software"), to deal |
|
* in the Software without restriction, including without limitation the rights |
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
* copies of the Software, and to permit persons to whom the Software is |
|
* furnished to do so, subject to the following conditions: |
|
* The above copyright notice and this permission notice shall be included in |
|
* all copies or substantial portions of the Software. |
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
* THE SOFTWARE. |
|
* -------------------------------------------------------------------------------------------------------- |
|
*/ |
|
yProperty = yProperty || 0; |
|
xProperty = xProperty || 1; |
|
|
|
var m = Math.floor, |
|
y = Math.abs, |
|
f = data.length; |
|
|
|
if (threshold >= f || 0 === threshold) { |
|
return data; |
|
} |
|
|
|
var n = [], |
|
t = 0, |
|
p = (f - 2) / (threshold - 2), |
|
c = 0, |
|
v, |
|
u, |
|
w; |
|
|
|
n[t++] = data[c]; |
|
|
|
for (var e = 0; e < threshold - 2; e++) { |
|
for (var g = 0, |
|
h = 0, |
|
a = m((e + 1) * p) + 1, |
|
d = m((e + 2) * p) + 1, |
|
d = d < f ? d : f, |
|
k = d - a; a < d; a++) { |
|
g += +data[a][xProperty], h += +data[a][yProperty]; |
|
} |
|
|
|
for (var g = g / k, |
|
h = h / k, |
|
a = m((e + 0) * p) + 1, |
|
d = m((e + 1) * p) + 1, |
|
k = +data[c][xProperty], |
|
x = +data[c][yProperty], |
|
c = -1; a < d; a++) { |
|
"undefined" != typeof data[a] && |
|
(u = .5 * y((k - g) * (data[a][yProperty] - x) - (k - data[a][xProperty]) * (h - x)), |
|
u > c && (c = u, v = data[a], w = a)); |
|
} |
|
|
|
n[t++] = v; |
|
c = w; |
|
} |
|
|
|
n[t++] = data[f - 1]; |
|
|
|
return n; |
|
}; |
|
|
|
function resize() { |
|
// get the container dimensions |
|
var container = d3.select('#timeseries'); |
|
width = parseInt(container.style('width'), 10); |
|
width = width - margin.left - margin.right; |
|
height = parseInt(container.style('height'), 10); |
|
height = height - margin.top - margin.bottom; |
|
|
|
// set the new ranges and axes |
|
x.range([0, width]); |
|
y.range([height, 0]).nice(); |
|
|
|
// define this number of axis ticks relative to the pixel length of the axis |
|
xAxis.ticks(Math.max(width / 100, 2)); |
|
yAxis.ticks(Math.max(height / 50, 2)); |
|
|
|
d3.select(".x.axis") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(xAxis); |
|
|
|
d3.select(".y.axis") |
|
.call(yAxis); |
|
|
|
// calculate the downsample data using half the width as the threshold |
|
var downsampled = largestTriangleThreeBucket(data, width / 2, "ts", "data"); |
|
|
|
// redraw the path with the downsampled data |
|
d3.select("path.line") |
|
.datum(downsampled) |
|
.attr("d", line); |
|
|
|
// output the lengths |
|
d3.select(".log").text("data:" + data.length + " downsampled:" + downsampled.length); |
|
} |
|
|
|
d3.select(window).on('resize', resize); |
|
|
|
})(); |