Fase B5: temporal/linear graph view mode
Adds a third :temporal view mode alongside :tags-and-objects and :all-pages. The backend reuses build-all-pages-graph's node/link shape unchanged (every node already carries :block/created-at); only the client-side pixi layout differs, arranging nodes along a fixed chronological timeline (oldest first, staggered across a few lanes) instead of running the force simulation, following the existing grid-layout fixed-position precedent.
This commit is contained in:
@@ -35,8 +35,9 @@
|
||||
|
||||
(defn- valid-view-mode
|
||||
[view-mode]
|
||||
(if (= view-mode :all-pages)
|
||||
:all-pages
|
||||
(case view-mode
|
||||
:all-pages :all-pages
|
||||
:temporal :temporal
|
||||
:tags-and-objects))
|
||||
|
||||
(defn- clamp-number
|
||||
@@ -335,7 +336,13 @@
|
||||
:class "graph-mode-tab"}
|
||||
(view-mode-tab-label
|
||||
(t :graph/view-mode-all-pages)
|
||||
(and loading? (= view-mode :all-pages))))))}))
|
||||
(and loading? (= view-mode :all-pages))))
|
||||
(shui/tabs-trigger
|
||||
{:value "temporal"
|
||||
:class "graph-mode-tab"}
|
||||
(view-mode-tab-label
|
||||
(t :graph/view-mode-temporal)
|
||||
(and loading? (= view-mode :temporal))))))}))
|
||||
|
||||
(defn- tags-group
|
||||
[settings set-settings! available-tags selected-tag-ids tag-query set-tag-query!]
|
||||
@@ -430,34 +437,37 @@
|
||||
link-distance (valid-link-distance (:link-distance settings))
|
||||
grid-layout? (true? (:grid-layout? settings))
|
||||
show-journals? (not (false? (:show-journals? settings)))
|
||||
depth-disabled? (depth-control-disabled? selected-nodes)]
|
||||
depth-disabled? (depth-control-disabled? selected-nodes)
|
||||
temporal? (= view-mode :temporal)]
|
||||
(settings-group
|
||||
{:settings settings
|
||||
:set-settings! set-settings!
|
||||
:id :layout
|
||||
:title (t :graph/layout)
|
||||
:meta (t :graph/layout-force)
|
||||
:meta (if temporal? (t :graph/layout-temporal) (t :graph/layout-force))
|
||||
:children [:div.graph-layout-controls
|
||||
[:div.graph-layout-stats
|
||||
[:span (t :graph/node-count (count (:nodes graph-data)))]
|
||||
[:span (t :graph/link-count (count (:links graph-data)))]]
|
||||
(layout-slider
|
||||
{:label (t :graph/layout-depth)
|
||||
:value depth
|
||||
:min 1
|
||||
:max 5
|
||||
:step 1
|
||||
:disabled? depth-disabled?
|
||||
:on-change #(set-settings! (fn [settings]
|
||||
(assoc settings :depth (valid-depth %))))})
|
||||
(layout-slider
|
||||
{:label (t :graph/layout-link-distance)
|
||||
:value link-distance
|
||||
:min 36
|
||||
:max 180
|
||||
:step 6
|
||||
:on-change #(set-settings! (fn [settings]
|
||||
(assoc settings :link-distance (valid-link-distance %))))})
|
||||
(when-not temporal?
|
||||
[:<>
|
||||
(layout-slider
|
||||
{:label (t :graph/layout-depth)
|
||||
:value depth
|
||||
:min 1
|
||||
:max 5
|
||||
:step 1
|
||||
:disabled? depth-disabled?
|
||||
:on-change #(set-settings! (fn [settings]
|
||||
(assoc settings :depth (valid-depth %))))})
|
||||
(layout-slider
|
||||
{:label (t :graph/layout-link-distance)
|
||||
:value link-distance
|
||||
:min 36
|
||||
:max 180
|
||||
:step 6
|
||||
:on-change #(set-settings! (fn [settings]
|
||||
(assoc settings :link-distance (valid-link-distance %))))})])
|
||||
(when (= view-mode :tags-and-objects)
|
||||
[:<>
|
||||
(layout-toggle
|
||||
|
||||
@@ -483,6 +483,7 @@
|
||||
(case view-mode
|
||||
:all-pages :all-pages
|
||||
:page :page
|
||||
:temporal :temporal
|
||||
:tags-and-objects))
|
||||
|
||||
(defn labels-visible-by-default?
|
||||
@@ -1000,6 +1001,26 @@
|
||||
(vec (concat linked-nodes
|
||||
(place-isolated-ring-nodes isolated degree dark? isolated-start-radius)))))
|
||||
|
||||
(def ^:private temporal-lane-count 6)
|
||||
(def ^:private temporal-node-gap 46)
|
||||
(def ^:private temporal-lane-gap 30)
|
||||
|
||||
(defn- temporal-layout-nodes
|
||||
"Lays out nodes along a fixed, linear timeline ordered by :block/created-at
|
||||
(oldest first), instead of running the force simulation. Nodes without a
|
||||
timestamp sort first. Staggered across a few lanes on the y-axis so nodes
|
||||
created close together don't fully overlap."
|
||||
[nodes degree dark?]
|
||||
(->> nodes
|
||||
(sort-by (fn [node] [(or (:block/created-at node) 0) (str (:id node))]))
|
||||
(map-indexed
|
||||
(fn [idx node]
|
||||
(let [lane (mod idx temporal-lane-count)
|
||||
x (* idx temporal-node-gap)
|
||||
y (* lane temporal-lane-gap)]
|
||||
(decorate-node node degree dark? x y))))
|
||||
vec))
|
||||
|
||||
(defn- all-pages-fast-layout-nodes
|
||||
[nodes degree dark?]
|
||||
(let [node-count (count nodes)
|
||||
@@ -1383,8 +1404,17 @@
|
||||
mode (layout-mode (count nodes) view-mode)
|
||||
fast-all-pages? (and (= :fast mode)
|
||||
(= view-mode :all-pages))]
|
||||
(if fast-all-pages?
|
||||
(cond
|
||||
(= view-mode :temporal)
|
||||
(let [node-id-set (set (map :id nodes))
|
||||
filtered-links (keep-links-with-nodes links node-id-set)
|
||||
degree (build-degree-map filtered-links)]
|
||||
(temporal-layout-nodes nodes degree dark?))
|
||||
|
||||
fast-all-pages?
|
||||
(all-pages-fast-layout-nodes nodes (build-fast-degree-map nodes links) dark?)
|
||||
|
||||
:else
|
||||
(let [node-id-set (set (map :id nodes))
|
||||
filtered-links (keep-links-with-nodes links node-id-set)
|
||||
degree (build-degree-map filtered-links)
|
||||
|
||||
@@ -48,8 +48,9 @@
|
||||
|
||||
(defn- normalize-view-mode
|
||||
[view-mode]
|
||||
(if (= view-mode :all-pages)
|
||||
:all-pages
|
||||
(case view-mode
|
||||
:all-pages :all-pages
|
||||
:temporal :temporal
|
||||
:tags-and-objects))
|
||||
|
||||
(def ^:private large-all-pages-fast-threshold 10000)
|
||||
@@ -923,7 +924,11 @@
|
||||
result (case view-mode
|
||||
:tags-and-objects
|
||||
(build-tags-and-objects-graph db)
|
||||
:all-pages
|
||||
(:all-pages :temporal)
|
||||
;; :temporal reuses the same node/link shape as :all-pages
|
||||
;; (every node already carries :block/created-at); only the
|
||||
;; client-side layout differs, arranging nodes on a fixed
|
||||
;; timeline instead of running the force simulation.
|
||||
(build-all-pages-graph db opts))]
|
||||
(assoc result :meta {:view-mode view-mode})))
|
||||
|
||||
|
||||
@@ -750,6 +750,7 @@
|
||||
:graph/layout-force "Force graph"
|
||||
:graph/layout-grid-layout "Grid layout"
|
||||
:graph/layout-link-distance "Link distance"
|
||||
:graph/layout-temporal "Timeline"
|
||||
:graph/layout-show-journals "Show journals"
|
||||
:graph/leave-action "Leave this graph"
|
||||
:graph/leave-confirm-desc "Are you sure you want to leave this graph?"
|
||||
@@ -787,6 +788,7 @@
|
||||
:graph/view-mode "View mode"
|
||||
:graph/view-mode-all-pages "All pages"
|
||||
:graph/view-mode-tags "Tags"
|
||||
:graph/view-mode-temporal "Timeline"
|
||||
|
||||
:graph.diagnostics/checksum-failed-error "Failed to compute graph checksum diagnostics."
|
||||
:graph.diagnostics/checksum-recomputed-success "Checksum recomputed. Recomputed: {1}, local: {2}, remote: {3}. Downloaded {4}.edn with {5} blocks and checksum attributes {6}."
|
||||
|
||||
17
src/test/frontend/extensions/graph/pixi/logic_test.cljs
Normal file
17
src/test/frontend/extensions/graph/pixi/logic_test.cljs
Normal file
@@ -0,0 +1,17 @@
|
||||
(ns frontend.extensions.graph.pixi.logic-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[frontend.extensions.graph.pixi.logic :as logic]))
|
||||
|
||||
(deftest temporal-view-mode-lays-out-nodes-on-a-fixed-timeline
|
||||
(let [nodes [{:id "a" :kind "page" :block/created-at 300}
|
||||
{:id "b" :kind "page" :block/created-at 100}
|
||||
{:id "c" :kind "page" :block/created-at 200}]
|
||||
links []
|
||||
result (logic/layout-nodes nodes links :temporal false)
|
||||
by-id (into {} (map (juxt :id identity) result))]
|
||||
(testing "nodes are ordered chronologically along the x-axis, oldest first"
|
||||
(is (< (:x (by-id "b")) (:x (by-id "c")) (:x (by-id "a")))))
|
||||
(testing "every node gets a fixed position and radius, no force simulation"
|
||||
(is (every? #(and (number? (:x %)) (number? (:y %)) (number? (:radius %))) result)))
|
||||
(testing "re-running layout for the same input is deterministic"
|
||||
(is (= result (logic/layout-nodes nodes links :temporal false))))))
|
||||
Reference in New Issue
Block a user