diff --git a/clojurescript/boids/boids.asciidoc b/clojurescript/boids/boids.asciidoc new file mode 100644 index 00000000..8aa173e9 --- /dev/null +++ b/clojurescript/boids/boids.asciidoc @@ -0,0 +1,239 @@ +=== Animated SVG graphics in ClojureScript + +[role="byline"] +by Tom White + +==== Problem + +You want to build an animated graphical SVG application on an HTML page (for example, a Boids simulation). + +==== Solution + +You can use the https://github.com/dribnet/strokes[Strokes library] to employ D3 from ClojureScript. + +In your dependencies: + +[source, clojure] +---- +[net.drib/strokes "0.5.0"] +---- + + +Before looking at the whole version, try building a minimal viable +solution to see the core bits of creating objects and animating them +in a web page. + +[source,clojure] +---- +(ns boids + (:require [strokes :refer [d3 timer category10]])) + +(strokes/bootstrap) + +(def width 960) +(def height 500) + +(def boids (atom [ + {:id "a" :x 200 :y 200} + {:id "b" :x 200 :y 300} + {:id "c" :x 300 :y 200} + {:id "d" :x 400 :y 300} + {:id "f" :x 500 :y 300}])) + +(def colorfn (category10)) + +(def svg (-> d3 (.select "body") (.append "svg") + (.attr {:width width :height height}))) + +(defn draw-boids [] + (let [circles (-> svg (.selectAll "circle") (.data @boids))] + (-> circles (.enter) + (.append "circle") + (.style "fill" #(colorfn %2))) + + (-> circles + (.attr {:transform #(str "translate(" (:x %) "," (:y %) ")") + :r 10})))) + +(defn update-one-boid [b] + (let [brownianize #(+ % -3 (rand-int 7)) + newx (brownianize (:x b)) + newy (brownianize (:y b))] + (merge b {:x newx :y newy}))) + +(defn update-boids [] + (swap! boids #(mapv update-one-boid %))) + +(timer (fn [] + (update-boids) + (draw-boids) + false)) +---- + +http://s.trokes.org/dribnet/6460749[Live demo here] + +==== Discussion + + +https://github.com/dribnet/strokes[Strokes] is a library which +allows for idiomatic usage of http://d3js.org/[D3.js] from +ClojureScript. Here, five objects were put in a vector wrapped by an atom +to represent the boids, and then D3's timer function was used to +repeatedly update and rerender these objects. Before getting too +distracted by the logic of intelligent boids, it's useful to see how +short the code is when the boids are simply circles moving randomly on +the screen. + +The main update logic of the model happens in update-one-boid, which takes a boid +as input and returns the same boid with an updated position. The draw-boids function +then simply takes whatever boids are in the atom and renders each at the correct position. + +Finally, take a look at the full version, which has the same basic structure, but which +includes all of the logic needed to run a full boids simulation. This version was +adapted from Sean Luke's excellent Java-based http://cs.gmu.edu/~eclab/projects/mason/[mason simulation library] +and is written with some glaring inefficiencies to make it easier to understand. + +http://s.trokes.org/dribnet/6460753[live demo here.] + +[source, clojure] +---- +(ns boids + (:require [strokes :refer [d3 timer]])) + +(strokes/bootstrap) + +(def width 960) +(def height 500) + +(def settings { :randomness 4.0 + :neighborhood 80.0 + :momentum 1.0 + :avoidance 8.0 + :cohesion 1.0 + :consistency 0.4 + :jump 3.0 + :dead-prop 0.25 }) + +(defn rand-range [low high] + (+ low (rand (- high low)))) + +(defn newboid [] + {:id (gensym "boid-") + :loc [(rand-int width) (rand-int height)] + :lastd (mapv #(rand-range -1 1) (range 2)) + :dead (< (rand) (:dead-prop settings))}) + +(def boids (atom (vec (take 40 (repeatedly newboid))))) + +(def colorfn (strokes/category20c)) + +(def svg (-> d3 (.select "body") (.append "svg") + (.attr {:width width :height height}))) + +(defn orientation [b] + (let [[dx dy] (:lastd b)] + (if (and (zero? dx) (zero? dy)) + 0 + (Math/atan2 dy dx)))) + +(defn angle-to-svg [a] + (/ (* 180 a) Math/PI)) + +(defn boid-transform [b] + (let [[x y] (:loc b)] + (str "translate(" x "," y ") " + "rotate (" (-> b orientation angle-to-svg) ")"))) + +(defn draw-boids [] + (let [shapes (-> svg (.selectAll "polygon") (.data @boids))] + (-> shapes (.enter) + (.append "polygon") + (.attr "points" "20,0 0,10 -10,0 0,-10") + (.style "fill" #(if (:dead %1) "black" (colorfn %2)))) + (-> shapes + (.attr {:transform boid-transform})))) + +(defn momentum [b] + (:lastd b)) + +(defn randomness [b] + (let [s 0.05 + x (rand-range -1.0 1.0) + y (rand-range -1.0 1.0) + l (Math/sqrt (+ (* x x) (* y y)))] + [(/ (* s x) l) (/ (* s y) l)])) + +(defn avoidance [b nbrs] + (let [pos (:loc b) + dxys (mapv #(mapv - pos (:loc %)) nbrs) + lensquared (mapv (fn [[x y]] (+ (* x x) (* y y))) dxys) + xys (mapv (fn [[dx dy] l] + (let [denom (+ (* l l) 1)] [(/ dx denom) (/ dy denom)])) + dxys lensquared) + v (reduce #(mapv + % %2) [0 0] xys) + ct (if (empty? nbrs) 1 (count nbrs))] + (mapv #(/ (* 9000 %) ct) v))) + +(defn cohesion [b nbrs] + (let [pos (:loc b) + dxys (mapv #(mapv - pos (:loc %)) nbrs) + v (reduce #(mapv + % %2) [0 0] dxys) + ct (if (empty? nbrs) 1 (count nbrs))] + (mapv #(/ (/ % -100) ct) v))) + +(defn consistency [b nbrs] + (let [pos (:loc b) + dxys (mapv momentum nbrs) + v (reduce #(mapv + % %2) [0 0] dxys) + ct (if (empty? nbrs) 1 (count nbrs))] + (mapv #(/ % ct) v))) + +(defn wrap [[x y]] + [(mod x width) (mod y height)]) + +(defn is-near? [pos r b] + (let [dv (mapv - pos (:loc b)) + md (reduce + (mapv Math/abs dv))] + ; are we already outside the bounding box (or coincident) + (if (or (> md r) (zero? md)) + false + (let [[x y] dv + l (Math/sqrt (+ (* x x) (* y y)))] + (< l r))))) + +(defn neighbors-of [b] + (filter (partial is-near? (:loc b) (:neighborhood settings)) @boids)) + +(defn update-one-boid [b] + (if (:dead b) + b + (let [loc (:loc b) + neighbors (neighbors-of b) + live-neighbors (remove :dead neighbors) + ran (mapv #(* % (:randomness settings)) (randomness b)) + mom (mapv #(* % (:momentum settings)) (momentum b)) + avd (mapv #(* % (:avoidance settings)) (avoidance b neighbors)) + coh (mapv #(* % (:cohesion settings)) (cohesion b live-neighbors)) + con (mapv #(* % (:consistency settings)) (consistency b live-neighbors)) + [dx dy] (mapv + ran mom avd coh con) + dis (Math/sqrt (+ (* dx dx) (* dy dy))) + jump (:jump settings) + nowd (if (> dis 0) + (map #(* (/ % dis) jump) [dx dy]) + [0 0]) + lastd (mapv #(+ (* 0.7 %) (* 0.3 %2)) (momentum b) nowd) + loc (mapv + loc lastd)] + (merge b {:loc (wrap loc) :lastd lastd})))) + +(defn update-boids [] + (swap! boids #(mapv update-one-boid %))) + +(timer (fn [] + (update-boids) + (draw-boids) + false)) +---- + +==== See Also + +* http://en.wikipedia.org/wiki/Boids[Boids] diff --git a/clojurescript/c2/c2.asciidoc b/clojurescript/c2/c2.asciidoc new file mode 100644 index 00000000..88b382a0 --- /dev/null +++ b/clojurescript/c2/c2.asciidoc @@ -0,0 +1,85 @@ +=== Visualizing Data with C2 +[role="byline"] +by Kevin Lynagh + +==== Problem + +You want to visualize data with HTML or SVG and CSS. + +==== Solution + +Use https://github.com/lynaghk/c2[C2] to draw, e.g., a bar chart. + +In your dependencies: + +[source, clojure] +---- +[com.keminglabs/c2 "0.2.3"] +---- + +[source,css] +--- +.bar { + width: 100% +} +.bar-fill { + display: inline-block; + background-color: gray; + height: 2em; +} +.label { + display: block; + vertical-align: middle; + width: 2em; +} +--- + +[source,clojure] +---- +(ns barchart + (:require-macros [c2.util :refer [bind!]]) + (:require [c2.core :refer [unify]] + [c2.scale :as scale])) + +(def !my-data (atom {"A" 1, "B" 2, "C" 4, "D" 3})) + +(def x-scale + (scale/linear :domain [0 4] :range [0 100])) + +(def x-scale-percent + #(str (x-scale %) "%")) + +(bind! "#barchart" + [:div#barchart + [:h2 "Rad barchart!"] + [:table + (unify @!my-data + (fn [[label val]] + [:tr + [:td + [:span.label label]] + [:td.bar + [:div.bar-fill {:style {:width (x-scale-percent val)}}]]]))]]) +---- + + +==== Discussion + +C2 is a D3-inspired data visualization library for Clojure and ClojureScript. +As with D3, the core idea is to build declarative mappings from your data to HTML or SVG markup. +This lets you leverage CSS and the existing web ecosystem to construct bespoke data visualizations. + +Compose pure functions to map your data to a Hiccup representation of the DOM, and then let C2 handle rendering into actual elements. +The `bind!` macro merges its body (which should evaluate into the desired Hiccup representation of the DOM) into the specified selector. +Watches are automatically added to any atoms dereferenced within the body, which is reevaluated (and DOM updated) when any of those atoms update. + +C2 includes many functions useful for visualizing data, including linear and logarithmic scales, map projections, and axis templates. +Since C2 visualizations are just functions that output Hiccup representations of the DOM, they can be used both in ClojureScript (where they access a live DOM) and Clojure (where they render as markup strings). + +TIP: Just because C2's ClojureScript Hiccup renderer allows you to inline CSS properties directly as maps doesn't mean you should put all of your styling there. +Keep the inline styles limited to the visual encodings you are making from the data (`left`/`top` in a scatterplot, `width` in a bar chart, `color` in a choropleth) and neatly place style-related CSS in full stylesheets. + +==== See Also + +* The extensive http://d3js.org/[D3.js] gallery for what's possible with HTML/SVG + CSS +* Any book by Stephen Few for practical tips on visualizing data diff --git a/clojurescript/cljsbuild/cljsbuild.asciidoc b/clojurescript/cljsbuild/cljsbuild.asciidoc new file mode 100644 index 00000000..9efb6ebb --- /dev/null +++ b/clojurescript/cljsbuild/cljsbuild.asciidoc @@ -0,0 +1,81 @@ +=== Getting set up with lein-cljsbuild +[role="byline"] +by Eric Normand + +==== Problem + +You want to compile ClojureScript to Javascript easily. + +==== Solution + +Use https://github.com/emezeske/lein-cljsbuild[lein-cljsbuild], a +Leiningen plugin for compiling ClojureScript applications. + +In your project.clj: + +[source, clojure] +---- +:plugins [[lein-cljsbuild "0.3.2"]] + +:cljsbuild {:builds [{:source-paths ["src-cljs"]}]} +---- + +Then, at the command line in your project's directory: + +[source, shell] +---- +$ lein cljsbuild once +---- + +The compiler will output to target/cljsbuild-main.js + +==== Discussion + +The first time you run lein-cljsbuild, it will fetch and use the +ClojureScript compiler and the Google Closure compiler. + +Above, you ran the `once` command to compile the code once. You can +run `auto` to have the plugin automatically recompile whenever the +source files change. For the best development experience, leave `auto` +running while you code. It will output any errors and warnings to the +terminal. + +[source, shell] +---- +$ lein cljsbuild auto +---- + +To remove the compiled files generated by lein-cljsbuild, run the +`clean` command: + +[source, shell] +---- +$ lein cljsbuild clean +---- + +There are various options for configuring the builds. All of the +options are set in the `:cljsbuild` entry in the project.clj. You can +have multiple builds, each with their own options. + +Here is a commented example showing some of the more common +options. This should go in project.clj. + +[source, clojure] +---- +:cljsbuild {:builds [{:source-paths ["src-cljs"] + :compiler { + ;; use the externs from jQuery + :externs ["externs/jquery-1.9.js"] + ;; output the compilation to a different file + :output-to "target/main.js" + ;; output code in a human-readable indentation + :pretty-print true + ;; turn on advanced optimizations + :optimizations :advanced}}]} +---- + +==== See Also + +* See the https://github.com/emezeske/lein-cljsbuild[lein-cljsbuild] GitHub repository for more commands. +* https://github.com/emezeske/lein-cljsbuild/blob/0.3.2/sample.project.clj[This +sample project.clj file] for information about all of the options. diff --git a/clojurescript/clojurescript.asciidoc b/clojurescript/clojurescript.asciidoc new file mode 100644 index 00000000..03bd53a4 --- /dev/null +++ b/clojurescript/clojurescript.asciidoc @@ -0,0 +1,12 @@ +[[ch_clojurescript]] +== ClojureScript + +include::cljsbuild/cljsbuild.asciidoc[] + +include::dommy/dommy.asciidoc[] + +include::c2/c2.asciidoc[] + +include::jsinterop/jsinterop.asciidoc[] + +include::boids/boids.asciidoc[] diff --git a/clojurescript/dommy/dommy.asciidoc b/clojurescript/dommy/dommy.asciidoc new file mode 100644 index 00000000..e468cf51 --- /dev/null +++ b/clojurescript/dommy/dommy.asciidoc @@ -0,0 +1,208 @@ +=== DOM manipulation and client side templating in ClojureScript +[role="byline"] +by Ian Davis, Aria Haghighi + +==== Problem + +You want to create and manipulate DOM nodes natively in ClojureScript + +==== Solution + +Use https://github.com/Prismatic/dommy[Dommy], an open source project that provides fast, functional DOM +manipulation and templating. + +Add the following dependency to your +project.clj+: + +[source, clojure] +---- +[prismatic/dommy "0.1.1"] +---- + +Dynamically generating DOM elements is simple with Dommy, and matches the excellent syntax of +https://github.com/weavejester/hiccup[Hiccup], a library for server-side templating in Clojure. + +[source, clojure] +---- +(def +default-avatar-attrs+ + {:style {:width "70px" + :height "70px"}}) + +(deftemplate avatar [user] + [:a.user-avatar {:href (:profile-url user) + [:img.user-avatar-image ^:attrs (assoc +default-avatar-attrs+ :src (:img-url user)]] +---- + +Composing templates programmatically is simple too, giving you the ability to write much more +functional and idiomatic code. + +[source, clojure] +---- +(deftemplate list-element [item] + [:li ^:text (:text item)]) + +(deftemplate my-list [items] + [:#my-list.shopping-list + [:ul (map list-element items)] + [:button.remove-button "Clear list"]) +---- + +To select for a DOM element, use either the +sel1+ function, which returns the first element matching a +selector, or +sel+ which returns a +seq+ of all elements matching the selector. They take two arguments, +the reference node and the selector. If you're just selecting from the +Document+ as a whole, you can omit +the reference node. + +[source, clojure] +---- +(sel (my-list some-items) :ul) +(sel1 :#login-submit-button) +---- + +For manipulating DOM nodes, use any of the functions in +dommy.core+, depending on what kind of manipulation +you want to do. If you're planning to do multiple changes at once to a single DOM node, make sure to use the ++->+ macro to keep things clean and organized. + +[source, clojure] +---- +(-> (sel1 :#login-submit-button) + (dommy/set-style! :width "100px") + dommy/show!) +---- + +For event-listeners, Dommy provides some extra-special syntactic sugar. For instance, if you want to listen to +multiple events on a node, you can just list them both in the same call to +dommy/listen!+. + +[source, clojure] +---- +(dommy/listen! (sel1 :button.pwn) + :mousedown (fn [e] (js/alert "Pwn!")) + :touchstart (fn [e] (js/alert "Phone Pwn!")))) +---- + +If you want to listen dynamically on a node that can get switched out dynamically, use Dommy's live-listener +syntax. This requires that the reference node exist, but it checks for the existence of the child node when +the event actually fires. + +[source, clojure] +---- +(dommy/listen! [js/document :li.todo-list-item] + :click #(js/alert "Removed item")) +---- + +Unlike in jQuery, Dommy methods only work on a single DOM object at a time. So, if you want to operate on all +DOM nodes of a given type (like what +sel+ returns) use ClojureScript's built-in sequence processing functions. +Since many Dommy methods operate using side-effects, use +doseq+ or +doall+ to avoid getting bitten by Clojure's +lazy sequences. + +[source, clojure] +---- +(->> (sel [:ul.my-list :li]) + (sort-by #(-> % (sel1 :input.last-name) .-value)) + (filter #(-> % (sel1 :input.age) .-value int (>= 21))) + (take 10) + (doseq #(dommy/add-class! % :first-ten-adults))) +---- + +==== Discussion + +Let's take a closer look at what's actually going on in the two templates from before. + +[source, clojure] +---- +(def +default-avatar-attrs+ + {:style {:width "70px" + :height "70px"}}) + +(deftemplate avatar [user] + [:a.user-avatar {:href (:profile-url user) + [:img.user-avatar-image ^:attrs (assoc +default-avatar-attrs+ :src (:img-url user))]]) + +(deftemplate list-element [item] + [:li ^:text (:text item)]) + +(deftemplate my-list [items] + [:#my-list.shopping-list + [:ul (map list-element items)] + [:button.remove-button "Clear list"]) +---- + +* The first keyword in each vector always describes the element tag, including its classes and id, using css syntax. +If no tag is specified, it defaults to div. Thus, the +:#my-list.shopping-list+ becomes ++
...
+. +* If the second item in the vector is a map literal, or is type-hinted with +^:attrs+, then it's treated as an +attribute map. +* Any string literals (like +"Clear list"+ above), can simply be included in the vector. However, as with attributes, +if you're generating the value dynamically, you must type-hint it with +^:text+. +* The remaining items in the vector are all just recursively generated as child nodes. +* Any +lists+, like the +(map ...)+ above, create a document fragment, which means they eventually just get resolved +as a flat list of child nodes. + +The reason you have to typehint when using +deftemplate+ is because Dommy relies on macros under the hood. In fact, +Dommy uses macros everywhere to help the ClojureScript compiler generate the most efficient Javascript it can. +This makes its templating and DOM selection faster than what you can get in Javascript. + +[source, clojure] +---- +(sel1 :body) ; => document.body +(sel1 :#my-id) ; => document.getElementById("my-id") +(sel parent :.child) ; => [].slice.call(parent.getElementsByClassName("child")) +(sel ".c1, .c2") ; => [].slice.call(document.querySelector(".c1, .c2")) + +(deftemplate example [datum] + [:li [:a {:href (str "#show/" (:key datum))} + [:div.class1.class2 {:id (str "item" (:key datum))} + [:span.anchor (:name datum)]]]]) + +---- +[source, javascript] +---- +$('body') // 15x slower than (sel1 :body) +$('#my-id') // 7x slower than (sel1 :#my-id) +$('.child', parent) // 2.5x slower than (sel parent :.child) +$('.c1, .c2') // 1.2x slower than (sel ".c1, .c2") + +$('li').append( + $('').attr('href', '#show/' + datum.key) + .addClass('anchor') + .append( $('
').addClass('class1').addClass('class2') + .attr('id', 'item' + datum.key) + .append( $('').text(datum.name) ))) +// 3.5x slower than (deftemplate example ...) +---- + +And, finally, Dommy is very extensible. If you have your own objects that you want to play nice with Dommy, +all you have to do is implement the +PElement+ protocol. For each side-effect, Dommy returns the original input, +not the DOM node, which allows you to effortlessly combine the chaining of both DOM manipulation and object +manipulation: + +[source, clojure] +---- +(defprotocol PWidget + (do-something! [this])) + +(defrecord Widget [container data] + dommy.templates/PElement + (-elem [this] container) + PWidget + (do-something! [this] + (do-stuff! data))) + +(defn make-widget [data] + (Widget. [:.widget data])) + +(-> (make-widget “BIG DATA”) + (dommy/add-class! :buzzword) + do-something! + (dommy/insert-after! (sel1 :.pre-widget-thing))) + +---- + +==== See Also + +* https://github.com/Prismatic/dommy[The source on GitHub] to examine Dommy in more depth and propose changes. +* http://blog.fogus.me/2012/04/25/the-clojurescript-compilation-pipeline/[The ClojureScript Compilation Pipeline], +an excellent blog post by Fogus, to understand exactly how the macro-compilation process works. +* The http://blog.getprismatic.com/blog/2013/1/22/the-magic-of-macros-lighting-fast-templating-in-clojurescript[Templating] and +http://blog.getprismatic.com/blog/2013/4/29/faster-better-dom-manipulation-with-dommy-and-clojurescript[DOM manipulation] +posts on the Prismatic blog for a more detailed look into Dommy's performant use of macros. +* The https://github.com/ibdknox/jayq[JayQ], https://github.com/ckirkendall/enfocus[Enfocus], or https://github.com/levand/domina[Domina] +DOM manipulation libraries, if Dommy doesn't do the trick for you. diff --git a/clojurescript/jsinterop/jsinterop.asciidoc b/clojurescript/jsinterop/jsinterop.asciidoc new file mode 100644 index 00000000..7567dc7f --- /dev/null +++ b/clojurescript/jsinterop/jsinterop.asciidoc @@ -0,0 +1,225 @@ +=== Using JavaScript Libraries from ClojureScript +[role="byline"] +by Tom White + +==== Problem + +You want to use an existing JavaScript library from ClojureScript as painlessly as possible. + +==== Solution + +Use https://github.com/dribnet/mrhyde[mrhyde] to map between JavaScript and ClojureScript datatypes. + +As an example, let's assume you wanted to use the rickshaw library, which provides a simple framework +for drawing charts of time series data on a web page. Naturally, you'd rather not worry about JavaScript +semantics, but instead hope to simply declaratively generate charts from ClojureScript datatypes. + +We'll use the +http://code.shutterstock.com/rickshaw/tutorial/introduction.html#example_07[fanciest example] from +the rickshaw documentation and adapt it to ClojureScript using mrhyde. + +The *JavaScript* for this example is reprinted verbatim below. + +[source, javascript] +---- +var palette = new Rickshaw.Color.Palette(); + +var graph = new Rickshaw.Graph( { + element: document.querySelector("#chart"), + width: 540, + height: 240, + renderer: 'line', + series: [ + { + name: "Northeast", + data: [ { x: -1893456000, y: 25868573 }, { x: -1577923200, y: 29662053 }, { x: -1262304000, y: 34427091 }, { x: -946771200, y: 35976777 }, { x: -631152000, y: 39477986 }, { x: -315619200, y: 44677819 }, { x: 0, y: 49040703 }, { x: 315532800, y: 49135283 }, { x: 631152000, y: 50809229 }, { x: 946684800, y: 53594378 }, { x: 1262304000, y: 55317240 } ], + color: palette.color() + }, + { + name: "Midwest", + data: [ { x: -1893456000, y: 29888542 }, { x: -1577923200, y: 34019792 }, { x: -1262304000, y: 38594100 }, { x: -946771200, y: 40143332 }, { x: -631152000, y: 44460762 }, { x: -315619200, y: 51619139 }, { x: 0, y: 56571663 }, { x: 315532800, y: 58865670 }, { x: 631152000, y: 59668632 }, { x: 946684800, y: 64392776 }, { x: 1262304000, y: 66927001 } ], + color: palette.color() + }, + { + name: "South", + data: [ { x: -1893456000, y: 29389330 }, { x: -1577923200, y: 33125803 }, { x: -1262304000, y: 37857633 }, { x: -946771200, y: 41665901 }, { x: -631152000, y: 47197088 }, { x: -315619200, y: 54973113 }, { x: 0, y: 62795367 }, { x: 315532800, y: 75372362 }, { x: 631152000, y: 85445930 }, { x: 946684800, y: 100236820 }, { x: 1262304000, y: 114555744 } ], + color: palette.color() + }, + { + name: "West", + data: [ { x: -1893456000, y: 7082086 }, { x: -1577923200, y: 9213920 }, { x: -1262304000, y: 12323836 }, { x: -946771200, y: 14379119 }, { x: -631152000, y: 20189962 }, { x: -315619200, y: 28053104 }, { x: 0, y: 34804193 }, { x: 315532800, y: 43172490 }, { x: 631152000, y: 52786082 }, { x: 946684800, y: 63197932 }, { x: 1262304000, y: 71945553 } ], + color: palette.color() + } + ] +} ); + +var x_axis = new Rickshaw.Graph.Axis.Time( { graph: graph } ); + +var y_axis = new Rickshaw.Graph.Axis.Y( { + graph: graph, + orientation: 'left', + tickFormat: Rickshaw.Fixtures.Number.formatKMBT, + element: document.getElementById('y_axis'), +} ); + +var legend = new Rickshaw.Graph.Legend( { + element: document.querySelector('#legend'), + graph: graph +} ); + +var offsetForm = document.getElementById('offset_form'); + +offsetForm.addEventListener('change', function(e) { + var offsetMode = e.target.value; + + if (offsetMode == 'lines') { + graph.setRenderer('line'); + graph.offset = 'zero'; + } else { + graph.setRenderer('stack'); + graph.offset = offsetMode; + } + graph.render(); + +}, false); + +graph.render(); +---- + +In your +project.clj+: + +[source, clojure] +---- +[net.drib/mrhyde "0.5.2"] +---- + +Then compile the following ClojureScript file and replace the script reference in the enclosing HTML page. +You will also want to include the https://github.com/dribnet/ArrayLike.js[ArrayLike.js polyfill] *before* the JavaScript libraries you want to use from +ClojureScript. + +[source,clojure] +---- +(ns rickshaw.example07 + (:require [mrhyde.core :as mrhyde] + [mrhyde.extend-js])) + +(mrhyde/bootstrap) + +(def Rickshaw (this-as ct (:Rickshaw ct))) + +(def document js/document) + +(def palette (Rickshaw.Color.Palette.)) + +(def graph (Rickshaw.Graph. { + :element (-> document (.querySelector "#chart")) + :width 780 + :height 500 + :renderer "line" + :series [ + { :name "Northeast" + :data [ { :x -1893456000 :y 25868573 } { :x -1577923200 :y 29662053 } { :x -1262304000 :y 34427091 } { :x -946771200 :y 35976777 } { :x -631152000 :y 39477986 } { :x -315619200 :y 44677819 } { :x 0 :y 49040703 } { :x 315532800 :y 49135283 } { :x 631152000 :y 50809229 } { :x 946684800 :y 53594378 } { :x 1262304000 :y 55317240 } ] + :color (-> palette .color) + } + { :name "Midwest" + :data [ { :x -1893456000 :y 29888542 } { :x -1577923200 :y 34019792 } { :x -1262304000 :y 38594100 } { :x -946771200 :y 40143332 } { :x -631152000 :y 44460762 } { :x -315619200 :y 51619139 } { :x 0 :y 56571663 } { :x 315532800 :y 58865670 } { :x 631152000 :y 59668632 } { :x 946684800 :y 64392776 } { :x 1262304000 :y 66927001 } ] + :color (-> palette .color) + } + { :name "South" + :data [ { :x -1893456000 :y 29389330 } { :x -1577923200 :y 33125803 } { :x -1262304000 :y 37857633 } { :x -946771200 :y 41665901 } { :x -631152000 :y 47197088 } { :x -315619200 :y 54973113 } { :x 0 :y 62795367 } { :x 315532800 :y 75372362 } { :x 631152000 :y 85445930 } { :x 946684800 :y 100236820 } { :x 1262304000 :y 114555744 } ] + :color (-> palette .color) + } + { :name "West" + :data [ { :x -1893456000 :y 7082086 } { :x -1577923200 :y 9213920 } { :x -1262304000 :y 12323836 } { :x -946771200 :y 14379119 } { :x -631152000 :y 20189962 } { :x -315619200 :y 28053104 } { :x 0 :y 34804193 } { :x 315532800 :y 43172490 } { :x 631152000 :y 52786082 } { :x 946684800 :y 63197932 } { :x 1262304000 :y 71945553 } ] + :color (-> palette .color) + }]})) + +(def x_axis + (Rickshaw.Graph.Axis.Time. { :graph graph })) + +(def y_axis + (Rickshaw.Graph.Axis.Y. { + :graph graph + :orientation "left" + :tickFormat (get-in Rickshaw [:Fixtures :Number :formatKMBT]) + :element (-> document (.getElementById "y_axis"))})) + +(def legend + (Rickshaw.Graph.Legend. { + :element (-> document (.querySelector "#legend")) + :graph graph})) + +(def offsetForm + (-> document (.getElementById "offset_form"))) + +(-> offsetForm (.addEventListener "change" (fn [e] + (let [offsetMode (get-in e [:target :value])] + (if (= offsetMode "lines") + (do + (-> graph (.setRenderer "line")) + (assoc! graph :offset "zero")) + ; else + (do + (-> graph (.setRenderer "stack")) + (assoc! graph :offset offsetMode)))) + (-> graph (.render))) false)) + +(-> graph .render) +---- + +http://s.trokes.org/dribnet/6209254[See it running live here] + +==== Discussion + +mrhyde is a library that modifies the core ClojureScript datatypes so that JavaScript +libraries can use them natively. After running the bootstrap function, +ClojureScript vectors can be accessed from JavaScript +as if they were native JavaScript arrays, and ClojureScript maps can be accessed +from JavaScript as if they were native JavaScript objects. Though this adds some +overhead to ClojureScript datatype creation, it can greatly simplify many +interop scenarios. + +For example, in the line: + +[source, clojure] +---- +(Rickshaw.Graph.Axis.Time. { :graph graph }) +---- + +we are passing a native ClojureScript map to a Rickshaw constructor +which expects a JavaScript object. Elsewhere we have more complicated +datatypes such as a map containing a vector containing a map which are +handed directly to Rickshaw functions which interpret the data as +an object containing an array containing an object. + +By also requiring mrhyde.extend-js (taken nearly directly from +http://keminglabs.com/blog/angular-cljs-mobile-weather-app/[Kevin Lynagh's project]) +we get interop in the other +direction, allowing ClojureScript to access JavaScript objects more idiomatically. + +In this example, it means we can replace the following two lines of interop-heavy code: + +[source, clojure] +---- +(-> Rickshaw .-Fixtures .-Number .-formatKMBT) +;... +(aset graph "offset" offsetMode) +---- + +with the much more clojure idiomatic + +[source, clojure] +---- +(get-in Rickshaw [:Fixtures :Number :formatKMBT]) +;... +(assoc! graph :offset offsetMode) +---- + +TIP: When adapting a JavaScript example like this, you don't have to do it all at once. Start with a working +JavaScript example and then start replacing it bottom to top with ClojureScript that references the JavaScript +objects directly. When you have replaced the entire JavaScript source, you'll be ready to make +similar ClojureScript examples from scratch. + +==== See Also + +* tbd