FossilRepo

Improve DAG graph: proper active rail tracking through all rows - Precompute line spans: each checkin's line extends from its row to its parent's row across all intermediate rows - Active rails computed by checking which spans cover each row - Vertical lines properly persist through rows where branches are active - Fork/merge connectors drawn where parent is on a different rail - Fixes branches appearing disconnected in the timeline

lmata 2026-04-06 21:08 trunk
Commit 04985a1f1d0a8871ac7628013002226ae771f3802055d179450205915fc7b394
1 file changed +41 -37
+41 -37
--- fossil/views.py
+++ fossil/views.py
@@ -811,69 +811,73 @@
811811
812812
813813
def _compute_dag_graph(entries):
814814
"""Compute DAG graph positions for timeline entries.
815815
816
- Returns a list of dicts wrapping each entry with graph rendering data:
817
- - node_x: pixel x position of the node
818
- - lines: list of (x1, x2) connections to draw between this row and the next
816
+ Tracks active rails through each row and draws fork/merge connectors
817
+ where a child is on a different rail than its parent.
819818
"""
820
- rail_pitch = 16 # pixels between rails
821
- rail_offset = 20 # left margin
819
+ rail_pitch = 16
820
+ rail_offset = 20
821
+ max_rail = max((e.rail for e in entries if e.rail >= 0), default=0)
822
+ graph_width = rail_offset + (max_rail + 2) * rail_pitch
822823
823
- # Build rid-to-index lookup for connecting lines
824
+ # Build rid-to-index and rid-to-rail lookups
824825
rid_to_idx = {}
826
+ rid_to_rail = {}
825827
for i, entry in enumerate(entries):
826828
rid_to_idx[entry.rid] = i
829
+ if entry.event_type == "ci":
830
+ rid_to_rail[entry.rid] = max(entry.rail, 0)
831
+
832
+ # For each row, compute:
833
+ # 1. Which vertical rails are active (have a line passing through)
834
+ # 2. Whether there's a fork/merge connector to draw
835
+
836
+ # Precompute: for each checkin, the range of rows its line spans
837
+ # (from the entry's row to its parent's row)
838
+ active_spans = [] # (rail, start_idx, end_idx)
839
+ for i, entry in enumerate(entries):
840
+ if entry.event_type == "ci" and entry.parent_rid in rid_to_idx:
841
+ parent_idx = rid_to_idx[entry.parent_rid]
842
+ if parent_idx > i:
843
+ rail = max(entry.rail, 0)
844
+ active_spans.append((rail, i, parent_idx))
827845
828846
result = []
829847
for i, entry in enumerate(entries):
830848
rail = max(entry.rail, 0) if entry.rail >= 0 else 0
831849
node_x = rail_offset + rail * rail_pitch
832850
833
- # Determine what vertical lines to draw through this row
834
- # Active rails: any branch that has entries above and below this point
851
+ # Active rails at this row: any span that covers this row
835852
active_rails = set()
836
-
837
- # The current entry's rail is active if it has a parent below
838
- if entry.event_type == "ci" and entry.parent_rid in rid_to_idx:
839
- parent_idx = rid_to_idx[entry.parent_rid]
840
- if parent_idx > i: # parent is below in the list (older)
841
- active_rails.add(rail)
842
-
843
- # Check if any entries above connect through this row to entries below
844
- for j in range(i):
845
- prev = entries[j]
846
- if prev.event_type == "ci" and prev.parent_rid in rid_to_idx:
847
- parent_idx = rid_to_idx[prev.parent_rid]
848
- if parent_idx > i: # parent is below this row
849
- prev_rail = max(prev.rail, 0)
850
- active_rails.add(prev_rail)
851
-
852
- # Compute line segments as pixel positions
853
+ for span_rail, span_start, span_end in active_spans:
854
+ if span_start <= i <= span_end:
855
+ active_rails.add(span_rail)
856
+
853857
lines = [{"x": rail_offset + r * rail_pitch} for r in sorted(active_rails)]
854858
855
- # Connection from this node's rail to parent's rail (if different = branch/merge line)
859
+ # Fork/merge connector: if this entry's parent is on a different rail,
860
+ # draw a horizontal connector at the parent's row (where the line joins)
856861
connector = None
857862
if entry.event_type == "ci" and entry.parent_rid in rid_to_idx:
858863
parent_idx = rid_to_idx[entry.parent_rid]
859
- if parent_idx == i + 1: # immediate next entry
860
- parent_rail = max(entries[parent_idx].rail, 0)
861
- if parent_rail != rail:
862
- parent_x = rail_offset + parent_rail * rail_pitch
863
- connector = {
864
- "left": min(node_x, parent_x),
865
- "width": abs(node_x - parent_x),
866
- }
867
-
868
- max_rail = max((e.rail for e in entries if e.rail >= 0), default=0)
864
+ parent_rail = rid_to_rail.get(entry.parent_rid, 0)
865
+ if parent_rail != rail and parent_idx == i + 1:
866
+ # Connector at this row going to parent's rail
867
+ parent_x = rail_offset + parent_rail * rail_pitch
868
+ connector = {
869
+ "left": min(node_x, parent_x),
870
+ "width": abs(node_x - parent_x),
871
+ }
872
+
869873
result.append(
870874
{
871875
"entry": entry,
872876
"node_x": node_x,
873877
"lines": lines,
874878
"connector": connector,
875
- "graph_width": rail_offset + (max_rail + 2) * rail_pitch,
879
+ "graph_width": graph_width,
876880
}
877881
)
878882
879883
return result
880884
--- fossil/views.py
+++ fossil/views.py
@@ -811,69 +811,73 @@
811
812
813 def _compute_dag_graph(entries):
814 """Compute DAG graph positions for timeline entries.
815
816 Returns a list of dicts wrapping each entry with graph rendering data:
817 - node_x: pixel x position of the node
818 - lines: list of (x1, x2) connections to draw between this row and the next
819 """
820 rail_pitch = 16 # pixels between rails
821 rail_offset = 20 # left margin
 
 
822
823 # Build rid-to-index lookup for connecting lines
824 rid_to_idx = {}
 
825 for i, entry in enumerate(entries):
826 rid_to_idx[entry.rid] = i
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827
828 result = []
829 for i, entry in enumerate(entries):
830 rail = max(entry.rail, 0) if entry.rail >= 0 else 0
831 node_x = rail_offset + rail * rail_pitch
832
833 # Determine what vertical lines to draw through this row
834 # Active rails: any branch that has entries above and below this point
835 active_rails = set()
836
837 # The current entry's rail is active if it has a parent below
838 if entry.event_type == "ci" and entry.parent_rid in rid_to_idx:
839 parent_idx = rid_to_idx[entry.parent_rid]
840 if parent_idx > i: # parent is below in the list (older)
841 active_rails.add(rail)
842
843 # Check if any entries above connect through this row to entries below
844 for j in range(i):
845 prev = entries[j]
846 if prev.event_type == "ci" and prev.parent_rid in rid_to_idx:
847 parent_idx = rid_to_idx[prev.parent_rid]
848 if parent_idx > i: # parent is below this row
849 prev_rail = max(prev.rail, 0)
850 active_rails.add(prev_rail)
851
852 # Compute line segments as pixel positions
853 lines = [{"x": rail_offset + r * rail_pitch} for r in sorted(active_rails)]
854
855 # Connection from this node's rail to parent's rail (if different = branch/merge line)
 
856 connector = None
857 if entry.event_type == "ci" and entry.parent_rid in rid_to_idx:
858 parent_idx = rid_to_idx[entry.parent_rid]
859 if parent_idx == i + 1: # immediate next entry
860 parent_rail = max(entries[parent_idx].rail, 0)
861 if parent_rail != rail:
862 parent_x = rail_offset + parent_rail * rail_pitch
863 connector = {
864 "left": min(node_x, parent_x),
865 "width": abs(node_x - parent_x),
866 }
867
868 max_rail = max((e.rail for e in entries if e.rail >= 0), default=0)
869 result.append(
870 {
871 "entry": entry,
872 "node_x": node_x,
873 "lines": lines,
874 "connector": connector,
875 "graph_width": rail_offset + (max_rail + 2) * rail_pitch,
876 }
877 )
878
879 return result
880
--- fossil/views.py
+++ fossil/views.py
@@ -811,69 +811,73 @@
811
812
813 def _compute_dag_graph(entries):
814 """Compute DAG graph positions for timeline entries.
815
816 Tracks active rails through each row and draws fork/merge connectors
817 where a child is on a different rail than its parent.
 
818 """
819 rail_pitch = 16
820 rail_offset = 20
821 max_rail = max((e.rail for e in entries if e.rail >= 0), default=0)
822 graph_width = rail_offset + (max_rail + 2) * rail_pitch
823
824 # Build rid-to-index and rid-to-rail lookups
825 rid_to_idx = {}
826 rid_to_rail = {}
827 for i, entry in enumerate(entries):
828 rid_to_idx[entry.rid] = i
829 if entry.event_type == "ci":
830 rid_to_rail[entry.rid] = max(entry.rail, 0)
831
832 # For each row, compute:
833 # 1. Which vertical rails are active (have a line passing through)
834 # 2. Whether there's a fork/merge connector to draw
835
836 # Precompute: for each checkin, the range of rows its line spans
837 # (from the entry's row to its parent's row)
838 active_spans = [] # (rail, start_idx, end_idx)
839 for i, entry in enumerate(entries):
840 if entry.event_type == "ci" and entry.parent_rid in rid_to_idx:
841 parent_idx = rid_to_idx[entry.parent_rid]
842 if parent_idx > i:
843 rail = max(entry.rail, 0)
844 active_spans.append((rail, i, parent_idx))
845
846 result = []
847 for i, entry in enumerate(entries):
848 rail = max(entry.rail, 0) if entry.rail >= 0 else 0
849 node_x = rail_offset + rail * rail_pitch
850
851 # Active rails at this row: any span that covers this row
 
852 active_rails = set()
853 for span_rail, span_start, span_end in active_spans:
854 if span_start <= i <= span_end:
855 active_rails.add(span_rail)
856
 
 
 
 
 
 
 
 
 
 
 
 
 
857 lines = [{"x": rail_offset + r * rail_pitch} for r in sorted(active_rails)]
858
859 # Fork/merge connector: if this entry's parent is on a different rail,
860 # draw a horizontal connector at the parent's row (where the line joins)
861 connector = None
862 if entry.event_type == "ci" and entry.parent_rid in rid_to_idx:
863 parent_idx = rid_to_idx[entry.parent_rid]
864 parent_rail = rid_to_rail.get(entry.parent_rid, 0)
865 if parent_rail != rail and parent_idx == i + 1:
866 # Connector at this row going to parent's rail
867 parent_x = rail_offset + parent_rail * rail_pitch
868 connector = {
869 "left": min(node_x, parent_x),
870 "width": abs(node_x - parent_x),
871 }
872
 
873 result.append(
874 {
875 "entry": entry,
876 "node_x": node_x,
877 "lines": lines,
878 "connector": connector,
879 "graph_width": graph_width,
880 }
881 )
882
883 return result
884

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button