How to embed a highcharts.js plot in a reagent component
Draft of 2016.09.13
May include: programming ↘ clojure ↗ &c.
I’m working on a followup to the previous essay(s) on genetic programming and symbolic regression. But instead of being just another essay (because that would be easy), it’s an actual working browser-based system. You’ll be able to “do some genetic programming” in your browser, and based on the testing I’ve done so far it seems perky enough to permit some nearly-realistic scenarios to be explored.
Not done, but soon. In the meantime, I should write down a few of the things I’ve learned along the way. Because as far as I can tell, Googling won’t help a lot.
The litany of interlocking and interdependent packages is getting huge. Such is modern development.
I’m working in ClojureScript. I’m using the excellent reagent
templating system, which is really a wrapper around Facebook’s also-excellent React.js
system for user interfaces. One of the things the demo needs to be able to do is show (and update) a highcharts.js
plot.
If you’re here because you’re trying to do that same thing, you’ve almost certainly been to this cookbook entry that “explains” it. Watch me do a psychic trick: I bet you’re still looking for more detailed info. Oh look, you are.
There are plenty of excellent introductions, and one especially wordy and informative overview of how reagent
works at the re-frame
project’s landing page. Anybody doing anything in reagent
seems to end up there, so you may as well wade in and read it beforehand, since inevitably you will get lost in a maze of twisty passages when you try to do something “real” and the referral chain will take you there anyway. It’s well-written and chatty, something you might already know I admire.
Anyway, it won’t help much with the problem I had. I mean it will help you understand why (in a philosophical and architectural sense) I was having a problem to begin with, but in the end it does not contain the if-this-then-do-that
prescriptive advice I’m going to write here for my future self to discover when he has forgotten how painful the last four days have been. Hey, Future Self: Also you should not sit in one place in the Big Red Chair for more than nine hours straight working on your new problem, even if you’ve forgotten the constant pain doing so caused you when you were working on this one.
Charts
Highcharts is a downloadable JavaScript library. Like many JS libraries (and almost all object-oriented software in the world) you would normally use it in a JavaScript setting by constructing an instance of the core Chart
object, and passing it some configuration data in the form of a JS object. The cookbook entry does a thorough job explaining that.
The trick of it is, you’re not working in JavaScript, and (in reagent
) you’re not working in an object-oriented setting. It’s relatively easy to embed a one-time passive unchanging plot in a page; that’s exactly what the cookbook is explaining. If you want to show data, just once, that will do it for you.
But if you want to update the chart ever, that’s the tricky part, because reagent
is displaying immutable data only.
Credit where due
The particular solution I present here is lifted more or less directly from Zach Charlop-Powers, who was actually talking about d3. I found that only after a near-sleepless night trying to parse this Google Groups thread, where (either in that or another thread) somebody slipped while typing and said “d3” where they meant “reagent”. So like Poirot bothered by something the Duchess said when first they met that afternoon, I finally stumbled on the murderer, but had not yet discovered the motive.
I believe the motive, in this case, is that reagent
is trying so very diligently to push the complexity out into simple Clojure-ish immutable state, and the rest of the JavaScript world is not cooperating. As Day8 has explained, you really need a “Form-3” reagent
class when you’re trying to dance with the JavaScript devil. Which is why you’re here at all, I bet.
And the motive becomes evident: They are trying to keep you from seeing the Horrors that lurk behind the veil. It was fun while it lasted, wasn’t it?
Don’t mind those tentacles. Let’s go on a tour.
Component nesting
I’ll quit the chit-chat and show you how I ended up working this out. By the time you read this (Future Self, or other Frustrated Googler) the details of reagent
component structure may have changed; it’s a lively and diligently maintained library, and it literally changed while I was working this out. So expect things to be slightly different at least.
I included the Highcharts dependency in my project.clj
:dependencies
vector, specifically the cljsjs/highcharts
ClojureScript wrapper. Notice that I did not include jQuery as its cljsjs
wrapper.
Instead, I added the standard jQuery CDN link in my HTML template. To be honest, I might have been better off bringing in Highcharts via CDN as well, but I didn’t try. It was a long and frustrating four days. But at least one crucial thing I learned: loading both Highcharts and jQuery with cljsjs
will cause a compilation error, and you’ll go down a rat-hole learning about extern
files for the Google Closure Compiler using the :advanced
compression algorithm, and you will not be happy.
So:
- Bring in Highcharts. You can try loading it at the page level; I loaded it at the
project.clj
level. Make sure you also load it into the namespace you’re working in. - Bring in jQuery. In this case because you’ll want to use
$
, and to be honest I’m sure you’re very smart and can work out some ingenious way to do that with stuff already in the Google Closure libraries present by default in ClojureScript. But then again, if you’re making a real Web app then quickly something else will want jQuery, so just bring the damned thing in and simplify your life. - In your
reagent
page code, you’ll build a series of nested components:- A Highcharts configuration function that returns a Highcharts plot configuration for a given data series. I found this easier to do by massaging ClojureScript hash-maps and vectors and then converting those into JavaScript with the
clj->js
macro, as you’ll see below, but you might be more comfortable working in JavaScript objects. Also, I am plotting several data series that are all changing over time, and so I inserted all of the:series
attribute as a whole; if you are only changing one series, your configuration constructor function will be slightly different of course. - A
reagent/atom
that contains the data you will be passing into the function above. Whenever you want your chart to change (and be updated), you willswap!
orreset!
the value of this atom. - An inner chart component. This is a function (here, an explicit call to
reagent/create-class)
that returns areagent
component, but note that the argument it accepts is not the data atom, it’s the contents of that atom. This component will draw a single instance of your Highcharts plot, invoking your configuration function to construct the chart, and placing it in an#id
-specified DOM component. As you see in the code below, thisreagent
component must have at least three constituent life-cycle functions::reagent-render
builds the empty HTMLdiv
and assigns the correct#id
and attributes (width
,height
, maybe abackground-color
so you can see where it’s supposed to be when it inevitably fails the first few times…):component-did-mount
draws the plot in thediv
the first time:component-did-update
draws the plot in thediv
whenever your data atom changes, and it is crucial and the syntax is horrifying and esoteric and to be honest I don’t personally understand what it’s even doing, but there you go
- An outer chart component. This is a relatively simple Form 2
reagent
component, a function that manages the persistent state of your data atom, and updates the chart whenever that changes. You can see the inner component there, in the code below, where I call[inner-charter @series-atom]
, and notice again that while the outer component is addressing the atom, the inner component is being sent the dereferenced contents of the atom. - Finally, you will want to invoke that outer chart component function in some higher-order component. Maybe in the root of the page, or maybe in some other component. It doesn’t matter. You’ll call that, passing in the atom in which your data is held and updated.
- A Highcharts configuration function that returns a Highcharts plot configuration for a given data series. I found this easier to do by massaging ClojureScript hash-maps and vectors and then converting those into JavaScript with the
When you look over that last outer component code below, you’ll see I’ve included a simple button labeled “Add!”. When you click that button, it will update the atom to include a new series (of some random numbers, in this example).
That button could be anywhere on the page. It might not even be in this file, or on this page, depending on where your data atom resides and how you manipulate it. And of course it might not be a button-push at all that triggers the change: you can set up a component that downloads new data to plot on a time schedule, or subscribes to some channel from which new data packets arrive, or link it to the page load event, or anything you can imagine.
The big lesson, for me, is this: Outer component watches the atom. Inner component receives just the data from that atom, and makes the picture and places it in a DOM element appropriately. And because this is reagent
, whenever the atom containing your data is updated, the chart will be replaced with a new one.
Not perfect
This is not the solution I was looking for. If I were feeling ambitious, I probably would have worked another three or four days learning enough about the horrifying syntax of JavaScript interoperability and invoked Highcharts’s built-in series.addPoint
method. But in this case I’m adding a new series, and I was tired and I’d been sitting in the Big Red Chair for way too long. So you do it, and let me know how that works out.
No, seriously: please let me know.
Example code
(ns reagent-gp.views.intro (:require [reagent.core :as reagent] [cljsjs.highcharts] )) (defn chart-config [data-series] {:chart {:type "scatter"} :tooltip {:crosshairs [true,true]} :title {:text "Bill's xy Plot"} :xAxis {:title {:text "evaluation order"}} :yAxis {:title {:text "error"} :labels {:overflow "justify"}} :credits {:enabled false} :series data-series}) (defn some-random-pairs [howmany] (repeatedly howmany (fn [] [(inc (rand-int 1000)) (inc (rand-int 1000))]))) (defn append-series [old-vector new-data] (conj old-vector {:name (str "errors-" (inc (count old-vector))) :animation false :data (into [] new-data) })) (defonce every-series (reagent/atom (append-series [] (some-random-pairs 1000)))) (defn inner-charter [series-values] (reagent/create-class {:reagent-render (fn [] [:div [:div#mychart {:style {:min-width "310px" :max-width "800px" :background-color "lightgray" :height "500px" :margin "0 auto"}} ]]) :component-did-mount (fn [] (let [graphstate (clj->js (chart-config series-values))] (js/$ (fn [] (.highcharts (js/$ "#mychart") graphstate))) )) :component-did-update (fn [this] (let [[_ series-values] (reagent/argv this) graphstate (clj->js (chart-config series-values))] (js/$ (fn [] (.highcharts (js/$ "#mychart") graphstate))))) })) (defn outer-charter [series-atom] (fn [] [:div [:button.btn.btn-primary {:on-click #(swap! series-atom (fn [i] (append-series i (some-random-pairs 1000))))} "Add!"] [:p "Plotting " (count @series-atom) " series of " (count (:data (first @series-atom)))] [inner-charter @series-atom]] )) (defn intro-page [] [:div [:h1 "Landing page"] [:p "This is a simple plotter. Punch the button, it will make speckles...."] [outer-charter every-series] ])