Skip to content

Commit 08f85db

Browse files
add algebra-of-data
1 parent f9748f8 commit 08f85db

File tree

1 file changed

+161
-0
lines changed

1 file changed

+161
-0
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
^{:kindly/hide-code true
2+
:clay {:title "The Algebra of Data"
3+
:quarto {:type :post
4+
:author [:timothypratley]
5+
:draft true
6+
:date "2025-12-29"
7+
:description "Exploring algebraic operators for generic data"
8+
:category :data-visualization
9+
:tags [:datavis]
10+
:keywords [:datavis]}}}
11+
(ns data-visualization.aog.algebra-of-data)
12+
13+
;; Inspired by [the algebra of graphics](../aog_in_clojure_part1.html).
14+
15+
;; ## Axioms
16+
17+
;; We shall take it as given that we are interested in combining sequence of maps:
18+
19+
[{:theme "dark"} {:font "serif"}]
20+
21+
;; AoG calls the maps "layers", but in this article I'll call them "configs".
22+
23+
;; ## Transitive operators
24+
25+
;; One way to combine configs is to concatenate them:
26+
27+
(def add
28+
(comp vec concat))
29+
30+
(add [{:theme "dark"}]
31+
[{:font "serif"}])
32+
33+
;; Another thing we can do is a cartesian merge:
34+
35+
(defn cross-merge
36+
;; Given 2 sequences, merge all combinations
37+
([xs ys]
38+
(vec (for [x xs
39+
y ys]
40+
(merge x y))))
41+
;; Extended to work with multiple arguments
42+
([xs ys & rest]
43+
(reduce cross-merge (cross-merge xs ys) rest))
44+
([xs] xs)
45+
([] []))
46+
47+
;; The standard case of cross-merging 2 configs
48+
49+
(cross-merge [{:theme "dark"}]
50+
[{:font "serif"}])
51+
52+
;; Extended to 3 configs
53+
54+
(cross-merge [{:theme "dark"}]
55+
[{:font "serif"}]
56+
[{:x :customer :y :order-count}])
57+
58+
;; ## Why is this interesting?
59+
60+
;; In short we can specify a larger structure with overlapping concerns concisely
61+
62+
(def a [{:theme "dark" :color "black"}])
63+
(def b [{:theme "light" :color "white"}])
64+
(def c [{:font "serif" :size 12}])
65+
(def d [{:font "sans-serif" :size 14}])
66+
67+
(cross-merge (add a b) (add c d))
68+
69+
;; We gained some expressive power.
70+
71+
;; ## Constructors
72+
73+
;; Given we like configs due to their combinatorial expressiveness,
74+
;; we may introduce some constructors to help us prepare configs concisely.
75+
76+
(defn theme [name color]
77+
[{:theme name :color color}])
78+
79+
(theme "dark" "black")
80+
81+
(defn font [name color]
82+
[{:font name :size color}])
83+
84+
(font "serif" 12)
85+
86+
(defn dims [x y]
87+
[{:x x :y y}])
88+
89+
(dims :customers :orders)
90+
91+
;; Now we can start combining our constructors and operators
92+
93+
(cross-merge (add (theme "dark" "black")
94+
(font "serif" 12))
95+
(dims :customers :orders))
96+
97+
;; ## Threading
98+
99+
;; Rather than constructors, we could define "methods"
100+
;; that accept configs, and cross-merge them with constructed configs.
101+
102+
(defn dims* [configs x y]
103+
(cross-merge configs [{:x x :y y}]))
104+
105+
(defn theme* [configs name color]
106+
(cross-merge configs [{:theme name :color color}]))
107+
108+
(defn font* [configs name color]
109+
(cross-merge configs [{:font name :size color}]))
110+
111+
(-> (theme "dark" "black")
112+
(font* "serif" 12))
113+
114+
;; A key collision implies addition
115+
;; TODO: explain this idea in more detail,
116+
;; TLDR: threading can represent almost everything
117+
118+
;; ## Unification
119+
120+
;; The most obvious thing we can do with "configs" is merge them together.
121+
122+
(def configs
123+
(cross-merge (add (theme "dark" "black")
124+
(font "serif" 12))
125+
(add (dims :customers :orders)
126+
(dims :products :orders))))
127+
128+
configs
129+
130+
(apply merge configs)
131+
132+
;; But doing so loses information (specifically additions).
133+
;; Rather we might wish to detect conflicts where they occur.
134+
135+
(apply merge-with list configs)
136+
137+
;; Convert config map values into sets so merging can unify
138+
;; multiple possible values per key without duplicates.
139+
;; This is a simple form of "unification": collapsing alternatives.
140+
(defn normalize-to-sets [m]
141+
(update-vals m hash-set))
142+
143+
;; Merge a sequence of config maps, unifying values as sets
144+
;; (flat, unique per key).
145+
(defn merge-as-sets [configs]
146+
(apply merge-with clojure.set/union
147+
(map normalize-to-sets configs)))
148+
149+
(merge-as-sets configs)
150+
151+
;; Why is this interesting?
152+
;; Well in a chart for example we will have some properties
153+
;; that are global, and some that are "layered".
154+
;; This just demonstrates one process to convert an algebraic representation
155+
;; into a structured representation.
156+
157+
;; ## Conclusion
158+
159+
;; I think these ideas are more broadly applicable than just graphics.
160+
;; The only domain specific part about them is defining useful constructors,
161+
;; and the unification behaviors.

0 commit comments

Comments
 (0)