Skip to content

Commit b989f9b

Browse files
committed
composable_plotting wip
1 parent 88d5afe commit b989f9b

1 file changed

Lines changed: 102 additions & 92 deletions

File tree

src/data_visualization/aog/composable_plotting.clj

Lines changed: 102 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,10 @@ mpg
381381
;; The two phases compose through threading:
382382
;; `(-> data (view pairs) (lay (point)))` reads as
383383
;; "these column pairings, drawn as points" -- what, then how.
384+
;;
385+
;; > *The next sections build the rendering pipeline piece by piece.
386+
;; > To see results first and return for the implementation,
387+
;; > skip to [Scatter Plots](#scatter-plots).*
384388
;; ---
385389

386390
;; ## Theme and Colors
@@ -555,7 +559,7 @@ mpg
555559
[:rect :count]
556560
[:bar :bin])
557561
;; one categorical, one numerical → strip plot
558-
(or (not= x-type y-type))
562+
(not= x-type y-type)
559563
[:point :identity]
560564
;; both numerical → scatter
561565
:else [:point :identity])
@@ -797,7 +801,6 @@ mpg
797801
(fn [coord-type] coord-type))
798802

799803
(defmethod show-ticks? :default [_] true)
800-
(defmethod show-ticks? :polar [_] false)
801804

802805
(defmethod make-coord :cartesian [_ sx sy pw ph m]
803806
(fn [dx dy] [(sx dx) (sy dy)]))
@@ -813,70 +816,9 @@ mpg
813816
:center (coord-fn 5 50)
814817
:top-right (coord-fn 10 100)})
815818

816-
;; ### ⚙️ Flip
817-
;;
818-
;; Swaps x and y axes. Histograms become horizontal, bar charts grow sideways.
819-
820-
(defmethod make-coord :flip [_ sx sy pw ph m]
821-
(fn [dx dy] [(sx dy) (sy dx)]))
822-
823-
(defmethod render-grid :flip [_ sx sy pw ph m cfg]
824-
(render-grid :cartesian sx sy pw ph m cfg))
825-
826819
(defmethod render-grid :default [_ sx sy pw ph m cfg]
827820
(render-grid :cartesian sx sy pw ph m cfg))
828821

829-
;; ### ⚙️ Polar
830-
;;
831-
;; [Polar coordinates](https://en.wikipedia.org/wiki/Polar_coordinate_system)
832-
;; map x to angle and y to radius. Bars become wedges, scatters wrap into a disc.
833-
834-
(defmethod make-coord :polar [_ sx sy pw ph m]
835-
(let [cx (/ pw 2.0) cy (/ ph 2.0)
836-
r-max (- (min cx cy) m)
837-
x-lo (double m) x-span (double (- pw m m))
838-
y-lo (double m) y-span (double (- ph m m))]
839-
(fn [dx dy]
840-
(let [px (sx dx) py (sy dy)
841-
;; Normalize to [0,1]: t-angle sweeps the full circle,
842-
;; t-radius goes from center (0) to edge (1).
843-
t-angle (/ (- px x-lo) (max 1.0 x-span))
844-
t-radius (/ (- (+ y-lo y-span) py) (max 1.0 y-span))
845-
angle (* 2.0 Math/PI t-angle)
846-
radius (* r-max t-radius)]
847-
[(+ cx (* radius (Math/cos (- angle (/ Math/PI 2.0)))))
848-
(+ cy (* radius (Math/sin (- angle (/ Math/PI 2.0)))))]))))
849-
850-
(defmethod make-coord-px :polar [_ sx sy pw ph m]
851-
(let [cx (/ pw 2.0) cy (/ ph 2.0)
852-
r-max (- (min cx cy) m)
853-
x-lo (double m) x-span (double (- pw m m))
854-
y-lo (double m) y-span (double (- ph m m))]
855-
(fn [px py]
856-
(let [t-angle (/ (- px x-lo) (max 1.0 x-span))
857-
t-radius (/ (- (+ y-lo y-span) py) (max 1.0 y-span))
858-
angle (* 2.0 Math/PI t-angle)
859-
radius (* r-max t-radius)]
860-
[(+ cx (* radius (Math/cos (- angle (/ Math/PI 2.0)))))
861-
(+ cy (* radius (Math/sin (- angle (/ Math/PI 2.0)))))]))))
862-
863-
(defmethod render-grid :polar [_ sx sy pw ph m cfg]
864-
(let [cfg (or cfg defaults)
865-
cx (/ pw 2.0) cy (/ ph 2.0)
866-
r-max (- (min cx cy) m)]
867-
(into [:g]
868-
(concat
869-
(for [i (range 1 6)
870-
:let [r (* r-max (/ i 5.0))]]
871-
[:circle {:cx cx :cy cy :r r :fill "none"
872-
:stroke (:grid theme) :stroke-width (:grid-stroke-width cfg)}])
873-
(for [i (range 8)
874-
:let [a (* i (/ Math/PI 4))]]
875-
[:line {:x1 cx :y1 cy
876-
:x2 (+ cx (* r-max (Math/cos a)))
877-
:y2 (+ cy (* r-max (Math/sin a)))
878-
:stroke (:grid theme) :stroke-width (:grid-stroke-width cfg)}])))))
879-
880822
;; ---
881823

882824
;; ## Drawing Marks
@@ -993,7 +935,7 @@ mpg
993935
n (tick-count (- pw (* 2 m)) (:tick-spacing-x cfg))
994936
ticks (ws/ticks sx n)
995937
labels (format-ticks sx ticks)]
996-
(into [:g {:font-size (:font-size theme) :fill "#666"}]
938+
(into [:g {:font-size (:font-size theme) :fill "#666" :font-family "sans-serif"}]
997939
(map (fn [t label]
998940
[:text {:x (sx t) :y (- ph 2) :text-anchor "middle"} label])
999941
ticks labels))))
@@ -1007,7 +949,7 @@ mpg
1007949
n (tick-count (- ph (* 2 m)) (:tick-spacing-y cfg))
1008950
ticks (ws/ticks sy n)
1009951
labels (format-ticks sy ticks)]
1010-
(into [:g {:font-size (:font-size theme) :fill "#666"}]
952+
(into [:g {:font-size (:font-size theme) :fill "#666" :font-family "sans-serif"}]
1011953
(map (fn [t label]
1012954
[:text {:x (- m 3) :y (+ (sy t) 3) :text-anchor "end"} label])
1013955
ticks labels))))
@@ -1227,8 +1169,9 @@ mpg
12271169

12281170
;; ### ⚙️ `wrap-plot`
12291171
;;
1230-
;; Wraps SVG as hiccup. Tooltip and brush interactions are added later
1231-
;; in the [Interactivity](#interactivity) section.
1172+
;; Wraps SVG as hiccup. This initial definition is a passthrough;
1173+
;; it is **redefined** in the [Interactivity](#interactivity) section
1174+
;; to add tooltip and brush support.
12321175

12331176
(defn wrap-plot
12341177
"Wrap SVG content as hiccup. Interaction modes are added later."
@@ -1460,6 +1403,7 @@ mpg
14601403
:y-domain (:y-domain stat)
14611404
:first-3-bins (mapv #(select-keys % [:min :max :count])
14621405
(take 3 (:bin-maps (first (:bins stat)))))})
1406+
14631407
;; ### ⚙️ `render-mark` `:bar`
14641408
;;
14651409
;; Bars projected as 4-corner polygons, works with cartesian, flip, and polar.
@@ -1495,6 +1439,16 @@ mpg
14951439
(lay (histogram {:color :species}))
14961440
plot)
14971441

1442+
;; ### ⚙️ Flip
1443+
;;
1444+
;; Swaps x and y axes. Histograms become horizontal, bar charts grow sideways.
1445+
1446+
(defmethod make-coord :flip [_ sx sy pw ph m]
1447+
(fn [dx dy] [(sx dy) (sy dx)]))
1448+
1449+
(defmethod render-grid :flip [_ sx sy pw ph m cfg]
1450+
(render-grid :cartesian sx sy pw ph m cfg))
1451+
14981452
;; ### 🧪 Flipped Histogram
14991453
;;
15001454
;; `:flip` swaps the axes -- bars grow leftward:
@@ -1515,9 +1469,8 @@ mpg
15151469

15161470
;; ### ⚙️ Line Constructors
15171471

1518-
(defn line-mark
1519-
"Line mark with identity stat. Named `line-mark` to avoid shadowing
1520-
SVG's [:line] element used elsewhere in this namespace."
1472+
(defn line
1473+
"Line mark with identity stat."
15211474
([] {:mark :line :stat :identity})
15221475
([opts] (merge {:mark :line :stat :identity} opts)))
15231476

@@ -1547,7 +1500,7 @@ mpg
15471500
(-> (view {:year [2018 2019 2020 2021 2022]
15481501
:sales [10 15 13 17 20]}
15491502
[[:year :sales]])
1550-
(lay (line-mark))
1503+
(lay (line))
15511504
plot)
15521505

15531506
;; ### 🧪 Colored Line Chart
@@ -1557,7 +1510,7 @@ mpg
15571510
:region ["East" "East" "East" "East" "East"
15581511
"West" "West" "West" "West" "West"]}
15591512
[[:year :sales]])
1560-
(lay (line-mark {:color :region}))
1513+
(lay (line {:color :region}))
15611514
plot)
15621515

15631516
;; ---
@@ -1574,15 +1527,15 @@ mpg
15741527
;; Base views duplicated, each copy with a different layer:
15751528

15761529
(-> (view iris [[:sepal-length :sepal-width]])
1577-
(lay (point) (line-mark))
1530+
(lay (point) (line))
15781531
(->> (mapv #(select-keys % [:x :y :mark :stat]))))
15791532

15801533
;; ### 🧪 Scatter + Line Overlay
15811534

15821535
(-> (view {:year [2018 2019 2020 2021 2022]
15831536
:sales [10 15 13 17 20]}
15841537
[[:year :sales]])
1585-
(lay (point) (line-mark))
1538+
(lay (point) (line))
15861539
plot)
15871540

15881541
;; ### 🧪 Colored Scatter + Line
@@ -1592,7 +1545,7 @@ mpg
15921545
:region ["East" "East" "East" "East" "East"
15931546
"West" "West" "West" "West" "West"]}
15941547
[[:year :sales]])
1595-
(lay (point {:color :region}) (line-mark {:color :region}))
1548+
(lay (point {:color :region}) (line {:color :region}))
15961549
plot)
15971550

15981551
;; ---
@@ -1653,6 +1606,7 @@ mpg
16531606
resolve-view
16541607
compute-stat
16551608
kind/pprint)
1609+
16561610
;; ### ⚙️ `compute-stat` `:loess`
16571611
;;
16581612
;; Loess via fastmath.interpolation (80 sample points, x values deduplicated).
@@ -1822,7 +1776,7 @@ mpg
18221776
(if coord-px
18231777
[:polygon {:points (arc-polygon-points coord-px x-lo x-hi py-lo py-hi 20)
18241778
:fill color :opacity opacity}]
1825-
[:rect {:x x-lo :y (clojure.core/min py-lo py-hi)
1779+
[:rect {:x x-lo :y (min py-lo py-hi)
18261780
:width (- x-hi x-lo)
18271781
:height (Math/abs (- py-lo py-hi))
18281782
:fill color :opacity opacity}])))
@@ -1832,7 +1786,7 @@ mpg
18321786
(let [{:keys [all-colors sx sy coord-px position cfg]} ctx
18331787
cfg (or cfg defaults)
18341788
bw (ws/data sx :bandwidth)
1835-
n-colors (clojure.core/count (:bars stat))
1789+
n-colors (count (:bars stat))
18361790
cum-y (atom {})
18371791
active-map (when (= position :dodge)
18381792
(into {}
@@ -1845,28 +1799,28 @@ mpg
18451799
(into [:g]
18461800
(mapcat (fn [bi {:keys [color counts]}]
18471801
(let [c (if color (color-for all-colors color) (:default-color cfg))]
1848-
(for [{:keys [category count]} counts
1849-
:when (or (= position :stack) (pos? count))
1850-
:let [band-info (sx category true)
1802+
(for [{cat :category cnt :count} counts
1803+
:when (or (= position :stack) (pos? cnt))
1804+
:let [band-info (sx cat true)
18511805
band-start (:rstart band-info)
18521806
band-end (:rend band-info)
18531807
band-mid (/ (+ band-start band-end) 2.0)]]
18541808
(if (= position :stack)
1855-
(let [base (get @cum-y category 0)
1809+
(let [base (get @cum-y cat 0)
18561810
py-lo (sy base)
1857-
py-hi (sy (+ base count))
1811+
py-hi (sy (+ base cnt))
18581812
x-lo (- band-mid (* bw 0.4))
18591813
x-hi (+ band-mid (* bw 0.4))]
1860-
(swap! cum-y assoc category (+ base count))
1814+
(swap! cum-y assoc cat (+ base cnt))
18611815
(render-bar-elem coord-px x-lo x-hi py-lo py-hi c cfg))
1862-
(let [active (get active-map category)
1863-
n-active (clojure.core/count active)
1816+
(let [active (get active-map cat)
1817+
n-active (count active)
18641818
active-idx (.indexOf ^java.util.List active bi)
1865-
sub-bw (/ (* bw 0.8) (clojure.core/max 1 n-active))
1819+
sub-bw (/ (* bw 0.8) (max 1 n-active))
18661820
x-lo (+ (- band-mid (/ (* n-active sub-bw) 2.0)) (* active-idx sub-bw))
18671821
x-hi (+ x-lo sub-bw)
18681822
py-lo (sy 0)
1869-
py-hi (sy count)]
1823+
py-hi (sy cnt)]
18701824
(render-bar-elem coord-px x-lo x-hi py-lo py-hi c cfg))))))
18711825
(range) (:bars stat)))))
18721826

@@ -1876,13 +1830,13 @@ mpg
18761830
cfg (or cfg defaults)
18771831
bw (ws/data sx :bandwidth)
18781832
groups (:points stat)
1879-
n-groups (clojure.core/count groups)
1880-
sub-bw (/ (* bw 0.8) (clojure.core/max 1 n-groups))
1833+
n-groups (count groups)
1834+
sub-bw (/ (* bw 0.8) (max 1 n-groups))
18811835
cum-y (atom {})]
18821836
(into [:g]
18831837
(mapcat (fn [gi {:keys [color xs ys]}]
18841838
(let [c (if color (color-for all-colors color) (:default-color cfg))]
1885-
(for [i (range (clojure.core/count xs))
1839+
(for [i (range (count xs))
18861840
:let [cat (nth xs i)
18871841
val (nth ys i)
18881842
band-info (sx cat true)
@@ -1916,15 +1870,15 @@ mpg
19161870
(defmethod render-x-ticks :categorical [_ sx pw ph m cfg]
19171871
(let [ticks (ws/ticks sx)
19181872
labels (map str ticks)]
1919-
(into [:g {:font-size (:font-size theme) :fill "#666"}]
1873+
(into [:g {:font-size (:font-size theme) :fill "#666" :font-family "sans-serif"}]
19201874
(map (fn [t label]
19211875
[:text {:x (sx t) :y (- ph 2) :text-anchor "middle"} label])
19221876
ticks labels))))
19231877

19241878
(defmethod render-y-ticks :categorical [_ sy pw ph m cfg]
19251879
(let [ticks (ws/ticks sy)
19261880
labels (map str ticks)]
1927-
(into [:g {:font-size (:font-size theme) :fill "#666"}]
1881+
(into [:g {:font-size (:font-size theme) :fill "#666" :font-family "sans-serif"}]
19281882
(map (fn [t label]
19291883
[:text {:x (- m 3) :y (+ (sy t) 3) :text-anchor "end"} label])
19301884
ticks labels))))
@@ -2227,6 +2181,7 @@ mpg
22272181
[:sepal-length :sepal-width :petal-length]))
22282182
(when-off-diagonal {:color :species})
22292183
plot)
2184+
22302185
;; ### 🧪 Faceted Scatter
22312186
;;
22322187
;; One panel per species -- `facet` splits views by a column:
@@ -2364,6 +2319,59 @@ mpg
23642319
(scale :y :log)
23652320
plot)
23662321

2322+
;; ### ⚙️ Polar
2323+
;;
2324+
;; [Polar coordinates](https://en.wikipedia.org/wiki/Polar_coordinate_system)
2325+
;; map x to angle and y to radius. Bars become wedges, scatters wrap into a disc.
2326+
2327+
(defmethod show-ticks? :polar [_] false)
2328+
2329+
(defmethod make-coord :polar [_ sx sy pw ph m]
2330+
(let [cx (/ pw 2.0) cy (/ ph 2.0)
2331+
r-max (- (min cx cy) m)
2332+
x-lo (double m) x-span (double (- pw m m))
2333+
y-lo (double m) y-span (double (- ph m m))]
2334+
(fn [dx dy]
2335+
(let [px (sx dx) py (sy dy)
2336+
;; Normalize to [0,1]: t-angle sweeps the full circle,
2337+
;; t-radius goes from center (0) to edge (1).
2338+
t-angle (/ (- px x-lo) (max 1.0 x-span))
2339+
t-radius (/ (- (+ y-lo y-span) py) (max 1.0 y-span))
2340+
angle (* 2.0 Math/PI t-angle)
2341+
radius (* r-max t-radius)]
2342+
[(+ cx (* radius (Math/cos (- angle (/ Math/PI 2.0)))))
2343+
(+ cy (* radius (Math/sin (- angle (/ Math/PI 2.0)))))]))))
2344+
2345+
(defmethod make-coord-px :polar [_ sx sy pw ph m]
2346+
(let [cx (/ pw 2.0) cy (/ ph 2.0)
2347+
r-max (- (min cx cy) m)
2348+
x-lo (double m) x-span (double (- pw m m))
2349+
y-lo (double m) y-span (double (- ph m m))]
2350+
(fn [px py]
2351+
(let [t-angle (/ (- px x-lo) (max 1.0 x-span))
2352+
t-radius (/ (- (+ y-lo y-span) py) (max 1.0 y-span))
2353+
angle (* 2.0 Math/PI t-angle)
2354+
radius (* r-max t-radius)]
2355+
[(+ cx (* radius (Math/cos (- angle (/ Math/PI 2.0)))))
2356+
(+ cy (* radius (Math/sin (- angle (/ Math/PI 2.0)))))]))))
2357+
2358+
(defmethod render-grid :polar [_ sx sy pw ph m cfg]
2359+
(let [cfg (or cfg defaults)
2360+
cx (/ pw 2.0) cy (/ ph 2.0)
2361+
r-max (- (min cx cy) m)]
2362+
(into [:g]
2363+
(concat
2364+
(for [i (range 1 6)
2365+
:let [r (* r-max (/ i 5.0))]]
2366+
[:circle {:cx cx :cy cy :r r :fill "none"
2367+
:stroke (:grid theme) :stroke-width (:grid-stroke-width cfg)}])
2368+
(for [i (range 8)
2369+
:let [a (* i (/ Math/PI 4))]]
2370+
[:line {:x1 cx :y1 cy
2371+
:x2 (+ cx (* r-max (Math/cos a)))
2372+
:y2 (+ cy (* r-max (Math/sin a)))
2373+
:stroke (:grid theme) :stroke-width (:grid-stroke-width cfg)}])))))
2374+
23672375
;; ### 🧪 Polar Scatter
23682376
;;
23692377
;; `coord :polar` wraps the same scatter into polar space:
@@ -2423,6 +2431,7 @@ mpg
24232431
[(hline 3.0)
24242432
(vline 6.0)
24252433
(hband 2.5 3.5)]
2434+
24262435
;; ### ⚙️ `render-annotation` methods
24272436

24282437
(defmethod render-annotation :rule-h [ann {:keys [coord-fn x-domain cfg]}]
@@ -2635,6 +2644,7 @@ mpg
26352644
(-> (view iris [[:sepal-length :sepal-width]])
26362645
(lay (point {:color :species}))
26372646
(plot {:tooltip true}))
2647+
26382648
;; ### 🧪 Brushable Scatter
26392649
;;
26402650
;; `:brush true` adds a drag-to-select rectangle:

0 commit comments

Comments
 (0)