"""Generate publication-quality charts from OpenAlex data.""" import numpy as np import pandas as pd import matplotlib.pyplot as plt import matplotlib.ticker as mticker import seaborn as sns from config import DATA_DIR, FIG_DIR, LAST_FULL_YEAR, COUNTRY_NAMES # Seaborn styling sns.set_theme(style="white", font_scale=1.1) PALETTE = sns.color_palette("tab10") def setup_fig(figsize=(12, 6)): fig, ax = plt.subplots(figsize=figsize) return fig, ax def save(fig, name): path = FIG_DIR / name fig.savefig(path, dpi=200, bbox_inches="tight", facecolor="white") plt.close(fig) print(f" → {path}") def plot_total_articles(): """Bar chart of total articles per year with exponential trend.""" df = pd.read_csv(DATA_DIR / "yearly_totals.csv") full = df[df["year"] <= LAST_FULL_YEAR].copy() fig, ax = setup_fig() ax.bar(full["year"], full["count"], color=PALETTE[0], width=0.8, edgecolor="none") # Exponential trend line x = np.array(full["year"] - full["year"].min(), dtype=float) y = np.array(full["count"], dtype=float) coeffs = np.polyfit(x, np.log(y), 1) trend_x = np.linspace(0, x.max(), 100) trend_y = np.exp(coeffs[1]) * np.exp(coeffs[0] * trend_x) trend_years = trend_x + full["year"].min() ax.plot(trend_years, trend_y, color="red", linewidth=2, linestyle="--", label=f"Exponential fit (r={coeffs[0]*100:.1f}%/yr)", zorder=5) ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v/1e6:.1f}M")) ax.set_xlabel("Year") ax.set_ylabel("Number of Articles") ax.set_title("Global Scientific Article Production (2000–2025)", fontsize=14, fontweight="bold") ax.legend(loc="upper left") save(fig, "total_articles_per_year.png") def plot_yoy_growth(): """Line chart of year-over-year growth rate.""" df = pd.read_csv(DATA_DIR / "yearly_totals.csv") full = df[df["year"] <= LAST_FULL_YEAR].sort_values("year").copy() full["yoy"] = full["count"].pct_change() * 100 full = full.dropna() fig, ax = setup_fig() ax.plot(full["year"], full["yoy"], marker="o", markersize=5, color=PALETTE[0], linewidth=2) ax.axhline(y=5.6, color="red", linestyle="--", alpha=0.7, label="Hanson et al. reported rate (5.6%)") ax.axhline(y=0, color="gray", linestyle="-", alpha=0.3) ax.set_xlabel("Year") ax.set_ylabel("Year-over-Year Growth (%)") ax.set_title("Annual Growth Rate of Scientific Articles", fontsize=14, fontweight="bold") ax.legend(loc="upper right") ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%")) save(fig, "yoy_growth_rate.png") def plot_field_growth(): """Stacked area chart of articles by domain.""" df = pd.read_csv(DATA_DIR / "yearly_by_domain.csv") full = df[df["year"] <= LAST_FULL_YEAR].copy() pivot = full.pivot_table(index="year", columns="domain_name", values="count", aggfunc="sum").fillna(0) pivot = pivot.sort_index() fig, ax = setup_fig() ax.stackplot(pivot.index, [pivot[col] for col in pivot.columns], labels=pivot.columns, alpha=0.85) ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v/1e6:.1f}M")) ax.set_xlabel("Year") ax.set_ylabel("Number of Articles") ax.set_title("Scientific Articles by Domain (2000–2025)", fontsize=14, fontweight="bold") ax.legend(loc="upper left", fontsize=9) save(fig, "field_growth_absolute.png") def plot_field_growth_rates(): """Horizontal bar chart of fastest-growing fields (2015→2025).""" df = pd.read_csv(DATA_DIR / "yearly_by_field.csv") full = df[df["year"] <= LAST_FULL_YEAR].copy() y1, y2 = 2015, LAST_FULL_YEAR f1 = full[full["year"] == y1].set_index("field_name")["count"] f2 = full[full["year"] == y2].set_index("field_name")["count"] growth = pd.DataFrame({"c_start": f1, "c_end": f2}).dropna() growth["growth_pct"] = (growth["c_end"] - growth["c_start"]) / growth["c_start"] * 100 growth = growth.sort_values("growth_pct", ascending=True) # ascending for horizontal bar fig, ax = setup_fig(figsize=(12, 8)) ax.barh(growth.index, growth["growth_pct"], color="steelblue", edgecolor="none") ax.set_xlabel("Growth (%)") ax.set_title(f"Field Growth Rates ({y1}–{y2})", fontsize=14, fontweight="bold") ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%")) save(fig, "field_growth_rates.png") def plot_field_share(): """100% stacked area chart showing domain composition.""" df = pd.read_csv(DATA_DIR / "yearly_by_domain.csv") full = df[df["year"] <= LAST_FULL_YEAR].copy() pivot = full.pivot_table(index="year", columns="domain_name", values="count", aggfunc="sum").fillna(0) pivot = pivot.sort_index() shares = pivot.div(pivot.sum(axis=1), axis=0) * 100 fig, ax = setup_fig() ax.stackplot(shares.index, [shares[col] for col in shares.columns], labels=shares.columns, alpha=0.85) ax.set_ylim(0, 100) ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%")) ax.set_xlabel("Year") ax.set_ylabel("Share of Articles") ax.set_title("Domain Composition of Scientific Output", fontsize=14, fontweight="bold") ax.legend(loc="center right", fontsize=9) save(fig, "field_share_over_time.png") def plot_top_countries(): """Line chart for top 10 countries.""" df = pd.read_csv(DATA_DIR / "yearly_by_country.csv") full = df[df["year"] <= LAST_FULL_YEAR].copy() # Find top 10 by latest year latest = full[full["year"] == LAST_FULL_YEAR] top10 = latest.nlargest(10, "count")["country_code"].tolist() fig, ax = setup_fig() for i, cc in enumerate(top10): sub = full[full["country_code"] == cc].sort_values("year") label = COUNTRY_NAMES.get(cc, cc) ax.plot(sub["year"], sub["count"], marker=".", linewidth=2, label=label, color=PALETTE[i % len(PALETTE)]) ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v/1e6:.1f}M" if v >= 1e6 else f"{v/1e3:.0f}K")) ax.set_xlabel("Year") ax.set_ylabel("Number of Articles") ax.set_title("Top 10 Countries by Scientific Output (2000–2025)", fontsize=14, fontweight="bold") ax.legend(loc="upper left", fontsize=8, ncol=2) save(fig, "top_countries_over_time.png") def plot_country_share(): """Share of global output for top 5 countries over time.""" df = pd.read_csv(DATA_DIR / "yearly_by_country.csv") full = df[df["year"] <= LAST_FULL_YEAR].copy() totals = pd.read_csv(DATA_DIR / "yearly_totals.csv") yearly_total = totals[totals["year"] <= LAST_FULL_YEAR].set_index("year")["count"] # Top 5 by latest year latest = full[full["year"] == LAST_FULL_YEAR] top5 = latest.nlargest(5, "count")["country_code"].tolist() fig, ax = setup_fig() for i, cc in enumerate(top5): country = full[full["country_code"] == cc].groupby("year")["count"].sum() share = (country / yearly_total * 100).dropna() label = COUNTRY_NAMES.get(cc, cc) ax.plot(share.index, share.values, linewidth=2.5, label=label, color=PALETTE[i], marker=".") ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%")) ax.set_xlabel("Year") ax.set_ylabel("Share of Global Output (%)") ax.set_title("Global Share: Top 5 Countries (2000–2025)", fontsize=14, fontweight="bold") ax.legend(loc="upper right", fontsize=9) save(fig, "country_share_shift.png") def main(): FIG_DIR.mkdir(exist_ok=True) print("Generating charts...") plot_total_articles() plot_yoy_growth() plot_field_growth() plot_field_growth_rates() plot_field_share() plot_top_countries() plot_country_share() print("Done.") if __name__ == "__main__": main()