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
Commit
04985a1f1d0a8871ac7628013002226ae771f3802055d179450205915fc7b394
Parent
25bbd0575b81f31…
1 file changed
+41
-37
+41
-37
| --- fossil/views.py | ||
| +++ fossil/views.py | ||
| @@ -811,69 +811,73 @@ | ||
| 811 | 811 | |
| 812 | 812 | |
| 813 | 813 | def _compute_dag_graph(entries): |
| 814 | 814 | """Compute DAG graph positions for timeline entries. |
| 815 | 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 | |
| 816 | + Tracks active rails through each row and draws fork/merge connectors | |
| 817 | + where a child is on a different rail than its parent. | |
| 819 | 818 | """ |
| 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 | |
| 822 | 823 | |
| 823 | - # Build rid-to-index lookup for connecting lines | |
| 824 | + # Build rid-to-index and rid-to-rail lookups | |
| 824 | 825 | rid_to_idx = {} |
| 826 | + rid_to_rail = {} | |
| 825 | 827 | for i, entry in enumerate(entries): |
| 826 | 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)) | |
| 827 | 845 | |
| 828 | 846 | result = [] |
| 829 | 847 | for i, entry in enumerate(entries): |
| 830 | 848 | rail = max(entry.rail, 0) if entry.rail >= 0 else 0 |
| 831 | 849 | node_x = rail_offset + rail * rail_pitch |
| 832 | 850 | |
| 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 | |
| 835 | 852 | 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 | + | |
| 853 | 857 | lines = [{"x": rail_offset + r * rail_pitch} for r in sorted(active_rails)] |
| 854 | 858 | |
| 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) | |
| 856 | 861 | connector = None |
| 857 | 862 | if entry.event_type == "ci" and entry.parent_rid in rid_to_idx: |
| 858 | 863 | 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 | + | |
| 869 | 873 | result.append( |
| 870 | 874 | { |
| 871 | 875 | "entry": entry, |
| 872 | 876 | "node_x": node_x, |
| 873 | 877 | "lines": lines, |
| 874 | 878 | "connector": connector, |
| 875 | - "graph_width": rail_offset + (max_rail + 2) * rail_pitch, | |
| 879 | + "graph_width": graph_width, | |
| 876 | 880 | } |
| 877 | 881 | ) |
| 878 | 882 | |
| 879 | 883 | return result |
| 880 | 884 |
| --- 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 |