如何创建风格化的树形图?

问题描述 投票:0回答:1

我一直在制作一棵与兄弟会相邻的大大小小的树,并正在寻找一种方法来使其自动化,以便随着更多人的加入而发生变化。每个人的名字、年龄、大小都在 Excel 电子表格中。我可以用什么来模拟我在这里所做的设计?具体来说,茎的样式和根据年份将节点间隔得更远的能力。

这是我想要自动化的设计:

The tree I want to emulate

我尝试使用anytree和graphviz,但找不到模拟茎的方法或基于年份的间距的简单解决方案。

这是示例数据:

姓名 年份 仪器 额外 额外 额外 额外 小1 小2 小3
T1P1 1990 小号 T1P2
T1P2 1991 小号 T1P1
T2P1 1997 小号 T2P2
T2P2 2001 小号 T2P1 T2P3 T2P4 T2P5
T2P3 2003 小号 T2P2
T2P4 2004年 小号 T2P2
T2P5 2006年 小号 T2P2
T3P1 2000 小号 T3P2
T3P2 2004年 小号 T3P1 T3P3 T3P4
T3P3 2005 小号 T3P2 T3P5 T3P6
T3P5 2006年 小号 T3P3
T3P6 2007年 小号 T3P3
T3P4 2006年 小号 T3P2 T3P7
T3P7 2010 长笛 T3P4

这是我使用anytree 的基本方法和结果:

import openpyxl
from PIL import Image, ImageDraw, ImageFont
import re
from anytree import Node, RenderTree
from collections import Counter
import os

# Create a directory to store the individual name card images
cards_dir = "C:/Users/Chris Fitz/Documents/Fun/Trumpet History/trumpettree/cards"
os.makedirs(cards_dir, exist_ok=True)

# Load the .xlsx file
file_path = 'C:/Users/Chris Fitz/Documents/Fun/Trumpet History/trumpettree/sampletrees.xlsx'
workbook = openpyxl.load_workbook(file_path)
sheet = workbook.active

# Read the data starting from row 2 to the last row with data (max_row) in columns A to N
people_data = []
for row in sheet.iter_rows(min_row=2, max_row=sheet.max_row, min_col=1, max_col=14):
    person_info = [cell.value for cell in row]
    people_data.append(person_info)

# Tree Data Making
# Dictionary to hold people by their names
people_dict = {}

# List to hold the root nodes of multiple trees
root_nodes = []

# Sets to track parents and children
parents_set = set()
children_set = set()

# Dictionary to track parent-child relationships for conflict detection
parent_child_relationships = {}

# List to store the individual trees as objects
family_trees = []  # List to hold each separate family tree

# Iterate over the people data and create nodes for each person
for i, person_info in enumerate(people_data, start=2):  # i starts at 2 for row index
    name = person_info[0]  # Assuming name is in the first column (column A)
    column_b_data = person_info[1]  # Column B data (second column)
    parent_name = person_info[7]  # Column H for parent (8th column)
    children_names = person_info[8:14]  # Columns I to N for children (9th to 14th columns)

    # Check if this name is already in the people_dict
    if name not in people_dict:
        # Create the person node (this is the current node) without column B info at this point
        person_node = Node(name)  # Create the person node with just the name

        # If parent_name is empty, this is a root node for a new tree
        if parent_name:
            if parent_name in people_dict:
                parent_node = people_dict[parent_name]
            else:
                parent_node = Node(parent_name)
                people_dict[parent_name] = parent_node  # Add the parent to the dictionary

            person_node.parent = parent_node  # Set the parent for the current person
            # Add to the parents set
            parents_set.add(parent_name)
        else:
            # If no parent is referenced, this could be the root or top-level node
            root_nodes.append(person_node)  # Add to root_nodes list

        # Store the person node in the dictionary (this ensures we don't create duplicates)
        people_dict[name] = person_node

        # Create child nodes for the person and add them to the children set
        for child_name in children_names:
            if child_name:
                # Create child node without modifying its name with additional info from the parent
                if child_name not in people_dict:
                    child_node = Node(child_name, parent=person_node)
                    people_dict[child_name] = child_node  # Store the child in the dictionary
                children_set.add(child_name)

                # Add the parent-child relationship for conflict checking
                if child_name not in parent_child_relationships:
                    parent_child_relationships[child_name] = set()
                parent_child_relationships[child_name].add(name)

# Print out the family trees for each root node (disconnected trees)
for root_node in root_nodes:
    family_tree = []
    for pre, fill, node in RenderTree(root_node):
        family_tree.append(f"{pre}{node.name}")
    family_trees.append(family_tree)  # Save each tree as a separate list of names
    print(f"\nFamily Tree starting from {root_node.name}:")
    for pre, fill, node in RenderTree(root_node):
        print(f"{pre}{node.name}")

# Tree Chart Making
# Extract the years from the first four characters in Column B
years = []
for person_info in people_data:
    column_b_data = person_info[1]
    if column_b_data:
        year_str = str(column_b_data)[:4]
        if year_str.isdigit():
            years.append(int(year_str))

# Calculate the range of years (from the minimum year to the maximum year)
min_year = min(years) if years else 0
max_year = max(years) if years else 0
year_range = max_year - min_year + 1 if years else 0

# Create a base image with a solid color (header space)
base_width = 5000
base_height = 300 + (100 * year_range)  # Header (300px) + layers of 100px strips based on the year range
base_color = "#B3A369"
base_image = Image.new("RGB", (base_width, base_height), color=base_color)

# Create a drawing context
draw = ImageDraw.Draw(base_image)

# Define the text and font for the header
text = "The YJMB Trumpet Section Family Tree"
font_path = "C:/Windows/Fonts/calibrib.ttf"
font_size = 240
font = ImageFont.truetype(font_path, font_size)

# Get the width and height of the header text using textbbox
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]

# Calculate the position to center the header text horizontally
x = (base_width - text_width) // 2
y = (300 - text_height) // 2  # Vertically center the text in the first 300px

# Add the header text to the image
draw.text((x, y), text, font=font, fill=(255, 255, 255))

# List of colors for the alternating strips
colors = ["#FFFFFF", "#003057", "#FFFFFF", "#B3A369"]
strip_height = 100

# Font for the year text
year_font_size = 60
year_font = ImageFont.truetype(font_path, year_font_size)

# Add the alternating colored strips beneath the header
y_offset = 300  # Start just below the header text
for i in range(year_range):
    strip_color = colors[i % len(colors)]

    # Draw the strip
    draw.rectangle([0, y_offset, base_width, y_offset + strip_height], fill=strip_color)

    # Calculate the text to display (the year for this strip)
    year_text = str(min_year + i)

    # Get the width and height of the year text using textbbox
    bbox = draw.textbbox((0, 0), year_text, font=year_font)
    year_text_width = bbox[2] - bbox[0]
    year_text_height = bbox[3] - bbox[1]

    # Calculate the position to center the year text vertically on the strip
    year_text_x = 25  # Offset 25px from the left edge
    year_text_y = y_offset + (strip_height - year_text_height) // 2 - 5  # Vertically center the text

    # Determine the text color based on the strip color
    year_text_color = "#003057" if strip_color == "#FFFFFF" else "white"

    # Add the year text to the strip
    draw.text((year_text_x, year_text_y), year_text, font=year_font, fill=year_text_color)

    # Move the offset for the next strip
    y_offset += strip_height

# Font for the names on the name cards (reduced to size 22)
name_font_size = 22
name_font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", name_font_size)

# Initialize counters for each year (based on the range of years)
year_counters = {year: 0 for year in range(min_year, max_year + 1)}

# Create a list of names from the spreadsheet, split on newlines where appropriate
for i, person_info in enumerate(people_data):
    name = person_info[0]  # Assuming name is in the first column (column A)
    original_name = name
    column_b_data = person_info[1]  # Column B data (second column)
    column_c_data = person_info[2]  # Column C data (third column)

    # Choose the correct name card template based on Column C
    if column_c_data and "Trumpet" not in column_c_data:
        # Use the blue name card template if Column C doesn't include "Trumpet"
        name_card_template = Image.open("C:/Users/Chris Fitz/Documents/Fun/Trumpet History/trumpettree/blank_blue_name_card.png")
    else:
        # Use the default name card template if Column C includes "Trumpet"
        name_card_template = Image.open("C:/Users/Chris Fitz/Documents/Fun/Trumpet History/trumpettree/blank_name_card.png")

    if column_b_data:
        year_str = str(column_b_data)[:4]
        if year_str.isdigit():
            year = int(year_str)
            year_index = year - min_year  # Find the corresponding year index (from 0 to year_range-1)

            person_node.year = year

            person_node.name = name

            # Check if the name contains "VET" or "RAT"
            if "VET" in name or "RAT" in name:
                # Replace the first space with a newline
                name_lines = name.split(' ', 1)
                name = name_lines[0] + '\n' + name_lines[1]
            elif name == "Special Case":
                # Special case for "Special Case"
                name_lines = name.split('-')
                name = name_lines[0] + '\n' + name_lines[1]  # Add newline after the hyphen
            else:
                # Split on the last space if it doesn't contain "VET" or "RAT"
                name_lines = name.split(' ')
                if len(name_lines) > 1:
                    name = ' '.join(name_lines[:-1]) + '\n' + name_lines[-1]
                else:
                    name_lines = [name]

            # Create a copy of the name card for each person
            name_card_copy = name_card_template.copy()
            card_draw = ImageDraw.Draw(name_card_copy)

            # Calculate the total height of all the lines combined (with some padding between lines)
            line_heights = []
            total_text_height = 0
            for line in name.split('\n'):
                line_bbox = card_draw.textbbox((0, 0), line, font=name_font)
                line_height = line_bbox[3] - line_bbox[1]
                line_heights.append(line_height)
                total_text_height += line_height

            # Shift the text up by 8 pixels and calculate the vertical starting position
            start_y = (name_card_template.height - total_text_height) // 2 - 6  # Shifted up by 8px

            # Draw each line centered horizontally
            current_y = start_y
            first_line_raised = False  # To track if the first line has 'gjpqy' characters
            for i, line in enumerate(name.split('\n')):
                line_bbox = card_draw.textbbox((0, 0), line, font=name_font)
                line_width = line_bbox[2] - line_bbox[0]

                # Calculate the horizontal position to center this line
                line_x = (name_card_template.width - line_width) // 2

                # Draw the line at the correct position
                card_draw.text((line_x, current_y), line, font=name_font, fill="black")

                if i == 0 and any(char in line for char in 'gjpqy'):
                    # If the first line contains any of the letters, lower it by 7px (5px padding + 2px extra)
                    current_y += line_heights[i] + 7  # 5px for space, 2px additional for g, j, p, q, y
                    first_line_raised = True
                elif i == 0:
                    # If the first line doesn't contain those letters, add 7px space
                    current_y += line_heights[i] + 7
                else:
                    # For subsequent lines, add the usual space
                    if first_line_raised:
                        # If first line was adjusted for 'gjpqy', raise second line by 2px
                        current_y += line_heights[i] - 2  # Raise second line by 2px
                    else:
                        current_y += line_heights[i] + (5 if i == 0 else 0)


            # Position for the name card in the appropriate year strip
            card_x = 25 + year_text_x + year_text_width  # 25px to the right of the year text
            card_y = 300 + (strip_height * year_index) + (strip_height - name_card_template.height) // 2  # Vertically center in the strip based on year

            # Assign card and y position attributes to each person
            person_node.card = name_card_copy
            person_node.y = card_y
            # print(person_node.y)

            # Use the counter for the corresponding year to determine x_offset
            x_offset = card_x + year_counters[year] * 170  # Add offset for each subsequent name card
            year_counters[year] += 1  # Increment the counter for this year
            # print(f"{year_counters[year]}")

            card_file_path = os.path.join(cards_dir, f"{original_name}.png")
            person_node.card.save(card_file_path)

            # Paste the name card onto the image at the calculated position
            base_image.paste(name_card_copy, (x_offset, person_node.y), name_card_copy)

# Save the final image with name cards
base_image.save("final_image_with_name_cards_updated.png")
base_image.show()

输出镜像原作背景美学的示例

这是我使用 graphviz 的方法:

import openpyxl
from anytree import Node, RenderTree
import os
from graphviz import Digraph
from PIL import Image

# Create a directory to store the family tree images
trees_dir = "C:/Users/Chris Fitz/Documents/Fun/Trumpet History/trumpettree/trees"
cards_dir = "C:/Users/Chris Fitz/Documents/Fun/Trumpet History/trumpettree/cards"
os.makedirs(trees_dir, exist_ok=True)

# Load the .xlsx file
file_path = 'C:/Users/Chris Fitz/Documents/Fun/Trumpet History/trumpettree/sampletrees.xlsx'
workbook = openpyxl.load_workbook(file_path)
sheet = workbook.active

# Read the data starting from row 2 to the last row with data (max_row) in columns A to N
people_data = []
for row in sheet.iter_rows(min_row=2, max_row=sheet.max_row, min_col=1, max_col=14):
    person_info = [cell.value for cell in row]
    people_data.append(person_info)

# Tree Data Making
people_dict = {}  # Dictionary to hold people by their names
root_nodes = []  # List to hold the root nodes of multiple trees
parents_set = set()  # Sets to track parents and children
children_set = set()
parent_child_relationships = {}  # Dictionary to track parent-child relationships

# Create nodes for each person
for i, person_info in enumerate(people_data, start=2):  # i starts at 2 for row index
    name = person_info[0]
    parent_name = person_info[7]
    children_names = person_info[8:14]  # Columns I to N for children

    if name not in people_dict:
        person_node = Node(name)

        # If no parent is mentioned, add as a root node
        if parent_name:
            parent_node = people_dict.get(parent_name, Node(parent_name))
            people_dict[parent_name] = parent_node  # Add the parent to the dictionary
            person_node.parent = parent_node  # Set the parent for the current person
            parents_set.add(parent_name)
        else:
            root_nodes.append(person_node)

        people_dict[name] = person_node  # Store the person node

        # Create child nodes for the person
        for child_name in children_names:
            if child_name:
                if child_name not in people_dict:
                    child_node = Node(child_name, parent=person_node)
                    people_dict[child_name] = child_node
                children_set.add(child_name)

                if child_name not in parent_child_relationships:
                    parent_child_relationships[child_name] = set()
                parent_child_relationships[child_name].add(name)

# Function to generate the family tree graph using Graphviz
def generate_tree_graph(root_node):
    graph = Digraph(format='png', engine='dot', strict=True)

    def add_node_edges(node):
        # Image file path
        image_path = os.path.join(cards_dir, f"{node.name}.png")  # Assuming each person has a PNG image named after them

        if os.path.exists(image_path):
            # If the image exists, replace the node with the image, and remove any text label
            graph.node(node.name, image=image_path, shape="none", label='')
        else:
            # Fallback to text if no image is found (this can be further adjusted if needed)
            graph.node(node.name, label=node.name, shape='rect')

        # Add edges (parent-child relationships)
        if node.parent:
            graph.edge(node.parent.name, node.name)
        
        for child in node.children:
            add_node_edges(child)

    add_node_edges(root_node)
    return graph

# Generate and save tree images
tree_images = []
for root_node in root_nodes:
    tree_graph = generate_tree_graph(root_node)
    tree_image_path = os.path.join(trees_dir, f"{root_node.name}_family_tree")
    tree_graph.render(tree_image_path, format='png')
    tree_images.append(tree_image_path)

# Resize all tree images to be the same size
target_width = 800  # Target width for each tree image
target_height = 600  # Target height for each tree image
resized_images = []

for image_path in tree_images:
    image = Image.open(f"{image_path}.png")
    resized_images.append(image)

# Create a new image large enough to hold all resized tree images side by side
total_width = target_width * len(resized_images)
max_height = max(image.height for image in resized_images)

# Create a blank white image to paste the resized trees into
combined_image = Image.new('RGB', (total_width, max_height), color='white')

# Paste each resized tree image into the combined image
x_offset = 0
for image in resized_images:
    combined_image.paste(image, (x_offset, 0))
    x_offset += image.width

# Save the final combined image as a single PNG file
combined_image_path = 'C:/Users/Chris Fitz/Documents/Fun/Trumpet History/trumpettree/final_combined_family_tree.png'
combined_image.save(combined_image_path)

# Show the final combined image
combined_image.show()

使用正确的节点视觉效果显示树的输出示例

python charts graphviz anytree
1个回答
0
投票

子图和具有不可见边缘的垂直年份图可用于水平对齐项目。树的其他元素被分配与相应年份相同的

rank
。使用
splines=ortho
绘制直线。

用于编码:

  1. 创建年份图表
  2. 使用一组项目为每年创建子图
  3. 列出树元素之间的关系

DOT 语言的示例脚本:

graph mytree
{
  layout=dot
  splines=ortho
  ranksep=".1"
    
  // create graph of years
  node [shape=plaintext]
  edge [style=invis]
  2011 -- 2012 -- 2013 -- 2014 -- 2015 -- 2016 -- 2017 -- 2018
    
  node [shape=box]
  edge [style=""]
  // create subgraphs for each year
  subgraph y2011 { rank = same; 2011; t1; }
  subgraph y2012 { rank = same; 2012; }
  subgraph y2013 { rank = same; 2013; t2; }
  subgraph y2014 { rank = same; 2014; t3; }
  subgraph y2015 { rank = same; 2015; t4; }
  subgraph y2016 { rank = same; 2016; t5; }
  subgraph y2017 { rank = same; 2017; t6; }
  subgraph y2018 { rank = same; 2018; t7; t8; }
  // list the relationships
  t1 -- t2
  t2 -- t3
  t2 -- t4
  t4 -- t5
  t4 -- t6
  t6 -- t7
  t4 -- t8
}

结果:
tree chart, org-chart, family tree made with Graphviz

© www.soinside.com 2019 - 2024. All rights reserved.