anarchy.website
Toggle Dark Mode

Graphing a Poisson Distribution using Canvas

By Una Ada, May 21, 2018

A few semesters back I was taking a class called “Mathematics for Physics and Engineering II” which was a companion course for “Intermediate Physics Lab” that covered a lot of the mathematics used in data analysis. One example of this is the Poisson Distribution, which we had plenty of assignments on, including one that involved calculations. Rather than simply using one of the many available tools that existed already to calculate this, I decided to try to make my own program in JavaScript for it. Of course, this failed to produce accurate results due to an issue in calculating the reduced chi-squared ($\chi^2_{\text{red}}$) between the Poisson and the Gaussian Distribution it approximates as the mean increases, but the base code was still useful. I polished up the original which was built on a now defunct website as a standalone web page and kept doing so every once and a while as it came up. At the moment it’s available on my GitHub site at una-ada.github.io.

While it does work as it is embedded here, it looks significantly better in a wider frame. The part of it that I’m particularly interested in keeping for use in future projects is the graph axes. There are a few issues with these, especially the line not fully extending the length it should and so the last tick floating just off of it, but I like the general style. However, this is all written as HTML/CSS, completely based on DOM manipulation, which is very bad for graphs as they could easily be more complicated than the 100 bins shown here, so here I’ll be recreating this all using canvas.

So let’s begin with the basic theory, most of which can just be transferred over as it has nothing to do with the actual graphics and plotting. The Poisson Distribution describes the probability distribution of events with very low probabilities of happening given significant amounts of chances to happen. For instance: radiation, the probability of any given atom to decay is very low, but there are so many atoms that the even happens consistently anyway. This is a discrete distribution, unlike the Gaussian, so it only takes integer values:

\[P(x)=e^{-a}\frac{a^x}{x!},\tag1\]

where $a$ is the event rate (number of events per interval) and $x$ is some integer that describes the interval. Here $a$ is the mean and the standard deviation is $\sigma=\sqrt a$. Conveniently, we can define this recursively, allowing us to fill in an array fairly easily:

\[P(0)=e^{-a},\quad P(x)=P(x-1)\cdot\frac{a}{x}.\tag2\]

In JavaScript this looks something like

function poisson(a, rounds){
    var p = [Math.exp(-a)];
    for(let i = 1; i < rounds; i++)
        p[i] = (a * p[i - 1]) / i;
    return p;
}

where a is $a$ and rounds is the number of integers to evaluate the distribution over.

Now to get to the plotting! I’ll skip the actual plotting of the distribution for now since we don’t really have any axes to reference. The previous version used an element called #graph with the styling:

#graph {
    border-color:   #333;
    border-style:   solid;
    border-width:   0 0 1px 1px;
    height:         300px;
    left:           0;
    margin:         30px 0 40px 50px;
    margin-top:     calc(50vh - 170px);
    position:       relative;
    top:            0;
    width:          500px;
}

Of particular importance here is the border-width style, which defines a solid black line on the bottom and left of the element. These are the actual axis lines for the graph, everything in the graph is positioned relative to this box. We’ll have to emulate this with offset values since we can’t use elements like this in canvas. The end size of #graph is defined by its own function to allow it to adjust to different screen sizes:

function setSize(){
    var w = 500 * ~~((window.innerWidth-60) / 500);
    document.getElementById('body').style.width = w + 60 + 'px';
    document.getElementsByClassName('post')[0].style.width = w + 60 + 'px';
    document.getElementById('graph').style.width = w + 'px';
    plot(40);
}
window.onresize = setSize;
window.onload = setSize;

This also implies the element structure used to position the graph:

<div id="body">
    <div class="post">
        <div id="graph" style="cursor:move;"></div>
    </div>
</div>

.post was originally created to import styling from the website that this was first written on, but now exists purely to enforce the width of the graph. #body, however, does have some styling on its own:

#body {
    margin:     auto;
    position:   relative;
}

Combining all these styles we can come up with a position for the graph on a canvas. margin: auto; calculates even margins on either side of the element, so we can just subtract the width of the graph from the width of the screen and divide the result by 2 to get the left position of the graph. There is an offset of 60 in the previous code to create space for axis labels on the left, this can also be easily preserved. I’ll put all the axis rendering in its own function to make it easier to extract for use in other projects.

function loadAxes(){
    //	Calculate Positions
    var width	= 500 * ~~((window.innerWidth - 60) / 500),
        height  = 300,
        left	= ~~((window.innerWidth - width) / 2) + 60,
        top     = ~~((window.innerHeight - height) / 2);

    //	...
}

We’ll then need to create a canvas to draw in, this can be kept pretty simple by just making it fill the document:

<canvas id="graph"></canvas>
#graph {
    height:     100%;
    left:       0;
    position:   absolute;
    top:        0;
    width:      100%;
}

Of course, if width and height attributes are not set in the HTML then the canvas draw area defaults to some size and is just stretched by the CSS. This is compensated for when we set up the canvas in JavaScript:

var graph   = document.getElementById("graph"),
    rect    = graph.getBoundingClientRect();
graph.width = rect.width;
graph.height= rect.height;
var ctx     = graph.getContext("2d");

Now we can pass this ctx object into loadAxes and used the positions we calculated to draw out the axis lines:

function loadAxes(ctx){
    //	Calculate Positions
    //  ...

    //	Draw Axes
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(left, top);
    ctx.lineTo(left, top + height);
    ctx.lineTo(left + width, top + height);
    ctx.stroke();

    //  ...
}

Here’s what that looks like so far:

The lineWidth is set to 2 because when the stroke is drawn it moves out from the position of the actual line and is antialiased, so a lineWidth of 1 is drawn as a lineWidth of 2 but with an alpha of 0.5. This can, however, be fixed by shifting the positions by 0.5:

function loadAxes(ctx){
    //	Calculate Positions
    //  ...
        left	= ~~((window.innerWidth - width) / 2) + 30.5,
        top     = ~~((window.innerHeight - height) / 2) + 0.5;

    //	Draw Axes
    ctx.lineWidth = 1;
    //  ...
}

So now it looks like (I also changed + 60 to + 30 because that’s how it actually was in the original):

Now, onto the tick marks. The previous version used the following code:

//	PLOT TICK MARKS
for(let i = 0; i <= val.length; i ++){
    let t = document.createElement('div');
        t.className = 'plot-tick';
        t.style.left = (w / val.length) * i + 'px';
        if(i%5==0) t.style.height = 13 + 'px';
        e.appendChild(t);
    if(i%10==0){
        let l = document.createElement('div');
            l.className = 'plot-tick-label';
            l.innerText = i;
            l.style.left = ((w / val.length) * i) - 11 + 'px';
            e.appendChild(l);
    }
}

The val.length refers to the val array which would store the values to be graphed, essentially it is asking for the maximum value on the $x$-axis. We can just set this to be 100 for now. if(i%5==0) is a check for every fifth tick mark and is being used to set the height of these ticks to be 13px (taller than the other marks by 3px). if(i%10==0) is a check for every tenth tick mark to be labelled with an actual value. This uses the element classes plot-tick and plot-tick-label whose stylings are:

.plot-tick {
    background: #333;
    height:     10px;
    position:   absolute;
    top:        calc(100%);
    width:      1px;
    z-index:    10;
}
.plot-tick-label {
    color:      #333;
    position:   absolute;
    text-align: center;
    top:        calc(100% + 13px);
    width:      21px;
    z-index:    10;
}

Perhaps the most important part of the script for the ticks is the (w / val.length) * i phrase. This determines the scale of the ticks and the relative position by increment (i). We can store the taller ticks’ rate and height as constants for now to get them out of the rendering code itself but still have their values unchanging, then the rest of the plotting code is essentially the same as before but with canvas commands rather than DOM.

const
    TICK_LENGTH		= 10,
    EXTEND_LENGTH	= 13,
    EXTEND_RATE		= 5,
    LABEL_RATE		= 10;
function loadAxes(ctx, ticks=[100,5]){

    //  ...

    //	Draw x-axis Ticks
    for(let i = 0; i <= ticks[0]; i++){
        ctx.beginPath();
        let x = ~~((width / ticks[0]) * i) + left;
        ctx.moveTo(x, 0.5 + top + height);
        ctx.lineTo(x, 0.5 + top + height + (
            i % EXTEND_RATE == 0 ? EXTEND_LENGTH : TICK_LENGTH
        ));
        ctx.stroke();
    }
}

I threw a few half pixels in here and there to make sure that all the rendered lines are nice and crisp, and used a ternary operator to determine the length of the tick based on the EXTEND_RATE. ticks is an array of how many $x$- and $y$-axis ticks to be drawn ([x,y]) with a default of [100,5]. We can then repeat this process for the $y$-axis ticks:

//  ...
function loadAxes(ctx, ticks=[100,5]){

    //  ...

    //	Draw y-axis Ticks
    for(let i = 0; i <= ticks[1]; i++){
        ctx.beginPath();
        let y = ~~((height / ticks[1]) * i) + top;
        ctx.moveTo(0.5 + left, y);
        ctx.lineTo(0.5 + left - (
            i % EXTEND_RATE == 0 ? EXTEND_LENGTH : TICK_LENGTH
        ), y);
        ctx.stroke();
    }
}

Now our graph looks like this:

Adding in the tick labels is a bit more complicated since it requires manually placing the text rather than having the DOM handle the positioning. This can all be handled by textAlign and textBaseline:

function loadAxes(ctx, ticks=[100,5]){

    //  ...

    ctx.font 			= "13pt 'Roboto'";
    ctx.textAlign		= "center";
    ctx.textBaseline	= "top";

    //	Draw x-axis Ticks
    for(let i = 0; i <= ticks[0]; i++){
        //  ...

        //	Write Tick Labels
        if(i % LABEL_RATE == 0 || ticks[0] < LABEL_RATE)
            ctx.fillText(i, x, top + height + EXTEND_LENGTH + 1);
    }

    ctx.textAlign		= "right";
    ctx.textBaseline	= "middle";

    //	Draw y-axis Ticks
    for(let i = 0; i <= ticks[1]; i++){
        //  ...

        //	Write Tick Labels
        if(i % LABEL_RATE == 0 || ticks[1] < LABEL_RATE)
            ctx.fillText(ticks[1] - i, left - EXTEND_LENGTH - 1, y);
    }
}

Since there aren’t currently any values to actually graph, I used the index of the tick as the value to label it with. This can be changed by adding in some argument to the function called labelMax:

function loadAxes(ctx, ticks=[100,5], labelMax=[100,1]){

    //	...

    //	Draw x-axis Ticks
    for(let i = 0; i <= ticks[0]; i++){
        //  ...

        //	Write Tick Labels
        if(i % LABEL_RATE == 0 || ticks[0] < LABEL_RATE)
            ctx.fillText((i / ticks[0]) * labelMax[0], x,
                top + height + EXTEND_LENGTH + 1);
    }

    //  ...

    //	Draw y-axis Ticks
    for(let i = 0; i <= ticks[1]; i++){
        //  ...

        //	Write Tick Labels
        if(i % LABEL_RATE == 0 || ticks[1] < LABEL_RATE)
            ctx.fillText(((ticks[1] - i) / ticks[1]) * labelMax[1],
                left - EXTEND_LENGTH - 1, y);
    }
}

I also threw in the || ticks[n] < LABEL_RATE condition so that if the number of ticks is less than the prescribed rate at which to label them they’re just all labeled. This may not be the greatest solution but it works in this case.

At this point we should be able to write the actual plot function. This should encompass all of the other graph setup code including the call to loadAxes so it can alter the axes based on the results of the call to poisson. So that we can use them to position the graph properly, the loadAxes function should return the top, left, height, and width values as an object:

function loadAxes(ctx, ticks=[100,5], labelMax=[100,1]){

    //  ...

    return {
        "height" : height,
        "left"   : left,
        "top"    : top,
        "width"  : width
    };
}

Finally, we can start drawing the bins. The original version’s code for this uses rectangular elements called plot-lines:

for(let i = 0; i < val.length; i++){
    let l = document.createElement('div');
        l.className = 'plot-line';
        l.style.height = (val[i] * h) / m + 'px';
        l.style.width = (~~(w / val.length)) - 2 + 'px';
        l.style.left = (~~(w / val.length) * i) + 1 + 'px';
        e.appendChild(l);
}

The styling for plot-lines is:

.plot-line {
    background: #333;
    bottom:     0;
    position:   absolute;
    z-index:    10;
}

which is mostly useless in this case as it basically just describes a #333 colored box at the bottom of the graph, but completeness is important, imo. Rewriting the old version to just render rectangles on the canvas would be a tad sloppy so we can floor all the value (each time before they are used so we don’t end up with any gaps due to rounding) using bitwise nots (~). Doing so gives us a plot function like this:

function plot(n){
    var val 	= poisson(n),
        max		= Math.max.apply(Math,val),
        graph	= document.getElementById("graph"),
        rect 	= graph.getBoundingClientRect();
    graph.width = rect.width;
    graph.height= rect.height;
    var ctx		= graph.getContext("2d"),
        pos		= loadAxes(ctx,[100,5],[100,max]);

    for(let i = 0; i < val.length; i++){
        var w = ~~(pos.width / val.length),
            h = ~~((pos.height * val[i]) / max),
            x = ~~(pos.left + (w * i)),
            y = ~~(pos.top + (pos.height - h));
        ctx.fillRect(x,y,w,h);
    }
}

Now, if you look at the result, you may notice some glaring issues:

Namely that the $y$-axis labels are really long decimal numbers and that the distribution looks really harsh. We can soften this up a bit by changing the fill and stroke colors to #333 and adding some offsets to the bins:

function loadAxes(ctx, ticks=[100,5], labelMax=[100,1]){

    //  ...

    //	Draw Axes
    ctx.lineWidth = 1;
    ctx.strokeStyle = "#333";
    //  ...

    //  ...
}
function plot(n){
    //  ...

    ctx.fillStyle = "#333";
    for(let i = 0; i < val.length; i++){
        //  ...
        ctx.fillRect(x+1,y,w-2,h);
    }
}

The $y$-axis labels can be fixed by giving them the same treatment as they were given in the original version: multiply them by 1e4, floor, then divide by 100 and tack on a % sign.

function loadAxes(ctx, ticks=[100,5], labelMax=[100,1]){

    //  ...

    ctx.fillStyle		= "#333";

    //  ...

    //	Draw y-axis Ticks
    for(let i = 0; i <= ticks[1]; i++){
        //  ...

        //	Write Tick Labels
        if(i % LABEL_RATE == 0 || ticks[1] < LABEL_RATE)
            ctx.fillText(
                ~~(((ticks[1]-i)/ticks[1])*labelMax[1]*1e4)/100+"%",
                left - EXTEND_LENGTH - 1, y);
    }

    //  ...
}

All this improves the look of the graph significantly, just look at it! Though you should open it in it’s own tab so you can see it in all its glory and not just this shrunk down version.

To finish this all off we can make a few final adjustments. First off, the original version was not vertically centered exactly, it was calc(50vh - 170px) and there were only 4 $y$-axis ticks, so we can change that:

function loadAxes(ctx, ticks=[100,4], labelMax=[100,1]){
    //	Calculate Positions
    var width	= 500 * ~~((window.innerWidth - 60) / 500),
        height  = 300,
        left	= ~~((window.innerWidth - width) / 2) + 30.5,
        top     = ~~(window.innerHeight / 2) - 170 + 0.5;

    //  ...
}

We’d also like to see the mean (⟨j⟩) and standard deviation (σ) of the distribution as in the original. Once again it just takes a little adapting of the original code:

for(let i = -1; i <= 1; i++){
    if(n + (i * Math.sqrt(n))>val.length) break;
    if(i!=0){
        let m = document.createElement('div');
            m.className = 'plot-line';
            m.style.width = 1 + 'px';
            m.style.height = h + 'px';
            m.style.left = Math.min(w,(w/val.length)*(n+(i*Math.sqrt(n))))+'px';
            e.appendChild(m);
    }
    let k = document.createElement('div');
        k.className = 'plot-mark';
        k.innerText = i == 0 ? '⟨ j ⟩' : 'σ';
        k.style.left = Math.min(w,(w/val.length)*(n+(i*Math.sqrt(n))))-13+'px';
        e.appendChild(k);
}

This just passes from -σ to σ in increments of σ, with conditions along the way to treat each mark specially. Rewritten it looks like:

function plot(n){

    //  ...

    //	Plot Mean & Std. Dev.
    ctx.textAlign		= "center";
    ctx.textBaseline	= "bottom";
    for(let i = -1; i <= 1; i++){
        if(n + (i * Math.sqrt(n)) > val.length) break;
        let x		= (n + (i * Math.sqrt(n))),
            left	= pos.left + (x * (pos.width / val.length));
        ctx.fillText(i == 0 ? '⟨ j ⟩' : 'σ', left, pos.top - 5);
        if(i != 0){
            ctx.beginPath();
            ctx.moveTo(left, pos.top);
            ctx.lineTo(left, pos.top + pos.height);
            ctx.stroke();
        }
    }
}

Graphically this brings us up to date with the original, but there’s one thing missing: dragging. There’s no fun in static graphs, we ought to be able to see how the Poisson looks with different means, and we ought to be able to drag it there with the mouse, obviously. In fact, this part is almost copy and paste from the original.

//	LET USER DRAG GRAPH
window.draggingJ = false;
document.getElementById('graph').addEventListener('mousedown',function(e){
    window.draggingJ = true;
    var g = document.getElementById('graph'),
        r = g.getBoundingClientRect(),
        p = e.pageX - r.left,
        j = Math.min(Math.max(1,~~((p / r.width) * 100)),100);
    plot(j);
});
window.addEventListener('mouseup',function(e){
    window.draggingJ = false;
});
window.addEventListener('mousemove',function(e){
    if(window.draggingJ){
        var g = document.getElementById('graph'),
            r = g.getBoundingClientRect(),
            p = e.pageX - r.left,
            j = Math.min(Math.max(1,~~((p / r.width) * 100)),100);
        plot(j);
    }
});

The only part that wouldn’t work is the call for #graph’s bounding rectangle. This would have to be calculated manually in this case:

//	Drag Setup
window.draggingJ = false;
window.addEventListener('mousedown',function(e){
    window.draggingJ = true;
    var width	= 500 * ~~((window.innerWidth - 60) / 500),
        left	= ~~((window.innerWidth - width) / 2) + 30.5;
    var p = e.pageX - left,
        j = Math.min(Math.max(1,~~((p / width) * 100)),100);
    plot(j);
});
window.addEventListener('mouseup',function(e){
    window.draggingJ = false;
});
window.addEventListener('mousemove',function(e){
    if(window.draggingJ){
        var width	= 500 * ~~((window.innerWidth - 60) / 500),
            left	= ~~((window.innerWidth - width) / 2) + 30.5;
        var p = e.pageX - left,
            j = Math.min(Math.max(1,~~((p / width) * 100)),100);
        plot(j);
    }
});

With that added in, this is now up to date with the original! Hoorah! Holy fuck am I tired all of a sudden. Anyway here it is: