Plotting Datoms: Queries as Visual Mappings
datavis, algebra, datascript, queries, datoms
Most plotting libraries operate on dataframes analogous to a table with rows and columns. But what if our plots operated on a Datomic style database of facts instead?
DataScript is an in-memory database of datoms (entity-attribute-value triples). A plot query can select exactly the facts we want to visualize, binding them to visual channels. The query itself becomes the mapping: which attributes become x, y, color, or relationships.
(require '[datascript.core :as d])Why query-based plotting? Databases are relational. Entities can have arbitrary attributes and relationships to other entities. Queries let us express things that don’t fit in a single table: joins across entities, aggregations, derived relationships. A query-first approach makes relational data available for visualization.
First: some tiny datoms to play with.
(def penguins
[{:species "Adelie" :bill_length 29.1 :bill_depth 18.7 :sex "MALE"}
{:species "Adelie" :bill_length 33.5 :bill_depth 15.4 :sex "FEMALE"}
{:species "Chinstrap" :bill_length 43.5 :bill_depth 17.9 :sex "FEMALE"}
{:species "Gentoo" :bill_length 47.3 :bill_depth 13.8 :sex "MALE"}])(def penguin-db
(let [conn (d/create-conn)]
(d/transact! conn penguins)
@conn))Think of the database as many tiny facts. Each fact says: “entity has attribute value”.
penguin-db#datascript/DB {:schema nil,
:datoms
[[1 :bill_depth 18.7 536870913]
[1 :bill_length 29.1 536870913]
[1 :sex "MALE" 536870913]
[1 :species "Adelie" 536870913]
[2 :bill_depth 15.4 536870913]
[2 :bill_length 33.5 536870913]
[2 :sex "FEMALE" 536870913]
[2 :species "Adelie" 536870913]
[3 :bill_depth 17.9 536870913]
[3 :bill_length 43.5 536870913]
[3 :sex "FEMALE" 536870913]
[3 :species "Chinstrap" 536870913]
[4 :bill_depth 13.8 536870913]
[4 :bill_length 47.3 536870913]
[4 :sex "MALE" 536870913]
[4 :species "Gentoo" 536870913]]}How to plot a database:
(def default-palette
["#2563eb" "#f97316" "#10b981" "#a855f7" "#ef4444" "#14b8a6" "#f59e0b" "#6b7280"])(defn color-scale
"Given a sequence of category values,
assign consistent colors from the default palette."
[categories]
(let [domain (distinct categories)
colors (cycle default-palette)]
(into {} (map vector domain colors))))(defn plot-basic [g]
(let [{:keys [db query geometry]} (g 1)
results (vec (d/q query db))]
(for [geom geometry]
(case geom
:point (let [color-map (color-scale (map last results))]
(for [[x y color] results]
[:circle {:r 2, :cx x, :cy y
:fill (get color-map color "gray")}]))
:line (for [[x1 y1 x2 y2] results]
[:line {:x1 x1, :y1 y1, :x2 x2, :y2 y2}])))))To specify a plot, we provide a query
(def bill-scatter
[:graphic {:db penguin-db
:query '{:find [?x ?y ?color]
:where [[?e :species ?color]
[?e :bill_length ?x]
[?e :bill_depth ?y]]}
:geometry [:point]}])The coordinates fall out of the query bindings; geometry only chooses how to render them:
Wrap it in a tiny SVG viewport to see the result:
^:kind/hiccup
[:svg {:width "100%" :height "300"
:viewBox "0 0 50 50"
:xmlns "http://www.w3.org/2000/svg"}
[:g {:stroke "gray", :fill "none"}
(plot-basic bill-scatter)]]We can also query for relationships between entities. For example, pairs of penguins from the same species:
(def same-species-relationships
[:graphic {:db penguin-db
:query '{:find [?x1 ?y1 ?x2 ?y2]
:where [[?e1 :species ?s]
[?e2 :species ?s]
[?e1 :bill_length ?x1]
[?e1 :bill_depth ?y1]
[?e2 :bill_length ?x2]
[?e2 :bill_depth ?y2]
[(not= ?e1 ?e2)]]}
:geometry [:line]}])This small example shows that the mapping lives in the query. Queries can bind points and relationships between entities. Each geometry expects a specific binding shape (points: [x y color], lines: [x1 y1 x2 y2]), so the query and geometry must agree on that contract. The novelty here is expressiveness: a relational query can yield edges, something dataframe workflows don’t model directly.
^:kind/hiccup
[:svg {:width "100%" :height "300"
:viewBox "0 0 50 50"
:xmlns "http://www.w3.org/2000/svg"}
[:g {:stroke "gray", :fill "none"}
(plot-basic bill-scatter)
(plot-basic same-species-relationships)]]Plots as queries let us say more. Points and edges can be defined by a query. The novelty in this example is that relationships can be a first‑class things to draw.