Search code examples
clojureclojurescriptreagentre-frame

Google Chart CLJS Clojure


I tried to adapt this example in Google Chart. To re-frame framework, reagent. I would like to create a real-time chart, based on subscriptions. I tested with a simple counter =+-1.

I got error: Assert failed: Render must be a function, not nil (ifn? render-fun).

(defn draw-demo-chart 
   [d]
   (let [[columns vectors options chart] (r/children d)
         data (new js/google.visualization.DataTable)]
       (doall ;gotta keep the doall on maps. lazy sequence...
      (map (fn [[type name]]
            (.addColumn data type name)) columns))
      (.addRows data vectors)
      (.draw chart data options)
      (.load js/google "visualization" "1" (clj->js {:packages ["corechart" "orgchart" "calendar" "map" "geochart"]}))     
      (.setOnLoadCallback js/google draw-demo-chart)
      ))


(defn draw-demo-chart-container
    []
    (let [count    (re-frame/subscribe [:count])
          columns  (reaction [["date" "X"] ["number" "Y"]])
          vectors  (reaction (clj->js [[(new js/Date "07/11/14") 145] [(new js/Date "07/12/14") 15]
                                      [(new js/Date "07/13/14") 23] [(new js/Date "07/14/14") 234]]))
          options  (reaction (clj->js {:title (str @count)}))
          chart    (reaction (new js/google.visualization.LineChart (.getElementById js/document "linechart"))) ]
     (fn []
        [draw-demo-graph @columns @vectors @options @chart])))

(def draw-demo-graph 
       (r/create-class {:reagent-render  draw-demo-chart
                        :component-did-mount draw-demo-chart
                        :component-did-update draw-demo-chart}))

Solution

  • There are several challenges to using the Google Charts API:

    1. It loads asynchronously and can only be used when ready.

    I suggest using a flag to record whether the API is ready or not, this will allow it to render even if the API load after the component is mounted.

    (defonce ready?
      (reagent/atom false))
    
    (defonce initialize
      (do
        (js/google.charts.load (clj->js {:packages ["corechart"]}))
        (js/google.charts.setOnLoadCallback
          (fn google-visualization-loaded []
            (reset! ready? true)))))
    
    1. You need to call draw on a HTML element:

    The HTML Element will only exist if the component has mounted. You could use a ref to conveniently get the HTML element (otherwise you'll need to either save a reference to in on mount, or search for it).

    (defn draw-chart [chart-type data options]
      [:div
       (if @ready?
         [:div
          {:ref
           (fn [this]
             (when this
               (.draw (new (aget js/google.visualization chart-type) this)
                      (data-table data)
                      (clj->js options))))}]
         [:div "Loading..."])])
    

    You'll want to redraw any time any of the inputs change (which the above ref example does).

    1. Setting up the data source

    I suggest a convenience method for getting the data source:

    (defn data-table [data]
      (cond
        (map? data) (js/google.visualization.DataTable. (clj->js data))
        (string? data) (js/google.visualization.Query. data)
        (seqable? data) (js/google.visualization.arrayToDataTable (clj->js data))))
    
    1. Use it

    Now you can use your chart with reactive values!

    [draw-chart
        "LineChart"
        @some-data
        {:title (str "Clicks as of day " @day)}]
    

    Full code listing is at https://github.com/timothypratley/google-chart-example