1
0
Эх сурвалжийг харах

Graph revamp (#2372)

Graph revamp
Tienson Qin 4 жил өмнө
parent
commit
5a41300e80
33 өөрчлөгдсөн 1174 нэмэгдсэн , 431 устгасан
  1. 10 1
      externs.js
  2. 6 4
      package.json
  3. 2 2
      resources/css/common.css
  4. 10 1
      shadow-cljs.edn
  5. 6 1
      src/main/frontend/components/hierarchy.cljs
  6. 209 32
      src/main/frontend/components/page.cljs
  7. 22 0
      src/main/frontend/components/page.css
  8. 2 22
      src/main/frontend/components/right_sidebar.cljs
  9. 15 3
      src/main/frontend/components/search.cljs
  10. 2 2
      src/main/frontend/db.cljs
  11. 4 1
      src/main/frontend/db/default.cljs
  12. 19 2
      src/main/frontend/db/model.cljs
  13. 2 0
      src/main/frontend/dicts.cljs
  14. 4 3
      src/main/frontend/error.cljs
  15. 90 0
      src/main/frontend/extensions/graph.cljs
  16. 21 0
      src/main/frontend/extensions/graph.css
  17. 126 0
      src/main/frontend/extensions/graph/pixi.cljs
  18. 0 34
      src/main/frontend/extensions/graph_2d.cljs
  19. 7 1
      src/main/frontend/format/block.cljs
  20. 3 1
      src/main/frontend/fs/nfs.cljs
  21. 0 161
      src/main/frontend/graph.cljs
  22. 0 9
      src/main/frontend/graph.css
  23. 1 1
      src/main/frontend/handler/config.cljs
  24. 1 3
      src/main/frontend/handler/file.cljs
  25. 130 101
      src/main/frontend/handler/graph.cljs
  26. 2 4
      src/main/frontend/handler/search.cljs
  27. 1 1
      src/main/frontend/modules/shortcut/config.cljs
  28. 3 2
      src/main/frontend/modules/shortcut/data_helper.cljs
  29. 18 12
      src/main/frontend/rum.cljs
  30. 34 6
      src/main/frontend/state.cljs
  31. 18 5
      src/main/frontend/ui.cljs
  32. 59 1
      src/workspaces/workspaces/cards.cljs
  33. 347 15
      yarn.lock

+ 10 - 1
externs.js

@@ -65,7 +65,16 @@ dummy.transaction = function() {};
 dummy.getPath = function() {};
 dummy.getDoc = function() {};
 dummy.setValue = function() {};
-
+dummy.data = function() {};
+dummy.triangle = function() {};
+dummy.vee = function() {};
+dummy.destroy = function() {};
+dummy.changeData = function() {};
+dummy.layout = function() {};
+dummy.render = function() {};
+dummy.get = function() {};
+dummy.addItem = function() {};
+dummy.removeItem = function() {};
 
 /**
  * @typedef {{

+ 6 - 4
package.json

@@ -19,7 +19,7 @@
         "postcss-import-ext-glob": "^2.0.1",
         "postcss-nested": "5.0.5",
         "purgecss": "4.0.2",
-        "shadow-cljs": "^2.12.5",
+        "shadow-cljs": "2.12.5",
         "stylelint": "^13.8.0",
         "stylelint-config-standard": "^20.0.0",
         "tailwindcss": "2.2.4"
@@ -44,13 +44,13 @@
         "gulp:build": "cross-env NODE_ENV=production gulp build",
         "css:build": "postcss tailwind.all.css -o static/css/style.css --verbose --env production",
         "css:watch": "postcss tailwind.all.css -o static/css/style.css --verbose --watch",
-        "cljs:watch": "clojure -M:cljs watch app publishing electron",
+        "cljs:watch": "clojure -M:cljs watch app publishing electron cards",
         "cljs:electron-watch": "clojure -M:cljs watch app electron",
         "cljs:release": "clojure -M:cljs release app publishing electron",
         "cljs:electron-release": "clojure -M:cljs release app publishing electron --config-merge '{:asset-path \"./js\"}'",
         "cljs:test": "clojure -M:test compile test",
         "cljs:run-test": "node static/tests.js",
-        "cljs:watch-app": "clojure -M:cljs watch app",
+        "cljs:watch-app": "clojure -M:cljs watch app cards",
         "cljs:release-app": "clojure -M:cljs release app",
         "cljs:release-publishing": "clojure -M:cljs release publishing",
         "cljs:dev-release-app": "clojure -M:cljs release app --config-merge '{:closure-defines {frontend.config/DEV-RELEASE true}}'",
@@ -68,12 +68,14 @@
         "codemirror": "^5.58.1",
         "cypress-clojurescript-preprocessor": "^0.1.4",
         "cypress-real-events": "^1.5.0",
+        "d3-force": "^3.0.0",
         "diff": "5.0.0",
         "diff-match-patch": "^1.0.5",
         "electron": "^13.0.0",
         "fs": "^0.0.1-security",
         "fs-extra": "^9.1.0",
         "fuse.js": "^6.4.6",
+        "graphology": "^0.20.0",
         "gulp-cached": "^1.1.1",
         "highlight.js": "^10.4.1",
         "ignore": "^5.1.8",
@@ -81,6 +83,7 @@
         "jszip": "^3.5.0",
         "mldoc": "0.8.4",
         "path": "^0.12.7",
+        "pixi-graph-fork": "^0.0.9",
         "posthog-js": "^1.10.2",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
@@ -91,7 +94,6 @@
         "react-tippy": "^1.4.0",
         "react-transition-group": "^4.3.0",
         "reakit": "^0.11.1",
-        "url": "^0.11.0",
         "yargs-parser": "^20.2.4"
     }
 }

+ 2 - 2
resources/css/common.css

@@ -86,7 +86,7 @@ html[data-theme='dark'] {
 
 .white-theme,
 html[data-theme='light'] {
-  --ls-primary-background-color: white;
+  --ls-primary-background-color: #f6f6f6;
   --ls-secondary-background-color: #f7f6f4;
   --ls-tertiary-background-color: #f1eee8;
   --ls-quaternary-background-color: #e8e5de;
@@ -625,7 +625,7 @@ video {
   background-color: rgba(91, 33, 182);
 }
 
-.bg-purple-900	{
+.bg-purple-900  {
   background-color: rgba(76, 29, 149);
 }
 

+ 10 - 1
shadow-cljs.edn

@@ -6,7 +6,12 @@
  {:app
   {:target :browser
    :module-loader true
+   ;; handle `require(xxx.css)`
+   :js-options {:ignore-asset-requires true}
    :modules {:main {:init-fn frontend.core/init}
+             ;; :graph
+             ;; {:entries [frontend.extensions.graph.force]
+             ;;  :depends-on #{:main}}
              :code-editor
              {:entries [frontend.extensions.code]
               :depends-on #{:main}}
@@ -21,7 +26,7 @@
    :asset-path "/static/js"
    :release {:asset-path "https://asset.logseq.com/static/js"}
    :compiler-options {:infer-externs :auto
-                      :output-feature-set :es-next
+                      :output-feature-set :es-next-in
                       :source-map true
                       :externs ["datascript/externs.js"
                                 "externs.js"]
@@ -62,7 +67,11 @@
   :publishing
   {:target :browser
    :module-loader true
+   :js-options {:ignore-asset-requires true}
    :modules {:main {:init-fn frontend.publishing/init}
+             ;; :graph
+             ;; {:entries [frontend.extensions.graph.force]
+             ;;  :depends-on #{:main}}
              :code-editor
              {:entries [frontend.extensions.code]
               :depends-on #{:main}}

+ 6 - 1
src/main/frontend/components/hierarchy.cljs

@@ -9,6 +9,7 @@
             [frontend.state :as state]
             [frontend.text :as text]))
 
+;; FIXME: use block/namespace to get the relation
 (defn get-relation
   [page]
   (when (text/namespace-page? page)
@@ -22,7 +23,11 @@
 (rum/defc structures
   [page]
   (let [namespaces (get-relation page)]
-    (when (seq namespaces)
+    (when (and (seq namespaces)
+               (not (and (= 1
+                            (count namespaces)
+                            (count (first namespaces)))
+                         (not (string/includes? (ffirst namespaces) "/")))))
       [:div.page-hierachy.mt-6
        (ui/foldable
         [:h2.font-bold.opacity-30 "Hierarchy"]

+ 209 - 32
src/main/frontend/components/page.cljs

@@ -12,6 +12,7 @@
             [frontend.handler.graph :as graph-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.config :as config-handler]
             [frontend.state :as state]
             [clojure.string :as string]
             [frontend.components.block :as block]
@@ -20,7 +21,7 @@
             [frontend.components.reference :as reference]
             [frontend.components.svg :as svg]
             [frontend.components.export :as export]
-            [frontend.extensions.graph-2d :as graph-2d]
+            [frontend.extensions.graph :as graph]
             [frontend.components.hierarchy :as hierarchy]
             [frontend.ui :as ui]
             [frontend.components.content :as content]
@@ -34,7 +35,6 @@
             [goog.object :as gobj]
             [frontend.utf8 :as utf8]
             [frontend.date :as date]
-            [frontend.graph :as graph]
             [frontend.format.mldoc :as mldoc]
             [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
@@ -43,7 +43,8 @@
             [reitit.frontend.easy :as rfe]
             [frontend.text :as text]
             [frontend.modules.shortcut.core :as shortcut]
-            [frontend.handler.block :as block-handler]))
+            [frontend.handler.block :as block-handler]
+            [cljs-bean.core :as bean]))
 
 (defn- get-page-name
   [state]
@@ -427,45 +428,221 @@
              [:div {:key "page-unlinked-references"}
               (reference/unlinked-references route-page-name)])])))))
 
-(defonce layout (atom [js/window.outerWidth js/window.outerHeight]))
+(defonce layout (atom [js/window.innerWidth js/window.innerHeight]))
 
-(defonce graph-ref (atom nil))
 (defonce show-journal? (atom false))
 
+;; scrollHeight
+(rum/defcs graph-filter-section < (rum/local false ::open?)
+  [state title content {:keys [search-filters]}]
+  (let [open? (get state ::open?)]
+    (when (and (seq search-filters) (not @open?))
+      (reset! open? true))
+    [:li.relative
+     [:div
+      [:button.w-full.px-4.py-2.text-left.focus:outline-none {:on-click #(swap! open? not)}
+       [:div.flex.items-center.justify-between
+        title
+        (if @open? (svg/caret-down) (svg/caret-right))]]
+      (content open?)]]))
+
+(rum/defc filter-expand-area
+  [open? content]
+  [:div.relative.overflow-hidden.transition-all.max-h-0.duration-700
+   {:style {:max-height (if @open? 400 0)}}
+   content])
+
+(defonce *n-hops (atom nil))
+(defonce *focus-nodes (atom []))
+(defonce *graph-reset? (atom false))
+
+(rum/defc graph-filters < rum/reactive
+  [graph settings n-hops]
+  (let [{:keys [layout journal? orphan-pages? builtin-pages?]
+         :or {layout "gForce"
+              orphan-pages? true}} settings
+        set-setting! (fn [key value]
+                       (let [new-settings (assoc settings key value)]
+                         (config-handler/set-config! :graph/settings new-settings)))
+        search-graph-filters (state/sub :search/graph-filters)
+        focus-nodes (rum/react *focus-nodes)]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [:div.absolute.top-4.right-4.graph-filters
+       [:div.flex.flex-col
+        [:div.shadow-xl.rounded-sm
+         [:ul
+          (graph-filter-section
+           [:span.font-medium "Nodes"]
+           (fn [open?]
+             (filter-expand-area
+              open?
+              [:div
+               [:p.text-sm.opacity-70.px-4
+                (let [c1 (count (:nodes graph))
+                      s1 (if (> c1 1) "s" "")
+                      ;; c2 (count (:links graph))
+                      ;; s2 (if (> c2 1) "s" "")
+                      ]
+                  ;; (util/format "%d page%s, %d link%s" c1 s1 c2 s2)
+                  (util/format "%d page%s" c1 s1)
+                  )]
+               [:div.p-6
+               ;; [:div.flex.items-center.justify-between.mb-2
+               ;;  [:span "Layout"]
+               ;;  (ui/select
+               ;;    (mapv
+               ;;     (fn [item]
+               ;;       (if (= (:label item) layout)
+               ;;         (assoc item :selected "selected")
+               ;;         item))
+               ;;     [{:label "gForce"}
+               ;;      {:label "dagre"}])
+               ;;    (fn [value]
+               ;;      (set-setting! :layout value))
+               ;;    "graph-layout")]
+               [:div.flex.items-center.justify-between.mb-2
+                [:span "Journals"]
+                ;; FIXME: why it's not aligned well?
+                [:div.mt-1
+                 (ui/toggle journal?
+                            #(set-setting! :journal? (not journal?))
+                            true)]]
+               [:div.flex.items-center.justify-between.mb-2
+                [:span "Orphan pages"]
+                [:div.mt-1
+                 (ui/toggle orphan-pages?
+                            #(set-setting! :orphan-pages? (not orphan-pages?))
+                            true)]]
+               [:div.flex.items-center.justify-between.mb-2
+                [:span "Built-in pages"]
+                [:div.mt-1
+                 (ui/toggle builtin-pages?
+                            #(set-setting! :builtin-pages? (not builtin-pages?))
+                            true)]]
+               (when (seq focus-nodes)
+                 [:div.flex.flex-col.mb-2
+                  [:p {:title "N hops from selected nodes"}
+                   "N hops from selected nodes"]
+                  (ui/tippy {:html [:div.pr-3 n-hops]}
+                            (ui/slider (or n-hops 10)
+                                       {:min 1
+                                        :max 10
+                                        :on-change #(reset! *n-hops (int %))}))])
+
+               [:a.opacity-70.opacity-100 {:on-click (fn []
+                                                       (swap! *graph-reset? not)
+                                                       (reset! *focus-nodes [])
+                                                       (reset! *n-hops nil)
+                                                       (state/clear-search-filters!))}
+                "Reset Graph"]]])))
+          (graph-filter-section
+           [:span.font-medium "Search"]
+           (fn [open?]
+             (filter-expand-area
+              open?
+              [:div.p-6
+               (if (seq search-graph-filters)
+                 [:div
+                  (for [q search-graph-filters]
+                    [:div.flex.flex-row.justify-between.items-center.mb-2
+                     [:span.font-medium q]
+                     [:a.search-filter-close.opacity-70.opacity-100 {:on-click #(state/remove-search-filter! q)}
+                      svg/close]])
+
+                  [:a.opacity-70.opacity-100 {:on-click state/clear-search-filters!}
+                   "Clear All"]]
+                 [:a.opacity-70.opacity-100 {:on-click #(route-handler/go-to-search! :graph)}
+                  "Click to search"])]))
+           {:search-filters search-graph-filters})]]]])))
+
+(defn- graph-register-handlers
+  [graph focus-nodes n-hops]
+  (.on graph "nodeClick"
+       (fn [event node]
+         (graph/on-click-handler graph node event focus-nodes n-hops))))
+
+(rum/defc global-graph-inner < rum/reactive
+  [graph settings theme]
+  (let [[width height] (rum/react layout)
+        dark? (= theme "dark")
+        n-hops (rum/react *n-hops)
+        reset? (rum/react *graph-reset?)
+        focus-nodes (when n-hops (rum/react *focus-nodes))
+        graph (if (and (integer? n-hops)
+                       (seq focus-nodes)
+                       (not (:orphan-pages? settings)))
+                (graph-handler/n-hops graph focus-nodes n-hops)
+                graph)
+        graph (update graph :links (fn [links]
+                                     (let [nodes (set (map :id (:nodes graph)))]
+                                       (remove (fn [link]
+                                                 (and (not (nodes (:source link)))
+                                                      (not (nodes (:target link)))))
+                                               links))))]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [:div.relative#global-graph
+       (graph/graph-2d {:nodes (:nodes graph)
+                        :links (:links graph)
+                        :width (- width 24)
+                        :height (- height 48)
+                        :dark? dark?
+                        :register-handlers-fn
+                        (fn [graph]
+                          (graph-register-handlers graph *focus-nodes *n-hops))
+                        :reset? reset?})
+       (graph-filters graph settings n-hops)])))
+
+(defn- filter-graph-nodes
+  [nodes filters]
+  (if (seq filters)
+    (let [filter-patterns (map #(re-pattern (str "(?i)" (util/regex-escape %))) filters)]
+      (filter (fn [node] (some #(re-find % (:id node)) filter-patterns)) nodes))
+    nodes))
+
 (rum/defcs global-graph < rum/reactive
   (mixins/event-mixin
    (fn [state]
      (mixins/listen state js/window "resize"
                     (fn [e]
-                      (reset! layout [js/window.outerWidth js/window.outerHeight])))))
+                      (reset! layout [js/window.innerWidth js/window.innerHeight])))))
+  {:will-mount (fn [state]
+                 (state/set-search-mode! :graph)
+                 state)
+   :will-unmount (fn [state]
+                   (reset! *n-hops nil)
+                   (reset! *focus-nodes [])
+                   (state/set-search-mode! :global)
+                   state)}
   [state]
-  (let [theme (state/sub :ui/theme)
-        sidebar-open? (state/sub :ui/sidebar-open?)
-        [width height] (rum/react layout)
+  (let [settings (state/sub-graph-config)
+        theme (state/sub :ui/theme)
+        graph (graph-handler/build-global-graph theme settings)
+        search-graph-filters (state/sub :search/graph-filters)
+        graph (update graph :nodes #(filter-graph-nodes % search-graph-filters))
+        reset? (rum/react *graph-reset?)]
+    (global-graph-inner graph settings theme)))
+
+(rum/defc page-graph < db-mixins/query rum/reactive
+  []
+  (let [page (or
+              (and (= :page (state/sub [:route-match :data :name]))
+                   (state/sub [:route-match :path-params :name]))
+              (date/today))
+        theme (:ui/theme @state/state)
         dark? (= theme "dark")
-        graph (graph-handler/build-global-graph theme (rum/react show-journal?))]
-    (rum/with-context [[t] i18n/*tongue-context*]
-      [:div.relative#global-graph
-       (if (seq (:nodes graph))
-         (graph-2d/graph
-          (graph/build-graph-opts
-           graph
-           dark?
-           {:width (if (and (> width 1280) sidebar-open?)
-                     (- width 24 600)
-                     (- width 24))
-            :height height
-            :ref (fn [v] (reset! graph-ref v))
-            :ref-atom graph-ref}))
-         [:div.ls-center.mt-20
-          [:p.opacity-70.font-medium "Empty"]])
-       [:div.absolute.top-10.left-5
-        [:div.flex.flex-col
-         [:a.text-sm.font-medium
-          {:on-click (fn [_e]
-                       (swap! show-journal? not))}
-          (str (t :page/show-journals)
-               (if @show-journal? " (ON)"))]]]])))
+        graph (if (util/uuid-string? page)
+                (graph-handler/build-block-graph (uuid page) theme)
+                (graph-handler/build-page-graph page theme))]
+    (when (seq (:nodes graph))
+      [:div.sidebar-item.flex-col
+       (graph/graph-2d {:nodes (:nodes graph)
+                        :links (:links graph)
+                        :width 600
+                        :height 600
+                        :dark? dark?
+                        :register-handlers-fn
+                        (fn [graph]
+                          (graph-register-handlers graph (atom nil) (atom nil)))})])))
 
 (rum/defc all-pages < rum/reactive
   ;; {:did-mount (fn [state]

+ 22 - 0
src/main/frontend/components/page.css

@@ -43,6 +43,28 @@
     height: 20px;
 }
 
+.graph-filters {
+    width: 200px;
+    background: var(--ls-secondary-background-color);
+}
+
+.graph-filters ul {
+    margin-left: 0;
+}
+
+.graph-filters li {
+    list-style: none;
+    margin: 0;
+}
+
+.graph-layout {
+    background: var(--ls-secondary-background-color);
+}
+
+.search-filter-close svg {
+    transform: scale(0.7);
+}
+
 /* Change to another cursor style if Shift key is active */
 [data-active-keystroke*="Shift" i]
 :is(.journal-title, .page-title,

+ 2 - 22
src/main/frontend/components/right_sidebar.cljs

@@ -4,7 +4,7 @@
             [frontend.components.svg :as svg]
             [frontend.components.page :as page]
             [frontend.components.block :as block]
-            [frontend.extensions.graph-2d :as graph-2d]
+            [frontend.extensions.graph :as graph]
             [frontend.components.onboarding :as onboarding]
             [frontend.handler.route :as route-handler]
             [frontend.handler.page :as page-handler]
@@ -19,7 +19,6 @@
             [frontend.extensions.slide :as slide]
             [cljs-bean.core :as bean]
             [goog.object :as gobj]
-            [frontend.graph :as graph]
             [frontend.context.i18n :as i18n]
             [reitit.frontend.easy :as rfe]
             [frontend.db-mixins :as db-mixins]
@@ -45,25 +44,6 @@
               :sidebar?   true
               :repo       repo}))
 
-(rum/defc page-graph < db-mixins/query rum/reactive
-  []
-  (let [page (or
-              (and (= :page (state/sub [:route-match :data :name]))
-                   (state/sub [:route-match :path-params :name]))
-              (date/today))
-        theme (:ui/theme @state/state)
-        dark? (= theme "dark")
-        graph (if (util/uuid-string? page)
-                (graph-handler/build-block-graph (uuid page) theme)
-                (graph-handler/build-page-graph page theme))]
-    (when (seq (:nodes graph))
-      [:div.sidebar-item.flex-col
-       (graph-2d/graph
-        (graph/build-graph-opts
-         graph dark?
-         {:width  600
-          :height 600}))])))
-
 (defn recent-pages
   []
   (let [pages (->> (db/get-key-value :recent/pages)
@@ -107,7 +87,7 @@
 
     :page-graph
     [(str (t :right-side-bar/page-graph))
-     (page-graph)]
+     (page/page-graph)]
 
     :block-ref
     (when-let [block (db/entity repo [:block/uuid (:block/uuid (:block block-data))])]

+ 15 - 3
src/main/frontend/components/search.cljs

@@ -143,7 +143,7 @@
     (let [pages (when-not all? (map (fn [page] {:type :page :data page}) pages))
           files (when-not all? (map (fn [file] {:type :file :data file}) files))
           blocks (map (fn [block] {:type :block :data block}) blocks)
-          search-mode (state/get-search-mode)
+          search-mode (state/sub :search/mode)
           new-page (if (or
                         (and (seq pages)
                              (= (string/lower-case search-q)
@@ -154,7 +154,10 @@
                      [{:type :new-page}])
           result (if config/publishing?
                    (concat pages files blocks)
-                   (concat new-page pages files blocks))]
+                   (concat new-page pages files blocks))
+          result (if (= search-mode :graph)
+                   [{:type :graph-add-filter}]
+                   result)]
       [:div.rounded-md.shadow-lg.search-ac
        {:style (merge
                 {:top 48
@@ -170,6 +173,9 @@
                       (search-handler/clear-search!)
                       (leave-focus)
                       (case type
+                        :graph-add-filter
+                        (state/add-graph-search-filter! search-q)
+
                         :new-page
                         (page-handler/create! search-q)
 
@@ -222,6 +228,9 @@
          :item-render (fn [{:keys [type data]}]
                         (let [search-mode (state/get-search-mode)]
                           [:div {:class "py-2"} (case type
+                                                  :graph-add-filter
+                                                  [:b search-q]
+
                                                   :new-page
                                                   [:div.text.font-bold (str (t :new-page) ": ")
                                                    [:span.ml-1 (str "\"" search-q "\"")]]
@@ -284,7 +293,10 @@
           svg/search]
          [:input#search-field.block.w-full.h-full.pr-3.py-2.rounded-md.focus:outline-none.placeholder-gray-500.focus:placeholder-gray-400.sm:text-sm.sm:bg-transparent
           {:style {:padding-left "2rem"}
-           :placeholder (if (= search-mode :page)
+           :placeholder (case search-mode
+                          :graph
+                          (t :graph-search)
+                          :page
                           (t :page-search)
                           (t :search))
            :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here

+ 2 - 2
src/main/frontend/db.cljs

@@ -47,10 +47,10 @@
   get-latest-journals get-matched-blocks get-page get-page-alias get-page-alias-names get-page-blocks get-page-linked-refs-refed-pages
   get-page-blocks-count get-page-blocks-no-cache get-page-file get-page-format get-page-properties
   get-page-referenced-blocks get-page-referenced-pages get-page-unlinked-references get-page-referenced-blocks-no-cache
-  get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
+  get-all-pages get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   journal-page? local-native-fs? mark-repo-as-cloned! page-alias-set page-blocks-transform pull-block
   set-file-last-modified-at! transact-files-db! with-block-refs-count get-modified-pages page-empty? page-empty-or-dummy? get-alias-source-page
-  set-file-content! has-children? get-namespace-pages]
+  set-file-content! has-children? get-namespace-pages get-all-namespace-relation]
 
  [frontend.db.react
   get-current-marker get-current-page get-current-priority set-key-value

+ 4 - 1
src/main/frontend/db/default.cljs

@@ -1,10 +1,13 @@
 (ns frontend.db.default
   (:require [clojure.string :as string]))
 
+(defonce built-in-pages-names
+  #{"NOW" "LATER" "DOING" "DONE" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C"})
+
 (def built-in-pages
   (mapv (fn [p]
           {:block/name (string/lower-case p)
            :block/original-name p
            :block/journal? false
            :block/uuid (random-uuid)})
-        #{"NOW" "LATER" "DOING" "DONE" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C"}))
+        built-in-pages-names))

+ 19 - 2
src/main/frontend/db/model.cljs

@@ -118,7 +118,16 @@
          [?page :block/tags ?e]
          [?e :block/name ?tag]
          [?page :block/name ?page-name]]
-       (conn/get-conn repo)))
+    (conn/get-conn repo)))
+
+(defn get-all-namespace-relation
+  [repo]
+  (d/q '[:find ?page-name ?parent
+         :where
+         [?page :block/name ?page-name]
+         [?page :block/namespace ?e]
+         [?e :block/name ?parent]]
+    (conn/get-conn repo)))
 
 (defn get-pages
   [repo]
@@ -130,6 +139,14 @@
         (conn/get-conn repo))
        (map first)))
 
+(defn get-all-pages
+  [repo]
+  (d/q
+    '[:find [(pull ?page [*]) ...]
+      :where
+      [?page :block/name]]
+    (conn/get-conn repo)))
+
 (defn get-modified-pages
   [repo]
   (-> (d/q
@@ -1272,7 +1289,7 @@
 (defn get-namespace-pages
   [repo namespace]
   (assert (string? namespace))
-  (let [db (conn/get-conn repo)]
+  (when-let [db (conn/get-conn repo)]
     (when-not (string/blank? namespace)
       (let [namespace (string/lower-case (string/trim namespace))
             ids (->> (d/datoms db :aevt :block/name)

+ 2 - 0
src/main/frontend/dicts.cljs

@@ -242,6 +242,7 @@
                   "Search"
                   "Search or Create Page")
         :page-search "Search in the current page"
+        :graph-search "Search graph"
         :new-page "New page"
         :new-file "New file"
         :new-graph "Add new graph"
@@ -910,6 +911,7 @@
                      "搜索"
                      "搜索或者创建新页面")
            :page-search "在当前页面搜索"
+           :graph-search "搜索图谱"
            :new-page "新页面"
            :new-file "新文件"
            :graph "图谱"

+ 4 - 3
src/main/frontend/error.cljs

@@ -1,8 +1,9 @@
 (ns frontend.error
   (:require [clojure.string :as string]))
 
-(defonce ignored
-  #{"ResizeObserver loop limit exceeded"})
+(def ignored
+  #{"ResizeObserver loop limit exceeded"
+    "Uncaught TypeError:"})
 
 (defn ignored?
   [message]
@@ -10,5 +11,5 @@
     (boolean
      (some
       ;; TODO: some cases might need regex check
-      #(= (string/lower-case message) (string/lower-case %))
+      #(string/starts-with? (string/lower-case message) (string/lower-case %))
       ignored))))

+ 90 - 0
src/main/frontend/extensions/graph.cljs

@@ -0,0 +1,90 @@
+(ns frontend.extensions.graph
+  (:require [rum.core :as rum]
+            [frontend.rum :as r]
+            [frontend.ui :as ui]
+            [shadow.lazy :as lazy]
+            [frontend.handler.route :as route-handler]
+            [clojure.string :as string]
+            [cljs-bean.core :as bean]
+            [goog.object :as gobj]
+            [frontend.state :as state]
+            [frontend.db :as db]
+            [promesa.core :as p]
+            [clojure.set :as set]
+            [cljs-bean.core :as bean]
+            [frontend.extensions.graph.pixi :as pixi]
+            [frontend.util :as util :refer [profile]]
+            [cljs-bean.core :as bean]))
+
+(defonce clicked-page-timestamps (atom nil))
+
+(defn- highlight-node!
+  [^js graph node]
+  (.resetNodeStyle graph node
+                   (bean/->js {:color "#6366F1"
+                               :border {:width 2
+                                        :color "#6366F1"}})))
+
+(defn- highlight-neighbours!
+  [^js graph node]
+  (.forEachNeighbor
+   (.-graph graph) node
+   (fn [node attributes]
+     (let [attributes (bean/->clj attributes)
+           attributes (assoc attributes
+                             :color "#6366F1"
+                             :border {:width 2
+                                      :color "#6366F1"})]
+       (.resetNodeStyle graph node (bean/->js attributes))))))
+
+(defn- highlight-edges!
+  [^js graph node]
+  (.forEachEdge
+   (.-graph graph) node
+   (fn [edge attributes]
+     (.resetEdgeStyle graph edge (bean/->js {:width 1
+                                             :color "#A5B4FC"})))))
+
+(defn on-click-handler [graph node event *focus-nodes *n-hops]
+  (let [page-name (string/lower-case node)]
+    (when-not @*n-hops
+      ;; Don't trigger re-render
+      (swap! *focus-nodes
+            (fn [v]
+              (vec (distinct (conj v node))))))
+    ;; highlight current node
+    (let [node-attributes (-> (.getNodeAttributes (.-graph graph) node)
+                              (bean/->clj))]
+      (.setNodeAttribute (.-graph graph) node "parent" "ls-selected-nodes"))
+    (highlight-neighbours! graph node)
+    (highlight-edges! graph node)
+
+    ;; shift+click to select the page
+    (when-not (gobj/get event "shiftKey")
+      ;; double click to go to the page
+      (let [last-time (get @clicked-page-timestamps page-name)
+            new-time (util/time-ms)]
+        (swap! clicked-page-timestamps assoc page-name new-time)
+        (when (and last-time
+                   (< (- new-time last-time) 300))
+          (route-handler/redirect! {:to :page
+                                    :path-params {:name page-name}}))))))
+
+(defn reset-graph!
+  [^js graph]
+  (.resetView graph))
+
+(rum/defcs graph-2d <
+  (rum/local nil :ref)
+  {:did-update pixi/render!
+   :will-unmount (fn [state]
+                   (when-let [graph (:graph state)]
+                     (.destroy graph))
+                   (reset! clicked-page-timestamps nil)
+                   state)}
+  [state opts]
+  [:div.graph {:style {:height "100vh"}
+               :ref (fn [value]
+                      (let [ref (get state :ref)]
+                        (when (and ref value)
+                          (reset! ref value))))}])

+ 21 - 0
src/main/frontend/extensions/graph.css

@@ -0,0 +1,21 @@
+#global-graph,
+#page-graph {
+    min-height: 100% !important;
+    height: 100%;
+    width: 100%;
+    overflow: hidden;
+    position: relative;
+    z-index: 4;
+}
+
+#graphin-container {
+    height: 100%;
+    width: 100%;
+    position: relative;
+}
+.graphin-core {
+    height: 100%;
+    width: 100%;
+    min-height: 500px;
+    background: #fff;
+}

+ 126 - 0
src/main/frontend/extensions/graph/pixi.cljs

@@ -0,0 +1,126 @@
+(ns frontend.extensions.graph.pixi
+  (:require [rum.core :as rum]
+            [frontend.rum :as r]
+            [frontend.ui :as ui]
+            [shadow.lazy :as lazy]
+            [frontend.handler.route :as route-handler]
+            [frontend.util :as util :refer [profile]]
+            [clojure.string :as string]
+            [cljs-bean.core :as bean]
+            [goog.object :as gobj]
+            [frontend.state :as state]
+            [frontend.db :as db]
+            [promesa.core :as p]
+            [clojure.set :as set]
+            [cljs-bean.core :as bean]
+            ["pixi-graph-fork" :as Pixi-Graph]
+            ["graphology" :as graphology]
+            ["d3-force" :refer [forceSimulation forceManyBody forceCenter forceLink forceCollide forceRadial forceX forceY SimulationLinkDatum SimulationNodeDatum] :as force]))
+
+(def graph (gobj/get graphology "Graph"))
+
+(defonce colors
+  ["#1f77b4"
+   "#ff7f0e"
+   "#2ca02c"
+   "#d62728"
+   "#9467bd"
+   "#8c564b"
+   "#e377c2"
+   "#7f7f7f"
+   "#bcbd22"
+   "#17becf"])
+
+(defn default-style
+  [dark?]
+  {:node {:size (fn [node]
+                  (or (.-size node) 8))
+          :border {:width 0}
+          :color (fn [node]
+                   (if (gobj/get node "parent")
+                     (let [v (js/Math.abs (hash (.-id node)))]
+                       (nth colors (mod v (count colors))))
+                     (.-color node)))
+          :label {:content (fn [node] (.-id node))
+                  :type (.-TEXT (.-TextType Pixi-Graph))
+                  :fontSize 12
+                  :color "#333333"
+                  :backgroundColor "rgba(255, 255, 255, 0.5)"
+                  :padding 4}}
+   :edge {:width 1
+          :color (if dark? "#094b5a" "#cccccc")}})
+
+(defn default-hover-style
+  [dark?]
+  {:node {:color "#6366F1"
+          :border {:width 2
+                   :color "#6366F1"}
+          :label {:backgroundColor "rgba(238, 238, 238, 1)"}}
+   :edge {:color "#A5B4FC"}})
+
+;; TODO: animation
+;; (defn ticked [^js link ^js node]
+;;   (-> link
+;;       (.attr "x1" (fn [d] (.. d -source -x)))
+;;       (.attr "y1" (fn [d] (.. d -source -y)))
+;;       (.attr "x2" (fn [d] (.. d -target -x)))
+;;       (.attr "y2" (fn [d] (.. d -target -y))))
+
+;;   (-> node
+;;       (.attr "cx" (fn [d] (.-x d)))
+;;       (.attr "cy" (fn [d] (.-y d)))))
+
+(defn layout!
+  [nodes links]
+  (let [simulation (forceSimulation nodes)]
+    (-> simulation
+        (.force "link" (-> (forceLink)
+                           (.id (fn [d] (.-id d)))
+                           (.distance 180)
+                           (.links links)))
+        (.force "charge" (-> (forceManyBody)
+                             (.distanceMax 4000)
+                             (.theta 0.5)
+                             (.strength -600)))
+        (.force "collision" (-> (forceCollide)
+                                (.radius (+ 8 18))))
+        (.force "x" (-> (forceX 0) (.strength 0.02)))
+        (.force "y" (-> (forceX 0) (.strength 0.02)))
+        (.force "center" (forceCenter))
+        (.tick 30)
+        (.stop))))
+
+(defn render!
+  [state]
+  (when-let [graph (:graph state)]
+    (.destroy graph))
+  (let [{:keys [nodes links style hover-style height register-handlers-fn dark?]} (first (:rum/args state))
+        style (or style (default-style dark?))
+        hover-style (or hover-style (default-hover-style dark?))
+        graph (graph.)
+        nodes-set (set (map :id nodes))
+        links (->> (filter (fn [link]
+                             (and (nodes-set (:source link)) (nodes-set (:target link)))) links)
+                   (distinct))
+        nodes-js (bean/->js nodes)
+        links-js (bean/->js links)]
+    (layout! nodes-js links-js)
+    (doseq [node nodes-js]
+      (.addNode graph (.-id node) node))
+    (doseq [link links-js]
+      (.addEdge graph (.-id (.-source link)) (.-id (.-target link)) link))
+
+    (if-let [container-ref (:ref state)]
+      (let [graph (new (.-PixiGraph Pixi-Graph)
+                       (bean/->js
+                        {:container @container-ref
+                         :graph graph
+                         :style style
+                         :hoverStyle hover-style
+                         :height height}))]
+        (when register-handlers-fn
+          (register-handlers-fn graph))
+
+        ;; (.addEventListener container-ref)
+        (assoc state :graph graph))
+      state)))

+ 0 - 34
src/main/frontend/extensions/graph_2d.cljs

@@ -1,34 +0,0 @@
-(ns frontend.extensions.graph-2d
-  (:require [rum.core :as rum]
-            [frontend.loader :as loader]
-            [frontend.config :as config]
-            [goog.dom :as gdom]
-            [goog.object :as gobj]
-            [frontend.rum :as r]))
-
-;; TODO: extracted to a rum mixin
-(defn loaded? []
-  js/window.ForceGraph)
-
-(defonce graph-component
-  (atom nil))
-
-(defonce *loading? (atom true))
-
-(rum/defc graph < rum/reactive
-  {:init (fn [state]
-           (if @graph-component
-             (reset! *loading? false)
-             (do
-               (loader/load
-                (config/asset-uri "/static/js/react-force-graph.min.js")
-                (fn []
-                  (reset! graph-component
-                          (r/adapt-class (gobj/get js/window.ForceGraph "ForceGraph2D")))
-                  (reset! *loading? false)))))
-           state)}
-  [opts]
-  (let [loading? (rum/react *loading?)]
-    (when @graph-component
-      (@graph-component
-       opts))))

+ 7 - 1
src/main/frontend/format/block.cljs

@@ -272,13 +272,19 @@
   [original-page-name with-id?]
   (when original-page-name
     (let [[original-page-name page-name journal-day] (convert-page-if-journal original-page-name)
+          namespace? (and (string/includes? original-page-name "/")
+                          (text/namespace-page? original-page-name))
           m (merge
              {:block/name page-name
               :block/original-name original-page-name}
              (when with-id?
                (if-let [block (db/entity [:block/name page-name])]
                  {}
-                 {:block/uuid (db/new-block-id)})))]
+                 {:block/uuid (db/new-block-id)}))
+             (when namespace?
+               (let [namespace (first (util/split-last "/" original-page-name))]
+                 (when-not (string/blank? namespace)
+                   {:block/namespace {:block/name (string/lower-case namespace)}}))))]
       (if journal-day
         (merge m
                {:block/journal? true

+ 3 - 1
src/main/frontend/fs/nfs.cljs

@@ -133,13 +133,15 @@
                   format (-> (util/get-file-ext path)
                              (config/get-file-format))
                   pending-writes (state/get-write-chan-length)
-                  draw? (and path (string/ends-with? path ".excalidraw"))]
+                  draw? (and path (string/ends-with? path ".excalidraw"))
+                  config? (and path (string/ends-with? path "/config.edn"))]
             (p/let [_ (verify-permission repo file-handle true)
                     _ (utils/writeFile file-handle content)
                     file (.getFile file-handle)]
               (if (and local-content new?
                        (or
                         draw?
+                        config?
                         ;; Writing not finished
                         (> pending-writes 0)
                         ;; not changed by other editors

+ 0 - 161
src/main/frontend/graph.cljs

@@ -1,161 +0,0 @@
-(ns frontend.graph
-  (:require [frontend.handler.route :as route-handler]
-            [clojure.string :as string]
-            [cljs-bean.core :as bean]
-            [goog.object :as gobj]
-            [frontend.state :as state]
-            [frontend.db :as db]
-            [cljs-bean.core :as bean]))
-
-;; translated from https://github.com/vasturiano/react-force-graph/blob/master/example/highlight/index.html
-(defonce graph-mode (atom :dot-text))
-(defonce highlight-nodes (atom #{}))
-(defonce highlight-links (atom #{}))
-(defonce hover-node (atom nil))
-(defonce node-r 8)
-
-(defn- clear-highlights!
-  []
-  (reset! highlight-nodes #{})
-  (reset! highlight-links #{}))
-
-(defn- highlight-node!
-  [node]
-  (swap! highlight-nodes conj node))
-
-(defn- highlight-link!
-  [link]
-  (swap! highlight-links conj (bean/->clj link)))
-
-(defn- on-node-hover
-  [node]
-  (clear-highlights!)
-  (when node
-    (highlight-node! (gobj/get node "id"))
-    (doseq [neighbor (array-seq (gobj/get node "neighbors"))]
-      (highlight-node! neighbor))
-    (doseq [link (array-seq (gobj/get node "links"))]
-      (highlight-link! link)))
-  (reset! hover-node (gobj/get node "id")))
-
-(defn- on-link-hover
-  [link]
-  (clear-highlights!)
-  (when link
-    (highlight-link! link)
-    (highlight-node! (gobj/get link "source"))
-    (highlight-node! (gobj/get link "target"))))
-
-(defonce static-num (js/Math.pow 2 24))
-(defn get-color
-  [n]
-  (str "#" (-> (mod (* n 1234567)
-                    static-num)
-               (.toString 16)
-               (.padStart 6 "0"))))
-
-(defn- dot-text-mode
-  [node ctx global-scale dark?]
-  (let [hide-text? (< global-scale 0.45)
-        label (gobj/get node "id")
-        val (gobj/get node "val")
-        val (if (zero? val) 1 val)
-        font-size (min
-                   10
-                   (* (/ 15 global-scale) (js/Math.cbrt val)))
-        arc-radius (/ 3 global-scale)
-        _ (set! (.-font ctx)
-                (str font-size "px Inter"))
-        text-width (gobj/get (.measureText ctx label) "width")
-        x (gobj/get node "x")
-        y (gobj/get node "y")
-        color (gobj/get node "color")]
-    (set! (.-filltextAlign ctx) "center")
-    (set! (.-textBaseLine ctx) "middle")
-    (set! (.-fillStyle ctx) color)
-    (when-not hide-text?
-      (.fillText ctx label
-                 (- x (/ text-width 2))
-                 (- y (/ 9 global-scale))))
-
-    (.beginPath ctx)
-    (.arc ctx x y (if (zero? val)
-                    arc-radius
-                    (* arc-radius (js/Math.sqrt (js/Math.sqrt val)))) 0 (* 2 js/Math.PI) false)
-    (set! (.-fillStyle ctx)
-          (if (contains? @highlight-nodes label)
-            (if dark? "#A3BFFA" "#4C51BF")
-            (if dark? "#999" "#666")))
-    (.fill ctx)))
-
-(defn build-graph-data
-  [{:keys [links nodes]}]
-  (let [nodes (mapv
-               (fn [node]
-                 (let [links (filter (fn [{:keys [source target]}]
-                                       (let [node (:id node)]
-                                         (or (= source node) (= target node)))) links)]
-                   (assoc node
-                          :neighbors (vec
-                                      (distinct
-                                       (->>
-                                        (concat
-                                         (mapv :source links)
-                                         (mapv :target links))
-                                        (remove #(= (:id node) %)))))
-                          :links (vec links))))
-               nodes)]
-    {:links links
-     :nodes nodes}))
-
-(defn- build-graph-opts
-  [graph dark? option]
-  (let [nodes-count (count (:nodes graph))
-        graph-data (build-graph-data graph)]
-    (merge
-     {:graphData (bean/->js graph-data)
-      ;; :nodeRelSize node-r
-      :linkWidth (fn [link]
-                   (let [link {:source (gobj/get link "source")
-                               :target (gobj/get link "target")}]
-                     (if (contains? @highlight-links link) 5 1)))
-      :linkDirectionalParticles 2
-      :linkDirectionalParticleWidth (fn [link]
-                                      (let [link {:source (-> (gobj/get link "source")
-                                                              (gobj/get "id"))
-                                                  :target (-> (gobj/get link "target")
-                                                              (gobj/get "id"))}]
-                                        (if (contains? @highlight-links link) 2 0)))
-      :onNodeHover on-node-hover
-      :onLinkHover on-link-hover
-      :nodeLabel "id"
-      :linkColor (fn [] (if dark? "rgba(255,255,255,0.2)" "rgba(0,0,0,0.1)"))
-      :onZoom (fn [z]
-                (let [k (:k (bean/->clj z))]
-                  (reset! graph-mode
-                          (cond
-                            (< k 0.4)
-                            :dot
-
-                            :else
-                            :dot-text))))
-      :onNodeClick (fn [node event]
-                     (let [page-name (string/lower-case (gobj/get node "id"))]
-                       (if (gobj/get event "shiftKey")
-                         (let [repo (state/get-current-repo)
-                               page (db/entity repo [:block/name page-name])]
-                           (state/sidebar-add-block!
-                            repo
-                            (:db/id page)
-                            :page
-                            {:page page}))
-                         (route-handler/redirect! {:to :page
-                                                   :path-params {:name page-name}}))))
-      ;; :cooldownTicks 100
-      ;; :onEngineStop (fn []
-      ;;                 (when-let [ref (:ref-atom option)]
-      ;;                   (.zoomToFit @ref 400)))
-      :nodeCanvasObject
-      (fn [node ^CanvasRenderingContext2D ctx global-scale]
-        (dot-text-mode node ctx global-scale dark?))}
-     option)))

+ 0 - 9
src/main/frontend/graph.css

@@ -1,9 +0,0 @@
-#global-graph,
-#page-graph {
-  min-height: 100% !important;
-  height: 100%;
-  width: 100%;
-  overflow: hidden;
-  position: relative;
-  z-index: 4;
-}

+ 1 - 1
src/main/frontend/handler/config.cljs

@@ -6,7 +6,7 @@
 (defn set-config!
   [k v]
   (let [path (config/get-config-path)]
-    (file-handler/edn-file-set-key-value path k v state/set-config!)))
+    (file-handler/edn-file-set-key-value path k v)))
 
 (defn toggle-ui-show-brackets! []
   (let [show-brackets? (state/show-brackets?)]

+ 1 - 3
src/main/frontend/handler/file.cljs

@@ -301,7 +301,7 @@
         (reset-file! repo-url path default-content)))))
 
 (defn edn-file-set-key-value
-  [path k v ok-handler]
+  [path k v]
   (when-let [repo (state/get-current-repo)]
     (when-let [content (db/get-file-no-sub path)]
       (let [result (try
@@ -312,7 +312,5 @@
                        {}))
             ks (if (vector? k) k [k])
             new-result (rewrite/assoc-in result ks v)]
-        (when ok-handler (ok-handler repo new-result))
-        (state/set-config! repo new-result)
         (let [new-content (str new-result)]
           (set-file-content! repo path new-content))))))

+ 130 - 101
src/main/frontend/handler/graph.cljs

@@ -1,65 +1,75 @@
 (ns frontend.handler.graph
   (:require [frontend.db :as db]
             [clojure.string :as string]
-            [frontend.util :as util]
+            [frontend.util :as util :refer [profile]]
             [frontend.date :as date]
             [frontend.state :as state]
             [clojure.set :as set]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.db.default :as default-db]
+            [frontend.text :as text]))
 
-(defn- build-edges
-  [edges]
+(defn- build-links
+  [links]
   (map (fn [[from to]]
          {:source from
           :target to})
-       edges))
+       links))
 
 (defn- get-connections
-  [page edges]
+  [page links]
   (count (filter (fn [{:keys [source target]}]
                    (or (= source page)
                        (= target page)))
-                 edges)))
+                 links)))
 
 (defn- build-nodes
-  [dark? current-page edges tags nodes]
-  (let [pages (->> (set (flatten nodes))
+  [dark? current-page page-links tags nodes namespaces]
+  (let [parents (set (map last namespaces))
+        current-page (or current-page "")
+        pages (->> (set (flatten nodes))
                    (remove nil?))]
     (->>
      (mapv (fn [p]
              (when p
                (let [p (str p)
                      current-page? (= p current-page)
-                     block? (and p (util/uuid-string? p))
-                     color (if block?
-                             "#1a6376"
-                             (case [dark? current-page?] ; FIXME: Put it into CSS
-                              [false false] "#222222"
-                              [false true]  "#045591"
-                              [true false]  "#8abbbb"
-                              [true true]   "#ffffff"))
-                     color (if (contains? tags (string/lower-case (str p)))
+                     color (case [dark? current-page?] ; FIXME: Put it into CSS
+                             [false false] "#999"
+                             [false true]  "#045591"
+                             [true false]  "#93a1a1"
+                             [true true]   "#ffffff")
+                     color (if (contains? tags p)
                              (if dark? "orange" "green")
                              color)]
-                 {:id p
-                  :name p
-                  :val (get-connections p edges)
-                  :autoColorBy "group"
-                  :group (js/Math.ceil (* (js/Math.random) 12))
-                  :color color})))
+                 (let [n (get page-links p 1)
+                       size-v (if (> n 2)
+                                (js/Math.cbrt n)
+                                n)
+                       size-v (if (< size-v 1)
+                                1
+                                (int size-v))
+                       size (* size-v 8)]
+                   (cond->
+                     {:id p
+                      :label p
+                      :size size
+                      :color color}
+                     (contains? parents p)
+                     (assoc :parent true))))))
            pages)
      (remove nil?))))
 
+;; slow
 (defn- uuid-or-asset?
   [id]
-  (let [id (str id)]
-    (or (util/uuid-string? id)
-       (string/starts-with? id "../assets/")
-       (= id "..")
-       (string/starts-with? id "assets/")
-       (string/ends-with? id ".gif")
-       (string/ends-with? id ".jpg")
-       (string/ends-with? id ".png"))))
+  (or (util/uuid-string? id)
+      (string/starts-with? id "../assets/")
+      (= id "..")
+      (string/starts-with? id "assets/")
+      (string/ends-with? id ".gif")
+      (string/ends-with? id ".jpg")
+      (string/ends-with? id ".png")))
 
 (defn- remove-uuids-and-files!
   [nodes]
@@ -68,73 +78,59 @@
    nodes))
 
 (defn- normalize-page-name
-  [{:keys [nodes links] :as g}]
-  (let [all-pages (->> (set (apply concat
-                                   [(map :id nodes)
-                                    (map :source links)
-                                    (map :target links)]))
-                       (map string/lower-case))
-        names (db/pull-many '[:block/name :block/original-name] (mapv (fn [page]
-                                                                        (if (util/uuid-string? page)
-                                                                          [:block/uuid (uuid page)]
-                                                                          [:block/name page])) all-pages))
-        names (zipmap (map (fn [x] (get x :block/name)) names)
-                      (map (fn [x]
-                             (get x :block/original-name (:block/name x))) names))
-        nodes (mapv (fn [node] (assoc node :id (get names (:id node) (:id node)))) nodes)
-        links (->>
-               links
-               (remove (fn [{:keys [source target]}]
-                         (or (nil? source) (nil? target))))
-               (mapv (fn [{:keys [source target]}]
-                       (when (and (not (uuid-or-asset? source))
-                                  (not (uuid-or-asset? target)))
-                         {:source (get names (string/lower-case source))
-                          :target (get names (string/lower-case target))})))
-               (remove nil?)
-               (remove (fn [{:keys [source target]}]
-                         (or (nil? source) (nil? target)))))
+  [{:keys [nodes links page-name->original-name]}]
+  (let [links (->>
+               (map
+                 (fn [{:keys [source target]}]
+                   (let [source (get page-name->original-name source)
+                         target (get page-name->original-name target)]
+                     (when (and source target)
+                       {:source source :target target})))
+                 links)
+               (remove nil?))
         nodes (->> (remove-uuids-and-files! nodes)
-                   (util/distinct-by #(string/lower-case (:id %))))]
+                   (util/distinct-by (fn [node] (:id node)))
+                   (map (fn [node]
+                          (if-let [original-name (get page-name->original-name (:id node))]
+                            (assoc node :id original-name :label original-name)
+                            nil)))
+                   (remove nil?))]
     {:nodes nodes
      :links links}))
 
 (defn build-global-graph
-  [theme show-journal?]
+  [theme {:keys [journal? orphan-pages? builtin-pages?] :as settings}]
   (let [dark? (= "dark" theme)
-        current-page (:block/name (db/get-current-page))]
+        current-page (or (:block/name (db/get-current-page)) "")]
     (when-let [repo (state/get-current-repo)]
-      (let [relation (db/get-pages-relation repo show-journal?)
+      (let [relation (db/get-pages-relation repo journal?)
             tagged-pages (db/get-all-tagged-pages repo)
+            namespaces (db/get-all-namespace-relation repo)
             tags (set (map second tagged-pages))
-            linked-pages (-> (concat
-                              relation
-                              tagged-pages)
-                             flatten
-                             set)
-            all-pages (db/get-pages repo)
-            other-pages (->> (remove linked-pages all-pages)
-                             (remove nil?))
-            other-pages (if show-journal? other-pages
-                            (remove date/valid-journal-title? other-pages))
-            other-pages (if (seq other-pages)
-                          (map string/lower-case other-pages)
-                          other-pages)
-            nodes (concat (seq relation)
+            full-pages (db/get-all-pages repo)
+            get-original-name (fn [p] (or (:block/original-name p) (:block/name p)))
+            all-pages (map get-original-name full-pages)
+            page-name->original-name (zipmap (map :block/name full-pages) all-pages)
+            pages-after-journal-filter (if-not journal?
+                                         (remove :block/journal? full-pages)
+                                         full-pages)
+            links (concat (seq relation)
                           (seq tagged-pages)
-                          (if (seq other-pages)
-                            (map (fn [page]
-                                   [page])
-                                 other-pages)
-                            []))
-            edges (build-edges (remove
-                                (fn [[_ to]]
-                                  (nil? to))
-                                nodes))
-            nodes (build-nodes dark? current-page edges tags nodes)]
+                          (seq namespaces))
+            linked (set (flatten links))
+            nodes (cond->> (map :block/name pages-after-journal-filter)
+                    (not builtin-pages?)
+                    (remove (fn [p] (default-db/built-in-pages-names (string/upper-case p))))
+                    (not orphan-pages?)
+                    (filter #(contains? linked (string/lower-case %))))
+            page-links (reduce (fn [m [k v]] (-> (update m k inc)
+                                                 (update v inc))) {} links)
+            links (build-links (remove (fn [[_ to]] (nil? to)) links))
+            nodes (build-nodes dark? (string/lower-case current-page) page-links tags nodes namespaces)]
         (normalize-page-name
          {:nodes nodes
-          :links edges})))))
+          :links links
+          :page-name->original-name page-name->original-name})))))
 
 (defn build-page-graph
   [page theme]
@@ -147,7 +143,9 @@
             tags (remove #(= page %) tags)
             ref-pages (db/get-page-referenced-pages repo page)
             mentioned-pages (db/get-pages-that-mentioned-page repo page)
-            edges (concat
+            namespaces (db/get-all-namespace-relation repo)
+            links (concat
+                   namespaces
                    (map (fn [[p aliases]]
                           [page p]) ref-pages)
                    (map (fn [[p aliases]]
@@ -159,7 +157,7 @@
                                      (map first mentioned-pages))
                              (remove nil?)
                              (set))
-            other-pages-edges (mapcat
+            other-pages-links (mapcat
                                (fn [page]
                                  (let [ref-pages (-> (map first (db/get-page-referenced-pages repo page))
                                                      (set)
@@ -171,21 +169,27 @@
                                     (map (fn [p] [page p]) ref-pages)
                                     (map (fn [p] [p page]) mentioned-pages))))
                                other-pages)
-            edges (->> (concat edges other-pages-edges)
+            links (->> (concat links other-pages-links)
                        (remove nil?)
                        (distinct)
-                       (build-edges))
+                       (build-links))
             nodes (->> (concat
                         [page]
                         (map first ref-pages)
                         (map first mentioned-pages)
                         tags)
                        (remove nil?)
-                       (distinct)
-                       (build-nodes dark? page edges (set tags)))]
+                       (distinct))
+            nodes (build-nodes dark? page links (set tags) nodes namespaces)
+            full-pages (db/get-all-pages repo)
+            get-original-name (fn [p] (or (:block/original-name p)
+                                         (:block/name p)))
+            all-pages (map get-original-name full-pages)
+            page-name->original-name (zipmap (map :block/name full-pages) all-pages)]
         (normalize-page-name
          {:nodes nodes
-          :links edges})))))
+          :links links
+          :page-name->original-name page-name->original-name})))))
 
 (defn build-block-graph
   "Builds a citation/reference graph for a given block uuid."
@@ -193,13 +197,15 @@
   (let [dark? (= "dark" theme)]
     (when-let [repo (state/get-current-repo)]
       (let [ref-blocks (db/get-block-referenced-blocks block)
-            edges (concat
+            namespaces (db/get-all-namespace-relation repo)
+            links (concat
                    (map (fn [[p aliases]]
-                          [block p]) ref-blocks))
+                          [block p]) ref-blocks)
+                   namespaces)
             other-blocks (->> (concat (map first ref-blocks))
                               (remove nil?)
                               (set))
-            other-blocks-edges (mapcat
+            other-blocks-links (mapcat
                                 (fn [block]
                                   (let [ref-blocks (-> (map first (db/get-block-referenced-blocks block))
                                                        (set)
@@ -207,17 +213,40 @@
                                     (concat
                                      (map (fn [p] [block p]) ref-blocks))))
                                 other-blocks)
-            edges (->> (concat edges other-blocks-edges)
+            links (->> (concat links other-blocks-links)
                        (remove nil?)
                        (distinct)
-                       (build-edges))
+                       (build-links))
             nodes (->> (concat
                         [block]
                         (map first ref-blocks))
                        (remove nil?)
                        (distinct)
                        ;; FIXME: get block tags
-                       (build-nodes dark? block edges #{}))]
+                       )
+            nodes (build-nodes dark? block links #{} nodes namespaces)]
         (normalize-page-name
          {:nodes nodes
-          :links edges})))))
+          :links links})))))
+
+(defn n-hops
+  "Get all nodes that are n hops from nodes (a collection of node ids)"
+  [{:keys [links] :as graph} nodes level]
+  (let [search-nodes (fn [forward?]
+                       (let [links (group-by (if forward? :source :target) links)]
+                         (loop [nodes nodes
+                               level level]
+                          (if (zero? level)
+                            nodes
+                            (recur (distinct (apply concat nodes
+                                               (map
+                                                 (fn [id]
+                                                   (->> (get links id) (map (if forward? :target :source))))
+                                                 nodes)))
+                                   (dec level))))))
+        nodes (concat (search-nodes true) (search-nodes false))
+        nodes (set nodes)]
+    (update graph :nodes
+            (fn [full-nodes]
+              (filter (fn [node] (contains? nodes (:id node)))
+                      full-nodes)))))

+ 2 - 4
src/main/frontend/handler/search.cljs

@@ -34,10 +34,8 @@
   ([]
    (clear-search! true))
   ([clear-search-mode?]
-   (let [m (cond-> {:search/result nil
-                    :search/q ""}
-             clear-search-mode?
-             (assoc :search/mode :global))]
+   (let [m {:search/result nil
+            :search/q ""}]
      (swap! state/state merge m))
    (when-let [input (gdom/getElement "search-field")]
      (gobj/set input "value" ""))))

+ 1 - 1
src/main/frontend/modules/shortcut/config.cljs

@@ -259,7 +259,7 @@
     :go/search
     {:desc    "Full text search"
      :binding "mod+u"
-     :fn      route-handler/go-to-search!}
+     :fn      #(route-handler/go-to-search! nil)}
     :go/journals
     {:desc    "Jump to journals"
      :binding (if mac? "mod+j" "alt+j")

+ 3 - 2
src/main/frontend/modules/shortcut/data_helper.cljs

@@ -7,7 +7,8 @@
             [frontend.modules.shortcut.config :as config]
             [frontend.state :as state]
             [frontend.util :as util]
-            [lambdaisland.glogi :as log])
+            [lambdaisland.glogi :as log]
+            [frontend.handler.common :as common-handler])
   (:import [goog.ui KeyboardShortcutHandler]))
 (defonce default-binding
   (->> (vals config/default-config)
@@ -130,8 +131,8 @@
                         result
                         :shortcuts
                         #(dissoc (rewrite/sexpr %) k))]
-        (state/set-config! repo new-result)
         (let [new-content (str new-result)]
+          (common-handler/reset-config! repo new-content)
           (file/set-file-content! repo path new-content))))))
 
 (defn get-group

+ 18 - 12
src/main/frontend/rum.cljs

@@ -1,7 +1,8 @@
 (ns frontend.rum
   (:require [clojure.string :as s]
             [clojure.set :as set]
-            [clojure.walk :as w]))
+            [clojure.walk :as w]
+            [cljs-bean.core :as bean]))
 
 ;; copy from https://github.com/priornix/antizer/blob/35ba264cf48b84e6597743e28b3570d8aa473e74/src/antizer/core.cljs
 
@@ -31,29 +32,34 @@
                 data)))
 
 ;; adapted from https://github.com/tonsky/rum/issues/20
-(defn adapt-class [react-class]
-  (fn [& args]
+(defn adapt-class
+  ([react-class]
+   (adapt-class react-class false))
+  ([react-class skip-opts-transform?]
+   (fn [& args]
     (let [[opts children] (if (map? (first args))
                             [(first args) (rest args)]
                             [{} args])
           type# (first children)
-             ;; we have to make sure to check if the children is sequential
-             ;; as a list can be returned, eg: from a (for)
+          ;; we have to make sure to check if the children is sequential
+          ;; as a list can be returned, eg: from a (for)
           new-children (if (sequential? type#)
                          (let [result (daiquiri.interpreter/interpret children)]
                            (if (sequential? result)
                              result
                              [result]))
                          children)
-             ;; convert any options key value to a react element, if
-             ;; a valid html element tag is used, using sablono
+          ;; convert any options key value to a react element, if
+          ;; a valid html element tag is used, using sablono
           vector->react-elems (fn [[key val]]
                                 (if (sequential? val)
                                   [key (daiquiri.interpreter/interpret val)]
                                   [key val]))
-          new-options (into {} (map vector->react-elems opts))]
-         ;; (.dir js/console new-children)
+          new-options (into {}
+                            (if skip-opts-transform?
+                              opts
+                              (map vector->react-elems opts)))]
       (apply js/React.createElement react-class
-           ;; sablono html-to-dom-attrs does not work for nested hashmaps
-             (clj->js (map-keys->camel-case new-options :html-props true))
-             new-children))))
+        ;; sablono html-to-dom-attrs does not work for nested hashmaps
+        (bean/->js (map-keys->camel-case new-options :html-props true))
+        new-children)))))

+ 34 - 6
src/main/frontend/state.cljs

@@ -47,6 +47,7 @@
       :search/q ""
       :search/mode :global
       :search/result nil
+      :search/graph-filters []
 
       ;; modals
       :modal/show? false
@@ -142,6 +143,17 @@
 
       :view/components {}})))
 
+
+(defn sub
+  [ks]
+  (if (coll? ks)
+    (util/react (rum/cursor-in state ks))
+    (util/react (rum/cursor state ks))))
+
+(defn sub-current-route
+  []
+  (get-in (sub :route-match) [:data :name]))
+
 (defn get-route-match
   []
   (:route-match @state))
@@ -164,12 +176,6 @@
   []
   (get-in (get-route-match) [:query-params :p]))
 
-(defn sub
-  [ks]
-  (if (coll? ks)
-    (util/react (rum/cursor-in state ks))
-    (util/react (rum/cursor state ks))))
-
 (defn set-state!
   [path value]
   (if (vector? path)
@@ -260,6 +266,10 @@
   ;; Disable block timestamps for now, because it doesn't work with undo/redo
   false)
 
+(defn sub-graph-config
+  []
+  (:graph/settings (get (sub-config) (get-current-repo))))
+
 ;; Enable by default
 (defn show-brackets?
   []
@@ -1232,6 +1242,24 @@
   []
   (set-search-result! nil))
 
+(defn add-graph-search-filter!
+  [q]
+  (when-not (string/blank? q)
+    (update-state! :search/graph-filters
+                  (fn [value]
+                    (vec (distinct (conj value q)))))))
+
+(defn remove-search-filter!
+  [q]
+  (when-not (string/blank? q)
+    (update-state! :search/graph-filters
+                   (fn [value]
+                     (remove #{q} value)))))
+
+(defn clear-search-filters!
+  []
+  (set-state! :search/graph-filters []))
+
 (defn get-search-mode
   []
   (:search/mode @state))

+ 18 - 5
src/main/frontend/ui.cljs

@@ -18,7 +18,8 @@
             [frontend.ui.date-picker]
             [frontend.context.i18n :as i18n]
             [frontend.modules.shortcut.core :as shortcut]
-            [lambdaisland.glogi :as log]))
+            [lambdaisland.glogi :as log]
+            [frontend.config :as config]))
 
 (defonce transition-group (r/adapt-class TransitionGroup))
 (defonce css-transition (r/adapt-class CSSTransition))
@@ -608,14 +609,15 @@
   (when error
     (js/console.error error)
     (log/error :ui/catch-error error))
-  (if (some? error)
+  (if (and (not config/dev?) (some? error))
     error-view
     view))
 
 (rum/defc select
-  [options on-change]
-  [:select.mt-1.form-select.block.w-full.px-3.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5.ml-4
-   {:style     {:padding "0 0 0 12px"}
+  [options on-change class]
+  [:select.mt-1.block.px-3.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5.ml-4
+   {:class     (or class "form-select")
+    :style     {:padding "0 0 0 12px"}
     :on-change (fn [e]
                  (let [value (util/evalue e)]
                    (on-change value)))}
@@ -657,3 +659,14 @@
                                html))
                            [:div {:key "tippy"} ""])))
            child)))
+
+(defn slider
+  [default-value {:keys [min max on-change]}]
+  [:input.cursor-pointer
+   {:type  "range"
+    :value (int default-value)
+    :min   min
+    :max   max
+    :style {:width "100%"}
+    :on-change #(let [value (util/evalue %)]
+                  (on-change value))}])

+ 59 - 1
src/workspaces/workspaces/cards.cljs

@@ -5,7 +5,10 @@
             [nubank.workspaces.card-types.test :as ct.test]
             [cljs.test :refer [is async]]
             [rum.core :as rum]
-            [frontend.ui :as ui]))
+            [frontend.ui :as ui]
+            [frontend.extensions.graph :as graph]
+            [frontend.extensions.graph.pixi :as pixi]
+            [cljs-bean.core :as bean]))
 
 ;; simple function to create react elemnents
 (defn element [name props & children]
@@ -24,3 +27,58 @@
 (ws/defcard button-card
   (ct.react/react-card
    (ui-button)))
+
+(rum/defc graph
+  []
+  (graph/graph-2d
+   {:data {:nodes [{:id "a" :label "a"} {:id "b" :label "b"}]
+           :edges [{:source "a" :target "b"}]}
+    :width 150
+    :height 150
+    :fitView true}))
+
+(ws/defcard graph-card
+  (ct.react/react-card
+   (graph)))
+
+(defn- random-graph
+  [n]
+  (let [nodes (for [i (range 0 n)]
+                {:id (str i)
+                 :label (str i)})
+        edges (->
+               (for [i (range 0 (/ n 2))]
+                 (let [source i
+                       target (inc i)]
+                   {:id (str source target)
+                    :source (str source)
+                    :target (str target)}))
+               (distinct))]
+    {:nodes nodes
+     :links edges}))
+
+;; (rum/defc pixi-graph
+;;   []
+;;   (let [{:keys [nodes links]} (random-graph 4000)]
+;;     (pixi/graph (fn []
+;;                   {:nodes nodes
+;;                   :links links
+;;                   :style {:node {:size 15
+;;                                  :color "#666666"
+;;                                  :border {:width 2
+;;                                           :color "#ffffff"}
+;;                                  :label {:content (fn [node] (.-id node))
+;;                                          :type js/window.PixiGraph.TextType.TEXT
+;;                                          :fontSize 12
+;;                                          :color "#333333"
+;;                                          :backgroundColor "rgba(255, 255, 255, 0.5)"
+;;                                          :padding 4}}
+;;                           :edge {:width 1
+;;                                  :color "#cccccc"}}
+;;                   :hover-style {:node {:border {:color "#000000"}
+;;                                        :label {:backgroundColor "rgba(238, 238, 238, 1)"}}
+;;                                 :edge {:color "#999999"}}}))))
+
+;; (ws/defcard pixi-graph-card
+;;   (ct.react/react-card
+;;    (pixi-graph)))

+ 347 - 15
yarn.lock

@@ -1033,6 +1033,170 @@
     "@nodelib/fs.scandir" "2.1.4"
     fastq "^1.6.0"
 
+"@pixi-essentials/cull@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@pixi-essentials/cull/-/cull-1.1.0.tgz#e454b4c2475a8f7bcfb79d751a138a657ca7bb60"
+  integrity sha512-/IrobYs+ECZoYxGnmCbUBIcye0XX9ZRmeO9SLtOGI0DtsF+/r13OrGX42XUo8L76LMWurv78b5LdYxX33NrUNQ==
+
+"@pixi/app@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/app/-/app-6.0.4.tgz#138ce670c057dc351f3b88e6f27ac0fa6edf5cb0"
+  integrity sha512-+BiuaQtnOBR5/Q8+nXnHE2tuZyuBnqy/cwbIR1ImPnKAs7UaCcRLf1R0RvnRFu4KMP4ozTd810p0k84TzIguTA==
+  dependencies:
+    "@pixi/core" "6.0.4"
+    "@pixi/display" "6.0.4"
+
+"@pixi/[email protected]", "@pixi/constants@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/constants/-/constants-6.0.4.tgz#c0fb4ff79a81ad5dd2ce1bb3a1b8b3f2f01e221a"
+  integrity sha512-khwRMfuHVdFk93L+bf0mmCwtSloYlfBfjdseIAbJL+VSpeMG1S2DzCYlMCPdp4mvDLU9LvkH2U2leZGEIx5j7g==
+
+"@pixi/[email protected]", "@pixi/core@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/core/-/core-6.0.4.tgz#81979d49579c3b54df5f3da76d74037d82dd2d4d"
+  integrity sha512-r1ceyAz0z3usUs0uj4u2986vVT2tQixGNin2o9FNhPFDXbN5EaoKHLtrjGBt1iylK/EUH/nfL5zq0SGa/loW0A==
+  dependencies:
+    "@pixi/constants" "6.0.4"
+    "@pixi/math" "6.0.4"
+    "@pixi/runner" "6.0.4"
+    "@pixi/settings" "6.0.4"
+    "@pixi/ticker" "6.0.4"
+    "@pixi/utils" "6.0.4"
+
+"@pixi/[email protected]", "@pixi/display@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/display/-/display-6.0.4.tgz#f192edb57a1d68d0616b2d379284a84490df0411"
+  integrity sha512-v6hjx5Gm5aIlLQ7xrsZ2lstI1cv/MtbWXJOhU8LXckkrHHUvAuJgml3+0pcHw8YLuOlepZngUuiqy/XjceVk8A==
+  dependencies:
+    "@pixi/math" "6.0.4"
+    "@pixi/settings" "6.0.4"
+    "@pixi/utils" "6.0.4"
+
+"@pixi/graphics@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/graphics/-/graphics-6.0.4.tgz#3d2e75acb8145a80275b194413b06fb28505c663"
+  integrity sha512-CybR+DBkGB5llypPeib2A0J13mnPQwlQDqLRhlhXKkYxXQKXlPk5MWA7ZEg+4wKeqUUlrC+k70e5ZFYLC3AgEQ==
+  dependencies:
+    "@pixi/constants" "6.0.4"
+    "@pixi/core" "6.0.4"
+    "@pixi/display" "6.0.4"
+    "@pixi/math" "6.0.4"
+    "@pixi/sprite" "6.0.4"
+    "@pixi/utils" "6.0.4"
+
+"@pixi/interaction@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/interaction/-/interaction-6.0.4.tgz#9b1eaf903997adb55df0068d080ce6c2b4467042"
+  integrity sha512-4+FOKDpiF/+F9r3+y81xTBElcLqI3OpeeI9bkIw9pPHA41riXRQv+m0HWz76bGQK7zDAimAV9K2xff7Wa5nSeg==
+  dependencies:
+    "@pixi/core" "6.0.4"
+    "@pixi/display" "6.0.4"
+    "@pixi/math" "6.0.4"
+    "@pixi/ticker" "6.0.4"
+    "@pixi/utils" "6.0.4"
+
+"@pixi/[email protected]", "@pixi/loaders@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/loaders/-/loaders-6.0.4.tgz#1d7dc1aa762627396e63d0bc1276e134d12c2ef3"
+  integrity sha512-cw8QSkn8l8P06fINfwCZW+vUdhtOJ5G+T2qQm3HIDgI/J1tAsiRj3ufHop8xkHwYXrUeTf1LTqw+QdlZEVpJfg==
+  dependencies:
+    "@pixi/constants" "6.0.4"
+    "@pixi/core" "6.0.4"
+    "@pixi/utils" "6.0.4"
+    resource-loader "^3.0.1"
+
+"@pixi/[email protected]", "@pixi/math@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/math/-/math-6.0.4.tgz#360a8edd0dcbb7ff3a3053cf2dad9227a95e36f4"
+  integrity sha512-UwZ72CeZ2KsS4IlcEXgNiuD88omPk42Dct74+1G+R2+yPI+XRZq+hGQRTle/BbFYjxh9ccdQVyX9ToGv1XTd6Q==
+
+"@pixi/[email protected]":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/mesh/-/mesh-6.0.4.tgz#f9b211238289a384a56f8f1abfb8693cbc5ca4e7"
+  integrity sha512-uE1Qs4mXy0QVV3yjxlNeqthkXGS6Hkt5uR1fwrvdqxlQRkX69nRq+GZfInuRYDWqwAsl8eZWs7f+pLRDT+HFbA==
+  dependencies:
+    "@pixi/constants" "6.0.4"
+    "@pixi/core" "6.0.4"
+    "@pixi/display" "6.0.4"
+    "@pixi/math" "6.0.4"
+    "@pixi/settings" "6.0.4"
+    "@pixi/utils" "6.0.4"
+
+"@pixi/mixin-get-child-by-name@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-6.0.4.tgz#e6cef2094e913d5f3e119a2d6289888553392459"
+  integrity sha512-scUMBHlOmW0hpjltn4UCihJZvz3ysDYIW35ma9p9Lso2D9qKjsZpojQ6mc75FVWz53T0BjUmLW8LHA86Jic6MQ==
+  dependencies:
+    "@pixi/display" "6.0.4"
+
+"@pixi/[email protected]":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/runner/-/runner-6.0.4.tgz#74d595169c2e42f10235be75dbeb0213bcd3f039"
+  integrity sha512-ta6r36r2vC+fPB27URpSacPGQDtbJbdUoeGCJWAEwX+QI4vx4C9NYAcB0bIg8TLXiigCfA6by/RMnJ0dBiemFA==
+
+"@pixi/[email protected]":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/settings/-/settings-6.0.4.tgz#dd93f6ebce63134c47afd093466fcb911fc9e066"
+  integrity sha512-djiIsmULDwcHWNmEiZKm4zyVopu1NL+fClnbBmtDkGZw7nm37y6dOcdpYawJcxvE4/KLm6pspBiRTnrzdlqW7Q==
+  dependencies:
+    ismobilejs "^1.1.0"
+
+"@pixi/[email protected]", "@pixi/sprite@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/sprite/-/sprite-6.0.4.tgz#71af3d0fbbe6752db4b4e3b2c69192bccadcfbb3"
+  integrity sha512-6yMoHmfFhSRERLM1PUXceq9e6e1UH0YJkLoPVLv6gxMunfk6jPXeO8p9dDS2FQ8ZMSkO/16BKq27HIMKvF6Cvg==
+  dependencies:
+    "@pixi/constants" "6.0.4"
+    "@pixi/core" "6.0.4"
+    "@pixi/display" "6.0.4"
+    "@pixi/math" "6.0.4"
+    "@pixi/settings" "6.0.4"
+    "@pixi/utils" "6.0.4"
+
+"@pixi/text-bitmap@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/text-bitmap/-/text-bitmap-6.0.4.tgz#29d468b415e8d40056eb84ba4f2e0fc6b7a3441c"
+  integrity sha512-Nh2PXixqF0LFJ0xwmTib2HVWdhgsHn+dSYMVIec8LndDFQMTBw+X2XP1iHjVm0xhqOVdZI+Qfb2Trc0j2lINrw==
+  dependencies:
+    "@pixi/core" "6.0.4"
+    "@pixi/display" "6.0.4"
+    "@pixi/loaders" "6.0.4"
+    "@pixi/math" "6.0.4"
+    "@pixi/mesh" "6.0.4"
+    "@pixi/settings" "6.0.4"
+    "@pixi/text" "6.0.4"
+    "@pixi/utils" "6.0.4"
+
+"@pixi/[email protected]", "@pixi/text@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/text/-/text-6.0.4.tgz#d3ce6046ecbbc226fa694516856e07d76460c3c8"
+  integrity sha512-r9UJg8ivWvvS7nNyBaZBKX5zg5UCU37dIYbKXcHyiXnOvXO22tiQBfkPBrZCueeLXRouC9sHmDFya8rb5TE9HA==
+  dependencies:
+    "@pixi/core" "6.0.4"
+    "@pixi/math" "6.0.4"
+    "@pixi/settings" "6.0.4"
+    "@pixi/sprite" "6.0.4"
+    "@pixi/utils" "6.0.4"
+
+"@pixi/[email protected]", "@pixi/ticker@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/ticker/-/ticker-6.0.4.tgz#ff046308e5de119b8de22c5ad56c4cb37923e54f"
+  integrity sha512-PkFfPP5vHlgnApLks0Ia0okmFu6KPqBdIyquDqHJAcBdgljedm32KS6K2EH37xelBOzYHScjZ2SQGiiebVfClw==
+  dependencies:
+    "@pixi/settings" "6.0.4"
+
+"@pixi/[email protected]", "@pixi/utils@^6.0.2":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@pixi/utils/-/utils-6.0.4.tgz#374f642195d6aef66fe67c78918a73a09a76dfe6"
+  integrity sha512-35JTWsAJ8Va0vvtUSQvyOr3kGedGKVuJnHDO89B8C8tSFtMpJYrR44vp1b1p1vOjNak+ulGehZc8LzlCqymViQ==
+  dependencies:
+    "@pixi/constants" "6.0.4"
+    "@pixi/settings" "6.0.4"
+    "@types/earcut" "^2.1.0"
+    earcut "^2.2.2"
+    eventemitter3 "^3.1.0"
+    url "^0.11.0"
+
 "@popperjs/core@^2.8.3":
   version "2.9.2"
   resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
@@ -1148,6 +1312,11 @@
   dependencies:
     tippy.js "^6.3.1"
 
+"@types/earcut@^2.1.0":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@types/earcut/-/earcut-2.1.1.tgz#573a0af609f17005c751f6f4ffec49cfe358ea51"
+  integrity sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==
+
 "@types/expect@^1.20.4":
   version "1.20.4"
   resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz"
@@ -1163,7 +1332,7 @@
 
 "@types/glob@*":
   version "7.1.3"
-  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
   integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==
   dependencies:
     "@types/minimatch" "*"
@@ -1328,6 +1497,11 @@ ajv@^7.0.2:
     require-from-string "^2.0.2"
     uri-js "^4.2.2"
 
+almost-equal@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/almost-equal/-/almost-equal-1.1.0.tgz#f851c631138757994276aa2efbe8dfa3066cccdd"
+  integrity sha1-+FHGMROHV5lCdqou++jfowZszN0=
+
 alphanum-sort@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz"
@@ -1618,7 +1792,7 @@ atob@^2.1.2:
 
 autoprefixer@^9.8.6:
   version "9.8.6"
-  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz"
+  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
   integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==
   dependencies:
     browserslist "^4.12.0"
@@ -2059,7 +2233,7 @@ builtin-status-codes@^3.0.0:
 
 bytes@^3.0.0:
   version "3.1.0"
-  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 
 cache-base@^1.0.1:
@@ -2338,7 +2512,7 @@ [email protected], classnames@^2.2.5:
 
 [email protected]:
   version "4.2.3"
-  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz"
+  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
   integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==
   dependencies:
     source-map "~0.6.0"
@@ -2518,6 +2692,29 @@ color-name@^1.0.0, color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
+color-parse@^1.4.1:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/color-parse/-/color-parse-1.4.2.tgz#78651f5d34df1a57f997643d86f7f87268ad4eb5"
+  integrity sha512-RI7s49/8yqDj3fECFZjUI1Yi0z/Gq1py43oNJivAIIDSyJiOZLfYCRQEgn8HEVAj++PcRe8AnL2XF0fRJ3BTnA==
+  dependencies:
+    color-name "^1.0.0"
+
+color-rgba@^2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-2.2.3.tgz#f7f1240de7edc5fcafa79c4acb013a8eb2075aa0"
+  integrity sha512-C20bgnIy09NoXDzhu3RB/SHVlk0y+2zcnkumpVvGOWCrz3rF2xJLS53Fc2ai2Jebs3X7ILZFswN7vVLD2HLr2g==
+  dependencies:
+    color-parse "^1.4.1"
+    color-space "^1.14.6"
+
+color-space@^1.14.6:
+  version "1.16.0"
+  resolved "https://registry.yarnpkg.com/color-space/-/color-space-1.16.0.tgz#611781bca41cd8582a1466fd9e28a7d3d89772a2"
+  integrity sha512-A6WMiFzunQ8KEPFmj02OnnoUnqhmSaHaZ/0LVFcPTdlvm8+3aMJ5x1HRHy3bDHPkovkf4sS0f4wsVvwk71fKkg==
+  dependencies:
+    hsluv "^0.0.3"
+    mumath "^3.3.4"
+
 color-string@^1.5.4:
   version "1.5.5"
   resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
@@ -2744,7 +2941,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
 
 cross-spawn@^7.0.0, cross-spawn@^7.0.1:
   version "7.0.3"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
   dependencies:
     path-key "^3.1.0"
@@ -2980,6 +3177,30 @@ cypress@^7.5.0:
     url "^0.11.0"
     yauzl "^2.10.0"
 
+"d3-dispatch@1 - 3":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+  integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+d3-force@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
+  integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-quadtree "1 - 3"
+    d3-timer "1 - 3"
+
+"d3-quadtree@1 - 3":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
+  integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
+
+"d3-timer@1 - 3":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+  integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
 d@1, d@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz"
@@ -3075,6 +3296,11 @@ decompress-response@^3.3.0:
   dependencies:
     mimic-response "^1.0.0"
 
+deepmerge@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
+  integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+
 default-compare@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz"
@@ -3290,7 +3516,7 @@ duplexer3@^0.1.4:
 
 duplexify@^3.6.0:
   version "3.7.1"
-  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz"
+  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
   integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==
   dependencies:
     end-of-stream "^1.0.0"
@@ -3306,6 +3532,11 @@ each-props@^1.3.0:
     is-plain-object "^2.0.1"
     object.defaults "^1.1.0"
 
+earcut@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.2.tgz#41b0bc35f63e0fe80da7cddff28511e7e2e80d11"
+  integrity sha512-eZoZPPJcUHnfRZ0PjLvx2qBordSiO8ofC3vt+qACLM95u+4DovnbYNpQtJh0DNsWj8RnxrQytD4WA8gj5cRIaQ==
+
 ecc-jsbn@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -3456,7 +3687,7 @@ es6-error@^4.1.1:
 
 es6-iterator@^2.0.1, es6-iterator@^2.0.3, es6-iterator@~2.0.3:
   version "2.0.3"
-  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
   integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
   dependencies:
     d "1"
@@ -3506,6 +3737,11 @@ eventemitter2@^6.4.3:
   resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b"
   integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==
 
+eventemitter3@^3.1.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
+  integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
+
 events@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/events/-/events-2.1.0.tgz#2a9a1e18e6106e0e812aa9ebd4a819b3c29c0ba5"
@@ -3516,6 +3752,11 @@ events@^3.0.0:
   resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz"
   integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
 
+events@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+  integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
 evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz"
@@ -3865,7 +4106,7 @@ flatted@^3.1.0:
 
 flush-write-stream@^1.0.2:
   version "1.1.1"
-  resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
   integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==
   dependencies:
     inherits "^2.0.3"
@@ -4005,7 +4246,7 @@ get-caller-file@^1.0.1:
 
 get-caller-file@^2.0.5:
   version "2.0.5"
-  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
 get-intrinsic@^1.0.2:
@@ -4168,7 +4409,7 @@ global-modules@^1.0.0:
 
 global-modules@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
   integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==
   dependencies:
     global-prefix "^3.0.0"
@@ -4268,6 +4509,14 @@ graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6,
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.5.tgz"
   integrity sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw==
 
+graphology@^0.20.0:
+  version "0.20.0"
+  resolved "https://registry.yarnpkg.com/graphology/-/graphology-0.20.0.tgz#e29f7a1448852dfc7195646241f336b10127997a"
+  integrity sha512-h5mJWgZXpuZQzBAMWhVIOluGNIYUP043eh7BHcecRWhSkD4eiZAh+uM4XX2rGL1vig9XQ3JVYl9gmK+ajy+JPA==
+  dependencies:
+    events "^3.3.0"
+    obliterator "^1.6.1"
+
 gulp-cached@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/gulp-cached/-/gulp-cached-1.1.1.tgz"
@@ -4485,6 +4734,11 @@ hsla-regex@^1.0.0:
   resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz"
   integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
 
+hsluv@^0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/hsluv/-/hsluv-0.0.3.tgz#829107dafb4a9f8b52a1809ed02e091eade6754c"
+  integrity sha1-gpEH2vtKn4tSoYCe0C4JHq3mdUw=
+
 html-comment-regex@^1.1.0, html-comment-regex@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz"
@@ -4940,7 +5194,7 @@ is-obj@^2.0.0:
 
 is-path-cwd@^2.2.0:
   version "2.2.0"
-  resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb"
   integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==
 
 is-path-inside@^3.0.2:
@@ -4950,7 +5204,7 @@ is-path-inside@^3.0.2:
 
 is-plain-obj@^1.1.0:
   version "1.1.0"
-  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
   integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
 
 is-plain-obj@^2.0.0:
@@ -5058,6 +5312,11 @@ isexe@^2.0.0:
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz"
   integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
 
+ismobilejs@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ismobilejs/-/ismobilejs-1.1.1.tgz#c56ca0ae8e52b24ca0f22ba5ef3215a2ddbbaa0e"
+  integrity sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==
+
 isobject@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz"
@@ -5729,6 +5988,11 @@ min-indent@^1.0.0:
   resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz"
   integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
 
+mini-signals@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mini-signals/-/mini-signals-1.2.0.tgz#45b08013c5fae51a24aa1a935cd317c9ed721d74"
+  integrity sha1-RbCAE8X65RokqhqTXNMXye1yHXQ=
+
 mini-svg-data-uri@^1.0.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.2.3.tgz"
@@ -5833,6 +6097,13 @@ ms@^2.1.1:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
+mumath@^3.3.4:
+  version "3.3.4"
+  resolved "https://registry.yarnpkg.com/mumath/-/mumath-3.3.4.tgz#48d4a0f0fd8cad4e7b32096ee89b161a63d30bbf"
+  integrity sha1-SNSg8P2MrU57Mglu6JsWGmPTC78=
+  dependencies:
+    almost-equal "^1.1.0"
+
 mute-stdout@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz"
@@ -6140,6 +6411,11 @@ object.values@^1.1.0:
     es-abstract "^1.18.0-next.1"
     has "^1.0.3"
 
+obliterator@^1.6.1:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3"
+  integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==
+
 once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz"
@@ -6338,6 +6614,11 @@ parse-passwd@^1.0.0:
   resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz"
   integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
 
+parse-uri@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/parse-uri/-/parse-uri-1.0.3.tgz#f3c24a74907a4e357c1741e96ca9faadecfd6db5"
+  integrity sha512-upMnGxNcm+45So85HoguwZTVZI9u11i36DdxJfGF2HYWS2eh3TIx7+/tTi7qrEq15qzGkVhsKjesau+kCk48pA==
+
 pascalcase@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz"
@@ -6452,6 +6733,11 @@ pend@~1.2.0:
   resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz"
   integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
 
+penner@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/penner/-/penner-0.1.3.tgz#0b8b482d4e9b39af2f3d7c37592229b8acc29705"
+  integrity sha1-C4tILU6bOa8vPXw3WSIpuKzClwU=
+
 performance-now@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -6489,6 +6775,39 @@ pinkie@^2.0.0:
   resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz"
   integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
 
+pixi-graph-fork@^0.0.9:
+  version "0.0.9"
+  resolved "https://registry.yarnpkg.com/pixi-graph-fork/-/pixi-graph-fork-0.0.9.tgz#1eed19fb216da2ba618e7deaefef4194571a1e1b"
+  integrity sha512-N9j7BjCShk+3/beCT0ug70IVdzXQnXDvhGPAk3L1hFhcVnW/pyTpnXDGAIXpjFHZwU+Y/YrjMFFeRa1Hj3OcIA==
+  dependencies:
+    "@pixi-essentials/cull" "^1.1.0"
+    "@pixi/app" "^6.0.2"
+    "@pixi/constants" "^6.0.2"
+    "@pixi/core" "^6.0.2"
+    "@pixi/display" "^6.0.2"
+    "@pixi/graphics" "^6.0.2"
+    "@pixi/interaction" "^6.0.2"
+    "@pixi/loaders" "^6.0.2"
+    "@pixi/math" "^6.0.2"
+    "@pixi/mixin-get-child-by-name" "^6.0.2"
+    "@pixi/sprite" "^6.0.2"
+    "@pixi/text" "^6.0.2"
+    "@pixi/text-bitmap" "^6.0.2"
+    "@pixi/ticker" "^6.0.2"
+    "@pixi/utils" "^6.0.2"
+    color-rgba "^2.2.3"
+    deepmerge "^4.2.2"
+    events "^3.3.0"
+    pixi-viewport "^4.30.4"
+    tiny-typed-emitter "^2.0.3"
+
+pixi-viewport@^4.30.4:
+  version "4.31.0"
+  resolved "https://registry.yarnpkg.com/pixi-viewport/-/pixi-viewport-4.31.0.tgz#eb090e790fcdd51992b794d80eca21820ce95249"
+  integrity sha512-VnI080My0dBN5b+JQEOdhSEr3zX4jD0CchMxhBfpj2BQoPIUppxTsi/R/kEdQ5ZnjGCn76/k0Q0xeW3WbEKb7g==
+  dependencies:
+    penner "^0.1.3"
+
 [email protected]:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz"
@@ -7035,7 +7354,7 @@ pump@^3.0.0:
 
 pumpify@^1.3.5:
   version "1.5.1"
-  resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz"
+  resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
   integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
   dependencies:
     duplexify "^3.6.0"
@@ -7592,6 +7911,14 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.17.0, resolve@^1.4.0
     is-core-module "^2.1.0"
     path-parse "^1.0.6"
 
+resource-loader@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/resource-loader/-/resource-loader-3.0.1.tgz#33355bb5421e2994f59454bbc7f6dbff8df06d47"
+  integrity sha512-fBuCRbEHdLCI1eglzQhUv9Rrdcmqkydr1r6uHE2cYHvRBrcLXeSmbE/qI/urFt8rPr/IGxir3BUwM5kUK8XoyA==
+  dependencies:
+    mini-signals "^1.2.0"
+    parse-uri "^1.0.0"
+
 responselike@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz"
@@ -7800,7 +8127,7 @@ [email protected]:
     which "^1.3.1"
     ws "^3.0.0"
 
-shadow-cljs@^2.12.5:
[email protected]:
   version "2.12.5"
   resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4"
   integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ==
@@ -8541,6 +8868,11 @@ timsort@^0.3.0:
   resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz"
   integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
 
+tiny-typed-emitter@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.0.3.tgz#4335e3a75127ae7faba91b02e91615d97dc8db7d"
+  integrity sha512-MaCqhHlp6EAWN25yqBlajgd4scxxI2eJr7+EgoUAOV9UkMU3us/yp2bEnc2yOvyeDF8TUWuaz3zZCPGTKFJIpA==
+
 tippy.js@^6.3.1:
   version "6.3.1"
   resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.1.tgz#3788a007be7015eee0fd589a66b98fb3f8f10181"
@@ -9180,7 +9512,7 @@ wrappy@1:
 
 write-file-atomic@^3.0.3:
   version "3.0.3"
-  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
   integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
   dependencies:
     imurmurhash "^0.1.4"