Graphing in Clojure

Introduction

The problem

I was writing a Clojure Swing application [using seesaw]](https://github.com/clj-commons/seesaw) that would benefit from a basic line graph. This was an annoying problem for me because I didn’t want to try and do that by hand. Fortunately it turns out that not only does Java have a plotting library but it is actually remarkably good for easy things (not quite up to the standard of Hadley Wickham’s ggplot, but not much is). There is also the Clojure library Incanter but I didn’t explore it.

Dependencies

Debian testing, 2024-03-26.

$ sudo apt install libjfreechart-java
$ dpkg -L libjfreechart-java | grep jar
/usr/share/java/jfreechart-1.0.19.jar
/usr/share/java/jfreechart.jar
/usr/share/maven-repo/org/jfree/jfreechart/1.0.19/jfreechart-1.0.19.jar
/usr/share/maven-repo/org/jfree/jfreechart/debian/jfreechart-debian.jar

project.clj:

(defproject demo "1.0.0-SNAPSHOT"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.11.2"]
                 ;; Swing GUI library
                 [seesaw              "1.5.0"]]
  :main demo.core
  ;; Dependency on JFreeChart
  :resource-paths ["/usr/share/maven-repo/org/jfree/jfreechart/debian/jfreechart-debian.jar" "resources/"]
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})

Creating a Chart

Namespace

(ns demo.core
  (:require
   [seesaw.core :as seesaw]
   [seesaw.mig :as mig])
  (:import
   [org.jfree.chart ChartPanel JFreeChart]
   [org.jfree.chart.plot XYPlot]
   [org.jfree.chart.axis NumberAxis NumberTickUnit]
   [org.jfree.data.xy DefaultXYDataset]
   [org.jfree.chart.renderer.xy DefaultXYItemRenderer]))

Data structures

JFreeChart requires us to create a few things:

Dataset

(defn build-dataset-backing-array
  [series]
  (into-array (map double-array [(-> series count range) series])))

This was the part that was trickiest. If we want to plot some series (say [5 2 3 6]) then we need a 2D structure with the range and range specified [#_domain [1 2 3 4] #_range [5 2 3 6]] - but structured as a 2D double array. Clojure doesn’t give us tools to directly create a 2D array, but it turns out to be possible by creating 2xdouble arrays in the above fashion. I chose to add the array after constructing the dataset using addSeries. This seems to be the standard way since I only see a plain constructor - it may have been the only choice.

Thanks Fogus for explaining to everyone how that is done.

The design here by JFreeChart impressed me. This level of separation between display and data is well advised in a charting library and the interface looks to be quite flexible in the face of odd situations. It would be easy to create an XYPlot that coupled the data and the plot itself but someone here has gone the extra distance to keep things organised. There is also a balance between boilerplate and simplicity that seemed well navigated by this API.

The Chart Itself

(defn build-chart
  []
  (ChartPanel.
   (JFreeChart.
    (XYPlot. (DefaultXYDataset.)
             (doto (NumberAxis.) (.setTickUnit (NumberTickUnit. 1.0)))
             (doto (NumberAxis.) (.setTickUnit (NumberTickUnit. 1.0)))
             (DefaultXYItemRenderer.)))))

nil is acceptable for the arguments to XYPlot, but I started out with a nil renderer and the data didn’t render. Being able to pass nil seems like a red herring here.

Displaying the Chart

I’m experimenting with to creating maps like this:

#__(def ui 
  {:plot plot
   :chart chart 
   :panel chart-panel})

To store UIs, then passing them around in lieu of using a global variable. It seems to work well - individual components are easy to get to and often there is a :layout in the map to talk about the whole blob. I don’t break the chart up (into separate panel/plot/dataset) in this case because this is a limited graph, so when I want to update the dataset I’ll reach in through the components.

(defn build-ui 
  [] 
  (let [chart 
        (build-chart)]
  {:chart-panel chart 
   :layout (mig/mig-panel :items [[(seesaw/label "Demo Series") "wrap"]
                                  [chart ""]])}))
  
  
(defonce f 
  (seesaw/frame :title "Demo Frame"
                :visible? false))
  
(defn -main 
  [& _args]
  (let [ui (build-ui)]
    (-> ui :chart-panel .getChart .getPlot .getDataset (.addSeries "Data" (build-dataset-backing-array [5 2 3 6])))
    ;; Multiple series supported like this
    ;; (-> ui :chart-panel .getChart .getPlot .getDataset (.addSeries "Data2" (build-dataset-backing-array [6 3 2 5])))
    (seesaw/config! f :content (:layout ui))
    (seesaw/show! f)))
    
(-main)
The Chart

Appendix A - Plot v. Chart v. Graph

I was worried that using plot-graph-chart interchangeably here was not technically correct. Apparently a line graph is at once a plot, a graph and a chart so that use is acceptable.