Skip to content

Commit ab51db1

Browse files
feat(bokeh): implement subplot-mosaic (#3038)
## Implementation: `subplot-mosaic` - bokeh Implements the **bokeh** version of `subplot-mosaic`. **File:** `plots/subplot-mosaic/implementations/bokeh.py` **Parent Issue:** #3002 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20617510121)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent e4d5391 commit ab51db1

2 files changed

Lines changed: 265 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
""" pyplots.ai
2+
subplot-mosaic: Mosaic Subplot Layout with Varying Sizes
3+
Library: bokeh 3.8.1 | Python 3.13.11
4+
Quality: 90/100 | Created: 2025-12-31
5+
"""
6+
7+
import numpy as np
8+
from bokeh.io import export_png
9+
from bokeh.layouts import Spacer, column, row
10+
from bokeh.models import ColumnDataSource, Legend, LegendItem
11+
from bokeh.plotting import figure
12+
from bokeh.themes import Theme
13+
14+
15+
np.random.seed(42)
16+
17+
# Color palette (Python Blue and Yellow)
18+
PYTHON_BLUE = "#306998"
19+
PYTHON_YELLOW = "#FFD43B"
20+
ACCENT_GREEN = "#4CAF50"
21+
22+
# Define a theme for consistent grid styling
23+
theme = Theme(json={"attrs": {"Grid": {"grid_line_alpha": 0.3, "grid_line_dash": "dashed"}}})
24+
25+
26+
# Helper to apply theme styling to a figure
27+
def apply_theme(fig):
28+
fig.grid.grid_line_alpha = 0.3
29+
fig.grid.grid_line_dash = "dashed"
30+
31+
32+
# Data for various subplots - Dashboard with business metrics
33+
34+
# A: Large overview time series (top left, spans 2 rows)
35+
days = np.arange(1, 91)
36+
revenue = 50000 + np.cumsum(np.random.randn(90) * 2000) + days * 300
37+
source_a = ColumnDataSource(data={"day": days, "revenue": revenue})
38+
39+
# B: Scatter plot (top right) - two product lines
40+
products_a = np.random.rand(25) * 100
41+
profit_margin_a = products_a * 0.35 + np.random.randn(25) * 4 + 12
42+
products_b = np.random.rand(25) * 100
43+
profit_margin_b = products_b * 0.25 + np.random.randn(25) * 5 + 8
44+
source_b1 = ColumnDataSource(data={"products": products_a, "profit_margin": profit_margin_a})
45+
source_b2 = ColumnDataSource(data={"products": products_b, "profit_margin": profit_margin_b})
46+
47+
# C: Bar chart - Categories (middle left)
48+
categories = ["Electronics", "Clothing", "Food", "Books"]
49+
sales = [45000, 32000, 28000, 18000]
50+
source_c = ColumnDataSource(data={"category": categories, "sales": sales})
51+
52+
# D: Line chart - Monthly trend (bottom, spans full width) - two years
53+
months = np.arange(1, 13)
54+
orders_2023 = [1200, 1400, 1100, 1600, 1800, 2100, 1900, 2200, 2400, 2100, 2300, 2800]
55+
orders_2024 = [1400, 1600, 1350, 1850, 2100, 2400, 2200, 2500, 2700, 2350, 2600, 3100]
56+
source_d1 = ColumnDataSource(data={"month": months, "orders": orders_2023})
57+
source_d2 = ColumnDataSource(data={"month": months, "orders": orders_2024})
58+
59+
# E: Small metric - Conversion rate trend (two channels)
60+
weeks = np.arange(1, 13)
61+
conversion_web = 3.2 + np.cumsum(np.random.randn(12) * 0.15)
62+
conversion_mobile = 2.8 + np.cumsum(np.random.randn(12) * 0.18)
63+
source_e1 = ColumnDataSource(data={"week": weeks, "conversion": conversion_web})
64+
source_e2 = ColumnDataSource(data={"week": weeks, "conversion": conversion_mobile})
65+
66+
# F: Customer satisfaction
67+
quarters = ["Q1", "Q2", "Q3", "Q4"]
68+
satisfaction = [78, 82, 85, 88]
69+
source_f = ColumnDataSource(data={"quarter": quarters, "satisfaction": satisfaction})
70+
71+
# A: Large Revenue Overview (spans 2 columns, taller)
72+
p_a = figure(
73+
width=3200,
74+
height=1100,
75+
title="Quarterly Revenue Overview",
76+
x_axis_label="Day",
77+
y_axis_label="Revenue ($)",
78+
toolbar_location=None,
79+
)
80+
line_a = p_a.line("day", "revenue", source=source_a, line_width=4, color=PYTHON_BLUE)
81+
scatter_a = p_a.scatter("day", "revenue", source=source_a, size=8, color=PYTHON_BLUE, alpha=0.6)
82+
legend_a = Legend(items=[LegendItem(label="Daily Revenue", renderers=[line_a, scatter_a])], location="top_left")
83+
p_a.add_layout(legend_a)
84+
p_a.legend.label_text_font_size = "16pt"
85+
p_a.legend.background_fill_alpha = 0.7
86+
p_a.title.text_font_size = "28pt"
87+
p_a.xaxis.axis_label_text_font_size = "20pt"
88+
p_a.yaxis.axis_label_text_font_size = "20pt"
89+
p_a.xaxis.major_label_text_font_size = "16pt"
90+
p_a.yaxis.major_label_text_font_size = "16pt"
91+
apply_theme(p_a)
92+
93+
# B: Product Profitability Scatter with two product lines
94+
p_b = figure(
95+
width=1600,
96+
height=1100,
97+
title="Product Profitability",
98+
x_axis_label="Units Sold",
99+
y_axis_label="Profit Margin (%)",
100+
toolbar_location=None,
101+
)
102+
scatter_b1 = p_b.scatter(
103+
"products", "profit_margin", source=source_b1, size=18, color=PYTHON_BLUE, alpha=0.7, legend_label="Premium Line"
104+
)
105+
scatter_b2 = p_b.scatter(
106+
"products",
107+
"profit_margin",
108+
source=source_b2,
109+
size=18,
110+
color=PYTHON_YELLOW,
111+
alpha=0.7,
112+
line_color=PYTHON_BLUE,
113+
line_width=2,
114+
legend_label="Standard Line",
115+
)
116+
p_b.legend.location = "top_left"
117+
p_b.legend.label_text_font_size = "14pt"
118+
p_b.legend.background_fill_alpha = 0.7
119+
p_b.title.text_font_size = "24pt"
120+
p_b.xaxis.axis_label_text_font_size = "18pt"
121+
p_b.yaxis.axis_label_text_font_size = "18pt"
122+
p_b.xaxis.major_label_text_font_size = "14pt"
123+
p_b.yaxis.major_label_text_font_size = "14pt"
124+
apply_theme(p_b)
125+
126+
# C: Category Sales Bar Chart
127+
p_c = figure(
128+
width=2400,
129+
height=900,
130+
x_range=categories,
131+
title="Sales by Category",
132+
x_axis_label="Category",
133+
y_axis_label="Sales ($)",
134+
toolbar_location=None,
135+
)
136+
bars_c = p_c.vbar(
137+
x="category",
138+
top="sales",
139+
source=source_c,
140+
width=0.7,
141+
color=PYTHON_BLUE,
142+
alpha=0.85,
143+
line_color="white",
144+
line_width=2,
145+
legend_label="Q4 2024 Sales",
146+
)
147+
p_c.legend.location = "top_right"
148+
p_c.legend.label_text_font_size = "14pt"
149+
p_c.legend.background_fill_alpha = 0.7
150+
p_c.title.text_font_size = "24pt"
151+
p_c.xaxis.axis_label_text_font_size = "18pt"
152+
p_c.yaxis.axis_label_text_font_size = "18pt"
153+
p_c.xaxis.major_label_text_font_size = "14pt"
154+
p_c.yaxis.major_label_text_font_size = "14pt"
155+
p_c.xaxis.major_label_orientation = 0.3
156+
apply_theme(p_c)
157+
158+
# Empty cell spacer to demonstrate gap functionality (like "." in mosaic pattern)
159+
# This represents an intentional gap in the mosaic layout
160+
empty_spacer = Spacer(width=2400, height=450, background="#F5F5F5")
161+
162+
# F: Customer Satisfaction Bar
163+
p_f = figure(
164+
width=2400,
165+
height=450,
166+
x_range=quarters,
167+
title="Customer Satisfaction",
168+
x_axis_label="Quarter",
169+
y_axis_label="Score",
170+
toolbar_location=None,
171+
)
172+
p_f.vbar(
173+
x="quarter",
174+
top="satisfaction",
175+
source=source_f,
176+
width=0.6,
177+
color=PYTHON_YELLOW,
178+
alpha=0.9,
179+
line_color=PYTHON_BLUE,
180+
line_width=2,
181+
legend_label="Satisfaction Score",
182+
)
183+
p_f.legend.location = "top_left"
184+
p_f.legend.label_text_font_size = "12pt"
185+
p_f.legend.background_fill_alpha = 0.7
186+
p_f.title.text_font_size = "22pt"
187+
p_f.xaxis.axis_label_text_font_size = "16pt"
188+
p_f.yaxis.axis_label_text_font_size = "16pt"
189+
p_f.xaxis.major_label_text_font_size = "14pt"
190+
p_f.yaxis.major_label_text_font_size = "14pt"
191+
apply_theme(p_f)
192+
193+
# D: Monthly Orders Trend with main title (full width bottom) - comparing two years
194+
p_d = figure(
195+
width=4800,
196+
height=700,
197+
title="subplot-mosaic · bokeh · pyplots.ai",
198+
x_axis_label="Month",
199+
y_axis_label="Orders",
200+
toolbar_location=None,
201+
)
202+
line_d1 = p_d.line("month", "orders", source=source_d1, line_width=5, color=PYTHON_BLUE, legend_label="2023")
203+
scatter_d1 = p_d.scatter("month", "orders", source=source_d1, size=18, color=PYTHON_BLUE)
204+
line_d2 = p_d.line("month", "orders", source=source_d2, line_width=5, color=ACCENT_GREEN, legend_label="2024")
205+
scatter_d2 = p_d.scatter("month", "orders", source=source_d2, size=18, color=ACCENT_GREEN)
206+
p_d.legend.location = "top_left"
207+
p_d.legend.label_text_font_size = "18pt"
208+
p_d.legend.background_fill_alpha = 0.7
209+
p_d.legend.orientation = "horizontal"
210+
p_d.title.text_font_size = "32pt"
211+
p_d.xaxis.axis_label_text_font_size = "22pt"
212+
p_d.yaxis.axis_label_text_font_size = "22pt"
213+
p_d.xaxis.major_label_text_font_size = "18pt"
214+
p_d.yaxis.major_label_text_font_size = "18pt"
215+
apply_theme(p_d)
216+
217+
# Create mosaic layout: AAB / C.F / DDD pattern
218+
# Row 1: A (large, spans 2 cols) + B on right
219+
# Row 2: C left, empty spacer (gap), F on right
220+
# Row 3: D spans full width
221+
# The "." in the pattern is represented by empty_spacer
222+
223+
# Row 1: Large A + B
224+
row1 = row(p_a, p_b)
225+
226+
# Row 2: C + empty cell (gap) + F - demonstrates empty cell with "."
227+
row2 = row(p_c, column(empty_spacer, p_f))
228+
229+
# Full layout
230+
layout = column(row1, row2, p_d)
231+
232+
export_png(layout, filename="plot.png")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
library: bokeh
2+
specification_id: subplot-mosaic
3+
created: '2025-12-31T10:58:44Z'
4+
updated: '2025-12-31T11:24:35Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20617510121
7+
issue: 3002
8+
python_version: 3.13.11
9+
library_version: 3.8.1
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/subplot-mosaic/bokeh/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/subplot-mosaic/bokeh/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/subplot-mosaic/bokeh/plot.html
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- Excellent demonstration of mosaic layout concept with 6 diverse subplots of varying
17+
sizes
18+
- Cohesive visual design with Python brand colors (blue/yellow) and consistent styling
19+
- Realistic business dashboard scenario with meaningful metrics (revenue, profitability,
20+
sales, satisfaction)
21+
- Good use of Bokeh row/column layout system to create asymmetric grid
22+
- Legends are consistently styled with appropriate font sizes and background alpha
23+
- All subplots have clear, descriptive titles and axis labels with units
24+
weaknesses:
25+
- Code defines unused data sources (source_e1, source_e2 for conversion rate) that
26+
clutter the implementation
27+
- The Theme object is imported and defined but never applied via curdoc; apply_theme()
28+
duplicates this functionality
29+
- Helper function apply_theme() breaks KISS principle; grid styling could be inlined
30+
- Font sizes on smaller subplots (conversion rate, satisfaction) could be slightly
31+
larger for better readability
32+
- Could demonstrate Bokeh interactive features (hover tools, linked brushing) for
33+
richer dashboard experience

0 commit comments

Comments
 (0)