(import '[java.time LocalDateTime]
'[java.time.format DateTimeFormatter])java.time.format.DateTimeFormatterMay 2, 2026
Babashka recently joined the set of languages supported on Clojure Civitas, thanks to Babqua — a Quarto extension that evaluates {.clojure .bb} code blocks during render. The introductory post covers the setup. Here we want to push a little further and see how far Babashka goes beyond its familiar home in scripting and shell automation — in particular, whether it works comfortably as a tool for interactive data analysis.
This post is a small experiment in that direction. It is a Babashka-flavored adaptation of a Noj tutorial that walks through the same dataset using tablecloth and tableplot. Babashka does not ship those libraries, so we lean on what is already at hand: Clojure’s core seq operations (map, filter, group-by, sort-by) over sequences of maps, plus a few of Babashka’s bundled facilities — slurp for fetching over HTTP, clojure.string for tokenizing, java.time for parsing timestamps — and Plotly.js for the charts. The shape of the analysis is the same; only the toolbox is lighter.
The early signs are promising. Writing data-analysis posts in Babashka with quarto preview watching the file feels close to a live notebook: each save re-renders within a second or two — fresh bb process, fresh data, refreshed plots — and that tight loop is what makes the workflow look genuinely viable for data analysis. Babashka’s millisecond startup is doing most of the work.
The Clojure events calendar feed is an ICS file that lists meetups, conferences, study groups, and CFP deadlines from organizers across the Clojure community. It is maintained by Gert Goet, who started it in 2020 and has been steadily curating it since. Gert wrote a short retrospective on Clojureverse when the feed turned two — that post is a good starting point if you want to understand what the feed is and how it grew.
The feed is one of the few places where the rhythm of Clojure community activity is visible in one structured form. Reading it tells you about how user groups have been meeting, when conferences cluster in the calendar year, and how new initiatives appear and pick up. None of that information is hard to gather by hand, but having it as a single ICS file makes it tractable.
A sincere thank-you to Gert for keeping the feed alive, and to the organizers whose events flow into it.
Only the java.time classes need an explicit import. Babashka pre-aliases clojure.string as str in user code, so the familiar str/split, str/replace, and friends are already on hand without a require.
Babashka’s slurp accepts URLs directly, so a single call retrieves the live feed. Each render of this post fetches it again; if you want to keep state warm across renders, see the persistent-session option in the Babqua docs.
A peek at the raw ICS — the first slice is enough to see the structure. The ^:kind/... metadata used in the blocks below follows the Kindly convention for annotating values with how they should be rendered; Babqua’s kindly kinds page lists which kinds are currently supported.
BEGIN:VCALENDAR VERSION:2.0 PRODID:-//hacksw/handcal//NONSGML v1.0//EN X-WR-CALNAME:Clojure Events BEGIN:VEVENT UID:message_591097852@clojurians.zulipchat.com DTSTART:20260501T180000Z DTEND:20260501T200000Z SUMMARY:Clojure real-world-data 58 LOCATION:online DESCRIPTION:On Friday, we’ll have the usual weekly meeting of the real-world-data group.\n As usual, we will focus on community projects and library development, but people are invited to propose additional topics to discuss and share.\n The main topics for the coming meetings will be documenting Noj, data visualization, and additional updates.\n Updates about the agenda will be shared in the group chat: the real-world-data channel at the Zulip chat (requires login).\n If you wish to join the group, please reach out beforehand, and we’ll add you to the calendar event. \nparens1920×1440 98.2 KB\n\nZulip: https://clojurians.zulipchat.com/#narrow/stream/262224-events/near/591097852 URL:https://clojureverse.org/t/clojure-real-world-data-58/14884 END:VEVENT BEGIN:VEVENT UID:message_588675083@clojurians.zulipchat.com DTSTART:20260425T170000Z DTEND:20260425T190000Z SUMMARY:Clojure Community Check-In - Part I LOCATION:online DESCRIPTION:Event information: https://clojureverse.org/t/clojure-community-check-in/\n\nZulip: https://clojurians.zulipchat.com/#narrow/stream/262224-events/near/588675083 URL:https://clojureverse.org/t/clojure-community-check-in-part-i/14881 END:VEVENT BEGIN:VEVENT UID:message_588674973
Each event is wrapped in BEGIN:VEVENT / END:VEVENT and contains lines like SUMMARY:, DTSTART:, URL:. We only need those three fields.
We split the file on the boundary between events, then for each event we keep the lines that start with one of our three keys. The result of parsing one event is a small map; the result of parsing the file is a sequence of such maps.
(defn parse-event
"Return {:summary ... :dtstart ... :url ...} for a single VEVENT chunk."
[event-text]
(->> event-text
str/split-lines
(keep (fn [line]
(when-let [field (re-find #"URL:|SUMMARY:|DTSTART:" line)]
[(-> field (str/replace ":" "") str/lower-case keyword)
(str/replace line field "")])))
(into {})))
(def raw-events
(->> (str/split feed-string #"END:VEVENT\nBEGIN:VEVENT")
(map parse-event)
(filter :dtstart)))
(count raw-events)A look at one parsed event:
The :dtstart field is a string like 20240315T120000Z. We parse it into a LocalDateTime, derive the year (handy for filtering), and keep an ISO-formatted string for charting (Plotly auto-detects date axes from ISO timestamps).
(def ics-formatter
(DateTimeFormatter/ofPattern "yyyyMMdd'T'HHmmss'Z'"))
(defn enrich [event]
(let [datetime (LocalDateTime/parse (:dtstart event) ics-formatter)]
(assoc event
:datetime datetime
:iso (str datetime)
:year (.getYear datetime))))
(def events
(->> raw-events
(map enrich)
(sort-by :datetime)))
(count events)A small table of the first few rows, just to confirm the shape:
| :iso | :summary | :url |
|---|---|---|
| 2020-10-31T09:00 | Practicalli Live Coding broadcast | https://youtu.be/QKBZYSITkRc |
| 2020-11-01T18:00 | Clojure and Data Science in Health Care | https://tinyurl.com/yxl3xh7o |
| 2020-11-07T20:00 | Reveal – Read Eval Visualize Loop | https://tinyurl.com/y2qj7m8k |
| 2020-11-10T18:30 | HATEOAS in Clojure | https://www.meetup.com/London-Clojurians/events/274159868/ |
| 2020-11-12T02:00 | Simplicity & Power of theClojure CLI (Nate … | https://www.meetup.com/Los-Angeles-Clojure-Users-Group/events/274336226/ |
Adding a running index gives us a y-coordinate. Each event is a point at (date, total-events-so-far), so the line traces the feed’s growth.
(def with-cumulative-count
(map-indexed (fn [index event]
(assoc event :count (inc index)))
events))
^:kind/plotly
{:data [{:type "scatter"
:mode "lines+markers"
:x (mapv :iso with-cumulative-count)
:y (mapv :count with-cumulative-count)
:text (mapv :summary with-cumulative-count)
:hovertemplate "%{text}<br>%{x|%Y-%m-%d}<extra></extra>"
:line {:width 1 :color "#5e81ac"}
:marker {:size 5 :color "#5e81ac"}}]
:layout {:xaxis {:title "date"}
:yaxis {:title "events so far"}
:width 740
:height 340
:margin {:t 20 :r 20}}}The plot shows a noticeable gap around 2022 — a stretch where the feed has very few entries. The reasons are historical and beyond the scope of this analysis; for the questions we want to ask here (which groups have been active, and how often they have been meeting), the cleaner thing to do is to restrict attention to 2023 and later, where the feed is densely populated.
The cumulative count is preserved, so the y-axis still measures global “events so far” — we are simply zooming in on the right-hand portion of the timeline.
^:kind/plotly
{:data [{:type "scatter"
:mode "lines+markers"
:x (mapv :iso recent-events)
:y (mapv :count recent-events)
:text (mapv :summary recent-events)
:hovertemplate "%{text}<br>%{x|%Y-%m-%d}<extra></extra>"
:line {:width 1 :color "#5e81ac"}
:marker {:size 5 :color "#5e81ac"}}]
:layout {:xaxis {:title "date"}
:yaxis {:title "events so far"}
:width 740
:height 340
:margin {:t 20 :r 20}}}Many event URLs contain a slug that identifies the organizing group — london-clojurians, los-angeles-clojure, data-recur, and so on. A small alternation regex covers the groups whose names appear frequently in the feed; everything else falls into an "other" bucket.
(def groups-pattern
#"london-clojurians|los-angeles-clojure|visual-tools|data-recur|real-world-data|scicloj-llm|scicloj-ai|macroexpand|clojure-dsp")
(defn assign-group [event]
(assoc event :group
(or (some->> (:url event) str/lower-case (re-find groups-pattern))
"other")))
(def grouped-events
(->> recent-events
(filter :url)
(map assign-group)))
(->> grouped-events
(map :group)
frequencies
(sort-by key))Re-using the global :count and adding :group as a color channel: each group’s events scatter along the same growth curve, and the colors give a sense of how the community’s activity has been spread over time.
^:kind/plotly
{:data (->> (group-by :group grouped-events)
(sort-by key)
(mapv (fn [[group events-in-group]]
{:type "scatter"
:mode "markers"
:name group
:x (mapv :iso events-in-group)
:y (mapv :count events-in-group)
:text (mapv :summary events-in-group)
:hovertemplate "%{text}<br>%{x|%Y-%m-%d}<extra>%{fullData.name}</extra>"
:marker {:size 7}})))
:layout {:xaxis {:title "date"}
:yaxis {:title "events so far"}
:width 780
:height 400
:legend {:title {:text "group"}}
:margin {:t 20 :r 20}}}The previous plot uses a single global counter, so each group’s points lie on the overall curve. To follow how each group has been evolving on its own — when it picked up, when it went quiet, when something new started — we want a per-group running count: how many events that group has held up to each date. In tablecloth this is group-by + add-column + ungroup; in plain Clojure it is group-by + mapcat over the partitions.
(def per-group-series
(->> grouped-events
(group-by :group)
(mapcat (fn [[_ events-in-group]]
(->> events-in-group
(sort-by :datetime)
(map-indexed (fn [index event]
(assoc event :group-count (inc index)))))))))
(->> per-group-series
(filter #(= "london-clojurians" (:group %)))
(take 3)
(map #(select-keys % [:iso :group :group-count :summary])))({:iso "2023-01-10T18:30", :group "london-clojurians", :group-count 1, :summary "Lisp curse vs Lisp envy (by Mauricio Szabo)"} {:iso "2023-01-24T18:30", :group "london-clojurians", :group-count 2, :summary "Open Source Licenses for Developers (by Martin..."} {:iso "2023-02-07T18:30", :group "london-clojurians", :group-count 3, :summary "Simpler User Interfaces with Membrane (by Adri..."})Layering points on top of lines gives a per-group trajectory. Each line tells its own story — periods of momentum, pauses, fresh starts — and together they sketch where the community has been heading.
^:kind/plotly
{:data (->> (group-by :group per-group-series)
(sort-by key)
(mapv (fn [[group events-in-group]]
{:type "scatter"
:mode "lines+markers"
:name group
:x (mapv :iso events-in-group)
:y (mapv :group-count events-in-group)
:text (mapv :summary events-in-group)
:hovertemplate "%{text}<br>%{x|%Y-%m-%d}<extra>%{fullData.name}</extra>"
:line {:width 1.5}
:marker {:size 6}})))
:layout {:xaxis {:title "date"}
:yaxis {:title "events per group"}
:width 780
:height 440
:legend {:title {:text "group"}}
:margin {:t 20 :r 20}}}The real-world-data line in particular is worth pointing at. The Real-World Data dev group, recently reinitiated by Timothy Pratley, has been meeting on a steady weekly rhythm — each point on that line is a session that took preparation, hosting, and follow-through. The slope is a record of persistent work, and a thank-you is due to Tim and the participants who have kept it going.
The substance of this analysis lives in the feed itself, not in the code that reads it. The feed exists because Gert decided to maintain it and because organizers around the Clojure world keep listing their events. Counting and plotting are easy; the curation that produces the underlying data is the work that makes the rest possible.
If you organize a Clojure event and it is not yet in the feed, the Clojureverse thread is a good place to start; submitting an event is a single CLI call.
If you are curious about Babashka for data analysis — or interested in how far this lighter toolbox can be pushed — we would be glad to hear from you. The Real-World Data dev group is one of the spaces where this kind of exploration is happening, and visitors and collaborators are welcome.
For more on writing Babashka posts on Civitas, see the introductory post and the Babqua docs.