|
| 1 | +^{:kindly/hide-code true |
| 2 | + :clay {:title "Tableplot Tutorial: Customizing Plots with Parameter Substitution" |
| 3 | + :quarto {:author [:daslu] |
| 4 | + :description "Learn how to customize Tableplot visualizations using substitution parameters." |
| 5 | + :category :clojure |
| 6 | + :type :post |
| 7 | + :draft true |
| 8 | + :date "2025-11-10" |
| 9 | + :tags [:data-visualization :tableplot :tutorial]}}} |
| 10 | +(ns data-visualization.tableplot-parameter-flow |
| 11 | + (:require [scicloj.kindly.v4.kind :as kind] |
| 12 | + [tablecloth.api :as tc] |
| 13 | + [scicloj.tableplot.v1.plotly :as plotly])) |
| 14 | + |
| 15 | +^:kindly/hide-code |
| 16 | +(kind/hiccup |
| 17 | + [:style |
| 18 | + ".printedClojure { |
| 19 | + max-height:600px; |
| 20 | + overflow-y: auto; |
| 21 | +}"]) |
| 22 | + |
| 23 | +;; ## Introduction |
| 24 | +;; |
| 25 | +;; [Tableplot](https://scicloj.github.io/tableplot/) is a declarative plotting library |
| 26 | +;; that makes it easy to create interactive visualizations from tabular data. One of its |
| 27 | +;; most powerful features is **parameter substitution** - the ability to customize plot |
| 28 | +;; appearance and behavior by passing parameters that override defaults. |
| 29 | +;; |
| 30 | +;; This tutorial is a brief intro to this feature. |
| 31 | + |
| 32 | +;; ::: {.callout-tip collapse="true"} |
| 33 | +;; ### Background: The Layered Grammar of Graphics |
| 34 | +;; |
| 35 | +;; Tableplot is inspired by the [**layered grammar of graphics**](https://vita.had.co.nz/papers/layered-grammar.html), |
| 36 | +;; a framework for understanding and building statistical visualizations. Originally developed by |
| 37 | +;; Leland Wilkinson and later refined by Hadley Wickham in ggplot2, the grammar views plots as |
| 38 | +;; compositions of independent components: data, aesthetic mappings, geometric objects, scales, |
| 39 | +;; coordinates, and facets. |
| 40 | +;; |
| 41 | +;; The challenge in implementing such a grammar is achieving multiple goals simultaneously: |
| 42 | +;; |
| 43 | +;; - **Succinct**: Simple things should be simple - sensible defaults for common cases |
| 44 | +;; - **Declarative**: Describe *what* you want, not *how* to draw it |
| 45 | +;; - **Flexible**: Support customization without sacrificing simplicity |
| 46 | +;; - **Observable**: Make the details visible and understandable when needed |
| 47 | +;; - **Extensible**: Allow users to work with internals without breaking abstractions |
| 48 | +;; |
| 49 | +;; Tableplot addresses these challenges by mostly adopting [Hanami](https://github.com/jsa-aerial/hanami)'s |
| 50 | +;; solution as a starting point. Hanami introduced a template-based approach with substitution keys, |
| 51 | +;; allowing hierarchical defaults: you can rely on conventions for quick plots, or override specific |
| 52 | +;; details when needed. The templates approach makes the transformation process observable, |
| 53 | +;; as we'll see in this tutorial. |
| 54 | +;; |
| 55 | +;; **Further reading:** |
| 56 | + |
| 57 | +;; - [ggplot2: Elegant Graphics for Data Analysis](https://ggplot2-book.org/) - The definitive guide showing how to balance simplicity and flexibility |
| 58 | +;; - [Demystifying stat_ layers in ggplot2](https://yjunechoe.github.io/posts/2020-09-26-demystifying-stat-layers-ggplot2/) - June Choe's exploration of how the grammar elegantly handles data transformations, with special focus on making the internals observable and extensible |
| 59 | +;; - [Analyzing Data with Clojure (Kevin Lynagh, 2012)](https://www.youtube.com/watch?v=xyGggdg31mc) - An early Clojure attempt to handle the challenge of building a grammar of graphics |
| 60 | + |
| 61 | +;; ::: |
| 62 | + |
| 63 | +;; ## A toy challenge: Customizing Grid Colors |
| 64 | +;; |
| 65 | +;; Let's start with a basic dataset and plot. |
| 66 | + |
| 67 | +(def sample-data |
| 68 | + (tc/dataset {:x [1 2 3 4 5] |
| 69 | + :y [2 4 3 5 7]})) |
| 70 | + |
| 71 | +;; In this tutorial, we'll use Tableplot's [Plotly.js](https://plotly.com/javascript/) API, |
| 72 | +;; which generates interactive Plotly.js visualizations. Tableplot also supports other backends |
| 73 | +;; like Vega-Lite and an experimental transpilation API. |
| 74 | + |
| 75 | +;; We can make a basic plot with two layers. |
| 76 | +;; This is really easy with our data, because the |
| 77 | +;; `:x` and `:y` columns are used by default for the plot's axes. |
| 78 | + |
| 79 | +(-> sample-data |
| 80 | + (plotly/layer-point {:=mark-size 20}) |
| 81 | + plotly/layer-line) |
| 82 | + |
| 83 | +;; ::: {.callout-tip collapse="true"} |
| 84 | +;; ### A brief look inside |
| 85 | + |
| 86 | +;; By default, when used in [Kindly](https://scicloj.github.io/kindly-noted/)-compatible |
| 87 | +;; tools like [Clay](https://scicloj.github.io/clay/) and in Clojure Civitas posts, |
| 88 | +;; Tableplot's plots are configured to be displayed visually. |
| 89 | + |
| 90 | +;; But we can also change the `kind` annotation, so that we can see them as plain |
| 91 | +;; Clojure data structures. |
| 92 | + |
| 93 | +(-> sample-data |
| 94 | + (plotly/layer-point {:=mark-size 20}) |
| 95 | + plotly/layer-line |
| 96 | + kind/pprint) |
| 97 | + |
| 98 | +;; You see, what API functions such as `plotly/layer-line` generate are |
| 99 | +;; certain maps called |
| 100 | +;; [templates](https://github.com/jsa-aerial/hanami?tab=readme-ov-file#templates-substitution-keys-and-transformations), |
| 101 | +;; a brilliant concept from the [Hanami](https://github.com/jsa-aerial/hanami) library. |
| 102 | + |
| 103 | +;; This is not the resulting Plotly.js specification yet. |
| 104 | +;; It is a potential for it, specifying lots of partial intermediate |
| 105 | +;; values, called substitution keys. |
| 106 | +;; By Tableplot's convention, substitution keys are keywords beginning with `=`, |
| 107 | +;; such as `:=layout` or `:=mark-color`. |
| 108 | +;; |
| 109 | +;; **Why templates?** They separate *what you want* (data mappings, colors, sizes) |
| 110 | +;; from *how to render it* (the actual Plotly.js specification). This gives you |
| 111 | +;; flexibility: you can override specific details or let defaults handle everything. |
| 112 | +;; |
| 113 | +;; Substitution keys can have default values, which can also be functions computing |
| 114 | +;; them from the values defined by other keys. On the user side, |
| 115 | +;; we may override any of these, as we'll see below. |
| 116 | + |
| 117 | +;; What if we actually want to see not the template, but the resulting |
| 118 | +;; Plotly.js specification? This is what `plotly/plot` is for. |
| 119 | + |
| 120 | +(-> sample-data |
| 121 | + (plotly/layer-point {:=mark-size 20}) |
| 122 | + plotly/layer-line |
| 123 | + plotly/plot |
| 124 | + kind/pprint) |
| 125 | + |
| 126 | +;; ::: |
| 127 | + |
| 128 | +;; ### Goal |
| 129 | + |
| 130 | +;; Assume that we now wish to colour the grid lines: vertical by green, |
| 131 | +;; horizontal by red. After all, what would be a |
| 132 | +;; better way to teach Tufte's [data-ink ratio](https://infovis-wiki.net/wiki/Data-Ink_Ratio) principle than doing exactly |
| 133 | +;; what it asks us to avoid, by adding some chartjunk? |
| 134 | + |
| 135 | +;; Here are three approaches, each with different tradeoffs. |
| 136 | + |
| 137 | +;; ## Approach 1: Using the relevant substitution keys |
| 138 | + |
| 139 | +;; Sometimes, what we need can be precisely specified in Tableplot. |
| 140 | +;; You may find the following in Tableplot's |
| 141 | +;; [Plotly API reference](https://scicloj.github.io/tableplot/tableplot_book.plotly_reference.html#yaxis-gridcolor): |
| 142 | + |
| 143 | +;; - [`:=xaxis-gridcolor`](https://scicloj.github.io/tableplot/tableplot_book.plotly_reference.html#xaxis-gridcolor) - The color for the x axis grid lines |
| 144 | +;; - [`:=yaxis-gridcolor`](https://scicloj.github.io/tableplot/tableplot_book.plotly_reference.html#yaxis-gridcolor) - The color for the y axis grid lines |
| 145 | + |
| 146 | +;; To use them, you can add a `base` before your plot layers, |
| 147 | +;; and configure it with these keys. We use `plotly/base` because its |
| 148 | +;; parameters flow to all subsequent layers, which is useful when |
| 149 | +;; composing multiple layers with shared settings. |
| 150 | + |
| 151 | +(-> sample-data |
| 152 | + (plotly/base {:=xaxis-gridcolor "green" |
| 153 | + :=yaxis-gridcolor "red"}) |
| 154 | + (plotly/layer-point {:=mark-size 20}) |
| 155 | + plotly/layer-line) |
| 156 | + |
| 157 | +;; ::: {.callout-tip collapse="true"} |
| 158 | +;; ### A brief look inside |
| 159 | + |
| 160 | +;; Let us see what actually has changed in the |
| 161 | +;; resulting specification: |
| 162 | + |
| 163 | +(-> sample-data |
| 164 | + (plotly/base {:=xaxis-gridcolor "green" |
| 165 | + :=yaxis-gridcolor "red"}) |
| 166 | + (plotly/layer-point {:=mark-size 20}) |
| 167 | + plotly/layer-line |
| 168 | + plotly/plot |
| 169 | + kind/pprint) |
| 170 | + |
| 171 | +;; ::: |
| 172 | + |
| 173 | +;; ## Approach 2: Overriding a broader-scope key |
| 174 | + |
| 175 | +;; What if the specific keys you need don't exist in Tableplot yet? |
| 176 | +;; |
| 177 | +;; Plotly.js itself will always be richer and more flexible than Tableplot's |
| 178 | +;; parameter system. |
| 179 | + |
| 180 | +;; Imagine that the above `:=xaxis-gridcolor` & `:=yaxis-gridcolor` would |
| 181 | +;; not be supported. |
| 182 | + |
| 183 | +;; If you read about |
| 184 | +;; [Styling and Coloring Axes and the Zero-Line](https://plotly.com/javascript/axes/#styling-and-coloring-axes-and-the-zero-line) |
| 185 | +;; in the Plotly.js docs, you will |
| 186 | +;; see that, under the `layout` part of the specification, |
| 187 | +;; you can specify `gridcolor` for each of the axes. |
| 188 | + |
| 189 | +;; In Tableplot, we can specify the whole layout using `:=layout`, and thus |
| 190 | +;; have anything we need in there. |
| 191 | + |
| 192 | +;; - [`:=layout`](https://scicloj.github.io/tableplot/tableplot_book.plotly_reference.html#layout) - The layout part of the resulting Plotly.js specification |
| 193 | + |
| 194 | +(-> sample-data |
| 195 | + (plotly/base {:=layout {:xaxis {:gridcolor "green"} |
| 196 | + :yaxis {:gridcolor "red"}}}) |
| 197 | + (plotly/layer-point {:=mark-size 20}) |
| 198 | + plotly/layer-line) |
| 199 | + |
| 200 | +;; Notice that a few other details of the aesthetics have changed, |
| 201 | +;; like the plot's background color. |
| 202 | + |
| 203 | +;; That is what happens when we override the whole `:=layout`. |
| 204 | +;; It is a powerful option, that you may or may not like, depending on |
| 205 | +;; your use case. |
| 206 | + |
| 207 | +;; ::: {.callout-tip collapse="true"} |
| 208 | +;; ### A brief look inside |
| 209 | + |
| 210 | +;; Let us see what happens: |
| 211 | + |
| 212 | +(-> sample-data |
| 213 | + (plotly/base {:=layout {:xaxis {:gridcolor "green"} |
| 214 | + :yaxis {:gridcolor "red"}}}) |
| 215 | + (plotly/layer-point {:=mark-size 20}) |
| 216 | + plotly/layer-line |
| 217 | + plotly/plot |
| 218 | + kind/pprint) |
| 219 | + |
| 220 | +;; As expected this time, the layout is small and simple, |
| 221 | +;; just what you specified. |
| 222 | + |
| 223 | +;; By the way, if you read further in that link to the docs, you will see |
| 224 | +;; that `:=layout` depends on `:=xaxis-gridcolor` and `:=yaxis-gridcolor`, |
| 225 | +;; among other things. When we specified those narrow-scope keys |
| 226 | +;; in our previous example, we actually went through affecting the |
| 227 | +;; broad-scope key, `:=layout`. |
| 228 | + |
| 229 | +;; ::: |
| 230 | + |
| 231 | +;; ## Approach 3: Direct Manipulation After `plotly/plot` |
| 232 | + |
| 233 | +;; The previous approaches work within Tableplot's API. But what if you need |
| 234 | +;; more surgical control — to use Plotly.js concepts while preserving most defaults? |
| 235 | + |
| 236 | +;; Of course we can do that! |
| 237 | + |
| 238 | +;; Of course, the answer has been in front of us the whole time: |
| 239 | +;; [It's just data](https://www.youtube.com/watch?v=jlPaby7suOc&t=1000s). |
| 240 | + |
| 241 | +;; We do not need to use Tableplot's API for everything. |
| 242 | +;; |
| 243 | +;; After we call `plotly/plot`, we can process the actual Plotly.js |
| 244 | +;; specification, as data. |
| 245 | + |
| 246 | +(-> sample-data |
| 247 | + (plotly/layer-point {:=mark-size 20}) |
| 248 | + plotly/layer-line |
| 249 | + plotly/plot |
| 250 | + (assoc-in [:layout :xaxis :gridcolor] "green") |
| 251 | + (assoc-in [:layout :yaxis :gridcolor] "red")) |
| 252 | + |
| 253 | +;; ::: {.callout-tip collapse="true"} |
| 254 | +;; ### A brief look inside |
| 255 | + |
| 256 | +;; You already know what to expect here: |
| 257 | + |
| 258 | +(-> sample-data |
| 259 | + (plotly/layer-point {:=mark-size 20}) |
| 260 | + plotly/layer-line |
| 261 | + plotly/plot |
| 262 | + (assoc-in [:layout :xaxis :gridcolor] "green") |
| 263 | + (assoc-in [:layout :yaxis :gridcolor] "red") |
| 264 | + kind/pprint) |
| 265 | + |
| 266 | +;; ::: |
| 267 | + |
| 268 | +;; ## Summary |
| 269 | +;; |
| 270 | +;; Tableplot's parameter substitution system gives you three levels of control: |
| 271 | +;; |
| 272 | +;; 1. **Specific substitution keys** (`:=xaxis-gridcolor`, `:=yaxis-gridcolor`) |
| 273 | +;; - ✅ Most convenient and discoverable |
| 274 | +;; - ✅ Preserves all other defaults |
| 275 | +;; - ❌ Limited to what Tableplot explicitly supports |
| 276 | +;; |
| 277 | +;; 2. **Broad-scope keys** (`:=layout`) |
| 278 | +;; - ✅ Full Plotly.js flexibility |
| 279 | +;; - ✅ Declarative, within Tableplot's API |
| 280 | +;; - ❌ Overrides ALL defaults for that scope |
| 281 | +;; |
| 282 | +;; 3. **Direct data manipulation** (`assoc-in` after `plotly/plot`) |
| 283 | +;; - ✅ Complete control |
| 284 | +;; - ✅ Surgical precision - only change what you want |
| 285 | +;; - ❌ More verbose |
| 286 | +;; - ❌ Leaves Tableplot's template system |
| 287 | +;; |
| 288 | +;; The key insight: **it's all just data**. Templates with substitution keys give you |
| 289 | +;; flexibility without magic. You can always drop down to plain Clojure data manipulation |
| 290 | +;; when needed. |
| 291 | +;; |
| 292 | +;; For more examples and the complete API reference, see the |
| 293 | +;; [Tableplot documentation](https://scicloj.github.io/tableplot/). |
0 commit comments