|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | dashboard-metrics-tiles: Real-Time Dashboard Tiles |
3 | | -Library: letsplot 4.8.2 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2026-01-19 |
| 3 | +Library: letsplot 4.10.1 | Python 3.13.13 |
| 4 | +Quality: 88/100 | Updated: 2026-05-21 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import numpy as np |
8 | 10 | import pandas as pd |
9 | 11 | from lets_plot import ( |
|
20 | 22 | ggplot, |
21 | 23 | ggsize, |
22 | 24 | labs, |
| 25 | + layer_tooltips, |
23 | 26 | scale_color_manual, |
24 | 27 | scale_fill_manual, |
25 | 28 | scale_x_continuous, |
|
31 | 34 |
|
32 | 35 | LetsPlot.setup_html() |
33 | 36 |
|
| 37 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 38 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 39 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 40 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 41 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 42 | + |
| 43 | +# Status colors: good uses brand green (Okabe-Ito #1), warning amber, critical red |
| 44 | +STATUS_COLORS = {"good": "#009E73", "warning": "#F59E0B", "critical": "#EF4444", "bad": "#EF4444"} |
| 45 | + |
34 | 46 | np.random.seed(42) |
35 | 47 |
|
36 | | -# Define metrics data |
| 48 | +# Data |
37 | 49 | metrics = [ |
38 | 50 | {"name": "CPU Usage", "value": 45, "unit": "%", "change": -5.2, "status": "good"}, |
39 | 51 | {"name": "Memory", "value": 72, "unit": "%", "change": 8.3, "status": "warning"}, |
|
43 | 55 | {"name": "Throughput", "value": 847, "unit": "req/s", "change": 3.7, "status": "good"}, |
44 | 56 | ] |
45 | 57 |
|
46 | | -# Generate sparkline history for each metric (20 points) |
47 | 58 | all_data = [] |
48 | 59 | for m in metrics: |
49 | 60 | base = m["value"] |
50 | | - # Generate trend data ending at current value |
51 | 61 | trend = np.cumsum(np.random.randn(20) * (base * 0.08)) + base * 0.85 |
52 | | - # Normalize to end near current value |
53 | 62 | trend = trend - trend[-1] + base |
54 | 63 |
|
55 | | - # Format value display |
56 | | - if m["value"] >= 1000: |
57 | | - value_str = f"{m['value']:,}" |
58 | | - else: |
59 | | - value_str = str(m["value"]) |
| 64 | + value_str = f"{m['value']:,}" if m["value"] >= 1000 else str(m["value"]) |
60 | 65 | value_display = f"{value_str}{m['unit']}" |
61 | 66 |
|
62 | | - # Determine change indicator |
63 | 67 | change = m["change"] |
64 | 68 | if change >= 0: |
65 | 69 | arrow = "▲" |
66 | | - # Up is bad for CPU, Memory, Error Rate, Response Time |
67 | 70 | change_color = "bad" if m["name"] in ["CPU Usage", "Memory", "Error Rate", "Response Time"] else "good" |
68 | 71 | else: |
69 | 72 | arrow = "▼" |
70 | | - # Down is good for CPU, Memory, Error Rate, Response Time |
71 | 73 | change_color = "good" if m["name"] in ["CPU Usage", "Memory", "Error Rate", "Response Time"] else "bad" |
72 | 74 |
|
73 | 75 | change_str = f"{arrow} {abs(change):.1f}%" |
74 | 76 |
|
| 77 | + # Prepend warning badge to facet strip label for prominent visual distinction |
| 78 | + facet_label = f"⚠ {m['name']}" if m["status"] == "warning" else m["name"] |
| 79 | + |
75 | 80 | for i, val in enumerate(trend): |
76 | 81 | all_data.append( |
77 | 82 | { |
78 | | - "metric": m["name"], |
| 83 | + "metric": facet_label, |
79 | 84 | "x": i, |
80 | 85 | "y": val, |
81 | 86 | "status": m["status"], |
|
87 | 92 | ) |
88 | 93 |
|
89 | 94 | df = pd.DataFrame(all_data) |
90 | | - |
91 | | -# Status colors |
92 | | -status_colors = {"good": "#22C55E", "warning": "#F59E0B", "critical": "#EF4444"} |
93 | | - |
94 | | -# Map status to color in dataframe |
95 | | -df["fill_color"] = df["status"].map(status_colors) |
96 | | -df["line_color"] = df["status"].map(status_colors) |
97 | | - |
98 | | -# Create the last point markers |
99 | 95 | last_points = df[df["is_last"]].copy() |
100 | | - |
101 | | -# Create label data at midpoint |
102 | 96 | label_data = df[df["value_label"] != ""].copy() |
103 | 97 |
|
104 | | -# Get y ranges for positioning labels |
105 | 98 | y_stats = df.groupby("metric").agg({"y": ["min", "max"]}).reset_index() |
106 | 99 | y_stats.columns = ["metric", "y_min", "y_max"] |
107 | 100 |
|
108 | 101 | label_data = label_data.merge(y_stats, on="metric") |
109 | | -label_data["y_value"] = label_data["y_max"] + (label_data["y_max"] - label_data["y_min"]) * 0.45 |
110 | | -label_data["y_change"] = label_data["y_min"] - (label_data["y_max"] - label_data["y_min"]) * 0.25 |
| 102 | +label_data["y_value"] = label_data["y_max"] + (label_data["y_max"] - label_data["y_min"]) * 0.55 |
| 103 | +label_data["y_change"] = label_data["y_min"] - (label_data["y_max"] - label_data["y_min"]) * 0.40 |
111 | 104 |
|
112 | | -# Build the plot with facets |
| 105 | +# Plot |
113 | 106 | plot = ( |
114 | 107 | ggplot(df, aes("x", "y")) |
115 | | - + geom_area(aes(fill="status"), alpha=0.25, show_legend=False) |
116 | | - + geom_line(aes(color="status"), size=2, show_legend=False) |
117 | | - + geom_point(data=last_points, mapping=aes(color="status"), size=5, show_legend=False) |
118 | | - # Value labels |
| 108 | + + geom_area(aes(fill="status"), alpha=0.25, show_legend=False, tooltips="none") |
| 109 | + + geom_line( |
| 110 | + aes(color="status"), size=2, show_legend=False, tooltips=layer_tooltips().line("@metric").line("value|@y{.1f}") |
| 111 | + ) |
| 112 | + + geom_point( |
| 113 | + data=last_points, |
| 114 | + mapping=aes(color="status"), |
| 115 | + size=5, |
| 116 | + show_legend=False, |
| 117 | + tooltips=layer_tooltips().line("@metric").line("current|@y{.1f}"), |
| 118 | + ) |
119 | 119 | + geom_text( |
120 | | - data=label_data, mapping=aes(x="x", y="y_value", label="value_label"), size=22, fontface="bold", color="#1F2937" |
| 120 | + data=label_data, mapping=aes(x="x", y="y_value", label="value_label"), size=22, fontface="bold", color=INK |
121 | 121 | ) |
122 | | - # Change indicator labels |
123 | 122 | + geom_text( |
124 | 123 | data=label_data, |
125 | 124 | mapping=aes(x="x", y="y_change", label="change_label", color="change_color"), |
126 | 125 | size=14, |
127 | 126 | show_legend=False, |
128 | 127 | ) |
129 | | - + scale_fill_manual(values={"good": "#22C55E", "warning": "#F59E0B", "critical": "#EF4444"}) |
130 | | - + scale_color_manual(values={"good": "#22C55E", "warning": "#F59E0B", "critical": "#EF4444", "bad": "#EF4444"}) |
| 128 | + + scale_fill_manual(values=STATUS_COLORS) |
| 129 | + + scale_color_manual(values=STATUS_COLORS) |
131 | 130 | + scale_x_continuous(expand=[0.05, 0.05]) |
132 | | - + scale_y_continuous(expand=[0.4, 0.4]) |
| 131 | + + scale_y_continuous(expand=[0.55, 0.55]) |
133 | 132 | + facet_wrap("metric", ncol=3, scales="free_y") |
134 | | - + labs(title="dashboard-metrics-tiles · letsplot · pyplots.ai") |
| 133 | + + labs(title="dashboard-metrics-tiles · python · letsplot · anyplot.ai") |
135 | 134 | + theme( |
136 | | - plot_title=element_text(size=28, face="bold", color="#1F2937", hjust=0.5), |
137 | | - strip_text=element_text(size=18, face="bold", color="#4B5563"), |
138 | | - strip_background=element_rect(fill="#F3F4F6", color="#E5E7EB"), |
| 135 | + plot_title=element_text(size=16, face="bold", color=INK, hjust=0.5), |
| 136 | + strip_text=element_text(size=14, face="bold", color=INK_SOFT), |
| 137 | + strip_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), |
139 | 138 | axis_text=element_blank(), |
140 | 139 | axis_ticks=element_blank(), |
141 | 140 | axis_title=element_blank(), |
142 | 141 | axis_line=element_blank(), |
143 | 142 | panel_grid=element_blank(), |
144 | | - panel_background=element_rect(fill="#FFFFFF", color="#E5E7EB", size=1), |
145 | | - plot_background=element_rect(fill="#F9FAFB"), |
| 143 | + panel_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=1), |
| 144 | + plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), |
| 145 | + legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), |
146 | 146 | panel_spacing_x=30, |
147 | 147 | panel_spacing_y=30, |
148 | 148 | ) |
149 | | - + ggsize(1600, 900) |
| 149 | + + ggsize(800, 450) |
150 | 150 | ) |
151 | 151 |
|
152 | | -# Save PNG (scale=3 gives 4800x2700) |
153 | | -ggsave(plot, "plot.png", path=".", scale=3) |
154 | | - |
155 | | -# Save HTML for interactivity |
156 | | -ggsave(plot, "plot.html", path=".") |
| 152 | +# Save |
| 153 | +ggsave(plot, f"plot-{THEME}.png", path=".", scale=4) |
| 154 | +ggsave(plot, f"plot-{THEME}.html", path=".") |
0 commit comments