From ba8fca6f8714a05ddd97b3bf39fb0de105dcecd8 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 15 May 2026 23:21:56 +0200 Subject: [PATCH] Nicer stats page --- RNS/Utilities/rngit/pages.py | 184 ++++++++++++++++++++++------------- 1 file changed, 119 insertions(+), 65 deletions(-) diff --git a/RNS/Utilities/rngit/pages.py b/RNS/Utilities/rngit/pages.py index 907da3e1..84c61859 100644 --- a/RNS/Utilities/rngit/pages.py +++ b/RNS/Utilities/rngit/pages.py @@ -103,6 +103,15 @@ class NomadNetworkNode(): CLR_DIFF_R = "`F900" CLR_DIFF_P = "`F0aa" + RCLR_PUSH = "B9A810" + RCLR_PUSH_G = "791212" + RCLR_FETCH = "10b981" + RCLR_FETCH_G = "1c5e71" + RCLR_VIEW = "3b82f6" + RCLR_VIEW_G = "13428A" + RCLR_DOWNLOAD = "7831E0" + RCLR_DOWNLOAD_G = "c5754d" + # Yes, I'm being intentionally weird here. If you # want to use tabs, three spaces is all you get. TAB_WIDTH = " " @@ -1111,48 +1120,56 @@ class NomadNetworkNode(): content_parts.append(self.m_heading(f"Stats for {repo_name}", 2)) v_total = stats["views"]["total"] - v_peak = stats["views"]["peak"] + v_peak = stats["views"]["peak"] + v_tday = stats["views"]["daily"][-1] if len(stats["views"]["daily"]) else 0 + f_total = stats["fetches"]["total"] - f_peak = stats["fetches"]["peak"] - p_total = stats["pushes"]["total"] - p_peak = stats["pushes"]["peak"] - d_total = stats["downloads_combined"]["total"] - d_peak = stats["downloads_combined"]["peak"] + f_peak = stats["fetches"]["peak"] + f_tday = stats["fetches"]["daily"][-1] if len(stats["fetches"]["daily"]) else 0 - content_parts.append(f"\n`F66dViews`f : {v_total:>5} total {self.CLR_DIM}(peak: {v_peak:>3})`f\n") - content_parts.append(f"`F0a0Fetches`f : {f_total:>5} total {self.CLR_DIM}(peak: {f_peak:>3})\n`f") - content_parts.append(f"`Faa0Pushes`f : {p_total:>5} total {self.CLR_DIM}(peak: {p_peak:>3})\n`f") - content_parts.append(f"`F0aaActivity`f : {stats['activity_score']:>5} points\n\n") + p_total = stats["pushes"]["total"] + p_peak = stats["pushes"]["peak"] + p_tday = stats["pushes"]["daily"][-1] if len(stats["pushes"]["daily"]) else 0 + + d_total = stats["downloads_combined"]["total"] + d_peak = stats["downloads_combined"]["peak"] + d_tday = stats["downloads_combined"]["daily"][-1] if len(stats["downloads_combined"]["daily"]) else 0 + + content_parts.append( f"\n`FT{self.RCLR_FETCH}Fetches`f : {f_total:>5} total {self.CLR_DIM} today: {f_tday:>3} peak: {f_peak:>3} \n`f") + content_parts.append( f"`FT{self.RCLR_PUSH}Pushes`f : {p_total:>5} total {self.CLR_DIM} today: {p_tday:>3} peak: {p_peak:>3} \n`f") + content_parts.append( f"`FT{self.RCLR_VIEW}Views`f : {v_total:>5} total {self.CLR_DIM} today: {v_tday:>3} peak: {v_peak:>3} `f\n") + content_parts.append(f"`FT{self.RCLR_DOWNLOAD}Downloads`f : {d_total:>5} total {self.CLR_DIM} today: {d_tday:>3} peak: {d_peak:>3} `f\n") + content_parts.append( f"`F0aaActivity`f : {stats['activity_score']:>5} points\n\n") content_parts.append(f"{act_color}{act_label}`f over the last {stats['actual_days']} days ({stats['date_range']})\n\n") - - if v_total > 0: - content_parts.append(self.m_heading(f"Views", 2)) - content_parts.append("\n") - content_parts.append(self.render_chart(stats["views"]["daily"], stats["timeline_labels"], color="66d")) - content_parts.append("\n") - + if f_total > 0: content_parts.append(self.m_heading(f"Fetches", 2)) content_parts.append("\n") - content_parts.append(self.render_chart(stats["fetches"]["daily"], stats["timeline_labels"], color="0a0")) + content_parts.append(self.render_chart(stats["fetches"]["daily"], stats["timeline_labels"], color=self.RCLR_FETCH, secondary_color=self.RCLR_FETCH_G)) content_parts.append("\n") if p_total > 0: content_parts.append(self.m_heading(f"Pushes", 2)) content_parts.append("\n") - content_parts.append(self.render_chart(stats["pushes"]["daily"], stats["timeline_labels"], color="aa0")) + content_parts.append(self.render_chart(stats["pushes"]["daily"], stats["timeline_labels"], color=self.RCLR_PUSH, secondary_color=self.RCLR_PUSH_G)) + content_parts.append("\n") + + if v_total > 0: + content_parts.append(self.m_heading(f"Views", 2)) + content_parts.append("\n") + content_parts.append(self.render_chart(stats["views"]["daily"], stats["timeline_labels"], color=self.RCLR_VIEW, secondary_color=self.RCLR_VIEW_G)) content_parts.append("\n") if d_total > 0: content_parts.append(self.m_heading(f"Downloads", 2)) content_parts.append("\n") - content_parts.append(self.render_chart(stats["downloads_combined"]["daily"], stats["timeline_labels"], color="a22")) + content_parts.append(self.render_chart(stats["downloads_combined"]["daily"], stats["timeline_labels"], color=self.RCLR_DOWNLOAD, secondary_color=self.RCLR_DOWNLOAD_G, gradient_factor=1.7)) content_parts.append("\n") if stats["activity_score"] > 0: content_parts.append(self.m_heading("Combined Activity", 2)) content_parts.append("\n") - content_parts.append(self.render_combined_chart(stats["views"]["daily"], stats["fetches"]["daily"], stats["pushes"]["daily"], stats["timeline_labels"])) + content_parts.append(self.render_combined_chart(stats["views"]["daily"], stats["fetches"]["daily"], stats["pushes"]["daily"], stats["downloads_combined"]["daily"], stats["timeline_labels"])) else: content_parts.append(self.m_italic("\nNo development activity recorded for this repository in the selected time period.\n\n")) @@ -2302,8 +2319,8 @@ class NomadNetworkNode(): # Stats Renderers # ################### - def render_chart(self, data, labels, color="666", height=10): - return self.render_chart_halfblock(data, labels, color=color, height=height) + def render_chart(self, data, labels, color="666", height=10, secondary_color=None, gradient_factor=None): + return self.render_chart_halfblock(data, labels, color=color, height=height, secondary_color=secondary_color, gradient_factor=gradient_factor) def render_chart_full_block(self, data, labels, color="666", height=10): if not data or all(d == 0 for d in data): return "No data available\n" @@ -2345,7 +2362,8 @@ class NomadNetworkNode(): return "".join(lines) - def render_chart_halfblock(self, data, labels, color="666", height=10, secondary_color=None, gradient_factor=1.3): + def render_chart_halfblock(self, data, labels, color="666", height=10, secondary_color=None, gradient_factor=None): + if not gradient_factor: gradient_factor = 1.3 if not data or all(d == 0 for d in data): return "No data available\n" max_val = max(data) if max(data) > 0 else 1 num_points = len(data) @@ -2401,57 +2419,93 @@ class NomadNetworkNode(): return "".join(lines) - def render_combined_chart(self, views, fetches, pushes, labels, height=4): - if not views or not labels: return "No data available\n" + def render_combined_chart(self, views, fetches, pushes, downloads, labels, height=6, colors=None, dim=0.87): + if not views or not all([views, fetches, pushes, downloads]): return "No data available\n" + + if colors is None: + colors = { 'views': self.RCLR_VIEW, + 'fetches': self.RCLR_FETCH, + 'pushes': self.RCLR_PUSH, + 'downloads': self.RCLR_DOWNLOAD } + + def expand(c): return ''.join(ch*2 for ch in c) if len(c) == 3 else c[:6] + def hex_to_rgb(h): return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) + def gradient_color(c, g, t, f=1.0): c = hex_to_rgb(c); g = hex_to_rgb(g); return ''.join(f"{int(g[i] + (c[i] - g[i]) * min(1, t*f)):02x}" for i in range(3)) + cat_colors = {'views': gradient_color(expand(colors.get('views', self.RCLR_VIEW)), "000000", dim), + 'fetches': gradient_color(expand(colors.get('fetches', self.RCLR_FETCH)), "000000", dim), + 'pushes': gradient_color(expand(colors.get('pushes', self.RCLR_PUSH)), "000000", dim), + 'downloads': gradient_color(expand(colors.get('downloads', self.RCLR_DOWNLOAD)), "000000", dim) } + + # Stack order + categories = ['pushes', 'fetches', 'views', 'downloads'] + cat_data = [pushes, fetches, views, downloads] - all_data = [v + f + p for v, f, p in zip(views, fetches, pushes)] - max_val = max(all_data) if all(all_data) > 0 else 1 num_points = len(views) + legend = " ".join(f"`FT{cat_colors[cat]}`BT{cat_colors[cat]}██`f`b {cat.capitalize()}" for cat in categories) + lines = [f"{legend}\n\n"] - hsep = "" - indent = "" - bar_width = 1 - - lines = [] - lines.append(f"{indent}`F66d██`f Views `F0a0██`f Fetches `Faa0██`f Pushes\n\n") for row in range(height, 0, -1): - threshold = (row - 1) / height * max_val - row_line = f"{indent}│" + lower_min = (row - 1) / height + lower_max = (row - 0.5) / height + upper_min = (row - 0.5) / height + upper_max = row / height + + line = "│" + for i in range(num_points): - v, f, p = views[i], fetches[i], pushes[i] - total = v + f + p + total = sum(d[i] for d in cat_data) + if total == 0: + line += " " + continue - if total > threshold: - # Determine which "layer" this row represents - # Priority: Pushes > fetches > views for display - if p > 0 and threshold < (v + f + p) and threshold >= (v + f): row_line += f"`Faa0{'█'*bar_width}`f{hsep}" - elif f > 0 and threshold < (v + f) and threshold >= v: row_line += f"`F0a0{'▓'*bar_width}`f{hsep}" - elif v > 0 and threshold < v: row_line += f"`F66d{'░'*bar_width}`f{hsep}" - else: - # Mixed or partial, show dominant - if p >= f and p >= v: row_line += f"`Faa0{'▒'*bar_width}`f{hsep}" - elif f >= v: row_line += f"`F0a0{'▒'*bar_width}`f{hsep}" - else: row_line += f"`F66d{'▒'*bar_width}`f{hsep}" - else: row_line += f"{' '*bar_width}{hsep}" - row_line += "\n" - lines.append(row_line) + cumsum = 0 + cat_ranges = {} + for cat, data in zip(categories, cat_data): + start = cumsum / total + cumsum += data[i] + end = cumsum / total + cat_ranges[cat] = (start, end) + + def pixel_to_cat(pmin, pmax): + for cat in categories: + cstart, cend = cat_ranges[cat] + # Check if pixel overlaps this category + # Return the category (they're mutually exclusive in stacked chart) + if pmin < cend and pmax > cstart: return cat + return None + + upper_cat = pixel_to_cat(upper_min, upper_max) + lower_cat = pixel_to_cat(lower_min, lower_max) + + if upper_cat is None and lower_cat is None: line += " " + elif upper_cat == lower_cat and upper_cat is not None: + col = cat_colors[upper_cat] + line += f"`FT{col}`BT{col}█`f`b" + elif upper_cat is not None and lower_cat is not None: + upper_col = cat_colors[upper_cat] + lower_col = cat_colors[lower_cat] + line += f"`FT{upper_col}`BT{lower_col}▀`f`b" + elif upper_cat is not None: + col = cat_colors[upper_cat] + line += f"`FT{col}▀`f" + else: + col = cat_colors[lower_cat] + line += f"`FT{col}▄`f" + + lines.append(line + "\n") - hsj = "┴"*len(hsep) - bottom_border = "└" + hsj.join(["─" * bar_width] * num_points) + "┘" - lines.append(indent + bottom_border + "\n") - - chart_width = len(bottom_border) - first_label = f"{labels[0][:12]:<12}" - final_label = f"{labels[-1][:12]:>12}" - middle_space = chart_width-len(first_label)-len(final_label) - - label_line = f"{indent}{self.CLR_DIM}{first_label}`f" - label_line += " " * middle_space - label_line += f"{self.CLR_DIM}{final_label}`f\n" - lines.append(label_line) + bottom = "└" + "─" * num_points + "┘" + lines.append(bottom + "\n") + + if labels: + first = str(labels[0])[:12] + last = str(labels[-1])[:12] + mid_space = len(bottom) - len(first) - len(last) + lines.append(f"{self.CLR_DIM}{first}{" " * mid_space}{last}`f\n") return "".join(lines) + ####################### # Connection Handlers # #######################