MCTChart preserves precision of plot values

This commit is contained in:
Pete Richards
2015-08-12 13:59:32 -07:00
parent b40494ac95
commit bb8c8a75ab
2 changed files with 75 additions and 24 deletions

View File

@ -11,10 +11,10 @@ define(
function PlotController($scope) { function PlotController($scope) {
var plotHistory = []; var plotHistory = [];
var isLive = true; var isLive = true;
var maxDomain = 0; var maxDomain = +new Date();
var domainOffset = +new Date();
var subscriptions = []; var subscriptions = [];
var setToDefaultViewport = function() { var setToDefaultViewport = function() {
// TODO: We shouldn't set the viewport until we have received data or something has given us a reasonable viewport.
$scope.viewport = { $scope.viewport = {
topLeft: { topLeft: {
domain: maxDomain - DOMAIN_INTERVAL, domain: maxDomain - DOMAIN_INTERVAL,
@ -35,7 +35,7 @@ define(
}; };
$scope.displayableDomain = function(domainValue) { $scope.displayableDomain = function(domainValue) {
// TODO: Call format function provided by domain object. // TODO: Call format function provided by domain object.
return new Date(domainValue + domainOffset).toUTCString(); return new Date(domainValue).toUTCString();
}; };
$scope.series = []; $scope.series = [];
@ -43,10 +43,7 @@ define(
$scope.rectangles = []; $scope.rectangles = [];
var updateSeriesFromTelemetry = function(series, seriesIndex, telemetry) { var updateSeriesFromTelemetry = function(series, seriesIndex, telemetry) {
if (typeof domainOffset === 'undefined') { var domainValue = telemetry.getDomainValue(telemetry.getPointCount() - 1);
domainOffset = telemetry.getDomainValue(telemetry.getPointCount() - 1);
}
var domainValue = telemetry.getDomainValue(telemetry.getPointCount() - 1) - domainOffset;
var rangeValue = telemetry.getRangeValue(telemetry.getPointCount() - 1); var rangeValue = telemetry.getRangeValue(telemetry.getPointCount() - 1);
// Track the biggest domain we've seen for sticky-ness. // Track the biggest domain we've seen for sticky-ness.
maxDomain = Math.max(maxDomain, domainValue); maxDomain = Math.max(maxDomain, domainValue);

View File

@ -10,6 +10,26 @@ define(
var TEMPLATE = "<canvas style='position: absolute; background: none; width: 100%; height: 100%;'></canvas>"; var TEMPLATE = "<canvas style='position: absolute; background: none; width: 100%; height: 100%;'></canvas>";
/**
* Offsetter adjusts domain and range values by a fixed amount,
* generally increasing the precision of the 32 bit float representation
* required for plotting.
*
* @constructor
*/
function Offsetter(domainOffset, rangeOffset) {
this.domainOffset = domainOffset;
this.rangeOffset = rangeOffset;
}
Offsetter.prototype.domain = function(dataDomain) {
return dataDomain - this.domainOffset;
};
Offsetter.prototype.range = function(dataRange) {
return dataRange - this.rangeOffset;
};
/** /**
* MCTChart draws charts utilizing a drawAPI. * MCTChart draws charts utilizing a drawAPI.
* *
@ -22,7 +42,8 @@ define(
isDestroyed = false, isDestroyed = false,
activeInterval, activeInterval,
drawAPI, drawAPI,
lines = []; lines = [],
offset;
drawAPI = DrawLoader.getDrawAPI(canvas); drawAPI = DrawLoader.getDrawAPI(canvas);
@ -34,15 +55,35 @@ define(
if (isDestroyed) { if (isDestroyed) {
return; return;
} }
requestAnimationFrame(redraw); requestAnimationFrame(redraw);
canvas.width = canvas.offsetWidth; canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight; canvas.height = canvas.offsetHeight;
drawAPI.clear(); drawAPI.clear();
createOffset();
if (!offset) {
return;
}
updateViewport(); updateViewport();
drawSeries(); drawSeries();
drawRectangles(); drawRectangles();
} }
function createOffset() {
if (offset) {
return;
}
if (!$scope.viewport ||
!$scope.viewport.topLeft ||
!$scope.viewport.bottomRight) {
return;
}
offset = new Offsetter(
$scope.viewport.topLeft.domain,
$scope.viewport.topLeft.range
);
}
function drawIfResized() { function drawIfResized() {
if (canvas.width !== canvas.offsetWidth || if (canvas.width !== canvas.offsetWidth ||
canvas.height !== canvas.offsetHeight) { canvas.height !== canvas.offsetHeight) {
@ -59,6 +100,9 @@ define(
function drawSeries() { function drawSeries() {
// TODO: Don't regenerate lines on each frame. // TODO: Don't regenerate lines on each frame.
if (!$scope.series || !$scope.series.length) {
return;
}
lines = $scope.series.map(lineFromSeries); lines = $scope.series.map(lineFromSeries);
lines.forEach(function(line) { lines.forEach(function(line) {
drawAPI.drawLine( drawAPI.drawLine(
@ -73,8 +117,14 @@ define(
if ($scope.rectangles) { if ($scope.rectangles) {
$scope.rectangles.forEach(function(rect) { $scope.rectangles.forEach(function(rect) {
drawAPI.drawSquare( drawAPI.drawSquare(
[rect.start.domain, rect.start.range], [
[rect.end.domain, rect.end.range], offset.domain(rect.start.domain),
offset.range(rect.start.range)
],
[
offset.domain(rect.end.domain),
offset.range(rect.end.range)
],
rect.color rect.color
); );
}); });
@ -83,13 +133,23 @@ define(
function updateViewport() { function updateViewport() {
var dimensions = [ var dimensions = [
Math.abs($scope.viewport.topLeft.domain - $scope.viewport.bottomRight.domain), Math.abs(
Math.abs($scope.viewport.topLeft.range - $scope.viewport.bottomRight.range) offset.domain($scope.viewport.topLeft.domain) -
offset.domain($scope.viewport.bottomRight.domain)
),
Math.abs(
offset.range($scope.viewport.topLeft.range) -
offset.range($scope.viewport.bottomRight.range)
)
]; ];
var origin = [ var origin = [
$scope.viewport.topLeft.domain, offset.domain(
$scope.viewport.bottomRight.range $scope.viewport.topLeft.domain
),
offset.range(
$scope.viewport.bottomRight.range
)
]; ];
drawAPI.setDimensions( drawAPI.setDimensions(
@ -113,8 +173,8 @@ define(
// appears minimal. // appears minimal.
var lineBuffer = new Float32Array(20000); var lineBuffer = new Float32Array(20000);
for (var i = 0; i < series.data.length; i++) { for (var i = 0; i < series.data.length; i++) {
lineBuffer[2*i] = series.data[i].domain; lineBuffer[2*i] = offset.domain(series.data[i].domain);
lineBuffer[2*i+1] = series.data[i].range; lineBuffer[2*i+1] = offset.range(series.data[i].range);
} }
return { return {
color: series.color, color: series.color,
@ -123,15 +183,11 @@ define(
}; };
} }
function initializeLines() {
lines = $scope.series.map(lineFromSeries);
}
function onSeriesDataAdd(event, seriesIndex, points) { function onSeriesDataAdd(event, seriesIndex, points) {
var line = lines[seriesIndex]; var line = lines[seriesIndex];
points.forEach(function (point) { points.forEach(function (point) {
line.buffer[2*line.pointCount] = point.domain; line.buffer[2*line.pointCount] = offset.domain(point.domain);
line.buffer[2*line.pointCount+1] = point.range; line.buffer[2*line.pointCount+1] = offset.range(point.range);
line.pointCount += 1; line.pointCount += 1;
}); });
} }
@ -139,8 +195,6 @@ define(
// Check for resize, on a timer // Check for resize, on a timer
activeInterval = $interval(drawIfResized, 1000); activeInterval = $interval(drawIfResized, 1000);
// Initialize series
$scope.$watch('series', initializeLines);
$scope.$on('series:data:add', onSeriesDataAdd); $scope.$on('series:data:add', onSeriesDataAdd);
redraw(); redraw();