Permalink
20d25b7 Sep 19, 2016
2 contributors

Users who have contributed to this file

@postspectacular @acron0
1851 lines (1572 sloc) 74.4 KB

Contents

Namespace: thi.ng.geom.viz.core

This module currently consists of this single namespace dedicated to creating 2D (and soon 3D too) data visualizations in an output format agnostic way. To achieve that, an overall declarative and pipelined approach has been taken to create these visualizations:

  1. We first define a configuration map, supplying all data points, axis definitions, layout arguments/handlers, styling options etc.
  2. This map is then transformed into a tree/scenegraph of geometric primitives (shapes & groups) representing the visualization.
  3. This tree of still pure Clojure data is then further converted into the final output format, e.g. for SVG first into hiccup and then actual SVG/XML…

The declarative nature has several benefits:

  • place multiple data series w/ potentially different layout methods into same visualization
  • create template/preset specs (e.g. pre-styled, only inject new data points)
  • easy to integrate in central state atom pattern, compatible w/ Om/Reagent based setups
  • support multiple output targets from the same visualization spec

Apart from SVG (the only target supported at the moment), this module also aims to support WebGL scenegraph generation and thi.ng/luxor scene exports for rendering visualizations w/ Luxrender.

Example usage

This section shows some basic example outputs and general usage patterns for all implemented visualization methods. The section after then describes the various options in more detail.

Running all examples from REPL

Once you’ve tangled this document (see index.org), the following examples can be found in the /babel/examples folder of this module. Launching a REPL from within the babel directory, all examples can be run with this command and will create a number of SVG files in the same directory.

(doseq [f (rest (file-seq (java.io.File. "examples")))] (load-file (.getAbsolutePath f)))

Namespaces required by all examples

(require '[thi.ng.geom.viz.core :as viz] :reload)
(require '[thi.ng.geom.svg.core :as svg])
(require '[thi.ng.geom.core.vector :as v])
(require '[thi.ng.color.core :as col])
(require '[thi.ng.math.core :as m :refer [PI TWO_PI]])

Scatter plot

http://media.thi.ng/geom/viz/scatter-linear-3.svghttp://media.thi.ng/geom/viz/scatter-log-3.svg
Logarithmic X-axis, linear YLog X, Log Y
<<example-imports>>

(defn export-viz
  [spec path]
  (->> spec
       (viz/svg-plot2d-cartesian)
       (svg/svg {:width 600 :height 600})
       (svg/serialize)
       (spit path)))

(def spec
  {:x-axis (viz/log-axis
            {:domain [1 201]
             :range  [50 590]
             :pos    550})
   :y-axis (viz/linear-axis
            {:domain      [0.1 100]
             :range       [550 20]
             :major       10
             :minor       5
             :pos         50
             :label-dist  15
             :label-style {:text-anchor "end"}})
   :grid   {:attribs {:stroke "#caa"}
            :minor-x true
            :minor-y true}
   :data   [{:values  (map (juxt identity #(Math/sqrt %)) (range 0 200 2))
             :attribs {:fill "#0af" :stroke "none"}
             :layout  viz/svg-scatter-plot}
            {:values  (map (juxt identity #(m/random %)) (range 0 200 2))
             :attribs {:fill "none" :stroke "#f60"}
             :shape   (viz/svg-triangle-down 6)
             :layout  viz/svg-scatter-plot}]})

(export-viz spec "scatter-linear.svg")

(-> spec
    (assoc :y-axis (viz/log-axis
                    {:domain      [0.1 101]
                     :range       [550 20]
                     :pos         50
                     :label-dist  15
                     :label-style {:text-anchor "end"}}))
    (export-viz "scatter-log.svg"))

Line & area plot

http://media.thi.ng/geom/viz/lineplot-3.svghttp://media.thi.ng/geom/viz/areaplot-3.svg
Line plot (cartesian)Area plot (cartesian)
<<example-imports>>

(defn test-equation
  [t] (let [x (m/mix (- PI) PI t)] [x (* (Math/cos (* 0.5 x)) (Math/sin (* x x x)))]))

(defn export-viz
  [viz path] (->> viz (svg/svg {:width 600 :height 320}) (svg/serialize) (spit path)))

(def viz-spec
  {:x-axis (viz/linear-axis
            {:domain [(- PI) PI]
             :range  [50 580]
             :major  (/ PI 2)
             :minor  (/ PI 4)
             :pos    250})
   :y-axis (viz/linear-axis
            {:domain      [-1 1]
             :range       [250 20]
             :major       0.2
             :minor       0.1
             :pos         50
             :label-dist  15
             :label-style {:text-anchor "end"}})
   :grid   {:attribs {:stroke "#caa"}
            :minor-y true}
   :data   [{:values  (map test-equation (m/norm-range 200))
             :attribs {:fill "none" :stroke "#0af"}
             :layout  viz/svg-line-plot}]})

(-> viz-spec
    (viz/svg-plot2d-cartesian)
    (export-viz "lineplot.svg"))

;; same spec, just update style attribs & layout method
(-> viz-spec
    (update-in [:data 0] merge {:attribs {:fill "#0af"} :layout viz/svg-area-plot})
    (viz/svg-plot2d-cartesian)
    (export-viz "areaplot.svg"))

Same overall visualization setup, only using polar coordinate transform and redefined axis ranges (in radians)…

http://media.thi.ng/geom/viz/lineplot-polar-3.svghttp://media.thi.ng/geom/viz/areaplot-polar-3.svg
Line plot (polar)Area plot (polar)
(def viz-spec-polar
  {:x-axis (viz/linear-axis
            {:domain [(- PI) PI]
             :range  [(* 1.1 PI) (* 1.9 PI)]
             :major  (/ PI 2)
             :minor  (/ PI 16)
             :pos    280})
   :y-axis (viz/linear-axis
            {:domain [-1 1]
             :range  [60 280]
             :major  0.5
             :minor  0.25
             :pos    (* 1.1 PI)})
   :origin (v/vec2 300 310)
   :grid   {:attribs {:stroke "#caa" :fill "none"}
            :minor-x true
            :minor-y true}
   :data   [{:values  (map test-equation (m/norm-range 200))
             :attribs {:fill "none" :stroke "#0af"}
             :layout  viz/svg-line-plot}]})

(-> viz-spec-polar (viz/svg-plot2d-polar) (export-viz "lineplot-polar.svg"))

;; same spec, just update style attribs & layout method
(-> viz-spec-polar
    (update-in [:data 0] merge {:attribs {:fill "#0af"} :res 20 :layout viz/svg-area-plot})
    (viz/svg-plot2d-polar)
    (export-viz "areaplot-polar.svg"))

Bar graph

http://media.thi.ng/geom/viz/bars-3.svghttp://media.thi.ng/geom/viz/bars-interleave-3.svg
Single value per domain position3 interleaved values (datasets) per domain position
<<example-imports>>

(defn export-viz
  [viz path] (->> viz (svg/svg {:width 600 :height 320}) (svg/serialize) (spit path)))

(defn bar-spec
  [num width]
  (fn [idx col]
    {:values     (map (fn [i] [i (m/random 100)]) (range 2000 2016))
     :attribs    {:stroke       col
                  :stroke-width (str (dec width) "px")}
     :layout     viz/svg-bar-plot
     :interleave num
     :bar-width  width
     :offset     idx}))

(def viz-spec
  {:x-axis (viz/linear-axis
            {:domain [1999 2016]
             :range  [50 580]
             :major  1
             :pos    280
             :label  (viz/default-svg-label int)})
   :y-axis (viz/linear-axis
            {:domain      [0 100]
             :range       [280 20]
             :major       10
             :minor       5
             :pos         50
             :label-dist  15
             :label-style {:text-anchor "end"}})
   :grid   {:minor-y true}})

(-> viz-spec
    (assoc :data [((bar-spec 1 20) 0 "#0af")])
    (viz/svg-plot2d-cartesian)
    (export-viz "bars.svg"))

(-> viz-spec
    (assoc :data (map-indexed (bar-spec 3 6) ["#0af" "#fa0" "#f0a"]))
    (viz/svg-plot2d-cartesian)
    (export-viz "bars-interleave.svg"))

Radar plot

http://media.thi.ng/geom/viz/radarplot-3.svghttp://media.thi.ng/geom/viz/radarplot-minmax.svg
6 categories, 3 data sets, single values6 categories, 3 data sets, min-max intervals
<<example-imports>>

(def category->domain (zipmap [:C1 :C2 :C3 :C4 :C5 :C6] (range)))
(def domain->category (reduce-kv #(assoc % %3 %2) {} category->domain))

(defn random-radar-spec
  "Generates radar plot data spec w/ random values for each category in the form:
   {:C1 0.8 :C2 0.2 ...}"
  [color]
  {:values   (zipmap (keys category->domain) (repeatedly #(m/random 0.25 1)))
   :item-pos (fn [[k v]] [(category->domain k) v])
   :attribs  {:fill (col/rgba color)}
   :layout   viz/svg-radar-plot})

(defn random-radar-spec-minmax
  "Generates radar plot data spec w/ random value intervals for each category in the form:
   {:C1 [0.5 0.8] :C2 [0.12 0.2] ...}"
  [color]
  {:values       (zipmap
                  (keys category->domain)
                  (repeatedly #(let [x (m/random 0.5 1)] [(* x (m/random 0.25 0.75)) x])))
   :item-pos-min (fn [[k v]] [(category->domain k) (first v)])
   :item-pos-max (fn [[k v]] [(category->domain k) (peek v)])
   :attribs      {:fill (col/rgba color)}
   :layout       viz/svg-radar-plot-minmax})

(def viz-spec
  {:x-axis (viz/linear-axis
            {:domain     [0 5]
             :range      [0 (* (/ 5 6) TWO_PI)]
             :major      1
             :label-dist 20
             :pos        260
             :label      (viz/default-svg-label (comp name domain->category))})
   :y-axis (viz/linear-axis
            {:domain      [0 1.05]
             :range       [0 260]
             :major       0.5
             :minor       0.1
             :pos         (/ PI 2)
             :label-style {:text-anchor "start"}
             :label       (viz/default-svg-label viz/format-percent)})
   :grid   {:minor-x true :minor-y true}
   :origin (v/vec2 300 300)
   :circle true})

(->> (assoc viz-spec :data (mapv random-radar-spec [[0 0.66 1 0.33] [1 0.5 0 0.33] [1 0 0.8 0.33]]))
     (viz/svg-plot2d-polar)
     (svg/svg {:width 600 :height 600})
     (svg/serialize)
     (spit "radarplot.svg"))

(->> (assoc viz-spec :data (mapv random-radar-spec-minmax [[0 0.66 1 0.33] [1 0.5 0 0.33] [1 0 0.8 0.33]]))
     (viz/svg-plot2d-polar)
     (svg/svg {:width 600 :height 600})
     (svg/serialize)
     (spit "radarplot-minmax.svg"))

Stacked intervals

Plain intervals

http://media.thi.ng/geom/viz/intervals-3.svg

<<example-imports>>

(->> {:x-axis (viz/linear-axis
               {:domain [-10 310]
                :range  [50 550]
                :major  100
                :minor  50
                :pos    150})
      :y-axis (viz/linear-axis
               {:domain  [0 4]
                :range   [50 150]
                :visible false})
      :data   [{:values  [[0 100] [10 90] [80 200] [250 300] [150 170] [110 120]
                          [210 280] [180 280] [160 240] [160 170]]
                :attribs {:stroke-width "10px" :stroke-linecap "round" :stroke "#0af"}
                :layout  viz/svg-stacked-interval-plot}]}
     (viz/svg-plot2d-cartesian)
     (svg/svg {:width 600 :height 200})
     (svg/serialize)
     (spit "intervals.svg"))

Categorized timeline

This more complex example shows how to use structured data (here project descriptions) to create a timeline and visualize each item using a custom shape function w/ linear gradients (based on item type).

http://media.thi.ng/geom/viz/timeline-3.svg

<<example-imports>>
(require '[thi.ng.color.core :as col])
(import '[java.util Calendar GregorianCalendar])

(def items
  [{:title "toxiclibs"          :from #inst "2006-03" :to #inst "2013-06" :type :oss}
   {:title "thi.ng/geom"        :from #inst "2011-08" :to #inst "2015-10" :type :oss}
   {:title "thi.ng/trio"        :from #inst "2012-12" :to #inst "2015-06" :type :oss}
   {:title "thi.ng/fabric"      :from #inst "2014-12" :to #inst "2015-09" :type :oss}
   {:title "thi.ng/simplecl"    :from #inst "2012-10" :to #inst "2013-06" :type :oss}
   {:title "thi.ng/raymarchcl"  :from #inst "2013-02" :to #inst "2013-05" :type :oss}
   {:title "thi.ng/structgen"   :from #inst "2012-10" :to #inst "2013-02" :type :oss}
   {:title "thi.ng/luxor"       :from #inst "2013-10" :to #inst "2015-06" :type :oss}
   {:title "thi.ng/morphogen"   :from #inst "2014-03" :to #inst "2015-06" :type :oss}
   {:title "thi.ng/color"       :from #inst "2014-09" :to #inst "2015-10" :type :oss}
   {:title "thi.ng/validate"    :from #inst "2014-05" :to #inst "2015-06" :type :oss}
   {:title "thi.ng/ndarray"     :from #inst "2015-05" :to #inst "2015-06" :type :oss}
   {:title "thi.ng/tweeny"      :from #inst "2013-10" :to #inst "2015-01" :type :oss}
   {:title "Co(De)Factory"      :from #inst "2013-12" :to #inst "2014-08" :type :project}
   {:title "Chrome WebLab"      :from #inst "2011-05" :to #inst "2012-11" :type :project}
   {:title "ODI"                :from #inst "2013-07" :to #inst "2013-10" :type :project}
   {:title "LCOM"               :from #inst "2012-06" :to #inst "2013-05" :type :project}
   {:title "V&amp;A Ornamental" :from #inst "2010-12" :to #inst "2011-05" :type :project}
   {:title "Engine26"           :from #inst "2010-08" :to #inst "2010-12" :type :project}
   {:title "Resonate"           :from #inst "2012-04" :to #inst "2012-04" :type :workshop}
   {:title "Resonate"           :from #inst "2013-03" :to #inst "2013-03" :type :workshop}
   {:title "Resonate"           :from #inst "2014-04" :to #inst "2014-04" :type :workshop}
   {:title "Resonate"           :from #inst "2015-04" :to #inst "2015-04" :type :workshop}
   {:title "Resonate"           :from #inst "2012-04" :to #inst "2012-04" :type :talk}
   {:title "Resonate"           :from #inst "2013-03" :to #inst "2013-03" :type :talk}
   {:title "Resonate"           :from #inst "2014-04" :to #inst "2014-04" :type :talk}
   {:title "Resonate"           :from #inst "2015-04" :to #inst "2015-04" :type :talk}
   {:title "Retune"             :from #inst "2014-09" :to #inst "2014-09" :type :talk}
   {:title "Bezalel"            :from #inst "2011-04" :to #inst "2011-04" :type :workshop}
   {:title "V&amp;A"            :from #inst "2011-01" :to #inst "2011-03" :type :workshop}
   {:title "HEAD"               :from #inst "2010-10" :to #inst "2010-10" :type :workshop}
   {:title "ETH"                :from #inst "2010-11" :to #inst "2010-11" :type :workshop}
   {:title "SAC"                :from #inst "2012-11" :to #inst "2012-11" :type :workshop}
   {:title "SAC"                :from #inst "2014-12" :to #inst "2014-12" :type :workshop}
   {:title "MSA"                :from #inst "2013-04" :to #inst "2013-04" :type :workshop}
   {:title "Young Creators"     :from #inst "2014-06" :to #inst "2014-06" :type :workshop}
   {:title "EYEO"               :from #inst "2013-06" :to #inst "2013-06" :type :talk}
   {:title "Reasons"            :from #inst "2014-02" :to #inst "2014-02" :type :talk}
   {:title "Reasons"            :from #inst "2014-09" :to #inst "2014-09" :type :talk}])

(def item-type-colors {:project "#0af" :oss "#63f" :workshop "#9f0" :talk "#f9f"})

(def month (* (/ (+ (* 3 365) 366) 4.0 12.0) 24 60 60 1000))
(def year  (* month 12))

(defn ->epoch [^java.util.Date d] (.getTime d))

;; http://stackoverflow.com/questions/9001384/java-date-rounding
(defn round-to-year
  [epoch]
  (let [cal (GregorianCalendar.)]
    (doto cal
      (.setTimeInMillis (long epoch))
      (.add Calendar/MONTH 6)
      (.set Calendar/MONTH 0)
      (.set Calendar/DAY_OF_MONTH 1)
      (.set Calendar/HOUR 0)
      (.set Calendar/MINUTE 0)
      (.set Calendar/SECOND 0)
      (.set Calendar/MILLISECOND 0))
    (.get cal Calendar/YEAR)))

(defn make-gradient
  [[id base]]
  (let [base (col/as-hsva (col/css base))]
    (svg/linear-gradient
     id {} [0 base] [1 (col/adjust-saturation base -0.66)])))

(defn item-range [i] [(->epoch (:from i)) (->epoch (:to i))])

(defn timeline-spec
  [type offset]
  {:values     (if type (filter #(= type (:type %)) items) items)
   :offset     offset
   :item-range item-range
   :attribs    {:fill "white"
                :stroke "none"
                :font-family "Arial"
                :font-size 10}
   :shape      (viz/labeled-rect-horizontal
                {:h         14
                 :r         7
                 :min-width 30
                 :base-line 3
                 :label     :title
                 :fill      #(str "url(#" (name (:type %)) ")")})
   :layout     viz/svg-stacked-interval-plot})

;; Create stacked timeline with *all* items
(->> {:x-axis (viz/linear-axis
               {:domain [(->epoch #inst "2010-09") (->epoch #inst "2015-06")]
                :range  [10 950]
                :pos    160
                :major  year
                :minor  month
                :label  (viz/default-svg-label round-to-year)})
      :y-axis (viz/linear-axis
               {:domain  [0 9]
                :range   [10 160]
                :visible false})
      :grid   {:minor-x true}
      :data   [(timeline-spec nil 0)]}
     (viz/svg-plot2d-cartesian)
     (svg/svg
      {:width 960 :height 200}
      (apply svg/defs (map make-gradient item-type-colors)))
     (svg/serialize)
     (spit "timeline.svg"))

We can also group items by their :type property and arrange them separately along the Y-axis. This creates a less compact result, but better legibility. The example also shows how to re-use visualization spec fragments (via the use of our timeline-spec fn).

http://media.thi.ng/geom/viz/timeline-separate-3.svg

;; Create stacked timeline vertically grouped by item type
(->> {:x-axis (viz/linear-axis
               {:domain [(->epoch #inst "2010-09") (->epoch #inst "2015-06")]
                :range  [10 950]
                :pos    220
                :major  year
                :minor  month
                :label  (viz/default-svg-label round-to-year)})
      :y-axis (viz/linear-axis
               {:domain  [0 13]
                :range   [10 220]
                :visible false})
      :grid   {:minor-x true}
      :data   [(timeline-spec :project 0)
               (timeline-spec :oss 2)
               (timeline-spec :workshop 10)
               (timeline-spec :talk 11)]}
     (viz/svg-plot2d-cartesian)
     (svg/svg
      {:width 960 :height 245}
      (apply svg/defs (map make-gradient item-type-colors)))
     (svg/serialize)
     (spit "timeline-separate.svg"))

Heatmap

http://media.thi.ng/geom/viz/hm-rainbow2.svghttp://media.thi.ng/geom/viz/hm-orange-blue.svg
:rainbow2 gradient preset:orange-blue gradient preset

This demo uses procedural gradients provided by thi.ng/color. See link for list of available presets & how to define new gradients.

<<example-imports>>
(require '[thi.ng.color.gradients :as grad])
(require '[thi.ng.geom.core :as g])
(require '[thi.ng.geom.core.utils :as gu])
(require '[thi.ng.math.simplexnoise :as n])

(def test-matrix
  (->> (for [y (range 10) x (range 50)] (n/noise2 (* x 0.1) (* y 0.25)))
       (viz/matrix-2d 50 10)))

(defn heatmap-spec
  [id]
  {:matrix        test-matrix
   :value-domain  (viz/value-domain-bounds test-matrix)
   :palette       (->> id (grad/cosine-schemes) (apply grad/cosine-gradient 100))
   :palette-scale viz/linear-scale
   :layout        viz/svg-heatmap})

(defn cartesian-viz
  [prefix id & [opts]]
  (->> {:x-axis (viz/linear-axis
                 {:domain [0 50]
                  :range [50 550]
                  :major 10
                  :minor 5
                  :pos 280})
        :y-axis (viz/linear-axis
                 {:domain      [0 10]
                  :range       [280 20]
                  :major       1
                  :pos         50
                  :label-dist  15
                  :label-style {:text-anchor "end"}})
        :data   [(merge (heatmap-spec id) opts)]}
       (viz/svg-plot2d-cartesian)
       (svg/svg {:width 600 :height 300})
       (svg/serialize)
       (spit (str prefix "-" (name id) ".svg"))))

(cartesian-viz "hm" :rainbow2)
(cartesian-viz "hm" :orange-blue)
http://media.thi.ng/geom/viz/hmp-yellow-magenta-cyan.svghttp://media.thi.ng/geom/viz/hmp-green-magenta.svg
:yellow-magenta-cyan, polar projection:green-magenta, polar projection
(defn polar-viz
  [prefix id & [opts]]
  (->> {:x-axis (viz/linear-axis
                 {:domain [0 50]
                  :range [(* 1.1 PI) (* 1.9 PI)]
                  :major 10
                  :minor 5
                  :pos 280})
        :y-axis (viz/linear-axis
                 {:domain     [0 10]
                  :range      [90 280]
                  :major      5
                  :pos        (* 1.1 PI)
                  :major-size 10
                  :label-dist 20})
        :origin (v/vec2 300)
        :data   [(merge (heatmap-spec id) opts)]}
       (viz/svg-plot2d-polar)
       (svg/svg {:width 600 :height 320})
       (svg/serialize)
       (spit (str prefix "-" (name id) ".svg"))))

(polar-viz "hmp" :yellow-magenta-cyan)
(polar-viz "hmp" :green-magenta)
http://media.thi.ng/geom/viz/hms-rainbow2.svghttp://media.thi.ng/geom/viz/hmsp-rainbow2.svg
:rainbow2 w/ custom shape fn:rainbow2, polar projection, custom shape fn
;; using custom shape function applied for each matrix cell
;; (a circle fitting within the 4 points defining a grid cell)
(cartesian-viz "hms" :rainbow2 {:shape viz/circle-cell})
(polar-viz "hmsp" :rainbow2 {:shape viz/circle-cell})

Github commit history

Note: This example requires raynes/tentacles 0.3.0 to be added to your project in order to download the commit history of a given project.

This example downloads the commit history for this project from GitHub and produces a similar activity heatmap as shown on GH user pages (each column = 1 week).

Btw. You can use the local repo by switching the lines calling load-commits-fs and load-commits-gh

http://media.thi.ng/geom/viz/commit-history.svg

(require '[tentacles.repos :as repos])
(require '[thi.ng.ndarray.core :as nd])
(require '[clojure.string :as str])
(require '[clojure.java.shell :refer [sh]])

(def day         (* 24 60 60 1000))
(def week        (* 7 day))
(def fmt-iso8601 (java.text.SimpleDateFormat. "yyyy-MM-dd'T'HH:mm:ssX"))
(def fmt-month   (java.text.SimpleDateFormat. "MMM"))
(def fmt-year    (java.text.SimpleDateFormat. "yyyy"))
(def ->epoch #(try (.getTime (.parse fmt-iso8601 %)) (catch Exception e)))

(defn month-or-year
  [from]
  #(let [d (java.util.Date. (long (+ from (* % week))))]
     (.format (if (zero? (.getMonth d)) fmt-year fmt-month) d)))

(defn load-commits-gh
  [user repo]
  (prn (str "loading GH commit history: " user "/" repo))
  (->> (repos/commits user repo {:all-pages true})
       (map #(->epoch (get-in % [:commit :author :date])))
       (filter identity)))

(defn load-commits-fs
  [repo-path]
  (->> (sh "git" "log" "--pretty=format:%aI" :dir repo-path)
       :out
       str/split-lines
       (map ->epoch)
       (filter identity)))

(defn commits-per-week-day
  [t0 commits]
  (->> (for [c commits
             :let [t (- c t0)
                   w (int (/ t week))
                   d (int (/ (rem t week) day))]]
         [w d])
       (frequencies)
       (sort-by first)))

(defn commits->matrix
  [commits]
  (let [weeks (inc (- (ffirst (last commits)) (first (ffirst commits))))
        mat (nd/ndarray :int8 (byte-array (* 7 weeks)) [7 weeks])]
    (doseq [[[w d] n] commits] (nd/set-at mat d w n))
    mat))

(let [commits   (load-commits-fs ".")
      ;;commits   (load-commits-gh "thi-ng" "geom")
      [from to] (viz/value-domain-bounds commits)
      from      (* (long (/ from week)) week)
      to        (* (inc (long (/ to week))) week)
      mat       (commits->matrix (commits-per-week-day from commits))
      weeks     (last (nd/shape mat))
      max-x     (+ 50 (* weeks 10))]
  (->> {:x-axis (viz/linear-axis
                 {:domain [0 weeks]
                  :range  [50 max-x]
                  :major  4
                  :minor  1
                  :pos    85
                  :label  (viz/default-svg-label (month-or-year from))})
        :y-axis (viz/linear-axis
                 {:domain  [0 7]
                  :range   [10 80]
                  :visible false})
        :data   [{:matrix        mat
                  :value-domain  [1 (reduce max mat)]
                  :palette       (->> :yellow-red (grad/cosine-schemes) (apply grad/cosine-gradient 100))
                  :palette-scale viz/linear-scale
                  :layout        viz/svg-heatmap
                  :shape         viz/circle-cell}]}
       (viz/svg-plot2d-cartesian)
       (svg/svg {:width (+ 70 max-x) :height 120})
       (svg/serialize)
       (spit "commit-history.svg")))

Contour plot

http://media.thi.ng/geom/viz/contours-4.svghttp://media.thi.ng/geom/viz/contours-outline-4.svg
linear X/Y filledlinear X/Y outline
http://media.thi.ng/geom/viz/contours-log-4.svghttp://media.thi.ng/geom/viz/contours-log-outline-4.svg
log X/Y filledlog X/Y outline
<<example-imports>>
(require '[thi.ng.math.simplexnoise :as n])

(def viz-spec
  {:x-axis (viz/linear-axis
            {:domain [0 63]
             :range  [50 550]
             :major  8
             :minor  2
             :pos    550})
   :y-axis (viz/linear-axis
            {:domain      [0 63]
             :range       [550 50]
             :major       8
             :minor       2
             :pos         50
             :label-dist  15
             :label-style {:text-anchor "end"}})
   :data   [{:matrix       (->> (for [y (range 64) x (range 64)] (n/noise2 (* x 0.06) (* y 0.06)))
                                (viz/contour-matrix 64 64))
             :levels       (range -1 1 0.05)
             :value-domain [-1.0 1.0]
             :attribs      {:fill "none" :stroke "#0af"}
             :layout       viz/svg-contour-plot}]})

(def viz-spec-log
  (merge viz-spec
         {:x-axis (viz/log-axis
                   {:domain [0 64]
                    :range [50 550]
                    :base 2
                    :pos 555})
          :y-axis (viz/log-axis
                   {:domain      [0 64]
                    :range       [550 50]
                    :base        2
                    :pos         45
                    :label-dist  15
                    :label-style {:text-anchor "end"}})}))

(def fill-attribs {:fill (col/rgba 0.0 0.66 1.0 0.05) :stroke "#fff"})

(defn export-viz
  [viz path] (->> viz (svg/svg {:width 600 :height 600}) (svg/serialize) (spit path)))

(->> {"contours-outline.svg"     [viz-spec false]
      "contours.svg"             [viz-spec true]
      "contours-log-outline.svg" [viz-spec-log false]
      "contours-log.svg"         [viz-spec-log true]}
     (run!
      (fn [[path [spec fill?]]]
        (-> (if fill? (assoc-in spec [:data 0 :attribs] fill-attribs) spec)
            (viz/svg-plot2d-cartesian)
            (export-viz path)))))

An animated variation with polar coordinates:

http://media.thi.ng/geom/viz/contours-polar.gif

GIS terrain contours w/ elevation color gradient

http://media.thi.ng/geom/viz/terrain-24.svghttp://media.thi.ng/geom/viz/terrain-18.svg
contour delta = 24contour delta = 18
http://media.thi.ng/geom/viz/terrain-12.svghttp://media.thi.ng/geom/viz/terrain-6.svg
contour delta = 12contour delta = 6
<<example-imports>>
(require '[thi.ng.color.gradients :as grad])

(defn load-image
  [path]
  (let [img (javax.imageio.ImageIO/read (java.io.File. path))
        w   (.getWidth img)
        h   (.getHeight img)
        rgb (.getRGB img 0 0 w h (int-array (* w h)) 0 w)]
    (viz/contour-matrix w h (map #(bit-and % 0xff) rgb))))

(def viz-spec
  {:x-axis (viz/linear-axis
            {:domain [0 79]
             :range [50 550]
             :major 8
             :minor 2
             :pos 550})
   :y-axis (viz/linear-axis
            {:domain      [0 79]
             :range       [50 550]
             :major       8
             :minor       2
             :pos         50
             :label-dist  15
             :label-style {:text-anchor "end"}})
   :data   [{:matrix          (load-image "../../assets/california-detail-gis.png")
             :value-domain    [0.0 255.0]
             :attribs         {:fill "none"}
             :palette         (->> :orange-blue (grad/cosine-schemes) (apply grad/cosine-gradient 100))
             :contour-attribs (fn [col] {:stroke col})
             :layout          viz/svg-contour-plot}]})

(doseq [res [6 12 18 24]]
 (->> (assoc-in viz-spec [:data 0 :levels] (range 0 255 res))
      (viz/svg-plot2d-cartesian)
      (svg/svg {:width 600 :height 600})
      (svg/serialize)
      (spit (str "terrain-" res ".svg"))))

Visualization spec format

The main configuration map should have at least the following keys, common to all supported visualization methods. Visualizations are created by taking a series of data points and mapping them from their source :domain into new coordinate system (:range). Furthermore, this target coordinate system itself can be transformed via projections, e.g. to translate from cartesian into polar coordinates (e.g. see Radar plot and other polar examples above).

KeyValueRequiredDescription
:x-axishorizontal axis spec mapYX-axis behavior & representation details
:y-axisvertical axis spec mapYY-axis behavior & representation details
:gridgrid spec mapNOptional background axis grid
:datavector of dataset specsYAllows multiple datasets in visualization

The following options are only used for visualizations using svg-plot2d-polar:

KeyValueRequiredDescription
:origin2D vector, e.g. [x y]YCenter pos of radial layout, only required for polar projection
:circlebooleanNtrue if axis & grid should be using full circles, only required for polar projection

Axis definitions (:x-axis / :y-axis)

Axis specs are usually created via one of the available axis generator functions (linear-axis, log-axis, lens-axis). These functions too take a map w/ some of the same keys, but replace some vals with transformed data and autofill default values for others.

KeyValueRequiredDefaultDescription
:scalescale functionYnilScale function to translate domain values into visualization coordinates
:domainvec of domain boundsYnilLower & upper bound of data source interval
:rangevec of range boundsYnilLower & upper bound of projected coordinates
:posnumberYnilDraw position of the axis (ypos for X-axis, xpos for Y-axis)
:majorseq of domain valuesNnilSeq of domain positions at which to draw labeled tick marks
:minorseq of domain valuesNnilSeq of domain positions at which
:major-sizenumberN10Length of major tick marks
:minor-sizenumberN5Length of minor tick marks
:labelfunctionN(default-svg-label (value-format 2))Function to format & emit tick labels
:label-distnumberN10 + major-sizeDistance of value labels from axis
:label-stylemapNsee next sectionStyle attribute map for value labels
:label-ynumberN0Vertical offset for Y-axis labels
:attribsmapN{:stroke "black"}Axis line attribs attributes
:visiblebooleanNtrueFlag if axis will be visible in visualization

About tick marks

The linear-axis & lens-axis interprete the given :major and :minor values as the intended step distance between ticks and generate ticks at multiples of the given value.

The log-axis generator auto-creates ticks based on the :base of the logarithm.

Notes for polar projection

  • the :range interval of the x-axis must be an angle interval in radians (see above example)
  • the :range interval of the y-axis must be a radius interval

Same goes for :pos values: The :pos for x-axis is a radius, the :pos for y-axis is an angle in radians

Default axis label styling

For SVG export, each axis is exported as its own group (incl. tick marks & labels). By default the following label style is applied to each group, however this can be overridden for individual labels by specifying a custom :label function.

{:fill        "black"
 :stroke      "none"
 :font-family "Arial, sans-serif"
 :font-size   10
 :text-anchor "middle"}

Axis grid definition (:grid)

Note: If no :grid spec is given in the main spec, no background grid will be displayed…

KeyValueRequiredDefaultDescription
:attribshashmapNdefault attribsallows extra attributes to be injected (e.g. for SVG)
:minor-xbooleanNfalseif false only uses major tick mark positions on X axis
:minor-ybooleanNfalseif false only uses major tick mark positions on Y axis

Dataset specs (:data)

The format of these maps is largely dependent on the concrete visualization methods used, but most have the following keys in common:

KeyRequiredDefaultDescription
:layoutYnilLayout function to map data points
:valuesYnilData points to be mapped
:attribsNnilStyling & other attributes to attach to surrounding group node
:item-posNidentityFunction returning domain position of single data point

The :item-pos key is only needed if the data items are in a non-standard format. By the default most layout functions expect the data points to be a 2-element vector [domain-pos value]. Using an :item-pos lookup fn however, data items can be supplied in other form (eg. as maps). Also see examples above…

Scales

Scaling functions merely provide a means to map values from a source interval (domain) to a target interval (range). The latter usually represents values in the visualization space (e.g. partial screen coordinates).

The functions below can be useful also outside a visualization context, but here are used in conjunction with their related axis definition functions described below. When creating visualizations, we would not usually use these scaling functions directly, but use them implicitly via our defined axis specs.

Linear scale

(defn linear-scale
  [domain range]
  (fn [x] (m/map-interval x domain range)))

Logarithmic scale

(defn log
  [base]
  (let [lb (Math/log base)]
    #(/ (cond
          (pos? %) (Math/log %)
          (neg? %) (- (Math/log (- %)))
          :else 0)
        lb)))

(defn log-scale
  [base [d1 d2 :as domain] [r1 r2 :as range]]
  (let [log* (log base)
        d1l  (log* d1)
        dr   (- (log* d2) d1l)]
    (fn [x] (m/mix r1 r2 (/ (- (log* x) d1l) dr)))))

Lens scale (dilating / bundling)

The lens-scale defines a non-linear mapping by specifying a focal position in the domain interval, as well as a lens strength which controls the compression or expansion of the domain space around this focal point. If strength is positive, the lens is dilating. If negative, it is bundling (compressing). A strength of zero causes a normal/linear scaling behavior.

The two animations below show the effect of individually adjusting the focus and lens strength:

http://media.thi.ng/geom/viz/lens-focus-2.gifhttp://media.thi.ng/geom/viz/lens-strength-4.gif
Focus shift, constant strength = 0.5Lens strength adjustment, constant focus = 0.0
(defn lens-scale
  [focus strength [d1 d2] [r1 r2]]
  (let [dr (- d2 d1)
        f  (/ (- focus d1) dr)]
    (fn [x] (m/mix-lens r1 r2 (/ (- x d1) dr) f strength))))

Axis & tick generators

Common axis factory

(defn axis-common*
  [{:keys [visible major-size minor-size label attribs label-style label-dist]
    :or {visible true major-size 10, minor-size 5}
    :as spec}]
  (assoc spec
         :visible     visible
         :major-size  major-size
         :minor-size  minor-size
         :label       (or label (default-svg-label (value-formatter 2)))
         :attribs     (merge
                      {:stroke "black"}
                      attribs)
         :label-style (merge
                       <<default-label-style>>
                       label-style)
         :label-dist  (or label-dist (+ 10 major-size))))

Linear

(defn lin-tick-marks
  [[d1 d2] delta]
  (if (m/delta= delta 0.0 m/*eps*)
    '()
    (let [dr (- d2 d1)
          d1' (m/roundto d1 delta)]
      (filter #(m/in-range? d1 d2 %) (range d1' (+ d2 delta) delta)))))

(defn linear-axis
  [{:keys [domain range major minor] :as spec}]
  (let [major' (if major (lin-tick-marks domain major))
        minor' (if minor (lin-tick-marks domain minor))
        minor' (if (and major' minor')
                 (filter (complement (set major')) minor')
                 minor')]
    (-> spec
        (assoc
         :scale (linear-scale domain range)
         :major major'
         :minor minor')
        (axis-common*))))

Logarithmic

(defn log-ticks-domain
  [base d1 d2]
  (let [log* (log base)] [(m/floor (log* d1)) (m/ceil (log* d2))]))

(defn log-tick-marks-major
  [base [d1 d2]]
  (let [[d1l d2l] (log-ticks-domain base d1 d2)]
    (->> (for [i (range d1l (inc d2l))]
           (if (>= i 0)
             (* (/ 1 base) (Math/pow base i))
             (* (/ 1 base) (- (Math/pow base (- i))))))
         (filter #(m/in-range? d1 d2 %)))))

(defn log-tick-marks-minor
  [base [d1 d2]]
  (let [[d1l d2l] (log-ticks-domain base d1 d2)
        ticks (if (== 2 base) [0.75] (range 2 base))]
    (->> (for [i (range d1l (inc d2l)) j ticks]
           (if (>= i 0)
             (* (/ j base) (Math/pow base i))
             (* (/ j base) (- (Math/pow base (- i))))))
         (filter #(m/in-range? d1 d2 %)))))

(defn log-axis
  [{:keys [base domain range] :or {base 10} :as spec}]
  (-> spec
      (assoc
       :scale (log-scale base domain range)
       :major (log-tick-marks-major base domain)
       :minor (log-tick-marks-minor base domain))
      (axis-common*)))

Lens axis

The lens axis is a modified linear-axis with two additional required attributes to control the domain space deformation in order to compress or expand the space around a given focal point and therefore introduce a non-linear arrangement. See lens-scale above for further details.

  • :focus - the domain value acting as lens focus (by default the center of the domain is used)
  • :strength - the lens strength & direction (normalized values -1.0 … + 1.0, default = 0.5)
(defn lens-axis
  [{:keys [domain range focus strength major minor]
    :or {strength 0.5} :as spec}]
  (let [major' (if major (lin-tick-marks domain major))
        minor' (if minor (lin-tick-marks domain minor))
        minor' (if (and major' minor')
                 (filter (complement (set major')) minor')
                 minor')
        focus  (or focus (/ (apply + domain) 2.0))]
    (-> spec
        (assoc
         :scale    (lens-scale focus strength domain range)
         :major    major'
         :minor    minor'
         :focus    focus
         :strength strength)
        (axis-common*))))

Domain analysis ops

Raw values to domain point conversion

Most of the visualization methods in this module expect a seq of data points in the format: [domain-position value]. The function uniform-domain-points is useful to convert a sequence of pure values (without position) into a seq of uniformly spaced data points along the full breadth of the given domain:

(defn uniform-domain-points
  "Given a vector of domain bounds and a collection of data values
  (without domain position), produces a lazy-seq of 2-element vectors
  representing the values of the original coll uniformly spread over
  the full domain range, with each of the form: [domain-pos value]."
  [[d1 d2] values]
  (map
   (fn [t v] [(m/mix d1 d2 t) v])
   (m/norm-range (dec (count values)))
   values))

Domain bounds

(def domain-bounds-x #(gu/axis-bounds 0 %))

(def domain-bounds-y #(gu/axis-bounds 1 %))

(def domain-bounds-z #(gu/axis-bounds 2 %))

(defn total-domain-bounds
  [f & colls]
  (transduce
   (map f)
   (completing (fn [[aa ab] [xa xb]] [(min aa xa) (max ab xb)]))
   [m/INF+ m/INF-] colls))

Matrix value domain

(defn value-domain-bounds
  [mat]
  (let [vals (seq mat)]
    [(reduce min vals) (reduce max vals)]))

Axial visualization methods

This section defines the various layout/plotting methods, each with a brief description and lists of custom :data spec options. See the “Dataset specs” section and examples above for details about other (required) keys…

Line plot

This method simply represents the mapped values as a single line-strip (polyline). Values are automatically sorted by domain position, so can be initially unordered.

(defn svg-line-plot
  [v-spec d-spec]
  (svg/line-strip (map first (process-points v-spec d-spec)) (:attribs d-spec)))

Area plot

Similar to the line plot method above, however resulting points are represented as closed polygon (and hence an :attribs key should be supplied w/ a :fill color).

Note: When using polar coordinate mapping (via svg-plot2d-polar), a :res option should be given too in order to create an arc approximation closing the polygon along the X-axis (e.g. :res 20).

KeyRequiredDefaultDescription
:resN1Number of points used to close polygon along X-axis
(defn svg-area-plot
  [{:keys [y-axis project] :as v-spec} {:keys [res] :as d-spec}]
  (let [ry1     (first (:range y-axis))
        points  (mapv first (process-points (assoc v-spec :project vec2) d-spec))
        p       (vec2 (first (peek points)) ry1)
        q       (vec2 (ffirst points) ry1)
        points  (concat points (mapv (partial g/mix p q) (m/norm-range (or res 1))))]
    (svg/polygon (mapv project points) (:attribs d-spec))))

Radar plot

This plot method is intended to be only used with svg-plot2d-polar.

KeyRequiredDefaultDescription
:shapeNsvg/polygonShape function receiving seq of all points & attribs
(defn svg-radar-plot
  [v-spec {:keys [shape] :or {shape svg/polygon} :as d-spec}]
  (shape (mapv first (process-points v-spec d-spec)) (:attribs d-spec)))

Min-Max radar plot

This version of the radar plot expects a min/max interval for each data item. For example a single data point of [2 0.25 0.75] would define a domain position at x=2 and an interval of 0.25-0.75. If no :item-pos-* options are supplied this 3-element vector format is assumed for each data point.

KeyRequiredDefaultDescription
:item-pos-minN[x min]Function to provide min. data point
:item-pos-maxN[x max]Function to provide max. data point
:shapeNsvg/pathShape function receiving seq of outer & inner points and attribs
(defn svg-radar-plot-minmax
  [v-spec
   {:keys [item-pos-min item-pos-max shape]
    :or   {shape #(svg/path (concat % [[:Z]] %2 [[:Z]]) %3)}
    :as   d-spec}]
  (let [min-points (->> (assoc d-spec :item-pos (or item-pos-min (fn [i] (take 2 i))))
                        (process-points v-spec)
                        (mapv first)
                        (points->path-segments))
        max-points (->> (assoc d-spec :item-pos (or item-pos-max (fn [i] [(first i) (nth i 2)])))
                        (process-points v-spec)
                        (mapv first)
                        (points->path-segments))]
    (shape max-points min-points (assoc (:attribs d-spec) :fill-rule "evenodd"))))

Scatter plot

KeyRequiredDefaultDescription
:shapeNcircleFunction returning shape primitive for each data point
(defn svg-scatter-plot
  [v-spec {:keys [attribs shape] :as d-spec}]
  (->> (assoc d-spec :shape (or shape (fn [[p]] (svg/circle p 3))))
       (process-points v-spec)
       (apply svg/group attribs)))

Bar plot

KeyRequiredDefaultDescription
:item-posNidentityFunction returning domain position of single data point
:shapeNlineFunction returning shape primitive for each data point
:interleaveN1Number of bars per domain position
:offsetN0Only used for interleaved bars, index position
:bar-widthN0Only used for interleaved bars, width of single bar
:shapeYsvg/lineFunction returning shape primitive for each data point
(defn svg-bar-plot
  [{:keys [x-axis y-axis project] :or {project vec2}}
   {:keys [values attribs shape item-pos interleave offset bar-width]
    :or   {shape      (fn [a b _] (svg/line a b))
           item-pos   identity
           interleave 1
           bar-width  0
           offset     0}}]
  (let [domain  (:domain x-axis)
        base-y  ((:scale y-axis) (first (:domain y-axis)))
        mapper  (value-mapper (:scale x-axis) (:scale y-axis))
        offset  (+ (* -0.5 (* interleave bar-width)) (* (+ offset 0.5) bar-width))]
    (->> values
         (sequence
          (comp
           (map (juxt item-pos identity))
           (filter #(m/in-range? domain (ffirst %)))
           (map
            (fn [[p i]]
              (let [[ax ay] (mapper p)
                    ax (+ ax offset)]
                (shape (project [ax ay]) (project [ax base-y]) i))))))
         (apply svg/group attribs))))

Heatmap

KeyRequiredDefaultDescription
:matrixYnilNDArray instance of data grid
:paletteYnilColor list
:palette-scaleNlinear-scaleMapping function of matrix values to palette index
:value-domainN[0 1]Domain interval of matrix values
:clampNfalseIf true, matrix values are clamped to value domain

Note: If :clamp is not enabled, the :value-domain acts as filter and will not include cells with values outside the domain, resulting in holes in the visualization. On the other hand, if :clamp is enabled, the :value-domain acts as a kind of amplification or compression function.

(defn svg-heatmap
  [{:keys [x-axis y-axis project]}
   {:keys [matrix value-domain clamp palette palette-scale attribs shape]
    :or {value-domain  [0.0 1.0]
         palette-scale linear-scale
         shape         #(svg/polygon [%1 %2 %3 %4] {:fill %5})}
    :as d-spec}]
  (let [scale-x (:scale x-axis)
        scale-y (:scale y-axis)
        pmax    (dec (count palette))
        scale-v (palette-scale value-domain [0 pmax])]
    (apply svg/group
     attribs
     (for [p     (nd/position-seq matrix)
           :let  [[y x] p
                  v     (nd/get-at matrix y x)]
           :when (or clamp (m/in-range? value-domain v))]
       (shape
        (project [(scale-x x) (scale-y y)])
        (project [(scale-x (inc x)) (scale-y y)])
        (project [(scale-x (inc x)) (scale-y (inc y))])
        (project [(scale-x x) (scale-y (inc y))])
        (palette (m/clamp (int (scale-v v)) 0 pmax)))))))

Contour lines

Given a 2D matrix (a thi.ng.ndarray.core.NDArray instance) of data values and a seq of thresholds, this function computes number of polygons for each threshold level.

Note: Since the data format for this method is different to the other layouts, we’re using the :matrix key instead of :values to emphasize this difference…

KeyRequiredDefaultDescription
:matrixYnilNDArray instance of data grid
:levelsYnilSeq of threshold values to find contours for
:paletteYnilColor list
:palette-scaleNlinear-scaleMapping function of matrix values to palette index
:value-domainN[0 1]Domain interval of matrix values
:contour-attribsNnilFunction to produce shape attribs map for each threshold level
(defn matrix-2d
  [w h values] (nd/ndarray :float32 values [h w]))

(defn contour-matrix
  [w h values]
  (contours/set-border-2d (matrix-2d w h values) -1e9))

(defn contour->svg
  [scale-x scale-y project]
  (fn [attribs contour]
    (let [contour (map (fn [[y x]] [(scale-x x) (scale-y y)]) contour)]
      (svg/polygon (map project contour) attribs))))

(defn svg-contour-plot
  [{:keys [x-axis y-axis project]}
   {:keys [matrix attribs levels palette palette-scale value-domain contour-attribs]
    :or   {value-domain    [0.0 1.0]
           palette         [[1 1 1]]
           palette-scale   linear-scale
           contour-attribs (constantly nil)}}]
  (let [pmax       (dec (count palette))
        scale-v    (palette-scale value-domain [0 pmax])
        contour-fn (contour->svg (:scale x-axis) (:scale y-axis) project)]
    (->> levels
         (sort)
         (mapv
          (fn [iso]
            (let [c-attribs (contour-attribs (palette (m/clamp (int (scale-v iso)) 0 pmax)))]
              (apply svg/group
                     {} (mapv
                         (partial contour-fn c-attribs)
                         (contours/find-contours-2d matrix iso))))))
         (apply svg/group attribs))))

Stacked intervals

KeyRequiredDefaultDescription
:item-rangeNidentityFunction returning domain interval for each data item
:offsetN0Y-axis offset for this data series
:shapeNsvg/lineFunction returning shape primitive for each data item
(defn overlap? [[a b] [c d]] (and (<= a d) (>= b c)))

(defn compute-row-stacking
  [item-range coll]
  (reduce
   (fn [grid x]
     (let [r (item-range x)]
       (loop [[row & more] grid idx 0]
         (if (or (nil? row) (not (some #(overlap? r (item-range %)) row)))
           (update-in grid [idx] #(conj (or % []) x))
           (recur more (inc idx))))))
   [] coll))

(defn process-interval-row
  [item-range mapper [d1 d2]]
  (fn [i row]
    (map
     (fn [item]
       (let [[a b] (item-range item)]
         [(mapper [(max d1 a) i]) (mapper [(min d2 b) i]) item]))
     row)))

(defn svg-stacked-interval-plot
  [{:keys [x-axis y-axis]}
   {:keys [values attribs shape item-range offset]
    :or   {shape (fn [[a b]] (svg/line a b))
           item-range identity
           offset 0}}]
  (let [scale-x (:scale x-axis)
        scale-y (:scale y-axis)
        domain  (:domain x-axis)
        mapper  (value-mapper scale-x scale-y)]
    (->> values
         (filter #(overlap? domain (item-range %)))
         (sort-by (comp first item-range))
         (compute-row-stacking item-range)
         (mapcat (process-interval-row item-range mapper domain) (range offset 1e6))
         (mapv shape)
         (apply svg/group attribs))))

Non-axial visualization methods

Bubble chart

KeyRequiredDefaultDescription
:itemsYnil
:boundsYnilShape instance defining visualization bounds
:originN[0 0]
:iterN50
:attractN0.25
:repelN-1.0
:jitterN0

TBD

Force directed graph layout

TBD

2D Cartesian Plotting (SVG)

SVG axis generators

(defn svg-axis*
  [{:keys [major minor attribs label-style]} axis tick1-fn tick2-fn label-fn]
  (svg/group
   (merge {:stroke "#000"} attribs)
   (map tick1-fn major)
   (map tick2-fn minor)
   (apply svg/group (merge {:stroke "none"} label-style) (mapv label-fn major))
   axis))

(defn svg-x-axis-cartesian
  [{:keys [scale major-size minor-size label-dist pos label] [r1 r2] :range
    :as spec}]
  (let [y-major (+ pos major-size)
        y-minor (+ pos minor-size)
        y-label (+ pos label-dist)]
    (svg-axis*
     spec (svg/line [r1 pos] [r2 pos])
     #(let [x (scale %)] (svg/line [x pos] [x y-major]))
     #(let [x (scale %)] (svg/line [x pos] [x y-minor]))
     #(label (vec2 (scale %) y-label) %))))

(defn svg-y-axis-cartesian
  [{:keys [scale major-size minor-size label-dist label-y pos label] [r1 r2] :range
    :or {label-y 0}
    :as spec}]
  (let [x-major (- pos major-size)
        x-minor (- pos minor-size)
        x-label (- pos label-dist)]
    (svg-axis*
     spec (svg/line [pos r1] [pos r2])
     #(let [y (scale %)] (svg/line [pos y] [x-major y]))
     #(let [y (scale %)] (svg/line [pos y] [x-minor y]))
     #(label (vec2 x-label (+ (scale %) label-y)) %))))

Generic plotting helpers

(defn select-ticks
  [axis minor?] (if minor? (concat (:minor axis) (:major axis)) (:major axis)))

(defn svg-axis-grid2d-cartesian
  [x-axis y-axis {:keys [attribs minor-x minor-y]}]
  (let [[x1 x2] (:range x-axis)
        [y1 y2] (:range y-axis)
        scale-x (:scale x-axis)
        scale-y (:scale y-axis)]
    (svg/group
     (merge {:stroke "#ccc" :stroke-dasharray "1 1"} attribs)
     (if (:visible x-axis)
       (map #(let [x (scale-x %)] (svg/line [x y1] [x y2])) (select-ticks x-axis minor-x)))
     (if (:visible y-axis)
       (map #(let [y (scale-y %)] (svg/line [x1 y] [x2 y])) (select-ticks y-axis minor-y))))))

(defn svg-plot2d-cartesian
  [{:keys [x-axis y-axis grid data] :as opts}]
  (let [opts (assoc opts :project vec2)]
    (svg/group
     {}
     (if grid (svg-axis-grid2d-cartesian x-axis y-axis grid))
     (map (fn [spec] ((:layout spec) opts spec)) data)
     (if (:visible x-axis) (svg-x-axis-cartesian x-axis))
     (if (:visible y-axis) (svg-y-axis-cartesian y-axis)))))

2D Polar Plotting (SVG)

SVG axis generators

(defn svg-x-axis-polar
  [{:keys [x-axis project circle origin]}]
  (let [{:keys [scale major-size minor-size label-dist pos]} x-axis
        label   (or (:label x-axis) (default-svg-label (value-formatter 2)))
        [r1 r2] (:range x-axis)
        o       origin]
    (svg-axis*
     x-axis
     (if circle
       (svg/circle o pos {:fill "none"})
       (svg/arc o pos r1 r2 (> (m/abs-diff r1 r2) m/PI) true {:fill "none"}))
     #(let [x (scale %)]
        (svg/line (project [x pos]) (project [x (+ pos major-size)])))
     #(let [x (scale %)]
        (svg/line (project [x pos]) (project [x (+ pos minor-size)])))
     #(let [x (scale %)]
        (label (project [x (+ pos label-dist)]) %)))))

(defn svg-y-axis-polar
  [{:keys [y-axis project]}]
  (let [{:keys [scale label-y pos] :or {label-y 0}} y-axis
        label   (or (:label y-axis) (default-svg-label (value-formatter 2)))
        [r1 r2] (:range y-axis)
        a       (project [pos r1])
        b       (project [pos r2])
        nl      (g/normalize (g/normal (g/- a b)) (:label-dist y-axis))
        n1      (g/normalize nl (:major-size y-axis))
        n2      (g/normalize nl (:minor-size y-axis))]
    (svg-axis*
     y-axis
     (svg/line a b)
     #(let [p (project [pos (scale %)])]
        (svg/line p (g/+ p n1)))
     #(let [p (project [pos (scale %)])]
        (svg/line p (g/+ p n2)))
     #(let [p (project [pos (+ (scale %) label-y)])]
        (label (g/+ p nl) %)))))
(defn svg-axis-grid2d-polar
  [{:keys [x-axis y-axis origin circle project] {:keys [attribs minor-x minor-y]} :grid}]
  (let [[x1 x2] (:range x-axis)
        [y1 y2] (:range y-axis)
        scale-x (:scale x-axis)
        scale-y (:scale y-axis)
        great?  (> (m/abs-diff x1 x2) m/PI)]
    (svg/group
     (merge {:stroke "#ccc" :stroke-dasharray "1 1"} attribs)
     (if (:visible x-axis)
       (map
        #(let [x (scale-x %)] (svg/line (project [x y1]) (project [x y2])))
        (select-ticks x-axis minor-x)))
     (if (:visible y-axis)
       (map
        #(let [y (scale-y %)]
           (if circle
             (svg/circle origin y {:fill "none"})
             (svg/arc origin y x1 x2 great? true {:fill "none"})))
        (select-ticks y-axis minor-y))))))

(defn svg-plot2d-polar
  [{:keys [x-axis y-axis grid data origin] :as spec}]
  (let [spec (assoc spec :project (polar-projection origin))]
    (svg/group
     {}
     (if grid (svg-axis-grid2d-polar spec))
     (map (fn [spec'] ((:layout spec') spec spec')) data)
     (if (:visible x-axis) (svg-x-axis-polar spec))
     (if (:visible y-axis) (svg-y-axis-polar spec)))))

Projections

(defn polar-projection
  [origin]
  (let [o (vec2 origin)]
    (fn [[x y]] (g/+ o (g/as-cartesian (vec2 y x))))))

Value transformations

(defn value-mapper
  [scale-x scale-y] (fn [[x y]] [(scale-x x) (scale-y y)]))

(defn value-transducer
  [{:keys [cull-domain cull-range scale-x scale-y project shape item-pos]}]
  (let [mapper   (value-mapper scale-x scale-y)
        item-pos (or item-pos identity)]
    (cond->       (map (juxt item-pos identity))
      cull-domain (comp (filter #(m/in-range? cull-domain (ffirst %))))
      :always     (comp (map (fn [[p i]] [(mapper p) i])))
      cull-range  (comp (filter #(m/in-range? cull-range (peek (first %)))))
      project     (comp (map (fn [[p i]] [(project p) i])))
      shape       (comp (map shape)))))

(defn process-points
  [{:keys [x-axis y-axis project]} {:keys [values item-pos shape]}]
  (let [[ry1 ry2] (:range y-axis)]
    (->> (if item-pos
           (sort-by (comp first item-pos) values)
           (sort-by first values))
         (sequence
          (value-transducer
           {:cull-domain (:domain x-axis)
            :cull-range  (if (< ry1 ry2) [ry1 ry2] [ry2 ry1])
            :item-pos    item-pos
            :scale-x     (:scale x-axis)
            :scale-y     (:scale y-axis)
            :project     project
            :shape       shape})))))
(defn points->path-segments
  [[p & more]] (reduce #(conj % [:L %2]) [[:M p]] more))

Value formatting

(defn value-formatter
  [prec]
  (let [fmt [(f/float prec)]]
    (fn [x] (f/format fmt x))))

(defn format-percent
  [x] (str (int (* x 100)) "%"))

(defn default-svg-label
  [f] (fn [p x] (svg/text p (f x))))

Custom shapes

This section provides some preset shape functions for use with scatter plots or stacked interval plots (see examples at beginning of this file).

(defn svg-triangle-up
  [w]
  (let [h (* w (Math/sin m/THIRD_PI))
        w (* 0.5 w)]
    (fn [[[x y]]] (svg/polygon [[(- x w) (+ y h)] [(+ x w) (+ y h)] [x y]]))))

(defn svg-triangle-down
  [w]
  (let [h (* w (Math/sin m/THIRD_PI))
        w (* 0.5 w)]
    (fn [[[x y]]] (svg/polygon [[(- x w) (- y h)] [(+ x w) (- y h)] [x y]]))))

(defn svg-square
  [r] (let [d (* r 2.0)] (fn [[[x y]]] (svg/rect [(- x r) (- y r)] d d))))

(defn labeled-rect-horizontal
  [{:keys [h r label fill min-width base-line]}]
  (let [r2 (* -2 r)
        h2 (* 0.5 h)]
    (fn [[[ax ay :as a] [bx :as b] item]]
      (svg/group
       {}
       (svg/rect
        [(- ax r) (- ay h2)] (- bx ax r2) h
        {:fill (fill item) :rx r :ry r})
       (if (< min-width (- bx ax))
         (svg/text [ax (+ base-line ay)] (label item)))))))

(defn circle-cell
  [a b c d col]
  (svg/circle (gu/centroid [a b c d]) (* 0.5 (g/dist a b)) {:fill col}))

3D Plotting

TBD

Date & time helpers

(defn epoch->cal
  [epoch] (doto (GregorianCalendar.) (.setTimeInMillis (long epoch))))

(defn clear-millis [^GregorianCalendar cal] (.set cal Calendar/MILLISECOND 0) cal)
(defn clear-second [^GregorianCalendar cal] (.set Calendar/SECOND 0) (clear-millis cal))
(defn clear-minute [^GregorianCalendar cal] (.set Calendar/MINUTE 0) (clear-second cal))
(defn clear-hour [^GregorianCalendar cal] (.set cal Calendar/HOUR 0) (clear-minute cal))
(defn clear-day-of-month [^GregorianCalendar cal] (.set cal Calendar/DAY_OF_MONTH 1) (clear-hour cal))
(defn clear-day-of-week [^GregorianCalendar cal] (.set cal Calendar/DAY_OF_WEEK 1) (clear-hour cal))
(defn clear-month [^GregorianCalendar cal] (.set cal Calendar/MONTH 0) (clear-day-of-month cal))

(defn year [^GregorianCalendar cal] (.get cal Calendar/YEAR))
(defn month [^GregorianCalendar cal] (.get cal Calendar/MONTH))
(defn day-of-month [^GregorianCalendar cal] (.get cal Calendar/DAY_OF_MONTH))
(defn day-of-week [^GregorianCalendar cal] (.get cal Calendar/DAY_OF_WEEK))
(defn hour [^GregorianCalendar cal] (.get cal Calendar/HOUR))
(defn minute [^GregorianCalendar cal] (.get cal Calendar/MINUTE))
(defn second [^GregorianCalendar cal] (.get cal Calendar/SECOND))

(defn round-to-year
  [epoch]
  (let [cal (epoch->cal epoch)]
    (doto cal
      (.add Calendar/MONTH 6)
      (clear-month))))

(defn round-to-month
  [epoch]
  (doto (epoch->cal epoch)
    (.setTimeInMillis (long epoch))
    (.add Calendar/DAY_OF_MONTH 16)
    (clear-day-of-month)))

(defn round-to-week
  [epoch]
  (doto (epoch->cal epoch)
    (.setTimeInMillis (long epoch))
    (.add Calendar/DAY_OF_WEEK 4)
    (clear-day-of-week)))

(defn round-to-day-of-month
  [epoch]
  (doto (epoch->cal epoch)
    (.add Calendar/HOUR 12)
    (clear-hour)))

(defn round-to-day-of-week
  [epoch]
  (doto (epoch->cal epoch)
    (.add Calendar/HOUR 12)
    (clear-hour)))

(defn round-to-hour
  [epoch]
  (doto (epoch->cal epoch)
    (.add Calendar/MINUTE 30)
    (clear-minute)))

Complete namespace definitions

(ns thi.ng.geom.viz.core
  (:require
   [thi.ng.geom.core :as g]
   [thi.ng.geom.core.vector :as v :refer [vec2 vec3]]
   [thi.ng.geom.core.utils :as gu]
   [thi.ng.geom.svg.core :as svg]
   [thi.ng.ndarray.core :as nd]
   [thi.ng.ndarray.contours :as contours]
   [thi.ng.math.core :as m]
   [thi.ng.strf.core :as f]))

<<transformers>>

<<projections>>

<<formatters>>

<<analysis>>

<<scale>>

<<axis>>

<<shapes>>

<<plot-2d>>