001  (ns org.clojars.punit-naik.draw
002    (:require [org.clojars.punit-naik.canvas :refer [add-mark]]
003              [org.clojars.punit-naik.chart-actions :refer [select-on-click]]
004              [org.clojars.punit-naik.data-transformations :refer [apply-filter]]))
005  
006  (defn change-axis-color
007    [spec color]
008    (update-in
009     spec
010     [:config :axis]
011     assoc
012     :gridColor color
013     :labelColor color
014     :tickColor color
015     :titleColor color))
016  
017  (defn change-legend-color
018    [spec color]
019    (update-in spec [:config :legend] assoc
020               :labelColor color
021               :titleColor color))
022  
023  (defn generate-colour-range
024    "Generates `vega-lite` spec for colour ranges for field values"
025    [data range-mapping-fn]
026    {:domain data
027     :range (map range-mapping-fn data)})
028  
029  (defn add-axes
030    "Adds axes to the plot in the Vega-lite spec"
031    [vl-spec
032     {:keys [x-fld-name x-fld-type x-agg-op x-fld-opts
033             y-fld-name y-fld-type y-agg-op y-fld-opts]
034      :or {x-fld-opts {} y-fld-opts {}}}]
035    (let [x-axis (conj {:field x-fld-name
036                        :type x-fld-type}
037                       x-fld-opts)
038          y-axis (conj {:field y-fld-name
039                        :type y-fld-type}
040                       y-fld-opts)
041          x-axis-with-agg (if x-agg-op (assoc x-axis :aggregate x-agg-op) x-axis)
042          y-axis-with-agg (if y-agg-op (assoc y-axis :aggregate y-agg-op) y-axis)
043          axes {:x x-axis-with-agg :y y-axis-with-agg}]
044      (update vl-spec :encoding merge axes)))
045  
046  (defn get-fld-info
047    "Gets the info of the field on `fld`-Axis"
048    [spec fld]
049    (get-in spec [:encoding fld]))
050  
051  (defn add-dashes
052    "Adds dashes to the stroke of a line chart"
053    ([spec fld-name] (add-dashes spec fld-name "nominal"))
054    ([spec fld-name fld-type]
055     (update spec :encoding assoc :strokeDash {:field fld-name :type fld-type
056                                               :legend nil})))
057  
058  (defn add-opacity
059    "Adds opacity/transparency to the stroke of a line chart based on `fld-name`"
060    ([spec fld-name] (add-opacity spec fld-name "quantitative"))
061    ([spec fld-name fld-type]
062     (update spec :encoding assoc :strokeOpacity {:field fld-name :type fld-type
063                                                  :legend nil})))
064  
065  (defn add-rule-for-line
066    "Adds a rule (vertical line) when hovered over any point, works even when not exactly hovered over a point
067     Makes clicking on line charts easier"
068    [spec chart-type x-fld-name y-fld-name stack-fld-name chart-interpolate]
069    (assoc
070     spec
071     :layer
072     [(select-on-click
073       {:encoding {:color {:value "transparent", :condition {:field stack-fld-name, :selection "hover"}}},
074        :mark {:strokeOpacity 0.5, :strokeDash [4 2], :type "rule", :point {:size 60, :filled true}},
075        :selection {:hover {:nearest true, :type "single", :empty "none", :on "mouseover", :clear "mouseout"}}}
076       {:select-name :B, :select-fld stack-fld-name})
077      (-> (add-mark nil (cond-> {:type chart-type
078                                 :invalid "filter"}
079                          (= chart-type "moving-avg") (assoc :type "line")
080                          (or (= chart-type "line")
081                              (= chart-type "moving-avg")) (assoc :interpolate chart-interpolate)))
082          (select-on-click {:select-fld stack-fld-name :select-name :C})
083          (apply-filter {:field x-fld-name :valid true})
084          (apply-filter {:field y-fld-name :valid true}))]))
085  
086  (defn stack
087    "Stacks charts based on a particular field on the aggregated field of the chart
088     NOTE: Only to be used when there is an aggregated field in the chart in `encoding` and not in `transform`"
089    [vl-spec
090     {:keys [stack-fld stack-fld-type stack-fld-opts]
091      :or {stack-fld-opts {}}}]
092    (let [color {:color (merge {:field stack-fld :type stack-fld-type} stack-fld-opts)}]
093      (update vl-spec :encoding merge color)))
094  
095  (defn get-stack-fld-info
096    "Gets the info the the field using which the chart is being stacked (coloured)"
097    [spec]
098    (get-fld-info spec :color))
099  
100  (defn get-step-size
101    "Gets the step size for grouped bar charts based on data"
102    [{:keys [width] :as spec}]
103    (let [labels (->> spec :data :values
104                      (map :label))
105          total-groups (->> labels
106                            (partition-by identity)
107                            first count)
108          step (double (/ (- width (* (dec total-groups) 2))
109                          (count labels)))
110          step (if (< step 1) 1 step)
111          new-width {:step step
112                     ;; Just to keep track of the width of the line chart
113                     ;; Does not have to do anything with the actual spec
114                     :original-value width}]
115      new-width))
116  
117  (defn header-for-grouped-bar-chart
118    [x-fld-info total-records bucket]
119    (merge (:axis x-fld-info)
120           {:labelAngle 90
121            :labelPadding 0
122            :labelAlign "left"}
123           (when (> total-records 100)
124             {:labelExpr (str "[(timeFormat(datum.value, "
125                              (cond
126                                (contains? #{"1d" "1w"}
127                                           bucket) "'%d'"
128                                (contains? #{"1M" "1q"}
129                                           bucket) "'%m'"
130                                (contains? #{"1Y"}
131                                           bucket) "'%Y'")
132                              ") % " (if (= bucket "1d")
133                                       7 3) ") == 0 ? "
134                              "timeFormat (datum.value, '%d') + "
135                              "'-' +"
136                              "timeFormat (datum.value, '%m') + "
137                              "'-' +"
138                              "timeFormat(datum.value, '%Y') "
139                              ": '']")
140              :labelFontSize 9})))
141  
142  (defn line->grouped-bar
143    "Converts a spec with a multi line series (stacked) chart to a grouped bar chart
144     Calculates width of each group's bar based on the number of stacks (colours) and current width of the chart"
145    [spec bucket]
146    (let [spacing 0
147          total-records (->> spec :data :values count)
148          x-fld-info (get-fld-info spec :x)
149          stack-fld-info (get-stack-fld-info spec)
150          new-width (get-step-size spec)
151          new-x-fld-info {:x (-> stack-fld-info
152                                 (dissoc :scale)
153                                 (assoc :title "")
154                                 (assoc :axis (merge (:axis x-fld-info)
155                                                     {:labels false :ticks false})))}
156          column-info {:column (assoc x-fld-info
157                                      :spacing spacing
158                                      :header (header-for-grouped-bar-chart x-fld-info total-records bucket))}
159          new-mark (assoc (:mark spec) :type "bar")]
160      (-> spec
161          (add-mark new-mark)
162          (update-in [:encoding :y] dissoc :impute)
163          (update :encoding merge new-x-fld-info)
164          (update :encoding merge column-info)
165          (assoc :width new-width))))
166  
167  (defn grouped-bar->line
168    "Converts a spec with a grouped bar chart to a multi line series (stacked) chart"
169    [spec]
170    (let [width {:width (get-in spec [:width :original-value])}
171          new-x-fld-info {:x (-> spec
172                                 (get-in [:encoding :column])
173                                 (dissoc :spacing)
174                                 (update :axis dissoc :labelExpr)
175                                 (update :header dissoc :labelExpr))}
176          new-mark (assoc (:mark spec)
177                          :type "line"
178                          :interpolate "monotone")]
179      (-> spec
180          (add-mark new-mark)
181          (update :encoding dissoc :column)
182          (update :encoding merge new-x-fld-info)
183          (merge width))))
184  
185  (defn custom-stack-colours
186    "Adds custom colour to every label/stack field
187     `colours-map` is a map with field values as keys and colours (hex strings) as values"
188    [vl-spec colours-map]
189    (assoc-in
190     vl-spec
191     [:encoding :color :scale]
192     (reduce
193      (fn [scale [field-value colour]]
194        (-> scale
195            (update :domain conj field-value)
196            (update :range conj colour)))
197      {:domain [] :range []}
198      colours-map)))