|
| 1 | +import pandas as pd |
| 2 | +import plotly.express as px |
| 3 | +import dash_bootstrap_components as dbc |
| 4 | +from dash import dcc, html |
| 5 | + |
| 6 | + |
| 7 | +def _build_source_year_df(df, year_col, item_col, source_name): |
| 8 | + """Return normalized [year, item, Source] rows for one source.""" |
| 9 | + if df is None or df.empty or year_col not in df.columns or item_col not in df.columns: |
| 10 | + return pd.DataFrame(columns=["year", "item", "Source"]) |
| 11 | + |
| 12 | + tmp = df[[year_col, item_col]].copy() |
| 13 | + tmp[year_col] = pd.to_numeric(tmp[year_col], errors="coerce") |
| 14 | + tmp[item_col] = tmp[item_col].fillna("").astype(str).str.strip() |
| 15 | + tmp = tmp.dropna(subset=[year_col]) |
| 16 | + tmp = tmp[tmp[item_col] != ""] |
| 17 | + |
| 18 | + if tmp.empty: |
| 19 | + return pd.DataFrame(columns=["year", "item", "Source"]) |
| 20 | + |
| 21 | + tmp["year"] = tmp[year_col].astype(int) |
| 22 | + tmp["item"] = tmp[item_col] |
| 23 | + tmp["Source"] = source_name |
| 24 | + return tmp[["year", "item", "Source"]] |
| 25 | + |
| 26 | +def _make_source_growth_figure( |
| 27 | + source_year_df, |
| 28 | + source_name, |
| 29 | + line_color, |
| 30 | + yearly_label="New repos created", |
| 31 | + cumulative_label="Total repos to date", |
| 32 | +): |
| 33 | + """Build one cumulative growth chart for a single source.""" |
| 34 | + if source_year_df is None or source_year_df.empty: |
| 35 | + return px.line(title=f"No {source_name} time-series data available") |
| 36 | + |
| 37 | + year_plot_df = ( |
| 38 | + source_year_df.groupby("year", as_index=False) |
| 39 | + .agg({"item": list}) |
| 40 | + .sort_values("year") |
| 41 | + ) |
| 42 | + |
| 43 | + year_plot_df["items_str"] = year_plot_df["item"].apply(lambda items: "<br>".join(items)) |
| 44 | + year_plot_df["yearly_count"] = year_plot_df["item"].apply(len) |
| 45 | + |
| 46 | + year_plot_df["yearly_cumulative_count"] = year_plot_df["yearly_count"].cumsum() |
| 47 | + |
| 48 | + fig = px.line( |
| 49 | + year_plot_df, |
| 50 | + x="year", |
| 51 | + y="yearly_cumulative_count", |
| 52 | + markers=True, |
| 53 | + title=source_name, |
| 54 | + labels={"year": "Year", "yearly_cumulative_count": "Cumulative Items"}, |
| 55 | + custom_data=["items_str", "yearly_count"], |
| 56 | + template="simple_white", |
| 57 | + ) |
| 58 | + |
| 59 | + fig.update_traces( |
| 60 | + marker={"size": 7, "color": line_color}, |
| 61 | + line={"color": line_color}, |
| 62 | + hovertemplate=( |
| 63 | + f"Year: %{{x}}<br>" |
| 64 | + f"{yearly_label}: %{{customdata[1]}}<br>" |
| 65 | + f"{cumulative_label}: %{{y}}<extra></extra>" |
| 66 | + ), |
| 67 | + ) |
| 68 | + |
| 69 | + fig.update_layout( |
| 70 | + showlegend=False, |
| 71 | + height=430, |
| 72 | + margin={"l": 40, "r": 20, "t": 60, "b": 50}, |
| 73 | + yaxis_title="Cumulative Items", |
| 74 | + ) |
| 75 | + fig.update_xaxes(title_text="Year") |
| 76 | + return fig |
| 77 | + |
| 78 | + |
| 79 | +def get_combined_layout(github_df, epmc_entries_df, pypi_first_releases_df, epmc_citations): |
| 80 | + """Build top-level combined chart layout for all three data sources.""" |
| 81 | + gh_df = github_df.copy() if github_df is not None else pd.DataFrame() |
| 82 | + ep_df = epmc_entries_df.copy() if epmc_entries_df is not None else pd.DataFrame() |
| 83 | + py_df = pypi_first_releases_df.copy() if pypi_first_releases_df is not None else pd.DataFrame() |
| 84 | + ct_df = pd.DataFrame( |
| 85 | + epmc_citations.get("citations_over_years", []) |
| 86 | + if isinstance(epmc_citations, dict) |
| 87 | + else epmc_citations if isinstance(epmc_citations, list) |
| 88 | + else [] |
| 89 | + ).reindex(columns=["pub_year", "year_count"]) |
| 90 | + |
| 91 | + if not gh_df.empty and "created_on" in gh_df.columns: |
| 92 | + gh_df["created_on_year"] = pd.to_datetime(gh_df["created_on"], errors="coerce", utc=True).dt.year |
| 93 | + |
| 94 | + if not py_df.empty and "release_date" in py_df.columns: |
| 95 | + py_df["release_year"] = pd.to_datetime(py_df["release_date"], errors="coerce", utc=True).dt.year |
| 96 | + |
| 97 | + if not ct_df.empty: |
| 98 | + ct_df["pub_year"] = pd.to_numeric(ct_df["pub_year"], errors="coerce") |
| 99 | + ct_df["year_count"] = pd.to_numeric(ct_df["year_count"], errors="coerce").fillna(0).astype(int) |
| 100 | + ct_df = ct_df[(ct_df["pub_year"] > 2013) & (ct_df["year_count"] > 0)] |
| 101 | + ct_df = ct_df.loc[ct_df.index.repeat(ct_df["year_count"])].copy() |
| 102 | + ct_df["citation_item"] = "citation-" + ct_df.groupby("pub_year").cumcount().add(1).astype(str) |
| 103 | + |
| 104 | + github_year_df = _build_source_year_df(gh_df, "created_on_year", "name", "GitHub Repositories") |
| 105 | + epmc_year_df = _build_source_year_df(ep_df, "pub_year", "title", "GA4GH-Related Articles") |
| 106 | + pypi_year_df = _build_source_year_df(py_df, "release_year", "project_name", "PyPI Packages") |
| 107 | + citations_year_df = _build_source_year_df(ct_df, "pub_year", "citation_item", "Europe PMC Cumulative Citations") |
| 108 | + |
| 109 | + gh_fig = _make_source_growth_figure( |
| 110 | + github_year_df, "GitHub Repositories", "#1b75bb" |
| 111 | + ) |
| 112 | + |
| 113 | + epmc_fig = _make_source_growth_figure( |
| 114 | + epmc_year_df, "GA4GH-Related Articles", "#e34a3a", |
| 115 | + yearly_label="New articles", |
| 116 | + cumulative_label="Total articles to date", |
| 117 | + ) |
| 118 | + pypi_fig = _make_source_growth_figure( |
| 119 | + pypi_year_df, "PyPI Packages", "#9f79b0", |
| 120 | + yearly_label="New libraries created", |
| 121 | + cumulative_label="Total libraries to date", |
| 122 | + ) |
| 123 | + |
| 124 | + citations_fig = _make_source_growth_figure( |
| 125 | + citations_year_df, |
| 126 | + "Europe PMC Cumulative Citations Per Year", |
| 127 | + "#8cc63e", |
| 128 | + yearly_label="New citations", |
| 129 | + cumulative_label="Total citations to date", |
| 130 | + ) |
| 131 | + |
| 132 | + gh_fig.update_layout(yaxis_title="Cumulative Repositories") |
| 133 | + epmc_fig.update_layout(yaxis_title="Cumulative Articles") |
| 134 | + pypi_fig.update_layout(yaxis_title="Cumulative Libraries") |
| 135 | + citations_fig.update_layout(yaxis_title="Cumulative Citations") |
| 136 | + |
| 137 | + |
| 138 | + return dbc.Card( |
| 139 | + dbc.CardBody( |
| 140 | + html.Figure([ |
| 141 | + dbc.Row( |
| 142 | + [ |
| 143 | + dbc.Col(dcc.Graph(id="combined-growth-epmc", figure=epmc_fig), lg=6, md=6, sm=12), |
| 144 | + dbc.Col(dcc.Graph(id="combined-citations-over-years", figure=citations_fig), lg=6, md=6, sm=12), |
| 145 | + dbc.Col(dcc.Graph(id="combined-growth-github", figure=gh_fig), lg=6, md=6, sm=12), |
| 146 | + dbc.Col(dcc.Graph(id="combined-growth-pypi", figure=pypi_fig), lg=6, md=6, sm=12), |
| 147 | + ], |
| 148 | + className="g-3", |
| 149 | + ), |
| 150 | + html.Figcaption("Cumulative number of GA4GH-Related Articles and their Citations from Europe PMC, as well as GitHub Repositories, and PyPI Packages per year.") |
| 151 | + ]) |
| 152 | + ), |
| 153 | + className="mb-4 shadow-sm", |
| 154 | + style={"borderRadius": "12px"}, |
| 155 | + ) |
0 commit comments