This file is archived and only kept for reference - DO NOT edit
This section shows some basic example outputs and general usage patterns for all currently implemented visualization methods. The section after then describes the various options in more detail.
See instructions in examples/all.org about running all the examples in this module from the REPL. Note, these examples are all for Clojure, but (apart from outputting SVG files), the same functionality is available in Clojurescript (with or without React.js).
(require '[thi.ng.geom.viz.core :as viz] :reload)
(require '[thi.ng.geom.svg.core :as svg])
(require '[thi.ng.geom.vector :as v])
(require '[thi.ng.color.core :as col])
(require '[thi.ng.math.core :as m :refer [PI TWO_PI]])
Logarithmic X-axis, linear Y | Log 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 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)…
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"))
Single value per domain position | 3 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"))
6 categories, 3 data sets, single values | 6 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"))
<<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"))
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).
<<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&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&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).
;; 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"))
: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.utils :as gu])
(require '[thi.ng.math.noise :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 (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)
: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)
: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})
Note: This example can optionally use raynes/tentacles 0.3.0 (needs to be manually added to your project) 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
(default) and load-commits-gh
…
;;(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)))
;; currently disabled to due CLJ 1.8 incompatibility
#_(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 (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")))
linear X/Y filled | linear X/Y outline |
log X/Y filled | log X/Y outline |
<<example-imports>>
(require '[thi.ng.math.noise :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:
contour delta = 24 | contour delta = 18 |
contour delta = 12 | contour 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 (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"))))