PyQt6 Interactive Family Tree Graph Creation
- Zartom
- Aug 26
- 12 min read

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 |
Comments