AndreasThinks commited on
Commit
b2f76a4
Β·
verified Β·
1 Parent(s): 2d97c94

feat: period selector on Trends tab (week/month/3m/6m/12m/2y)

Browse files
Files changed (1) hide show
  1. app.py +108 -37
app.py CHANGED
@@ -674,14 +674,61 @@ with tab_trends:
674
  st.subheader("πŸ“ˆ Trends Over Time")
675
  st.caption("How government open source has evolved β€” new activity, rising tags, and shifting languages.")
676
 
677
- # ---- 1. New repos per month (last 24 months) ----
678
- st.markdown("### πŸ—“οΈ New Repositories per Month")
679
- st.caption("Monthly repo creation over the last 2 years.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
 
681
- cutoff_24m = (datetime.now(timezone.utc) - timedelta(days=730)).strftime("%Y-%m-%dT%H:%M:%SZ")
682
  conds_m, params_m = build_where(base_table="r")
683
  conds_m.append("r.created_at >= ?")
684
- params_m.append(cutoff_24m)
685
  w_m = ("WHERE " + " AND ".join(conds_m)) if conds_m else ""
686
  tj_m = "JOIN repository_tags rt ON r.html_url = rt.html_url" if sel_tags else ""
687
  if sel_tags:
@@ -690,43 +737,68 @@ with tab_trends:
690
  params_m.extend(sel_tags)
691
  w_m = ("WHERE " + " AND ".join(conds_m)) if conds_m else ""
692
 
693
- df_monthly = query_df(
694
- f"""SELECT SUBSTR(r.created_at, 1, 7) as month, COUNT(DISTINCT r.html_url) as new_repos
695
  FROM repositories r {tj_m} {w_m}
696
- GROUP BY month ORDER BY month""",
697
  params_m,
698
  )
699
- df_monthly = df_monthly[df_monthly["month"].str.match(r"^\d{{4}}-\d{{2}}$", na=False)]
700
- if not df_monthly.empty:
701
  fig = px.bar(
702
- df_monthly, x="month", y="new_repos",
703
- labels={"month": "Month", "new_repos": "New repositories"},
704
  color="new_repos", color_continuous_scale="Blues",
705
  )
706
- fig.update_layout(coloraxis_showscale=False, xaxis_title="Month", yaxis_title="New repos")
707
  st.plotly_chart(fig, use_container_width=True)
708
  else:
709
- st.info("Not enough data for monthly chart.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
710
 
711
  st.divider()
712
 
713
  # ---- 2. Tag momentum ----
714
- st.markdown("### πŸš€ Tag Momentum")
715
  st.caption(
716
- "Tags ranked by growth β€” repos tagged with each label in the last 12 months vs the previous 12 months. "
717
  "Higher ratio = faster-growing category."
718
  )
719
 
720
- now_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
721
- cutoff_12m = (datetime.now(timezone.utc) - timedelta(days=365)).strftime("%Y-%m-%dT%H:%M:%SZ")
722
- cutoff_24m_str = (datetime.now(timezone.utc) - timedelta(days=730)).strftime("%Y-%m-%dT%H:%M:%SZ")
723
 
724
- conds_base, params_base = build_where(base_table="r")
725
- w_base = (" AND ".join(conds_base)) if conds_base else ""
726
- and_base = ("AND " + w_base) if w_base else ""
727
-
728
- tf_sql_mom = _tag_filter_sql("rt.tag")
729
- tf_params_mom = _tag_filter_params()
730
  df_momentum = query_df(
731
  f"""
732
  SELECT
@@ -737,11 +809,11 @@ with tab_trends:
737
  JOIN repositories r ON rt.html_url = r.html_url
738
  WHERE r.created_at IS NOT NULL AND {tf_sql_mom} {and_base}
739
  GROUP BY rt.tag
740
- HAVING recent >= 5 AND prior >= 5
741
  ORDER BY (CAST(recent AS FLOAT) / prior) DESC
742
  LIMIT 30
743
  """,
744
- [cutoff_12m, cutoff_24m_str, cutoff_12m] + tf_params_mom + params_base,
745
  )
746
 
747
  if not df_momentum.empty:
@@ -751,7 +823,7 @@ with tab_trends:
751
  col_m1, col_m2 = st.columns(2)
752
 
753
  with col_m1:
754
- st.markdown("**Fastest growing tags** (last 12m vs prior 12m)")
755
  fig = px.bar(
756
  df_momentum.head(20), x="growth_ratio", y="tag", orientation="h",
757
  color="growth_ratio", color_continuous_scale="Greens",
@@ -766,7 +838,7 @@ with tab_trends:
766
  st.plotly_chart(fig, use_container_width=True)
767
 
768
  with col_m2:
769
- st.markdown("**Top 20 by absolute recent volume**")
770
  df_recent_top = query_df(
771
  f"""
772
  SELECT rt.tag, COUNT(DISTINCT r.html_url) as recent_count
@@ -774,26 +846,27 @@ with tab_trends:
774
  WHERE r.created_at >= ? AND {tf_sql_mom} {and_base}
775
  GROUP BY rt.tag ORDER BY recent_count DESC LIMIT 20
776
  """,
777
- [cutoff_12m] + tf_params_mom + params_base,
778
  )
779
  if not df_recent_top.empty:
780
  fig2 = px.bar(
781
  df_recent_top, x="recent_count", y="tag", orientation="h",
782
  color="recent_count", color_continuous_scale="Purples",
783
- labels={"recent_count": "Repos (last 12m)", "tag": "Tag"},
784
  )
785
  fig2.update_layout(
786
  yaxis=dict(autorange="reversed"), height=550,
787
- coloraxis_showscale=False, xaxis_title="Repos (last 12m)", yaxis_title="",
 
788
  )
789
  st.plotly_chart(fig2, use_container_width=True)
790
 
791
  # Emerging tags table
792
- st.markdown("**Emerging tags** β€” ratio > 1.5, sorted by growth")
793
  df_emerging = df_momentum[df_momentum["growth_ratio"] >= 1.5][
794
  ["tag", "recent", "prior", "growth_ratio", "growth_pct"]
795
  ].rename(columns={
796
- "tag": "Tag", "recent": "Last 12m", "prior": "Prior 12m",
797
  "growth_ratio": "Ratio", "growth_pct": "Growth %"
798
  })
799
  if not df_emerging.empty:
@@ -801,7 +874,7 @@ with tab_trends:
801
  else:
802
  st.info("No tags with >50% growth in this period.")
803
  else:
804
- st.info("Not enough tagged data yet to compute momentum.")
805
 
806
  st.divider()
807
 
@@ -829,7 +902,6 @@ with tab_trends:
829
  df_lang_year = df_lang_year[df_lang_year["year"].str.match(r"^\d{4}$", na=False)]
830
 
831
  if not df_lang_year.empty:
832
- # Absolute count line chart
833
  fig_lang = px.line(
834
  df_lang_year, x="year", y="count", color="language",
835
  labels={"year": "Year", "count": "New repositories", "language": "Language"},
@@ -838,7 +910,6 @@ with tab_trends:
838
  fig_lang.update_layout(legend=dict(orientation="h", y=-0.25))
839
  st.plotly_chart(fig_lang, use_container_width=True)
840
 
841
- # Share / normalised stacked area
842
  st.caption("As a share of all new repos that year (top 10 languages).")
843
  df_totals = df_lang_year.groupby("year")["count"].sum().reset_index().rename(columns={"count": "total"})
844
  df_share = df_lang_year.merge(df_totals, on="year")
 
674
  st.subheader("πŸ“ˆ Trends Over Time")
675
  st.caption("How government open source has evolved β€” new activity, rising tags, and shifting languages.")
676
 
677
+ # ---- Period selector ----
678
+ PERIOD_OPTIONS = {
679
+ "This week": 7,
680
+ "This month": 30,
681
+ "Last 3 months": 90,
682
+ "Last 6 months": 180,
683
+ "Last 12 months": 365,
684
+ "Last 2 years": 730,
685
+ }
686
+ period_label = st.radio(
687
+ "Period", list(PERIOD_OPTIONS.keys()), index=4,
688
+ horizontal=True, key="trends_period",
689
+ )
690
+ period_days = PERIOD_OPTIONS[period_label]
691
+ period_prior_days = period_days * 2 # prior window = same length, shifted back
692
+
693
+ now_utc = datetime.now(timezone.utc)
694
+ now_str = now_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
695
+ cutoff_recent = (now_utc - timedelta(days=period_days)).strftime("%Y-%m-%dT%H:%M:%SZ")
696
+ cutoff_prior = (now_utc - timedelta(days=period_prior_days)).strftime("%Y-%m-%dT%H:%M:%SZ")
697
+
698
+ # Granularity: week/month buckets depending on period
699
+ if period_days <= 30:
700
+ bucket_fmt = "%Y-%W" # ISO week
701
+ bucket_label = "Week"
702
+ elif period_days <= 365:
703
+ bucket_fmt = "%Y-%m" # Month
704
+ bucket_label = "Month"
705
+ else:
706
+ bucket_fmt = "%Y-%m"
707
+ bucket_label = "Month"
708
+
709
+ # For the repos chart, show the bucket pattern for the selected period
710
+ # SQLite STRFTIME format
711
+ if period_days <= 30:
712
+ sql_bucket = "STRFTIME('%Y-%W', r.created_at)"
713
+ bucket_re = r"^\d{4}-\d{2}$"
714
+ else:
715
+ sql_bucket = "SUBSTR(r.created_at, 1, 7)"
716
+ bucket_re = r"^\d{4}-\d{2}$"
717
+
718
+ st.divider()
719
+
720
+ conds_base, params_base = build_where(base_table="r")
721
+ w_base = (" AND ".join(conds_base)) if conds_base else ""
722
+ and_base = ("AND " + w_base) if w_base else ""
723
+ tf_sql_mom = _tag_filter_sql("rt.tag")
724
+ tf_params_mom = _tag_filter_params()
725
+
726
+ # ---- 1. New repos per period ----
727
+ st.markdown(f"### πŸ—“οΈ New Repositories β€” {period_label}")
728
 
 
729
  conds_m, params_m = build_where(base_table="r")
730
  conds_m.append("r.created_at >= ?")
731
+ params_m.append(cutoff_recent)
732
  w_m = ("WHERE " + " AND ".join(conds_m)) if conds_m else ""
733
  tj_m = "JOIN repository_tags rt ON r.html_url = rt.html_url" if sel_tags else ""
734
  if sel_tags:
 
737
  params_m.extend(sel_tags)
738
  w_m = ("WHERE " + " AND ".join(conds_m)) if conds_m else ""
739
 
740
+ df_activity = query_df(
741
+ f"""SELECT {sql_bucket} as bucket, COUNT(DISTINCT r.html_url) as new_repos
742
  FROM repositories r {tj_m} {w_m}
743
+ GROUP BY bucket ORDER BY bucket""",
744
  params_m,
745
  )
746
+ df_activity = df_activity[df_activity["bucket"].str.match(bucket_re, na=False)]
747
+ if not df_activity.empty:
748
  fig = px.bar(
749
+ df_activity, x="bucket", y="new_repos",
750
+ labels={"bucket": bucket_label, "new_repos": "New repositories"},
751
  color="new_repos", color_continuous_scale="Blues",
752
  )
753
+ fig.update_layout(coloraxis_showscale=False, xaxis_title=bucket_label, yaxis_title="New repos")
754
  st.plotly_chart(fig, use_container_width=True)
755
  else:
756
+ st.info("Not enough data for this period.")
757
+
758
+ # Fastest growing repos this period (by stars delta proxy: recently created + high stars)
759
+ st.markdown(f"#### 🌟 Top New Repos β€” {period_label}")
760
+ st.caption("Highest-starred repositories created in the selected period.")
761
+ conds_nr, params_nr = build_where(base_table="r")
762
+ conds_nr.append("r.created_at >= ?")
763
+ params_nr.append(cutoff_recent)
764
+ w_nr = ("WHERE " + " AND ".join(conds_nr)) if conds_nr else ""
765
+ tj_nr = "JOIN repository_tags rt ON r.html_url = rt.html_url" if sel_tags else ""
766
+ if sel_tags:
767
+ ph = ",".join(["?"] * len(sel_tags))
768
+ conds_nr.append(f"rt.tag IN ({ph})")
769
+ params_nr.extend(sel_tags)
770
+ w_nr = ("WHERE " + " AND ".join(conds_nr)) if conds_nr else ""
771
+ df_new_repos = query_df(
772
+ f"""SELECT r.name, r.owner, r.country, r.language, r.stars, r.created_at, r.html_url
773
+ FROM repositories r {tj_nr} {w_nr}
774
+ GROUP BY r.html_url ORDER BY r.stars DESC LIMIT 20""",
775
+ params_nr,
776
+ )
777
+ if not df_new_repos.empty:
778
+ st.dataframe(
779
+ df_new_repos,
780
+ column_config={
781
+ "html_url": st.column_config.LinkColumn("URL", display_text="Open"),
782
+ "stars": st.column_config.NumberColumn("⭐ Stars"),
783
+ "created_at": st.column_config.TextColumn("Created"),
784
+ },
785
+ use_container_width=True, hide_index=True,
786
+ )
787
+ else:
788
+ st.info("No new repos in this period.")
789
 
790
  st.divider()
791
 
792
  # ---- 2. Tag momentum ----
793
+ st.markdown(f"### πŸš€ Tag Momentum β€” {period_label} vs prior {period_label.lower()}")
794
  st.caption(
795
+ f"Tags ranked by growth β€” repos created in the selected period vs the equivalent period before it. "
796
  "Higher ratio = faster-growing category."
797
  )
798
 
799
+ # Minimum repo count scales with period to avoid noise on short windows
800
+ min_repos = max(2, period_days // 60)
 
801
 
 
 
 
 
 
 
802
  df_momentum = query_df(
803
  f"""
804
  SELECT
 
809
  JOIN repositories r ON rt.html_url = r.html_url
810
  WHERE r.created_at IS NOT NULL AND {tf_sql_mom} {and_base}
811
  GROUP BY rt.tag
812
+ HAVING recent >= {min_repos} AND prior >= {min_repos}
813
  ORDER BY (CAST(recent AS FLOAT) / prior) DESC
814
  LIMIT 30
815
  """,
816
+ [cutoff_recent, cutoff_prior, cutoff_recent] + tf_params_mom + params_base,
817
  )
818
 
819
  if not df_momentum.empty:
 
823
  col_m1, col_m2 = st.columns(2)
824
 
825
  with col_m1:
826
+ st.markdown(f"**Fastest growing tags**")
827
  fig = px.bar(
828
  df_momentum.head(20), x="growth_ratio", y="tag", orientation="h",
829
  color="growth_ratio", color_continuous_scale="Greens",
 
838
  st.plotly_chart(fig, use_container_width=True)
839
 
840
  with col_m2:
841
+ st.markdown(f"**Top tags by volume**")
842
  df_recent_top = query_df(
843
  f"""
844
  SELECT rt.tag, COUNT(DISTINCT r.html_url) as recent_count
 
846
  WHERE r.created_at >= ? AND {tf_sql_mom} {and_base}
847
  GROUP BY rt.tag ORDER BY recent_count DESC LIMIT 20
848
  """,
849
+ [cutoff_recent] + tf_params_mom + params_base,
850
  )
851
  if not df_recent_top.empty:
852
  fig2 = px.bar(
853
  df_recent_top, x="recent_count", y="tag", orientation="h",
854
  color="recent_count", color_continuous_scale="Purples",
855
+ labels={"recent_count": f"Repos ({period_label.lower()})", "tag": "Tag"},
856
  )
857
  fig2.update_layout(
858
  yaxis=dict(autorange="reversed"), height=550,
859
+ coloraxis_showscale=False,
860
+ xaxis_title=f"Repos ({period_label.lower()})", yaxis_title="",
861
  )
862
  st.plotly_chart(fig2, use_container_width=True)
863
 
864
  # Emerging tags table
865
+ st.markdown("**Emerging tags** β€” ratio > 1.5")
866
  df_emerging = df_momentum[df_momentum["growth_ratio"] >= 1.5][
867
  ["tag", "recent", "prior", "growth_ratio", "growth_pct"]
868
  ].rename(columns={
869
+ "tag": "Tag", "recent": period_label, "prior": f"Prior {period_label.lower()}",
870
  "growth_ratio": "Ratio", "growth_pct": "Growth %"
871
  })
872
  if not df_emerging.empty:
 
874
  else:
875
  st.info("No tags with >50% growth in this period.")
876
  else:
877
+ st.info("Not enough tagged data for this period β€” try a longer window.")
878
 
879
  st.divider()
880
 
 
902
  df_lang_year = df_lang_year[df_lang_year["year"].str.match(r"^\d{4}$", na=False)]
903
 
904
  if not df_lang_year.empty:
 
905
  fig_lang = px.line(
906
  df_lang_year, x="year", y="count", color="language",
907
  labels={"year": "Year", "count": "New repositories", "language": "Language"},
 
910
  fig_lang.update_layout(legend=dict(orientation="h", y=-0.25))
911
  st.plotly_chart(fig_lang, use_container_width=True)
912
 
 
913
  st.caption("As a share of all new repos that year (top 10 languages).")
914
  df_totals = df_lang_year.groupby("year")["count"].sum().reset_index().rename(columns={"count": "total"})
915
  df_share = df_lang_year.merge(df_totals, on="year")