Archives
Trending
Support
Login
clear text
XML
Django
JavaScript
MATLAB
C
C++
C#
Python
SQL
Shell
Bash
Markdown
YAML
JSON
HTML
CSS
PHP
Java
Ruby
Go
Rust
Swift
Kotlin
Arduino
TypeScript
Perl
Autohotkey
Lua
SQF
R
Scala
Haskell
Groovy
Dart
Clojure
VB.NET
Objective-C
PowerShell
Bash
CoffeeScript
Verilog
# timeline_visualizer.py # Requires: matplotlib, numpy # Usage: create instance, add people/events, then render() import math import datetime from typing import List, Dict, Any, Tuple, Optional import matplotlib.pyplot as plt import matplotlib.colors as mcolors from matplotlib.patches import FancyBboxPatch, Rectangle import numpy as np class TimelineVisualizer: """ Simple, reusable visualizer that draws horizontal age-based timelines for multiple people (parallel bars). X axis = AGE (not calendar year). """ def __init__(self, people: List[Dict[str, Any]], age_max: int = 100, size: Tuple[int, int] = (1920, 1080), dpi: int = 100, font_family: str = "DejaVu Sans", pastel_palette: Optional[List[str]] = None): """ people: list of dicts, each with keys: - 'name': str - 'birth_year': int - 'events': list of dicts with either: Point event: {'year': int, 'label': str, 'color': Optional[str], 'position': Optional[str]} position: 'close_to_bar', 'away_from_bar', or 'auto' (default) Duration event: {'start_year': int, 'end_year': int, 'label': str} age_max: maximum age shown on X axis size: (width_px, height_px) """ self.people = people self.age_max = age_max self.size = size self.dpi = dpi self.font_family = font_family # Modern, vibrant social media color palette self.pastel_palette = pastel_palette or ["#FF6B6B", "#4ECDC4", "#45B7D1", "#FFA07A"] plt.rcParams['font.family'] = self.font_family def year_to_age(self, birth_year: int, year: int) -> float: return year - birth_year def render(self, filename: str = "parallel_timelines.png", show_grid_every: int = 10, bar_height: float = 0.6): # Figure sizing: Matplotlib expects inches fig_w = self.size[0] / self.dpi fig_h = self.size[1] / self.dpi fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=self.dpi) # Y positions for parallel bars n = len(self.people) if n == 0: raise ValueError("No people provided to visualize.") ys = np.linspace(n - 1, 0, n) # top-to-bottom order # Draw a modern infographic-style background fig.patch.set_facecolor("#F8F9FA") ax.set_facecolor("#F8F9FA") # Draw dashed vertical separators (every show_grid_every years) for age in range(0, self.age_max + 1, show_grid_every): ax.vlines(age, -1, n, linestyles='dashed', linewidth=0.8, alpha=0.3, color='#666666') # Draw horizontal bars (life spans) and events for i, person in enumerate(self.people): y = ys[i] name = person.get('name', f"Person {i+1}") birth = int(person['birth_year']) # Determine max age for this person (special handling for 1985) person_age_max = 40 if birth == 1985 else self.age_max # draw full-age bar (0 .. age_max) with stroke and rounded corners bar_color = self.pastel_palette[i % len(self.pastel_palette)] # Draw bar with rounded corners bar = FancyBboxPatch((0, y - bar_height/2), person_age_max, bar_height, boxstyle="round,pad=0.02", edgecolor='none', # We'll draw borders manually linewidth=0, facecolor=bar_color, alpha=0.85, zorder=2) ax.add_patch(bar) # Manually draw borders with precise positioning border_width = 2.5 border_color = '#2C3E50' border_offset = border_width / 2000 # Slight inset to align with bar edge # Special fade effect for 1985 if birth == 1985: # Add gradient fade from bar color to white background fade_width = 1.5 # Fade over about 1.5 age units num_steps = 30 background_color = mcolors.to_rgb('#F8F9FA') bar_rgb = mcolors.to_rgb(bar_color) for step in range(num_steps): x_start = person_age_max + (step * fade_width / num_steps) x_width = fade_width / num_steps # Interpolate between bar color and white background ratio = step / num_steps r = bar_rgb[0] * (1 - ratio) + background_color[0] * ratio g = bar_rgb[1] * (1 - ratio) + background_color[1] * ratio b = bar_rgb[2] * (1 - ratio) + background_color[2] * ratio fade_color = (r, g, b) fade_rect = Rectangle((x_start, y - bar_height/2), x_width, bar_height, facecolor=fade_color, edgecolor='none', alpha=0.85, zorder=2.1) ax.add_patch(fade_rect) # Fading top border for step in range(num_steps): x_start = person_age_max + (step * fade_width / num_steps) x_end = person_age_max + ((step + 1) * fade_width / num_steps) ratio = step / num_steps alpha_val = 1.0 - ratio ax.plot([x_start, x_end], [y + bar_height/2 - border_offset, y + bar_height/2 - border_offset], linewidth=border_width, color=border_color, alpha=alpha_val, solid_capstyle='butt', zorder=3) # Fading bottom border for step in range(num_steps): x_start = person_age_max + (step * fade_width / num_steps) x_end = person_age_max + ((step + 1) * fade_width / num_steps) ratio = step / num_steps alpha_val = 1.0 - ratio ax.plot([x_start, x_end], [y - bar_height/2 + border_offset, y - bar_height/2 + border_offset], linewidth=border_width, color=border_color, alpha=alpha_val, solid_capstyle='butt', zorder=3) # Top border (up to person_age_max) ax.plot([0, person_age_max], [y + bar_height/2 - border_offset, y + bar_height/2 - border_offset], linewidth=border_width, color=border_color, solid_capstyle='round', zorder=3) # Bottom border (up to person_age_max) ax.plot([0, person_age_max], [y - bar_height/2 + border_offset, y - bar_height/2 + border_offset], linewidth=border_width, color=border_color, solid_capstyle='round', zorder=3) # Left border ax.plot([0, 0], [y - bar_height/2 + border_offset, y + bar_height/2 - border_offset], linewidth=border_width, color=border_color, solid_capstyle='round', zorder=3) # Right border - only for 1885, not for 1985 (ongoing) if birth != 1985: ax.plot([person_age_max, person_age_max], [y - bar_height/2 + border_offset, y + bar_height/2 - border_offset], linewidth=border_width, color=border_color, solid_capstyle='round', zorder=3) # person label positioning: top for 1985 (to avoid dot overlap), bottom for 1885 if birth == 1985: # Upper left corner to avoid overlapping with dots at bottom ax.text(1, y + bar_height/2 - 0.05, f"{name}", ha='left', va='top', fontsize=16, weight='bold', color='white', zorder=3, bbox=dict(boxstyle="round,pad=0.3", facecolor='#2C3E50', alpha=0.8, edgecolor='none')) else: # Bottom left corner (default) ax.text(1, y - bar_height/2 + 0.05, f"{name}", ha='left', va='bottom', fontsize=16, weight='bold', color='white', zorder=3, bbox=dict(boxstyle="round,pad=0.3", facecolor='#2C3E50', alpha=0.8, edgecolor='none')) # events: annotate on the bar # Divide bar height into thirds for vertical positioning bar_third = bar_height / 3 events = person.get('events', []) for ev_idx, ev in enumerate(events): label = ev.get('label', '') # Check if this is a duration event (has start_year and end_year) is_duration = 'start_year' in ev and 'end_year' in ev if is_duration: # Duration event (e.g., wars, political eras) start_year = int(ev['start_year']) end_year = int(ev['end_year']) start_age = self.year_to_age(birth, start_year) end_age = self.year_to_age(birth, end_year) # skip if completely outside the plotted range if end_age < 0 or start_age > person_age_max: continue # Clamp to visible range start_age = max(0, start_age) end_age = min(person_age_max, end_age) mid_age = (start_age + end_age) / 2 # Position closer to vertical center of the chart # Move toward the center between the two bars if birth == 1885: y_duration = y - bar_third * 0.5 # Move down slightly else: y_duration = y + bar_third * 0.5 # Move up slightly # Draw horizontal line connecting the markers ax.plot([start_age, end_age], [y_duration, y_duration], linewidth=2.5, color='#2C3E50', alpha=0.8, solid_capstyle='butt', zorder=5) # Draw semi-circle markers at start and end (like split dots) # Left semi-circle (start) ax.plot(start_age, y_duration, marker='o', markersize=12, markeredgecolor='white', markerfacecolor='#2C3E50', markeredgewidth=2, zorder=6) # Right semi-circle (end) ax.plot(end_age, y_duration, marker='o', markersize=12, markeredgecolor='white', markerfacecolor='#2C3E50', markeredgewidth=2, zorder=6) # Label above the line (event name) ax.text(mid_age, y_duration + 0.08, label, ha='center', va='bottom', fontsize=12, weight='600', color='#2C3E50', zorder=7) # Years below the line ax.text(mid_age, y_duration - 0.08, f"{start_year}–{end_year}", ha='center', va='top', fontsize=11, weight='500', color='#2C3E50', zorder=7) else: # Regular point event year = int(ev['year']) age = self.year_to_age(birth, year) # skip if outside the plotted range if age < 0 or age > person_age_max: continue # Position to the outside: top third for 1885, bottom third for 1985 if birth == 1885: y_point = y + bar_third # Move towards top else: y_point = y - bar_third # Move towards bottom # marker with uniform color for clean look ax.plot(age, y_point, marker='o', markersize=12, markeredgecolor='white', markerfacecolor='#2C3E50', markeredgewidth=2, zorder=5) # smart annotation placement: above for 1885 (top), below for 1985 (bottom) # Position controlled by "position" field: "close_to_bar" or "away_from_bar" # Increased offsets for 3-row labels position = ev.get('position', 'auto') if birth == 1885: # Labels on top base_offset = 0.6 # More space from bar extra_offset = 0.4 if position == 'close_to_bar': vert_offset = base_offset elif position == 'away_from_bar': vert_offset = base_offset + extra_offset else: # Auto: alternate based on index (fallback) vert_offset = base_offset + (extra_offset if ev_idx % 2 == 1 else 0) else: # Labels on bottom (1985) base_offset = -0.7 extra_offset = -0.4 if position == 'close_to_bar': vert_offset = base_offset elif position == 'away_from_bar': vert_offset = base_offset + extra_offset else: # Auto: alternate based on index (fallback) vert_offset = base_offset + (extra_offset if ev_idx % 2 == 1 else 0) ax.annotate(f"{label}\n{year}\n{age} éves", xy=(age, y_point), xytext=(age, y_point + vert_offset), ha='center', va='center', fontsize=13, weight='500', textcoords='data', arrowprops=dict(arrowstyle="-", linewidth=1.2, alpha=0.7, color='#2C3E50'), bbox=dict(boxstyle="round,pad=0.4", fc="white", ec="#2C3E50", linewidth=1.5, alpha=0.95), zorder=6) # Labels, limits, aesthetics - adjusted for better label visibility ax.set_xlim(-5, self.age_max + 5) ax.set_ylim(-2, n + 0.3) # Extra space at bottom and top for labels ax.set_yticks([]) # we use inline labels at left instead # X-axis ticks for age markers ax.set_xticks(range(0, self.age_max + 1, show_grid_every)) ax.tick_params(axis='x', labelsize=12, colors='#2C3E50') ax.set_xlabel("Életkor (év)", fontsize=16, weight='bold', color='#2C3E50', labelpad=10) ax.set_title("Néhány meghatározó felfedezés és esemény két, 100 év különbséggel született ember életében", fontsize=22, weight='bold', pad=20, color='#2C3E50') # Tidy up spines for spine in ['top', 'right', 'left']: ax.spines[spine].set_visible(False) ax.spines['bottom'].set_linewidth(1.5) ax.spines['bottom'].set_color('#2C3E50') plt.tight_layout() plt.savefig(filename, dpi=self.dpi, bbox_inches='tight', facecolor=fig.get_facecolor()) plt.close(fig) print(f"Saved timeline to: {filename}") if __name__ == "__main__": # Example usage with the two people you described (corrected ages) people = [ { "name": "Születési év: 1885", "birth_year": 1885, "events": [ {"year": 1895, "label" : "Némafilm", "position" : "close_to_bar"}, {"year": 1910, "label": "Ford T-modell", "color": "#6FBF73", "position" : "close_to_bar"}, {"year": 1928, "label": "Penicillin", "color": "#6FBF73", "position" : "away_from_bar"}, {"start_year": 1914, "end_year": 1918, "label": "1. világháború"}, {"year": 1925, "label": "Magyar Rádió", "color": "#F2C57C", "position" : "close_to_bar"}, {"start_year": 1939, "end_year": 1945, "label": "2. világháború"}, {"year": 1947, "label" : "Tranzisztor", "color": "#6FBF73", "position" : "close_to_bar"}, {"year": 1957, "label": "Magyar Televízió", "color": "#A3C4F3", "position" : "close_to_bar"}, {"year": 1964, "label": "Audiokazetta", "color": "#A3C4F3", "position" : "away_from_bar"}, {"year": 1976, "label": "VHS", "color": "#6FBF73", "position" : "close_to_bar"}, {"year": 1982, "label": "Commodore 64", "color": "#A18CD1", "position" : "away_from_bar"}, ] }, { "name": "Születési év: 1985", "birth_year": 1985, "events": [ {"year": 1986, "label": "Csernobil", "color": "#6FBF73", "position" : "close_to_bar"}, {"year": 1993, "label": "Megalakul az EU", "color": "#6FBF73", "position" : "away_from_bar"}, {"year": 1995, "label": "World Wide Web", "color": "#6FBF73", "position" : "close_to_bar"}, {"year": 2001, "label": "9/11", "color": "#E07A5F", "position" : "away_from_bar"}, {"year": 2008, "label": "Android 1.0", "color": "#F2C57C", "position" : "away_from_bar"}, {"year": 2004, "label": "Magyarország EU tag", "color": "#E07A5F", "position" : "close_to_bar"}, {"start_year": 2010, "end_year": 2026, "label": "NER"}, {"year": 2016, "label": "Brexit", "color": "#E07A5F", "position" : "close_to_bar"}, {"year": 2020, "label": "COVID-19", "color": "#A18CD1", "position" : "away_from_bar"}, {"year": 2023, "label": "ChatGPT", "color": "#A18CD1", "position" : "close_to_bar"}, ] } ] tv = TimelineVisualizer(people=people, age_max=100, size=(1920, 1080), dpi=120) tv.render("parallel_age_timelines.png")
Mark as private (unlisted)
for 30 minutes
for 6 hours
for 1 day
for 1 week
for 1 month
for 1 year
everlasting (like CCCP)