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-line
s:
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-line
s 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: