Files
BlueRoseNote/.claude/skills/obsidian-canvas-creator/references/layout-algorithms.md

16 KiB

Layout Algorithms for Obsidian Canvas

Detailed algorithms for positioning nodes in MindMap and Freeform layouts.

Layout Principles

Universal Spacing Constants

HORIZONTAL_SPACING = 320  // Minimum horizontal space between node centers
VERTICAL_SPACING = 200    // Minimum vertical space between node centers
NODE_PADDING = 20         // Internal padding within nodes

Collision Detection

Before finalizing any node position, verify:

def check_collision(node1, node2):
    """Returns True if nodes overlap or are too close"""
    center1_x = node1.x + node1.width / 2
    center1_y = node1.y + node1.height / 2
    center2_x = node2.x + node2.width / 2
    center2_y = node2.y + node2.height / 2
    
    dx = abs(center1_x - center2_x)
    dy = abs(center1_y - center2_y)
    
    min_dx = (node1.width + node2.width) / 2 + HORIZONTAL_SPACING
    min_dy = (node1.height + node2.height) / 2 + VERTICAL_SPACING
    
    return dx < min_dx or dy < min_dy

MindMap Layout Algorithm

1. Radial Tree Layout

Place root at center, arrange children radially.

Step 1: Position Root Node

root = {
    "x": 0 - (root_width / 2),  # Center horizontally
    "y": 0 - (root_height / 2), # Center vertically
    "width": root_width,
    "height": root_height
}

Step 2: Calculate Primary Branch Positions

Distribute first-level children around root:

def position_primary_branches(root, children, radius=400):
    """Position first-level children in a circle around root"""
    n = len(children)
    angle_step = 2 * pi / n
    
    positions = []
    for i, child in enumerate(children):
        angle = i * angle_step
        
        # Calculate position on circle
        x = root.center_x + radius * cos(angle) - child.width / 2
        y = root.center_y + radius * sin(angle) - child.height / 2
        
        positions.append({"x": x, "y": y})
    
    return positions

Radius Selection:

  • Small canvases (≤10 children): 400px
  • Medium canvases (11-20 children): 500px
  • Large canvases (>20 children): 600px

Step 3: Position Secondary Branches

For each primary branch, arrange its children:

Horizontal Layout (preferred for most cases):

def position_secondary_horizontal(parent, children, distance=350):
    """Arrange children horizontally to the right of parent"""
    n = len(children)
    total_height = sum(child.height for child in children)
    total_spacing = (n - 1) * VERTICAL_SPACING
    
    # Start position (top of vertical arrangement)
    start_y = parent.center_y - (total_height + total_spacing) / 2
    
    positions = []
    current_y = start_y
    
    for child in children:
        x = parent.x + parent.width + distance
        y = current_y
        
        positions.append({"x": x, "y": y})
        current_y += child.height + VERTICAL_SPACING
    
    return positions

Vertical Layout (for left/right primary branches):

def position_secondary_vertical(parent, children, distance=250):
    """Arrange children vertically below parent"""
    n = len(children)
    total_width = sum(child.width for child in children)
    total_spacing = (n - 1) * HORIZONTAL_SPACING
    
    # Start position (left of horizontal arrangement)
    start_x = parent.center_x - (total_width + total_spacing) / 2
    
    positions = []
    current_x = start_x
    
    for child in children:
        x = current_x
        y = parent.y + parent.height + distance
        
        positions.append({"x": x, "y": y})
        current_x += child.width + HORIZONTAL_SPACING
    
    return positions

Step 4: Balance and Adjust

After initial placement, check for collisions and adjust:

def balance_layout(nodes):
    """Adjust nodes to prevent overlaps"""
    max_iterations = 10
    
    for iteration in range(max_iterations):
        collisions = find_all_collisions(nodes)
        if not collisions:
            break
        
        for node1, node2 in collisions:
            # Move node2 away from node1
            dx = node2.center_x - node1.center_x
            dy = node2.center_y - node1.center_y
            distance = sqrt(dx*dx + dy*dy)
            
            # Calculate required distance
            min_dist = calculate_min_distance(node1, node2)
            
            if distance > 0:
                # Move proportionally
                move_x = (dx / distance) * (min_dist - distance) / 2
                move_y = (dy / distance) * (min_dist - distance) / 2
                
                node2.x += move_x
                node2.y += move_y

2. Tree Layout (Hierarchical Top-Down)

Alternative for deep hierarchies.

Positioning Formula

def position_tree_layout(root, tree):
    """Top-down tree layout"""
    # Level 0 (root)
    root.x = 0 - root.width / 2
    root.y = 0 - root.height / 2
    
    # Process each level
    for level in range(1, max_depth):
        nodes_at_level = get_nodes_at_level(tree, level)
        
        # Calculate horizontal spacing
        total_width = sum(node.width for node in nodes_at_level)
        total_spacing = (len(nodes_at_level) - 1) * HORIZONTAL_SPACING
        
        start_x = -(total_width + total_spacing) / 2
        y = level * (150 + VERTICAL_SPACING)  # 150px level height
        
        current_x = start_x
        for node in nodes_at_level:
            node.x = current_x
            node.y = y
            current_x += node.width + HORIZONTAL_SPACING

Freeform Layout Algorithm

1. Content-Based Grouping

First, identify natural groupings in content:

def identify_groups(nodes, content_structure):
    """Group nodes by semantic relationships"""
    groups = []
    
    # Analyze content structure
    for section in content_structure:
        group_nodes = [node for node in nodes if node.section == section]
        
        if len(group_nodes) > 1:
            groups.append({
                "label": section.title,
                "nodes": group_nodes
            })
    
    return groups

2. Grid-Based Zone Layout

Divide canvas into zones for different groups:

def layout_zones(groups, canvas_width=2000, canvas_height=1500):
    """Arrange groups in grid zones"""
    n_groups = len(groups)
    
    # Calculate grid dimensions
    cols = ceil(sqrt(n_groups))
    rows = ceil(n_groups / cols)
    
    zone_width = canvas_width / cols
    zone_height = canvas_height / rows
    
    # Assign zones
    zones = []
    for i, group in enumerate(groups):
        col = i % cols
        row = i // cols
        
        zone = {
            "x": col * zone_width - canvas_width / 2,
            "y": row * zone_height - canvas_height / 2,
            "width": zone_width * 0.9,  # Leave 10% margin
            "height": zone_height * 0.9,
            "group": group
        }
        zones.append(zone)
    
    return zones

3. Within-Zone Node Positioning

Position nodes within each zone:

Option A: Organic Flow

def position_organic(zone, nodes):
    """Organic, flowing arrangement within zone"""
    positions = []
    
    # Start at zone top-left with margin
    current_x = zone.x + 50
    current_y = zone.y + 50
    row_height = 0
    
    for node in nodes:
        # Check if node fits in current row
        if current_x + node.width > zone.x + zone.width - 50:
            # Move to next row
            current_x = zone.x + 50
            current_y += row_height + VERTICAL_SPACING
            row_height = 0
        
        positions.append({
            "x": current_x,
            "y": current_y
        })
        
        current_x += node.width + HORIZONTAL_SPACING
        row_height = max(row_height, node.height)
    
    return positions

Option B: Structured Grid

def position_grid(zone, nodes):
    """Grid arrangement within zone"""
    n = len(nodes)
    cols = ceil(sqrt(n))
    rows = ceil(n / cols)
    
    cell_width = (zone.width - 100) / cols  # 50px margin each side
    cell_height = (zone.height - 100) / rows
    
    positions = []
    for i, node in enumerate(nodes):
        col = i % cols
        row = i // cols
        
        # Center node in cell
        x = zone.x + 50 + col * cell_width + (cell_width - node.width) / 2
        y = zone.y + 50 + row * cell_height + (cell_height - node.height) / 2
        
        positions.append({"x": x, "y": y})
    
    return positions

4. Cross-Zone Connections

Calculate optimal edge paths between zones:

def calculate_edge_path(from_node, to_node):
    """Determine edge connection points"""
    # Calculate centers
    from_center = (from_node.x + from_node.width/2, 
                   from_node.y + from_node.height/2)
    to_center = (to_node.x + to_node.width/2,
                 to_node.y + to_node.height/2)
    
    # Determine best sides to connect
    dx = to_center[0] - from_center[0]
    dy = to_center[1] - from_center[1]
    
    # Choose sides based on direction
    if abs(dx) > abs(dy):
        # Horizontal connection
        from_side = "right" if dx > 0 else "left"
        to_side = "left" if dx > 0 else "right"
    else:
        # Vertical connection
        from_side = "bottom" if dy > 0 else "top"
        to_side = "top" if dy > 0 else "bottom"
    
    return {
        "fromSide": from_side,
        "toSide": to_side
    }

Advanced Techniques

Force-Directed Layout

For complex networks with many cross-connections:

def force_directed_layout(nodes, edges, iterations=100):
    """Spring-based layout algorithm"""
    # Constants
    SPRING_LENGTH = 200
    SPRING_CONSTANT = 0.1
    REPULSION_CONSTANT = 5000
    
    for iteration in range(iterations):
        # Calculate repulsive forces (all pairs)
        for node1 in nodes:
            force_x, force_y = 0, 0
            
            for node2 in nodes:
                if node1 == node2:
                    continue
                
                dx = node1.x - node2.x
                dy = node1.y - node2.y
                distance = sqrt(dx*dx + dy*dy)
                
                if distance > 0:
                    # Repulsive force
                    force = REPULSION_CONSTANT / (distance * distance)
                    force_x += (dx / distance) * force
                    force_y += (dy / distance) * force
            
            node1.force_x = force_x
            node1.force_y = force_y
        
        # Calculate attractive forces (connected nodes)
        for edge in edges:
            node1 = get_node(edge.fromNode)
            node2 = get_node(edge.toNode)
            
            dx = node2.x - node1.x
            dy = node2.y - node1.y
            distance = sqrt(dx*dx + dy*dy)
            
            # Spring force
            force = SPRING_CONSTANT * (distance - SPRING_LENGTH)
            
            node1.force_x += (dx / distance) * force
            node1.force_y += (dy / distance) * force
            node2.force_x -= (dx / distance) * force
            node2.force_y -= (dy / distance) * force
        
        # Apply forces
        for node in nodes:
            node.x += node.force_x
            node.y += node.force_y

Hierarchical Clustering

Group related nodes automatically:

def hierarchical_cluster(nodes, similarity_threshold=0.7):
    """Cluster nodes by content similarity"""
    clusters = []
    
    # Calculate similarity matrix
    similarity = calculate_similarity_matrix(nodes)
    
    # Agglomerative clustering
    current_clusters = [[node] for node in nodes]
    
    while len(current_clusters) > 1:
        # Find most similar clusters
        max_sim = 0
        merge_i, merge_j = 0, 1
        
        for i in range(len(current_clusters)):
            for j in range(i + 1, len(current_clusters)):
                sim = cluster_similarity(current_clusters[i], 
                                       current_clusters[j], 
                                       similarity)
                if sim > max_sim:
                    max_sim = sim
                    merge_i, merge_j = i, j
        
        if max_sim < similarity_threshold:
            break
        
        # Merge clusters
        current_clusters[merge_i].extend(current_clusters[merge_j])
        current_clusters.pop(merge_j)
    
    return current_clusters

Layout Optimization

Minimize Edge Crossings

def minimize_crossings(nodes, edges):
    """Reduce edge crossing through node repositioning"""
    crossings = count_crossings(edges)
    
    # Try swapping adjacent nodes
    improved = True
    while improved:
        improved = False
        
        for i in range(len(nodes) - 1):
            # Swap nodes i and i+1
            swap_positions(nodes[i], nodes[i+1])
            new_crossings = count_crossings(edges)
            
            if new_crossings < crossings:
                crossings = new_crossings
                improved = True
            else:
                # Swap back
                swap_positions(nodes[i], nodes[i+1])

Visual Balance

def calculate_visual_weight(canvas):
    """Calculate center of mass for visual balance"""
    total_weight = 0
    weighted_x = 0
    weighted_y = 0
    
    for node in canvas.nodes:
        # Weight is proportional to area
        weight = node.width * node.height
        total_weight += weight
        
        weighted_x += node.center_x * weight
        weighted_y += node.center_y * weight
    
    center_x = weighted_x / total_weight
    center_y = weighted_y / total_weight
    
    # Shift entire canvas to center at (0, 0)
    offset_x = -center_x
    offset_y = -center_y
    
    for node in canvas.nodes:
        node.x += offset_x
        node.y += offset_y

Performance Optimization

Spatial Indexing

For large canvases, use spatial indexing to speed up collision detection:

class SpatialGrid:
    """Grid-based spatial index for fast collision detection"""
    
    def __init__(self, cell_size=500):
        self.cell_size = cell_size
        self.grid = {}
    
    def add_node(self, node):
        """Add node to grid"""
        cells = self.get_cells(node)
        for cell in cells:
            if cell not in self.grid:
                self.grid[cell] = []
            self.grid[cell].append(node)
    
    def get_cells(self, node):
        """Get grid cells node occupies"""
        min_x = int(node.x / self.cell_size)
        max_x = int((node.x + node.width) / self.cell_size)
        min_y = int(node.y / self.cell_size)
        max_y = int((node.y + node.height) / self.cell_size)
        
        cells = []
        for x in range(min_x, max_x + 1):
            for y in range(min_y, max_y + 1):
                cells.append((x, y))
        return cells
    
    def get_nearby_nodes(self, node):
        """Get nodes in nearby cells"""
        cells = self.get_cells(node)
        nearby = set()
        
        for cell in cells:
            if cell in self.grid:
                nearby.update(self.grid[cell])
        
        return nearby

Common Layout Patterns

Timeline Layout

For chronological content:

def layout_timeline(events, direction="horizontal"):
    """Create timeline layout"""
    if direction == "horizontal":
        for i, event in enumerate(events):
            event.x = i * (event.width + HORIZONTAL_SPACING)
            event.y = 0
    else:  # vertical
        for i, event in enumerate(events):
            event.x = 0
            event.y = i * (event.height + VERTICAL_SPACING)

Circular Layout

For cyclical processes:

def layout_circular(nodes, radius=500):
    """Arrange nodes in a circle"""
    n = len(nodes)
    angle_step = 2 * pi / n
    
    for i, node in enumerate(nodes):
        angle = i * angle_step
        node.x = radius * cos(angle) - node.width / 2
        node.y = radius * sin(angle) - node.height / 2

Matrix Layout

For comparing multiple dimensions:

def layout_matrix(nodes, rows, cols):
    """Arrange nodes in a matrix"""
    cell_width = 400
    cell_height = 250
    
    for i, node in enumerate(nodes):
        row = i // cols
        col = i % cols
        
        node.x = col * cell_width
        node.y = row * cell_height

Quality Checks

Before finalizing layout, verify:

  1. No Overlaps: All nodes have minimum spacing
  2. Balanced: Visual center near (0, 0)
  3. Accessible: All nodes reachable via edges
  4. Readable: Text sizes appropriate for zoom level
  5. Efficient: Edge paths reasonably direct

Use these algorithms as foundations, adapting to specific content and user preferences.