-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to use d3 force simulation? #10
Comments
PS: Here's the blocks link I mentioned which gives an example of this: https://bl.ocks.org/mbostock/4062045 |
Ok, final thing for today:
to
Prints out multiple nodes with values for:
alongside their other attrs, so it works. I'm just not sure what happens to them, as I've not worked out where they're stored/modified by the force-simulation. Turning the tick function from:
to
gives me
and
Without the
So I would expect to see the |
@Folcon It looks like, with the current implementation of rid3, the only way to do this is with a Here is a working example though: (ns folcon.core
(:require
[reagent.core :as reagent]
[goog.object :as gobj]
[rid3.core :as rid3 :refer [rid3->]]
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Vars
(defonce app-state
(reagent/atom {}))
(def nodes
[{ :id "mammal" :group 0 :label "Mammals" :level 1 }
{ :id "dog" :group 0 :label "Dogs" :level 2 }
{ :id "cat" :group 0 :label "Cats" :level 2 }
{ :id "fox" :group 0 :label "Foxes" :level 2 }
{ :id "elk" :group 0 :label "Elk" :level 2 }
{ :id "insect" :group 1 :label "Insects" :level 1 }
{ :id "ant" :group 1 :label "Ants" :level 2 }
{ :id "bee" :group 1 :label "Bees" :level 2 }
{ :id "fish" :group 2 :label "Fish" :level 1 }
{ :id "carp" :group 2 :label "Carp" :level 2 }
{ :id "pike" :group 2 :label "Pikes" :level 2 }
])
(def links
[{ :target "mammal" :source "dog" :strength 0.7 }
{ :target "mammal" :source "cat" :strength 0.7 }
{ :target "mammal" :source "fox" :strength 0.7 }
{ :target "mammal" :source "elk" :strength 0.7 }
{ :target "insect" :source "ant" :strength 0.7 }
{ :target "insect" :source "bee" :strength 0.7 }
{ :target "fish" :source "carp" :strength 0.7 }
{ :target "fish" :source "pike" :strength 0.7 }
{ :target "cat" :source "elk" :strength 0.1 }
{ :target "carp" :source "ant" :strength 0.1 }
{ :target "elk" :source "bee" :strength 0.1 }
{ :target "dog" :source "cat" :strength 0.1 }
{ :target "fox" :source "ant" :strength 0.1 }
{ :target "pike" :source "cat" :strength 0.1 }
])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Page
(def width 960)
(def height 600)
(def link-force
(-> js/d3
.forceLink
(.id (fn [link]
(gobj/get link "id")))
(.strength (fn [link]
(gobj/get link "strength")))))
(def simulation
(-> js/d3
.forceSimulation
(.force "link" link-force)
(.force "charge"
(.strength (js/d3.forceManyBody)
-120))
(.force "center"
(js/d3.forceCenter
(/ width 2)
(/ height 2)))))
(defn get-node-color [node]
(let [level (gobj/get node "level")]
(if (= 1 level)
"red"
"grey")))
(defn viz []
(let [ratom (reagent/atom {:dataset {:nodes nodes
:links links}})]
(fn []
(let []
[rid3/viz
{:id "my-viz"
:ratom ratom
:svg {:did-mount (fn [node ratom]
(rid3-> node
{:width width
:height height
}))}
:pieces
[{:kind :raw
:did-mount (fn [ratom]
(let [nodes (-> @ratom
:dataset
:nodes
clj->js)
links (-> @ratom
:dataset
:links
clj->js)
linkElements (-> js/d3
(.select "#my-viz svg .rid3-main-container")
(.append "g")
(.attr "class" "links")
(.selectAll "line")
(.data links)
.enter
(.append "line"))
nodeElements (-> js/d3
(.select "#my-viz svg .rid3-main-container")
(.append "g")
(.attr "class" "nodes")
(.selectAll "circle")
(.data nodes)
.enter
(.append "circle"))
textElements (-> js/d3
(.select "#my-viz svg .rid3-main-container")
(.append "g")
(.attr "class" "texts")
(.selectAll "text")
(.data nodes)
.enter
(.append "text"))]
(rid3-> linkElements
{:stroke-width 1
:stroke "rgba(50, 50, 50, 0.2)"})
(rid3-> nodeElements
{:r 10
:fill get-node-color})
(rid3-> textElements
{:font-size 15
:dx 15
:dy 4}
(.text (fn [node]
(gobj/get node "label"))))
(-> simulation
(.nodes nodes)
(.on "tick" (fn []
(-> nodeElements
(.attr "cx" (fn [node]
(gobj/get node "x")))
(.attr "cy" (fn [node]
(gobj/get node "y"))))
(-> textElements
(.attr "x" (fn [node]
(gobj/get node "x")))
(.attr "y" (fn [node]
(gobj/get node "y"))))
(-> linkElements
(.attr "x1" (fn [link]
(aget link "source" "x")))
(.attr "y1" (fn [link]
(aget link "source" "y")))
(.attr "x2" (fn [link]
(aget link "target" "x")))
(.attr "y2" (fn [link]
(aget link "target" "y")))))))
;; needs to be after .on
(-> simulation
(.force "link")
(.links links))
))
}
]}]))))
(defn page [ratom]
[:div
[viz]
])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Initialize App
(defn dev-setup []
(when ^boolean js/goog.DEBUG
(enable-console-print!)
(println "dev mode")
))
(defn reload []
(reagent/render [page app-state]
(.getElementById js/document "app")))
(defn ^:export main []
(dev-setup)
(reload)) |
@gadfly361 Thanks for this, I haven't vanished off the face of the earth ;)... Just been a bit busy and I won't be able to take a proper look over this until next weekend. I'll give it a proper go and see where I get =)... Would it be useful to put this into the docs? |
So I've finally had a chance to experiment with this again. I'm putting together a more complete re-frame example, with things like on-click and drag/drop. I'm not sure if this is desired behaviour, but if you do this, then the graph never updates though the dispatch event triggers and the subscription has been updated. The only way to change this I've noticed so far is to set (defn d3-mouse-pos []
((.. js/d3 -mouse) (-> js/d3 .-event .-currentTarget)))
(defn display-graph-inner [graph-sub]
(let [graph-name (gensym "display-graph")
width 960 height 600
resolution 20 r 15
bounding-box {:min-x 10 :max-x (- width 10) :min-y 10 :max-y (- height 10)}
make-node (fn [[x y]]
(let [c (count (:nodes @graph-sub))
_ (.log js/console "make-node" x y)]
{:id c :label c :x x :y y}))]
(fn [graph-sub]
[rid3/viz
{:id graph-name
:ratom graph-sub
:svg {:did-mount (fn [node ratom]
(rid3-> node
{:width width
:height height
:oncontextmenu "return false"
:viewBox (str 0 " " 0 " " width " " height)
:pointer-events :all}))}
:pieces
[{:kind :raw
:did-mount
(fn [ratom]
(let [nodes (-> @ratom
:nodes
clj->js)
links (-> @ratom
:edges
clj->js)
_ (.log js/console "nodes::" nodes "\nedges::" links)
click-handler (fn [] (.log js/console "click" (js->clj (d3-mouse-pos)))
(re-frame/dispatch [:graph/add-node (make-node (js->clj (d3-mouse-pos)))])
(.log js/console "click" @graph-sub))
container (-> js/d3
(.select (str "#" graph-name " svg"))
(.on "click" click-handler))
_ (.log js/console "container::" container)
{:keys [min-x max-x min-y max-y]} bounding-box
nodeElements (-> js/d3
(.select (str "#" graph-name " svg .rid3-main-container"))
(.append "g")
(.attr "class" "nodes")
(.selectAll "circle")
(.data nodes)
.enter
(.append "circle"))
textElements (-> js/d3
(.select (str "#" graph-name " svg .rid3-main-container"))
(.append "g")
(.attr "class" "texts")
(.selectAll "text")
(.data nodes)
.enter
(.append "text"))
round-to-nearest (fn [n resolution]
(-> n
(/ resolution)
(Math/round)
(* resolution)))
round-to-grid (fn [pos k]
(-> pos
(max (condp = k
:x min-x
:y min-y))
(min (condp = k
:x max-x
:y max-y))
(round-to-nearest resolution)))
get-in-bounds (fn [k n]
;; k is :x or :y n is the node
(-> n
(gobj/get (name k))
(round-to-grid k)))]
(rid3-> nodeElements
{:r 10 :fill "green"
:cx (fn [d] (get-in-bounds :x d))
:cy (fn [d] (get-in-bounds :y d))})
(rid3-> textElements
{:font-size 15
:dx 15
:dy 4
:x (fn [d] (get-in-bounds :x d))
:y (fn [d] (get-in-bounds :y d))}
(.text (fn [node]
(or (gobj/get node "label") (gobj/getKeys node)))))))}]}])))
(defn display-graph [sub]
(let [graph (re-frame/subscribe sub)]
[display-graph-inner graph]))
[:div [display-graph [:graph/show]]] Would you like me to tweak this so that it can be added to the docs? |
I've tried a couple of variants such as: (defn d3-mouse-pos []
((.. js/d3 -mouse) (-> js/d3 .-event .-currentTarget)))
(defn display-graph-inner [graph-sub]
(let [graph-name (gensym "display-graph")
width 960 height 600
resolution 20 r 15
bounding-box {:min-x 10 :max-x (- width 10) :min-y 10 :max-y (- height 10)}
{:keys [min-x max-x min-y max-y]} bounding-box
make-node (fn [[x y]]
(let [c (count (:nodes @graph-sub))
_ (.log js/console "make-node" x y)]
{:id c :label c :x x :y y}))
round-to-nearest (fn [n resolution]
(-> n
(/ resolution)
(Math/round)
(* resolution)))
round-to-grid (fn [pos k]
(-> pos
(max (condp = k)
:x min-x
:y min-y)
(min (condp = k)
:x max-x
:y max-y)
(round-to-nearest resolution)))
get-in-bounds (fn [k n]
;; k is :x or :y n is the node
(-> n
(gobj/get (name k))
(round-to-grid k)))]
(fn [graph-sub]
[rid3/viz
{:id graph-name
:ratom graph-sub
:svg {:did-mount (fn [node ratom]
(rid3-> node
{:width width
:height height
:oncontextmenu "return false"
:viewBox (str 0 " " 0 " " width " " height)
:pointer-events :all}))}
:pieces
[{:kind :raw
:did-mount
(fn [ratom]
(let [nodes (-> @ratom
:nodes
clj->js)
links (-> @ratom
:edges
clj->js)
_ (.log js/console "nodes::" nodes "\nedges::" links)
click-handler (fn [] (.log js/console "click" (js->clj (d3-mouse-pos)))
(re-frame/dispatch [:graph/add-node (make-node (js->clj (d3-mouse-pos)))])
(.log js/console "click" @graph-sub))
container (-> js/d3
(.select (str "#" graph-name " svg"))
(.on "click" click-handler))
_ (.log js/console "container::" container)
node-refs (-> js/d3
(.select (str "#" graph-name " svg .rid3-main-container"))
(.append "g")
(.attr "class" "nodes")
(.selectAll "circle")
(.data nodes))]
(rid3-> node-refs
(#(do (.log js/console "node-refs::" (js-keys %)) %))
.exit
.remove)
(rid3-> node-refs
.enter
(.append "circle"
{:id (fn [d] (gobj/get d "id"))
:r 10 :fill "green"
:cx (fn [d] (get-in-bounds :x d))
:cy (fn [d] (get-in-bounds :y d))}))))}]}])))
(defn display-graph [sub]
(let [graph (re-frame/subscribe sub)]
[display-graph-inner graph]))
[:div [display-graph [:graph/show]]] I think I'm going to start src diving to see what I'm missing >_<... |
Ok, I think that works =)... (defn d3-mouse-pos []
((.. js/d3 -mouse) (-> js/d3 .-event .-currentTarget)))
(defn display-graph-inner [graph-sub]
(let [graph-name (gensym "display-graph")
width 960 height 600
resolution 20 r 15
bounding-box {:min-x 10 :max-x (- width 10) :min-y 10 :max-y (- height 10)}
{:keys [min-x max-x min-y max-y]} bounding-box
make-node (fn [[x y]]
(let [c (count (:nodes @graph-sub))]
{:id c :label c :x x :y y}))
round-to-nearest (fn [n resolution]
(-> n
(/ resolution)
(Math/round)
(* resolution)))
round-to-grid (fn [pos k]
(-> pos
(max (condp = k
:x min-x
:y min-y))
(min (condp = k
:x max-x
:y max-y))
(round-to-nearest resolution)))
get-in-bounds (fn [k n]
;; k is :x or :y n is the node
(-> n
(gobj/get (name k))
(round-to-grid k)))
translate (fn [left top]
(str "translate("
(or left 0)
","
(or top 0)
")"))
click-handler (fn [] (.log js/console "click" (js->clj (d3-mouse-pos)))
(re-frame/dispatch [:graph/add-node (make-node (js->clj (d3-mouse-pos)))]))
mount-graph (fn [ratom]
(let [nodes (-> @ratom
:nodes
clj->js)
links (-> @ratom
:edges
clj->js)
container (-> js/d3
(.select (str "#" graph-name " svg"))
(.on "click" click-handler))
node-refs (-> js/d3
(.select (str "#" graph-name " svg .rid3-main-container"))
(.append "g")
(.attr "class" "nodes")
(.selectAll "circle")
(.data nodes))
text-refs (-> js/d3
(.select (str "#" graph-name " svg .rid3-main-container"))
(.append "g")
(.attr "class" "texts")
(.selectAll "text")
(.data nodes))]
(rid3-> node-refs
.enter
(.append "circle")
{:id (fn [d] (gobj/get d "id"))
:r 10 :fill "green"
:cx (fn [d] (get-in-bounds :x d))
:cy (fn [d] (get-in-bounds :y d))})
(rid3-> text-refs
.enter
(.append "text")
{:id (fn [d] (gobj/get d "id"))
:font-size 15
:dx 15
:dy 4
:x (fn [d] (get-in-bounds :x d))
:y (fn [d] (get-in-bounds :y d))}
(.text (fn [node]
(or (gobj/get node "label") (gobj/getKeys node)))))))
update-graph (fn [ratom]
(let [nodes (-> @ratom
:nodes
clj->js)
links (-> @ratom
:edges
clj->js)
node-refs (-> js/d3
(.select (str "#" graph-name " svg .rid3-main-container"))
(.selectAll "circle")
(.data nodes))
text-refs (-> js/d3
(.select (str "#" graph-name " svg .rid3-main-container"))
(.selectAll "text")
(.data nodes))]
(rid3-> node-refs
.exit
.remove)
(rid3-> text-refs
.exit
.remove)
(rid3-> node-refs
.enter
(.append "circle")
{:id (fn [d] (gobj/get d "id"))
:r 10 :fill "green"
:cx (fn [d] (get-in-bounds :x d))
:cy (fn [d] (get-in-bounds :y d))})
(rid3-> text-refs
.enter
(.append "text")
{:id (fn [d] (gobj/get d "id"))
:font-size 15
:dx 15
:dy 4
:x (fn [d] (get-in-bounds :x d))
:y (fn [d] (get-in-bounds :y d))}
(.text (fn [node]
(or (gobj/get node "label") (gobj/getKeys node)))))))]
(fn [graph-sub]
[rid3/viz
{:id graph-name
:ratom graph-sub
:svg {:did-mount (fn [node ratom]
(rid3-> node
{:width width
:height height
:oncontextmenu "return false"
:viewBox (str 0 " " 0 " " width " " height)
:pointer-events :all}))}
:pieces
[{:kind :raw
:did-mount mount-graph
:did-update update-graph}]}])))
[:div [display-graph [:graph/show]]] @gadfly361 If you'd like me to turn this into an example I can do that =)... |
@Folcon Hey I'd love to see this as an example! I'll be working on something similar soon and would love to benefit from your blood sweat and tears! |
Hey @escherize, what things would you like me to cover? :)... Also I'm doing most of this in |
Hey @Folcon!
Ideally I want to build a component that I can pass (or subscribe to) a
data structure of a graph, and have it enter, exit, and update accordingly.
Eventually I will want to use re-frame, but maybe it's outside the scope of
a rid3 example, don't ya think?
…On Thu, Nov 15, 2018 at 6:04 PM Folcon ***@***.***> wrote:
Hey @escherize <https://github.com/escherize>, what things would you like
me to cover? :)... Also I'm doing most of this in re-frame, I can leave
that out to make it more generic, or would that kind of thing be useful?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#10 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAU8-DuHbD3VoDD12CbWuCU6pKf3DUcTks5uvgEfgaJpZM4UST73>
.
|
@Folcon Thanks for following up on this! I haven't had time to dive through your latest example, but a functioning example would be great for the repo! And in vanilla reagent would be ideal :) |
d3-force exampleThis is a rewrite of Force-Directed Graph example to cljs and vanilla Reagent. All you need is [rid3 "0.2.1-1"]
[cljsjs/d3 "4.3.0-4"] to dependencies in project.clj, add [rid3.core :as rid3 :refer [rid3->]]
[cljsjs.d3] to (defn viz
[ratom]
(let [{:keys [links nodes]} @ratom
width 950
height 800
nodes-group "nodes"
node-tag "circle"
links-group "links"
link-tag "line"
component-id "rid3-force-demo"
links (clj->js links)
nodes (clj->js nodes)
nodes-sel (volatile! nil)
links-sel (volatile! nil)
sim (doto (js/d3.forceSimulation nodes)
(.force "link" (-> (js/d3.forceLink links)
(.id #(.-index %))))
(.force "charge" (js/d3.forceManyBody))
(.force "center" (js/d3.forceCenter (/ width 2) (/ height 2)))
(.on "tick" (fn []
(when-let [s @links-sel]
(rid3-> s
{:x1 #(.. % -source -x)
:y1 #(.. % -source -y)
:x2 #(.. % -target -x)
:y2 #(.. % -target -y)}))
(when-let [s @nodes-sel]
(rid3-> s
{:cx #(.-x %)}
{:cy #(.-y %)})))))
color (js/d3.scaleOrdinal (.-schemeCategory10 js/d3))
drag (-> (js/d3.drag)
(.on "start" (fn started
[_d _ _]
(if (-> js/d3 .-event .-active zero?)
(doto sim
(.alphaTarget 0.3)
(.restart)))))
(.on "drag" (fn dragged
[d _ _]
(let [event (.-event js/d3)]
(set! (.-fx d) (.-x event))
(set! (.-fy d) (.-y event)))))
(.on "end" (fn ended
[d _ _]
(if (-> js/d3 .-event .-active zero?)
(.alphaTarget sim 0))
(set! (.-fx d) nil)
(set! (.-fy d) nil))))]
[rid3/viz {:id component-id
:ratom ratom
:svg {:did-mount (fn [svg _ratom]
(rid3-> svg
{:width width
:height height
:viewBox #js [0 0 width height]}))}
:pieces [{:kind :elem-with-data
:class links-group
:tag link-tag
:prepare-dataset (fn [_ratom] links)
:did-mount (fn [sel _ratom]
(vreset! links-sel sel)
(rid3-> sel
{:stroke "#999"
:stroke-opacity 0.6
:stroke-width #(-> (.-value %)
js/Math.sqrt)}))}
{:kind :elem-with-data
:class nodes-group
:tag node-tag
:prepare-dataset (fn [_ratom] nodes)
:did-mount (fn [sel _ratom]
(vreset! nodes-sel sel)
(rid3-> sel
{:stroke "#fff"
:stroke-width 1.5
:r 5
:fill #(color (.-group %))}
(.call drag)))}]}])) Notes and thoughts
|
Thank you for your example, this was very helpful! I was able to reproduce a working force simulation from it. Regarding the The secret sauce of rid3's ratom is here. It will cause a re-render when any of its data changes. I made some tweaks to your example to show how you can make the force simulation re-render when the dataset changes (see below). I want to call out a few things:
(def width 950)
(def height 800)
(def color (js/d3.scaleOrdinal (.-schemeCategory10 js/d3)))
(defn create-sim [links nodes helper-atom]
(doto (js/d3.forceSimulation nodes)
(.force "link" (-> (js/d3.forceLink links)
(.id #(.-index %))))
(.force "charge" (js/d3.forceManyBody))
(.force "center" (js/d3.forceCenter (/ width 2) (/ height 2)))
(.on "tick" (fn []
(when-let [s (:links-sel @helper-atom)]
(rid3-> s
{:x1 #(.. % -source -x)
:y1 #(.. % -source -y)
:x2 #(.. % -target -x)
:y2 #(.. % -target -y)}))
(when-let [s (:nodes-sel @helper-atom)]
(rid3-> s
{:cx #(.-x %)}
{:cy #(.-y %)}))))))
(defn create-drag [ratom]
(let [sim (:sim @ratom)]
(-> (js/d3.drag)
(.on "start" (fn started
[_d _ _]
(if (-> js/d3 .-event .-active zero?)
(doto sim
(.alphaTarget 0.3)
(.restart)))))
(.on "drag" (fn dragged
[d _ _]
(let [event (.-event js/d3)]
(set! (.-fx d) (.-x event))
(set! (.-fy d) (.-y event)))))
(.on "end" (fn ended
[d _ _]
(if (-> js/d3 .-event .-active zero?)
(.alphaTarget sim 0))
(set! (.-fx d) nil)
(set! (.-fy d) nil))))))
(defn update-ratom [ratom helper-atom data]
(let [{:keys [links nodes]} data
links (clj->js links)
nodes (clj->js nodes)
{:keys [sim]} @ratom]
(swap! ratom assoc
:sim (create-sim links nodes helper-atom)
:links links
:nodes nodes)))
(defn viz []
;; using a form-2 component so the ratom and helper-atom survive rerenders
(let [
;; when the `ratom` gets updated, it will trigger a rerender bc of this line:
;; https://github.com/gadfly361/rid3/blob/8cee683f797c214106339d9f2a4a2b0708dc1ddf/src/main/rid3/viz.cljs#L12
ratom (reagent/atom
{:sim nil
:links nil
:nodes nil})
;; needs to be in separate atom to prevent performance issues
;; note: when the helper-atom gets updated, it doesn't cause a rerender
helper-atom (atom {:links-sel nil
:nodes-sel nil})]
(fn [] ;; need an inner fn to be a form-2 component
[:div
[:button
{:on-click (fn []
(update-ratom ratom helper-atom data/miserables))}
"Dataset 1"]
[:button
{:on-click (fn []
(update-ratom ratom helper-atom data/miserables2))}
"Dataset 2"]
[rid3/viz {:id "rid3-force-demo"
:ratom ratom
:svg {:did-mount (fn [svg ratom]
(rid3-> svg
{:width width
:height height
:viewBox #js [0 0 width height]})
(update-ratom ratom helper-atom data/miserables))
;; override the did-update fall-back to did-mount
;; if you don't, you'll observe performance issues because it'll keep updating the ratom and keep rerendering
:did-update (fn [_ _] )
}
:pieces [{:kind :elem-with-data
:class "links"
:tag "line"
;; the data should be derived from the ratom, otherwise it may not cause a rerender when / if the data changes
:prepare-dataset (fn [ratom]
(:links @ratom))
:did-mount (fn [sel ratom]
(swap! helper-atom assoc :links-sel sel)
(rid3-> sel
{:stroke "#999"
:stroke-opacity 0.6
:stroke-width #(-> (.-value %)
js/Math.sqrt)}))}
{:kind :elem-with-data
:class "nodes"
:tag "circle"
;; the data should be derived from the ratom, otherwise it may not cause a rerender when / if the data changes
:prepare-dataset (fn [ratom]
(:nodes @ratom))
:did-mount (fn [sel ratom]
(swap! helper-atom assoc :nodes-sel sel)
(rid3-> sel
{:stroke "#fff"
:stroke-width 1.5
:r 5
:fill #(color (.-group %))}
(.call (create-drag ratom))))}]}]
]))) |
Sorry, I should have been explicit about intentionally not handling data change: My goal was to keep the example to a bare minimum, and to actually avoid opening "the can of update" (just yet). You see, handling update is hard. In your example, for instance, the update is handled by Once we got that going, another problem appears. You see, when you update the data, it's as if the simulation started all over from scratch, with the nodes "exploding" from the origin on each update. I'd rather see the existing nodes to retain their position and velocity (or their fixed position, too!), while entering nodes join in nicely. I was able to do the sim state carryover as well, but I think that goes far beyond the scope of a minimal example. Also, I'm not very happy with any of the solutions I came up with so far. They are all too much of a spaghetti code, too many moving parts with unintuitive dependencies. I'll try and whip up another example that would tackle all this stuff as good as possible. |
So this is where I'm at right now: proper (?) handling of data updates in D3 simulation and rid3. Let's look at important bits.
(defn create-sim
[d3-vars]
(let [{:keys [width height]} @d3-vars]
(doto (js/d3.forceSimulation)
(.stop)
(.force "link" (-> (js/d3.forceLink) (.id #(.-index %))))
(.force "charge" (js/d3.forceManyBody))
(.force "center" (js/d3.forceCenter (/ width 2) (/ height 2)))
(.on "tick" (fn tick []
(when-let [s (:links-sel @d3-vars)]
(rid3-> s
{:x1 #(.. % -source -x)
:y1 #(.. % -source -y)
:x2 #(.. % -target -x)
:y2 #(.. % -target -y)}))
(when-let [s (:nodes-sel @d3-vars)]
(rid3-> s
{:cx #(.-x %)}
{:cy #(.-y %)}))))))) In (defn create-drag
[sim]
(-> (js/d3.drag)
(.on "start" (fn started
[_d _ _]
(if (-> js/d3 .-event .-active zero?)
(doto sim
(.alphaTarget 0.3)
(.restart)))))
(.on "drag" (fn dragged
[d _ _]
(let [event (.-event js/d3)]
(set! (.-fx d) (.-x event))
(set! (.-fy d) (.-y event)))))
(.on "end" (fn ended
[d _ _]
(if (-> js/d3 .-event .-active zero?)
(.alphaTarget sim 0))
(set! (.-fx d) nil)
(set! (.-fy d) nil))))) Now, First we index original nodes by This code is kind of creepy mix of clj map and native js arrays, and I hate it. Ewwwww, again. I'd love to have this hidden away in some library ( (defn merge-nodes
[orig new id]
(let [orig-map (into {} (map-indexed (fn [i n] [(id n) i]) orig))]
(doseq [n new]
(when-let [old (aget orig (orig-map (id n)))]
(when-let [x (.-x old)] (set! (.-x n) x))
(when-let [y (.-y old)] (set! (.-y n) y))
(when-let [vx (.-vx old)] (set! (.-vx n) vx))
(when-let [vy (.-vy old)] (set! (.-vy n) vy))
(when-let [fx (.-fx old)] (set! (.-fx n) fx))
(when-let [fy (.-fy old)] (set! (.-fy n) fy))))
new)) Having (defn update-sim! [sim alpha {:keys [links nodes]}]
(let [old-nodes (.nodes sim)
new-nodes (merge-nodes old-nodes nodes #(.-name %))]
(doto sim
(.nodes new-nodes)
(-> (.force "link") (.links links))
(.alpha alpha)
(.restart)))) Now, let's put it all together in a level 2 component. Note that (Also I'm noticing a developing pattern here. It started as volatiles for One thing I dislike (and which probably points out a flaw in this whole approach) is that (defn viz
[ratom]
(let [d3-vars (atom {:width 950
:height 800
:links-sel nil
:nodes-sel nil})
sim (create-sim d3-vars)
drag (create-drag sim)
color (js/d3.scaleOrdinal (.-schemeCategory10 js/d3))]
(fn [ratom]
[rid3/viz {:id "rid3-force-demo"
:ratom ratom
:svg {:did-mount (fn [svg ratom]
(let [{:keys [width height]} @d3-vars]
(rid3-> svg
{:width width
:height height
:viewBox #js [0 0 width height]}))
(update-sim! sim 1 @ratom))
:did-update (fn [svg ratom]
(update-sim! sim 0.3 @ratom))}
:pieces [{:kind :elem-with-data
:class "links"
:tag "line"
:prepare-dataset (fn [ratom] (:links @ratom))
:did-mount (fn [sel _ratom]
(swap! d3-vars assoc :links-sel sel)
(rid3-> sel
{:stroke "#999"
:stroke-opacity 0.6
:stroke-width #(-> (.-value %)
js/Math.sqrt)}))}
{:kind :elem-with-data
:class "nodes"
:tag "circle"
:prepare-dataset (fn [ratom] (:nodes @ratom))
:did-mount (fn [sel _ratom]
(swap! d3-vars assoc :nodes-sel sel)
(rid3-> sel
{:stroke "#fff"
:stroke-width 1.5
:r 5
:fill #(color (.-group %))}
(.call drag)))}]}]))) And now for one last trick. As seen above, (defn prechew
[app-state]
(-> @app-state
(update :nodes clj->js)
(update :links clj->js)))
(defn demo
[]
[:div
[:button {:on-click #(reset! app-state (miserables-rand-links))} "Randomize links"]
[viz (reagent/track prechew app-state)]]) ...and that's it. The good thing is that it works. The bad thing is that this "basic" example is quite complex (at least my cognitive load is at its limits when dealing with it). |
Reusing the sim is a great idea! I tested out your example locally and it worked great for me. The concept of your 'prechew' never occurred to me, and I like it a lot 🙌 I like the name I think I will try to make the above change to rid3 in the next week or two (unless you have a strong preference against adding d3-vars to the rid3/viz api). Regarding something like |
Hey I stumbled across this just now when working on my own project. The example in #10 (comment) also works for me so far (with some errors when clicking on nodes, but those might be my fault?). I'm wondering if this code ever got upstreamed into the core rid3 codebase - I don't want to be working off of this example if there is a newer better way to accomplish this already in the library! |
@kovasap Hey 👋 thanks for asking! This never made it in to rid3, so the above is still the best recommendation we have. As you work through this, please feel free to drop any thoughts or improvements here :) |
Ok thanks for the quick response! After my initial experimentation there are three things I still am not sure how to do:
I'm very new to cljs, reagent, and rid3, so any pointers on how to accomplish these things (or where to read about how to do them) would be much appreciated! |
Specifically for dragging, when I add these print statements I can see that the (defn create-drag
[sim]
(-> (js/d3.drag)
(.on "start" (fn started
[_d _ _]
(if (-> js/d3 .-event .-active zero?)
(doto sim
(.alphaTarget 0.3)
(.restart)))))
(.on "drag" (fn dragged
[d _ _]
(let [event (.-event js/d3)]
(prn "d" d) ;; ADDED
(prn "event" event) ;; ADDED
(set! (.-fx d) (.-x event))
(set! (.-fy d) (.-y event)))))
(.on "end" (fn ended
[d _ _]
(if (-> js/d3 .-event .-active zero?)
(.alphaTarget sim 0))
(set! (.-fx d) nil)
(set! (.-fy d) nil))))) Any ideas what that might mean? |
I'm also trying to figure out where exactly to use the |
@prook Any ideas for #10 (comment)? |
Hi @kovasap. I'm not sure I'll be much help here -- I had to shift my focus elsewhere, and haven't returned back to this since. But let me invest 10 minutes to investigate. Quick peek at a somewhat recent d3 example tells me the event is passed to event callbacks as the first parameter. The global (defn create-drag
[sim]
(-> (js/d3.drag)
(.on "start" (fn started
[event d _]
(when (-> event .-active zero?)
(-> sim
(.alphaTarget 0.3)
(.restart)))
(set! (.-fx d) (.-x d))
(set! (.-fy d) (.-y d))))
(.on "drag" (fn dragged
[event d _]
(set! (.-fx d) (.-x event))
(set! (.-fy d) (.-y event))))
(.on "end" (fn ended
[event d _]
(when (-> event .-active zero?)
(-> sim
(.alphaTarget 0)))
(set! (.-fx d) nil)
(set! (.-fy d) nil))))) I haven't tested nor attempted to run this, but it should work -- it's a verbatim transcription of that js example to cljs. :) HTH |
Also, let me warn you -- in the most friendly way -- about biting off more than you can chew. This is a bit off-topic, but definitely something I wish I knew two years ago. If you're new to Clojure, CLJS and Reagent, I'd recommend to study that first, and leave the monsters for later. You see:
It's a mess unsuitable for baby steps. I've been in a position similar, if not identical, to yours. I struggled, I was overwhelmed, and made no real progress in the end. From my own experience, I'd recommend to take a look at re-frame -- it's a library built upon Reagent. It saves you from re-inventing the wheel when trying to manage your app's state. But most importantly, it has exceptionaly good documentation, which transcends re-frame itself, and makes you go AHA! about Clojure, Reagent, React, functional programming, immutability, testing, about programming in general. It's an afternoon worth of reading at most, and is time spent much better than fumbling about with CLJS/Reagent/JS interop/D3 for weeks. |
#10 (comment) works perfectly! Thanks for looking into the issue! #10 (comment) makes a lot of sense - I've started to realize this as I've worked on my project. I actually started working through a re-frame tutorial for making a d3 graph (like your code does). I stopped because it seemed to me like the library was just adding another layer of complexity it would be better for me to tackle later. Maybe now is the time to take another look. I will at least for sure read the linked documentation. Thanks for the code fix and the advice, it's much appreciated! |
Hey I've gotten my project into a state I'm fairly happy with (all the issues I raised here have been fixed). You can see it at https://kovasap.github.io/reddit-tree.html. Thanks for all the help and support! Posting here in part to also help others trying to do something similar : ). One very strange issue I have yet to resolve is that when I build my app with |
Firstly I'd like to say this library is great!
I'm just not sure how to hook in something like the d3 force simulation?
I've been trying to use the example code to get an idea of how this works, but I'm pretty stumped.
The state sim appears to initialise properly, but normally you use the tick to set the values of cx and cy values of the circles, but I've been trying to not directly mutate the dom. My current approach is to try and swap the values in the nodes/edges within the simulation?
The text was updated successfully, but these errors were encountered: