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
|
(defn- valid-view-mode
|
||||||
[view-mode]
|
[view-mode]
|
||||||
(if (= view-mode :all-pages)
|
(case view-mode
|
||||||
:all-pages
|
:all-pages :all-pages
|
||||||
|
:temporal :temporal
|
||||||
:tags-and-objects))
|
:tags-and-objects))
|
||||||
|
|
||||||
(defn- clamp-number
|
(defn- clamp-number
|
||||||
@@ -335,7 +336,13 @@
|
|||||||
:class "graph-mode-tab"}
|
:class "graph-mode-tab"}
|
||||||
(view-mode-tab-label
|
(view-mode-tab-label
|
||||||
(t :graph/view-mode-all-pages)
|
(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
|
(defn- tags-group
|
||||||
[settings set-settings! available-tags selected-tag-ids tag-query set-tag-query!]
|
[settings set-settings! available-tags selected-tag-ids tag-query set-tag-query!]
|
||||||
@@ -430,17 +437,20 @@
|
|||||||
link-distance (valid-link-distance (:link-distance settings))
|
link-distance (valid-link-distance (:link-distance settings))
|
||||||
grid-layout? (true? (:grid-layout? settings))
|
grid-layout? (true? (:grid-layout? settings))
|
||||||
show-journals? (not (false? (:show-journals? 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-group
|
||||||
{:settings settings
|
{:settings settings
|
||||||
:set-settings! set-settings!
|
:set-settings! set-settings!
|
||||||
:id :layout
|
:id :layout
|
||||||
:title (t :graph/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
|
:children [:div.graph-layout-controls
|
||||||
[:div.graph-layout-stats
|
[:div.graph-layout-stats
|
||||||
[:span (t :graph/node-count (count (:nodes graph-data)))]
|
[:span (t :graph/node-count (count (:nodes graph-data)))]
|
||||||
[:span (t :graph/link-count (count (:links graph-data)))]]
|
[:span (t :graph/link-count (count (:links graph-data)))]]
|
||||||
|
(when-not temporal?
|
||||||
|
[:<>
|
||||||
(layout-slider
|
(layout-slider
|
||||||
{:label (t :graph/layout-depth)
|
{:label (t :graph/layout-depth)
|
||||||
:value depth
|
:value depth
|
||||||
@@ -457,7 +467,7 @@
|
|||||||
:max 180
|
:max 180
|
||||||
:step 6
|
:step 6
|
||||||
:on-change #(set-settings! (fn [settings]
|
:on-change #(set-settings! (fn [settings]
|
||||||
(assoc settings :link-distance (valid-link-distance %))))})
|
(assoc settings :link-distance (valid-link-distance %))))})])
|
||||||
(when (= view-mode :tags-and-objects)
|
(when (= view-mode :tags-and-objects)
|
||||||
[:<>
|
[:<>
|
||||||
(layout-toggle
|
(layout-toggle
|
||||||
|
|||||||
@@ -483,6 +483,7 @@
|
|||||||
(case view-mode
|
(case view-mode
|
||||||
:all-pages :all-pages
|
:all-pages :all-pages
|
||||||
:page :page
|
:page :page
|
||||||
|
:temporal :temporal
|
||||||
:tags-and-objects))
|
:tags-and-objects))
|
||||||
|
|
||||||
(defn labels-visible-by-default?
|
(defn labels-visible-by-default?
|
||||||
@@ -1000,6 +1001,26 @@
|
|||||||
(vec (concat linked-nodes
|
(vec (concat linked-nodes
|
||||||
(place-isolated-ring-nodes isolated degree dark? isolated-start-radius)))))
|
(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
|
(defn- all-pages-fast-layout-nodes
|
||||||
[nodes degree dark?]
|
[nodes degree dark?]
|
||||||
(let [node-count (count nodes)
|
(let [node-count (count nodes)
|
||||||
@@ -1383,8 +1404,17 @@
|
|||||||
mode (layout-mode (count nodes) view-mode)
|
mode (layout-mode (count nodes) view-mode)
|
||||||
fast-all-pages? (and (= :fast mode)
|
fast-all-pages? (and (= :fast mode)
|
||||||
(= view-mode :all-pages))]
|
(= 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?)
|
(all-pages-fast-layout-nodes nodes (build-fast-degree-map nodes links) dark?)
|
||||||
|
|
||||||
|
:else
|
||||||
(let [node-id-set (set (map :id nodes))
|
(let [node-id-set (set (map :id nodes))
|
||||||
filtered-links (keep-links-with-nodes links node-id-set)
|
filtered-links (keep-links-with-nodes links node-id-set)
|
||||||
degree (build-degree-map filtered-links)
|
degree (build-degree-map filtered-links)
|
||||||
|
|||||||
@@ -48,8 +48,9 @@
|
|||||||
|
|
||||||
(defn- normalize-view-mode
|
(defn- normalize-view-mode
|
||||||
[view-mode]
|
[view-mode]
|
||||||
(if (= view-mode :all-pages)
|
(case view-mode
|
||||||
:all-pages
|
:all-pages :all-pages
|
||||||
|
:temporal :temporal
|
||||||
:tags-and-objects))
|
:tags-and-objects))
|
||||||
|
|
||||||
(def ^:private large-all-pages-fast-threshold 10000)
|
(def ^:private large-all-pages-fast-threshold 10000)
|
||||||
@@ -923,7 +924,11 @@
|
|||||||
result (case view-mode
|
result (case view-mode
|
||||||
:tags-and-objects
|
:tags-and-objects
|
||||||
(build-tags-and-objects-graph db)
|
(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))]
|
(build-all-pages-graph db opts))]
|
||||||
(assoc result :meta {:view-mode view-mode})))
|
(assoc result :meta {:view-mode view-mode})))
|
||||||
|
|
||||||
|
|||||||
@@ -750,6 +750,7 @@
|
|||||||
:graph/layout-force "Force graph"
|
:graph/layout-force "Force graph"
|
||||||
:graph/layout-grid-layout "Grid layout"
|
:graph/layout-grid-layout "Grid layout"
|
||||||
:graph/layout-link-distance "Link distance"
|
:graph/layout-link-distance "Link distance"
|
||||||
|
:graph/layout-temporal "Timeline"
|
||||||
:graph/layout-show-journals "Show journals"
|
:graph/layout-show-journals "Show journals"
|
||||||
:graph/leave-action "Leave this graph"
|
:graph/leave-action "Leave this graph"
|
||||||
:graph/leave-confirm-desc "Are you sure you want to 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 "View mode"
|
||||||
:graph/view-mode-all-pages "All pages"
|
:graph/view-mode-all-pages "All pages"
|
||||||
:graph/view-mode-tags "Tags"
|
:graph/view-mode-tags "Tags"
|
||||||
|
:graph/view-mode-temporal "Timeline"
|
||||||
|
|
||||||
:graph.diagnostics/checksum-failed-error "Failed to compute graph checksum diagnostics."
|
: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}."
|
: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