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)))