(ns microtables-frontend.core (:require [reagent.core :as r] ["mathjs" :as mathjs])) ; to generate random values ;for(let i = 0, s = new Set(); i < 10; i++){ let r = Math.floor(Math.random() * 15)+1, c = a[Math.floor(Math.random() * a.length)], k = `${c}${r}`; if(s.has(k)){ i--; continue; } s.add(k); v.push(`{:row ${r} :col "${c}" :value "${Math.floor(Math.random() * 10000)}"}`); } (def sample-data [{:row 1 :col "A" :value "59" :view :display} {:row 5 :col "C" :value "269" :view :display} {:row 4 :col "B" :value "7893" :view :display} {:row 2 :col "F" :value "8650" :view :display} {:row 6 :col "D" :value "4065" :view :display} {:row 7 :col "F" :value "5316" :view :display} {:row 12 :col "A" :value "2405" :view :display} {:row 5 :col "B" :value "7863" :view :display} {:row 9 :col "E" :value "3144" :view :display} {:row 10 :col "D" :value "8272" :view :display} {:row 11 :col "D" :value "2495" :view :display} {:row 15 :col "E" :value "8968" :view :display} {:row 7 :col "B" :value "=C5 + D6" :view :display} {:row 8 :col "B" :value "=B7 * 2" :view :display} {:row 7 :col "C" :value "=D1" :view :display}]) (defonce data-atom (r/atom sample-data)) (defn highest [dir data] (apply max (map dir data))) ; COLUMN NAMES (defn upgrade-letter-code [s] (let [l (last s)] (cond (empty? s) [65] (= l 90) (conj (upgrade-letter-code (subvec s 0 (dec (count s)))) 65) :else (conj (subvec s 0 (dec (count s))) (inc l))))) (defn next-letter [lc] (apply str (map char (upgrade-letter-code (mapv #(.charCodeAt % 0) lc))))) (def col-letters (iterate next-letter "A")) ; CHANGE VALUE FUNCTIONS (defn update-value [c r datum value] (if (nil? datum) (swap! data-atom conj {:row r :col c :value value}) (swap! data-atom (fn [d] (map #(if (and (= r (:row %)) (= c (:col %))) (assoc % :value value) %) d))))) (defn toggle-display [c r view-mode] (println (str " toggling " c r " to " view-mode)) (swap! data-atom (fn [d] (map #(if (and (= r (:row %)) (= c (:col %))) (assoc % :view view-mode) %) d)))) ; CALCULATION / FORMULA EVALUATION FUNCTIONS (def parse-variables (memoize (fn [expression] (js->clj (.getVariables mathjs expression))))) (def evaluate-expression (memoize (fn [expression variables] (.evaluate mathjs expression (clj->js variables))))) (def str->rc (memoize (fn [s] (let [c (re-find #"^[A-Z]+" s) r (.parseInt js/window (re-find #"[0-9]+$" s))] {:row r :col c})))) (defn find-cell [data c r] (some #(if (and (= (:col %) c) (= (:row %) r)) %) data)) (defn find-val [data c r] (let [l (find-cell data c r) v (get l :display (get l :value))] (println "found?" c r v l (get l :value) (get l :display)) (cond (nil? v) 0 (and (string? v) (= (first v) "=")) :not-yet :else v))) (defn copy-display-values [data display-values] (let [removed (map #(-> % (dissoc :vars) (dissoc :refs) (dissoc :found) (dissoc :inputs)) display-values)] (into data removed))) ;TODO: figure out how to re-evaluate only when the cell modified affects other cells (defn re-evaluate [data] (let [remove-old-displays (map #(dissoc % :display) data) {has-formula true original-values false} (group-by #(= (first (:value %)) "=") remove-old-displays) ;TODO: detect circular references variables (map #(assoc % :vars (parse-variables (subs (:value %) 1))) has-formula) first-mapped-cell-keys (map (fn [datum] (assoc datum :refs (map str->rc (:vars datum)))) variables)] (loop [values original-values mapped-cell-keys first-mapped-cell-keys] (let [search-values (map (fn [datum] (assoc datum :found (map #(find-val (concat values mapped-cell-keys) (:col %) (:row %)) (:refs datum)))) mapped-cell-keys) {not-ready true ready nil} (group-by (fn [datum] (some #(= :not-yet %) (:found datum))) search-values) prepped-for-eval (map (fn [datum] (assoc datum :inputs (apply hash-map (interleave (:vars datum) (:found datum))))) ready) evaluated (map (fn [datum] (assoc datum :display (evaluate-expression (subs (:value datum) 1) (:inputs datum)))) prepped-for-eval) updated-values (copy-display-values values evaluated)] (if (nil? not-ready) updated-values (recur updated-values not-ready)))))) (defn on-enter-cell [c r e] (println (str "entering cell " c r)) (toggle-display c r :value)) (defn on-leave-cell [c r e] (println (str "leaving cell " c r)) (toggle-display c r :display) (swap! data-atom re-evaluate)) ;; ------------------------- ;; Views (defn cell [c r data] (let [datum (some #(if (and (= c (:col %)) (= r (:row %))) %) data)] ^{:key (str c r)} [:td [:input {:value (if (= (get datum :view nil) :value) (get datum :value "") (get datum :display (get datum :value ""))) :on-change #(update-value c r datum (.. % -target -value)) :on-blur (partial on-leave-cell c r) :on-focus (partial on-enter-cell c r)}]])) (defn row [r cols data] ^{:key (str "row-" r)} [:tr (cons ^{:key (str "row-head-" r)} [:th (str r)] (map #(cell % r data) cols))]) (defn header-row [cols] ^{:key "header"} [:tr (cons ^{:key "corner"} [:th] (map (fn [c] ^{:key (str "col-head-" c)} [:th c]) cols))]) (defn sheet [data] [:table [:tbody (let [maxrow (highest :row data) cols (take-while (partial not= (next-letter (highest :col data))) col-letters)] (cons (header-row cols) (map #(row % cols data) (range 1 (inc maxrow)))))]]) (defn app [] [:div [:h3 "Microtables"] [sheet @data-atom]]) ;; ------------------------- ;; Initialize app (swap! data-atom re-evaluate) ; evalutate any formulas the first time (defn mount-root [] (r/render [app] (.getElementById js/document "app"))) (defn init! [] (mount-root))