com.keminglabs/c20.2.3-SNAPSHOTDeclarative data visualization in Clojure(Script). dependencies
| (this space intentionally left almost blank) | |||||||||||||||
(ns c2.core (:use [clojure.string :only [join]])) | ||||||||||||||||
Map | (defn unify
([data mapping]
(map mapping data))
([container data mapping]
(into container (unify data mapping)))) | |||||||||||||||
(ns c2.util (:use [reflex.macros :only [computed-observable]])) | ||||||||||||||||
Print and return native JavaScript argument. | (defmacro p
[x]
`(let [res# ~x]
(.log js/console res#)
res#)) | |||||||||||||||
Pretty print and return argument (uses | (defmacro pp
[x]
`(let [res# ~x]
(.log js/console (prn-str res#))
res#)) | |||||||||||||||
Profile | (defmacro profile
[descr & body]
`(let [start# (.getTime (js/Date.))
ret# (do ~@body)]
(print (str ~descr ": " (- (.getTime (js/Date.)) start#) " msecs"))
ret#)) | |||||||||||||||
(defmacro timeout [delay & body] `(js/setTimeout (fn [] ~@body) ~delay)) (defmacro interval [delay & body] `(js/setInterval (fn [] ~@body) ~delay)) | ||||||||||||||||
Define record and corresponding constructor that accepts keyword arguments. The constructor function is defined to be the given name, with the record having an underscore prefix. | (defmacro c2-obj
[name fields-with-defaults & body]
(let [recname (symbol (str "_" (clojure.core/name name)))]
`(do
(defrecord ~recname ~(into [] (map (comp symbol clojure.core/name)
(keys fields-with-defaults)))
~@body)
(defn ~name [& ~'kwargs]
(~(symbol (str "map->" (clojure.core/name recname)))
(merge ~fields-with-defaults (apply hash-map ~'kwargs))))))) | |||||||||||||||
Element-by-element operations between sequences. Used by c2.maths for vector arithmetic. Modified from Incanter. | (defmacro combine-with
[A B op fun]
`(cond
(and (number? ~A) (number? ~B)) (~op ~A ~B)
(and (coll? ~A) (coll? ~B)) (map ~op ~A ~B)
(and (number? ~A) (coll? ~B)) (map ~op (replicate (count ~B) ~A) ~B)
(and (coll? ~A) (number? ~B)) (map ~op ~A (replicate (count ~A) ~B)))) | |||||||||||||||
Merges | (defmacro bind!
[el hiccup-el]
`(let [co# (computed-observable ~hiccup-el)
$el# (c2.dom/->dom ~el)]
(singult.core/merge! $el# @co#)
(add-watch co# :update-dom #(singult.core/merge! $el# @co#))
co#)) | |||||||||||||||
Detaches | (defmacro with-detached
[el & body]
`(let [$el# (c2.dom/->dom ~el)
$parent# (c2.dom/parent $el#)]
(dom/remove! $el#)
~@body
(dom/append! $parent# $el#)
$el#)) | |||||||||||||||
Core functions that map data to DOM elements. | (ns c2.core
(:use-macros [c2.util :only [p pp]])
(:require [singult.core :as singult]
;;These namespaces required here so they're sucked into deps calculation.
;;(Some macros expand with calls to fns in these namespaces)
[c2.dom :as dom]
[reflex.core :as reflex])) | |||||||||||||||
(def node-data singult/node-data) | ||||||||||||||||
(defn unify [data mapping & args]
;;Execute the mapping on the first datum so that atoms within the mapping fn will be derefed.
;;This should eliminate confusion wherin c2.util#bind! doesn't pick up on dependencies within the unify mapping fn (because the mapping fn isn't executed until rendering, which happens after computed-observable dependencies are calculated).
(when (seq data)
(mapping (first data)))
(apply singult/unify data mapping args)) | ||||||||||||||||
(ns c2.dom
(:refer-clojure :exclude [val])
(:use-macros [c2.util :only [p pp timeout bind!]]
[clojure.core.match.js :only [match]])
(:require [clojure.string :as string]
[singult.core :as singult]
[goog.dom :as gdom]
[goog.dom.forms :as gforms]
[goog.dom.classes :as gclasses]
[goog.style :as gstyle])) | ||||||||||||||||
Going down a terrible, terrible road here... | (js* "Element.prototype.matchesSelector = Element.prototype.webkitMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector") | |||||||||||||||
Seq over native JavaScript node collections | (when (js* "typeof NodeList != \"undefined\"")
(extend-type js/NodeList
ISeqable
(-seq [array] (array-seq array 0)))) | |||||||||||||||
(extend-type js/HTMLCollection ISeqable (-seq [array] (array-seq array 0))) | ||||||||||||||||
(declare select) | ||||||||||||||||
(defprotocol IDom (->dom [x] "Converts x to a live DOM node")) | ||||||||||||||||
(extend-protocol IDom string (->dom [selector] (select selector)) PersistentVector (->dom [v] (singult/render v))) | ||||||||||||||||
(when (js* "typeof Node != \"undefined\"")
(extend-type js/Node
IDom
(->dom [node] node))) | ||||||||||||||||
Select a single DOM node via CSS selector, optionally scoped by second arg. Like select, but returns a collection of nodes. | (defn select ([selector] (.querySelector js/document selector)) ([selector container] (.querySelector (->dom container) selector))) (defn select-all ([selector] (.querySelectorAll js/document selector)) ([selector container] (.querySelectorAll (->dom container) selector))) | |||||||||||||||
Does live | (defn matches-selector? [node selector] (.matchesSelector node selector)) | |||||||||||||||
Return the children of a live DOM element. | (defn children [node] (.-children (->dom node))) | |||||||||||||||
Return parent of a live DOM node. | (defn parent [node] (.-parentNode (->dom node))) | |||||||||||||||
Make element last child of container. Returns live child. | (defn append!
[container el]
(let [el (->dom el)]
(gdom/appendChild (->dom container) el)
el)) | |||||||||||||||
Make element first child of container. Returns live DOM child. | (defn prepend!
[container el]
(let [el (->dom el)]
(gdom/insertChildAt (->dom container) el 0)
el)) | |||||||||||||||
Remove element from DOM and return it.
| (defn remove! [el] (gdom/removeNode (->dom el))) | |||||||||||||||
Replace live DOM node with a new one, returning the latter.
| (defn replace!
[old new]
(let [new (->dom new)]
(gdom/replaceNode new (->dom old))
new)) | |||||||||||||||
Get or set inline element style. | (defn style
([el] (throw (js/Error. "TODO: return map of element styles")))
([el x]
(let [el (->dom el)]
(match [x]
[(k :guard keyword?)] (gstyle/getComputedStyle el (name k))
[(m :guard map?)]
(do
(doseq [[k v] m] (style el k v))
el))))
([el k v]
(gstyle/setStyle (->dom el) (name k)
(match [v]
[s :guard string?] s
[n :guard number?]
(if (#{:height :width :top :left :bottom :right} (keyword k))
(str n "px")
n)))
el)) | |||||||||||||||
Get or set element attributes. | (defn attr
([el] (let [attrs (.-attributes (->dom el))]
(into {} (for [i (range (.-length attrs))]
[(keyword (.-name (aget attrs i)))
(.-value (aget attrs i))]))))
([el x]
(let [el (->dom el)]
(match [x]
[(k :guard keyword?)] (.getAttribute el (name k))
[(m :guard map?)]
(do (doseq [[k v] m] (attr el k v))
el))))
([el k v]
(let [el (->dom el)]
(if (nil? v)
(.removeAttribute el (name k))
(if (= :style k)
(style el v)
(.setAttribute el (name k) v)))
el))) | |||||||||||||||
Get or set element text, returning element | (defn text
([el]
(gdom/getTextContent (->dom el)))
([el v]
(let [el (->dom el)]
(gdom/setTextContent el v)
el))) | |||||||||||||||
Get or set element value. | (defn val
([el]
(gforms/getValue (->dom el)))
([el v]
(let [el (->dom el)]
(gforms/setValue el v)
el))) | |||||||||||||||
Add or remove | (defn classed! [el class classed?] (gclasses/enable (->dom el) (name class) classed?) el) | |||||||||||||||
TODO: make these kind of shortcuts macros for better performance. | (defn add-class! [el class] (classed! el class true)) (defn remove-class! [el class] (classed! el class false)) | |||||||||||||||
Call this fn with a fn that should be executed on the next browser animation frame. | (def request-animation-frame
(or (.-requestAnimationFrame js/window)
(.-webkitRequestAnimationFrame js/window)
#(timeout 10 (%)))) | |||||||||||||||
(ns c2.event
(:use [cljs.reader :only [read-string]]
[c2.core :only [node-data]])
(:require [c2.dom :as dom]
[goog.events :as gevents])) | ||||||||||||||||
Execute fn when browser load event fires. | (defn on-load [f] (.listen goog.events js/window goog.events.EventType.LOAD f)) | |||||||||||||||
Attach | (defn on-raw
[node event-type f
& {:keys [capture]
:or {capture false}}]
(gevents/listen (dom/->dom node) (name event-type) f capture)) | |||||||||||||||
Attach delegate Example usage:
This method should be preferred over attaching event handlers to individual nodes created by a | (defn on
([node event-type f] (on node "*" event-type f))
([node selector event-type f
& {:keys [capture]
:or {capture false}}]
(gevents/listen (dom/->dom node)
(name event-type)
(fn [event]
;;Check to see if the target is what we want to listen to.
;;This could be, say, a data-less button that is a child of a node with c2 data.
(if (dom/matches-selector? (.-target event) selector)
;;Loop through the parent nodes of the event origin node, event.target, until we reach one with c2 data attached.
(loop [$node (.-target event)]
(if-let [d (node-data $node)]
;;Then, call the handler on this node
(f d $node event)
(if-let [parent (dom/parent $node)]
(recur parent))))))
capture))) | |||||||||||||||
(ns c2.util (:require [singult.core :as singult])) | ||||||||||||||||
Convert something into a collection, if it's not already. | (defn ->coll [x] (if (coll? x) x [x])) | |||||||||||||||
^:clj (ns c2.geo.core
(:use [c2.maths :only [rad sq sqrt sin asin cos radians-per-degree]]
[clojure.core.match :only [match]]
[clojure.string :only [join]])) | ||||||||||||||||
^:cljs (ns c2.geo.core
(:refer-clojure :exclude [map])
(:use-macros [clojure.core.match.js :only [match]])
(:use [c2.maths :only [rad sq sqrt sin asin cos radians-per-degree]])) | ||||||||||||||||
Use JS native map and join fns; this is about 3 times faster than using CLJS seqs and str. | ^:cljs (do
(defn ->arr [c]
(if (= js/Array (type c))
c
(into-array c)))
(defn join
([c] (join c))
([sep c] (.join (->arr c) sep)))
(defn map [f c] (.map (->arr c) f))) | |||||||||||||||
Convert geoJSON to SVG path data. Kwargs:
| (defn geo->svg
[geo & {:keys [projection]
:or {projection identity}}]
(let [project (fn [coordinate]
(join "," (projection coordinate)))
coords->path (fn [coordinates]
(str "M"
(join "L" (map project coordinates))
"Z"))]
;;See http://geojson.org/geojson-spec.html
;;This SVG rendering doesn't implement the full spec.
(match [geo]
[{:type "FeatureCollection" :features xs}]
(join (map #(geo->svg % :projection projection) xs))
[{:type "Feature" :geometry g}] (geo->svg g :projection projection)
[{:type "Polygon" :coordinates xs}]
(join (map coords->path xs))
[{:type "MultiPolygon" :coordinates xs}]
;;It'd be nice to recurse to the actual branch that handles Polygon, instead of repeating...
(join (map (fn [subpoly]
(join (map coords->path subpoly)))
xs))))) | |||||||||||||||
Convert coordinates (potentially map of | (defn ->latlon
[coordinates]
(match [coordinates]
[[lat lon]] [lat lon]
[{:lat lat :lon lon}] [lat lon])) | |||||||||||||||
Radius of OUR AWESOME PLANET, in kilometers | (def radius-of-earth 6378.1) | |||||||||||||||
Calculate the great-circle distance between two lat/lon coordinates on a sphere with radius | (defn haversine
([co1 co2] (haversine co1 co2 radius-of-earth))
([co1 co2 r]
(let [[lat1 lon1] (->latlon co1)
[lat2 lon2] (->latlon co2)
square-half-chord (+ (sq (sin (/ (rad (- lat2 lat1)) 2)))
(* (cos (rad lat1))
(cos (rad lat2))
(sq (sin (/ (rad (- lon2 lon1)) 2)))))
angular-distance (* (asin (sqrt square-half-chord)) 2)]
(* angular-distance r)))) | |||||||||||||||
^:clj (ns c2.geo.geom
(:use [clojure.core.match :only [match]]
[clojure.string :only [join]]
[c2.maths :only [abs add sub div mul]])
(:require c2.geom.polygon)) | ||||||||||||||||
^:cljs (ns c2.geo.geom
(:use-macros [clojure.core.match.js :only [match]])
(:use [c2.maths :only [abs add sub div mul]])
(:require [c2.geom.polygon :as c2.geom.polygon])) | ||||||||||||||||
Calculate the area of a geoJSON feature. Kwargs:
| (defn area
[geo & {:keys [projection]
:or {projection identity}}]
(let [polygon-area (fn [poly-coordinates]
(let [area (fn [coordinates]
(abs (c2.geom.polygon/area (map projection coordinates))))]
;;area of exterior boundary - interior holes
(apply - (area (first poly-coordinates))
(map area (rest poly-coordinates)))))]
(abs (match [geo]
[{:type "FeatureCollection" :features xs}]
(apply + (map area xs))
[{:type "Feature" :geometry g}]
(area g)
[{:type "Polygon" :coordinates xs}]
(polygon-area xs)
[{:type "MultiPolygon" :coordinates xs}]
(apply + (map polygon-area xs)))))) | |||||||||||||||
Calculate the centroid coordinates of a geoJSON feature. Kwargs:
| (defn centroid
[geo & {:keys [projection]
:or {projection identity}}]
(let [polygon-centroid (fn [poly-coordinates]
;;Compute polygon centroid by geometric decomposition.
;;http://en.wikipedia.org/wiki/Centroid#By_geometric_decomposition
(let [areas (map (fn [coordinates]
(abs (c2.geom.polygon/area (map projection coordinates))))
poly-coordinates)]
;;Return hashmap containing the area so weighted centroid can be calculated for MultiPolygons.
{:centroid (div (apply sub (map (fn [coordinates area]
(mul (c2.geom.polygon/centroid coordinates)
area))
poly-coordinates areas))
(apply - areas))
:area (apply + areas)}))]
(match [geo]
[{:type "Feature" :geometry g}]
(centroid g)
[{:type "Polygon" :coordinates xs}]
(:centroid (polygon-centroid xs))
[{:type "MultiPolygon" :coordinates xs}]
(let [centroids (map polygon-centroid xs)]
(div (apply add (map (fn [{:keys [centroid area]}]
(mul centroid area))
centroids))
(apply add (map :area centroids))))))) | |||||||||||||||
^:clj (ns c2.geo.projection
(:use [c2.maths :only [radians-per-degree
sin cos sqrt]]
[c2.util :only [c2-obj]])) | ||||||||||||||||
^:cljs (ns c2.geo.projection
(:use-macros [c2.util :only [c2-obj]])
(:use [c2.maths :only [radians-per-degree
sin cos sqrt]])) | ||||||||||||||||
The Albers equal-area conic projection | (c2-obj albers
{:origin [-98 38]
:parallels [29.5, 45.5]
:scale 1000
:translate [480 250]}
clojure.lang.IFn
(invoke [this coordinates]
(let [[lon lat] coordinates
phi1 (* radians-per-degree (first parallels))
phi2 (* radians-per-degree (second parallels))
lng0 (* radians-per-degree (first origin))
lat0 (* radians-per-degree (second origin))
s (sin phi1), c (cos phi1)
n (* 0.5 (+ s (sin phi2)))
C (+ (* c c) (* 2 n s))
p0 (/ (sqrt (- C (* 2 n (sin lat0)))) n)
t (* n (- (* radians-per-degree lon)
lng0))
p (/ (sqrt (- C (* 2 n (sin (* radians-per-degree lat)))))
n)]
[(+ (* scale p (sin t)) (first translate))
(+ (* scale (- (* p (cos t)) p0)) ;;Note that we've negated the p0 - p*cos(t) term so the projection is into a coordinate system where positive y is downward.
(second translate))]))) | |||||||||||||||
Albers projection with Alaska, Hawaii, and Puerto Rico scaled and translated to fit nicely with each other | (c2-obj albers-usa
{:origin [-98 38]
:parallels [29.5, 45.5]
:scale 1000
:translate [480 250]}
clojure.lang.IFn
(invoke [_ coordinates]
(let [[lon lat] coordinates
lower48 (albers :origin origin
:parallels parallels
:scale scale
:translate translate)
[dx dy] translate
dz (/ (:scale lower48) 1000)
alaska (assoc lower48
:origin [-160 60]
:parallels [55 65]
:scale (* 0.6 (:scale lower48))
:translate [(- dx (* dz 400))
(+ dy (* dz 170))])
hawaii (assoc lower48
:origin [-160 20]
:parallels [8 18]
:translate [(- dx (* dz 190))
(+ dy (* dz 200))])
puerto-rico (assoc lower48
:origin [-60 10]
:parallels [8 18]
:scale (* 1.5 (:scale lower48))
:translate [(+ dx (* dz 580))
(+ dy (* dz 430))])]
((cond (and (> lat 50) (< lon -127)) alaska
(< lon -140) hawaii
(< lat 21) puerto-rico
:else lower48)
[lon lat])))) | |||||||||||||||
(ns c2.geom.polygon (:use [c2.maths :only [add div]])) | ||||||||||||||||
Closes a collection of coordinates by adding the first coordinate to the end | (defn close-coordinates
[coordinates]
(concat coordinates
[(first coordinates)])) | |||||||||||||||
Calculate area from list of counterclockwise coordinates, ref Wikipedia | (defn area
[coordinates]
(* 0.5 (apply + (map (fn [[[x0 y0] [x1 y1]]]
(- (* y0 x1)
(* x0 y1)))
(partition 2 1 (close-coordinates coordinates)))))) | |||||||||||||||
Calculate centroid from list of counterclockwise coordinates, ref Wikipedia | (defn centroid
[coordinates]
(div (apply add (map (fn [[[x0 y0] [x1 y1]]]
(let [cross (- (* y0 x1) (* x0 y1))]
[(* cross (+ x0 x1)), (* cross (+ y0 y1))]))
(partition 2 1 (close-coordinates coordinates))))
(* 6 (area coordinates)))) | |||||||||||||||
The histogram layout transforms data by grouping descrete data points into bins. (hist/histogram [{:name "sally" :age 20} {:name "al" :age 55} {:name "ali" :age 56} {:name "amanda" :age 12} {:name "andy" :age 26} {:name "brock" :age 30}] :value :age :range [13 30] :bins 3) | ||||||||||||||||
(ns c2.layout.histogram (:use [c2.maths :only [log]])) | ||||||||||||||||
(defn- binary-search [v target]
"VM agnostic j.u.Collections/binarySearch, from http://www.gettingclojure.com/cookbook:sequences"
(loop [low 0
high (dec (count v))]
(if (> low high)
(- (inc low))
(let [mid (quot (+ low high) 2)
mid-val (v mid)]
(cond (< mid-val target) (recur (inc mid) high)
(< target mid-val) (recur low (dec mid))
:else mid))))) | ||||||||||||||||
Calulate reasonable number of bins assuming an approximately normal distribution | (defn- sturges
[values]
(-> (count values)
(log 2)
int
inc)) | |||||||||||||||
Return the inclusive upper threshold of all bins | (defn- fixed-size-bins
[[mn mx] n]
(let [size (/ (- mx mn) n)]
(vec (for [step (range (inc n))]
(+ mn (* size step)))))) | |||||||||||||||
The histogram layout transforms data by grouping descrete data points into bins. Returns a collection of values with the following metadata set:
Kwargs:
| (defn histogram
[data & {:keys [value index range bins]
:or {value :value
range (fn [xs _] ((juxt (partial reduce min)
(partial reduce max))
xs))
bins (fn [r xs _] (fixed-size-bins r (sturges xs)))}}]
(let [values (map value data)
r (cond
(fn? range) (range values index)
:else range)
thresholds (cond
(fn? bins) (bins r values index)
(number? bins) (fixed-size-bins r bins)
:else bins)
binner (fn [e] (->> (value e)
; remove trailing threshold number
(binary-search (subvec thresholds 0 (dec (count thresholds))))
inc
Math/abs
dec))
groups (group-by binner
(filter #(and (>= (value %) (r 0))
(<= (value %) (r 1)))
data))]
(map-indexed (fn [index [mn mx]]
(let [group (or (groups index)
[])]
(with-meta group
{:x mn
:dx (- mx mn)
:y (count group)})))
(partition 2 1 thresholds)))) | |||||||||||||||
The partition layout transforms root node of hierarchy into a flat collection of nodes positioned and sized according to provided value fn. E.g., a doughnut plot can be created by partitioning the angular width and radius of a circle:
| ||||||||||||||||
(ns c2.layout.partition (:refer-clojure :exclude [partition])) | ||||||||||||||||
Transforms Kwargs:
| (defn partition
[root & {:keys [children value size output-key]
:or {children :children
value :value
size [1 1]
output-key :partition}}]
(defn depth [node]
(inc (if-let [cs (children node)]
(apply max (map depth cs))
0)))
(defn node-value [node]
(if-let [cs (children node)]
(apply + (map node-value cs))
(value node)))
(defn position [node depth x [dx dy]]
(concat
;;parent node
[(assoc node output-key (merge (output-key node)
{:depth depth
:value (node-value node)
:x x, :y (* depth dy)
:dx dx, :dy dy}))]
;;child nodes
(let [unit-cdx (/ dx (node-value node))
cs (children node)]
(flatten
(map (fn [child cx]
(position child (inc depth) cx [(* unit-cdx (node-value child)) dy]))
cs
;;Calculate each child's x-offset
(reductions (fn [cx child] (+ cx (* unit-cdx (node-value child))))
x cs))))))
(position root 0 0 [(first size) (/ (second size) (depth root))])) | |||||||||||||||
(comment
(use '[vomnibus.d3 :only [flare]])
(use '[c2.maths :only [Tau]])
(partition flare :value #(do % 1))
(partition {:name "rrr"
:children [ {:name "A"
:children [{:name "a" :value 1}
{:name "b" :value 1}
{:name "c" :value 1}]}
{:name "B"
:children [{:name "ba" :value 2}
{:name "bb" :value 2}
{:name "bc" :value 2}]}]})) | ||||||||||||||||
^:clj (ns c2.maths
(:use [c2.util :only [combine-with]])) | ||||||||||||||||
^:cljs (ns c2.maths
(:use-macros [c2.util :only [combine-with]])) | ||||||||||||||||
(def Pi Math/PI) (def Tau (* 2 Pi)) (def e Math/E) (def radians-per-degree (/ Pi 180)) (defn rad [x] (* radians-per-degree x)) (defn deg [x] (/ x radians-per-degree)) | ||||||||||||||||
(defn sin [x] (Math/sin x)) (defn asin [x] (Math/asin x)) (defn cos [x] (Math/cos x)) (defn acos [x] (Math/acos x)) (defn tan [x] (Math/tan x)) (defn atan [x] (Math/atan x)) | ||||||||||||||||
(defn expt ([x] (Math/exp x)) ([x y] (Math/pow x y))) | ||||||||||||||||
(defn sq [x] (expt x 2)) (defn sqrt [x] (Math/sqrt x)) | ||||||||||||||||
(defn floor [x] (Math/floor x)) (defn ceil [x] (Math/ceil x)) (defn abs [x] (Math/abs x)) | ||||||||||||||||
(defn log
([x] (Math/log x))
([base x] (/ (Math/log x)
(Math/log base)))) | ||||||||||||||||
(defn ^:clj log10 [x] (Math/log10 x))
(defn ^:cljs log10 [x] (/ (.log js/Math x)
(.-LN10 js/Math))) | ||||||||||||||||
Returns 2-vector of min and max elements in xs. | (defn extent [xs] [(apply min xs) (apply max xs)]) | |||||||||||||||
Arithemetic mean of collection TODO: replace mean and median with smarter algorithms for better performance. | (defn mean
[xs]
(/ (reduce + xs)
(count xs))) | |||||||||||||||
Median of a collection. | (defn median
[xs]
(let [sorted (sort xs)
n (count xs)]
(cond
(= n 1) (first sorted)
(odd? n) (nth sorted (/ (inc n) 2))
:else (let [mid (/ n 2)]
(mean [(nth sorted (floor mid))
(nth sorted (ceil mid))]))))) | |||||||||||||||
Inclusive range; same as core/range, but includes the end. | (defn irange
([start] (range start))
([start end] (concat (range start end) [end]))
([start end step]
(let [r (range start end step)]
(if (== (mod (first r) step)
(mod end step))
(concat r [end])
r)))) | |||||||||||||||
Checks if bottom <= x <= top. | (defn within? [x [bottom top]] (<= bottom x top)) | |||||||||||||||
element-by-element arithmetic Code modified from Incanter | (defn add
([& args] (reduce (fn [A B] (combine-with A B clojure.core/+ add)) args)))
(defn sub
([& args] (if (= (count args) 1)
(combine-with 0 (first args) clojure.core/- sub)
(reduce (fn [A B] (combine-with A B clojure.core/- sub)) args))))
(defn mul
([& args] (reduce (fn [A B] (combine-with A B clojure.core/* mul)) args)))
(defn div
([& args] (if (= (count args) 1)
(combine-with 1 (first args) clojure.core// div)
(reduce (fn [A B] (combine-with A B clojure.core// div)) args)))) | |||||||||||||||
Returns the quantiles of a dataset. Kwargs:
Algorithm is the same as R's quantile type=7. Transcribed from Jason Davies; https://github.com/jasondavies/science.js/blob/master/src/stats/quantiles.js | (defn quantile
[data & {:keys [probs] :or {probs [0 0.25 0.5 0.75 1]}}]
(let [xs (into [] (sort data))
n-1 (dec (count xs))]
(for [q probs]
(let [index (inc (* q n-1))
lo (int (floor index))
h (- index lo)
a (xs (dec lo))]
(if (= h 0)
a
(+ a (* h (- (xs lo) a)))))))) | |||||||||||||||
^:clj (ns c2.scale
(:use [c2.util :only [c2-obj]]
[c2.maths :only [expt]])
(:require [c2.maths :as maths]))
^:cljs (ns c2.scale
(:use-macros [c2.util :only [c2-obj]])
(:use [c2.maths :only [expt]])
(:require [c2.maths :as maths])) | ||||||||||||||||
(defprotocol IInvertable (invert [scale] "Inverted scale")) | ||||||||||||||||
Linear scale Kwargs:
| (c2-obj linear {:domain [0 1]
:range [0 1]}
clojure.lang.IFn
(invoke [_ x] (let [domain-length (- (last domain) (first domain))
range-length (- (last range) (first range))]
(+ (first range)
(* range-length
(/ (- x (first domain))
domain-length)))))
IInvertable
(invert [this]
(assoc this
:domain range
:range domain))) | |||||||||||||||
(declare log) | ||||||||||||||||
Power scale Kwargs:
| (c2-obj power {:domain [0 1]
:range [0 1]}
clojure.lang.IFn
(invoke [_ x]
((comp (linear :domain (map expt domain)
:range range)
expt) x))) | |||||||||||||||
Logarithmic scale Kwargs:
| (c2-obj log {:domain [1 10]
:range [0 1]}
clojure.lang.IFn
(invoke [_ x]
((comp (linear :domain (map maths/log domain)
:range range)
maths/log) x))) | |||||||||||||||
Collection of helpers for dealing with scalable vector graphics. Coordinates to any fn can be 2-vector | ^:clj (ns c2.svg
(:use [c2.core :only [unify]]
[c2.maths :only [Pi Tau radians-per-degree
sin cos mean]])) | |||||||||||||||
^:cljs (ns c2.svg
(:use [c2.core :only [unify]]
[c2.maths :only [Pi Tau radians-per-degree
sin cos mean]])
(:require [c2.dom :as dom])) | ||||||||||||||||
Stub for float fn, which does not exist on cljs runtime | ^:cljs (def float identity) | |||||||||||||||
Convert coordinates (potentially map of | (defn ->xy [coordinates] (cond (and (vector? coordinates) (= 2 (count coordinates))) coordinates (map? coordinates) [(:x coordinates) (:y coordinates)])) | |||||||||||||||
(defn translate [coordinates]
(let [[x y] (->xy coordinates)]
(str "translate(" (float x) "," (float y) ")"))) | ||||||||||||||||
(defn scale [coordinates]
(cond
(number? coordinates) (str "scale(" (float coordinates) ")")
(map? coordinates) [(:x coordinates) (:y coordinates)]
(and (vector? coordinates) (= 2 (count count))) coordinates)) | ||||||||||||||||
(defn rotate
([angle] (rotate angle [0 0]))
([angle coordinates]
(let [[x y] (->xy coordinates)]
(str "rotate(" (float angle) "," (float x) "," (float y) ")")))) | ||||||||||||||||
Returns map of | (defn ^:cljs get-bounds
[$svg-el]
(let [b (.getBBox $svg-el)]
{:x (.-x b)
:y (.-y b)
:width (.-width b)
:height (.-height b)})) | |||||||||||||||
Returns a transform string that will scale and center provided element | (defn transform-to-center
[element container]
(let [{ew :width eh :height x :x y :y} element
{w :width h :height} container
s (min (/ h eh) (/ w ew))]
(str (translate [(- (/ w 2) (* s (/ ew 2)))
(- (/ h 2) (* s (/ eh 2)))]);;translate scaled to center
" " (scale s) ;;scale
" " (translate [(- x) (- y)]) ;;translate to origin))) | |||||||||||||||
Scales and centers | (defn ^:cljs transform-to-center!
[$svg-el]
(let [$svg (.-ownerSVGElement $svg-el)
t (transform-to-center (get-bounds $svg-el)
{:width (js/parseFloat (dom/attr $svg :width))
:height (js/parseFloat (dom/attr $svg :height))})]
(dom/attr $svg-el :transform t))) | |||||||||||||||
Returns axis Kwargs:
| (defn axis
[scale ticks & {:keys [orientation
formatter
major-tick-width
text-margin
label
label-margin]
:or {orientation :left
formatter str
major-tick-width 6
text-margin 9
label-margin 28}}]
(let [[x y x1 x2 y1 y2] (case orientation
(:left :right) [:x :y :x1 :x2 :y1 :y2]
(:top :bottom) [:y :x :y1 :y2 :x1 :x2])
parity (case orientation
(:left :top) -1
(:right :bottom) 1)]
[:g {:class (str "axis " (name orientation))}
[:line.rule (apply hash-map (interleave [y1 y2] (:range scale)))]
[:g.ticks
;;Need to weave scale into tick stream so that unify updates nodes when the scale changes.
(unify (map vector ticks (repeat scale))
(fn [[d scale]]
[:g.tick.major-tick {:transform (translate {x 0 y (scale d)})}
[:text {x (* parity text-margin)} (formatter d)]
[:line {x1 0 x2 (* parity major-tick-width)}]]))]
(when label
[:text.label {:transform (str (translate {x (* parity label-margin)
y (mean (:range scale))})
" "
(case orientation
:left (rotate -90)
:right (rotate 90)
""))}
label])
])) | |||||||||||||||
(def ArcMax (- Tau 0.0000001)) | ||||||||||||||||
Calculate SVG path data for a circle of | (defn circle
([radius] (circle [0 0] radius))
([coordinates radius]
(let [[x y] (->xy coordinates)]
(str "M" (+ x radius) "," y
"A" (+ x radius) "," (+ y radius) " 0 1,1" (- (+ x radius)) "," y
"A" (+ x radius) "," (+ y radius) " 0 1,1" (+ x radius) "," y)))) | |||||||||||||||
Calculate SVG path data for an arc. | (defn arc
[& {:keys [inner-radius, outer-radius
start-angle, end-angle, angle-offset]
:or {inner-radius 0, outer-radius 1
start-angle 0, end-angle Pi, angle-offset 0}}]
(let [r0 inner-radius
r1 outer-radius
[a0 a1] (sort [(+ angle-offset start-angle)
(+ angle-offset end-angle)])
da (- a1 a0)
large-arc-flag (if (< da Pi) "0" "1")
s0 (sin a0), c0 (cos a0)
s1 (sin a1), c1 (cos a1)]
;;SVG "A" parameters: (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
;;see http://www.w3.org/TR/SVG/paths.html#PathData
(if (>= da ArcMax)
;;Then just draw a full annulus
(str "M0," r1
"A" r1 "," r1 " 0 1,1 0," (- r1)
"A" r1 "," r1 " 0 1,1 0," r1
(if (not= 0 r0) ;;draw inner arc
(str "M0," r0
"A" r0 "," r0 " 0 1,0 0," (- r0)
"A" r0 "," r0 " 0 1,0 0," r0))
"Z")
;;Otherwise, draw the wedge
(str "M" (* r1 c0) "," (* r1 s0)
"A" r1 "," r1 " 0 " large-arc-flag ",1 " (* r1 c1) "," (* r1 s1)
(if (not= 0 r0) ;;draw inner arc
(str "L" (* r0 c1) "," (* r0 s1)
"A" r0 "," r0 " 0 " large-arc-flag ",0 " (* r0 c0) "," (* r0 s0))
"L0,0")
"Z")))) | |||||||||||||||
Implementation of An Extension of Wilkinson’s Algorithm for Positioning Tick Labels on Axes by Justin Talbot, Sharon Lin, and Pat Hanrahan. See also Talbot's website. | ||||||||||||||||
^:clj (ns c2.ticks
(:use [c2.maths :only [sq ceil floor log10 expt irange within?]]
[iterate :only [iter]])) | ||||||||||||||||
^:cljs (ns c2.ticks
(:use-macros [iterate :only [iter]])
(:use [c2.maths :only [sq ceil floor log10 expt irange within?]])) | ||||||||||||||||
Preference-ordered list of nice step sizes | (def Q [1 5 2 2.5 4 3]) | |||||||||||||||
Index of x in coll | (defn index-of
[x coll]
(first (for [[idx y] (map-indexed vector coll)
:when (= y x)] idx))) | |||||||||||||||
(defn label-range-contains-zero? [l-min l-max l-step] (and (> l-max 0) (< l-min 0) (zero? (mod l-min l-step)))) | ||||||||||||||||
Objective function modeling niceness of step sizes and whether a range includes zero | (defn simplicity
[q j label-range-contains-zero]
(let [v (if label-range-contains-zero 1 0)]
(if (<= (count Q) 1)
(+ (- 1 j) v)
(+ (- 1 (/ (index-of q Q) (dec (count Q))) j)
v)))) | |||||||||||||||
(defn max-simplicity [q j] (simplicity q j true)) | ||||||||||||||||
Objective function based on distances between extreme data and extreme labels | (defn coverage
[d-min d-max l-min l-max]
(- 1 (* 0.5 (/ (+ (sq (- d-max l-max))
(sq (- d-min l-min)))
(sq (* 0.1 (- d-max d-min))))))) | |||||||||||||||
When the label range is centered on the data range | (defn max-coverage
[d-min d-max span]
(let [d-range (- d-max d-min)]
(if (> span d-range)
(- 1 (sq (/ (- span d-range)
(* 0.2 d-range))))
1))) | |||||||||||||||
Objective function for a candidate density | (defn density [r rt] ;;Note the formula should be 2-, not 1- as in the paper. (- 2 (max (/ r rt) (/ rt r)))) | |||||||||||||||
Since the arguments are the same, I don't see what the story is with density / max density. This isn't satisfactorily explained in the paper. | (defn max-density [r rt]
(if (>= r rt)
(- 2 (/ r rt))
1)) | |||||||||||||||
Balance the relative merits of different metrics | (defn- w
[[simplicity coverage density legibility]]
(let [w [0.2 0.25 0.5 0.05]]
(+ (* simplicity (w 0))
(* coverage (w 1))
(* density (w 2))
(* legibility (w 3))))) | |||||||||||||||
Find best ticks for the data range Kwargs:
Since there are no test input/output datasets for the labeling algorithm, I played it safe and copied the imperative algorithm from the paper. If you rewrite it in an understandable and performant functional style, I'll accept a pull request and buy you a bottle of whiskey. | (defn search
[[d-min d-max] & {:keys [target-density length
clamp?]
:or {target-density 0.01 ;;Default to one label per 100 px
length 500
clamp? false}}]
(let [best-score (atom -2)
label (atom {})]
(iter {for q in Q}
(iter {for j from 1}
{for ms = (max-simplicity q j)}
{return-if (< (w [ms 1 1 1]) @best-score)}
(iter {for k from 2}
{for md = (max-density (/ k length) target-density)}
{return-if (< (w [ms 1 md 1]) @best-score)}
(let [delta ;;power of ten by which to multiply the step size
(/ (- d-max d-min)
(* (inc k) j k))]
(iter {for z from (ceil (log10 delta))}
{for l-step = (* q j (expt 10 z))}
{for mc = (max-coverage d-min d-max (* (dec k) l-step))}
{return-if (< (w [ms mc md 1]) @best-score)}
(iter {for start
from (- (floor (/ d-max l-step))
(dec k))
to (/ d-min l-step)
by (/ 1 j)}
{for l-min = (* start l-step)}
{for l-max = (+ l-min (* (dec k) l-step))}
{for s = (simplicity q j (label-range-contains-zero? l-min l-max l-step))}
{for c = (coverage d-min d-max l-min l-max)}
{for d = (density (/ k length) target-density)}
{for score = (w [s c d 1])}
{return-if (< score @best-score)}
;;(println "inner loop")
;;todo, optimize legibility
(reset! best-score score)
(reset! label {:min l-min
:max l-max
:step l-step})))))))
(let [l @label
extent [(if clamp? d-min (min (:min l) d-min))
(if clamp? d-max (max (:max l) d-max))]]
{:extent extent
:min (first extent) :max (second extent)
:ticks (filter #(within? % extent)
(irange (:min l) (:max l) (:step l)))}))) | |||||||||||||||
(comment
(search [0.0 5000.0]
:length 900
:clamp? true)
(search [1 9])
(search [1 9] :target-density (/ 1 30))
(search [10.4 33.9]
:clamp? false)
) | ||||||||||||||||