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) ) | ||||||||||||||||