├── example.png ├── .gitignore ├── project.clj ├── LICENCE.txt ├── README.md └── src └── gg4clj └── core.clj /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/gg4clj/master/example.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | /.gorilla-port 11 | .idea 12 | .DS_Store 13 | *.iml 14 | Rplots.pdf 15 | 16 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | ;;;; This file is part of gg4clj. Copyright (C) 2014-, Jony Hudson. 2 | ;;;; 3 | ;;;; gg4clj is licenced to you under the MIT licence. See the file LICENCE.txt for full details. 4 | 5 | (defproject gg4clj "0.1.0" 6 | :description "A simple wrapper for R's ggplot2 in Clojure and Gorilla REPL." 7 | :url "https://github.com/JonyEpsilon/gg4clj" 8 | :license {:name "MIT"} 9 | :dependencies [[org.clojure/clojure "1.6.0"] 10 | [gorilla-renderable "1.0.0"]] 11 | :plugins [[lein-gorilla "0.3.4"]]) 12 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-, Jony Hudson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 6 | persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 9 | Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 12 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 14 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gg4clj 2 | 3 | gg4clj is a lightweight wrapper to make it easy to use R's ggplot2 library from Clojure. It provides a straightforward 4 | way to express R code as Clojure data, including easy mapping between Clojure data and R's data.frame, and some plumbing 5 | to send this code to R and recover the rendered graphics. It also provides a Gorilla REPL renderer plugin to allow 6 | rendered plots to be displayed inline in Gorilla worksheets. It is not a Clojure rewrite of ggplot2 - it calls R, which 7 | must be installed on your system (see below), to render the plots. You'll need to be familiar with R and ggplot2, or 8 | else the commands will seem fairly cryptic. 9 | 10 | An example, generating some random numbers in Clojure, and then plotting a 2D-density in ggplot: 11 | 12 | ```clojure 13 | (defn b-m 14 | [] 15 | (let [a (rand) 16 | b (rand) 17 | r (Math/sqrt (* -2 (Math/log a))) 18 | th (* 2 Math/PI b)] 19 | (* r (Math/cos th)))) 20 | 21 | (def g-dat {:g1 (repeatedly 50 b-m) :g2 (repeatedly 50 b-m)}) 22 | 23 | (gg4clj/view [[:<- :g (gg4clj/data-frame g-dat)] 24 | (gg4clj/r+ 25 | [:ggplot :g [:aes :g1 :g2]] 26 | [:xlim -2 2] 27 | [:ylim -2 2] 28 | [:geom_point {:colour "steelblue" :size 4}] 29 | [:stat_density2d {:colour "#FF29D2"}] 30 | [:theme_bw])] 31 | {:width 5 :height 5}) 32 | ``` 33 | 34 | ![Example plot](example.png) 35 | 36 | See more examples in the `ws/demo.clj` worksheet, which you can view here: 37 | 38 | http://viewer.gorilla-repl.org/view.html?source=github&user=JonyEpsilon&repo=gg4clj&path=ws/demo.clj 39 | 40 | 41 | ## Setup 42 | 43 | To use gg4clj in your project add it as a dependency to your `project.clj` file: 44 | 45 | ```clojure 46 | [gg4clj "0.1.0"] 47 | ``` 48 | 49 | You will need to have R installed, and on your path so it's accessible from the command line. If you can run `Rscript` 50 | from the command line, then you should be good to go. You will also need to make sure that the ggplot2 library is 51 | installed in R, which you can do by running the following in R (you only need to do this once): 52 | 53 | ```R 54 | install.packages("ggplot2") 55 | ``` 56 | 57 | ## Usage 58 | 59 | Central to gg4clj is a straightforward mapping of R code to Clojure data. There are only a few rules: 60 | 61 | - R symbols are represented by Clojure keywords. 62 | - R strings are represented by Clojure strings. 63 | - All other Clojure types, for instance numbers, will be `pr-str`ed and fed to R. 64 | - R function calls are represented by Clojure vectors, with the function name as the first element, and the (positional) 65 | arguments as subsequent elements. 66 | - The last argument to a function call can be a map, which is used to represent named arguments. 67 | 68 | So, for instance `[:qplot :mpg :hp {:data :mtcars :color [:factor :cyl]}]` translates to 69 | `qplot(mpg, hp, data = mtcars, color = factor(cyl))`. The function `to-r` can be used to show what R code is generated 70 | for a given data structure, and is useful for debugging. 71 | 72 | A few additional helper functions are provided for manipulating R code. The function `data-frame` takes a Clojure map, 73 | whose values are (equal length) seqs of data, and constructs an R data.frame with map keys as column names, 74 | and map values as corresponding column data. Row names can be passed as a `:row.names` entry in this map. Note that this 75 | function generates Clojure data corresponding to the R code that would generate the data.frame. This might seem 76 | confusing, but it makes sense when considering using it for plotting (see examples). 77 | 78 | ggplot2 makes extensive use of the `+` operator for adding layers etc. This is represented in Clojure data with a `:+` 79 | function i.e. `[:+ thing1 thing2]`. However, R's `+` operator is at most binary, so this is inconvenient for adding more 80 | than two things. To help with this gg4clj provides the `r+` function which will take any number of arguments, and 81 | construct the R code which adds them together. For example, `(gg4clj/r+ :a :b :c)` evaluates to `[:+ [:+ :a :b] :c]`. 82 | 83 | The function `render` does the work of sending code to R for evaluation. It takes a Clojure data structure, representing 84 | some R code and: 85 | 86 | - Prefixes it with code to load ggplot2. 87 | - Postfixes it with code to save the last generated plot to a temporary file. 88 | - Sends the code to R for evaluation, reads the generated plot, and cleans up. 89 | 90 | It returns a string which is the plot rendered as SVG. It can take options (currently `:width` and `:height`) to control 91 | the rendered output. Each evaluation is done in a new R process. 92 | 93 | Finally, the function `view` integrates the above into Gorilla REPL. It manages wrapping the generated plot for 94 | rendering in Gorilla, and takes care of attaching the Clojure form of the R code as the plot's alternate value, so that 95 | value copy-and-paste works seamlessly. It is called in the same manner as `render`. 96 | 97 | For examples of how to put all of the above functions together, please refer to the examples in the demo worksheet: 98 | 99 | http://viewer.gorilla-repl.org/view.html?source=github&user=JonyEpsilon&repo=gg4clj&path=ws/demo.clj 100 | 101 | 102 | ## License 103 | 104 | gg4clj is licensed to you under the MIT licence. See LICENCE.txt for details. 105 | 106 | Copyright © 2014- Jony Hudson -------------------------------------------------------------------------------- /src/gg4clj/core.clj: -------------------------------------------------------------------------------- 1 | ;;;; This file is part of gg4clj. Copyright (C) 2014-, Jony Hudson. 2 | ;;;; 3 | ;;;; gg4clj is licenced to you under the MIT licence. See the file LICENCE.txt for full details. 4 | 5 | (ns gg4clj.core 6 | (:import (java.io File) 7 | (java.util UUID)) 8 | (:require [clojure.java.shell :as shell] 9 | [clojure.string :as string] 10 | [gorilla-renderable.core :as render])) 11 | 12 | 13 | ;; * Functions for building R code * 14 | 15 | (declare to-r) 16 | 17 | (defn- quote-string 18 | "Wraps a string in escaped quotes." 19 | [st] 20 | (str "\"" st "\"")) 21 | 22 | (defn- function-name 23 | "R operators can be called in prefix form with a function name that is the quoted string 24 | of the operator name. This function handles a selection of the operators as special cases." 25 | [f] 26 | (case f 27 | :+ (quote-string "+") 28 | :<- (quote-string "<-") 29 | (name f))) 30 | 31 | (defn- fn-from-vec 32 | "An R function call is represented by a Clojure vector with the function name, given as a keyword, in 33 | the first element. Subsequent elements can be used to represent positional or named arguments (see below). 34 | This function transforms one of these function-call vectors into the equivalent R code, returned as a string." 35 | [vec] 36 | (str (function-name (first vec)) "(" 37 | (string/join ", " (map to-r (rest vec))) 38 | ")")) 39 | 40 | (defn- named-args-from-map 41 | "Named arguments to R functions are specified as Clojure maps. This function constructs the snippet of 42 | the argument string corresponding to the given named arguments. Note that the argument order may not be 43 | the same as specified when the map is created." 44 | [arg-map] 45 | (string/join ", " (map #(str (name %) " = " (to-r (% arg-map))) (keys arg-map)))) 46 | 47 | (defn r+ 48 | "A helper function for adding things together (i.e. ggplot2 layers). Call it with the arguments you want 49 | to add together, in the same manner as core/+." 50 | [& args] 51 | (reduce (fn [a b] [:+ a b]) args)) 52 | 53 | (defn to-r 54 | "Takes a Clojure representation of R code, and returns the corresponding R code as a string." 55 | [code] 56 | (cond 57 | ;; vectors are either function calls or lists of commands 58 | (vector? code) (if (vector? (first code)) 59 | (string/join ";\n" (map to-r code)) 60 | (fn-from-vec code)) 61 | (map? code) (named-args-from-map code) 62 | (keyword? code) (name code) 63 | (string? code) (quote-string code) 64 | true (pr-str code))) 65 | 66 | (defn data-frame 67 | "A helper function that takes frame-like data in the 'natural' Clojure format of 68 | {:key [vector of values] :key2 [vector ...] ...} and returns the Clojure representation 69 | of R code to make a data.frame." 70 | [data-map] 71 | [:data.frame 72 | (apply hash-map (mapcat (fn [e] [(key e) (into [:c] (val e))]) data-map))]) 73 | 74 | 75 | ;; * Functions for driving R * 76 | 77 | (defn- rscript 78 | "Execute a file of R code in a new R session. No output will be returned. If the R process exits abnormally, then the 79 | error output will be printed to the console." 80 | [script-path] 81 | (let [return-val (shell/sh "Rscript" "--vanilla" script-path)] 82 | ;; rscript is quite chatty, so only pass on err text if exit was abnormal 83 | (when (not= 0 (:exit return-val)) 84 | (println (:err return-val))))) 85 | 86 | 87 | ;; * Wrappers for ggplot2 functions * 88 | 89 | (defn- wrap-ggplot 90 | "Wraps the given R command with commands to load ggplot2 and save the last plot to the given file." 91 | [command filepath width height] 92 | (to-r 93 | [[:library :ggplot2] 94 | command 95 | [:ggsave {:filename filepath :width width :height height}]])) 96 | 97 | (defn- mangle-ids 98 | "ggplot produces SVGs with elements that have id attributes. These ids are unique within each plot, but are 99 | generated in such a way that they clash when there's more than one plot in a document. This function takes 100 | an SVG string and replaces the ids with globally unique ids. It returns a string. 101 | 102 | This is a workaround which could be removed if there was a way to generate better SVG in R. Also: 103 | http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454" 104 | [svg] 105 | (let [ids (map last (re-seq #"id=\"([^\"]*)\"" svg)) 106 | id-map (zipmap ids (repeatedly (count ids) #(str (UUID/randomUUID))))] 107 | (-> svg 108 | (string/replace #"id=\"([^\"]*)\"" #(str "id=\"" (get id-map (last %)) "\"")) 109 | (string/replace #"\"#([^\"]*)\"" #(str "\"#" (get id-map (last %)) "\"")) 110 | (string/replace #"url\(#([^\"]*)\)" #(str "url(#" (get id-map (last %)) ")"))))) 111 | 112 | (defn render 113 | "Takes a ggplot2 command, expressed in the Clojure representation of R code, and returns the plot rendered to SVG 114 | as a string. Options can be passed in a second argument, if wished, as a map. Supported options are :width of plot 115 | (in inches!) and :height. If only :width is given then a sensible default height will be chosen." 116 | ([plot-command] (render plot-command {})) 117 | ([plot-command options] 118 | (let [width (or (:width options) 6.5) 119 | height (or (:height options) (/ width 1.618)) 120 | r-file (File/createTempFile "gg4clj" ".r") 121 | r-path (.getAbsolutePath r-file) 122 | ;;_ (println r-path) 123 | out-file (File/createTempFile "gg4clj" ".svg") 124 | out-path (.getAbsolutePath out-file) 125 | _ (spit r-path (wrap-ggplot plot-command out-path width height)) 126 | _ (rscript r-path) 127 | rendered-plot (slurp out-path) 128 | _ (.delete r-file) 129 | _ (.delete out-file)] 130 | (mangle-ids rendered-plot)))) 131 | 132 | 133 | ;; * Gorilla REPL rendering * 134 | 135 | (defrecord GGView [plot-command options]) 136 | 137 | ;; This renderer displays the rendered SVG output, and attaches the plot-command (in Clojure) as 138 | ;; the rendered item's value. 139 | (extend-type GGView 140 | render/Renderable 141 | (render [self] 142 | {:type :html :content (render (:plot-command self) (:options self)) :value (pr-str self)})) 143 | 144 | (defn view 145 | "View a ggplot2 command, expressed in the Clojure representation of R code, in Gorilla REPL. Options can be passed 146 | in a second argument, if wished, as a map. Supported options are :width of plot (in inches!) and :height. If only 147 | :width is given then a sensible default height will be chosen." 148 | ([plot-command] (view plot-command {})) 149 | ([plot-command options] 150 | (GGView. plot-command options))) --------------------------------------------------------------------------------