├── .gitignore ├── project.clj ├── src └── repl_plot │ └── core.clj └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject repl-plot "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.5.1"]]) 7 | -------------------------------------------------------------------------------- /src/repl_plot/core.clj: -------------------------------------------------------------------------------- 1 | (ns repl-plot.core) 2 | 3 | (defn- data-range 4 | "Returns the min and max of the list as [min, max]" 5 | [xs] 6 | [(apply min xs) (apply max xs)]) 7 | 8 | (defn- range-step 9 | "Returns the step size between two spaces in the grid along an axis." 10 | [x-min x-max max-width] 11 | (/ (- x-max x-min) max-width)) 12 | 13 | (defn- draw-x-axis 14 | "Draws the x axis with the numbers running vertically" 15 | [x-display display-step max-width row-pre-print row-post-print] 16 | (let [intervals-to-display (map first (partition-all (int display-step) x-display))] 17 | (println (row-pre-print) (apply str (repeat (inc max-width) "-"))) 18 | (doseq [row-nums (partition (count intervals-to-display) 19 | (apply interleave intervals-to-display))] 20 | (print (row-pre-print) "") ; Need to add the "" for an extra space 21 | (doseq [n row-nums] 22 | (print (apply str n (repeat (dec display-step) " ")))) 23 | (print (row-post-print)"\n")))) 24 | 25 | (defn- find-closest 26 | "Find the closest number to n in the interval" 27 | [n interval] 28 | (first (apply min-key second (map (fn [k] [k (Math/abs (- k n))]) interval)))) 29 | 30 | (defn- x-y-to-matrix 31 | "Maps the xy pairs into an adjency list represented as a map, where the keys 32 | are [x,y] pairs and the values are what should be shown in that space. If a 33 | space is nil, nothing should be in that space." 34 | [xy-pairs] 35 | (into {} (map (fn [pair] [pair "*"]) xy-pairs))) 36 | 37 | (defn- draw-matrix 38 | "Given an adjency list, draw the spaces in the terminal." 39 | [x-intervals y-intervals xy-set row-pre-print row-post-print] 40 | (doseq [y (reverse y-intervals)] 41 | (row-pre-print y) 42 | (doseq [x x-intervals] 43 | (if (xy-set [x y]) 44 | (print (xy-set [x y])) 45 | (print " "))) 46 | (row-post-print y) 47 | (print "\n"))) 48 | 49 | (defn- displayify-intervals 50 | "Take intervals of doubles and turn them into strings we can display." 51 | [intervals precision prepend-padding?] 52 | (let [format-precision #(format (str "%." (int precision) "f") (double %)) 53 | str-intervals (map format-precision intervals) 54 | str-max (apply max (map count str-intervals)) 55 | prepend-true [#(apply str (repeat (- str-max (count %)) " ")) (fn [_] "")] 56 | [prepend-fn append-fn] (if prepend-padding? 57 | prepend-true 58 | (reverse prepend-true))] 59 | (map (fn [x] 60 | (str (prepend-fn x) x (append-fn x))) 61 | str-intervals))) 62 | 63 | (defn- intervalize 64 | [xs max-intervals] 65 | (let [[x-min x-max] (data-range xs) 66 | x-step (range-step x-min x-max max-intervals)] 67 | (range x-min (+ x-max x-step) x-step))) 68 | 69 | (defn plot 70 | "Plot a scatter plot in the terminal. xs and ys are expected to be lists of 71 | numbers where i-th number in xs is paired with the i-th number in ys. 72 | 73 | The display of the plot can be tuned by providing key-value pairs. 74 | :max-width is the number of columns to use in the terminal. 75 | :max-height is the number of rows to use in the terminal. 76 | :x-axis-display-step the number of spaces between x ticks on the axis 77 | :precision for the x and y axis, the number of decimal points to display" 78 | [xs ys & {:keys [max-width max-height x-axis-display-step precision] 79 | :or {max-width 60 max-height 30 x-axis-display-step 15 precision 1}}] 80 | (let [x-intervals (intervalize xs max-width) 81 | y-intervals (intervalize ys max-height) 82 | xs-intervalized (map #(find-closest % x-intervals) xs) 83 | ys-intervalized (map #(find-closest % y-intervals) ys) 84 | xy-pairs (partition 2 (interleave xs-intervalized ys-intervalized)) 85 | x-display (displayify-intervals x-intervals precision false) 86 | y-display (displayify-intervals y-intervals precision true) 87 | y->y-display (zipmap y-intervals y-display) 88 | xy-matrix (x-y-to-matrix xy-pairs)] 89 | (draw-matrix x-intervals y-intervals xy-matrix #(print (y->y-display %) "|" ) identity) 90 | (draw-x-axis x-display x-axis-display-step max-width (fn [] (apply str (repeat (inc (apply max (map count y-display))) " "))) (fn [] "")))) 91 | 92 | (defn hist 93 | "Plots a histogram into the terminal. data is expected to be a list of tuples, 94 | where the first is the category and the second is the count 95 | 96 | The display of the histogram can be tuned by providing key-value pairs. 97 | :max-items is the number of items we will display before scaling." 98 | [data & {:keys [max-items] :or {max-items 60}}] 99 | (let [max-count (apply max (map second data)) 100 | max-items (min max-count max-items) 101 | max-category-len (apply max (map (comp count str first) data)) 102 | max-count-len (apply max (map (comp count str second) data))] 103 | (doseq [[category cnt] data] 104 | (printf (str "%" max-category-len "s %" max-count-len "d %s\n") 105 | category 106 | cnt 107 | (clojure.string/join "" (repeat (* max-items (/ cnt max-count)) "#")))))) 108 | 109 | (comment (plot (repeatedly 100 #(rand 10)) (repeatedly 100 #(rand 10)) :max-width 60 :max-height 20 :display-step 15.0 :precision 1) 110 | (plot (range 0 1.05 0.1) (map #(* % %) (range 0 1.05 0.1)) :max-width 60.0 :max-height 30.0 :display-step 15 :precision 1) 111 | (hist [[:apples (rand-int 300)] [:bananas (rand-int 200)] [:oranges (rand-int 150)]])) 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # repl-plot 2 | 3 | A library that allows you to plot in the terminal. This is not meant to take this place of nice stylized graphs in your favorite plotting library. repl-plot is here for quick and dirty views of your data, in the repl, so you can refine your ideas. 4 | 5 | ## Set up 6 | 7 | Just add this line to your project.clj: 8 | 9 | [![Clojars Project](http://clojars.org/repl-plot/latest-version.svg)](http://clojars.org/repl-plot) 10 | 11 | ## Plotting 12 | 13 | ### Scatterplot 14 | For a basic scatterplot, simply supply repl-plot.core/plot with a list of xs and ys. 15 | ```clojure 16 | user=> (use 'repl-plot.core) 17 | nil 18 | user=> (let [xs (range 0 10 0.3) 19 | #_=> ys (map #(Math/sin %) xs)] 20 | #_=> (plot xs ys)) 21 | 1.1 | 22 | 1.0 | * * * * 23 | 0.9 | * * 24 | 0.9 | * * 25 | 0.8 | * * 26 | 0.7 | 27 | 0.7 | * * 28 | 0.6 | * 29 | 0.5 | * 30 | 0.5 | 31 | 0.4 | * * 32 | 0.3 | * 33 | 0.3 | * 34 | 0.2 | 35 | 0.1 | * * 36 | 0.1 | 37 | 0.0 |* * 38 | -0.1 | 39 | -0.1 | * 40 | -0.2 | * 41 | -0.3 | * 42 | -0.3 | 43 | -0.4 | 44 | -0.5 | * * 45 | -0.5 | * 46 | -0.6 | 47 | -0.7 | * 48 | -0.7 | 49 | -0.8 | * 50 | -0.9 | * 51 | -0.9 | * 52 | -1.0 | * * 53 | ------------------------------------------------------------- 54 | 0 2 5 7 9 55 | . . . . . 56 | 0 5 0 4 9 57 | ``` 58 | 59 | There are additional tuning parameters that adjust the look of your plot, here is an example using all of them. 60 | ```clojure 61 | user=> (let [xs (range 0 10 0.3) 62 | #_=> ys (map #(Math/sin %) xs)] 63 | #_=> (plot xs ys :max-width 75 :max-height 35 :x-axis-display-step 10 :precision 2)) 64 | 1.00 | * * * 65 | 0.94 | * * * 66 | 0.88 | * 67 | 0.83 | * 68 | 0.77 | * * 69 | 0.71 | 70 | 0.66 | * * 71 | 0.60 | * 72 | 0.54 | * 73 | 0.49 | 74 | 0.43 | * * 75 | 0.37 | 76 | 0.31 | * * 77 | 0.26 | 78 | 0.20 | 79 | 0.14 | * * 80 | 0.09 | 81 | 0.03 | * 82 | -0.03 |* 83 | -0.08 | 84 | -0.14 | * 85 | -0.20 | * 86 | -0.26 | * 87 | -0.31 | 88 | -0.37 | 89 | -0.43 | * 90 | -0.48 | * 91 | -0.54 | * 92 | -0.60 | 93 | -0.65 | 94 | -0.71 | * 95 | -0.77 | * 96 | -0.83 | 97 | -0.88 | * 98 | -0.94 | * 99 | -1.00 | * * 100 | ---------------------------------------------------------------------------- 101 | 0 1 2 3 5 6 7 9 102 | . . . . . . . . 103 | 0 3 6 9 2 6 9 2 104 | 0 2 4 6 8 0 2 4 105 | ``` 106 | Take a look at the doc string for repl-plot.core/plot for an explanation of each. 107 | 108 | Finally, it is worth mentioning that the plot can start to look wonky when you data is too dense with respect to the size of the grid (max-with x max-height). The reason for this is that continous points must be mapped to intervals. If the intervals are large, you may lose resolution of the data. 109 | 110 | ### Histogram 111 | 112 | A basic example of the histogram functionality: 113 | 114 | ```clojure 115 | user=> (hist [[:apples 5] [:bananas 2] [:oranges 1]]) 116 | :apples 5 ##### 117 | :bananas 2 ## 118 | :oranges 1 # 119 | ``` 120 | 121 | The histogram function will not bucket or sort for you, you are expected to do both. This is by design to give you the full flexibility in using the function. 122 | 123 | 124 | 125 | 126 | Copyright © 2015 127 | 128 | Distributed under the Eclipse Public License, the same as Clojure. 129 | --------------------------------------------------------------------------------