Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: fix: chart periodically jumping one pixel back and forth #133

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 14 additions & 3 deletions examples/example1.html
Expand Up @@ -11,15 +11,26 @@
}, 500);

function createTimeline() {
var chart = new SmoothieChart();
var chart = new SmoothieChart({
// millisPerPixel: 1000 / (3 * 75 + 1),
// millisPerPixel: 1000 / 80,
// millisPerPixel: 1000 / 75.5,
// millisPerPixel: 1000 / 75,
// millisPerPixel: 1000 / 74.6,
// millisPerPixel: 1000 / 72,
millisPerPixel: 1000 / (74.6 / 2),
// millisPerPixel: 1000 / 1,
maxValue: 10000,
minValue: 0,
});
chart.addTimeSeries(random, { strokeStyle: 'rgba(0, 255, 0, 1)', fillStyle: 'rgba(0, 255, 0, 0.2)', lineWidth: 4 });
chart.streamTo(document.getElementById("chart"), 500);
chart.streamTo(document.getElementById("chart"), 0);
}
</script>
</head>
<body onload="createTimeline()" style="background-color:#333333">

<canvas id="chart" width="100" height="100"></canvas>
<canvas id="chart" width="400" height="100"></canvas>

</body>
</html>
85 changes: 67 additions & 18 deletions smoothie.js
Expand Up @@ -95,6 +95,7 @@
* Add title option, by @mesca
* Fix data drop stoppage by rejecting NaNs in append(), by @timdrysdale
* Allow setting interpolation per time series, by @WofWca (#123)
* Fix chart constantly jumping in 1-2 pixel steps, by @WofWca (#131)
*/

;(function(exports) {
Expand Down Expand Up @@ -340,6 +341,8 @@
this.currentVisMinValue = 0;
this.lastRenderTimeMillis = 0;
this.lastChartTimestamp = 0;
// this.lastFrameWindowStart = Date.now();
this.lastFrameWindowStart = 0;

this.mousemove = this.mousemove.bind(this);
this.mouseout = this.mouseout.bind(this);
Expand Down Expand Up @@ -784,31 +787,77 @@
if (this.options.limitFPS > 0 && nowMillis - this.lastRenderTimeMillis < (1000/this.options.limitFPS))
return;

if (!this.isAnimatingScale) {
// We're not animating. We can use the last render time and the scroll speed to work out whether
// we actually need to paint anything yet. If not, we can return immediately.

// Render at least every 1/6th of a second. The canvas may be resized, which there is
// no reliable way to detect.
var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel);
time = time || nowMillis - (this.delay || 0);

if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) {
return;
// time -= time % this.options.millisPerPixel;

// if (!this.isAnimatingScale) {
// // We're not animating. We can use the last render time and the scroll speed to work out whether
// // we actually need to paint anything yet. If not, we can return immediately.
// var sameTime = this.lastChartTimestamp === time;
// if (sameTime) {
// // Render at least every 1/6th of a second. The canvas may be resized, which there is
// // no reliable way to detect.
// var needToRenderInCaseCanvasResized = nowMillis - this.lastRenderTimeMillis > 1000/6;
// if (!needToRenderInCaseCanvasResized) {
// return;
// }
// }
// }

// We increment time in pixel steps, so pixel interpolation on the image is always the same,
// so lines do not appear to wobble as they move along the canvas.
const period = this.options.millisPerPixel;
// This is like `lastFrameWindowEnd = this.lastFrameWindowStart + period`, but
// with truncation / floating point error mitigation.
const lastFrameWindowEnd = Math.round(this.lastFrameWindowStart / period + 1) * period;
let nextFrameWindowStart = lastFrameWindowEnd;
console.log((time - nextFrameWindowStart) / period, time - nextFrameWindowStart);
if (time >= nextFrameWindowStart) {
// The amount of frame timing accuracy (frame temporal placement) (how much off a frame can be from the time it ideally belongs to) we allow to sacrifice in order to keep chart movement speed smooth
// const maxFrameTimeOffsetMillis = 1000 / 50; // one 50 FPS frame should be an alright margin.
const maxFrameTimeOffsetMillis = 20;
// const maxFrameTimeOffsetMillis = 100;
const minNextFrameTime = time - maxFrameTimeOffsetMillis;
const minNextFrameWindowStart = minNextFrameTime - minNextFrameTime % period;

if (nextFrameWindowStart < minNextFrameWindowStart) {
// Have to skip some intermediate frames.
console.log('Skipping n frames', (nextFrameWindowStart - minNextFrameWindowStart) / period, nextFrameWindowStart - minNextFrameWindowStart)
nextFrameWindowStart = minNextFrameWindowStart;
}
// nextFrameWindowStart = Math.max(nextFrameWindowStart, minNextFrameWindowStart);
} else {
if (time < this.lastFrameWindowStart) {
// This can only happen if the value of the `time` argument is smaller than the value
// passed to it on the previous call. Which currenly only happens when the user calls it manually.
nextFrameWindowStart = time - time % period;
} else {
if (!this.isAnimatingScale) {
// Don't have to redraw anything.
console.log('same frame, until next frame', time - this.lastFrameWindowStart)
return;
}
}
}

this.resize();
this.lastFrameWindowStart = nextFrameWindowStart;
time = nextFrameWindowStart;

// // Debug assertions.
// const num = (time / this.options.millisPerPixel);
// if (Math.abs(num - Math.round(num)) > 0.0001) {
// console.error('Time not rounded', num - Math.round(num), time % this.options.millisPerPixel, time, this.options.millisPerPixel)
// }
if (time <= this.lastChartTimestamp) {
console.warn('same or smaller time', time, this.lastChartTimestamp)
}

this.lastRenderTimeMillis = nowMillis;

canvas = canvas || this.canvas;
time = time || nowMillis - (this.delay || 0);

// Round time down to pixel granularity, so motion appears smoother.
time -= time % this.options.millisPerPixel;

this.lastChartTimestamp = time;

this.resize();

canvas = canvas || this.canvas;
var context = canvas.getContext('2d'),
chartOptions = this.options,
dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight },
Expand Down