|
| 1 | +""" pyplots.ai |
| 2 | +timeline-basic: Event Timeline |
| 3 | +Library: pygal 3.1.0 | Python 3.13.11 |
| 4 | +Quality: 91/100 | Created: 2025-12-29 |
| 5 | +""" |
| 6 | + |
| 7 | +from datetime import date |
| 8 | + |
| 9 | +import cairosvg |
| 10 | +import pygal |
| 11 | +from pygal.style import Style |
| 12 | + |
| 13 | + |
| 14 | +# Data - Software project milestones |
| 15 | +events = [ |
| 16 | + (date(2024, 1, 15), "Project Kickoff", "Planning"), |
| 17 | + (date(2024, 2, 10), "Requirements Complete", "Planning"), |
| 18 | + (date(2024, 3, 20), "Architecture Design", "Design"), |
| 19 | + (date(2024, 4, 25), "Development Start", "Development"), |
| 20 | + (date(2024, 6, 15), "Alpha Release", "Development"), |
| 21 | + (date(2024, 8, 1), "Beta Release", "Testing"), |
| 22 | + (date(2024, 9, 10), "User Acceptance", "Testing"), |
| 23 | + (date(2024, 10, 20), "Production Launch", "Deployment"), |
| 24 | +] |
| 25 | + |
| 26 | +# Sort events by date |
| 27 | +events = sorted(events, key=lambda x: x[0]) |
| 28 | + |
| 29 | +# Category colors mapped to pygal color indices |
| 30 | +categories = ["Planning", "Design", "Development", "Testing", "Deployment"] |
| 31 | +category_colors = { |
| 32 | + "Planning": "#306998", |
| 33 | + "Design": "#FFD43B", |
| 34 | + "Development": "#4ECDC4", |
| 35 | + "Testing": "#FF6B6B", |
| 36 | + "Deployment": "#45B7D1", |
| 37 | +} |
| 38 | + |
| 39 | +# Custom style for large canvas (4800x2700) |
| 40 | +custom_style = Style( |
| 41 | + background="white", |
| 42 | + plot_background="white", |
| 43 | + foreground="#333333", |
| 44 | + foreground_strong="#333333", |
| 45 | + foreground_subtle="#666666", |
| 46 | + colors=tuple(category_colors[c] for c in categories), |
| 47 | + title_font_size=60, |
| 48 | + label_font_size=32, |
| 49 | + major_label_font_size=32, |
| 50 | + legend_font_size=36, |
| 51 | + value_font_size=28, |
| 52 | + tooltip_font_size=28, |
| 53 | +) |
| 54 | + |
| 55 | +# Reference date for x-axis positioning |
| 56 | +reference_date = date(2024, 1, 1) |
| 57 | + |
| 58 | +# Create XY chart - using native pygal scatter capabilities |
| 59 | +chart = pygal.XY( |
| 60 | + width=4800, |
| 61 | + height=2700, |
| 62 | + style=custom_style, |
| 63 | + title="Software Project Milestones · timeline-basic · pygal · pyplots.ai", |
| 64 | + show_legend=True, |
| 65 | + legend_at_bottom=False, |
| 66 | + legend_box_size=32, |
| 67 | + x_title="Month (2024)", |
| 68 | + y_title="Project Phase", |
| 69 | + show_dots=True, |
| 70 | + dots_size=18, |
| 71 | + stroke=False, |
| 72 | + show_x_guides=True, |
| 73 | + show_y_guides=True, |
| 74 | + margin=120, |
| 75 | + x_labels_major_every=1, |
| 76 | + truncate_legend=-1, |
| 77 | +) |
| 78 | + |
| 79 | +# Custom x-axis labels for months |
| 80 | +chart.x_labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov"] |
| 81 | +chart.x_labels_major = chart.x_labels |
| 82 | + |
| 83 | +# Y positions for alternating above/below layout - spread vertically to use more canvas |
| 84 | +y_positions = { |
| 85 | + 0: 4, # Above |
| 86 | + 1: 1, # Below |
| 87 | + 2: 4, # Above |
| 88 | + 3: 1, # Below |
| 89 | + 4: 4, # Above |
| 90 | + 5: 1, # Below |
| 91 | + 6: 4, # Above |
| 92 | + 7: 1, # Below |
| 93 | +} |
| 94 | + |
| 95 | +# Group events by category for legend |
| 96 | +category_data = {cat: [] for cat in categories} |
| 97 | + |
| 98 | +for i, (event_date, event_name, category) in enumerate(events): |
| 99 | + # X position: days since Jan 1, scaled to month |
| 100 | + days = (event_date - reference_date).days |
| 101 | + x_val = days / 30.44 # Average days per month |
| 102 | + |
| 103 | + # Y position for alternating layout |
| 104 | + y_val = y_positions[i] |
| 105 | + |
| 106 | + # Add data point with label |
| 107 | + category_data[category].append({"value": (x_val, y_val), "label": f"{event_name}\n{event_date.strftime('%b %d')}"}) |
| 108 | + |
| 109 | +# Add each category as a series |
| 110 | +for category in categories: |
| 111 | + if category_data[category]: |
| 112 | + chart.add(category, category_data[category]) |
| 113 | + |
| 114 | +# Set y range to maximize vertical usage (values 0-5 with padding) |
| 115 | +chart.range = (0, 5) |
| 116 | + |
| 117 | +# Render base SVG |
| 118 | +svg_string = chart.render().decode("utf-8") |
| 119 | + |
| 120 | +# Add a horizontal timeline axis in the middle (y=2.5) as visual anchor |
| 121 | +# Calculate plot area based on pygal defaults with our margins |
| 122 | +PLOT_LEFT = 400 |
| 123 | +PLOT_RIGHT = 4550 |
| 124 | +PLOT_TOP = 300 |
| 125 | +PLOT_BOTTOM = 2200 |
| 126 | +PLOT_HEIGHT = PLOT_BOTTOM - PLOT_TOP |
| 127 | + |
| 128 | +# Timeline at y=2.5 (middle of 0-5 range) |
| 129 | +timeline_y = PLOT_TOP + PLOT_HEIGHT * (1 - 2.5 / 5) # Invert because SVG y goes down |
| 130 | + |
| 131 | +# Create timeline axis elements |
| 132 | +timeline_svg = f""" |
| 133 | +<g class="timeline-axis"> |
| 134 | + <line x1="{PLOT_LEFT}" y1="{timeline_y:.0f}" x2="{PLOT_RIGHT}" y2="{timeline_y:.0f}" |
| 135 | + stroke="#999999" stroke-width="4" stroke-dasharray="15,8"/> |
| 136 | + <text x="{PLOT_LEFT - 80}" y="{timeline_y + 10:.0f}" font-family="Consolas, sans-serif" |
| 137 | + font-size="28" fill="#666666" text-anchor="end">Timeline</text> |
| 138 | +</g> |
| 139 | +""" |
| 140 | + |
| 141 | +# Inject timeline before </svg> |
| 142 | +svg_output = svg_string.replace("</svg>", f"{timeline_svg}\n</svg>") |
| 143 | + |
| 144 | +# Save outputs |
| 145 | +with open("plot.html", "w") as f: |
| 146 | + f.write(svg_output) |
| 147 | + |
| 148 | +cairosvg.svg2png(bytestring=svg_output.encode(), write_to="plot.png") |
0 commit comments