(defn ->graph [g gr parent] (let [l (or (:id g) (key g)) initial (or (:initial g) (-> g last :initial)) #_#__ (.log js/console "---- " initial (-> g last :initial)) n (name l) {:keys [nodes edges]} gr nodes' (conj nodes (let [] {:data {:id n :label n :parent parent}})) edges' (into [] (concat edges (if-let [on (:on (second g))] (map #(let [from n type (first %) to (last %)] {:data {:id (str "" n type) :source from :target to :label type}}) (seq on)) []))) ] (if-let [states (or (:states g) (:states (val g)))] (reduce (fn [acc next] (->graph next acc n)) {:nodes (conj nodes' {:data {:id (str "$initial-" n) :label "$initial" :parent n}}) :edges (conj edges' {:data {:id (str "$initial-edge-" n) :source (str "$initial-" n) :target initial}}) } states) {:nodes nodes' :edges edges'}) )) (defn gg [{:keys [id graph style layout] :or {style {} layout {}}}] (let [cy (r/atom nil) opts {:style style :layout layout} on-update (fn [c] (js/console.log @cy)) on-mount (fn [c] (js/setTimeout (fn [] (reset! cy (js/cytoscape (clj->js (merge {:container (.getElementById js/document (str "graph-view" id)) :elements {:nodes (:nodes graph) :edges (:edges graph)} :style [{:selector "node" :style {:label "data(id)"}}] :layout {:name "random" } } opts)))) (on-update c)) 1)) default-layout {:name "breadthfirst" } ids (atom 1) add-node (fn [] (let [node-count (.-length (.nodes @cy)) node (.add @cy (clj->js [{:group "nodes" :data {:id (swap! ids inc)}} {:group "edges" :data {:id (swap! ids inc) :source (str (rand-int node-count)) :target (str (rand-int node-count))}}]))] (.run (.layout (.elements @cy) (clj->js default-layout))) ) ) ] (r/create-class {:component-did-mount on-mount :component-did-update on-update :reagent-render (fn [props] [:div [:div [ant/button {:on-click add-node} "add node"]] [:div.w-full.h-screen {:id (str "graph-view" id) :style {}} ]] )}))) [h/js-loader {:scripts {#(exists? js/cytoscape) "https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.15.2/cytoscape.min.js" #(exists? js/a) "https://cdn.jsdelivr.net/npm/cytoscape-cose-bilkent@4.1.0/cytoscape-cose-bilkent.min.js"} :loading [:div "loading"] :loaded [:div [gg {:id "one" :style (str "node[label != '$initial'] { content: data(label); text-valign: center; text-halign: center; shape: rectangle; width: label; height: label; padding-left: 15px; padding-right: 15px; padding-top: 5px; padding-bottom: 5px; background-color: white; font-size: 10px; font-family: Helvetica Neue; } node:active { overlay-color: black; overlay-padding: 0; overlay-opacity: 0.1; } .foo { background-color: blue; } node[label = '$initial'] { visibility: hidden; } $node > node { padding-top: 1px; padding-left: 15px; padding-bottom: 15px; padding-right: 15px; text-valign: top; text-halign: center; background-color: #f5f5f5; } edge { curve-style: bezier; width: 1px; target-arrow-shape: triangle; label: data(label); font-size: 5px; font-weight: bold; text-background-color: #fff; text-background-padding: 3px; line-color: black; target-arrow-color: black; z-index: 100; text-wrap: wrap; text-background-color: white; text-background-opacity: 1; target-distance-from-node: 2px; } edge[label = ''] { source-arrow-shape: circle; source-arrow-color: black; } node[label = 'loading']{ background-color: white; } ") :layout {:name "cose"} :graph (->graph {:id "my-test/statechart" :initial "initialising" :states {:initialising {:on {:SYSTEM-START-SELECTED "not-casting"}} :not-casting {:on {:USER-CAST-SELECTED "casting"} :initial "nc-searching" :states {:nc-searching {:on {:USER-MOVIE-SELECTED "nc-detail-navigating" :USER-SEARCH-SELECTED "nc-system-search-doing"}} :nc-system-search-doing {:onEntry "do-movie-search" :on {:SYSTEM-MOVIE-SEARCH-DONE "nc-searching"}} :nc-detail-navigating {:onEntry "set-pre-selected-movie" :on {:USER-BACK-SELECTED "nc-searching" :USER-MOVIE-CONFIRMED "nc-playing"}} :nc-playing {:onEntry "set-selected-movie" :on {:USER-BACK-SELECTED "nc-detail-navigating" :USER-STOP-SELECTED "nc-detail-navigating" :USER-PAUSE-SELECTED "nc-pausing"}} :nc-pausing {:on {:USER-RESUME-SELECTED "nc-playing"}}}} :casting {:parallel true :initial "mobile" :on {:USER-STOP-CAST-SELECTED "not-casting"} :states {:mobile { :initial "searching" :states {:searching {:on {:USER-MOVIE-SELECTED "detail-navigating" :USER-SEARCH-SELECTED "system-search-doing" :USER-PLAYING-DETAIL-SELECTED "playing-detail-navigation"}} :system-search-doing {:onEntry "do-movie-search" :on {:SYSTEM-MOVIE-SEARCH-DONE "searching"}} :detail-navigating {:onEntry "set-pre-selected-movie" :on {:USER-BACK-SELECTED "searching" :USER-MOVIE-CONFIRMED "playing-detail-navigation" :USER-PLAYING-DETAIL-SELECTED "playing-detail-navigation"}} :playing-detail-navigation {:onEntry "set-selected-movie" :on { :USER-BACK_TO_SEARCHING_SELECTED "searching" :USER-BACK_TO_DETAIL_SELECTED "detail-navigating"}}} } :external { :initial "idling" :states {:idling {:on {:USER-PLAY-SELECTED "playing"}} :playing {:on {:USER-STOP-SELECTED "idling" :USER-PAUSE-SELECTED "pausing"}} :pausing {:on {:USER-RESUME-SELECTED "playing" :USER-STOP-SELECTED "idling"}}} }} }}} {:nodes [] :edges []} nil)}]]}]