import math
def create_lattice_svg():
# --- Configuration ---
# SVG dimensions.
row_height = 95
node_spacing = 77
node_radius = 20
# --- Lattice Data ---
# Define the numbers for each row of the lattice based on number of prime factors.
# The key (0-4) serves as both the number of factors and the row identifier.
lattice_data = {
4: [0], # The top node (0)
3: [8, 12, 18, 20, 27], # Numbers with three prime factors
2: [4, 6, 9, 10, 14], # Numbers with two prime factors
1: [2, 3, 5, 7], # Prime numbers
0: [1] # The number 1 (zero factors)
}
# --- Create a list of node objects with positions ---
nodes = []
# Determine the widest row to calculate total SVG width.
max_nodes_per_row = 0
for row in lattice_data.values():
if len(row) > max_nodes_per_row:
max_nodes_per_row = len(row)
# Adjust for ellipsis node
max_nodes_per_row += 1
# Calculate total SVG dimensions with padding.
width = max_nodes_per_row * node_spacing
num_rows = len(lattice_data)
height = num_rows * row_height
# Build the nodes list and calculate their positions.
row_keys = sorted(lattice_data.keys(), reverse=True)
for i, num_factors in enumerate(row_keys):
numbers = sorted(lattice_data[num_factors])
# Ellipses are added to rows that aren't the top or bottom
has_ellipsis = num_factors not in [0, 4]
# Calculate the y-position for the current row, from the top down.
y_pos = row_height * i + row_height / 2
# Calculate x-positions to center the numbers in the row.
num_nodes_in_row = len(numbers) + (1 if has_ellipsis else 0)
half_width = width / 2
half_node_span = ((num_nodes_in_row -1) / 2) * node_spacing
start_x = half_width - half_node_span
for j, number in enumerate(numbers):
x_pos = start_x + j * node_spacing
node = {
'value': number,
'type': 'number',
'num_factors': num_factors,
'x_pos': x_pos,
'y_pos': y_pos
}
nodes.append(node)
# Add positions for ellipsis nodes.
if has_ellipsis:
ellipsis_x_pos = start_x + len(numbers) * node_spacing
ellipsis_y_pos = y_pos
ellipsis_node = {
'value': '···',
'type': 'ellipsis',
'num_factors': num_factors,
'x_pos': ellipsis_x_pos,
'y_pos': ellipsis_y_pos
}
nodes.append(ellipsis_node)
# --- Start the SVG string with the header and styles ---
svg_string = f'''<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css">
.solid-line {{
fill: none;
stroke: #000000;
stroke-width: 1.5;
stroke-linecap: butt;
stroke-linejoin: miter;
stroke-miterlimit: 4;
stroke-opacity: 1;
stroke-dasharray: none;
stroke-dashoffset: 0;
}}
.dotted-line {{
fill: none;
stroke: #000000;
stroke-width: 1.5;
stroke-linecap: butt;
stroke-linejoin: miter;
stroke-miterlimit: 4;
stroke-opacity: 1;
stroke-dasharray: 1.5, 4.5;
stroke-dashoffset: 0;
}}
.node-circle {{
fill: #ffffff;
fill-opacity: 1;
fill-rule: evenodd;
stroke: none;
}}
.node-text {{
font-size: 30px;
font-style: normal;
font-variant: normal;
font-weight: normal;
font-stretch: normal;
text-indent: 0;
text-align: start;
text-decoration: none;
line-height: normal;
letter-spacing: normal;
word-spacing: normal;
text-transform: none;
direction: ltr;
block-progression: tb;
writing-mode: lr-tb;
text-anchor: middle;
dominant-baseline: middle;
color: #000000;
fill: #000000;
fill-opacity: 1;
fill-rule: nonzero;
stroke: none;
stroke-width: 1px;
marker: none;
visibility: visible;
display: inline;
overflow: visible;
enable-background: accumulate;
font-family: Liberation Serif, Times New Roman, serif;
}}
</style>
</defs>'''
# Painting order: lines first, then circles, then text on top. This is the order in which we add elements to the SVG string.
# --- Step 1: Draw all Lattice Lines ---
for parent_node in nodes:
for child_node in nodes:
# Skip if they are the same node
if parent_node == child_node:
continue
# Only connect nodes that are one level apart in terms of prime factors
if parent_node['num_factors'] != child_node['num_factors'] + 1:
continue
x1, y1 = parent_node['x_pos'], parent_node['y_pos']
x2, y2 = child_node['x_pos'], child_node['y_pos']
line_class = None
# Solid lines between numbers based on divisibility
if (parent_node['type'] == 'number' and child_node['type'] == 'number' and
parent_node['value'] % child_node['value'] == 0):
line_class = "solid-line"
# Dotted lines from numbers/ellipsis in the row below to the ellipsis
if (parent_node['type'] == 'ellipsis'):
line_class = "dotted-line"
# Dotted lines from 0 to the top row
if (parent_node['value'] == 0 and child_node['num_factors'] == 3):
line_class = "dotted-line"
# If a condition is met, draw the line
if line_class:
# Adjust start and end points of lines to meet the circle edges.
dy = y2 - y1
dx = x2 - x1
angle = math.atan2(dy, dx)
x1_adj = x1 + node_radius * math.cos(angle)
y1_adj = y1 + node_radius * math.sin(angle)
x2_adj = x2 - node_radius * math.cos(angle)
y2_adj = y2 - node_radius * math.sin(angle)
svg_string += f'''
<line x1="{x1_adj}" y1="{y1_adj}" x2="{x2_adj}" y2="{y2_adj}" class="{line_class}" />'''
# --- Step 2: Draw all Lattice Nodes ---
# actually, it looks better without the circles, they just cut off the lines
#for node in nodes:
# svg_string += f'''
#<circle cx="{node['x_pos']}" cy="{node['y_pos']}" r="{node_radius}" class="node-circle" />'''
# --- Step 3: Draw all Lattice Node Text ---
for node in nodes:
text_content = node['value']
svg_string += f'''
<text x="{node['x_pos']}" y="{node['y_pos']}" dy="0.1em" class="node-text">{text_content}</text>'''
# --- Close the SVG tag ---
svg_string += "\n</svg>"
return svg_string
if __name__ == "__main__":
svg_code = create_lattice_svg()
# Write the SVG string to a file
file_path = "divisibility_lattice.svg"
with open(file_path, "w") as f:
f.write(svg_code)
print(f"SVG file generated successfully at: {file_path}")