7  Further exploring ggplotly’s client side

(ns ggplotly-cont
  (:require [tablecloth.api :as tc]
            [scicloj.metamorph.ml.toydata.ggplot :as toydata.ggplot]
            [tech.v3.datatype.functional :as fun]
            [scicloj.kindly.v4.kind :as kind]
            [clojure.string :as str]
            [clojure.math :as math]
            [clojure2d.color :as color]
            [tablecloth.column.api :as tcc]))

The following template was generated from the JSON file in the HTML generated by ggplotly(ggplot(mpg, aes(x=hwy, y=displ, color=factor(cyl))) + geom_point() + geom_smooth(method="lm")) and then gradually generalizing it as a set of Clojure functions. This is a work-in-progress draft. Some parts are still hard-coded (e.g., colours).

(defn layer [{:keys [type x y color text name legendgroup showlegend]}]
  (merge {:hoveron "points",
          :legendgroup legendgroup,
          :showlegend showlegend
          :frame nil,
          :hoverinfo "text",
          :name name,
          :mode (case type
                  :point "markers"
                  :line "lines")
          :type "scatter",

          :xaxis "x",
          :yaxis "y",
          :x (vec x)
          :y (vec y)
          :text (vec text)}
         (case type
           :point {:marker {:autocolorscale false,
                            :color color,
                            :opacity 1,
                            :size 5.66929133858268,
                            :symbol "circle",
                            :line {:width 1.88976377952756, :color color}}}
           :line {:line {:width 3.77952755905512,
                         :color color,
                         :dash "solid"}})))
(def colors
  (->> :category10
       color/palette
       (mapv (fn [[r g b a]]
               (format "rgba(%d,%d,%d,%f)"
                       (int r)
                       (int g)
                       (int b)
                       (/ a 255.0))))))
(def config
  {:doubleClick "reset",
   :modeBarButtonsToAdd ["hoverclosest" "hovercompare"],
   :showSendToCloud false},)
(def highlight
  {:on "plotly_click",
   :persistent false,
   :dynamic false,
   :selectize false,
   :opacityDim 0.2,
   :selected {:opacity 1},
   :debounce 0},)
(defn texts [ds column-names]
  (-> ds
      (tc/select-columns column-names)
      tc/rows
      (->> (map (fn [row]
                  (str/join "<br />"
                            (map (partial format "%s: %s")
                                 column-names row)))))))
(defn ->tickvals [l r]
  (let [jump (-> (- r l)
                 (/ 6)
                 math/floor
                 int
                 (max 1))]
    (-> l
        math/ceil
        (range r jump))))
(defn axis [{:keys [minval maxval anchor title]}]
  (let [tickvals (->tickvals minval maxval)
        ticktext (mapv str tickvals)
        range-len (- maxval minval)
        range-expansion (* 0.1 range-len)
        expanded-range [(- minval range-expansion)
                        (+ maxval range-expansion)]]
    {:linewidth 0,
     :ticklen 3.65296803652968,
     :tickcolor "rgba(51,51,51,1)",
     :tickmode "array",
     :gridcolor "rgba(255,255,255,1)",
     :automargin true,
     :type "linear",
     :tickvals tickvals,
     :zeroline false,
     :title
     {:text title,
      :font {:color "rgba(0,0,0,1)", :family "", :size 14.6118721461187}},
     :tickfont {:color "rgba(77,77,77,1)", :family "", :size 11.689497716895},
     :autorange false,
     :showticklabels true,
     :showline false,
     :showgrid true,
     :ticktext ticktext,
     :ticks "outside",
     :gridwidth 0.66417600664176,
     :anchor "y",
     :domain [0 1],
     :hoverformat ".2f",
     :tickangle 0,
     :tickwidth 0.66417600664176,
     :categoryarray ticktext,
     :categoryorder "array",
     :range expanded-range},))
(defn layout [{:keys [xaxis yaxis]}]
  {:plot_bgcolor "rgba(235,235,235,1)",
   :paper_bgcolor "rgba(255,255,255,1)",
   :legend {:bgcolor "rgba(255,255,255,1)",
            :bordercolor "transparent",
            :borderwidth 1.88976377952756,
            :font {:color "rgba(0,0,0,1)", :family "", :size 11.689497716895},
            :title {:text "factor(cyl)",
                    :font {:color "rgba(0,0,0,1)", :family "", :size 14.6118721461187}}},
   :font {:color "rgba(0,0,0,1)", :family "", :size 14.6118721461187},
   :showlegend true,
   :barmode "relative",
   :hovermode "closest",
   :margin
   {:t 25.7412480974125,
    :r 7.30593607305936,
    :b 39.6955859969559,
    :l 31.4155251141553},
   :shapes [{:yref "paper",
             :fillcolor nil,
             :xref "paper",
             :y1 1,
             :type "rect",
             :line {:color nil, :width 0, :linetype []},
             :y0 0,
             :x1 1,
             :x0 0}]
   :xaxis xaxis
   :yaxis yaxis})
(delay
  (let [data toydata.ggplot/mpg
        point-layers (-> data
                         (tc/add-column "factor(cyl)" #(:cyl %))
                         (tc/group-by :cyl {:result-type :as-map})
                         (->> (sort-by key)
                              (map-indexed
                               (fn [i [group-name group-data]]
                                 (let [base {:x (:hwy group-data),
                                             :y (:displ group-data)
                                             :color (colors i)
                                             :name group-name
                                             :legendgroup group-name}
                                       predictions (map
                                                    (fun/linear-regressor (:hwy group-data)
                                                                          (:displ group-data))
                                                    (:hwy group-data))]
                                   [(-> base
                                        (assoc :type :point
                                               :showlegend true
                                               :y (:displ group-data)
                                               :text (-> group-data
                                                         (texts [:hwy :displ "factor(cyl)"])))
                                        layer)
                                    (-> base
                                        (assoc :type :line
                                               :showlegend false
                                               :y predictions
                                               :text (-> group-data
                                                         ;; (tc/add-column :displ predictions)
                                                         (texts [:hwy :displ "factor(cyl)"])))
                                        layer)])))
                              (apply concat)))
        xmin (-> data :hwy tcc/reduce-min)
        xmax (-> data :hwy tcc/reduce-max)
        ymin (-> data :displ tcc/reduce-min)
        ymax (-> data :displ tcc/reduce-max)
        xaxis (axis {:minval xmin
                     :maxval xmax
                     :anchor "x"
                     :title :hwy})
        yaxis (axis {:minval ymin
                     :maxval ymax
                     :anchor "x"
                     :title :displ})]
    (kind/htmlwidgets-ggplotly
     {:x
      {:config config
       :highlight highlight
       :base_url "https://plot.ly",
       :layout (layout {:xaxis xaxis
                        :yaxis yaxis})
       :data point-layers},
      :evals [],
      :jsHooks []})))
source: projects/datavis/ggplot/notebooks/ggplotly_cont.clj