Sine Time

An example p5.js sketch that pulls data from a remote endpoint.

The initial data load may take a few seconds. Press Toggle Smoothing to interpolate the graph over time.

Yellow indicates relative measured latency to the server. Red and blue are sin() & cos() of the current Unix timestamp divided by 1000, so they should produce a regular pattern.

Source Code for this Sketch

let dataz
let startTime
let smoothing = false
let httpFunc = 0

class Data {
	constructor(timeLimit) {
		this.timeLimit = timeLimit ? timeLimit : 30
		this.newest = 0
		this.initial = 0
		this.items = []
		this.maxLatency = 0
	}

	#calculateMaximums() {
		for(let i = 0; i < this.items.length; i++) {
			this.newest = max(this.newest, this.items[i].time)
			this.maxLatency = max(this.maxLatency, this.items[i].latency)
		}
	}

	append(value) {
		this.items.push(value)

		if(this.initial === 0) {
			// Track our initial value from the API plus our current time so we
			// can calculate an offset between client and server timestamps.
			//
			// It's possible for this value to be inaccurate because of abnormal
			// latency, but that's an edge case we're ignoring for now.
			this.initial = value.time
			startTime = Date.now()
		}

		// Because of network delay, values may be appended out of order. Don't
		// assume the last value added has the latest timestamp.
		this.#calculateMaximums()
	}

	/**
	 * prune removes messages older than timeLimit (in seconds) to prevent
	 * unbounded growth (running out of memory).
	 */
	prune() {
		// Server timezone may not match local timezone, so use the newest time
		// from the data instead of our clock.
		this.items = this.items.filter((x) => {
			return this.newest-x.time <= this.timeLimit
		})

		// Recalculate since we've removed data and the previous max may no
		// longer be in the dataset
		this.#calculateMaximums()
	}
}

/**
 * RequestMonitor is used for timing HTTP requests and displaying success or
 * error messages.
 */
class RequestMonitor {
	static debug = false

	constructor(requestType) {
		this.requestType = requestType;
		this.start = performance.now()
	}

	latency() {
		return performance.now()-this.start
	}

	success(...args) {
		if(RequestMonitor.debug){
			console.debug(`${this.requestType} succeeded in ${this.latency()}ms`, ...args)
		}
	}

	failed(...args) {
		if(RequestMonitor.debug) {
			console.error(`${this.requestType} failed in ${this.latency()}ms`, ...args)
		}
	}
}

/**
 * fetchData makes an HTTP call using the browser's native window.fetch() API.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
 */
function fetchData() {
	let rmon = new RequestMonitor("window.fetch")
	fetch("/sinetime.php").then(function(res) {
		if (res.ok) {
			res.json().then(data => {
				rmon.success(data)
				data.latency = rmon.latency()
				dataz.append(data)
			}).catch(function(error) {
				// invalid body
				rmon.failed(error)
			})
		} else {
			// http error
			rmon.failed(res.status, res.statusText, res.text())
		}
	}).catch(function(err) {
		// network error
		rmon.failed(err)
	})
}

/**
 * requestData makes an HTTP call using the browser's native, old-school
 * window.XMLHttpRequest.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
 */
function requestData() {
	let rmon = new RequestMonitor("window.XMLHttpRequest")
	let request = new XMLHttpRequest()
	request.timeout = 2000
	request.overrideMimeType("application/json")
	request.onload = function() {
		if (request.status >= 200 && request.status < 400) {
			try{
				let data = JSON.parse(request.response)
				rmon.success(data)
				data.latency = rmon.latency()
				dataz.append(data)
			} catch(error) {
				// invalid body
				rmon.failed(error)
			}
		} else {
			// http error
			rmon.failed(request.status, request.statusText, request.responseText)
		}
	}

	request.onerror = function() {
		// network error
		rmon.failed(request.error)
	}

	request.open('GET', "/sinetime.php", true)
	request.send()
}

/**
 * httpgetCallback makes an HTTP call using p5.js's httpGet function. The
 * response is handled using callbacks, rather than promises. This looks very
 * similar to the promise approach (below).
 *
 * @see https://p5js.org/reference/p5/httpGet/
 */
function httpgetCallback() {
	let rmon = new RequestMonitor("p5.prototype.httpGet:callback")
	httpGet("/sinetime.php", "json", false, (data) => {
		rmon.success(data)
		data.latency = rmon.latency()
		dataz.append(data)
	}, (res) => {
		rmon.failed(res.status, res.statusText, res.text())
	})
}

/**
 * httpgetPromise makes an HTTP call using p5.js's httpGet function. The
 * response is handled using promises. This looks very similar to the callback
 * approach (above).
 *
 * @see https://p5js.org/reference/p5/httpGet/
 */
function httpgetPromise() {
	let rmon = new RequestMonitor("p5.prototype.httpGet:promise")
	httpGet("/sinetime.php", "json").then(function(data) {
		rmon.success(data)
		data.latency = rmon.latency()
		dataz.append(data)
	}).catch(function(err) {
		rmon.failed(err)
	})
}

function normalizeY(input) {
	// input is expected to be from -1 to 1, and the graph should
	// be drawn in the middle 50% of the canvas
	return input*height/4+height/2
}

/**
 * normalizeX calculates the x position of a particular item, based on its
 * timestamp. The behavior of this function changes depending on the global
 * value of smoothing.
 *
 * Each second falls into a 10-pixel-wide "bucket". When smoothing is false,
 * x is calculated based on the current timestamp's value relative to the most
 * recent timestamp value (updated every second). When smoothing is true, x is
 * calculated based on the current timestamp's value to the current browser
 * time, adjusted by an offset (updated every frame).
 *
 * Thus, smoothing gives the _appearance_ of a continuous flow of data, even
 * though new data is only added every second. In both cases, older timestamps
 * are pushed to the left.
 *
 * @param input datum from dataz
 * @returns {number}
 */
function normalizeX(input) {
	if(smoothing) {
		// fake "realtime" animation for the graph by moving smoothly
		let offset = (Date.now()/1000-input.time)-(startTime/1000-dataz.initial)
		// Shift 10 pixels to the right so data roll in smoothly from off-screen
		return width-(offset*10)+10
	} else {
		return width-(dataz.newest-input.time)*10
	}
}

function normalizeLatency(input) {
	return height-10-(input/dataz.maxLatency)*(height-20)
}

function setup() {
	chrisDefaults()
	createCanvas(800, 100, P2D, canvas)
	frameRate(12)

	strokeWeight(3)

	// We're using 10 pixels for each second on our graph, so
	// we should limit our dataset to
	dataz = new Data(width/10)

	// Initiate DOS. I mean... fetch some data.
	window.setInterval(() => {
		// Cycle through the various ways we can fetch data via HTTP calls, to
		// demonstrate that they all work.
		switch(httpFunc) {
			case 0:
				fetchData()
				break
			case 1:
				requestData()
				break
			case 2:
				httpgetCallback()
				break
			case 3:
				httpgetPromise()
				break
		}

		httpFunc = ++httpFunc % 4
		dataz.prune()
	}, 1000)
}

function draw() {
	background(10)

	for(let i = 0; i < dataz.items.length; i++) {
		strokeWeight(4)
		stroke(0, 100, 50)
		point(normalizeX(dataz.items[i]), normalizeY(dataz.items[i].sine))

		stroke(240, 100, 50)
		point(normalizeX(dataz.items[i]), normalizeY(dataz.items[i].cosine))

		strokeWeight(1)
		stroke(40, 100, 50)
		line(normalizeX(dataz.items[i]), normalizeLatency(dataz.items[i].latency), normalizeX(dataz.items[i])-10, normalizeLatency(dataz.items[i].latency))
	}
}

document.addEventListener("DOMContentLoaded", (e) => {
	let p5ui = document.getElementById("p5ui")
	p5ui.innerHTML = `
	<div>
		<button id="toggleSmoothing">Toggle Smoothing</button>
	</div>
	`

	let toggleSmoothingButton = document.getElementById("toggleSmoothing")

	toggleSmoothingButton.addEventListener("click", (e) => {
		smoothing = !smoothing
	})
})

You may use all or part of the p5 code seen on this page for your own creations, without restriction. Attribution is appreciated but not required.

This sketch also includes the following sources: p5common.js . Sources are available under their respective licenses.

See all p5.js sketches