anarchy.website
Toggle Dark Mode

Mathematical Graphing Library

By Una Ada, July 14, 2018

Every now and again I get back to transcribing my old school notes as digital files on this site; which notes I work on really depends on my mood, and if you have read any of this site I’m sure you’re aware I’m more often than not in the mood for mathematics bullshit. To that end, one set of notes that’s been taunting me for months is that from my calculus classes (AP, II, and III). I’ve completed my differential equations before these due to one limitation: graphs. While there aren’t any graphs in my diffeq notes, there are plenty in my calculus notes, and there aren’t really any graphing libraries I like that can be used here. The closest I’ve found was embedding a Desmos graph, but there’s an issue with customization there and I really like consistent aesthetics.

Most JavaScript libraries for graphs are for data not mathematics, which is fine as I can see many more uses for those on the web than functions, but it puts me in this situation. There are a couple here and there that have gotten close to getting me to use them, but in the end I’ve opted for just writing my own.

For starters, I need some axes. Recently (perhaps more aplty “a little while back”) I rewrote an old script to work in canvas, read all about that here. I’m not going to reiterate the whole process of creating these axes as a lot of it is in that blog post, but what I will talk about is the fact that those axes are very specifically designed for that exact usage and nothing more. This calls for what I like to call generalizing, in this case that means offsetting the previously anchored axes by some arbitrary origin point. To make this all easier, I went ahead and started creating constants based on an options parameter of the form

const VARIABLE = options.variable || DEFAULT;

Because the original axes were explicitly showing the first quadrant, there were rendered as two strokes of a single path to avoid unnecessary complication, but I’m going to need all four quadrants at some point or another so that’ll have to change. This means taking the very simple

ctx.beginPath();
ctx.moveTo(left, top - 0.5);
ctx.lineTo(left, top + height);
ctx.lineTo(left + width + 0.5, top + height);
ctx.stroke();

and replacing it with a more complex

//  y-axis
ctx.beginPath();
ctx.moveTo(left + (width*ORIGIN.x), top - 0.5);
ctx.lineTo(left + (width*ORIGIN.x), top + height);
ctx.stroke();
//  x-axis
ctx.beginPath();
ctx.moveTo(left - 0.5,   top + (height*ORIGIN.y));
ctx.lineTo(left + width, top + (height*ORIGIN.y));
ctx.stroke();

as well as sprinkling those (width*ORIGIN.x) and (height*ORIGIN.y) terms throughout the rest of the code to offset everything. I should also note that, as implied by its usage here, the ORIGIN constant is an object of the form {x:NUMBER,y:NUMBER} where x and y are some fraction of the screen at which the origin will be placed by width and height respectively.

The most notable place that these offsets need to be added besides the initial axis renderings is the tick renderings. Just adding the position offsets makes these ticks render along the axes just fine, but they still count up form zero at one end up to some set maximum at the other, with the origin in the middle somewhere. This is just wrong, so I need to offset the beginning of the ticks. Doing this may make the position of those ticks rendered more correct but it also produces two further problems: many of the ticks extend beyond the visible region of the canvas and a large portion of the rendered axes is devoid of ticks. Both of these can be fixed by slightly redoing how the ticks are counted, setting a value for the total length of the axes and having it spanned in both the positive and negative directions up to the edges of the canvas. With some experimentation it has become evident that I did this wrong, but throwing in some 1-s seems to have fixed that.

With that the axes more or less match expected behavior and I can move on to the more exciting part of actually rendering a function. My very first assumption as to how this can be done is to loop across the $x$-axis and scale the pixel value into the actual mathematical value, plug that into the function, scale the result back into a pixel value, then use those values as a point on a path. It’s very rudimentary but there isn’t really any significant reason it wouldn’t work. With that said, the full implementation I came up with is here:

var scaleY = y=>(-1*y*(height/LABEL_RANGE_Y))+top+~~(height*ORIGIN.y),
    scaleX = x=>(x*(width/LABEL_RANGE_X))+left+~~(width*ORIGIN.x),
    invX   = x=>((~~(width*ORIGIN.x-x-left))*(LABEL_RANGE_X/width));
ctx.lineWidth       = FUNC_WIDTH;
ctx.strokeStyle     = FUNC_COLOR;
ctx.beginPath();
ctx.moveTo(left-0.5,scaleY(f(invX(0))));
for(let i=left;i<=left+width;i++){
    console.log(invX(i))
    ctx.lineTo(i,scaleY(f(invX(i))));
}
ctx.lineTo(left+width,top+~~(height*ORIGIN.y));
ctx.stroke();

And now to find excuses why this sucks, actually. First of all, minor detail, sometimes the tick labels get some floating point errors and just become so excessively long. This is a quick fix, just add some ROUNDING value so that

ctx.fillText(
    (i/TICK_COUNT_X)*LABEL_RANGE_X,
    x,top+(height*ORIGIN.y)+EXTEND_LENGTH+1
);

becomes

ctx.fillText(
    (~~((i/TICK_COUNT_X)*LABEL_RANGE_X*(10**ROUNDING)))/(10**ROUNDING),
    x,top+(height*ORIGIN.y)+EXTEND_LENGTH+1
);

which is a bit rough and sloppy but it gets the job done. Second, I want to test this using trigonometric functions, but those would look best if the $x$-axis is split into $2^n$ ticks and the $y$-axis into $n\cdot10$ ticks, which is only possible right now if those happen to be equivalent… that doesn’t happen very often so I’m going to add in separate settings for the $x$- and $y$-axes:

LABEL_RATE   = options.labelRate  || 10,
LABEL_RATE_X = options.labelRateX || LABEL_RATE,
LABEL_RATE_Y = options.labelRateY || LABEL_RATE,

Notice I have them defaulting to LABEL_RATE so unless you set them manually they act like they aren’t even there.

Ok, with the petty annoyances out of the way, I can get into the real testing of this. It absolutely hates the function $\sin\left(\frac1x\right)$ which is a surprise to absolutely nobody. The issue here is something called “aliasing,” something that should be quite familiar to most people reading this, I would assume. Essentially, the sample rate is lower than the frequency of the function so the full function isn’t rendered but a very butchered version of it based on what the samples could pick up. After searching far and wide for solutions that others have come up for this, I finally stumbled across a blog post called “Plotting High-frequency Functions Using a GPU” that discussed this exact situation. Of course, this is a completely different method of rendering the function, which means rewriting the entire function rendering code… all 14 lines of it… which I’m not exactly in the mood for…

Not only do I just “not feel like it” but the code provided by this post is all in some obscure language, seemingly developed by the author of the post, called Fragmetarium. Canvas isn’t really developed for pixel shaders, it’s developed for painting lines like I did in the first implementation. I’ve been searching various different ideas about this and the best I’m getting is something about painting image data from a source… oh, wait, there’s something about manipulating the pixel data of an image… this could be the right path. Ok, so I found a gist that takes the current state of the canvas and rewrites the pixel data. I believe the main point of this snippet itself is the usage of typed arrays for more efficient operation, which isn’t what I need it for but nonetheless is neat. The real meat of the code is this bit:

var imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
var buf = new ArrayBuffer(imageData.data.length);
var buf8 = new Uint8ClampedArray(buf);
var data = new Uint32Array(buf);
for (var y = 0; y < canvasHeight; ++y) {
    for (var x = 0; x < canvasWidth; ++x) {
        var value = dataset[y][x];
        data[y * canvasWidth + x] =
            (255   << 24) |    // alpha
            (value/2 << 16) |    // blue
            (value <<  8) |    // green
            255;            // red
    }
}
imageData.data.set(buf8);
ctx.putImageData(imageData, 0, 0);

There are obvious references to dataset which is created earlier in the code, using d3.js, but it’s basically just a 2d array of random numbers in the range $[0,255]$. Combining this with the calculation method of the previously mentioned post, I should be able to come up with some actual rendering code. Also, this will be painting all the pixels from scratch, so I need to move this to before the axis rendering, which is again just a minor change but very important. I’ll also need a way to convert from a $y$ position in the pixel array to a mathematical $y$ position just as I did with $x$:

invY   = y=>(((height*ORIGIN.y-y-top))*(LABEL_RANGE_Y/height)),


With that in place, I can use it to calculate whether or not the function exists at a particular point, basically just implementing the aforementioned pixel shader in JavaScript like so:

for(let y=0;y<height;++y)
    for(let x=0;x<width;++x){
        let count  = 0,
            samples = 0;
        for(let i=0;i<SAMPLES;i++)
            for(let j=0;j<SAMPLES;j++){
                if (i*i+j*j>SAMPLES**2) continue;
                samples++;
                let val = f(invX(x+i*SAMPLE_SIZE))-invY(y+j*SAMPLE_SIZE);
                count += val>0?1:-1;
            }
        // ...
    }

This count variable can then be used to calculate the color that a given pixel should have, I’m just going to use the alpha channel for a standard red color here because it’s the easiest to do:

var img  = ctx.getImageData(0,0,width,height),
    buf  = new ArrayBuffer(img.data.length),
    buf8 = new Uint8ClampedArray(buf),
    data = new Uint32Array(buf);
for(let y=0;y<height;++y)
    for(let x=0;x<width;++x){
        let count  = 0,
            samples = 0;
        // ...
        let a = (Math.abs(count)/(SAMPLES**2))*255;
        if(!(x%100)&&!(y%100))console.log(a)
        data[y*width+x] =
            (255-a << 24) | // alpha
            (0     << 16) | // blue
            (0     <<  8) | // green
             255;             // red
    }
img.data.set(buf8);
ctx.putImageData(img,PADDING_LEFT,PADDING_TOP);

Basically this is all just jamming code I found on the internet into the context of my own code, but y’know there’s a bunch of bugfixing work I’m not talking about because it’s really just like “oh no, that number is the inverse of what I had previously assumed” or “oops, that’s the wrong variable name” kind of shit.

Now that I know that this works, I can start tweaking it. First thing’s first, colors. I’m not quite to the point of knowing how to get it to parse a custom color code, but I can at least get it to properly render red without alpha channels. Basically, everything not on the path should be white: FFFFFF and everything on the path should be red: FF0000, so the blue and green values are the inverse of how much it’s on the path (apparently a is already this inverse so that’s nice).

data[y*width+x] =
    (255<<24)|// alpha
    (a  <<16)|// blue
    (a  << 8)|// green
    (255<< 0);// red


A quick note I forgot to mention, this initially didn’t really look any better than the path based renderer, but turns out that’s because I had a lot of ~~s scattered throughout the calculations. These were there so that straight lines would look like solid lines instead of translucent, but in this case it means that the aliasing remains. Without those there, certain settings are free to be non-integer values. I’m reminded of this because I’m attempting to increase the accuracy of the drawing. Initially I had SAMPLES set to 3 and SAMPLE_SIZE set to 0.5, but by using more small samples (say setting these to 6 and 0.5 respectively) the graph can be cleaned up. I’m ignoring it for now but adjusting these does change the width of the path, which will need to somehow be addressed in the future to allow the width to be customized without also affecting the aliasing.

This brings me to a much more tedious portion of this project: dragging. A big consideration in the sample rate settings was efficiency, but it still isn’t the most efficient code in the world. It would be nice to be able to run it in some other context that wasn’t built so high level as JavaScript code in the browser but that would contradict the whole point. Ignoring this for now, I’m just going to write a basic implementation of a mouse drag effect. Obviously all the code up until now will need to be packaged into its own function (draw()) so that it can be called multiple times as its dragged. Having done that, I can write a function that sets everything up for dragging when the mouse is clicked on the graph:

window.graph_drag_active = false;
window.graph_drag_origin = origin;
ctx.canvas.addEventListener("mousedown",e=>{
    window.graph_drag_position = {x:e.pageX,y:e.pageY};
    window.graph_drag_active = true;
});

Then just calculate the change in position whenever the mouse is moved after this initial click:

ctx.canvas.addEventListener("mousemove",e=>{
    if(graph_drag_active){
        var rect = ctx.canvas.getBoundingClientRect(),
            dx = (e.pageX - graph_drag_position.x)/rect.width,
            dy = (e.pageY - graph_drag_position.y)/rect.height;
        draw({
            x:graph_drag_origin.x + dx,
            y:graph_drag_origin.y + dy
        });
    }
});

I initially had problems with writing this part because I kept stupidly writing the origin object as origin = {valueForX,valueForY}. I don’t know how I went and forgot extremely basic syntax but what can you do. After that everything just needs to be cleared up when the mouse is lifted back up:

ctx.canvas.addEventListener("mouseup",e=>{
    if(graph_drag_active){
        var rect = ctx.canvas.getBoundingClientRect(),
            dx = (e.pageX - graph_drag_position.x)/rect.width,
            dy = (e.pageY - graph_drag_position.y)/rect.height;
        window.graph_drag_origin = {
            x:graph_drag_origin.x + dx,
            y:graph_drag_origin.y + dy
        };
        draw(graph_drag_origin);
        window.graph_drag_active = false;
    }
});

This implementation lags horrendously, to the point where I almost thought it wasn’t even running at first. Evidently the rendering is too inefficient to even run on the fly like this with the expectation of slowness. My first thought is to just lower the sample rate while dragging:

ctx.canvas.addEventListener("mousedown",e=>{

    // ...

    window.graph_drag_temp = [SAMPLES,SAMPLE_SIZE];
    SAMPLES = 2;
    SAMPLE_SIZE = 1;
});

// ...

ctx.canvas.addEventListener("mouseup",e=>{
    if(graph_drag_active){

        // ...

        SAMPLES = graph_drag_temp[0];
        SAMPLE_SIZE = graph_drag_temp[1];

        // ...

    }
});

This runs, it’s still laggy, it looks really bad, but you can tell that it’s actually following the cursor when dragged. But really, what else can be done? I tried optimizing all the code, removing redundant operations, simplifying calls, whatever you want to call it. But it’s barely getting any better, is there any solution that will actually make this run fast enough to feel natural?

That’s a rhetorical question, obviously if I’m lowering the sample rate when dragging I’m willing to sacrifice accuracy for efficiency when interacting, and what method do we know that makes that exact exchange? That’s right, I’m bringing the original render method back! Basically this just means adding a conditional that’s set to true when dragging and false otherwise, trying to show the code change here would just be a real mess so just go read the commit page on GitHub.

With that, this is basically as functional as I feel it needs to be for the moment, there are still some things I would like to add. Namely zooming in and out with scroll, possibly keyboard controls, offsetting labels that would be cut off by the edge, multiple function rendering, custom graph colors, custom graph widths, … I could go on, but that’s all for now.