23  Interactivity

Plotje produces SVG hiccup. Two layers of interaction are available:

The static GFM render of this notebook shows the SVGs as flat images. Open the HTML rendering to see the interactions live.

(ns plotje-book.interactivity
  (:require
   ;; Tablecloth -- dataset manipulation
   [tablecloth.api :as tc]
   ;; rdatasets -- bundled R datasets
   [scicloj.metamorph.ml.rdatasets :as rdatasets]
   ;; Kindly -- notebook rendering protocol
   [scicloj.kindly.v4.kind :as kind]
   ;; Plotje -- composable plotting
   [scicloj.plotje.api :as pj]))

Tooltips

Pass :tooltip true to pj/options and every data shape gets a data-tooltip attribute holding its column values. A small embedded script renders the tooltip on hover – no extra setup.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:title "Hover over a point for column values"
                 :tooltip true
                 :height 320}))
Hover over a point for column valuessepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Brush selection

:brush true enables drag-to-select. While dragging, a shaded rectangle follows the cursor; on release, points inside keep full opacity and points outside dim to 0.15. A short drag (less than 3 pixels each side) clears the selection. Selection is keyed by row index, so it tracks the same rows across every panel in the pose.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:title "Drag a rectangle to highlight a region"
                 :brush true
                 :height 320}))
Drag a rectangle to highlight a regionsepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Cross-panel linked highlighting

Because brush selection is keyed by data-row-idx (a stable integer attached to each rendered shape at extract time), the same selection lights up matching rows in every panel of a faceted pose. Drag in one species panel; the corresponding rows in the other two species panels respond immediately.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width)
    (pj/facet :species)
    (pj/options {:title "Brush on one panel, see linked points in the others"
                 :brush true
                 :tooltip true
                 :height 320}))
Brush on one panel, see linked points in the otherssepal widthsepal length682.02.53.03.54.04.56868setosaversicolorvirginica

Interval (Gantt) tooltips

lay-interval-h participates in the same tooltip/brush system. Each rectangle’s tooltip names the lane, the start, the end, and the color label; on temporal axes the start and end are formatted as date strings rather than raw epoch-milliseconds.

(-> {:start [#inst "2024-01-01" #inst "2024-02-15" #inst "2024-04-01"
             #inst "2024-05-10" #inst "2024-06-20"]
     :end   [#inst "2024-03-15" #inst "2024-04-20" #inst "2024-06-30"
             #inst "2024-07-10" #inst "2024-08-30"]
     :task  ["Design" "Build" "Test" "Deploy" "Document"]
     :team  ["UX" "Eng" "QA" "Eng" "UX"]}
    (pj/lay-interval-h :start :task {:x-end :end :color :team})
    (pj/options {:title "Hover for task: start → end, team"
                 :tooltip true
                 :height 320}))
Hover for task: start → end, teamtaskstartteamUXEngQAFeb-01Mar-01Apr-01May-01Jun-01Jul-01Aug-01DesignBuildTestDeployDocument

Custom wrapper: save as PNG

Browsers can serialize an SVG element to a PNG via a <canvas> round-trip. The wrapper below adds a “Save PNG” button that renders the current SVG into a canvas and triggers a download.

(let [plot-svg (pj/plot
                (-> (rdatasets/datasets-iris)
                    (pj/lay-point :sepal-length :sepal-width {:color :species})
                    (pj/options {:title "Click 'Save PNG' to download the rendering"
                                 :height 320})))
      attrs (second plot-svg)
      body (drop 2 plot-svg)
      plot-id (str "pj-png-" (System/nanoTime))
      btn-id (str plot-id "-save")
      script (str "document.getElementById('" btn-id "').addEventListener('click',function(){"
                  "var svg=document.getElementById('" plot-id "');"
                  "var w=svg.clientWidth||" (or (:width attrs) 600) ","
                  "h=svg.clientHeight||" (or (:height attrs) 400) ";"
                  "var data=new XMLSerializer().serializeToString(svg);"
                  "var img=new Image();"
                  "img.onload=function(){"
                  "var c=document.createElement('canvas');c.width=w;c.height=h;"
                  "c.getContext('2d').drawImage(img,0,0,w,h);"
                  "var a=document.createElement('a');"
                  "a.href=c.toDataURL('image/png');a.download='plotje.png';"
                  "document.body.appendChild(a);a.click();a.remove();};"
                  "img.src='data:image/svg+xml;base64,'+btoa(unescape(encodeURIComponent(data)));"
                  "});")]
  (kind/hiccup
   [:div
    [:button {:id btn-id
              :style "margin-bottom:6px; padding:4px 12px;"}
     "Save PNG"]
    (into [:svg (assoc attrs :id plot-id)] body)
    [:script script]]))
Click 'Save PNG' to download the renderingsepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Deferred: pan and zoom

A naive viewBox-based pan/zoom wrapper is easy to write – the SVG’s viewBox attribute can be mutated on mouse events to rescale the whole picture. The catch is that this rescales everything: the title, axis labels, and tick numbers all move and resize along with the data marks, which is rarely what a chart reader wants.

A useful pan/zoom keeps the chrome stable and rescales only the data area, recomputing tick positions as the visible range changes. That requires hooks Plotje doesn’t expose yet (a marker on the panel <g> so a script can target it specifically, plus an axis-tick recompute path). Until those hooks land, this chapter does not ship a pan/zoom example – a misleading half-measure would give the wrong impression of what the library offers.

What’s testable

The cells above produce SVG hiccup containing all the markup the browser needs (data-tooltip, data-row-idx, nsk-tooltip / nsk-brush-sel CSS, the save-PNG script). A Playwright-based check at dev-tools/check-interactivity.py walks the rendered HTML and verifies the data attributes, CSS rules, and the save-PNG button are all present in a real Chromium instance.

What’s next

source: notebooks/plotje_book/interactivity.clj