|
5 | 5 | :cljs [nextjournal.clerk :as-alias clerk])
|
6 | 6 | [nextjournal.clerk.viewer :as viewer]
|
7 | 7 | [clojure.set :as set]
|
8 |
| - [clojure.string :as str] |
9 |
| - [clojure.walk :as walk])) |
10 |
| - |
11 |
| -(def table-col-bars |
12 |
| - '(fn table-col-bars [{:keys [col-type category-count distribution width height idx]} {:keys [table-state idx]}] |
13 |
| - (reagent.core/with-let [!selected-bar (reagent.core/atom nil)] |
14 |
| - (let [width 140 |
15 |
| - height 30 |
16 |
| - last-index (dec (count distribution)) |
17 |
| - filtered-bars (-> (get (:filter @table-state) idx) |
18 |
| - not-empty)] |
19 |
| - [:div |
20 |
| - #_[:pre (pr-str filtered-bars)] |
21 |
| - [:div.text-slate-500.dark:text-slate-400.font-normal |
22 |
| - {:class "text-[12px] h-[24px] leading-[24px]"} |
23 |
| - (if-let [{:keys [count percentage]} @!selected-bar] |
24 |
| - (str count " rows (" (.toFixed (* 100 percentage) 2) "%)") |
25 |
| - col-type)] |
26 |
| - (into |
27 |
| - [:div.flex.relative |
28 |
| - {:style {:width width :height height} |
29 |
| - :class "rounded-sm overflow-hidden items-center "}] |
30 |
| - (map-indexed |
31 |
| - (fn [i {:as bar :keys [label count percentage range]}] |
32 |
| - (let [bar-width (* width percentage) |
33 |
| - filtered? (contains? filtered-bars label) |
34 |
| - selected? (or (= @!selected-bar bar) |
35 |
| - filtered?)] |
36 |
| - [:div.relative.overflow-hidden |
37 |
| - {:on-click #(do |
38 |
| - (if filtered? |
39 |
| - (swap! table-state update :filter update idx disj label) |
40 |
| - (swap! table-state update :filter update idx (fnil conj #{}) label))) |
41 |
| - :on-mouse-enter #(reset! !selected-bar bar) |
42 |
| - :on-mouse-leave #(reset! !selected-bar nil) |
43 |
| - :class (case label |
44 |
| - :unique "bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 " |
45 |
| - :empty "bg-orange-200 hover:bg-orange-300 dark:bg-pink-900 dark:bg-opacity-[0.7] dark:hover:bg-pink-800 " |
46 |
| - (cond-> ["bg-indigo-200 hover:bg-indigo-300 dark:bg-sky-700 dark:hover:bg-sky-500"] |
47 |
| - selected? (conj "bg-indigo-400"))) |
48 |
| - :style {:width bar-width |
49 |
| - :height height}} |
50 |
| - (when (and (contains? #{:unique :empty} label) (< 30 bar-width)) |
51 |
| - [:div.text-slate-500.dark:text-slate-300.font-normal.absolute.left-0.top-0.right-0.bottom-0.flex.items-center.justify-center.whitespace-nowrap |
52 |
| - {:class "text-[12px]"} |
53 |
| - (str (.toFixed (* 100 percentage) 2) "%" |
54 |
| - (when (and (= label :unique) (< 80 bar-width)) |
55 |
| - " unique") |
56 |
| - (when (and (= label :empty) (< 110 bar-width)) |
57 |
| - " empty/nil"))]) |
58 |
| - (when-not (= i last-index) |
59 |
| - [:div.absolute.top-0.right-0.bottom-0 |
60 |
| - {:class "bg-white bg-opacity-[0.7] dark:bg-black w-[1px]"}])])) |
61 |
| - distribution)) |
62 |
| - [:div.text-slate-500.dark:text-slate-400.font-normal.truncate |
63 |
| - {:class "text-[12px] h-[24px] mt-[1px] leading-[24px] " |
64 |
| - :style {:width width}} |
65 |
| - (if-let [{:keys [count label]} @!selected-bar] |
66 |
| - (case label |
67 |
| - :unique (str count " unique values") |
68 |
| - :empty (str count " empty/nil values") |
69 |
| - (str label)) |
70 |
| - (str "(" category-count " categories)"))]])))) |
71 |
| - |
72 |
| -(def table-col-histogram |
73 |
| - '(fn table-col-histogram [{:keys [col-type distribution width height] :as col} {:keys [table-state idx]}] |
74 |
| - (reagent.core/with-let [!selected-bar (reagent.core/atom nil) |
75 |
| - fmt (fn [x] |
76 |
| - (cond (and (>= x 1000) (< x 1000000)) |
77 |
| - (str (.toFixed (/ x 1000) 0) "K") |
78 |
| - (>= x 1000000) |
79 |
| - (str (.toFixed (/ x 1000000) 0) "M") |
80 |
| - :else (str (.toFixed x 0))))] |
81 |
| - (let [filtered-bars (-> (get (:filter @table-state) idx) |
82 |
| - not-empty) |
83 |
| - max (:count (apply max-key :count distribution)) |
84 |
| - last-index (dec (count distribution)) |
85 |
| - from (-> distribution first :range first) |
86 |
| - to (-> distribution last :range last)] |
87 |
| - [:div |
88 |
| - #_[:pre (pr-str [idx (:filter @table-state)])] |
89 |
| - [:div.text-slate-500.dark:text-slate-400.font-normal |
90 |
| - {:class "text-[12px] h-[24px] leading-[24px]"} |
91 |
| - (if-let [{:keys [count percentage]} @!selected-bar] |
92 |
| - (str count " rows (" (.toFixed (* percentage 100) 2) "%)") |
93 |
| - col-type)] |
94 |
| - (into |
95 |
| - [:div.flex.relative |
96 |
| - {:style {:width width :height height}}] |
97 |
| - (map-indexed |
98 |
| - (fn [i {:as bar row-count :count :keys [range]}] |
99 |
| - (let [bar-width (/ width (count distribution)) |
100 |
| - filtered? (contains? filtered-bars bar) |
101 |
| - selected? (or (= @!selected-bar bar) |
102 |
| - filtered?) |
103 |
| - last? (= i last-index)] |
104 |
| - [:div.relative.group |
105 |
| - {:on-mouse-enter #(reset! !selected-bar bar) |
106 |
| - :on-mouse-leave #(reset! !selected-bar nil) |
107 |
| - :style {:width bar-width |
108 |
| - :height (+ height 24)}} |
109 |
| - [:div.w-full.flex.items-end |
110 |
| - {:style {:height height}} |
111 |
| - [:div.w-full.relative |
112 |
| - {:on-click #(if filtered? |
113 |
| - (swap! table-state update :filter update idx disj bar) |
114 |
| - (swap! table-state update :filter update idx (fnil conj #{}) bar)) |
115 |
| - :style {:height (* (/ row-count max) height)} |
116 |
| - :class (let [css ["group-hover:bg-red-300 dark:bg-sky-700 dark:group-hover:bg-sky-500 "]] |
117 |
| - (if selected? |
118 |
| - (conj css "bg-red-400") |
119 |
| - (conj css "bg-red-200")))} |
120 |
| - (when-not last? |
121 |
| - [:div.absolute.top-0.right-0.bottom-0 |
122 |
| - {:class "bg-white dark:bg-black w-[1px]"}])]] |
123 |
| - [:div.relative |
124 |
| - {:class "mt-[1px] h-[1px] bg-slate-300 dark:bg-slate-700"} |
125 |
| - (when selected? |
126 |
| - [:div.absolute.left-0.top-0.bg-black.dark:bg-white |
127 |
| - {:class (str "h-[2px] " (if last? "right-0" "right-[1px]"))}])] |
128 |
| - (when selected? |
129 |
| - [:<> |
130 |
| - [:div.absolute.left-0.text-left.text-slate-500.dark:text-slate-400.font-normal.pointer-events-none |
131 |
| - {:class "text-[12px] h-[24px] leading-[24px] -translate-x-full" |
132 |
| - :style {:top height}} |
133 |
| - (fmt (first range))] |
134 |
| - [:div.absolute.right-0.text-right.text-slate-500.dark:text-slate-400.font-normal.pointer-events-none |
135 |
| - {:class "text-[12px] h-[24px] leading-[24px] translate-x-full" |
136 |
| - :style {:top height}} |
137 |
| - (fmt (last range))]])])) |
138 |
| - distribution)) |
139 |
| - [:div.text-slate-500.dark:text-slate-400.font-normal.truncate |
140 |
| - {:class "text-[12px] h-[24px] leading-[24px] " |
141 |
| - :style {:width width}} |
142 |
| - (when-not @!selected-bar |
143 |
| - [:div.relative.pointer-events-none |
144 |
| - [:div.absolute.left-0.top-0 (fmt from)] |
145 |
| - [:div.absolute.right-0.top-0 (fmt to)]])]])))) |
146 |
| - |
147 |
| -(def table-col-summary |
148 |
| - (walk/postwalk-replace {'table-col-histogram table-col-histogram |
149 |
| - 'table-col-bars table-col-bars} |
150 |
| - '(defn table-col-summary |
151 |
| - [{:as summary :keys [continuous?]} {:keys [table-state idx] :as opts}] |
152 |
| - (let [summary (assoc summary :width 140 :height 30) |
153 |
| - filtered? (get (:filter @table-state) idx)] |
154 |
| - [:div.flex |
155 |
| - [:div |
156 |
| - {:class (cond-> ["text-indigo-200"] |
157 |
| - filtered? |
158 |
| - (conj "text-black" "cursor-pointer")) |
159 |
| - :on-click #(when filtered? |
160 |
| - (swap! table-state update :filter dissoc idx))} |
161 |
| - "x" |
162 |
| - ] |
163 |
| - (if continuous? |
164 |
| - [table-col-histogram summary opts] |
165 |
| - [table-col-bars summary opts])])))) |
166 |
| - |
167 |
| -(def table-head-viewer-fn |
168 |
| - (walk/postwalk-replace |
169 |
| - {'table-col-summary table-col-summary} |
170 |
| - '(fn table-head-viewer [header-row {:as opts :keys [path table-state]}] |
171 |
| - (let [cells* (nextjournal.clerk.viewer/desc->values header-row) |
172 |
| - cells (mapcat #(if (vector? %) |
173 |
| - (let [fst (first %) |
174 |
| - vs (second %)] |
175 |
| - (map (fn [v] |
176 |
| - {:cell [fst v] |
177 |
| - :sub true}) |
178 |
| - vs)) |
179 |
| - [{:cell % |
180 |
| - :sub false}]) |
181 |
| - cells*) |
182 |
| - cells (map-indexed (fn [i e] |
183 |
| - (assoc e :idx i)) |
184 |
| - cells) |
185 |
| - cell->idx (zipmap (map :cell cells) (map :idx cells)) |
186 |
| - sub-headers (seq (filter :sub cells)) |
187 |
| - header-cells (map (fn [cell] |
188 |
| - {:idx (get cell->idx cell) |
189 |
| - :cell cell}) cells*)] |
190 |
| - [:thead |
191 |
| - (into [:tr.print:border-b-2.print:border-black] |
192 |
| - (keep (fn [cell] |
193 |
| - (let [header-cell (:cell cell) |
194 |
| - index (:idx cell) |
195 |
| - k (if (vector? header-cell) |
196 |
| - (first header-cell) |
197 |
| - header-cell) |
198 |
| - title (when (or (string? k) (keyword? k) (symbol? k)) k) |
199 |
| - {:keys [translated-keys column-layout number-col? filters update-filters! !expanded-at] :or {translated-keys {}}} opts] |
200 |
| - [:th.text-slate-600.text-xs.px-4.py-1.bg-slate-100.first:rounded-md-tl.last:rounded-md-r.border-l.border-slate-300.text-center.whitespace-nowrap.border-b |
201 |
| - (cond-> {:class (str |
202 |
| - "print:text-[10px] print:bg-transparent print:px-[5px] print:py-[2px] " |
203 |
| - (when sub-headers "first:border-l-0 ") |
204 |
| - (if (and (ifn? number-col?) (number-col? index)) "text-right " "text-left "))} |
205 |
| - (and column-layout (column-layout k)) (assoc :style (column-layout k)) |
206 |
| - (vector? header-cell) (assoc :col-span (count (first (rest header-cell)))) |
207 |
| - (and sub-headers (not (vector? header-cell))) (assoc :row-span 2) |
208 |
| - title (assoc :title title)) |
209 |
| - [:div (get translated-keys k k)] |
210 |
| - (when-not (vector? header-cell) |
211 |
| - (when-let [summary (:summary opts)] |
212 |
| - [table-col-summary (get-in summary [k]) |
213 |
| - {:table-state table-state |
214 |
| - :idx index}]))]))) |
215 |
| - header-cells) |
216 |
| - (when-not (empty? sub-headers) |
217 |
| - (into [:tr.print:border-b-2.print:border-black] |
218 |
| - (map |
219 |
| - (fn [{:keys [cell idx]}] |
220 |
| - [:th.text-slate-600.text-xs.px-4.py-1.bg-slate-100.first:rounded-md-tl.last:rounded-md-r.border-l.border-slate-300.text-center.whitespace-nowrap.border-b |
221 |
| - (let [sub-header-key (second cell)] |
222 |
| - [:<> (get (:translated-keys opts {}) sub-header-key sub-header-key) |
223 |
| - (when-let [summary (:summary opts)] |
224 |
| - [table-col-summary (get-in summary cell) |
225 |
| - {:table-state table-state |
226 |
| - :idx idx}])])]) |
227 |
| - sub-headers)))])))) |
| 8 | + [clojure.string :as str])) |
| 9 | + |
| 10 | +(clerk/require-cljs 'nextjournal.clerk-table-stats-sci) |
| 11 | + |
| 12 | +(comment |
| 13 | + (nextjournal.clerk.cljs-libs/clear-cljs!) |
| 14 | + @#'nextjournal.clerk.cljs-libs/cljs-graph |
| 15 | + (clojure.java.io/resource "nextjournal/clerk_table_stats_sci.cljs") |
| 16 | + ) |
228 | 17 |
|
229 | 18 | (defn deep-merge
|
230 | 19 | ([])
|
|
524 | 313 | head+body)]))})
|
525 | 314 |
|
526 | 315 | (def table-head-viewer
|
527 |
| - {:render-fn table-head-viewer-fn}) |
| 316 | + {:render-fn 'nextjournal.clerk-table-stats-sci/table-head-viewer}) |
528 | 317 |
|
529 | 318 | (def table-body-viewer
|
530 | 319 | {:render-fn '(fn [rows opts] (into [:tbody] (map-indexed (fn [idx row] (nextjournal.clerk.render/inspect-presented (update opts :path conj idx) row))) rows))})
|
531 | 320 |
|
532 | 321 | (def table-row-viewer
|
533 |
| - {:render-fn '(let [] |
534 |
| - (fn [row {:as opts :keys [path number-col? table-state]}] |
535 |
| - (into [:tr.print:border-b-gray-500.hover:bg-gray-200.print:hover:bg-transparent |
536 |
| - {:class (str "print:border-b-[1px] " |
537 |
| - (if (even? (peek path)) "bg-white" "bg-slate-50"))}] |
538 |
| - (map-indexed |
539 |
| - (fn [idx cell] |
540 |
| - [:td.px-4.py-2.text-sm.border-r.last:border-r-0 |
541 |
| - {:class (str "print:text-[10px] print:bg-transparent print:px-[5px] print:py-[2px] " |
542 |
| - (when (and (ifn? number-col?) (number-col? idx)) "text-right"))} |
543 |
| - (nextjournal.clerk.render/inspect-presented opts cell)])) |
544 |
| - row)))}) |
| 322 | + {:render-fn 'nextjournal.clerk-table-stats-sci/table-row-viewer}) |
545 | 323 |
|
546 | 324 | (defn tabular? [xs]
|
547 | 325 | (and (seqable? xs)
|
|
0 commit comments