Closed JobLeonard closed 7 years ago
BTW, this isn't a proposal; I'm going to do this since all of our architecture is pretty much ready to do it. Step one is a hour of work, step two is one, two days of work, tops. This is just documentation/clarification for myself.
Step 1 done during the flight from Stockholm to Amsterdam and pushed to the server using the internet in the train to my parents :)
So while creating a CanvasGrid element (the basic frame of which is done) I realised that to fully make this work, it needs to handle all kinds of things the browser normally does for us. For example, scrolling:
... plus lots of edge-cases because browsers are inconsistent in handling these events. This isn't really worth the hassle of implementing at the moment.
However, working on this was still very useful for figuring out how to render print versions of the plots, so it's not all wasted effort.
Closing this issue for now; instead of step 2 we're exploring Vega instead.
Our current set-up for plotting is a bit strangely intertwined:
LandscapeView
(I'll ignore the whole stack of wrapper components aroundLandscapeView
for now)Scatterplot
Scatterplot
contains aCanvas
componentCanvas
expects apaint
function to be passed to itScatterplot
also defines thispaint
function and binds it to itselfpaint
function is called with acontext
parameter (meaning canvas context, not to be confused with React context), which in turn has its ownwidth
andheight
fields. The painter function then draws on the context using theprops
,state
and other data provided byScatterplot
.In short, it's almost like a clear linear path, except that there is a back-and-forth going on between the plotter functions and canvas. Furthermore, only a small part of this chain really needs to involve React compoments: the part that is related to the sceen layouting. Everything else is either purely related to data-preparation, i.e.
LandscapeView
, or drawing on a context, i.e.Scatterplot
.What I want to do is untangle these functionalities and uncouple React as much as possible.
Step 1: rewrite the plotters as being just functions, without component logic
There is no need for Scatterplot or Sparkline to be a component: they just define and pass down a
paint
function toCanvas
. This is the render function of both of these:With that in mind we might as well remove the whole component aspect and turn them into functions that generate a
paint
function, that we then can pass to a Canvas element.This would remove the dependency on React as far as the plotter functions is concerned, which has two benefits:
In this set-up, Scatterplot would be a function that takes parameters
x
,y
,color
,colorMode
, and optional flag parameterslogScaleColor
,logScaleX
,logScaleY
, and then returns apaint
function that can be passed to aCanvas
. This takes very little effort to rewrite since all I'm doing is stripping React Component logic.Step 2: CanvasGrid & Printing
So here's the realization I had that triggered all of this: as far as the
paint
function is concerned, thewidth
andheight
attached to the context are the width and height of the context, but it doesn't have to be. For example, this is what the Canvas component does now:The result is that it draws to the full size of the canvas. But if we did this instead:
... we would draw the same plot four times in a 2x2 grid on one context. Note how simple this is in our existing architecture.
In other words: instead of creating, mounting, layouting individual canvases for every plot, we could create one big canvas and then plot on sub-sections of this canvas.
To do this, all we need to do is make a
CanvasGrid
component that takes an array of{ paint, x, y, width, height }
objects, with only thepaint
field being mandatory (we could even go nuts and do all kinds of rotation and stretching tricks withrotate
andscale
, but we don't really need that functionality and it would greatly complicate things - although I suppose rotations at 90 degree angles would be useful).CanvasGrid
would then handle the logic of setting up the context before calling the provided paint functions. Simple! Note that this allows for only drawing the visible plots. A check whether or notx, y, width, height
is contained within the context is extremely cheap, and we can simply skip calling the pain functions outside of the view. Let's say we have 50 sparklines, but only 15 fit in the view. That's 35paint
calls avoided - assuming that plotting is the most expensive part of every render loop (which is likely), that's a big boost in speed.At the moment this only really affects the sparklines view, but given the print-outs I see on Hannah' and Lars' desk, looks likely that they will want to render dozens of sparklines at once. In that case this method should be significantly faster, and lighter on memory usage, especially on mobile.
This situation does complicate questions like scrolling behaviour, but we need to tackle those issues anyway if we want to make the canvas interactive so I don't think that's a significant issue here.
Finally, this set-up allows for a really elegant way to render print-versions of our plots:
document.createElement('canvas')
,context
,context
to relevant paint functions with all the relevant transformations applied and data attached.UI wise we can just add a
print
button to the settings view, plus some options ( A4, US-letter, custom size in mm, 300DPI, 72DPI).