├── README.md ├── deps.edn ├── doc └── images │ ├── reagent-tree-example-custom.png │ ├── reagent-tree-example-simple.png │ └── reagent-tree.png ├── figwheel.edn ├── pom.xml ├── resources └── public │ ├── css │ └── style.css │ └── index.html └── src └── reagent_flowgraph └── core.cljs /README.md: -------------------------------------------------------------------------------- 1 | # reagent-flowgraph 2 | 3 | A reagent component for laying out tree nodes in 2D space. 4 | If you are not using reagent but want to draw trees your own way check [clj-tree-layout](https://github.com/jpmonettas/clj-tree-layout). 5 | 6 | ## Features 7 | 8 | - Tidy tree representations as per [Tilford and Reingold](http://hci.stanford.edu/cs448b/f09/lectures/CS448B-20091021-GraphsAndTrees.pdf) 9 | aesthetics rules. 10 | - Customizable node render function with any reagent component. 11 | 12 | ## Installation 13 | 14 | [![Clojars Project](https://img.shields.io/clojars/v/reagent-flowgraph.svg)](https://clojars.org/reagent-flowgraph) 15 | 16 | ## Usage 17 | 18 | ```clojure 19 | (ns tester.core 20 | (:require [reagent.core :as r] 21 | [reagent-flowgraph.core :refer [flowgraph]])) 22 | 23 | (defonce app-state (r/atom '(+ 1 2 (- 4 2) (/ 123 3) (inc 25)))) 24 | 25 | (defn app [] 26 | [:div 27 | [flowgraph @app-state 28 | :layout-width 1500 29 | :layout-height 500 30 | :branch-fn #(when (seq? %) %) 31 | :childs-fn #(when (seq? %) %) 32 | :render-fn (fn [n] [:div {:style {:border "1px solid black" 33 | :padding "10px" 34 | :border-radius "10px"}} 35 | (str n)] )]]) 36 | 37 | (defn init [] 38 | (r/render [app] (.getElementById js/document "app"))) 39 | ``` 40 | 41 | and that will render 42 | 43 | 44 | 45 | A more colorful example. Supouse we want to draw some aspects of the clojurescript analyzer output tree. 46 | 47 | ```clojure 48 | (ns tester.core 49 | (:require [reagent.core :as r] 50 | [reagent-flowgraph.core :refer [flowgraph]] 51 | [cljs.js :as j])) 52 | 53 | (defonce app-state (r/atom {})) 54 | 55 | (defmulti render-node :op) 56 | 57 | (def styles {:border "1px solid #586e75" :padding "10px" :border-radius "10px" 58 | :background-color "#002b36" :color "#B58900"}) 59 | 60 | (defmethod render-node :fn [n] 61 | [:div {:style (merge styles {:color "#DC322F"})} 62 | [:b (str "(" (:name (:name n)) " ...)")]]) 63 | 64 | (defmethod render-node :if [{:keys [then test else]}] 65 | [:div {:style (merge styles {:color "#CB4B16"})} 66 | [:div {:style {:text-align :center}} [:b "IF"]] 67 | [:div {:style {:display :flex}} 68 | [:div.if-test [:b "test"] [:div (str "'" (:form test) "'")]] 69 | [:div.if-then [:b "then"] [:div (str "'" (:form then) "'")]] 70 | [:div.if-else [:b "else"] [:div (str "'" (:form else) "'")]]]]) 71 | 72 | (defmethod render-node :default [n] 73 | [:div {:style styles} (str ":op " (:op n))]) 74 | 75 | (defn app [] 76 | [:div {:style {:background-color "#002b36"}} 77 | [flowgraph @app-state 78 | :layout-width 500 79 | :layout-height 1500 80 | :branch-fn :children 81 | :childs-fn :children 82 | :render-fn render-node 83 | :line-styles {:stroke-width 2 84 | :stroke "#CB4B16"}]]) 85 | 86 | (defn init [] 87 | (r/render [app] (.getElementById js/document "app")) 88 | 89 | (j/analyze-str (j/empty-state) 90 | "(defn factorial [n] 91 | (if (zero? n) 92 | 1 93 | (* n (factorial (dec n)))))" 94 | #(reset! app-state (:value %)))) 95 | ``` 96 | 97 | which will render 98 | 99 | 100 | 101 | ## Options 102 | 103 | #### :layout-width 104 | 105 | An integer representing the width of the layout panel. 106 | 107 | #### :layout-height 108 | 109 | An integer representing the height of the layout panel. 110 | 111 | #### :branch-fn 112 | 113 | Is a fn that, given a node, returns true if can have 114 | children, even if it currently doesn't. 115 | 116 | #### :childs-fn 117 | 118 | Is a fn that, given a branch node, returns a seq of its 119 | children. 120 | 121 | #### :render-fn 122 | 123 | A one parameter fn that can be used as a reagent component. Will receive the full node 124 | as a parameter. 125 | 126 | #### :line-styles 127 | 128 | A map with styles for the svg lines that join nodes. 129 | 130 | ## How does it works? 131 | 132 | The trick is in rendering all nodes twice. The action goes like this : 133 | 134 | - Traverse the tree rendering every node using :render-fn. 135 | - Collect the width and height of every node. 136 | - Now we have node sizes we use a library like [clj-tree-layout](https://github.com/jpmonettas/clj-tree-layout) to calculate nodes positions. 137 | - With positions we can calculate edges. 138 | - Render everything again. 139 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {reagent {:mvn/version "0.8.0-alpha2"} 2 | clj-tree-layout {:mvn/version "0.1.0"}} 3 | :paths ["src" "resources" "test"] 4 | :aliases {:dev {:extra-deps {org.clojure/clojurescript {:mvn/version "1.10.238"} 5 | org.clojure/clojure {:mvn/version "1.9.0"} 6 | com.cemerick/piggieback {:mvn/version "0.2.2"} 7 | figwheel-sidecar {:mvn/version "0.5.15"} 8 | binaryage/devtools {:mvn/version "0.9.9"} 9 | philoskim/debux {:mvn/version "0.4.5"}}}}} 10 | -------------------------------------------------------------------------------- /doc/images/reagent-tree-example-custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpmonettas/reagent-flowgraph/9a26e2a123d45fcaf430fffc9fd141d00a791ea0/doc/images/reagent-tree-example-custom.png -------------------------------------------------------------------------------- /doc/images/reagent-tree-example-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpmonettas/reagent-flowgraph/9a26e2a123d45fcaf430fffc9fd141d00a791ea0/doc/images/reagent-tree-example-simple.png -------------------------------------------------------------------------------- /doc/images/reagent-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpmonettas/reagent-flowgraph/9a26e2a123d45fcaf430fffc9fd141d00a791ea0/doc/images/reagent-tree.png -------------------------------------------------------------------------------- /figwheel.edn: -------------------------------------------------------------------------------- 1 | {:css-dirs ["resources/public/css"] 2 | :http-server-root "public" ;; default 3 | :server-port 3449 ;; default 4 | :builds [{:id "dev", 5 | :source-paths ["src"], 6 | :figwheel {:on-jsload "reagent-flowgraph.core/init"} 7 | :compiler 8 | {:main "reagent-flowgraph.core" 9 | :asset-path "js/out", 10 | :optimizations :none 11 | :preloads [devtools.preload] 12 | :output-to "resources/public/js/reagent-flowgraph.js", 13 | :output-dir "resources/public/js/out", 14 | :source-map-timestamp true}}]} 15 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | reagent-flowgraph 5 | reagent-flowgraph 6 | 0.1.1 7 | reagent-flowgraph 8 | 9 | 10 | org.clojure 11 | clojure 12 | 1.9.0 13 | 14 | 15 | reagent 16 | reagent 17 | 0.8.0-alpha2 18 | 19 | 20 | clj-tree-layout 21 | clj-tree-layout 22 | 0.1.0 23 | 24 | 25 | 26 | src 27 | 28 | 29 | src 30 | 31 | 32 | 33 | 34 | 35 | 36 | clojars 37 | Clojars repository 38 | https://clojars.org/repo 39 | 40 | 41 | 42 | 43 | 44 | clojars 45 | https://clojars.org/repo 46 | 47 | 48 | 49 | A reagent component for laying out tree nodes in 2D space. 50 | https://github.com/jpmonettas/reagent-flowgraph 51 | 52 | https://github.com/jpmonettas/reagent-flowgraph 53 | 54 | 55 | -------------------------------------------------------------------------------- /resources/public/css/style.css: -------------------------------------------------------------------------------- 1 | h1{color:red;} 2 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/reagent_flowgraph/core.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent-flowgraph.core 2 | (:require [reagent.core :as r] 3 | [clj-tree-layout.core :refer [layout-tree] :as tl])) 4 | 5 | (defn merge-dimensions [t dimensions] 6 | (-> t 7 | (merge (get dimensions (:node-id t))) 8 | (update :childs #(mapv (fn [c] (merge-dimensions c dimensions)) %)))) 9 | 10 | (defn dimension-and-redraw [panel-node-comp internal-nodes-a childs-fn branch-fn] 11 | (let [dn (r/dom-node panel-node-comp) 12 | dom-width (.-offsetWidth dn) 13 | dom-height (.-offsetHeight dn) 14 | dom-sizes (reduce (fn [r n] 15 | (if (-> n .-dataset .-type (= "node")) 16 | (assoc r (.-id n) [(.-offsetWidth n) (.-offsetHeight n)]) 17 | r)) 18 | {} 19 | (array-seq (.-childNodes dn))) 20 | dimensions (layout-tree @internal-nodes-a 21 | {:sizes dom-sizes 22 | :childs-fn :childs 23 | :id-fn :node-id 24 | :branch-fn :childs 25 | :h-gap 5 26 | :v-gap 50}) 27 | dimensioned-nodes (merge-dimensions @internal-nodes-a dimensions)] 28 | (.log js/console "Dimensions recalculated " dimensions) 29 | (when (not= @internal-nodes-a dimensioned-nodes) 30 | (reset! internal-nodes-a dimensioned-nodes)))) 31 | 32 | (defn node [id n x y render-fn] 33 | [:div 34 | {:id id 35 | :data-type :node 36 | :style {:position :absolute 37 | :top (or y 0) :left (or x 0)}} 38 | (render-fn n)]) 39 | 40 | (defn build-internal-tree [t branch-fn childs-fn] 41 | {:node-id (str (random-uuid)) 42 | :original-node t 43 | :childs (mapv #(build-internal-tree % branch-fn childs-fn) 44 | (childs-fn t))}) 45 | 46 | (defn flowgraph [nodes & {:keys [layout-width layout-height branch-fn childs-fn render-fn line-styles] 47 | :or {line-styles {:stroke :red 48 | :stroke-width 2}} }] 49 | (let [internal-nodes-a (r/atom (build-internal-tree nodes branch-fn childs-fn)) 50 | draws (atom 0)] 51 | (r/create-class 52 | {:component-did-mount (fn [this] 53 | (.log js/console "Component mounted, recalculating") 54 | (dimension-and-redraw this internal-nodes-a childs-fn branch-fn)) 55 | :component-did-update (fn [this] 56 | (.log js/console "Component updated. Draws " @draws) 57 | (when (<= @draws 2) 58 | (.log js/console "Component updated, recalculating") 59 | (swap! draws inc) 60 | (dimension-and-redraw this internal-nodes-a childs-fn branch-fn))) 61 | :component-will-receive-props (fn [this [old-nodes new-nodes]] 62 | (.log js/console "Component got new props, resetting internal atom.") 63 | (reset! draws 0) 64 | (reset! internal-nodes-a (build-internal-tree new-nodes branch-fn childs-fn))) 65 | :reagent-render (fn [] 66 | (let [nodes-seq (tree-seq :childs :childs @internal-nodes-a) 67 | links (when (-> nodes-seq first :width) ;; only calculate links when we have sizes 68 | (for [n nodes-seq 69 | cn (:childs n)] 70 | (let [{x :x y :y width :width height :height} n 71 | {cx :x cy :y cwidth :width cheight :height} cn] 72 | [(str (:node-id n) (:node-id cn)) 73 | (+ x (/ width 2)) 74 | (+ y height) 75 | (+ cx (/ cwidth 2)) 76 | cy])))] 77 | [:div#panel {:style {:position :relative}} 78 | 79 | ;; Links 80 | [:svg {:width layout-width 81 | :height layout-height} 82 | (for [[lid x1 y1 x2 y2] links] 83 | ^{:key lid} 84 | [:line {:x1 x1 :y1 y1 :x2 x2 :y2 y2 85 | :style line-styles}])] 86 | 87 | ;; Nodes 88 | (for [n nodes-seq] 89 | (let [{:keys [x y]} n] 90 | ^{:key (:node-id n)} [node (:node-id n) (:original-node n) x y render-fn]))]))}))) 91 | --------------------------------------------------------------------------------