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)