1717 "rnaseq" : "Bulk-Transcriptomics" ,
1818 "rna seq" : "Bulk-Transcriptomics" ,
1919 "rna seq." : "Bulk-Transcriptomics" ,
20-
2120 # Single-cell gene expression
2221 "sc gex" : "scRNA-Seq" ,
2322 "single cell rna-seq" : "scRNA-Seq" ,
2423 "scrna-seq" : "scRNA-Seq" ,
2524 "sc rna-seq" : "scRNA-Seq" ,
26-
2725 # ATAC-seq
2826 "bulk atacseq" : "Bulk-Epigenetics" ,
2927 "bulk atac-seq" : "Bulk-Epigenetics" ,
3028 "sc atac" : "scATAC-Seq" ,
3129 "sc atac-seq" : "scATAC-Seq" ,
32-
3330 # Multiome and others
3431 "10x multiome" : "10x Multiome" ,
3532 "resolveome" : "ResolveOME" ,
36-
3733 # Spatial platforms
3834 "spatial" : "Spatial" ,
3935 "visium" : "10x Visium" ,
4036 "10x visium" : "10x Visium" ,
4137 "xenium" : "10x Xenium" ,
4238 "xeniums" : "10x Xenium" ,
4339 "10x xenium" : "10x Xenium" ,
44-
4540 # Misc
4641 "mgx" : "Metagenomics" ,
4742 "workflow dev" : "Workflow development" ,
48- "workflow development" : "Workflow development" ,
49- "workflows" : "Workflow development" ,
50- "development" : "Workflow development" ,
43+ "workflow development" : "Workflow development" ,
44+ "workflows" : "Workflow development" ,
45+ "development" : "Workflow development" ,
5146 "other" : "Other" ,
5247}
5348
49+ # Color palette for modalities (GitHub-style colors)
50+ COLORS : Dict [str , str ] = {
51+ "Bulk-Transcriptomics" : "#e34c26" ,
52+ "scRNA-Seq" : "#3572A5" ,
53+ "Bulk-Epigenetics" : "#178600" ,
54+ "scATAC-Seq" : "#89e051" ,
55+ "10x Multiome" : "#f1e05a" ,
56+ "ResolveOME" : "#b07219" ,
57+ "Spatial" : "#555555" ,
58+ "10x Visium" : "#4F5D95" ,
59+ "10x Xenium" : "#DA5B0B" ,
60+ "Metagenomics" : "#701516" ,
61+ "Workflow development" : "#384d54" ,
62+ "Other" : "#cccccc" ,
63+ }
64+
65+
5466def normalize (modality : str ) -> str :
5567 m = modality .strip ()
5668 if not m :
5769 return ""
5870 key = m .lower ()
5971 return ALIASES .get (key , m )
6072
73+
6174def extract_modalities_from_markdown (md_text : str ) -> List [str ]:
6275 modalities : List [str ] = []
6376 in_counts_section = False
64-
6577 for line in md_text .splitlines ():
6678 # Skip the generated counts section entirely
6779 if START in line :
@@ -72,23 +84,19 @@ def extract_modalities_from_markdown(md_text: str) -> List[str]:
7284 continue
7385 if in_counts_section :
7486 continue
75-
7687 if not line .startswith ("|" ):
7788 continue
7889 if re .match (r"^\|\s*Project\s*\|" , line ):
7990 continue
8091 if re .match (r"^\|\s*-+\s*\|" , line ):
8192 continue
82-
8393 cells = [c .strip () for c in line .split ("|" )]
8494 # Expect at least 4 cells: leading empty, Project, Modality, Repo/Count, trailing empty
8595 if len (cells ) < 4 :
8696 continue
87-
8897 modality_cell = cells [2 ] # Modality is the second visible column
8998 if not modality_cell :
9099 continue
91-
92100 parts = re .split (r"\s*,\s*" , modality_cell )
93101 for p in parts :
94102 n = normalize (p )
@@ -97,7 +105,84 @@ def extract_modalities_from_markdown(md_text: str) -> List[str]:
97105 return modalities
98106
99107
108+ def get_color (modality : str ) -> str :
109+ """Get color for a modality, or generate a default one."""
110+ return COLORS .get (modality , "#cccccc" )
111+
112+
113+ def build_badge_svg (counts : Counter ) -> str :
114+ """Build an SVG badge similar to GitHub's language bar."""
115+ if not counts :
116+ return ""
117+
118+ total = sum (counts .values ())
119+ rows : List [Tuple [str , int ]] = sorted (
120+ counts .items (), key = lambda x : (- x [1 ], x [0 ].lower ())
121+ )
122+
123+ # SVG dimensions
124+ width = 600
125+ bar_height = 8
126+ legend_item_height = 25
127+ legend_height = len (rows ) * legend_item_height
128+ total_height = bar_height + 20 + legend_height
129+
130+ # Build bar segments
131+ x_pos = 0
132+ bar_rects = []
133+ for modality , count in rows :
134+ percentage = (count / total ) * 100
135+ segment_width = (count / total ) * width
136+ color = get_color (modality )
137+ bar_rects .append (
138+ f'<rect x="{ x_pos :.2f} " y="0" width="{ segment_width :.2f} " height="{ bar_height } " '
139+ f'fill="{ color } "><title>{ modality } : { count } ({ percentage :.1f} %)</title></rect>'
140+ )
141+ x_pos += segment_width
142+
143+ # Build legend
144+ legend_items = []
145+ y_pos = bar_height + 20
146+ for modality , count in rows :
147+ percentage = (count / total ) * 100
148+ color = get_color (modality )
149+ legend_items .append (
150+ f'<circle cx="6" cy="{ y_pos } " r="5" fill="{ color } "/>'
151+ )
152+ legend_items .append (
153+ f'<text x="18" y="{ y_pos + 4 } " class="legend-text">{ modality } </text>'
154+ )
155+ legend_items .append (
156+ f'<text x="{ width - 10 } " y="{ y_pos + 4 } " class="legend-percent" text-anchor="end">'
157+ f'{ percentage :.1f} % ({ count } )</text>'
158+ )
159+ y_pos += legend_item_height
160+
161+ svg = f'''<svg width="{ width } " height="{ total_height } " xmlns="http://www.w3.org/2000/svg">
162+ <style>
163+ .legend-text {{
164+ font: 12px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
165+ fill: #24292f;
166+ }}
167+ .legend-percent {{
168+ font: 12px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
169+ fill: #656d76;
170+ font-weight: 600;
171+ }}
172+ </style>
173+ <g id="bar">
174+ { chr (10 ).join (f" { rect } " for rect in bar_rects )}
175+ </g>
176+ <g id="legend">
177+ { chr (10 ).join (f" { item } " for item in legend_items )}
178+ </g>
179+ </svg>'''
180+
181+ return f'<p align="center">\n { svg } \n </p>'
182+
183+
100184def build_table (counts : Counter ) -> str :
185+ """Build a simple markdown table (fallback)."""
101186 rows : List [Tuple [str , int ]] = sorted (
102187 counts .items (), key = lambda x : (- x [1 ], x [0 ].lower ())
103188 )
@@ -109,12 +194,18 @@ def build_table(counts: Counter) -> str:
109194 lines .append (f"| { modality } | { cnt } |" )
110195 return "\n " .join (lines )
111196
112- def upsert_section (md_text : str , table_md : str ) -> str :
197+
198+ def upsert_section (md_text : str , badge_svg : str , table_md : str ) -> str :
199+ """Insert both badge and table into README."""
113200 section = (
114201 f"{ START } \n "
115202 f"\n "
116- f"### Modality counts\n \n "
117- f"{ table_md } \n "
203+ f"### Modality Distribution\n \n "
204+ f"{ badge_svg } \n \n "
205+ f"<details>\n "
206+ f"<summary>View as table</summary>\n \n "
207+ f"{ table_md } \n \n "
208+ f"</details>\n "
118209 f"\n "
119210 f"{ END } "
120211 )
@@ -128,21 +219,29 @@ def upsert_section(md_text: str, table_md: str) -> str:
128219 sep = "\n \n " if not md_text .endswith ("\n " ) else "\n "
129220 return md_text + sep + section + "\n "
130221
222+
131223def main () -> int :
132224 if not README .exists ():
133225 print ("README.md not found at repo root." )
134226 return 1
227+
135228 md_text = README .read_text (encoding = "utf-8" )
136229 modalities = extract_modalities_from_markdown (md_text )
137230 counts = Counter (modalities )
231+
232+ badge_svg = build_badge_svg (counts )
138233 table_md = build_table (counts )
139- new_md = upsert_section (md_text , table_md )
234+
235+ new_md = upsert_section (md_text , badge_svg , table_md )
236+
140237 if new_md != md_text :
141238 README .write_text (new_md , encoding = "utf-8" )
142239 print ("README.md updated with modality counts." )
143240 else :
144241 print ("No changes to README.md." )
242+
145243 return 0
146244
245+
147246if __name__ == "__main__" :
148247 raise SystemExit (main ())
0 commit comments