如何自动化生成支持自定义枝干样式与按年份调整节点间距的兄弟会成员关系树状图
如何自动化生成支持自定义枝干样式与按年份调整节点间距的兄弟会成员关系树状图
我一直在制作兄弟会相关的“大哥-小弟”关系树,现在想找个方法自动更新——毕竟会有更多人加入。所有人的姓名、年级、对应的大哥和小弟信息都存在Excel表格里。我想复刻自己手动做的这个设计,特别是枝干样式和按年份调整节点间距的功能,有什么工具或方法可以实现吗?
我想要复刻的设计样式

我试过用anytree和graphviz,但没找到复刻枝干样式的方法,也没找到简单的按年份调整间距的方案。
示例数据
| Name | Year | Instrument | Extra | Extra | Extra | Extra | Big | Little 1 | Little 2 | Little 3 |
|---|---|---|---|---|---|---|---|---|---|---|
| T1P1 | 1990 | Trumpet | T1P2 | |||||||
| T1P2 | 1991 | Trumpet | T1P1 | |||||||
| T2P1 | 1997 | Trumpet | T2P2 | |||||||
| T2P2 | 2001 | Trumpet | T2P1 | T2P3 | T2P4 | T2P5 | ||||
| T2P3 | 2003 | Trumpet | T2P2 | |||||||
| T2P4 | 2004 | Trumpet | T2P2 | |||||||
| T2P5 | 2006 | Trumpet | T2P2 | |||||||
| T3P1 | 2000 | Trumpet | T3P2 | |||||||
| T3P2 | 2004 | Trumpet | T3P1 | T3P3 | T3P4 | |||||
| T3P3 | 2005 | Trumpet | T3P2 | T3P5 | T3P6 | |||||
| T3P5 | 2006 | Trumpet | T3P3 | |||||||
| T3P6 | 2007 | Trumpet | T3P3 | |||||||
| T3P4 | 2006 | Trumpet | T3P2 | T3P7 | ||||||
| T3P7 | 2010 | Flute | 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
备注:内容来源于stack exchange,提问作者Chris Fitzpatrick




