Clojurescript, re-frame and modular data visualisation
One of the issues we still have is how to combine our custom data visualisations in multi-view dashboards with brushing/linking. We’ll have a programmer work on this for the next two years, but as clojure is a bit of a hobby I’d like to try that out as well. It appears that clojurescript re-frame fits the bill.
Get set up:
|
|
my-project/resources/public/index.html
is the landing page for the application. It contains a div
with id app
that will hold the actual html and svg that we’ll create in views.cljs
.
index.html
|
|
The my-project/src/cljs/my_project
directory hosts the most important files.
-
core.cljs
defines which div inindex.html
is used to mount the application. -
db.cljs
holds the application state. -
events.cljs
: no idea yet what this does… -
subs.cljs
contains subscriptions to different parts of the application state -
views.cljs
holds the actual application code
I still need to read the '6-domino cascade' part at the re-frame website, so it’s not all clear yet. Will update here where I get there…
Version 1 - Straightforwardly creating separate svg elements
core.cljs
: didn’t touch this
|
|
db.cljs
holds the app state
|
|
events.cljs
|
|
subs.cljs
|
|
views.cljs
is where I define the main panel, as well as the functions to draw circle and rectangle. These latter two should later be moved to separate files so as to make this truly modular.
Note the :on-mouse-over
and :on-click
(and keep a look at your browser console); we’ll probably use that later for brushing/linking.
|
|
Here’s the result:<br/> image::re-frame_screenshot.png[width=25%]
Here we’re just straightforwardly calling a (map circle @data)
to create the separate SVG elements.
Version 2 - A complete plot and passing options
Instead of directly creating a separate circle for each datapoint, we want to create a scatterplot that takes the complete dataset as input.
Step 1: replace map
with call to creator function
This is also very simple: we only have to create a scatterplot
function, and replace the call in the main-panel
.
|
|
circle
function, otherwise it will complain.
We now have to replace the call in the main-panel
:
|
|
Step 2: passing options to the scatterplot
We might have a border around our scatterplot. Or not… Of course we can create different functions: one for scatterplot without border, and one with border. This obviously is not scalable to other options. So let’s see if we can get this working: draw a scatterplot with a border next to one without.
|
|
This took a while to figure out, simply because the scatterplot
function needs to return a single data structure. I must say: figwheel helped a lot here.
|
|
Having access to a REPL that is connected to the browser works wonders. The lein figwheel dev
mentioned above starts a REPL that is connected live to your code. Whenever you save your file, these changes are reflected here as well. To investigate what the scatterplot
function returned, I’d just run it in that REPL:
|
|
This returns
|
|
Initially, I’d get output where the :rect
was not really part of the same data structure. Hence the flatten
→ partition
→ map vec
.
|
|
The final picture looks like this:<br/> image::re-frame_screenshot_borders.png[width=25%]
Next:
-
getting the id of the element on select/hover…
-
getting mouse position
Version 3 - Getting the mouse position
In order to get the mouse position, we need to unfortunately dig into javascript itself. The code block below prints the mouse position to console whenever the mouse moves over the scatterplot.
|
|
Version 4 - Changing state
At some point we’ll want to change state. Never having used atoms before, I did need some help from [this blog post](http://blog.klipse.tech/clojure/2019/02/17/reframe-tutorial.html).
Changes to db.cljs
:
|
|
In subs.cljs
:
|
|
In events.cljs
:
|
|
In views.cljs
, we change circle
so that the selected-point
stores the value of any point that is clicked on:
|
|
We can then show that e.g. if you click on one of the SVGs. We need to add selected-point
to the let
, and add an on-click
event to an SVG:
|
|
::
…
Version 5 - Very simple brushing/linking
OK. Next step: when I hover over one of the datapoints, I want that datapoint in all other plots to be highlighted.
At this point we need to add CSS to the picture. For the moment I have just put this in the head of the main index.html
…
|
|
The idea is that any selected datapoint will get a red border.
In db.cljs
we need to add an additional piece of state in the default-db
, namely which is the selected point:
|
|
subs.cljs
:
|
|
And here’s the tricky part: in the view we need to toggle the class of an SVG element between "selected" and an empty string so as to apply the correct CSS. An element is only redrawn if its parent (i.c. scatterplot
or barchart
) is redrawn, or if one of its properties changes. At this moment, the only property of scatterplot
are opts
and data
, but we need to add selected
to that.
|
|
The only thing changed here is the parameters; the code itself has not changed.
In the main-panel
we need to call scatterplot
differently of course: we will add the @selected-point parameter.
|
|
Finally, in the circle function we add a :class
parameter which is either "selected" or "" based on whether or not that datapoint is the same as @selected-point
or not. The :on-mouse-over
makes sure that we actually change the selected point when we move our mouse.
|
|
The result looks like this: