top of page

PyQt6 Interactive Family Tree Graph Creation

PyQt6 Interactive Family Tree Graph
PyQt6 Interactive Family Tree Graph Guide

Creating an interactive, expandable family tree-style graph in PyQt6 requires a blend of custom item drawing and careful layout management. This approach allows for a horizontal visualization of interlinked documents, where each node displays unique identifiers and descriptions. While PyQt6 offers powerful tools like the Graphics View Framework for such tasks, achieving a specific horizontal tree layout with interactive expansion and collapsing features necessitates subclassing QGraphicsItem and implementing custom logic for positioning and event handling. This guide will walk you through the essential steps to build such a visualization, ensuring clarity and interactivity.

This guide demonstrates how to create an interactive, expandable/collapsible horizontal tree-style graph in PyQt6, suitable for visualizing interlinked documents, akin to a family tree structure. We will focus on custom node rendering and layout management within PyQt6. The SEO keyphrase PyQt6 Interactive Family Tree Graph is integrated naturally.

Designing a PyQt6 Interactive Family Tree Graph

The core challenge is to render a horizontal tree structure where each node represents a document. These nodes should display unique index numbers and descriptions, and the entire graph must be expandable and collapsible. The visualization should resemble a family tree, spreading horizontally from a root node.

We aim to achieve this using PyQt6's graphics view framework, allowing for custom item drawing and interaction handling. The structure implies a parent-child relationship between documents, which needs to be visually represented.

Visualizing Document Interconnections

The requirement for a horizontal, tree-like layout is critical. Unlike standard vertical tree views, this demands a custom approach to node positioning and edge drawing, ensuring a clear, outward spread from the central root.

Each node needs to contain specific data: an index number and a description. The layout must accommodate these elements within visually distinct rectangular nodes, with the description potentially appearing below or inside the node itself.

Expandable and Collapsible Functionality

Users should be able to expand or collapse branches of the tree to manage complexity and focus on specific document relationships. This interactivity is key to navigating potentially large datasets.

Implementing this requires handling user input (like clicks) to toggle the visibility of child nodes and recalculate the layout dynamically.

PyQt6 Graphics View Framework for Tree Graphs

The PyQt6 Graphics View Framework (QGraphicsView, QGraphicsScene, QGraphicsItem) is ideally suited for creating custom, interactive visualizations like our tree graph. It provides a powerful abstraction layer for rendering and managing graphical items.

We will subclass QGraphicsItem to create custom nodes that can display text and handle user interactions. A custom layout manager or a recursive drawing approach will manage the horizontal tree structure.

Node Representation with QGraphicsItem

Each document will be represented by a custom QGraphicsItem subclass. This class will handle drawing the rectangular node, displaying the index number and description, and managing its position within the scene.

Event handling within the custom item will enable click events for expansion/collaboration. We’ll use QPainter to draw the node's shape and text, ensuring clear visual presentation.

Layout Strategy: Horizontal Tree Arrangement

A recursive function can determine the position of each node. Starting from the root, it calculates the horizontal space required for each subtree, arranging children horizontally to the right of their parent.

The depth of the recursion will determine the horizontal spread, and vertical positioning will be managed to avoid overlaps and maintain readability.

Custom Node Implementation in PyQt6

We will create a DocumentNode class inheriting from QGraphicsItem. This class will manage the node's geometry, content, and interaction logic.

Drawing Node Content

The paint method of our DocumentNode will use QPainter to draw a rectangle and render the document's index number and description. We will use QFontMetrics to determine text sizes for accurate bounding box calculations.

The description will be rendered within the node's bounding rectangle, potentially wrapping if it exceeds the available width. The index number will be prominently displayed, perhaps at the top of the node.

Handling Expansion and Collapsing

Clicking on a node will trigger a method to toggle the visibility of its children. This involves updating the QGraphicsItem's state and potentially triggering a scene layout update.

A boolean flag like is_expanded will control whether child nodes are visible. When collapsed, child items and their connecting lines will be hidden; when expanded, they will be shown and repositioned.

Setting Up the Graphics Scene and View

A QGraphicsScene will hold all the DocumentNode items and the lines connecting them. A QGraphicsView will provide a viewport to display the scene, enabling zooming and panning.

Populating the Scene

We’ll need a data structure to represent the document hierarchy (e.g., a tree or nested dictionaries). This data will be used to create and add DocumentNode instances to the scene, along with QGraphicsLineItem objects to draw the connections.

The initial placement of nodes will be handled by a layout algorithm, ensuring the horizontal tree structure is established upon scene population.

Interactivity and Navigation

The QGraphicsView allows for basic navigation like panning and zooming. The custom node interactions (expand/collapse) will be handled within the scene and node items themselves.

We can further enhance interactivity by implementing hover effects, selection highlighting, and drag-and-drop functionality if needed, all managed through the graphics view framework.

from PyQt6.QtWidgets import (QApplication, QGraphicsView, QGraphicsScene, 
                             QGraphicsItem, QStyleOptionGraphicsItem, QWidget)
from PyQt6.QtGui import QPainter, QColor, QFont, QPen, QBrush, QPolygonF
from PyQt6.QtCore import QRectF, Qt, QPointF
import sys

# --- Data Structure for Document Tree ---
class DocumentNodeData:
    def __init__(self, doc_id, description, children=None):
        self.doc_id = doc_id
        self.description = description
        self.children = children if children is not None else []

# --- Custom Graphics Item for Document Node ---
class DocumentNode(QGraphicsItem):
    def __init__(self, data, parent=None):
        super().__init__()
        self.data = data
        self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
        self.is_expanded = True
        self.children_items = []
        self.node_width = 150
        self.node_height = 80
        self.padding = 10
        self.font_size = 10
        self.setAcceptHoverEvents(True)

        self.setPos(0, 0) # Initial position, will be adjusted by layout

    def boundingRect(self):
        # Bounding rectangle for the node itself
        return QRectF(0, 0, self.node_width, self.node_height)

    def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget | None = None) -> None:
        rect = self.boundingRect()
        painter.setPen(QPen(Qt.GlobalColor.black, 1) if not self.isSelected() else QPen(Qt.GlobalColor.red, 2))
        painter.setBrush(QBrush(QColor(200, 230, 255))) # Light blue fill
        painter.drawRect(rect)

        # Draw Document ID
        painter.setFont(QFont('Arial', self.font_size + 2, QFont.Weight.Bold))
        painter.drawText(rect.adjusted(self.padding, self.padding, -self.padding, -self.padding/2), Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter, f"ID: {self.data.doc_id}")

        # Draw Description (below ID)
        painter.setFont(QFont('Arial', self.font_size))
        desc_rect = QRectF(rect.x() + self.padding, rect.y() + self.padding + 20, rect.width() - 2*self.padding, rect.height() - (self.padding + 20) - self.padding)
        painter.drawText(desc_rect, Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.TextWordWrap, self.data.description)

        # Indicate expand/collapse state
        if self.is_expanded:
            painter.drawText(rect.adjusted(-self.padding, -self.padding/2, self.padding, self.padding/2), Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter, "-")
        else:
            painter.drawText(rect.adjusted(-self.padding, -self.padding/2, self.padding, self.padding/2), Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter, "+")

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            # Check if click is on the expand/collapse indicator
            indicator_rect = QRectF(self.boundingRect().right() - 20, self.boundingRect().bottom() - 20, 20, 20)
            if indicator_rect.contains(event.pos()):
                self.is_expanded = not self.is_expanded
                self.scene().update() # Request scene redraw
                # Propagate collapse/expand to children
                for child_item in self.children_items:
                    child_item.setVisible(self.is_expanded)
                    # Also need to handle their children recursively
                    self.toggle_children_visibility(child_item, self.is_expanded)
                event.accept()
                return
        super().mousePressEvent(event)

    def toggle_children_visibility(self, item, visible):
        item.setVisible(visible)
        if item.is_expanded:
            for child in item.children_items:
                self.toggle_children_visibility(child, visible)

    def add_child(self, child_item):
        self.children_items.append(child_item)

# --- Layout Calculation --- 
# This is a simplified layout. A more robust solution would use a dedicated tree layout algorithm.
def arrange_tree_horizontally(node, x=0, y=0, level_spacing=200, node_spacing=20):
    node.setPos(x, y)
    current_x = x + node.node_width + node_spacing

    if node.is_expanded:
        for child in node.children_items:
            child_x = current_x
            child_y = y + node.node_height + level_spacing
            current_x = arrange_tree_horizontally(child, child_x, child_y, level_spacing, node_spacing)
    return current_x

# --- Main Application Window ---
class TreeGraphView(QGraphicsView):
    def __init__(self, scene, parent=None):
        super().__init__(scene, parent)
        self.setRenderHint(QPainter.RenderHint.Antialiasing)
        self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
        self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)

    def wheelEvent(self, event):
        # Zooming
        zoom_in_factor = 1.15
        zoom_out_factor = 1 / zoom_in_factor

        if event.angleDelta().y() > 0:
            zoom_factor = zoom_in_factor
        else:
            zoom_factor = zoom_out_factor
        self.scale(zoom_factor, zoom_factor)

# --- Sample Data ---
# Represents a small document hierarchy
data_tree = DocumentNodeData(
    doc_id=1,
    description="Root Document",
    children=[
        DocumentNodeData(
            doc_id=2,
            description="Child Document A",
            children=[
                DocumentNodeData(doc_id=4, description="Grandchild Document A1"),
                DocumentNodeData(doc_id=5, description="Grandchild Document A2")
            ]
        ),
        DocumentNodeData(
            doc_id=3,
            description="Child Document B",
            children=[
                DocumentNodeData(doc_id=6, description="Grandchild Document B1")
            ]
        )
    ]
)

def build_scene(root_data):
    scene = QGraphicsScene()
    
    # Recursive function to create nodes and add them to the scene
    def create_nodes(data, parent_item=None):
        node_item = DocumentNode(data)
        scene.addItem(node_item)
        
        if parent_item:
            parent_item.add_child(node_item)
            # Draw connecting line
            line = scene.addLine(parent_item.mapToScene(QPointF(parent_item.boundingRect().right(), parent_item.boundingRect().center().y()))[0], 
                                 node_item.mapToScene(QPointF(node_item.boundingRect().left(), node_item.boundingRect().center().y()))[0])
            line.setZValue(-1) # Draw lines behind nodes

        if node_item.is_expanded:
            for child_data in data.children:
                create_nodes(child_data, node_item)
        return node_item

    root_node_item = create_nodes(root_data)
    arrange_tree_horizontally(root_node_item, y=100) # Start drawing from y=100
    scene.setSceneRect(scene.itemsBoundingRect()) # Adjust scene rect to fit all items
    return scene

if __name__ == '__main__':
    app = QApplication(sys.argv)
    
    main_scene = build_scene(data_tree)
    view = TreeGraphView(main_scene)
    view.setWindowTitle('PyQt6 Interactive Family Tree Graph')
    view.setGeometry(100, 100, 1000, 700)
    view.show()
    
    sys.exit(app.exec())

PyQt6 Interactive Family Tree Graph Examples

The provided code defines a basic structure for a PyQt6 Interactive Family Tree Graph. It includes a DocumentNode class for visual representation and interaction, and a layout function to arrange nodes horizontally.

Node Structure and Rendering

The DocumentNode class inherits from QGraphicsItem. Its paint method handles drawing the node rectangle, document ID, and description. User interaction for expanding/collapsing is managed by detecting clicks on a specific area of the node.

The is_expanded flag controls the visibility of child nodes and their connecting lines. When a node is collapsed, its children are hidden, and when expanded, they become visible and are laid out.

Horizontal Layout Logic

The arrange_tree_horizontally function recursively positions nodes. It calculates the x-coordinate for each child based on the parent's position, width, and spacing, creating the horizontal spread. The y-coordinate is determined by the level, with vertical spacing between levels.

This simple layout ensures a tree-like structure spreading outwards. For more complex or aesthetically pleasing layouts, advanced tree-layout algorithms (like Reingold-Tilford) could be integrated.

Finalizing the PyQt6 Interactive Family Tree Graph

This implementation provides a foundational PyQt6 Interactive Family Tree Graph. The core components—custom nodes, horizontal layout, and expand/collapse functionality—are in place.

Further enhancements could include more sophisticated layout algorithms, richer node content, drag-and-drop capabilities, and better handling of large datasets through optimizations or alternative visualization libraries if PyQt6 alone becomes a bottleneck.

Related PyQt6 Graphing Tasks

Here are similar problems you might encounter when working with PyQt6 for data visualization.

Vertical Tree View with QTreeView

For a standard vertical tree structure, QTreeView with a custom model (like QStandardItemModel) is often simpler and more efficient than custom graphics items.

Node-Link Diagrams with QPainter

General node-link diagrams, not necessarily trees, can be created by manually positioning QGraphicsItems and drawing QGraphicsLineItems between them, offering great flexibility.

Interactive Data Plotting in PyQt6

For plotting data like line graphs or scatter plots, consider libraries like Matplotlib with its PyQt backend (matplotlib.backends.backend_qt5agg) for robust charting capabilities.

Custom Widget for Hierarchical Data

If the visualization is simpler, a custom QWidget subclass using paintEvent might suffice, though it lacks the performance and features of the graphics view framework for complex scenes.

Integrating External Graphing Libraries

For highly complex or specialized graphs (e.g., network graphs, complex trees), libraries like NetworkX combined with Matplotlib or other rendering backends might be necessary, though integrating them into PyQt6 requires careful handling of rendering.

Additional PyQt6 Graphics View Illustrations

These examples showcase specific aspects of PyQt6's graphics view framework relevant to custom visualizations.

Customizing Node Appearance on Hover

from PyQt6.QtWidgets import QStyleOptionGraphicsItem
from PyQt6.QtGui import QColor, QPen

class HoverNode(DocumentNode):
    def __init__(self, data, parent=None):
        super().__init__(data, parent)
        self.default_color = QColor(200, 230, 255)
        self.hover_color = QColor(150, 200, 255)
        self.is_hovered = False

    def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget | None = None) -> None:
        rect = self.boundingRect()
        painter.setPen(QPen(Qt.GlobalColor.black, 1) if not self.isSelected() else QPen(Qt.GlobalColor.red, 2))
        painter.setBrush(QBrush(self.hover_color if self.is_hovered else self.default_color))
        painter.drawRect(rect)
        # ... (rest of the drawing code for text) ...
        painter.setFont(QFont('Arial', self.font_size + 2, QFont.Weight.Bold))
        painter.drawText(rect.adjusted(self.padding, self.padding, -self.padding, -self.padding/2), Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter, f"ID: {self.data.doc_id}")
        painter.setFont(QFont('Arial', self.font_size))
        desc_rect = QRectF(rect.x() + self.padding, rect.y() + self.padding + 20, rect.width() - 2*self.padding, rect.height() - (self.padding + 20) - self.padding)
        painter.drawText(desc_rect, Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.TextWordWrap, self.data.description)

        if self.is_expanded:
            painter.drawText(rect.adjusted(-self.padding, -self.padding/2, self.padding, self.padding/2), Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter, "-")
        else:
            painter.drawText(rect.adjusted(-self.padding, -self.padding/2, self.padding, self.padding/2), Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter, "+")

    def hoverEnterEvent(self, event):
        self.is_hovered = True
        self.update()
        super().hoverEnterEvent(event)

    def hoverLeaveEvent(self, event):
        self.is_hovered = False
        self.update()
        super().hoverLeaveEvent(event)

This example shows how to implement hover effects by overriding hoverEnterEvent and hoverLeaveEvent to change the node's background color, enhancing user feedback.

Implementing Drag and Drop for Nodes

Component

Description

PyQt6 Class/Method

Document Node

Represents an individual document with ID and description. Handles interaction for expand/collapse.

QGraphicsItem subclass (e.g., DocumentNode)

Scene

Container for all graphical items (nodes and lines). Manages the overall drawing surface.

QGraphicsScene

View

Provides a scrollable and zoomable viewport onto the scene.

QGraphicsView

Layout

Arranges nodes horizontally, determining positions based on parent-child relationships and level.

Custom function (e.g., arrange_tree_horizontally)

Interactivity

Enables expanding/collapsing branches via user clicks on nodes.

Mouse event handling in QGraphicsItem (e.g., mousePressEvent)

Rendering

Draws node rectangles, text (ID, description), and connecting lines.

QPainter within the paint method of QGraphicsItem

From our network :

Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating
bottom of page