File size: 8,008 Bytes
7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd b4ddcf6 7cd1bcd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
import graphviz
import json
from tempfile import NamedTemporaryFile
import os
def generate_timeline_diagram(json_input: str, output_format: str) -> str:
"""
Generates a serpentine timeline diagram from JSON input.
Args:
json_input (str): A JSON string describing the timeline structure.
It must follow the Expected JSON Format Example below.
Expected JSON Format Example:
{
"title": "AI Development Timeline",
"events_per_row": 4,
"events": [
{
"id": "event_1",
"label": "Machine Learning Foundations",
"date": "1950-1960",
"description": "Early neural networks and perceptrons"
},
{
"id": "event_2",
"label": "Expert Systems Era",
"date": "1970-1980",
"description": "Rule-based AI systems"
},
{
"id": "event_3",
"label": "Neural Network Revival",
"date": "1980-1990",
"description": "Backpropagation algorithm"
}
]
}
Returns:
str: The filepath to the generated PNG image file.
"""
try:
if not json_input.strip():
return "Error: Empty input"
data = json.loads(json_input)
if 'events' not in data:
raise ValueError("Missing required field: events")
dot = graphviz.Digraph(
name='Timeline',
format='png',
graph_attr={
'rankdir': 'TB', # Top-to-Bottom for better control
'splines': 'ortho', # Straight lines with 90-degree bends
'bgcolor': 'white', # White background
'pad': '0.5', # Padding around the graph
'nodesep': '1.5', # Spacing between nodes
'ranksep': '1.5' # Spacing between ranks
}
)
base_color = '#19191a' # Hardcoded base color
title = data.get('title', '')
events = data.get('events', [])
events_per_row = data.get('events_per_row', 4) # Default to 4 events per row
if not events:
raise ValueError("Timeline must contain at least one event")
# Add title node if provided
if title:
dot.node(
'title',
title,
shape='plaintext',
fontsize='18',
fontweight='bold',
fontcolor=base_color
)
# Calculate positions and create serpentine layout
total_events = len(events)
previous_event_id = None
# Create invisible nodes for positioning and rank control
rows = []
current_row = []
# Group events into rows
for i, event in enumerate(events):
current_row.append(event)
if len(current_row) == events_per_row or i == total_events - 1:
rows.append(current_row)
current_row = []
# Process each row and create serpentine connections
for row_idx, row in enumerate(rows):
# Determine if row should be reversed (serpentine pattern)
is_reversed = row_idx % 2 == 1
if is_reversed:
row = row[::-1] # Reverse the row for serpentine effect
# Create invisible nodes for row positioning
row_nodes = []
for event_idx, event in enumerate(row):
original_idx = events.index(event)
event_id = event.get('id', f'event_{original_idx}')
event_label = event.get('label', f'Event {original_idx+1}')
event_date = event.get('date', '')
event_description = event.get('description', '')
# Create full label with date and description
if event_date and event_description:
full_label = f"{event_date}\\n{event_label}\\n{event_description}"
elif event_date:
full_label = f"{event_date}\\n{event_label}"
elif event_description:
full_label = f"{event_label}\\n{event_description}"
else:
full_label = event_label
# Calculate color opacity based on original position in timeline
if total_events == 1:
opacity = 'FF'
else:
opacity_value = int(255 * (1.0 - (original_idx * 0.7 / (total_events - 1))))
opacity = format(opacity_value, '02x')
node_color = f"{base_color}{opacity}"
font_color = 'white' if original_idx < total_events * 0.7 else 'black'
# Add the event node
dot.node(
event_id,
full_label,
shape='box',
style='filled,rounded',
fillcolor=node_color,
fontcolor=font_color,
fontsize='12',
width='2.5',
height='1.2'
)
row_nodes.append(event_id)
# Create horizontal connections within the row
for i in range(len(row_nodes) - 1):
dot.edge(
row_nodes[i],
row_nodes[i + 1],
color='#666666',
arrowsize='0.8',
penwidth='2'
)
# Connect to previous row (serpentine connection)
if row_idx > 0:
# Connect last node of previous row to first node of current row
prev_row_nodes = getattr(generate_timeline_diagram, 'prev_row_nodes', [])
if prev_row_nodes:
# Connect the end of previous row to start of current row
if (row_idx - 1) % 2 == 0: # Previous row was left-to-right
connection_start = prev_row_nodes[-1] # Last node of previous row
else: # Previous row was right-to-left
connection_start = prev_row_nodes[0] # First node of previous row (which was last visually)
if row_idx % 2 == 0: # Current row is left-to-right
connection_end = row_nodes[0] # First node of current row
else: # Current row is right-to-left
connection_end = row_nodes[-1] # Last node of current row (which will be first visually)
dot.edge(
connection_start,
connection_end,
color='#666666',
arrowsize='0.8',
penwidth='2'
)
# Store current row nodes for next iteration
generate_timeline_diagram.prev_row_nodes = row_nodes
# Connect title to first event if title exists and this is the first row
if title and row_idx == 0:
first_event = row_nodes[0] if row_idx % 2 == 0 else row_nodes[-1]
dot.edge('title', first_event, style='invis')
# Clean up the stored attribute
if hasattr(generate_timeline_diagram, 'prev_row_nodes'):
delattr(generate_timeline_diagram, 'prev_row_nodes')
with NamedTemporaryFile(delete=False, suffix=f'.{output_format}') as tmp:
dot.render(tmp.name, format=output_format, cleanup=True)
return f"{tmp.name}.{output_format}"
except json.JSONDecodeError:
return "Error: Invalid JSON format"
except Exception as e:
return f"Error: {str(e)}" |