Skip to content

Instantly share code, notes, and snippets.

@wrongu
Last active January 7, 2016 16:05
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 wrongu/064f3258fde3ff2e8cab to your computer and use it in GitHub Desktop.
Save wrongu/064f3258fde3ff2e8cab to your computer and use it in GitHub Desktop.
A very tiny plotting module for use with the html5 canvas
/* graph.js
*
* written by Richard Lange, January 2016
*
* This file provids a small set of html5 canvas plotting routines.
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALLIMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
function nearest_whole(x, interval){
// "round" x to the nearest multiple of interval
return Math.round(x / Math.abs(interval)) * Math.abs(interval);
}
var Grapher = {
major_grid: 1,
minor_grid: 0,
xlim: [-4,4],
ylim: [-2,2],
dim: [0,0],
init: function (selector, opts) {
opts = opts || {};
this.canvas = document.querySelector(selector);
this.dim = [this.canvas.width, this.canvas.height];
this.ctx = this.canvas.getContext("2d");
this.major_grid = this.major_grid || opts.major_grid;
this.minor_grid = this.minor_grid || opts.minor_grid;
this.xlim = this.xlim || opts.xlim;
this.ylim = this.ylim || opts.ylim;
this.stack = [];
this.grid();
return this;
},
figure: function () {
this.clear();
this.stack = [];
},
unit2pix: function (unit_xy){
// converts x,y to pixel coordinates
var x_pix_per_unit = this.dim[0] / (this.xlim[1] - this.xlim[0]),
y_pix_per_unit = this.dim[1] / (this.ylim[1] - this.ylim[0]);
var x_pix = (unit_xy[0] - this.xlim[0]) * x_pix_per_unit,
y_pix = (unit_xy[1] - this.ylim[0]) * y_pix_per_unit;
// reverse y coordinate so positive is up
y_pix = this.dim[1] - y_pix;
return [x_pix, y_pix];
},
draw_lines: function (coordinates, width, color){
if(coordinates.length < 2){
console.log("draw_lines with < 2 coordinates??");
} else{
this.ctx.lineWidth = width;
this.ctx.strokeStyle = color;
this.ctx.beginPath();
var start = this.unit2pix(coordinates[0]);
this.ctx.moveTo(start[0], start[1]);
for(var i = 1; i < coordinates.length; i += 1){
var px = this.unit2pix(coordinates[i]);
this.ctx.lineTo(px[0], px[1]);
}
this.ctx.stroke();
}
},
grid: function () {
// draw minor grid
if(this.minor_grid > 0){
var low_x = nearest_whole(this.xlim[0], this.minor_grid);
for(var x = low_x; x < this.xlim[1]; x += this.minor_grid){
this.draw_lines([[x, this.ylim[0]], [x, this.ylim[1]]], 0.5, "#808080");
}
var low_y = nearest_whole(this.ylim[0], this.minor_grid);
for(var y = low_y; y < this.ylim[1]; y += this.minor_grid){
this.draw_lines([[this.xlim[0], y], [this.xlim[1], y]], 0.5, "#808080");
}
}
// draw major gid
if(this.major_grid > 0){
this.ctx.strokeStyle = "#333";
this.ctx.lineWidth = 1.0;
var low_x = nearest_whole(this.xlim[0], this.major_grid);
for(var x = low_x; x < this.xlim[1]; x += this.major_grid){
this.draw_lines([[x, this.ylim[0]], [x, this.ylim[1]]], 1.0, "#202020");
}
var low_y = nearest_whole(this.ylim[0], this.major_grid);
for(var y = low_y; y < this.ylim[1]; y += this.major_grid){
this.draw_lines([[this.xlim[0], y], [this.xlim[1], y]], 1.0, "#202020");
}
}
// draw axes
if(this.xlim[0] < 0 && this.xlim[1] > 0){
this.draw_lines([[0, this.ylim[0]], [0, this.ylim[1]]], 2.0, "#000");
}
if(this.ylim[0] < 0 && this.ylim[1] > 0){
this.draw_lines([[this.xlim[0], 0], [this.xlim[1], 0]], 2.0, "#000");
}
},
linspace: function (lo, hi, n){
if(n === 1){
return [lo];
}
var vals = [];
for(var i = n-1; i >= 0; i -= 1){
vals[i] = lo + i * (hi - lo) / (n-1);
}
return vals;
},
plot: function (f, opts) {
opts = opts || {};
opts.width = opts.width || 1;
opts.color = opts.color || '#00f';
// record this function on the 'stack' for when plots are refreshed
this.stack.push({'func': this.plot, 'args': [].slice.call(arguments)});
var xs = this.linspace(this.xlim[0], this.xlim[1], Math.floor(this.dim[0]/2)),
points = [];
// populate points [x,f(x)]
for(var i = 0; i < xs.length; i += 1){
points[i] = [xs[i], f(xs[i])];
}
this.draw_lines(points, opts.width, opts.color);
},
shade_between: function (f1, f2, opts) {
opts = opts || {};
opts.fill = opts.fill || "rgba(255, 0, 0, 0.5)";
// record this function on the 'stack' for when plots are refreshed
this.stack.push({'func': this.shade_between, 'args': [].slice.call(arguments)});
this.ctx.fillStyle = opts.fill;
var xs = this.linspace(this.xlim[0], this.xlim[1], Math.floor(this.dim[0]/2)),
pts1 = [],
pts2 = [];
// populate points [x,f(x)]
for(var i = 0; i < xs.length; i += 1){
pts1[i] = this.unit2pix([xs[i], f1(xs[i])]);
pts2[i] = this.unit2pix([xs[i], f2(xs[i])]);
}
this.ctx.beginPath();
this.ctx.moveTo(pts1[0][0], pts1[0][1]);
// traverse f1 out
for(var i = 1; i < xs.length; i += 1){
this.ctx.lineTo(pts1[i][0], pts1[i][1]);
console.log(pts1[i]);
}
// traverse f2 back
for(var i = xs.length-1; i >= 0; i -= 1){
this.ctx.lineTo(pts2[i][0], pts2[i][1]);
console.log(pts2[i]);
}
this.ctx.closePath();
this.ctx.fill();
},
clear: function () {
this.ctx.clearRect(0, 0, this.dim[0], this.dim[1]);
},
redraw: function() {
this.clear();
this.grid();
var copy_stack = this.stack.slice(0);
this.stack = [];
for(var i = 0; i < copy_stack.length; i += 1){
console.log(copy_stack, this.stack);
copy_stack[i].func.apply(this, copy_stack[i].args);
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment