├── .gitignore
├── README.textile
├── images
├── analemma-logo.png
├── analemma-logo.svg
├── analemma-stack.svg
├── analemma.png
├── analemma.svg
├── rand-plot.svg
├── sin-cos-small.svg
├── sin-cos.png
└── sin-cos.svg
├── index.html
├── project.clj
└── src
├── analemma
├── charts.cljc
├── svg.cljc
└── xml.cljc
└── examples
├── analemma.clj
├── charts.clj
└── svg.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml
2 | *jar
3 | lib
4 | classes
5 | .#*
6 | target
7 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
49 |
50 |
51 |
54 |
55 |
Analemma is a Clojure library for generating
57 | charts and Scalable
59 | Vector Graphics (SVG). The code is available on Github
60 |
61 |
62 |
63 |
66 |
67 |
Architecture
68 |
69 |
72 |
73 |
75 |
76 |
Analemma was originally developed as an extended example to use
77 | when teaching
78 | Clojure programming, and so it's been designed to be lightweight and
79 | have no dependencies other than Clojure.
80 |
81 |
Analemma is designed as a stack of three simple libraries, each building
82 | on the one below it. At the bottom is analemma.xml, which provides a
83 | lightweight DSL for generating XML from Clojure data structures; if you've used James Reeves'
84 | Hiccup library
85 | for generating HTML, then the syntax will look familiar.
86 |
87 |
The next level up is analemma.svg, which provides a set of
88 | functions for generating and transforming SVG images and animations. SVG supports an
89 | enormous number of tags, most of which are not represented in this
90 | library; when an unimplemented tag is required, just drop down into analemma.xml
91 | and create the necessary SVG XML with Clojure data structures.
92 |
93 |
The final layer is analemma.charts, which provides basic charting
94 | functionality with a syntax similar to Incanter, and a visual theme similar
96 | to Hadley Wickham's ggplot2 library for
97 | R. The development of this
98 | library has just begun, and it is missing a lot of functionality; when
99 | additional features are required, just drop down into analemma.svg or
100 | analemma.xml.
101 |
102 |
analemma.charts examples
103 |
104 |
Plotting Analemma Data
105 |
106 |
107 |
(spit "analemma.svg"
108 | (emit-svg
109 | (-> (xy-plot :xmin -30 :maxx 10,
110 | :ymin -30 :maxy 30
111 | :height 500 :width 500)
112 | (add-points analemma-data))))
113 |
114 |
115 |
116 |
The full source code and data for this example can
117 | be found on
119 | github. The data was obtained from this
121 | site.
122 |
123 |
126 |
127 |
128 |
Plotting Sine and Cosine
129 |
130 |
131 |
(let [x (range -5 5 0.05)
132 | y1 (map #(Math/cos %) x)
133 | y2 (map #(Math/sin %) x)]
134 | (spit "sin-cos-small.svg"
135 | (emit-svg
136 | (-> (xy-plot :width 450 :height 200
137 | :xmin -5 :xmax 5
138 | :ymin -1.5 :ymax 1.5)
139 | (add-points [x y1]
140 | :transpose-data?? true
141 | :size 1)
142 | (add-points [x y2]
143 | :transpose-data?? true
144 | :size 1
145 | :fill (rgb 255 0 0))))))
146 |
147 |
148 |
149 |
The full source code and data for this example can
150 | be found on
152 | github.
153 |
154 |
157 |
158 |
159 |
Labeling Data Points
160 |
161 |
162 |
(let [x (repeatedly 25 #(rand-int 100))
163 | y (repeatedly 25 #(rand-int 100))]
164 | (spit "rand-plot.svg"
165 | (emit-svg
166 | (-> (xy-plot :width 500 :height 500
167 | :label-points? true)
168 | (add-points [x y] :transpose-data?? true)))))
169 |
170 |
171 |
172 |
The full source code and data for this example can
173 | be found on
175 | github.
176 |
177 |
180 |
181 |
182 |
analemma.svg examples
183 |
184 |
Analemma Logo
185 |
186 |
189 |
190 |
The full source code and data for this example can
191 | be found on
193 | github.
194 |
195 |
196 |
(emit
197 | (svg
198 | (apply group
199 | (-> (text "Analemma")
200 | (add-attrs :x 120 :y 60)
201 | (style :fill #"000066"
202 | :font-family "Garamond"
203 | :font-size "75px"
204 | :alignment-baseline :middle))
205 | (for [[x y] analemma-data]
206 | (circle (translate-value x -30 5 0 125)
207 | (translate-value y -25 30 125 0)
208 | 2 :fill "#000066")))))
209 |
210 |
211 |
Animated Analemma Stack
212 |
213 |
216 |
217 |
219 |
220 |
The full source code and data for this example can
221 | be found on
223 | github.
224 |
225 |
226 |
(defn txt-box [txt x y fill]
227 | (let [box-width 300
228 | box-height 50]
229 | (-> (svg
230 | (group
231 | (-> (rect 0 0 box-height box-width :rx 5 :ry 5)
232 | (style :stroke fill :fill fill))
233 | (-> (text txt)
234 | (add-attrs :x (/ box-width 2)
235 | :y (/ box-height 2)})
236 | (style :fill "#ffffff"
237 | :font-size "25px"
238 | :font-family "Verdana"
239 | :alignment-baseline :middle
240 | :text-anchor :middle))))
241 | (add-attrs :x x :y y))))
242 |
243 | (defn analemma-stack [directory]
244 | (spit (str directory "analemma-stack.svg")
245 | (emit
246 | (svg
247 | (-> (group
248 | (-> (txt-box "analemma.charts" 0 10 "#006600")
249 | (add-attrs :visibility :hidden)
250 | (animate :visibility :to :visible :begin 5)
251 | (animate :y :begin 5 :dur 1 :from 0 :to 10))
252 | (-> (txt-box "analemma.svg" 0 65 "#660000")
253 | (add-attrs :visibility :hidden)
254 | (animate :visibility :to :visible :begin 3)
255 | (animate :y :begin 3 :dur 2 :from 0 :to 65))
256 | (-> (txt-box "analemma.xml" 0 120 "#000066")
257 | (add-attrs :visibility :hidden)
258 | (animate :visibility :to :visible :begin 1)
259 | (animate :y :begin 1 :dur 4 :from 0 :to 120)))
260 | (translate 10 10))))))
261 |
262 |
263 |
264 |
analemma.xml examples
265 |
266 | The following code uses analemma.xml to produce a snippet of SVG XML.
267 |
268 |
(emit
269 | [:svg
270 | [:g {:x 100, :y 100}
271 | [:rect {:x 0, :y 0, :height 50, :width 300
272 | :style "stroke: #660000; fill: #660000;"}]
273 | [:text {:style "fill: #660000; font-size: 25px; font-family: Verdana"
274 | :x 150 :y 25}]]])
275 |
276 |
277 |
278 |
XML tags are represented as Clojure vectors. The first value in the vector represents the tag's name. If the
279 | second value is a Clojure map, it is treated as the tag's
280 | attributes. Any remaining values will be vectors representing child elements.
281 |
282 |
283 |
284 |
285 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject analemma "1.1.0"
2 | :description "An SVG DSL for Clojure and ClojureScript"
3 | :url "https://github.com/liebke/analemma"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :dependencies [[org.clojure/clojure "1.10.1"]])
7 |
--------------------------------------------------------------------------------
/src/analemma/charts.cljc:
--------------------------------------------------------------------------------
1 | (ns analemma.charts
2 | (:require [analemma.svg :refer [rect style line text group
3 | translate circle rgb svg
4 | translate-value]]
5 | [analemma.xml :refer [emit]]
6 | #?@(:cljs [[goog.string :as gstring]
7 | [goog.string.format]])))
8 |
9 | (def default-chart-props {:x 50, :y 50,
10 | :height 500, :width 750,
11 | :xmin 0, :xmax 100,
12 | :ymin 0, :ymax 100,
13 | :grid-lines 10,
14 | :label-points?? false,
15 | :points []
16 | :major-grid-color (rgb 255 255 255)
17 | :minor-grid-color (rgb 245 245 245)
18 | :background-color (rgb 225 225 225)
19 | :major-grid-width 2
20 | :minor-grid-width 1
21 | :axis-font-family "Verdana"
22 | :axis-font-size "12px"
23 | :axis-number-format "%.1f"
24 | :label-font-family "Verdana"
25 | :label-font-size "10px"
26 | :label-number-format "%.1f"})
27 |
28 | (defn chart-background [{:keys [height width background-color
29 | major-grid-color major-grid-width]}]
30 | (-> (rect 0 0 height width)
31 | (style :fill background-color
32 | :stroke major-grid-color
33 | :stroke-width major-grid-width)))
34 |
35 | (defn x-grid [{:keys [height width
36 | grid-lines
37 | major-grid-color minor-grid-color
38 | major-grid-width minor-grid-width]}]
39 | (let [grid-x-space (/ width grid-lines)]
40 | (for [i (range 1 grid-lines)]
41 | (-> (line (* i grid-x-space) 0 (* i grid-x-space) height)
42 | (style :stroke (if (even? i) major-grid-color minor-grid-color)
43 | :stroke-width (if (even? i) major-grid-width minor-grid-width))))))
44 |
45 | (defn y-grid [{:keys [height width
46 | grid-lines
47 | major-grid-color minor-grid-color
48 | major-grid-width minor-grid-width]}]
49 | (let [grid-y-space (/ height grid-lines)]
50 | (for [i (range 1 grid-lines)]
51 | (-> (line 0 (* i grid-y-space) width (* i grid-y-space))
52 | (style :stroke (if (even? i) major-grid-color minor-grid-color)
53 | :stroke-width (if (even? i) major-grid-width minor-grid-width))))))
54 |
55 | (defn x-axis [{:keys [height width
56 | xmin xmax
57 | grid-lines
58 | axis-font-family axis-font-size
59 | axis-number-format]}]
60 | (let [grid-x-space (/ width grid-lines)]
61 | (for [i (range 0 (inc grid-lines)) :when (even? i)]
62 | (-> (text {:x (* i grid-x-space) :y (+ 20 height)}
63 | (#?(:clj format :cljs gstring/format) axis-number-format
64 | (translate-value (* i grid-x-space)
65 | 0 width xmin xmax)))
66 | (style :fill (rgb 150 150 150)
67 | :font-family axis-font-family
68 | :font-size axis-font-size
69 | :text-anchor :middle)))))
70 |
71 | (defn y-axis [{:keys [height
72 | ymin ymax
73 | grid-lines
74 | axis-font-family axis-font-size
75 | axis-number-format]}]
76 | (let [grid-y-space (/ height grid-lines)]
77 | (for [i (range 1 (inc grid-lines)) :when (even? i)]
78 | (-> (text {:x 0 :y (- height (* i grid-y-space))}
79 | (#?(:clj format :cljs gstring/format) axis-number-format
80 | (translate-value (* i grid-y-space)
81 | 0 height ymin ymax)))
82 | (style :fill (rgb 150 150 150)
83 | :font-family axis-font-family
84 | :font-size axis-font-size
85 | :text-anchor :end
86 | :alignment-baseline :middle)))))
87 |
88 | (defn xy-plot [& options]
89 | (let [props (merge default-chart-props (apply hash-map options))]
90 | {:properties props
91 | :svg (-> (group (chart-background props))
92 | (translate (:x props) (:y props))
93 | (concat
94 | (x-grid props)
95 | (y-grid props)
96 | (x-axis props)
97 | (y-axis props)))}))
98 |
99 | (defn point-label [{:keys [label-font-family
100 | label-font-size
101 | label-number-format]}
102 | x* y* x y r & _]
103 | (-> (text {:x (+ x* r) :y (- y* r)}
104 | (str (#?(:clj format :cljs gstring/format) label-number-format (float x)) ","
105 | (#?(:clj format :cljs gstring/format) label-number-format (float y))))
106 | (style :fill (rgb 100 100 150)
107 | :font-family label-font-family
108 | :font-size label-font-size)))
109 |
110 | (defn add-point [chart x y r & options]
111 | (let [props (:properties chart)
112 | {:keys [height width
113 | xmin xmax
114 | ymin ymax
115 | label-points?]} props
116 | x* (translate-value x xmin xmax 0 width)
117 | y* (- height (translate-value y ymin ymax 0 height))
118 | point (apply circle x* y* r options)
119 | label (point-label props x* y* x y r)]
120 | (-> chart
121 | (update-in [:points] (fn [old] (conj old (apply assoc {:x x, :y y, :r r} options))))
122 | (assoc :svg (concat (:svg chart) (if label-points? [point label] [point]))))))
123 |
124 | (defn points->xy [points]
125 | (reduce (fn [[x y] [p1 p2]] [(conj x p1) (conj y p2)])
126 | [[] []] points))
127 |
128 | (defn xy->points [[x y]]
129 | (map (fn [p1 p2] [p1 p2]) x y))
130 |
131 | (defn add-points [chart data & {:keys [size sizes colors transpose-data?? fill]}]
132 | (let [[x y] (if transpose-data?? data (points->xy data))
133 | sizes (or sizes (repeat (count x) (or size 3)))
134 | colors (or colors (repeat (count x) (or fill (rgb 0 0 255))))
135 | data (map (fn [x y r color] [x y r color]) x y sizes colors)]
136 | (reduce (fn [svg [x y r color]] (add-point svg x y r :fill color))
137 | chart data)))
138 |
139 | (defn emit-svg [chart]
140 | (emit (svg (:svg chart))))
141 |
--------------------------------------------------------------------------------
/src/analemma/svg.cljc:
--------------------------------------------------------------------------------
1 | (ns analemma.svg
2 | (:require [analemma.xml :as xml]
3 | [clojure.string :as s]))
4 |
5 |
6 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
7 | ;; SVG FUNCTIONS
8 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
9 |
10 | (defn svg [& content]
11 | (let [xmlns {"xmlns:svg" "http://www.w3.org/2000/svg"
12 | "xmlns" "http://www.w3.org/2000/svg"
13 | "xmlns:xlink" "http://www.w3.org/1999/xlink"
14 | "version" "1.0"}
15 | attrs (if (map? (first content)) (first content) {})
16 | content (if (map? (first content)) (rest content) content)]
17 | (concat [:svg (merge xmlns attrs)] content)))
18 |
19 | (defn style-map [elem props]
20 | (let [styling (when (seq props)
21 | (reduce (fn [s [k v]]
22 | (str s " " (name k) ": "
23 | (if (keyword? v)
24 | (name v)
25 | v)
26 | "; "))
27 | "" props))]
28 | (xml/add-attrs elem :style styling)))
29 |
30 | (defn style [elem & properties]
31 | (style-map elem (apply hash-map properties)))
32 |
33 | (defn line [x1 y1 x2 y2 & options]
34 | (let [attrs (apply hash-map options)]
35 | [:line (apply merge {:x1 x1, :y1 y1, :x2 x2, :y2 y2} attrs)]))
36 |
37 | (defn rect [x y height width & options]
38 | (let [attrs (apply hash-map options)]
39 | [:rect (apply merge {:x x, :y y, :height height, :width width} attrs)]))
40 |
41 | (defn circle [cx cy r & options]
42 | (let [attrs (apply hash-map options)]
43 | [:circle (apply merge {:cx cx, :cy cy, :r r} attrs)]))
44 |
45 | (defn ellipse [cx cy rx ry & options]
46 | (let [attrs (apply hash-map options)]
47 | [:ellipse (apply merge {:cx cx, :cy cy, :rx rx, :ry ry} attrs)]))
48 |
49 | (defn polygon [[& points] & options]
50 | (let [attrs (apply hash-map options)
51 | points (reduce (fn [s [x y]] (str s " " x "," y))
52 | "" (partition 2 points))]
53 | [:polygon (apply merge {:points points}
54 | attrs)]))
55 |
56 | (defn text [& content]
57 | (concat [:text] content))
58 |
59 | (defn group [& content]
60 | (cons :g content))
61 |
62 | (defn draw [& commands]
63 | (reduce (fn [s [cmd args]] (str s " " (name cmd) (apply str (interpose "," args))))
64 | "" (partition 2 commands)))
65 |
66 | (defn path [draw-commands & options]
67 | (let [attrs (apply hash-map options)]
68 | [:path (merge {:d (apply draw draw-commands)} attrs)]))
69 |
70 | (defn tref [id]
71 | [:tref {"xlink:href" (str "#" (name id))}])
72 |
73 | (defn rgb [r g b]
74 | (str "rgb(" r "," g "," b ")"))
75 |
76 | (defn animate [elem attr & attrs]
77 | (-> elem
78 | (xml/add-content (-> [:animate {:attributeName (name attr),
79 | :begin 0, :fill "freeze"}]
80 | (xml/merge-attrs (apply hash-map attrs))))))
81 |
82 | (defn animate-motion [elem & attrs]
83 | (-> elem
84 | (xml/add-content (-> [:animateMotion {:begin 0, :fill "freeze"}]
85 | (xml/merge-attrs (apply hash-map attrs))))))
86 |
87 | (defn animate-color [elem attr & attrs]
88 | (-> elem
89 | (xml/add-content (-> [:animateColor {:attributeName (name attr),
90 | :begin 0, :fill "freeze"}]
91 | (xml/merge-attrs (apply hash-map attrs))))))
92 |
93 | (defn animate-transform [elem & attrs]
94 | (-> elem
95 | (xml/add-content (-> [:animateTransform {:attributeName "transform"
96 | :begin 0, :fill "freeze"}]
97 | (xml/merge-attrs (apply hash-map attrs))))))
98 |
99 | (defn transform [elem trans]
100 | (let [attrs (xml/get-attrs elem)
101 | trans (if (:transform attrs)
102 | (str (:transform attrs) " " trans)
103 | trans)]
104 | (xml/add-attrs elem :transform trans)))
105 |
106 | (defn rotate [elem angle x y]
107 | (transform elem (str "rotate(" angle "," x "," y ")")))
108 |
109 | (defn translate
110 | ([elem x] (transform elem (str "translate(" x ")")))
111 | ([elem x y] (transform elem (str "translate(" x "," y ")"))))
112 |
113 | (defn defs [[& bindings]]
114 | (let [bindings (partition 2 bindings)
115 | f (fn [defs-tag [id tag]]
116 | (conj defs-tag
117 | (xml/add-attrs tag :id (name id))))]
118 | (reduce f [:defs] bindings)))
119 |
120 | (defn text-path [text path-id]
121 | [:textPath {"xlink:href" (str "#" (name path-id))} text])
122 |
123 | (defn tspan [& content]
124 | (concat [:tspan] content))
125 |
126 | (defn image [href & options]
127 | (let [attrs (apply hash-map options)]
128 | [:image (merge {"xlink:href" href} attrs)]))
129 |
130 |
131 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
132 | ;; UTILITY FUNCTIONS
133 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
134 |
135 | (defn translate-value [v from-min from-max to-min to-max]
136 | (let [scale (/ (- to-max to-min)
137 | (- from-max from-min))
138 | trans (- to-min (* from-min scale))]
139 | (float (+ (* v scale) trans))))
140 |
141 | (defn parse-inline-css [css-str]
142 | (reduce (fn [m [k v]] (assoc m (keyword k) v))
143 | {} (map #(s/split % #":") (s/split css-str #";"))))
144 |
145 | (defn add-style [elem & styling]
146 | (let [css-str (or (:style (xml/get-attrs elem)) "")]
147 | (style-map elem (apply merge (parse-inline-css css-str) (apply hash-map styling)))))
--------------------------------------------------------------------------------
/src/analemma/xml.cljc:
--------------------------------------------------------------------------------
1 | (ns analemma.xml
2 | (:require #?(:clj [clojure.xml :as xml])
3 | [clojure.zip :as z])
4 | #?(:clj (:import [java.io ByteArrayInputStream])))
5 |
6 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
7 | ;; XML FUNCTIONS
8 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
9 |
10 | (defn has-attrs? [tag]
11 | (map? (second tag)))
12 |
13 | (defn has-content? [tag]
14 | (if (has-attrs? tag)
15 | (> (count tag) 2)
16 | (> (count tag) 1)))
17 |
18 | (defn get-name [tag]
19 | (if-let [n (first tag)]
20 | (name n)))
21 |
22 | (defn get-attrs [tag]
23 | (if (has-attrs? tag) (second tag) {}))
24 |
25 | (defn get-content [tag]
26 | (if (has-attrs? tag)
27 | (drop 2 tag)
28 | (rest tag)))
29 |
30 | (defn set-attrs [tag attrs]
31 | (concat [(get-name tag) attrs]
32 | (get-content tag)))
33 |
34 | (defn set-content [tag & content]
35 | (concat [(get-name tag) (get-attrs tag)]
36 | content))
37 |
38 | (defn add-attrs [tag & attrs]
39 | (concat [(get-name tag)
40 | (apply assoc (get-attrs tag) attrs)]
41 | (get-content tag)))
42 |
43 | (defn merge-attrs [tag attrs]
44 | (concat [(get-name tag)
45 | (merge (get-attrs tag) attrs)]
46 | (get-content tag)))
47 |
48 | (defn add-content [tag & content]
49 | (concat [(get-name tag)
50 | (get-attrs tag)]
51 | (concat (get-content tag)
52 | content)))
53 |
54 | (defn update-attrs [tag [& keys] update-fn & args]
55 | (set-attrs tag (apply update-in (get-attrs tag) keys update-fn args)))
56 |
57 | (defn emit-attrs [attrs]
58 | (when attrs
59 | (reduce (fn [s [k v]]
60 | (str s (name k) "=\"" (if (keyword? v) (name v) v) "\" "))
61 | "" attrs)))
62 |
63 | (defn emit-tag [tag]
64 | (if-let [n (get-name tag)]
65 | (str "<" n " "
66 | (emit-attrs (get-attrs tag))
67 | (if (seq (get-content tag))
68 | (str ">" (apply str (map #(if (string? %) % (emit-tag %)) (get-content tag)))
69 | "" n ">")
70 | "/>"))))
71 |
72 | (defn emit [& tags]
73 | (str ""
74 | (reduce #(str %1 (emit-tag %2)) "" tags)))
75 |
76 |
77 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
78 | ;; FUNCTIONS FOR PARSING XML FILES: CLOJURE ONLY
79 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
80 |
81 | #?(:clj (defn get-xml-map [xml-string]
82 | (xml/parse (ByteArrayInputStream. (.getBytes xml-string "UTF-8")))))
83 |
84 | #?(:clj (defn parse-xml-map [xml-map]
85 | (if (map? xml-map)
86 | (let [{:keys [tag attrs content]} xml-map]
87 | (lazy-cat (if attrs [tag attrs] [tag])
88 | (map parse-xml-map content)))
89 | xml-map)))
90 |
91 | #?(:clj (defn parse-xml [xml-string]
92 | (parse-xml-map (get-xml-map xml-string))))
93 |
94 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
95 | ;; FUNCTIONS FOR FILTERING AND TRANSFORMING PARSED XML
96 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
97 |
98 | (defn- select-loc?
99 | "Provides selector functionality used by filter-xml and transform-xml.
100 |
101 | Examples of selectors:
102 | :tag-name
103 | {attr-name val}
104 | [:and :tag-name {:attr val}]
105 | [:or :tag1 :tag2]
106 | [:and [:not {attr val}] [:or :tag1 :tag2]]
107 |
108 | "
109 | ([loc selector]
110 | (when (z/branch? loc)
111 | (let [node (z/node loc)]
112 | (cond
113 | (map? selector)
114 | (and (has-attrs? node)
115 | (= (select-keys (get-attrs node) (keys selector))
116 | selector))
117 | (or (string? selector) (keyword? selector))
118 | (= (get-name node) (name selector))
119 | (coll? selector)
120 | (condp = (first selector)
121 | :and (reduce #(and %1 (select-loc? loc %2)) true (next selector))
122 | :or (reduce #(or %1 (select-loc? loc %2)) false (next selector))
123 | :not (not (select-loc? loc [:or (second selector)]))))))))
124 |
125 | (defn filter-xml [xml-seq [& selectors]]
126 | (letfn [(filter-xml* [zip-loc [selector & child-selectors]]
127 | (loop [nodes [] loc zip-loc]
128 | (if (z/end? loc)
129 | nodes
130 | (recur
131 | (if (select-loc? loc selector)
132 | (if (seq child-selectors)
133 | (filter-xml* loc child-selectors)
134 | (conj nodes (z/node loc)))
135 | nodes)
136 | (z/next loc)))))]
137 | (filter-xml* (z/seq-zip xml-seq) selectors)))
138 |
139 | (defn transform-xml [xml-seq [& selectors] f & args]
140 | (letfn [(transform-xml* [zip-loc [selector & child-selectors] f & args]
141 | (loop [loc zip-loc]
142 | (if (z/end? loc)
143 | loc
144 | (recur
145 | (z/next
146 | (if (select-loc? loc selector)
147 | (if (seq child-selectors)
148 | (apply transform-xml* loc child-selectors f args)
149 | (apply z/edit loc f args))
150 | loc))))))]
151 | (z/root (apply transform-xml* (z/seq-zip xml-seq) selectors f args))))
152 |
--------------------------------------------------------------------------------
/src/examples/analemma.clj:
--------------------------------------------------------------------------------
1 | (ns examples.analemma
2 | (:use [analemma.charts :only [emit-svg xy-plot add-points]]
3 | [analemma.svg]
4 | [analemma.xml]
5 | [clojure.java.io :only [file]]))
6 |
7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
8 | ;; ANALEMMA CHART
9 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
10 |
11 | ;; http://www.wsanford.com/~wsanford/exo/sundials/analemma_calc.html
12 | (def analemma-data
13 | [[-15.165 -23.07]
14 | [-17.016 -22.70]
15 | [-19.171 -22.08]
16 | [-21.099 -21.27]
17 | [-22.755 -20.30]
18 | [-24.107 -19.16]
19 | [-25.446 -17.33]
20 | [-25.914 -16.17]
21 | [-26.198 -14.62]
22 | [-26.158 -12.96]
23 | [-25.814 -11.21]
24 | [-25.194 -9.39]
25 | [-24.520 -7.89]
26 | [-23.708 -6.37]
27 | [-22.529 -4.42]
28 | [-21.205 -2.45]
29 | [-19.777 -0.48]
30 | [-18.289 1.50]
31 | [-16.185 4.24]
32 | [-15.009 5.78]
33 | [-13.605 7.66]
34 | [-12.309 9.49]
35 | [-11.153 11.26]
36 | [-10.169 12.94]
37 | [-9.250 14.85]
38 | [-8.811 16.04]
39 | [-8.469 17.43]
40 | [-8.364 18.69]
41 | [-8.493 19.83]
42 | [-8.847 20.82]
43 | [-9.685 21.96]
44 | [-10.317 22.47]
45 | [-11.231 22.96]
46 | [-12.243 23.28]
47 | [-13.308 23.43]
48 | [-14.378 23.41]
49 | [-15.599 23.16]
50 | [-16.339 22.86]
51 | [-17.139 22.33]
52 | [-17.767 21.64]
53 | [-18.191 20.80]
54 | [-18.387 19.81]
55 | [-18.253 18.20]
56 | [-17.956 17.17]
57 | [-17.361 15.78]
58 | [-16.529 14.28]
59 | [-15.474 12.68]
60 | [-14.221 11.01]
61 | [-12.183 8.54]
62 | [-10.901 7.07]
63 | [-9.212 5.20]
64 | [-7.462 3.29]
65 | [-5.693 1.36]
66 | [-3.946 -0.59]
67 | [-1.938 -2.93]
68 | [-0.686 -4.48]
69 | [0.742 -6.39]
70 | [1.982 -8.28]
71 | [2.993 -10.11]
72 | [3.742 -11.88]
73 | [4.290 -14.23]
74 | [4.318 -15.49]
75 | [4.044 -16.97]
76 | [3.420 -18.33]
77 | [2.446 -19.55]
78 | [1.135 -20.63]
79 | [-0.852 -21.71]
80 | [-2.398 -22.29]
81 | [-4.538 -22.86]
82 | [-6.855 -23.24]
83 | [-9.286 -23.42]
84 | [-11.761 -23.41]
85 | [-14.691 -23.14]])
86 |
87 | (defn analemma [filename]
88 | (spit filename
89 | (emit-svg
90 | (-> (xy-plot :xmin -30 :xmax 10,
91 | :ymin -30 :ymax 30
92 | :height 500 :width 500)
93 | (add-points analemma-data)))))
94 |
95 |
96 | (defn analemma-logo [filename]
97 | (spit filename
98 | (emit
99 | (svg
100 | (apply group
101 | (-> (text "Analemma")
102 | (add-attrs :x 120 :y 60)
103 | (style :fill #"000066"
104 | :font-family "Garamond"
105 | :font-size "75px"
106 | :alignment-baseline :middle))
107 | (for [[x y] analemma-data]
108 | (circle (translate-value x -30 5 0 125)
109 | (translate-value y -25 30 125 0)
110 | 2 :fill "#000066")))))))
111 |
112 | (defn rotating-analemma-logo [filename]
113 | (spit filename
114 | (emit
115 | (svg
116 | (-> (image "file:images/analemma-logo.svg"
117 | :width 500 :height 700)
118 | (animate-transform :begin 0
119 | :dur 20
120 | :type :rotate
121 | :from "0 200 150"
122 | :to "360 200 150"
123 | :repeatCount :indefinite))))))
124 |
125 |
126 | (defn query-and-transform-analemma [filename]
127 | (let [logo (parse-xml (slurp (file "images/analemma-logo.svg")))
128 | red-analemma (-> (concat [:g] (filter-xml logo [:g :circle]))
129 | (transform-xml [:circle] #(add-attrs % :fill "#FF0000")))]
130 | (spit filename
131 | (emit
132 | (svg
133 | (-> red-analemma
134 | (animate-transform :begin 0
135 | :dur 20
136 | :type :rotate
137 | :from "0 100 100"
138 | :to "360 100 100"
139 | :repeatCount :indefinite)))))))
--------------------------------------------------------------------------------
/src/examples/charts.clj:
--------------------------------------------------------------------------------
1 | (ns examples.charts
2 | (:use [analemma.charts :only [emit-svg xy-plot add-points]]
3 | [analemma.svg :only [rgb]]))
4 |
5 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
6 | ;; BASIC CHART EXAMPLES
7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
8 |
9 | (defn rand-plot [directory]
10 | (let [x (repeatedly 25 #(rand-int 100))
11 | y (repeatedly 25 #(rand-int 100))]
12 | (spit (str directory "/rand-plot.svg")
13 | (emit-svg
14 | (-> (xy-plot :width 500 :height 500 :label-points? true)
15 | (add-points [x y] :transpose-data?? true))))))
16 |
17 | (defn sin-cos-plot [directory]
18 | (let [x (range -5 5 0.05)
19 | y1 (map #(Math/cos %) x)
20 | y2 (map #(Math/sin %) x)]
21 | (spit (str directory "/sin-cos-small.svg")
22 | (emit-svg
23 | (-> (xy-plot :width 450 :height 200
24 | :xmin -5 :xmax 5
25 | :ymin -1.5 :ymax 1.5)
26 | (add-points [x y1] :transpose-data?? true
27 | :size 1)
28 | (add-points [x y2] :transpose-data?? true
29 | :size 1
30 | :fill (rgb 255 0 0)))))))
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/examples/svg.clj:
--------------------------------------------------------------------------------
1 | (ns examples.svg
2 | (:use [analemma.xml :only [emit add-content add-attrs
3 | parse-xml transform-xml filter-xml]]
4 | analemma.svg
5 | [clojure.java.io :only [file]]))
6 |
7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
8 | ;; ANALEMMA SVG EXAMPLES
9 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
10 |
11 | (def ex-group (svg
12 | (svg {:x 50 :y 50}
13 | (-> (group
14 | (-> (line 10 10 85 10)
15 | (style :stroke "#006600"))
16 | (-> (rect 10 20 50 75)
17 | (style :stroke "#006600" :fill "#006600"))
18 | (-> (text {:x 10 :y 90} "Text grouped with shapes")
19 | (style :stroke "#660000" :fill "660000")))
20 | (rotate 45 50 50)))))
21 |
22 | ;; http://tutorials.jenkov.com/svg/rect-element.html
23 | (def ex-round-rects
24 | (svg
25 | (-> (rect 10 10 50 50 :rx 5 :ry 5)
26 | (style :stroke "#006600" :fill "#00cc00"))
27 | (-> (rect 70 10 50 50 :rx 10 :ry 10)
28 | (style :stroke "#006600" :fill "#00cc00"))
29 | (-> (rect 130 10 50 50 :rx 15 :ry 15)
30 | (style :stroke "#006600" :fill "#00cc00"))))
31 |
32 | (def ex-circle (svg
33 | (-> (circle 40 40 24)
34 | (style :stroke "#006600" :fill "#00cc00"))))
35 |
36 | (def ex-ellipse (svg
37 | (-> (ellipse 40 40 30 15)
38 | (style :stroke "#006600" :fill "#00cc00"))))
39 |
40 | (def ex-tri (svg
41 | (-> (polygon [0,0 30,0 15,30 0,0])
42 | (style :stroke "#006600" :fill "#33cc33"))))
43 |
44 | (def ex-oct (svg
45 | (-> (polygon [50,05 100,5 125,30 125,80 100,105 50,105 25,80 25,30])
46 | (style :stroke "#660000"
47 | :fill "#cc3333"
48 | :stroke-width 3))))
49 |
50 | ;; http://tutorials.jenkov.com/svg/path-element.html
51 | (def ex-path (svg
52 | (-> (path [:M [50,50]
53 | :A [30,30 0 0,1 35,20]
54 | :L [100,100]
55 | :M [110,110]
56 | :L [100,0]])
57 | (style :stroke "#660000" :fill :none))))
58 |
59 | (def ex-text (svg
60 | (text {:x 20 :y 40} "Example SVG text 1")
61 | (-> (line 10 40 150 40)
62 | (style :stroke "#000000"))))
63 |
64 | (def ex-text2 (svg
65 | (-> (text {:x 20 :y 40} "Rotated SVG text")
66 | (style :stroke :none :fill "#000000")
67 | (rotate 30 20 40))))
68 |
69 | (def ex-text3 (svg
70 | (-> (text {:x 20 :y 40} "Styled SVG text")
71 | (style :font-family "Arial"
72 | :font-size 34
73 | :stroke "#000000"
74 | :fill "#00ff00"))))
75 |
76 | (def ex-text4 (svg
77 | (text {:x 20 :y 10}
78 | (tspan "tspan line 1")
79 | (tspan "tspan line 2")
80 | (tspan "tspan line 3"))))
81 |
82 | (def ex-text5 (svg
83 | (text {:y 10}
84 | (tspan {:x 0} "tspan line 1")
85 | (tspan {:x 0 :dy 15} "tspan line 2")
86 | (tspan {:x 0 :dy 15} "tspan line 3"))))
87 |
88 | (def ex-tref (svg
89 | (defs [:the-text (text "A text that is referenced.")])
90 | (text {:x 20 :y 10} (tref :the-text))
91 | (text {:x 30 :y 30} (tref :the-text))))
92 |
93 | (def ex-text-path (svg
94 | (defs [:my-path (path [:M [75,20]
95 | :a [1,1 0 0,0 100,0]])])
96 | (-> (text {:x 10 :y 100}
97 | (text-path "Text along a curved path..." :my-path))
98 | (style :stroke "#000000"))))
99 |
100 | (def ex-text-path2 (svg
101 | (defs [:the-text (text "Text ref along a curved path...")
102 | :my-path (path [:M [75,20]
103 | :a [1,1 0 0,0 100,0]])])
104 | (-> (text {:x 10 :y 100}
105 | (text-path (tref :the-text) :my-path))
106 | (style :stroke "#000000"))))
107 |
108 | (def ex-img (svg
109 | (-> (rect 10 10 130 500)
110 | (style :fill "#000000"))
111 | (image "http://jenkov.com/images/layout/top-bar-logo.png"
112 | :x 20 :y 20 :width 300 :height 80)
113 | (-> (line 25 80 350 80)
114 | (style :stroke "#ffffff" :stroke-width 3))))
115 |
116 | (def ex-trans (svg
117 | (-> (rect 50 50 110 110)
118 | (style :stroke "#ff0000" :fill "#ccccff")
119 | (translate 30)
120 | (rotate 45 50 50))
121 | (-> (text {:x 70 :y 100} "Hello World")
122 | (translate 30)
123 | (rotate 45 50 50))))
124 |
125 | (def ex-animate (svg
126 | (-> (rect 10 10 110 110)
127 | (style :stroke "#ff0000" :fill "#0000ff")
128 | (animate-transform :begin 0
129 | :dur 20
130 | :type :rotate
131 | :from "0 60 60"
132 | :to "360 60 60"
133 | :repeatCount :indefinite))))
134 |
135 | ;; http://www.w3.org/TR/SVG/animate.html
136 | (def ex-anim2 (svg
137 | (-> (rect 1 1 298 798)
138 | (style :fill "none" :stroke "blue" :stroke-width 2))
139 |
140 | (-> (rect 300 100 300 100)
141 | (style :fill (rgb 255 255 0))
142 | (animate :x :begin 0 :dur 9 :from 300 :to 0)
143 | (animate :y :begin 0 :dur 9 :from 100 :to 0)
144 | (animate :width :begin 0 :dur 9 :from 300 :to 800)
145 | (animate :height :begin 0 :dur 9 :from 100 :to 300))
146 |
147 | (-> (group
148 | (-> (text "It's alive")
149 | (style :font-family :Verdana
150 | :font-size 35.27
151 | :visibility :hidden)
152 | (animate :visibility :to :visible :begin 3)
153 | (animate-motion :path (draw :M [0 0] :L [100 100])
154 | :begin 3 :dur 6)
155 | (animate-color :fill :from (rgb 0 0 255) :to (rgb 128 0 0)
156 | :begin 3 :dur 6)
157 | (animate-transform :type :rotate :from -30 :to 0 :begin 3 :dur 6)
158 | (animate-transform :type :scale :from 1 :to 3 :additive :sum
159 | :begin 3 :dur 6)))
160 | (translate 100 100))))
161 |
162 | (def ex-anim-logo (svg
163 | (-> (image "http://clojure.org/space/showimage/clojure-icon.gif"
164 | :width 100 :height 100)
165 | (animate :x :begin 0 :dur 9 :from 0 :to 300)
166 | (animate :y :begin 0 :dur 9 :from 0 :to 300))))
167 |
168 | (def ex-anim-logo2 (svg
169 | (-> (image "http://clojure.org/space/showimage/clojure-icon.gif"
170 | :width 100 :height 100)
171 | (animate-transform :type :rotate :from -30 :to 0 :begin 0 :dur 6))))
172 |
173 | (def ex-anim-logo3 (svg
174 | (-> (group
175 | (-> (image "http://clojure.org/space/showimage/clojure-icon.gif"
176 | :width 100 :height 100)
177 | (animate-transform :type :rotate
178 | :begin 0
179 | :dur 20
180 | :from "0 50 50"
181 | :to "360 50 50"
182 | :repeatCount :indefinite)))
183 |
184 | (translate 100 100))))
185 |
186 | (defn txt-box [txt x y fill]
187 | (let [box-width 300
188 | box-height 50]
189 | (-> (svg
190 | (group
191 | (-> (rect 0 0 box-height box-width :rx 5 :ry 5)
192 | (style :stroke fill :fill fill))
193 | (-> (text txt)
194 | (add-attrs :x (/ box-width 2)
195 | :y (/ box-height 2))
196 | (style :fill "#ffffff"
197 | :font-size "25px"
198 | :font-family "Verdana"
199 | :alignment-baseline :middle
200 | :text-anchor :middle))))
201 | (add-attrs :x x :y y))))
202 |
203 | (defn analemma-stack [directory]
204 | (spit (str directory "/analemma-stack.svg")
205 | (emit
206 | (svg
207 | (-> (group
208 | (-> (txt-box "analemma.charts" 0 10 "#006600")
209 | (add-attrs :visibility :hidden)
210 | (animate :visibility :to :visible :begin 5)
211 | (animate :y :begin 5 :dur 1 :from 0 :to 10))
212 | (-> (txt-box "analemma.svg" 0 65 "#660000")
213 | (add-attrs :visibility :hidden)
214 | (animate :visibility :to :visible :begin 3)
215 | (animate :y :begin 3 :dur 2 :from 0 :to 65))
216 | (-> (txt-box "analemma.xml" 0 120 "#000066")
217 | (add-attrs :visibility :hidden)
218 | (animate :visibility :to :visible :begin 1)
219 | (animate :y :begin 1 :dur 4 :from 0 :to 120)))
220 | (translate 10 10))))))
221 |
222 |
223 | (defn parse-us-map []
224 | (parse-xml (slurp "http://upload.wikimedia.org/wikipedia/commons/3/32/Blank_US_Map.svg")))
225 |
226 | (defn hide-california [filename]
227 | (spit filename
228 | (emit
229 | (transform-xml (parse-us-map)
230 | [{:id "CA"}]
231 | #(add-attrs % :visibility "hidden")))))
232 |
233 | (defn color-maryland [filename]
234 | (spit filename
235 | (emit
236 | (transform-xml (parse-us-map)
237 | [{:id "MD"}]
238 | (fn [elem]
239 | (-> (add-style elem :fill "#0000ff")
240 | (add-attrs :transform "scale(1.10)")))))))
241 |
242 |
243 | (defn select-maryland [filename]
244 | (spit filename
245 | (emit
246 | (svg (-> (apply svg {:x -600 :y -200}
247 | (filter-xml (parse-us-map)
248 | [[:or "sodipodi:namedview" :defs {:id "MD"}]]))
249 | (transform-xml [:svg]
250 | #(add-attrs %
251 | "xmlns:sodipodi" "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
252 | "xmlns:inkscape" "http://www.inkscape.org/namespaces/inkscape"))
253 | (transform-xml [{:id "MD"}]
254 | (fn [elem]
255 | (add-attrs elem :transform "scale(1.5)"))))))))
256 |
257 | (defn to-hex-string [n] (str "#" (Integer/toHexString n)))
258 |
259 | ;;(to-hex-string (translate-value 0.5 0 1 0 16777215))
260 |
261 | (def us-states
262 | {"AK" "ALASKA"
263 | "AL" "ALABAMA"
264 | "AR" "ARKANSAS"
265 | "AS" "AMERICAN SAMOA"
266 | "AZ" "ARIZONA"
267 | "CA" "CALIFORNIA"
268 | "CO" "COLORADO"
269 | "CT" "CONNECTICUT"
270 | "DC" "WASHINGTON, DC"
271 | "DE" "DELAWARE"
272 | "FL" "FLORIDA"
273 | "FM" "FEDERATED STATES OF MICRONESIA"
274 | "GA" "GEORGIA"
275 | "GU" "GUAM"
276 | "HI" "HAWAII"
277 | "IA" "IOWA"
278 | "ID" "IDAHO"
279 | "IL" "ILLINOIS"
280 | "IN" "INDIANA"
281 | "KS" "KANSAS"
282 | "KY" "KENTUCKY"
283 | "LA" "LOUISIANA"
284 | "MA" "MASSACHUSETTS"
285 | "MD" "MARYLAND"
286 | "ME" "MAINE"
287 | "MH" "MARSHALL ISLANDS"
288 | "MI" "MICHIGAN"
289 | "MN" "MINNESOTA"
290 | "MO" "MISSOURI"
291 | "MP" "NORTHERN MARIANA ISLANDS"
292 | "MS" "MISSISSIPPI"
293 | "MT" "MONTANA"
294 | "NC" "NORTH CAROLINA"
295 | "ND" "NORTH DAKOTA"
296 | "NE" "NEBRASKA"
297 | "NH" "NEW HAMPSHIRE"
298 | "NJ" "NEW JERSEY"
299 | "NM" "NEW MEXICO"
300 | "NV" "NEVADA"
301 | "NY" "NEW YORK"
302 | "OH" "OHIO"
303 | "OK" "OKLAHOMA"
304 | "OR" "OREGON"
305 | "PA" "PENNSYLVANIA"
306 | "PR" "PUERTO RICO"
307 | "PW" "PALAU"
308 | "RI" "RHODE ISLAND"
309 | "SC" "SOUTH CAROLINA"
310 | "SD" "SOUTH DAKOTA"
311 | "TN" "TENNESSEE"
312 | "TX" "TEXAS"
313 | "UT" "UTAH"
314 | "VA" "VIRGINIA"
315 | "VI" "VIRGIN ISLANDS"
316 | "VT" "VERMONT"
317 | "WA" "WASHINGTON"
318 | "WI" "WISCONSIN"
319 | "WV" "WEST VIRGINIA"
320 | "WY" "WYOMING"})
321 |
322 | (defn color-states [filename]
323 | (spit filename
324 | (emit
325 | (transform-xml (parse-us-map)
326 | [[:and [:not {:id "path57"}] [:or :g :path]]]
327 | (fn [elem]
328 | (add-style elem :fill (to-hex-string (rand 16777215))))))))
329 |
330 |
--------------------------------------------------------------------------------