File size: 7,574 Bytes
bfdae48 | 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 pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.path as mpath
import os
# Configure input and output paths
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
INPUT_CSV = os.path.join(SCRIPT_DIR, 'data', 'timeline_data.csv')
OUTPUT_PNG = os.path.join(PROJECT_ROOT, 'assets', 'timeline_output.png')
# Color configuration
COLORS = {
'Continuous': '#FCE4D6', # Light orange
'Discrete': '#E2F0D9', # Light green
'Border_Validated': '#1F3864', # Dark blue border
}
def get_rounded_rect_path(x, y, w, h, r_inch, ax):
"""Generate a rectangular path with perfect rounded corners to avoid border distortion"""
bbox = ax.get_window_extent().transformed(ax.figure.dpi_scale_trans.inverted())
width_inch = bbox.width
height_inch = bbox.height
xmin, xmax = ax.get_xlim()
ymin, ymax = ax.get_ylim()
x_range = xmax - xmin
y_range = ymax - ymin
# Convert physical inch radius to radius in data coordinate system
rx = r_inch * (x_range / width_inch)
ry = r_inch * (y_range / height_inch)
# Bezier curve control point ratio
kappa = 0.552284749831
cx = kappa * rx
cy = kappa * ry
verts = [
(x + rx, y),
(x + w - rx, y),
(x + w - rx + cx, y), (x + w, y + ry - cy), (x + w, y + ry),
(x + w, y + h - ry),
(x + w, y + h - ry + cy), (x + w - rx + cx, y + h), (x + w - rx, y + h),
(x + rx, y + h),
(x + rx - cx, y + h), (x, y + h - ry + cy), (x, y + h - ry),
(x, y + ry),
(x, y + ry - cy), (x + rx - cx, y), (x + rx, y),
(0, 0)
]
codes = [
mpath.Path.MOVETO,
mpath.Path.LINETO,
mpath.Path.CURVE4, mpath.Path.CURVE4, mpath.Path.CURVE4,
mpath.Path.LINETO,
mpath.Path.CURVE4, mpath.Path.CURVE4, mpath.Path.CURVE4,
mpath.Path.LINETO,
mpath.Path.CURVE4, mpath.Path.CURVE4, mpath.Path.CURVE4,
mpath.Path.LINETO,
mpath.Path.CURVE4, mpath.Path.CURVE4, mpath.Path.CURVE4,
mpath.Path.CLOSEPOLY
]
return mpath.Path(verts, codes)
def generate_timeline(csv_path, output_path):
if not os.path.exists(csv_path):
print(f"Error: Input file {csv_path} not found")
return
# Read CSV data
df = pd.read_csv(csv_path)
# --- Layout Constants ---
BOX_WIDTH = 0.85
BOX_HEIGHT = 0.25
BOX_SPACING = 0.05
TIMELINE_Y = 0.0
TIMELINE_TO_BOX_SPACING = 0.10
YEAR_TEXT_SPACING = 0.15
# Derived layout constants
VERTICAL_STEP = BOX_HEIGHT + BOX_SPACING
FIRST_BOX_Y_CENTER = TIMELINE_Y - TIMELINE_TO_BOX_SPACING - (BOX_HEIGHT / 2)
# Legend position (2 boxes below the 4th box of 2022)
# 2022 has 4 boxes (index 0, 1, 2, 3), so lowest box is at index 3
LOWEST_2022_BOX_Y = FIRST_BOX_Y_CENTER - 3 * VERTICAL_STEP
LEGEND_Y_START = LOWEST_2022_BOX_Y - 2 * VERTICAL_STEP
# Calculate bounds
X_MIN, X_MAX = 0.2, 7.0
Y_MAX = TIMELINE_Y + YEAR_TEXT_SPACING + 0.25
Y_MIN = LEGEND_Y_START - 2 * VERTICAL_STEP - (BOX_HEIGHT / 2) - 0.2
# Calculate figsize to maintain equal aspect ratio
FIGSIZE_X = 15.0
FIGSIZE_Y = FIGSIZE_X * ((Y_MAX - Y_MIN) / (X_MAX - X_MIN))
# Create chart
fig, ax = plt.subplots(figsize=(FIGSIZE_X, FIGSIZE_Y), dpi=300)
ax.set_xlim(X_MIN, X_MAX)
ax.set_ylim(Y_MIN, Y_MAX)
ax.set_aspect('equal')
ax.axis('off') # Hide axes
# Draw timeline (main line)
ax.plot([0.5, 6.7], [TIMELINE_Y, TIMELINE_Y], color='black', linewidth=3, zorder=1)
# Draw arrow
ax.arrow(6.7, TIMELINE_Y, 0.05, 0, head_width=0.05, head_length=0.1, fc='black', ec='black', linewidth=3, zorder=1)
# Map years to X coordinates
years = sorted(df['Year'].unique())
year_x_map = {year: i + 1 for i, year in enumerate(range(2021, 2027))}
# Draw year nodes and text
for year, x in year_x_map.items():
ax.plot(x, TIMELINE_Y, marker='o', color='black', markersize=8, zorder=2)
ax.text(x, TIMELINE_Y + YEAR_TEXT_SPACING, str(year), ha='center', va='bottom', fontsize=16, fontweight='bold', fontfamily='serif')
# Draw model boxes
corner_radius_inch = 0.05 # Control physical radius of rounded corners (inches)
for year in years:
year_data = df[df['Year'] == year]
x_center = year_x_map[year]
for i, row in enumerate(year_data.itertuples()):
y_center = FIRST_BOX_Y_CENTER - i * VERTICAL_STEP
# Determine background and border colors
bg_color = COLORS[row.Type]
if row.Validated == 'Yes':
edge_color = COLORS['Border_Validated']
line_width = 2
else:
edge_color = bg_color # No obvious border
line_width = 0
# Use custom path to draw rounded rectangle to avoid border distortion
path = get_rounded_rect_path(x_center - BOX_WIDTH/2, y_center - BOX_HEIGHT/2, BOX_WIDTH, BOX_HEIGHT, corner_radius_inch, ax)
rect = patches.PathPatch(
path,
facecolor=bg_color,
edgecolor=edge_color,
linewidth=line_width,
zorder=3
)
ax.add_patch(rect)
# Add text
ax.text(x_center, y_center, row.Model, ha='center', va='center', fontsize=14, fontfamily='serif', zorder=4)
# Draw legend (bottom left, inserted in the blank space below 2021 and 2022)
legend_x = 1.0
# 1. Continuous diffusion
path1 = get_rounded_rect_path(legend_x - BOX_WIDTH/2, LEGEND_Y_START - BOX_HEIGHT/2, BOX_WIDTH, BOX_HEIGHT, corner_radius_inch, ax)
rect1 = patches.PathPatch(
path1,
facecolor=COLORS['Continuous'],
edgecolor=COLORS['Continuous'],
linewidth=0,
zorder=3
)
ax.add_patch(rect1)
ax.text(legend_x + BOX_WIDTH/2 + 0.2, LEGEND_Y_START, 'Continuous diffusion', ha='left', va='center', fontsize=12, fontweight='bold', fontfamily='serif')
# 2. Discrete diffusion
path2 = get_rounded_rect_path(legend_x - BOX_WIDTH/2, LEGEND_Y_START - VERTICAL_STEP - BOX_HEIGHT/2, BOX_WIDTH, BOX_HEIGHT, corner_radius_inch, ax)
rect2 = patches.PathPatch(
path2,
facecolor=COLORS['Discrete'],
edgecolor=COLORS['Discrete'],
linewidth=0,
zorder=3
)
ax.add_patch(rect2)
ax.text(legend_x + BOX_WIDTH/2 + 0.2, LEGEND_Y_START - VERTICAL_STEP, 'Discrete diffusion', ha='left', va='center', fontsize=12, fontweight='bold', fontfamily='serif')
# 3. Validated under the setup of GPT-2-small
path3 = get_rounded_rect_path(legend_x - BOX_WIDTH/2, LEGEND_Y_START - 2*VERTICAL_STEP - BOX_HEIGHT/2, BOX_WIDTH, BOX_HEIGHT, corner_radius_inch, ax)
rect3 = patches.PathPatch(
path3,
facecolor='white',
edgecolor=COLORS['Border_Validated'],
linewidth=2,
zorder=3
)
ax.add_patch(rect3)
ax.text(legend_x + BOX_WIDTH/2 + 0.2, LEGEND_Y_START - 2*VERTICAL_STEP, 'Validated under the setup of GPT-2-small', ha='left', va='center', fontsize=12, fontweight='bold', fontfamily='serif')
# Save image
plt.tight_layout()
plt.savefig(output_path, format='png', bbox_inches='tight', pad_inches=0.1)
print(f"Chart successfully generated and saved to: {output_path}")
plt.close()
if __name__ == '__main__':
generate_timeline(INPUT_CSV, OUTPUT_PNG)
|