Question: I have been making a tree of fraternity-adjacent bigs and littles and was looking for a way to automate it for changes as more people join. Everyone's names and years, big, and littles are in an Excel spreadsheet. What could I use to emulate the design I did here? Specifically, the stem style and ability to space nodes further away depending on their year. This is the design I want to automate:  I tried using anytree and graphviz but couldn't find a way to emulate the stems or an easy solution for spacing based on years. Here's sample data:    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       Here's my basic approach using anytree and the results: 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()  example of output mirroring background aestetics of original work Here's my approach with 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()  example of output showing trees using proper node visuals

Answer: Solved it. from anytree import Node, RenderTree from collections import Counter import os import openpyxl from PIL import Image, ImageDraw, ImageFont import re # 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/YJMB Trumpet Trees.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 # Variable to track the current tree number tree_number = 0 # Start with tree 1 # A counter for nodes without children end_id_counter = 1 years = [] x_max = 0 # 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] # Name is in the first column (column A) rat_year = str(person_info[1])[:4] # Year they joined the marching band (second column) if rat_year.isdigit(): years.append(int(rat_year)) instrument = person_info[2] parent_name = person_info[7] # Column H for VET (8th column) children_names = person_info[8:14] # Columns I to N for RATs (9th to 14th columns) # Determine if the node has children (if any of the children_names is non-empty) has_children = any(child_name for child_name in children_names if child_name) if i < len(people_data) and not person_info[7]: # Parent is empty in that row tree_number += 1 # Increment tree number for the next family tree # Check if this name is already in the people_dict if name in people_dict: # If the person already exists in the dictionary, retrieve their node person_node = people_dict[name] # Update the rat_year for the existing person node if necessary person_node.rat_year = rat_year person_node.instrument = instrument else: # If the person does not exist in the dictionary, create a new node person_node = Node(name, tree_number=tree_number, id=0, has_children=has_children, rat_year=rat_year, x_coord=None, y_coord=None, instrument=instrument, children_nodes=[]) # Added children_nodes # 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, tree_number=tree_number, id=0, has_children=False, rat_year=None, x_coord=None, y_coord=None, instrument=None, children_nodes=[]) # Added children_nodes 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) # After setting the parent, update the parent's has_children flag parent_node.has_children = True # Set has_children to True for the parent node parent_node.children_nodes.append(person_node) # Add to parent's children_nodes else: root_nodes.append(person_node) # Add to root_nodes list people_dict[name] = person_node # Add the new person node to the dictionary # Now create child nodes for the given children names for child_name in children_names: if child_name: if child_name not in people_dict: child_node = Node(child_name, parent=person_node, tree_number=tree_number, id=0, has_children=False, rat_year=rat_year, x_coord=None, y_coord=None, instrument=instrument, children_nodes=[]) # Added children_nodes people_dict[child_name] = child_node children_set.add(child_name) # If the child node has been created, we need to ensure the parent's has_children flag is True person_node.has_children = True person_node.children_nodes.append(people_dict[child_name]) # Add child to parent's children_nodes if child_name not in parent_child_relationships: parent_child_relationships[child_name] = set() parent_child_relationships[child_name].add(name) # After all nodes are created, we calculate x and y coordinates for each node new_id = 1 start_x_coord = 200 curr_tree = 1 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 end_id_counter = 1 # 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) # print(f"\nFamily Tree starting from {root_node.name}:") for pre, fill, node in RenderTree(root_node): node.id = new_id new_id += 1 if not node.has_children: new_tree = node.tree_number if new_tree != curr_tree: start_x_coord += 200 curr_tree = node.tree_number node.end_id = end_id_counter end_id_counter += 1 node.x_coord = start_x_coord start_x_coord += 170 else: node.end_id = 0 if getattr(node, 'x_coord', 'N/A') and getattr(node, 'x_coord', 'N/A') > x_max: x_max = node.x_coord # Print details for each node # print(f"{pre}{node.name} (ID: {node.id}, Tree Number: {node.tree_number}, Has Children: {node.has_children}, End ID: {getattr(node, 'end_id', 'N/A')}, X Coord: {getattr(node, 'x_coord', 'N/A')}, Y Coord: {getattr(node, 'y_coord', 'N/A')}, Rat Year: {getattr(node, 'rat_year', 'N/A')}, Instrument: {getattr(node, 'children_nodes', 'N/A')})") # Now assign X coordinates to nodes where X is None (based on their children) while any(node.x_coord is None for node in people_dict.values()): for node in people_dict.values(): if node.has_children: children_with_coords = [child for child in node.children if child.x_coord is not None] if len(children_with_coords) == len(node.children): # Check if all children have x_coord average_x_coord = sum(child.x_coord for child in children_with_coords) / len(children_with_coords) node.x_coord = round(average_x_coord) # Set the parent's x_coord to the average # 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) # print(f"\nFamily Tree starting from {root_node.name}:") # for pre, fill, node in RenderTree(root_node): # print(f"{pre}{node.name} (ID: {node.id}, Tree Number: {node.tree_number}, Has Children: {node.has_children}, End ID: {getattr(node, 'end_id', 'N/A')}, Children Nodes: {getattr(node, 'children_nodes', 'N/A')})") # fix the rat_year attribute for even-numbered generations (done) # use that to determine y value (done) # determine x values from the bottom up recursively (done) # # Print duplicate ids, if any # if duplicates: # print("\nDuplicate IDs found:", duplicates) # else: # print("\nNo duplicates found.") #----------------------------------------------------------# # Tree Chart Making # Extract the years from the first four characters in Column B (done in lines 51-53 now) # Calculate the range of years (from the minimum year to the maximum year) (107-109) # Create a base image with a solid color (header space) base_width = x_max + 200 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 node in people_dict.values(): # Choose the correct name card template based on Column C if node.instrument and "Trumpet" not in node.instrument: name_card_template = Image.open("C:/Users/Chris Fitz/Documents/Fun/Trumpet History/trumpettree/blank_blue_name_card.png") else: name_card_template = Image.open("C:/Users/Chris Fitz/Documents/Fun/Trumpet History/trumpettree/blank_name_card.png") if node.rat_year: year_string = str(node.rat_year)[:4] if year_string.isdigit(): year = int(year_string) year_index = year - min_year # Find the corresponding year index (from 0 to year_range-1) name = node.name # Check if the name contains "VET" or "RAT" if "VET" in name or "RAT" in name: name_lines = name.split(' ', 1) name = name_lines[0] + '\n' + name_lines[1] elif name == "Xxx Xxxxxx-Xxxxxxx": name_lines = name.split('-') name = name_lines[0] + '\n' + name_lines[1] # Add newline after the hyphen else: 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 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] line_x = (name_card_template.width - line_width) // 2 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'): current_y += line_heights[i] + 7 first_line_raised = True elif i == 0: current_y += line_heights[i] + 7 else: if first_line_raised: current_y += line_heights[i] - 2 else: current_y += line_heights[i] + (5 if i == 0 else 0) # Position for the name card in the appropriate year strip card_y = 300 + (strip_height * year_index) + (strip_height - name_card_template.height) // 2 # Vertically center in the strip based on year node.y_coord = card_y # Assign card and y position attributes to each person person_node.card = name_card_copy person_node.y_coord = card_y # Use the counter for the corresponding year to determine x_offset year_counters[year] += 1 card_file_path = os.path.join(cards_dir, f"{node.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, (node.x_coord, node.y_coord), name_card_copy) # Create a list of names from the spreadsheet, split on newlines where appropriate for node in people_dict.values(): # Add black rectangle beneath the name card if the node has children if node.has_children: if len(node.children_nodes) == 1: child_node = getattr(node, 'children_nodes', 'N/A')[0] # Only one child, so get the first (and only) child # print(getattr(child_node, 'y_coord', 'N/A')) # Coordinates for the rectangle (centered beneath the name card) rect_x = node.x_coord + (name_card_template.width - 6) // 2 # Center the rectangle rect_y = node.y_coord + (name_card_template.height - 2) # Just below the name card rect_y_bottom = int(getattr(child_node, 'y_coord', 'N/A')) + 1 # Bottom of rectangle is aligned with the y_coord of the child # Draw the rectangle draw.rectangle([rect_x - 1, rect_y, rect_x + 6, rect_y_bottom], fill=(111, 111, 111)) else: # Calculate the leftmost and rightmost x-coordinates of the child nodes min_x = min(getattr(child, 'x_coord', 0) for child in node.children_nodes) max_x = max(getattr(child, 'x_coord', 0) for child in node.children_nodes) # Calculate the center of the rectangle (between the leftmost and rightmost child nodes) rect_x = (min_x + max_x) // 2 # Center x-coordinate between the children rect_y = (node.y_coord + min(getattr(child, 'y_coord', node.y_coord) for child in node.children_nodes)) // 2 rect_width = max_x - min_x draw.rectangle([rect_x - rect_width // 2 + 75, rect_y + 36, rect_x + rect_width // 2 + 75, rect_y + 6 + 37], fill=(111, 111, 111)) parent_y_bottom = rect_y + 36 # Coordinates for the rectangle (centered beneath the name card) rect_x = node.x_coord + (name_card_template.width - 6) // 2 # Center the rectangle rect_y = node.y_coord + (name_card_template.height - 2) # Just below the name card draw.rectangle([rect_x - 1, rect_y, rect_x + 6, parent_y_bottom], fill=(111, 111, 111)) # Now create a vertical rectangle for each child node for child in node.children_nodes: child_x = getattr(child, 'x_coord', 0) child_center_x = child_x + (name_card_template.width - 6) // 2 # x-center of the child child_y_bottom = parent_y_bottom # The bottom of the rectangle should align with the parent's bottom # Draw the rectangle from the center of the child node up to the parent's y-bottom draw.rectangle([child_center_x - 1, child_y_bottom, child_center_x + 6, getattr(child, 'y_coord', 0) + 1], fill=(111, 111, 111)) # 6px wide # 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) print(f"\nFamily Tree starting from {root_node.name}:") for pre, fill, node in RenderTree(root_node): # print(f"{pre}{node.name} (ID: {node.id}, Tree Number: {node.tree_number}, Has Children: {node.has_children}, End ID: {getattr(node, 'end_id', 'N/A')}, Children Nodes: {getattr(node, 'children_nodes', 'N/A')})") print(f"{pre}{node.name} (ID: {node.id}, Tree Number: {node.tree_number}, Has Children: {node.has_children}, End ID: {getattr(node, 'end_id', 'N/A')}, Y Coord: {getattr(node, 'y_coord', 'N/A')}, Children: {len(getattr(node, 'children_nodes', 'N/A'))})") # Save the final image with name cards and black rectangles base_image.save("YJMB_Trumpet_Section_Family_Trees_2024.png") base_image.show()