AndreasThinks commited on
Commit
b69f783
·
verified ·
1 Parent(s): f2cc925

feat: add Trends tab (tag momentum, monthly repos, language trends)

Browse files
Files changed (1) hide show
  1. app.py +249 -4
app.py CHANGED
@@ -181,8 +181,8 @@ 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
 
@@ -634,9 +634,254 @@ with tab_insights:
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
  )
 
181
  st.caption("Exploring 70k+ government GitHub repositories worldwide")
182
 
183
  # ==================== TABS ====================
184
+ tab_overview, tab_explorer, tab_tags, tab_insights, tab_trends, tab_about = st.tabs(
185
+ ["📊 Overview", "🔍 Explorer", "🏷️ Tags", "💡 Insights", "📈 Trends", "ℹ️ About"]
186
  )
187
 
188
 
 
634
  st.info("Not enough data for heatmap.")
635
 
636
 
637
+ # ==================== TRENDS ====================
638
+ with tab_trends:
639
+ st.subheader("📈 Trends Over Time")
640
+ st.caption("How government open source has evolved — new activity, rising tags, and shifting languages.")
641
+
642
+ # ---- 1. New repos per month (last 24 months) ----
643
+ st.markdown("### 🗓️ New Repositories per Month")
644
+ st.caption("Monthly repo creation over the last 2 years.")
645
+
646
+ cutoff_24m = (datetime.now(timezone.utc) - timedelta(days=730)).strftime("%Y-%m-%dT%H:%M:%SZ")
647
+ conds_m, params_m = build_where(base_table="r")
648
+ conds_m.append("r.created_at >= ?")
649
+ params_m.append(cutoff_24m)
650
+ w_m = ("WHERE " + " AND ".join(conds_m)) if conds_m else ""
651
+ tj_m = "JOIN repository_tags rt ON r.html_url = rt.html_url" if sel_tags else ""
652
+ if sel_tags:
653
+ ph = ",".join(["?"] * len(sel_tags))
654
+ conds_m.append(f"rt.tag IN ({ph})")
655
+ params_m.extend(sel_tags)
656
+ w_m = ("WHERE " + " AND ".join(conds_m)) if conds_m else ""
657
+
658
+ df_monthly = query_df(
659
+ f"""SELECT SUBSTR(r.created_at, 1, 7) as month, COUNT(DISTINCT r.html_url) as new_repos
660
+ FROM repositories r {tj_m} {w_m}
661
+ GROUP BY month ORDER BY month""",
662
+ params_m,
663
+ )
664
+ df_monthly = df_monthly[df_monthly["month"].str.match(r"^\d{{4}}-\d{{2}}$", na=False)]
665
+ if not df_monthly.empty:
666
+ fig = px.bar(
667
+ df_monthly, x="month", y="new_repos",
668
+ labels={"month": "Month", "new_repos": "New repositories"},
669
+ color="new_repos", color_continuous_scale="Blues",
670
+ )
671
+ fig.update_layout(coloraxis_showscale=False, xaxis_title="Month", yaxis_title="New repos")
672
+ st.plotly_chart(fig, use_container_width=True)
673
+ else:
674
+ st.info("Not enough data for monthly chart.")
675
+
676
+ st.divider()
677
+
678
+ # ---- 2. Tag momentum ----
679
+ st.markdown("### 🚀 Tag Momentum")
680
+ st.caption(
681
+ "Tags ranked by growth — repos tagged with each label in the last 12 months vs the previous 12 months. "
682
+ "Higher ratio = faster-growing category."
683
+ )
684
+
685
+ now_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
686
+ cutoff_12m = (datetime.now(timezone.utc) - timedelta(days=365)).strftime("%Y-%m-%dT%H:%M:%SZ")
687
+ cutoff_24m_str = (datetime.now(timezone.utc) - timedelta(days=730)).strftime("%Y-%m-%dT%H:%M:%SZ")
688
+
689
+ conds_base, params_base = build_where(base_table="r")
690
+ w_base = (" AND ".join(conds_base)) if conds_base else ""
691
+ and_base = ("AND " + w_base) if w_base else ""
692
+
693
+ df_momentum = query_df(
694
+ f"""
695
+ SELECT
696
+ rt.tag,
697
+ COUNT(DISTINCT CASE WHEN r.created_at >= ? THEN r.html_url END) as recent,
698
+ COUNT(DISTINCT CASE WHEN r.created_at >= ? AND r.created_at < ? THEN r.html_url END) as prior
699
+ FROM repository_tags rt
700
+ JOIN repositories r ON rt.html_url = r.html_url
701
+ WHERE r.created_at IS NOT NULL {and_base}
702
+ GROUP BY rt.tag
703
+ HAVING recent >= 5 AND prior >= 5
704
+ ORDER BY (CAST(recent AS FLOAT) / prior) DESC
705
+ LIMIT 30
706
+ """,
707
+ [cutoff_12m, cutoff_24m_str, cutoff_12m] + params_base,
708
+ )
709
+
710
+ if not df_momentum.empty:
711
+ df_momentum["growth_ratio"] = (df_momentum["recent"] / df_momentum["prior"]).round(2)
712
+ df_momentum["growth_pct"] = ((df_momentum["growth_ratio"] - 1) * 100).round(1)
713
+
714
+ col_m1, col_m2 = st.columns(2)
715
+
716
+ with col_m1:
717
+ st.markdown("**Fastest growing tags** (last 12m vs prior 12m)")
718
+ fig = px.bar(
719
+ df_momentum.head(20), x="growth_ratio", y="tag", orientation="h",
720
+ color="growth_ratio", color_continuous_scale="Greens",
721
+ labels={"growth_ratio": "Growth ratio (recent / prior)", "tag": "Tag"},
722
+ hover_data={"recent": True, "prior": True, "growth_pct": True},
723
+ )
724
+ fig.update_layout(
725
+ yaxis=dict(autorange="reversed"), height=550,
726
+ coloraxis_showscale=False, xaxis_title="Growth ratio", yaxis_title="",
727
+ )
728
+ fig.add_vline(x=1.0, line_dash="dash", line_color="grey", annotation_text="no change")
729
+ st.plotly_chart(fig, use_container_width=True)
730
+
731
+ with col_m2:
732
+ st.markdown("**Top 20 by absolute recent volume**")
733
+ df_recent_top = query_df(
734
+ f"""
735
+ SELECT rt.tag, COUNT(DISTINCT r.html_url) as recent_count
736
+ FROM repository_tags rt JOIN repositories r ON rt.html_url = r.html_url
737
+ WHERE r.created_at >= ? {and_base}
738
+ GROUP BY rt.tag ORDER BY recent_count DESC LIMIT 20
739
+ """,
740
+ [cutoff_12m] + params_base,
741
+ )
742
+ if not df_recent_top.empty:
743
+ fig2 = px.bar(
744
+ df_recent_top, x="recent_count", y="tag", orientation="h",
745
+ color="recent_count", color_continuous_scale="Purples",
746
+ labels={"recent_count": "Repos (last 12m)", "tag": "Tag"},
747
+ )
748
+ fig2.update_layout(
749
+ yaxis=dict(autorange="reversed"), height=550,
750
+ coloraxis_showscale=False, xaxis_title="Repos (last 12m)", yaxis_title="",
751
+ )
752
+ st.plotly_chart(fig2, use_container_width=True)
753
+
754
+ # Emerging tags table
755
+ st.markdown("**Emerging tags** — ratio > 1.5, sorted by growth")
756
+ df_emerging = df_momentum[df_momentum["growth_ratio"] >= 1.5][
757
+ ["tag", "recent", "prior", "growth_ratio", "growth_pct"]
758
+ ].rename(columns={
759
+ "tag": "Tag", "recent": "Last 12m", "prior": "Prior 12m",
760
+ "growth_ratio": "Ratio", "growth_pct": "Growth %"
761
+ })
762
+ if not df_emerging.empty:
763
+ st.dataframe(df_emerging, use_container_width=True, hide_index=True)
764
+ else:
765
+ st.info("No tags with >50% growth in this period.")
766
+ else:
767
+ st.info("Not enough tagged data yet to compute momentum.")
768
+
769
+ st.divider()
770
+
771
+ # ---- 3. Language trends ----
772
+ st.markdown("### 💻 Language Trends")
773
+ st.caption("Year-over-year share of new repositories by primary language — top 10 languages.")
774
+
775
+ df_lang_year = query_df(
776
+ f"""
777
+ SELECT SUBSTR(r.created_at, 1, 4) as year, r.language,
778
+ COUNT(DISTINCT r.html_url) as count
779
+ FROM repositories r
780
+ WHERE r.language IS NOT NULL AND r.language != ''
781
+ AND r.created_at IS NOT NULL
782
+ AND r.language IN (
783
+ SELECT language FROM repositories
784
+ WHERE language IS NOT NULL AND language != ''
785
+ GROUP BY language ORDER BY COUNT(*) DESC LIMIT 10
786
+ )
787
+ AND SUBSTR(r.created_at, 1, 4) BETWEEN '2015' AND SUBSTR(?, 1, 4)
788
+ GROUP BY year, r.language ORDER BY year
789
+ """,
790
+ [now_str],
791
+ )
792
+ df_lang_year = df_lang_year[df_lang_year["year"].str.match(r"^\d{4}$", na=False)]
793
+
794
+ if not df_lang_year.empty:
795
+ # Absolute count line chart
796
+ fig_lang = px.line(
797
+ df_lang_year, x="year", y="count", color="language",
798
+ labels={"year": "Year", "count": "New repositories", "language": "Language"},
799
+ markers=True,
800
+ )
801
+ fig_lang.update_layout(legend=dict(orientation="h", y=-0.25))
802
+ st.plotly_chart(fig_lang, use_container_width=True)
803
+
804
+ # Share / normalised stacked area
805
+ st.caption("As a share of all new repos that year (top 10 languages).")
806
+ df_totals = df_lang_year.groupby("year")["count"].sum().reset_index().rename(columns={"count": "total"})
807
+ df_share = df_lang_year.merge(df_totals, on="year")
808
+ df_share["share"] = (df_share["count"] / df_share["total"] * 100).round(1)
809
+
810
+ fig_share = px.area(
811
+ df_share, x="year", y="share", color="language",
812
+ labels={"year": "Year", "share": "Share of new repos (%)", "language": "Language"},
813
+ groupnorm="",
814
+ )
815
+ fig_share.update_layout(legend=dict(orientation="h", y=-0.25), yaxis_title="Share (%)")
816
+ st.plotly_chart(fig_share, use_container_width=True)
817
+ else:
818
+ st.info("Not enough data for language trends.")
819
+
820
+
821
+ # ==================== ABOUT ====================
822
+ with tab_about:
823
+ st.subheader("About GovTech GitHub Explorer")
824
+ st.write(
825
+ """
826
+ **GovTech GitHub Explorer** maps the global landscape of government open source software.
827
+ It discovers, scrapes, and automatically categorises every public GitHub repository
828
+ belonging to government organisations worldwide — updated weekly.
829
+ """
830
+ )
831
+
832
+ st.subheader("How it works")
833
+ col_a1, col_a2, col_a3, col_a4 = st.columns(4)
834
+ with col_a1:
835
+ st.markdown("### 🔍 Discover")
836
+ st.write("Government GitHub accounts are sourced from the [government.github.com](https://github.com/github/government.github.com) registry — ~2,000 organisations across 100+ countries.")
837
+ with col_a2:
838
+ st.markdown("### 🕷️ Scrape")
839
+ st.write("Repository metadata is collected via the GitHub API using a GitHub App installation, giving high-throughput authenticated access.")
840
+ with col_a3:
841
+ st.markdown("### 🏷️ Tag")
842
+ st.write("An LLM pipeline (Qwen3-32B via OpenRouter) reads each repository's metadata and README, then assigns structured tags and categories.")
843
+ with col_a4:
844
+ st.markdown("### 📊 Explore")
845
+ st.write("Tags are clustered into groups using embedding similarity, and the full dataset is published to HuggingFace for anyone to use.")
846
+
847
+ st.divider()
848
+
849
+ st.subheader("Data")
850
+ col_d1, col_d2, col_d3 = st.columns(3)
851
+ total_a = query_one("SELECT COUNT(*) FROM repositories")
852
+ tagged_a = query_one("SELECT COUNT(DISTINCT html_url) FROM repository_tags")
853
+ tag_count_a = query_one("SELECT COUNT(*) FROM tags")
854
+ col_d1.metric("Repositories", f"{total_a:,}")
855
+ col_d2.metric("Tagged", f"{tagged_a:,}")
856
+ col_d3.metric("Unique tags", f"{tag_count_a:,}")
857
+
858
+ st.write(
859
+ "The full dataset — including repo metadata, tags, and tag groups — is available on "
860
+ "[HuggingFace](https://huggingface.co/datasets/AndreasThinks/government-github-repos) "
861
+ "in CSV, Parquet, and SQLite formats. Updated every Sunday."
862
+ )
863
+
864
+ st.divider()
865
+
866
+ st.subheader("Source")
867
+ st.write(
868
+ "The scraper, tagger, and dashboard are all open source. "
869
+ "Pull requests and issues welcome."
870
+ )
871
+ st.markdown("[github.com/AndreasThinks/open-govtech-report](https://github.com/AndreasThinks/open-govtech-report)")
872
+
873
+ st.divider()
874
+ st.markdown(
875
+ "✨ A project by [AndreasThinks](https://andreasthinks.me), built with ❤️ using Streamlit, "
876
+ "and some ✨vibes✨",
877
+ unsafe_allow_html=True,
878
+ )
879
+
880
+
881
  st.divider()
882
  st.caption(
883
+ "Data sourced from government GitHub accounts worldwide. Updated weekly. "
884
  "| [GitHub](https://github.com/AndreasThinks/open-govtech-report) "
885
+ "| [Dataset](https://huggingface.co/datasets/AndreasThinks/government-github-repos) "
886
+ "| ✨ A project by [AndreasThinks](https://andreasthinks.me)"
887
  )