Explorar o código

Add toggleable sum/average/both modes to the totals row (todo 7.2)

Clicking the totals row's header cell now cycles it between sum,
average, and both (stacked, smaller font), with the header glyph
reflecting the active mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc3ocGYfboSgear11YZcDv
Brandon Wong hai 3 días
pai
achega
7d9e58ba34

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 1
CONTEXT.md


+ 16 - 0
frontend/resources/public/site.css

@@ -135,6 +135,22 @@ td, th {
     color: #b00020;
 }
 
+/* Totals row header cell: click to cycle sum / average / both */
+#main-table .totals-row th.totals-toggle {
+    cursor: pointer;
+}
+#main-table .totals-row th.totals-toggle:hover {
+    background: #dde5f0;
+}
+
+/* "Both" mode: sum and average stacked in smaller font */
+.totals-row--both td {
+    font-size: 0.75em;
+}
+.totals-row--both .totals-value {
+    line-height: 1.3;
+}
+
 th {
     text-align: center;
     min-width: 40px;

+ 1 - 0
frontend/src/cljs/microtables_frontend/db.cljs

@@ -8,6 +8,7 @@
 
 (def default-db
   {:controls nil
+   :totals-mode :sum
    ;TODO: add "start" and "end" corners as selection
    :position {:cursor nil
               :selection nil #_{:start {:col "A" :row 5}

+ 8 - 0
frontend/src/cljs/microtables_frontend/events.cljs

@@ -87,3 +87,11 @@
  (fn [db [_ new-state]]
    (println "::set-controls-state" new-state)
    (assoc-in db [:controls] new-state)))
+
+(def totals-mode-cycle
+  {:sum :average, :average :both, :both :sum})
+
+(re-frame/reg-event-db
+ ::cycle-totals-mode
+ (fn [db _]
+   (update db :totals-mode totals-mode-cycle)))

+ 21 - 6
frontend/src/cljs/microtables_frontend/subs.cljs

@@ -56,9 +56,9 @@
         (if (js/isNaN n) nil n))
       :else nil)))
 
-(defn column-total
-  "Sums the numeric cells in a column (a {row-num datum} map).
-  Returns a number, :error (if any cell is in an error state), or nil (no numeric cells)."
+(defn column-aggregate
+  "Aggregates the numeric cells in a column (a {row-num datum} map).
+  Returns {:sum n :count n}, :error (if any cell is in an error state), or nil (no numeric cells)."
   [column]
   (reduce (fn [acc [_ datum]]
             (if (= acc :error)
@@ -66,7 +66,9 @@
               (let [v (numeric-cell-value datum)]
                 (cond
                   (= v :error) :error
-                  (number? v) (+ (or acc 0) v)
+                  (number? v) (-> (or acc {:sum 0 :count 0})
+                                  (update :sum + v)
+                                  (update :count inc))
                   :else acc))))
           nil
           column))
@@ -76,13 +78,26 @@
   [n]
   (js/parseFloat (.toFixed n 10)))
 
+(defn- finalize-aggregate
+  "Turns a column-aggregate result into {:sum n :avg n} for display, passing :error/nil through unchanged."
+  [agg]
+  (cond
+    (= agg :error) :error
+    (map? agg) {:sum (round-for-display (:sum agg))
+                :avg (round-for-display (/ (:sum agg) (:count agg)))}
+    :else nil))
+
 (re-frame/reg-sub
  ::totals-row
  (fn [db]
    (reduce-kv (fn [acc col rows]
-                (let [total (column-total rows)]
-                  (assoc acc col (if (number? total) (round-for-display total) total))))
+                (assoc acc col (finalize-aggregate (column-aggregate rows))))
               {}
               (:table-data db))))
 
+(re-frame/reg-sub
+ ::totals-mode
+ (fn [db]
+   (:totals-mode db :sum)))
+
 

+ 2 - 1
frontend/src/cljs/microtables_frontend/views.cljs

@@ -8,9 +8,10 @@
 (defn main-panel []
   (let [data (re-frame/subscribe [::subs/table-data])
         totals (re-frame/subscribe [::subs/totals-row])
+        totals-mode (re-frame/subscribe [::subs/totals-mode])
         controls-state (re-frame/subscribe [::subs/controls-state])]
     [:div#main-layout
-     [:div.sheet-container [sheet @data @totals]]
+     [:div.sheet-container [sheet @data @totals @totals-mode]]
      [control-panel @controls-state]]))
 
 

+ 23 - 12
frontend/src/cljs/microtables_frontend/views/sheet.cljs

@@ -48,22 +48,33 @@
                      ^{:key "corner"} [:th]
                      (map (fn [c] ^{:key (str "col-head-" c)} [:th c]) cols))])
 
-(defn totals-cell [c totals]
-  (let [total (get totals c)]
+(defn totals-cell [c totals mode]
+  (let [agg (get totals c)]
     ^{:key (str "totals-" c)}
-    [:td {:class (when (= total :error) "totals-error")}
+    [:td {:class (when (= agg :error) "totals-error")}
      (cond
-       (= total :error) "Error"
-       (number? total) (str total)
-       :else "")]))
+       (= agg :error) "Error"
+       (nil? agg) ""
+       (= mode :both) [:<>
+                       [:div.totals-value (str (:sum agg))]
+                       [:div.totals-value (str (:avg agg))]]
+       (= mode :average) (str (:avg agg))
+       :else (str (:sum agg)))]))
 
-(defn totals-row [cols totals]
-  ^{:key "totals"} [:tr.totals-row
+(def totals-mode-symbol
+  {:sum "Σ" :average "x̄" :both "Σ / x̄"})
+
+(defn totals-row [cols totals mode]
+  ^{:key "totals"} [:tr.totals-row {:class (when (= mode :both) "totals-row--both")}
                     (cons
-                     ^{:key "totals-corner"} [:th "Σ"]
-                     (map #(totals-cell % totals) cols))])
+                     ^{:key "totals-corner"}
+                     [:th.totals-toggle
+                      {:on-click #(re-frame/dispatch [::events/cycle-totals-mode])
+                       :title "Click to cycle sum / average / both"}
+                      (totals-mode-symbol mode)]
+                     (map #(totals-cell % totals mode) cols))])
 
-(defn sheet [data totals]
+(defn sheet [data totals mode]
   [:table
    {:id "main-table"}
    [:tbody
@@ -75,4 +86,4 @@
       (concat
        [(header-row cols)]
        (map #(row % cols maxrow data) (range 1 (inc maxrow)))
-       [(totals-row cols totals)]))]])
+       [(totals-row cols totals mode)]))]])

+ 17 - 1
todo.md

@@ -134,7 +134,15 @@ When a cell or range is selected, show a compact set of floating action buttons
 
   A pinned row below the table showing the sum (and possibly average) of each column, updating reactively. Show only for columns that contain numbers. Make sure that it is not visually confused for another cell/row.
 
-- [ ] **7.2 Always-visible row totals column**
+- [x] **7.2 Toggleable column totals row**
+
+  By default, the fixed totals row shows the sum of any non-empty column. Clicking on that row's header (the capital sigma symbol on the left) (which should of course have the right "clickable" mouse cursor when hovering) changes it to the average of any non-empty column. Clicking a second time changes it to both the sum and the average (with a smaller font). Clicking a third time changes it back to the sum, and subsequent clicks continue cycling between these modes.
+
+- [ ] **7.3 Column totals row mode**
+
+  Idea: the totals row shows the sum by default. Clicking on the header (on the left) swaps it for the average. Either that, or it always shows both, one on top of the other, in smaller font.
+
+- [ ] **7.4 Always-visible row totals column**
 
   Equivalent pinned column to the right of the table for row aggregates. Perhaps, due to concerns about mobile screen real estate, the row totals could be toggleable (not "always-visible" after all) - maybe behind the row header.
 
@@ -277,6 +285,14 @@ The goal is intentional minimalism — not bare HTML. The current stylesheet has
 - [ ] **11.1 Local storage persistence**
 
   Save the table to the browser's `localStorage` on change and restore on page load. Provides durable-but-local storage with no backend required.
+  - Save session by default by date and time, but renameable
+  - A control button in the toolbar for creating a brand new session
+    - If the current session's table was completly empty (and no changes), don't bother saving it before opening a fresh session
+  - Another control button in the toolbar for opening a saved session
+    - This opens a modal with a most-recently-opened-ordered list of sessions
+    - There are options to rename or to delete (with a confirmation modal)
+    - For now, this modal has two tabs: one is the list of sessions, and the second is a help tab with information about how the save feature is to be used, and caveats about using localStorage (ie: to the effect of, sessions don't transfer between devices, disappear if browser data is wiped or was in incognito mode, etc)
+  - When the page is loaded, it opens the most recent session by default
 
 - [ ] **11.2 Server-side persistence**