Skip to content

Commit bf3e420

Browse files
github-actions[bot]claudeMarkusNeusinger
authored
feat(letsplot): implement dashboard-metrics-tiles (#7596)
## Implementation: `dashboard-metrics-tiles` - python/letsplot Implements the **python/letsplot** version of `dashboard-metrics-tiles`. **File:** `plots/dashboard-metrics-tiles/implementations/python/letsplot.py` **Parent Issue:** #3791 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26203546386)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 2165cd1 commit bf3e420

2 files changed

Lines changed: 210 additions & 169 deletions

File tree

Lines changed: 50 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
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
55
"""
66

7+
import os
8+
79
import numpy as np
810
import pandas as pd
911
from lets_plot import (
@@ -20,6 +22,7 @@
2022
ggplot,
2123
ggsize,
2224
labs,
25+
layer_tooltips,
2326
scale_color_manual,
2427
scale_fill_manual,
2528
scale_x_continuous,
@@ -31,9 +34,18 @@
3134

3235
LetsPlot.setup_html()
3336

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+
3446
np.random.seed(42)
3547

36-
# Define metrics data
48+
# Data
3749
metrics = [
3850
{"name": "CPU Usage", "value": 45, "unit": "%", "change": -5.2, "status": "good"},
3951
{"name": "Memory", "value": 72, "unit": "%", "change": 8.3, "status": "warning"},
@@ -43,39 +55,32 @@
4355
{"name": "Throughput", "value": 847, "unit": "req/s", "change": 3.7, "status": "good"},
4456
]
4557

46-
# Generate sparkline history for each metric (20 points)
4758
all_data = []
4859
for m in metrics:
4960
base = m["value"]
50-
# Generate trend data ending at current value
5161
trend = np.cumsum(np.random.randn(20) * (base * 0.08)) + base * 0.85
52-
# Normalize to end near current value
5362
trend = trend - trend[-1] + base
5463

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"])
6065
value_display = f"{value_str}{m['unit']}"
6166

62-
# Determine change indicator
6367
change = m["change"]
6468
if change >= 0:
6569
arrow = "▲"
66-
# Up is bad for CPU, Memory, Error Rate, Response Time
6770
change_color = "bad" if m["name"] in ["CPU Usage", "Memory", "Error Rate", "Response Time"] else "good"
6871
else:
6972
arrow = "▼"
70-
# Down is good for CPU, Memory, Error Rate, Response Time
7173
change_color = "good" if m["name"] in ["CPU Usage", "Memory", "Error Rate", "Response Time"] else "bad"
7274

7375
change_str = f"{arrow} {abs(change):.1f}%"
7476

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+
7580
for i, val in enumerate(trend):
7681
all_data.append(
7782
{
78-
"metric": m["name"],
83+
"metric": facet_label,
7984
"x": i,
8085
"y": val,
8186
"status": m["status"],
@@ -87,70 +92,63 @@
8792
)
8893

8994
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
9995
last_points = df[df["is_last"]].copy()
100-
101-
# Create label data at midpoint
10296
label_data = df[df["value_label"] != ""].copy()
10397

104-
# Get y ranges for positioning labels
10598
y_stats = df.groupby("metric").agg({"y": ["min", "max"]}).reset_index()
10699
y_stats.columns = ["metric", "y_min", "y_max"]
107100

108101
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
111104

112-
# Build the plot with facets
105+
# Plot
113106
plot = (
114107
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+
)
119119
+ 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
121121
)
122-
# Change indicator labels
123122
+ geom_text(
124123
data=label_data,
125124
mapping=aes(x="x", y="y_change", label="change_label", color="change_color"),
126125
size=14,
127126
show_legend=False,
128127
)
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)
131130
+ 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])
133132
+ 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")
135134
+ 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),
139138
axis_text=element_blank(),
140139
axis_ticks=element_blank(),
141140
axis_title=element_blank(),
142141
axis_line=element_blank(),
143142
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),
146146
panel_spacing_x=30,
147147
panel_spacing_y=30,
148148
)
149-
+ ggsize(1600, 900)
149+
+ ggsize(800, 450)
150150
)
151151

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

Comments
 (0)