Spaces:
Running
Running
Avery commited on
Commit Β·
f2cc925
1
Parent(s): 63542f9
dashboard: global filters, activity views, momentum, org/tag explorer
Browse files- Global sidebar filters (country, org, language, tag, activity window)
propagating across all tabs
- Dual timeline: repos created vs last-pushed per year
- Rising Stars view (active last 12m) alongside Most Starred
- Most Active Organisations chart and table
- Tag adoption over time chart (top 10 tags by creation year)
- Explorer: sortable, tag filter, org filter, last-pushed column
- Links to GitHub source and HF dataset throughout
README.md
CHANGED
|
@@ -11,4 +11,10 @@ datasets:
|
|
| 11 |
---
|
| 12 |
|
| 13 |
# GovTech GitHub Explorer
|
|
|
|
| 14 |
Exploring 70k+ government GitHub repositories worldwide.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
# GovTech GitHub Explorer
|
| 14 |
+
|
| 15 |
Exploring 70k+ government GitHub repositories worldwide.
|
| 16 |
+
|
| 17 |
+
Filter by country, organisation, language, tag, and activity window. See which governments are actively building, what technologies they're using, and how that's changed over time.
|
| 18 |
+
|
| 19 |
+
**Data**: [AndreasThinks/government-github-repos](https://huggingface.co/datasets/AndreasThinks/government-github-repos) β 70k+ repos, updated weekly.
|
| 20 |
+
**Source**: [AndreasThinks/open-govtech-report](https://github.com/AndreasThinks/open-govtech-report) on GitHub.
|
app.py
CHANGED
|
@@ -4,9 +4,10 @@ import plotly.express as px
|
|
| 4 |
import plotly.graph_objects as go
|
| 5 |
import sqlite3
|
| 6 |
import os
|
| 7 |
-
import json
|
| 8 |
import logging
|
|
|
|
| 9 |
|
|
|
|
| 10 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S")
|
| 11 |
logger = logging.getLogger("govtech-dashboard")
|
| 12 |
logger.info("Starting GovTech Dashboard...")
|
|
@@ -17,9 +18,6 @@ st.set_page_config(
|
|
| 17 |
layout="wide",
|
| 18 |
)
|
| 19 |
|
| 20 |
-
st.title("ποΈ GovTech GitHub Explorer")
|
| 21 |
-
st.caption("Exploring 70k+ government GitHub repositories worldwide")
|
| 22 |
-
|
| 23 |
|
| 24 |
def get_db_path():
|
| 25 |
candidates = [
|
|
@@ -29,11 +27,9 @@ def get_db_path():
|
|
| 29 |
"../govtech.db",
|
| 30 |
]
|
| 31 |
for p in candidates:
|
| 32 |
-
logger.info(f"Checking for DB at: {os.path.abspath(p)}")
|
| 33 |
if os.path.exists(p):
|
| 34 |
logger.info(f"Found local DB: {os.path.abspath(p)}")
|
| 35 |
return os.path.abspath(p)
|
| 36 |
-
# Download from HuggingFace
|
| 37 |
logger.info("No local DB found, downloading from HuggingFace Hub...")
|
| 38 |
try:
|
| 39 |
from huggingface_hub import hf_hub_download
|
|
@@ -51,14 +47,6 @@ def get_db_path():
|
|
| 51 |
|
| 52 |
|
| 53 |
DB_PATH = get_db_path()
|
| 54 |
-
logger.info(f"Using database: {DB_PATH} ({os.path.getsize(DB_PATH) / 1024 / 1024:.1f} MB)")
|
| 55 |
-
try:
|
| 56 |
-
_conn = sqlite3.connect(DB_PATH)
|
| 57 |
-
_tables = [r[0] for r in _conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()]
|
| 58 |
-
_conn.close()
|
| 59 |
-
logger.info(f"DB tables: {', '.join(_tables)}")
|
| 60 |
-
except Exception as e:
|
| 61 |
-
logger.error(f"DB validation failed: {e}")
|
| 62 |
|
| 63 |
|
| 64 |
def get_conn():
|
|
@@ -74,148 +62,280 @@ def query_df(sql, params=None):
|
|
| 74 |
|
| 75 |
|
| 76 |
@st.cache_data(ttl=300)
|
| 77 |
-
def query_one(sql):
|
| 78 |
conn = get_conn()
|
| 79 |
cur = conn.cursor()
|
| 80 |
-
result = cur.execute(sql).fetchone()[0]
|
| 81 |
conn.close()
|
| 82 |
return result
|
| 83 |
|
| 84 |
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
tab_overview, tab_explorer, tab_tags, tab_insights = st.tabs(
|
| 87 |
["π Overview", "π Explorer", "π·οΈ Tags", "π‘ Insights"]
|
| 88 |
)
|
| 89 |
|
|
|
|
| 90 |
# ==================== OVERVIEW ====================
|
| 91 |
with tab_overview:
|
| 92 |
-
|
|
|
|
|
|
|
| 93 |
account_count = query_one("SELECT COUNT(*) FROM accounts")
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
c1, c2, c3, c4 = st.columns(4)
|
| 98 |
-
c1.metric("Repositories", f"{
|
| 99 |
c2.metric("Accounts", f"{account_count:,}")
|
| 100 |
-
c3.metric("Countries",
|
| 101 |
-
c4.metric("
|
| 102 |
|
| 103 |
st.divider()
|
| 104 |
|
| 105 |
col_left, col_right = st.columns(2)
|
| 106 |
|
| 107 |
with col_left:
|
| 108 |
-
st.subheader("Top
|
| 109 |
df_countries = query_df(
|
| 110 |
-
"SELECT country, COUNT(
|
|
|
|
| 111 |
)
|
| 112 |
-
|
| 113 |
-
df_countries, x="country", y="count",
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
fig.update_layout(showlegend=False, xaxis_title="Country", yaxis_title="Repositories")
|
| 117 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 118 |
|
| 119 |
with col_right:
|
| 120 |
-
st.subheader("Top
|
| 121 |
df_langs = query_df(
|
| 122 |
-
"SELECT language, COUNT(
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
)
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
| 130 |
|
| 131 |
st.subheader("Repository Creation Timeline")
|
| 132 |
df_timeline = query_df(
|
| 133 |
-
"SELECT SUBSTR(created_at, 1, 4) as year, COUNT(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
)
|
| 135 |
df_timeline = df_timeline[df_timeline["year"].str.match(r"^\d{4}$", na=False)]
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
)
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
|
| 144 |
# ==================== EXPLORER ====================
|
| 145 |
with tab_explorer:
|
| 146 |
-
|
| 147 |
-
fc1, fc2, fc3 = st.columns(3)
|
| 148 |
-
|
| 149 |
-
all_countries = query_df(
|
| 150 |
-
"SELECT DISTINCT country FROM repositories ORDER BY country"
|
| 151 |
-
)["country"].tolist()
|
| 152 |
-
all_languages = query_df(
|
| 153 |
-
"SELECT DISTINCT language FROM repositories WHERE language IS NOT NULL AND language != '' ORDER BY language"
|
| 154 |
-
)["language"].tolist()
|
| 155 |
-
|
| 156 |
-
with fc1:
|
| 157 |
-
sel_countries = st.multiselect("Country", all_countries, key="exp_country")
|
| 158 |
-
sel_languages = st.multiselect("Language", all_languages, key="exp_lang")
|
| 159 |
-
|
| 160 |
-
with fc2:
|
| 161 |
-
min_stars = st.slider("Minimum Stars", 0, 1000, 0, key="exp_stars")
|
| 162 |
-
search_text = st.text_input("Search name/description", key="exp_search")
|
| 163 |
-
|
| 164 |
-
with fc3:
|
| 165 |
-
show_archived = st.checkbox("Include archived", value=True, key="exp_arch")
|
| 166 |
-
show_forks = st.checkbox("Include forks", value=True, key="exp_forks")
|
| 167 |
-
|
| 168 |
-
# Build query
|
| 169 |
-
conditions = []
|
| 170 |
-
params = []
|
| 171 |
-
|
| 172 |
-
if sel_countries:
|
| 173 |
-
placeholders = ",".join(["?"] * len(sel_countries))
|
| 174 |
-
conditions.append(f"country IN ({placeholders})")
|
| 175 |
-
params.extend(sel_countries)
|
| 176 |
-
|
| 177 |
-
if sel_languages:
|
| 178 |
-
placeholders = ",".join(["?"] * len(sel_languages))
|
| 179 |
-
conditions.append(f"language IN ({placeholders})")
|
| 180 |
-
params.extend(sel_languages)
|
| 181 |
-
|
| 182 |
-
if min_stars > 0:
|
| 183 |
-
conditions.append("stars >= ?")
|
| 184 |
-
params.append(min_stars)
|
| 185 |
-
|
| 186 |
-
if not show_archived:
|
| 187 |
-
conditions.append("(archived = 0 OR archived IS NULL)")
|
| 188 |
-
|
| 189 |
-
if not show_forks:
|
| 190 |
-
conditions.append("(fork = 0 OR fork IS NULL)")
|
| 191 |
|
|
|
|
|
|
|
| 192 |
if search_text:
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
-
|
| 197 |
|
| 198 |
-
count_sql = f"SELECT COUNT(*) FROM repositories {where}"
|
| 199 |
conn = get_conn()
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
conn.close()
|
| 202 |
|
| 203 |
-
st.write(f"**{total_results:,}** repositories
|
| 204 |
|
| 205 |
page_size = 50
|
| 206 |
total_pages = max(1, (total_results + page_size - 1) // page_size)
|
| 207 |
page = st.number_input("Page", min_value=1, max_value=total_pages, value=1, key="exp_page")
|
| 208 |
offset = (page - 1) * page_size
|
| 209 |
|
|
|
|
|
|
|
|
|
|
| 210 |
data_sql = f"""
|
| 211 |
-
SELECT html_url, name, owner, country, language, stars, forks,
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
| 214 |
LIMIT ? OFFSET ?
|
| 215 |
"""
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
df_results = query_df(data_sql, params_page)
|
| 219 |
|
| 220 |
if not df_results.empty:
|
| 221 |
st.dataframe(
|
|
@@ -230,6 +350,9 @@ with tab_explorer:
|
|
| 230 |
"forks": st.column_config.NumberColumn("π΄ Forks"),
|
| 231 |
"license": st.column_config.TextColumn("License"),
|
| 232 |
"created_at": st.column_config.TextColumn("Created"),
|
|
|
|
|
|
|
|
|
|
| 233 |
},
|
| 234 |
use_container_width=True,
|
| 235 |
hide_index=True,
|
|
@@ -241,55 +364,83 @@ with tab_explorer:
|
|
| 241 |
|
| 242 |
# ==================== TAGS ====================
|
| 243 |
with tab_tags:
|
|
|
|
|
|
|
| 244 |
tagged_count_t = query_one("SELECT COUNT(DISTINCT html_url) FROM repository_tags")
|
| 245 |
total_repos_t = query_one("SELECT COUNT(*) FROM repositories")
|
| 246 |
|
| 247 |
-
if tagged_count_t < total_repos_t * 0.
|
| 248 |
pct = tagged_count_t / total_repos_t * 100 if total_repos_t > 0 else 0
|
| 249 |
st.info(
|
| 250 |
f"ποΈ **Tagging in progress** β {tagged_count_t:,} of {total_repos_t:,} repositories tagged ({pct:.1f}%). "
|
| 251 |
-
|
| 252 |
)
|
| 253 |
|
| 254 |
-
st.
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
)
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
|
|
|
| 284 |
st.subheader("Browse Repos by Tag")
|
| 285 |
-
|
| 286 |
-
if
|
| 287 |
-
sel_tag = st.selectbox("Select a tag",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
df_tag_repos = query_df(
|
| 289 |
-
"""SELECT r.name, r.owner, r.country, r.language, r.stars,
|
| 290 |
-
FROM repository_tags
|
| 291 |
-
|
| 292 |
-
|
| 293 |
)
|
| 294 |
st.write(f"**{len(df_tag_repos)}** repos tagged with **{sel_tag}**")
|
| 295 |
if not df_tag_repos.empty:
|
|
@@ -298,37 +449,132 @@ with tab_tags:
|
|
| 298 |
column_config={
|
| 299 |
"html_url": st.column_config.LinkColumn("URL", display_text="Open"),
|
| 300 |
"confidence": st.column_config.ProgressColumn("Confidence", min_value=0, max_value=1),
|
|
|
|
|
|
|
| 301 |
},
|
| 302 |
use_container_width=True,
|
| 303 |
hide_index=True,
|
| 304 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
else:
|
| 306 |
-
st.info("No
|
| 307 |
|
| 308 |
|
| 309 |
# ==================== INSIGHTS ====================
|
| 310 |
with tab_insights:
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
"
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
|
| 325 |
st.divider()
|
| 326 |
-
col_a, col_b = st.columns(2)
|
| 327 |
|
| 328 |
-
|
|
|
|
|
|
|
| 329 |
st.subheader("π License Breakdown")
|
| 330 |
df_lic = query_df(
|
| 331 |
-
"SELECT license, COUNT(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
)
|
| 333 |
if not df_lic.empty:
|
| 334 |
top_n = 10
|
|
@@ -342,58 +588,55 @@ with tab_insights:
|
|
| 342 |
fig.update_traces(textposition="inside", textinfo="percent+label")
|
| 343 |
st.plotly_chart(fig, use_container_width=True)
|
| 344 |
|
| 345 |
-
with
|
| 346 |
st.subheader("π΄ Fork vs Original")
|
| 347 |
-
fork_count = query_one(
|
| 348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
m1, m2 = st.columns(2)
|
| 350 |
-
m1.metric("Original
|
| 351 |
-
m2.metric("Forked
|
| 352 |
fig = px.pie(
|
| 353 |
pd.DataFrame({"type": ["Original", "Fork"], "count": [original_count, fork_count]}),
|
| 354 |
names="type", values="count", hole=0.4,
|
| 355 |
-
color_discrete_sequence=["#2ecc71", "#e74c3c"]
|
| 356 |
)
|
| 357 |
st.plotly_chart(fig, use_container_width=True)
|
| 358 |
|
| 359 |
-
st.divider()
|
| 360 |
-
|
| 361 |
-
st.subheader("π Most Recently Active Repos (by last push)")
|
| 362 |
-
df_active = query_df(
|
| 363 |
-
"SELECT name, owner, country, language, stars, pushed_at, html_url FROM repositories WHERE pushed_at IS NOT NULL ORDER BY pushed_at DESC LIMIT 20"
|
| 364 |
-
)
|
| 365 |
-
st.dataframe(
|
| 366 |
-
df_active,
|
| 367 |
-
column_config={
|
| 368 |
-
"html_url": st.column_config.LinkColumn("URL", display_text="Open"),
|
| 369 |
-
},
|
| 370 |
-
use_container_width=True,
|
| 371 |
-
hide_index=True,
|
| 372 |
-
)
|
| 373 |
-
|
| 374 |
st.divider()
|
| 375 |
st.subheader("π Language Γ Country Heatmap")
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
if not df_heat.empty:
|
| 386 |
pivot = df_heat.pivot_table(index="country", columns="language", values="count", fill_value=0)
|
| 387 |
-
fig = px.imshow(
|
| 388 |
-
|
| 389 |
-
color_continuous_scale="YlOrRd",
|
| 390 |
-
labels=dict(x="Language", y="Country", color="Repos"),
|
| 391 |
-
aspect="auto",
|
| 392 |
-
)
|
| 393 |
fig.update_layout(height=500)
|
| 394 |
st.plotly_chart(fig, use_container_width=True)
|
| 395 |
else:
|
| 396 |
st.info("Not enough data for heatmap.")
|
| 397 |
|
|
|
|
| 398 |
st.divider()
|
| 399 |
-
st.caption(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import plotly.graph_objects as go
|
| 5 |
import sqlite3
|
| 6 |
import os
|
|
|
|
| 7 |
import logging
|
| 8 |
+
from datetime import datetime, timedelta, timezone
|
| 9 |
|
| 10 |
+
# Configure logging
|
| 11 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S")
|
| 12 |
logger = logging.getLogger("govtech-dashboard")
|
| 13 |
logger.info("Starting GovTech Dashboard...")
|
|
|
|
| 18 |
layout="wide",
|
| 19 |
)
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
def get_db_path():
|
| 23 |
candidates = [
|
|
|
|
| 27 |
"../govtech.db",
|
| 28 |
]
|
| 29 |
for p in candidates:
|
|
|
|
| 30 |
if os.path.exists(p):
|
| 31 |
logger.info(f"Found local DB: {os.path.abspath(p)}")
|
| 32 |
return os.path.abspath(p)
|
|
|
|
| 33 |
logger.info("No local DB found, downloading from HuggingFace Hub...")
|
| 34 |
try:
|
| 35 |
from huggingface_hub import hf_hub_download
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
DB_PATH = get_db_path()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
def get_conn():
|
|
|
|
| 62 |
|
| 63 |
|
| 64 |
@st.cache_data(ttl=300)
|
| 65 |
+
def query_one(sql, params=None):
|
| 66 |
conn = get_conn()
|
| 67 |
cur = conn.cursor()
|
| 68 |
+
result = cur.execute(sql, params or []).fetchone()[0]
|
| 69 |
conn.close()
|
| 70 |
return result
|
| 71 |
|
| 72 |
|
| 73 |
+
@st.cache_data(ttl=600)
|
| 74 |
+
def load_filter_options():
|
| 75 |
+
conn = get_conn()
|
| 76 |
+
countries = pd.read_sql_query(
|
| 77 |
+
"SELECT DISTINCT country FROM repositories WHERE country IS NOT NULL AND country != '' ORDER BY country", conn
|
| 78 |
+
)["country"].tolist()
|
| 79 |
+
languages = pd.read_sql_query(
|
| 80 |
+
"SELECT DISTINCT language FROM repositories WHERE language IS NOT NULL AND language != '' ORDER BY language", conn
|
| 81 |
+
)["language"].tolist()
|
| 82 |
+
tags = pd.read_sql_query(
|
| 83 |
+
"SELECT tag, COUNT(*) as c FROM repository_tags GROUP BY tag ORDER BY c DESC", conn
|
| 84 |
+
)["tag"].tolist()
|
| 85 |
+
orgs = pd.read_sql_query(
|
| 86 |
+
"SELECT owner, COUNT(*) as c FROM repositories GROUP BY owner ORDER BY c DESC LIMIT 300", conn
|
| 87 |
+
)["owner"].tolist()
|
| 88 |
+
conn.close()
|
| 89 |
+
return countries, languages, tags, orgs
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# ==================== SIDEBAR FILTERS ====================
|
| 93 |
+
st.sidebar.title("π Filters")
|
| 94 |
+
st.sidebar.caption("Applied across all tabs")
|
| 95 |
+
|
| 96 |
+
countries, languages, tags, orgs = load_filter_options()
|
| 97 |
+
|
| 98 |
+
sel_countries = st.sidebar.multiselect("π Country", countries, key="g_country")
|
| 99 |
+
sel_orgs = st.sidebar.multiselect("π’ Organisation", orgs, key="g_org")
|
| 100 |
+
sel_languages = st.sidebar.multiselect("π» Language", languages, key="g_lang")
|
| 101 |
+
sel_tags = st.sidebar.multiselect("π·οΈ Tag", tags, key="g_tag")
|
| 102 |
+
|
| 103 |
+
st.sidebar.divider()
|
| 104 |
+
st.sidebar.subheader("π
Activity")
|
| 105 |
+
activity_options = {
|
| 106 |
+
"All time": None,
|
| 107 |
+
"Active last 3 months": 90,
|
| 108 |
+
"Active last 6 months": 180,
|
| 109 |
+
"Active last 12 months": 365,
|
| 110 |
+
"Active last 2 years": 730,
|
| 111 |
+
}
|
| 112 |
+
activity_label = st.sidebar.selectbox("Last pushed", list(activity_options.keys()), index=0, key="g_activity")
|
| 113 |
+
activity_days = activity_options[activity_label]
|
| 114 |
+
|
| 115 |
+
st.sidebar.divider()
|
| 116 |
+
show_archived = st.sidebar.checkbox("Include archived", value=False, key="g_arch")
|
| 117 |
+
show_forks = st.sidebar.checkbox("Include forks", value=True, key="g_forks")
|
| 118 |
+
min_stars = st.sidebar.slider("Min stars", 0, 500, 0, key="g_stars")
|
| 119 |
+
|
| 120 |
+
st.sidebar.divider()
|
| 121 |
+
st.sidebar.markdown(
|
| 122 |
+
"π [GitHub Repo](https://github.com/AndreasThinks/open-govtech-report) | "
|
| 123 |
+
"[Dataset on HF](https://huggingface.co/datasets/AndreasThinks/government-github-repos)",
|
| 124 |
+
unsafe_allow_html=True,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def build_where(extra_conditions=None, base_table="r", tag_table="rt"):
|
| 129 |
+
"""Build a WHERE clause and params list from global sidebar filters."""
|
| 130 |
+
conditions = list(extra_conditions or [])
|
| 131 |
+
params = []
|
| 132 |
+
|
| 133 |
+
if sel_countries:
|
| 134 |
+
ph = ",".join(["?"] * len(sel_countries))
|
| 135 |
+
conditions.append(f"{base_table}.country IN ({ph})")
|
| 136 |
+
params.extend(sel_countries)
|
| 137 |
+
|
| 138 |
+
if sel_orgs:
|
| 139 |
+
ph = ",".join(["?"] * len(sel_orgs))
|
| 140 |
+
conditions.append(f"{base_table}.owner IN ({ph})")
|
| 141 |
+
params.extend(sel_orgs)
|
| 142 |
+
|
| 143 |
+
if sel_languages:
|
| 144 |
+
ph = ",".join(["?"] * len(sel_languages))
|
| 145 |
+
conditions.append(f"{base_table}.language IN ({ph})")
|
| 146 |
+
params.extend(sel_languages)
|
| 147 |
+
|
| 148 |
+
if activity_days:
|
| 149 |
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=activity_days)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 150 |
+
conditions.append(f"{base_table}.pushed_at >= ?")
|
| 151 |
+
params.append(cutoff)
|
| 152 |
+
|
| 153 |
+
if not show_archived:
|
| 154 |
+
conditions.append(f"({base_table}.archived = 0 OR {base_table}.archived IS NULL)")
|
| 155 |
+
|
| 156 |
+
if not show_forks:
|
| 157 |
+
conditions.append(f"({base_table}.fork = 0 OR {base_table}.fork IS NULL)")
|
| 158 |
+
|
| 159 |
+
if min_stars > 0:
|
| 160 |
+
conditions.append(f"{base_table}.stars >= ?")
|
| 161 |
+
params.append(min_stars)
|
| 162 |
+
|
| 163 |
+
return conditions, params
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def build_tag_join_where(extra_conditions=None):
|
| 167 |
+
"""Build WHERE for queries that need to join repository_tags for tag filter."""
|
| 168 |
+
conditions, params = build_where(extra_conditions)
|
| 169 |
+
tag_join = ""
|
| 170 |
+
if sel_tags:
|
| 171 |
+
tag_join = "JOIN repository_tags rt ON r.html_url = rt.html_url"
|
| 172 |
+
ph = ",".join(["?"] * len(sel_tags))
|
| 173 |
+
conditions.append(f"rt.tag IN ({ph})")
|
| 174 |
+
params.extend(sel_tags)
|
| 175 |
+
where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
|
| 176 |
+
return where, params, tag_join
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# ==================== HEADER ====================
|
| 180 |
+
st.title("ποΈ GovTech GitHub Explorer")
|
| 181 |
+
st.caption("Exploring 70k+ government GitHub repositories worldwide")
|
| 182 |
+
|
| 183 |
+
# ==================== TABS ====================
|
| 184 |
tab_overview, tab_explorer, tab_tags, tab_insights = st.tabs(
|
| 185 |
["π Overview", "π Explorer", "π·οΈ Tags", "π‘ Insights"]
|
| 186 |
)
|
| 187 |
|
| 188 |
+
|
| 189 |
# ==================== OVERVIEW ====================
|
| 190 |
with tab_overview:
|
| 191 |
+
where, params, tag_join = build_tag_join_where()
|
| 192 |
+
|
| 193 |
+
total_filtered = query_one(f"SELECT COUNT(DISTINCT r.html_url) FROM repositories r {tag_join} {where}", params)
|
| 194 |
account_count = query_one("SELECT COUNT(*) FROM accounts")
|
| 195 |
+
country_count_val = query_one(
|
| 196 |
+
f"SELECT COUNT(DISTINCT r.country) FROM repositories r {tag_join} {where}", params
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
# Active in last 12m within filtered set
|
| 200 |
+
active_cutoff = (datetime.now(timezone.utc) - timedelta(days=365)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 201 |
+
active_conditions, active_params = build_where([f"r.pushed_at >= ?"])
|
| 202 |
+
active_params_full = active_params.copy()
|
| 203 |
+
active_params_full.insert(
|
| 204 |
+
len(active_params) - 1 if active_params else 0, active_cutoff
|
| 205 |
+
)
|
| 206 |
+
# Simpler: just count directly
|
| 207 |
+
conn = get_conn()
|
| 208 |
+
conds_12m, p_12m = build_where()
|
| 209 |
+
conds_12m.append("r.pushed_at >= ?")
|
| 210 |
+
p_12m.append(active_cutoff)
|
| 211 |
+
tj2 = "JOIN repository_tags rt ON r.html_url = rt.html_url" if sel_tags else ""
|
| 212 |
+
if sel_tags:
|
| 213 |
+
ph = ",".join(["?"] * len(sel_tags))
|
| 214 |
+
conds_12m.append(f"rt.tag IN ({ph})")
|
| 215 |
+
p_12m.extend(sel_tags)
|
| 216 |
+
w12 = ("WHERE " + " AND ".join(conds_12m)) if conds_12m else ""
|
| 217 |
+
active_12m = conn.execute(
|
| 218 |
+
f"SELECT COUNT(DISTINCT r.html_url) FROM repositories r {tj2} {w12}", p_12m
|
| 219 |
+
).fetchone()[0]
|
| 220 |
+
conn.close()
|
| 221 |
|
| 222 |
c1, c2, c3, c4 = st.columns(4)
|
| 223 |
+
c1.metric("Repositories", f"{total_filtered:,}")
|
| 224 |
c2.metric("Accounts", f"{account_count:,}")
|
| 225 |
+
c3.metric("Countries", country_count_val)
|
| 226 |
+
c4.metric("Active last 12m", f"{active_12m:,}", help="Repos with a push in the last 12 months")
|
| 227 |
|
| 228 |
st.divider()
|
| 229 |
|
| 230 |
col_left, col_right = st.columns(2)
|
| 231 |
|
| 232 |
with col_left:
|
| 233 |
+
st.subheader("Top Countries by Repositories")
|
| 234 |
df_countries = query_df(
|
| 235 |
+
f"SELECT r.country, COUNT(DISTINCT r.html_url) as count FROM repositories r {tag_join} {where} GROUP BY r.country ORDER BY count DESC LIMIT 20",
|
| 236 |
+
params,
|
| 237 |
)
|
| 238 |
+
if not df_countries.empty:
|
| 239 |
+
fig = px.bar(df_countries, x="country", y="count", color="count", color_continuous_scale="Blues")
|
| 240 |
+
fig.update_layout(showlegend=False, xaxis_title="Country", yaxis_title="Repositories", coloraxis_showscale=False)
|
| 241 |
+
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
|
| 242 |
|
| 243 |
with col_right:
|
| 244 |
+
st.subheader("Top Languages")
|
| 245 |
df_langs = query_df(
|
| 246 |
+
f"""SELECT r.language, COUNT(DISTINCT r.html_url) as count
|
| 247 |
+
FROM repositories r {tag_join} {where}
|
| 248 |
+
{"AND" if where else "WHERE"} r.language IS NOT NULL AND r.language != ''
|
| 249 |
+
GROUP BY r.language ORDER BY count DESC LIMIT 15""",
|
| 250 |
+
params,
|
| 251 |
)
|
| 252 |
+
if not df_langs.empty:
|
| 253 |
+
fig = px.bar(df_langs, x="count", y="language", orientation="h", color="count", color_continuous_scale="Greens")
|
| 254 |
+
fig.update_layout(showlegend=False, yaxis=dict(autorange="reversed"), xaxis_title="Repositories", yaxis_title="", coloraxis_showscale=False)
|
| 255 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 256 |
|
| 257 |
st.subheader("Repository Creation Timeline")
|
| 258 |
df_timeline = query_df(
|
| 259 |
+
f"""SELECT SUBSTR(r.created_at, 1, 4) as year, COUNT(DISTINCT r.html_url) as count
|
| 260 |
+
FROM repositories r {tag_join} {where}
|
| 261 |
+
{"AND" if where else "WHERE"} r.created_at IS NOT NULL
|
| 262 |
+
GROUP BY year ORDER BY year""",
|
| 263 |
+
params,
|
| 264 |
)
|
| 265 |
df_timeline = df_timeline[df_timeline["year"].str.match(r"^\d{4}$", na=False)]
|
| 266 |
+
|
| 267 |
+
# Also pull active repos per year (pushed_at within 12m of each year-end β proxy: pushed in that year or later)
|
| 268 |
+
df_pushed = query_df(
|
| 269 |
+
f"""SELECT SUBSTR(r.pushed_at, 1, 4) as year, COUNT(DISTINCT r.html_url) as active
|
| 270 |
+
FROM repositories r {tag_join} {where}
|
| 271 |
+
{"AND" if where else "WHERE"} r.pushed_at IS NOT NULL
|
| 272 |
+
GROUP BY year ORDER BY year""",
|
| 273 |
+
params,
|
| 274 |
)
|
| 275 |
+
df_pushed = df_pushed[df_pushed["year"].str.match(r"^\d{4}$", na=False)]
|
| 276 |
+
|
| 277 |
+
if not df_timeline.empty:
|
| 278 |
+
fig = go.Figure()
|
| 279 |
+
fig.add_trace(go.Scatter(
|
| 280 |
+
x=df_timeline["year"], y=df_timeline["count"],
|
| 281 |
+
name="Created", fill="tozeroy", mode="lines",
|
| 282 |
+
line=dict(color="#3b82f6"), fillcolor="rgba(59,130,246,0.2)"
|
| 283 |
+
))
|
| 284 |
+
if not df_pushed.empty:
|
| 285 |
+
fig.add_trace(go.Scatter(
|
| 286 |
+
x=df_pushed["year"], y=df_pushed["active"],
|
| 287 |
+
name="Last pushed", fill="tozeroy", mode="lines",
|
| 288 |
+
line=dict(color="#10b981"), fillcolor="rgba(16,185,129,0.15)"
|
| 289 |
+
))
|
| 290 |
+
fig.update_layout(xaxis_title="Year", yaxis_title="Repositories", legend=dict(orientation="h"))
|
| 291 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 292 |
+
st.caption("'Last pushed' shows when repositories last received a commit β a proxy for active maintenance.")
|
| 293 |
|
| 294 |
|
| 295 |
# ==================== EXPLORER ====================
|
| 296 |
with tab_explorer:
|
| 297 |
+
where_e, params_e, tag_join_e = build_tag_join_where()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
|
| 299 |
+
# Extra local search
|
| 300 |
+
search_text = st.text_input("Search name / description", key="exp_search")
|
| 301 |
if search_text:
|
| 302 |
+
where_e_conds, _ = build_where()
|
| 303 |
+
where_e_conds.append("(r.name LIKE ? OR r.description LIKE ?)")
|
| 304 |
+
params_e_local = params_e + [f"%{search_text}%", f"%{search_text}%"]
|
| 305 |
+
where_e_local = ("WHERE " + " AND ".join(where_e_conds + (["(r.name LIKE ? OR r.description LIKE ?)"] if search_text else []))) if where_e_conds else ""
|
| 306 |
+
else:
|
| 307 |
+
params_e_local = params_e
|
| 308 |
|
| 309 |
+
sort_col = st.selectbox("Sort by", ["stars", "forks", "pushed_at", "created_at"], key="exp_sort")
|
| 310 |
|
|
|
|
| 311 |
conn = get_conn()
|
| 312 |
+
count_sql = f"SELECT COUNT(DISTINCT r.html_url) FROM repositories r {tag_join_e} {where_e}"
|
| 313 |
+
if search_text:
|
| 314 |
+
extra = " AND (r.name LIKE ? OR r.description LIKE ?)"
|
| 315 |
+
total_results = conn.execute(count_sql + extra, params_e + [f"%{search_text}%", f"%{search_text}%"]).fetchone()[0]
|
| 316 |
+
else:
|
| 317 |
+
total_results = conn.execute(count_sql, params_e).fetchone()[0]
|
| 318 |
conn.close()
|
| 319 |
|
| 320 |
+
st.write(f"**{total_results:,}** repositories match current filters")
|
| 321 |
|
| 322 |
page_size = 50
|
| 323 |
total_pages = max(1, (total_results + page_size - 1) // page_size)
|
| 324 |
page = st.number_input("Page", min_value=1, max_value=total_pages, value=1, key="exp_page")
|
| 325 |
offset = (page - 1) * page_size
|
| 326 |
|
| 327 |
+
search_clause = " AND (r.name LIKE ? OR r.description LIKE ?)" if search_text else ""
|
| 328 |
+
search_params = [f"%{search_text}%", f"%{search_text}%"] if search_text else []
|
| 329 |
+
|
| 330 |
data_sql = f"""
|
| 331 |
+
SELECT r.html_url, r.name, r.owner, r.country, r.language, r.stars, r.forks,
|
| 332 |
+
r.license, r.created_at, r.pushed_at, r.archived, r.fork
|
| 333 |
+
FROM repositories r {tag_join_e} {where_e} {search_clause}
|
| 334 |
+
GROUP BY r.html_url
|
| 335 |
+
ORDER BY r.{sort_col} DESC
|
| 336 |
LIMIT ? OFFSET ?
|
| 337 |
"""
|
| 338 |
+
df_results = query_df(data_sql, params_e + search_params + [page_size, offset])
|
|
|
|
|
|
|
| 339 |
|
| 340 |
if not df_results.empty:
|
| 341 |
st.dataframe(
|
|
|
|
| 350 |
"forks": st.column_config.NumberColumn("π΄ Forks"),
|
| 351 |
"license": st.column_config.TextColumn("License"),
|
| 352 |
"created_at": st.column_config.TextColumn("Created"),
|
| 353 |
+
"pushed_at": st.column_config.TextColumn("Last pushed"),
|
| 354 |
+
"archived": st.column_config.CheckboxColumn("Archived"),
|
| 355 |
+
"fork": st.column_config.CheckboxColumn("Fork"),
|
| 356 |
},
|
| 357 |
use_container_width=True,
|
| 358 |
hide_index=True,
|
|
|
|
| 364 |
|
| 365 |
# ==================== TAGS ====================
|
| 366 |
with tab_tags:
|
| 367 |
+
where_t, params_t, tag_join_t = build_tag_join_where()
|
| 368 |
+
|
| 369 |
tagged_count_t = query_one("SELECT COUNT(DISTINCT html_url) FROM repository_tags")
|
| 370 |
total_repos_t = query_one("SELECT COUNT(*) FROM repositories")
|
| 371 |
|
| 372 |
+
if tagged_count_t < total_repos_t * 0.99:
|
| 373 |
pct = tagged_count_t / total_repos_t * 100 if total_repos_t > 0 else 0
|
| 374 |
st.info(
|
| 375 |
f"ποΈ **Tagging in progress** β {tagged_count_t:,} of {total_repos_t:,} repositories tagged ({pct:.1f}%). "
|
| 376 |
+
"Results below reflect partially tagged data."
|
| 377 |
)
|
| 378 |
|
| 379 |
+
col_tl, col_tr = st.columns(2)
|
| 380 |
+
|
| 381 |
+
with col_tl:
|
| 382 |
+
st.subheader("Top Tags")
|
| 383 |
+
df_top_tags = query_df(
|
| 384 |
+
f"""SELECT rt2.tag, COUNT(DISTINCT r.html_url) as count
|
| 385 |
+
FROM repositories r
|
| 386 |
+
JOIN repository_tags rt2 ON r.html_url = rt2.html_url
|
| 387 |
+
{tag_join_t.replace("rt", "rt_f") if sel_tags else ""}
|
| 388 |
+
{where_t.replace("rt.", "rt2.") if where_t else ""}
|
| 389 |
+
{"AND" if where_t else "WHERE"} rt2.tag IS NOT NULL
|
| 390 |
+
GROUP BY rt2.tag ORDER BY count DESC LIMIT 30""",
|
| 391 |
+
params_t,
|
| 392 |
)
|
| 393 |
+
if not df_top_tags.empty:
|
| 394 |
+
fig = px.bar(
|
| 395 |
+
df_top_tags, x="count", y="tag", orientation="h",
|
| 396 |
+
color="count", color_continuous_scale="Purples",
|
| 397 |
+
)
|
| 398 |
+
fig.update_layout(yaxis=dict(autorange="reversed"), showlegend=False, height=600,
|
| 399 |
+
xaxis_title="Repositories", yaxis_title="", coloraxis_showscale=False)
|
| 400 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 401 |
|
| 402 |
+
with col_tr:
|
| 403 |
+
st.subheader("Tags by Year Created")
|
| 404 |
+
st.caption("Repos tagged with each technology, by creation year β shows technology adoption over time.")
|
| 405 |
+
|
| 406 |
+
# Pick top 10 tags for the chart
|
| 407 |
+
if not df_top_tags.empty:
|
| 408 |
+
top10_tags = df_top_tags.head(10)["tag"].tolist()
|
| 409 |
+
ph = ",".join(["?"] * len(top10_tags))
|
| 410 |
+
conds_ty, params_ty = build_where(base_table="r")
|
| 411 |
+
conds_ty.append(f"rt3.tag IN ({ph})")
|
| 412 |
+
params_ty.extend(top10_tags)
|
| 413 |
+
conds_ty.append("r.created_at IS NOT NULL")
|
| 414 |
+
w_ty = ("WHERE " + " AND ".join(conds_ty)) if conds_ty else ""
|
| 415 |
+
df_tag_time = query_df(
|
| 416 |
+
f"""SELECT SUBSTR(r.created_at,1,4) as year, rt3.tag, COUNT(DISTINCT r.html_url) as count
|
| 417 |
+
FROM repositories r JOIN repository_tags rt3 ON r.html_url = rt3.html_url
|
| 418 |
+
{w_ty}
|
| 419 |
+
GROUP BY year, rt3.tag ORDER BY year""",
|
| 420 |
+
params_ty,
|
| 421 |
+
)
|
| 422 |
+
df_tag_time = df_tag_time[df_tag_time["year"].str.match(r"^\d{4}$", na=False)]
|
| 423 |
+
if not df_tag_time.empty:
|
| 424 |
+
fig = px.line(df_tag_time, x="year", y="count", color="tag",
|
| 425 |
+
labels={"year": "Year", "count": "Repos created", "tag": "Tag"})
|
| 426 |
+
fig.update_layout(legend=dict(orientation="h", y=-0.3))
|
| 427 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 428 |
|
| 429 |
+
st.divider()
|
| 430 |
st.subheader("Browse Repos by Tag")
|
| 431 |
+
browse_tags = df_top_tags["tag"].tolist() if not df_top_tags.empty else tags
|
| 432 |
+
if browse_tags:
|
| 433 |
+
sel_tag = st.selectbox("Select a tag", browse_tags, key="tag_browse")
|
| 434 |
+
sort_tag = st.selectbox("Sort by", ["stars", "pushed_at", "created_at"], key="tag_sort")
|
| 435 |
+
conds_br, params_br = build_where(base_table="r")
|
| 436 |
+
conds_br.append("rt_b.tag = ?")
|
| 437 |
+
params_br.append(sel_tag)
|
| 438 |
+
w_br = ("WHERE " + " AND ".join(conds_br)) if conds_br else ""
|
| 439 |
df_tag_repos = query_df(
|
| 440 |
+
f"""SELECT r.name, r.owner, r.country, r.language, r.stars, r.pushed_at, rt_b.confidence, r.html_url
|
| 441 |
+
FROM repository_tags rt_b JOIN repositories r ON rt_b.html_url = r.html_url
|
| 442 |
+
{w_br} ORDER BY r.{sort_tag} DESC LIMIT 200""",
|
| 443 |
+
params_br,
|
| 444 |
)
|
| 445 |
st.write(f"**{len(df_tag_repos)}** repos tagged with **{sel_tag}**")
|
| 446 |
if not df_tag_repos.empty:
|
|
|
|
| 449 |
column_config={
|
| 450 |
"html_url": st.column_config.LinkColumn("URL", display_text="Open"),
|
| 451 |
"confidence": st.column_config.ProgressColumn("Confidence", min_value=0, max_value=1),
|
| 452 |
+
"stars": st.column_config.NumberColumn("β Stars"),
|
| 453 |
+
"pushed_at": st.column_config.TextColumn("Last pushed"),
|
| 454 |
},
|
| 455 |
use_container_width=True,
|
| 456 |
hide_index=True,
|
| 457 |
)
|
| 458 |
+
|
| 459 |
+
st.divider()
|
| 460 |
+
st.subheader("Tag Groups")
|
| 461 |
+
df_groups = query_df("SELECT id, name, description FROM tag_groups ORDER BY name")
|
| 462 |
+
if not df_groups.empty:
|
| 463 |
+
for _, grp in df_groups.iterrows():
|
| 464 |
+
with st.expander(f"π {grp['name']}" + (f" β {grp['description']}" if grp["description"] else "")):
|
| 465 |
+
df_members = query_df(
|
| 466 |
+
"SELECT tag FROM tag_group_members WHERE group_id = ? ORDER BY tag",
|
| 467 |
+
[int(grp["id"])]
|
| 468 |
+
)
|
| 469 |
+
if not df_members.empty:
|
| 470 |
+
st.write(", ".join(df_members["tag"].tolist()))
|
| 471 |
+
else:
|
| 472 |
+
st.write("No tags in this group yet.")
|
| 473 |
else:
|
| 474 |
+
st.info("No tag groups defined yet.")
|
| 475 |
|
| 476 |
|
| 477 |
# ==================== INSIGHTS ====================
|
| 478 |
with tab_insights:
|
| 479 |
+
where_i, params_i, tag_join_i = build_tag_join_where()
|
| 480 |
+
|
| 481 |
+
col_ia, col_ib = st.columns(2)
|
| 482 |
+
|
| 483 |
+
with col_ia:
|
| 484 |
+
st.subheader("β Most Starred")
|
| 485 |
+
df_top = query_df(
|
| 486 |
+
f"""SELECT r.name, r.owner, r.country, r.stars, r.language, r.pushed_at, r.html_url
|
| 487 |
+
FROM repositories r {tag_join_i} {where_i}
|
| 488 |
+
GROUP BY r.html_url ORDER BY r.stars DESC LIMIT 25""",
|
| 489 |
+
params_i,
|
| 490 |
+
)
|
| 491 |
+
st.dataframe(
|
| 492 |
+
df_top,
|
| 493 |
+
column_config={
|
| 494 |
+
"html_url": st.column_config.LinkColumn("URL", display_text="Open"),
|
| 495 |
+
"stars": st.column_config.NumberColumn("β Stars"),
|
| 496 |
+
"pushed_at": st.column_config.TextColumn("Last pushed"),
|
| 497 |
+
},
|
| 498 |
+
use_container_width=True,
|
| 499 |
+
hide_index=True,
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
with col_ib:
|
| 503 |
+
st.subheader("π Rising Stars (active last 12m, sorted by stars)")
|
| 504 |
+
rising_cutoff = (datetime.now(timezone.utc) - timedelta(days=365)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 505 |
+
conds_r, params_r = build_where(base_table="r")
|
| 506 |
+
conds_r.append("r.pushed_at >= ?")
|
| 507 |
+
params_r.append(rising_cutoff)
|
| 508 |
+
tj_r = "JOIN repository_tags rt ON r.html_url = rt.html_url" if sel_tags else ""
|
| 509 |
+
if sel_tags:
|
| 510 |
+
ph = ",".join(["?"] * len(sel_tags))
|
| 511 |
+
conds_r.append(f"rt.tag IN ({ph})")
|
| 512 |
+
params_r.extend(sel_tags)
|
| 513 |
+
w_r = ("WHERE " + " AND ".join(conds_r)) if conds_r else ""
|
| 514 |
+
df_rising = query_df(
|
| 515 |
+
f"""SELECT r.name, r.owner, r.country, r.stars, r.language, r.pushed_at, r.html_url
|
| 516 |
+
FROM repositories r {tj_r} {w_r}
|
| 517 |
+
GROUP BY r.html_url ORDER BY r.stars DESC LIMIT 25""",
|
| 518 |
+
params_r,
|
| 519 |
+
)
|
| 520 |
+
st.dataframe(
|
| 521 |
+
df_rising,
|
| 522 |
+
column_config={
|
| 523 |
+
"html_url": st.column_config.LinkColumn("URL", display_text="Open"),
|
| 524 |
+
"stars": st.column_config.NumberColumn("β Stars"),
|
| 525 |
+
"pushed_at": st.column_config.TextColumn("Last pushed"),
|
| 526 |
+
},
|
| 527 |
+
use_container_width=True,
|
| 528 |
+
hide_index=True,
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
st.divider()
|
| 532 |
+
|
| 533 |
+
st.subheader("π Most Active Organisations")
|
| 534 |
+
st.caption("Organisations ranked by number of repos with a push in the last 12 months.")
|
| 535 |
+
conds_ao, params_ao = build_where(base_table="r")
|
| 536 |
+
conds_ao.append("r.pushed_at >= ?")
|
| 537 |
+
params_ao.append((datetime.now(timezone.utc) - timedelta(days=365)).strftime("%Y-%m-%dT%H:%M:%SZ"))
|
| 538 |
+
tj_ao = "JOIN repository_tags rt ON r.html_url = rt.html_url" if sel_tags else ""
|
| 539 |
+
if sel_tags:
|
| 540 |
+
ph = ",".join(["?"] * len(sel_tags))
|
| 541 |
+
conds_ao.append(f"rt.tag IN ({ph})")
|
| 542 |
+
params_ao.extend(sel_tags)
|
| 543 |
+
w_ao = ("WHERE " + " AND ".join(conds_ao)) if conds_ao else ""
|
| 544 |
+
df_active_orgs = query_df(
|
| 545 |
+
f"""SELECT r.owner, r.country, COUNT(DISTINCT r.html_url) as active_repos,
|
| 546 |
+
SUM(r.stars) as total_stars
|
| 547 |
+
FROM repositories r {tj_ao} {w_ao}
|
| 548 |
+
GROUP BY r.owner ORDER BY active_repos DESC LIMIT 20""",
|
| 549 |
+
params_ao,
|
| 550 |
)
|
| 551 |
+
col_org1, col_org2 = st.columns(2)
|
| 552 |
+
with col_org1:
|
| 553 |
+
if not df_active_orgs.empty:
|
| 554 |
+
fig = px.bar(df_active_orgs, x="active_repos", y="owner", orientation="h",
|
| 555 |
+
color="active_repos", color_continuous_scale="Oranges",
|
| 556 |
+
labels={"active_repos": "Active repos (12m)", "owner": ""})
|
| 557 |
+
fig.update_layout(yaxis=dict(autorange="reversed"), showlegend=False,
|
| 558 |
+
height=500, coloraxis_showscale=False)
|
| 559 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 560 |
+
with col_org2:
|
| 561 |
+
if not df_active_orgs.empty:
|
| 562 |
+
st.dataframe(df_active_orgs, use_container_width=True, hide_index=True,
|
| 563 |
+
column_config={"total_stars": st.column_config.NumberColumn("β Total stars"),
|
| 564 |
+
"active_repos": st.column_config.NumberColumn("Active repos (12m)")})
|
| 565 |
|
| 566 |
st.divider()
|
|
|
|
| 567 |
|
| 568 |
+
col_ic, col_id = st.columns(2)
|
| 569 |
+
|
| 570 |
+
with col_ic:
|
| 571 |
st.subheader("π License Breakdown")
|
| 572 |
df_lic = query_df(
|
| 573 |
+
f"""SELECT r.license, COUNT(DISTINCT r.html_url) as count
|
| 574 |
+
FROM repositories r {tag_join_i} {where_i}
|
| 575 |
+
{"AND" if where_i else "WHERE"} r.license IS NOT NULL AND r.license != ''
|
| 576 |
+
GROUP BY r.license ORDER BY count DESC""",
|
| 577 |
+
params_i,
|
| 578 |
)
|
| 579 |
if not df_lic.empty:
|
| 580 |
top_n = 10
|
|
|
|
| 588 |
fig.update_traces(textposition="inside", textinfo="percent+label")
|
| 589 |
st.plotly_chart(fig, use_container_width=True)
|
| 590 |
|
| 591 |
+
with col_id:
|
| 592 |
st.subheader("π΄ Fork vs Original")
|
| 593 |
+
fork_count = query_one(
|
| 594 |
+
f"SELECT COUNT(DISTINCT r.html_url) FROM repositories r {tag_join_i} {where_i} {'AND' if where_i else 'WHERE'} r.fork = 1",
|
| 595 |
+
params_i,
|
| 596 |
+
)
|
| 597 |
+
original_count = query_one(
|
| 598 |
+
f"SELECT COUNT(DISTINCT r.html_url) FROM repositories r {tag_join_i} {where_i} {'AND' if where_i else 'WHERE'} (r.fork = 0 OR r.fork IS NULL)",
|
| 599 |
+
params_i,
|
| 600 |
+
)
|
| 601 |
m1, m2 = st.columns(2)
|
| 602 |
+
m1.metric("Original", f"{original_count:,}")
|
| 603 |
+
m2.metric("Forked", f"{fork_count:,}")
|
| 604 |
fig = px.pie(
|
| 605 |
pd.DataFrame({"type": ["Original", "Fork"], "count": [original_count, fork_count]}),
|
| 606 |
names="type", values="count", hole=0.4,
|
| 607 |
+
color_discrete_sequence=["#2ecc71", "#e74c3c"],
|
| 608 |
)
|
| 609 |
st.plotly_chart(fig, use_container_width=True)
|
| 610 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
st.divider()
|
| 612 |
st.subheader("π Language Γ Country Heatmap")
|
| 613 |
+
df_heat = query_df(
|
| 614 |
+
f"""SELECT r.country, r.language, COUNT(DISTINCT r.html_url) as count
|
| 615 |
+
FROM repositories r {tag_join_i} {where_i}
|
| 616 |
+
{"AND" if where_i else "WHERE"} r.language IS NOT NULL AND r.language != ''
|
| 617 |
+
AND r.country IN (
|
| 618 |
+
SELECT country FROM repositories GROUP BY country ORDER BY COUNT(*) DESC LIMIT 15
|
| 619 |
+
)
|
| 620 |
+
AND r.language IN (
|
| 621 |
+
SELECT language FROM repositories WHERE language IS NOT NULL AND language != ''
|
| 622 |
+
GROUP BY language ORDER BY COUNT(*) DESC LIMIT 12
|
| 623 |
+
)
|
| 624 |
+
GROUP BY r.country, r.language""",
|
| 625 |
+
params_i,
|
| 626 |
+
)
|
| 627 |
if not df_heat.empty:
|
| 628 |
pivot = df_heat.pivot_table(index="country", columns="language", values="count", fill_value=0)
|
| 629 |
+
fig = px.imshow(pivot, text_auto=True, color_continuous_scale="YlOrRd",
|
| 630 |
+
labels=dict(x="Language", y="Country", color="Repos"), aspect="auto")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
fig.update_layout(height=500)
|
| 632 |
st.plotly_chart(fig, use_container_width=True)
|
| 633 |
else:
|
| 634 |
st.info("Not enough data for heatmap.")
|
| 635 |
|
| 636 |
+
|
| 637 |
st.divider()
|
| 638 |
+
st.caption(
|
| 639 |
+
"Data sourced from government GitHub accounts worldwide. Built with Streamlit. "
|
| 640 |
+
"| [GitHub](https://github.com/AndreasThinks/open-govtech-report) "
|
| 641 |
+
"| [Dataset](https://huggingface.co/datasets/AndreasThinks/government-github-repos)"
|
| 642 |
+
)
|