|
| 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") |
0 commit comments