|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | facet-grid: Faceted Grid Plot |
3 | | -Library: bokeh 3.8.1 | Python 3.13.11 |
4 | | -Quality: 90/100 | Created: 2025-12-30 |
| 3 | +Library: bokeh 3.9.0 | Python 3.13.13 |
| 4 | +Quality: 87/100 | Updated: 2026-05-13 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | +import time |
| 9 | +from pathlib import Path |
| 10 | + |
7 | 11 | import numpy as np |
8 | 12 | import pandas as pd |
9 | | -from bokeh.io import export_png |
| 13 | +from bokeh.io import output_file, save |
10 | 14 | from bokeh.layouts import column, gridplot |
11 | | -from bokeh.models import ColumnDataSource, Div |
| 15 | +from bokeh.models import ColumnDataSource, Div, HoverTool |
12 | 16 | from bokeh.plotting import figure |
| 17 | +from selenium import webdriver |
| 18 | +from selenium.webdriver.chrome.options import Options |
| 19 | + |
| 20 | + |
| 21 | +# Theme tokens |
| 22 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 23 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 24 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 25 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 26 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
13 | 27 |
|
| 28 | +# Okabe-Ito palette |
| 29 | +OKABE_ITO = [ |
| 30 | + "#009E73", # bluish green (brand) |
| 31 | + "#D55E00", # vermillion |
| 32 | + "#0072B2", # blue |
| 33 | +] |
14 | 34 |
|
15 | 35 | # Data - Product performance across regions and seasons |
16 | 36 | np.random.seed(42) |
|
37 | 57 |
|
38 | 58 | df = pd.DataFrame(data) |
39 | 59 |
|
40 | | -# Colors for each region - darker orange instead of yellow for better visibility |
41 | | -colors = {"North": "#306998", "South": "#E69F00", "East": "#4ECDC4"} |
| 60 | +# Map regions to colors |
| 61 | +color_map = {"North": OKABE_ITO[0], "South": OKABE_ITO[1], "East": OKABE_ITO[2]} |
42 | 62 |
|
43 | 63 | # Create grid of plots (rows=seasons, cols=regions) |
44 | | -# Target: 4800x2700 total. With 3 cols, 4 rows + title/legend: |
45 | | -# - Subplot width: 4800 / 3 = 1600 |
46 | | -# - Subplot height: (2700 - 120 title - 100 legend) / 4 = 620 |
47 | 64 | plots = [] |
48 | 65 |
|
49 | 66 | for season in seasons: |
50 | 67 | row_plots = [] |
51 | 68 | for region in regions: |
52 | 69 | subset = df[(df["region"] == region) & (df["season"] == season)] |
53 | | - source = ColumnDataSource(data={"x": subset["marketing_spend"], "y": subset["sales"]}) |
| 70 | + source = ColumnDataSource( |
| 71 | + data={"x": subset["marketing_spend"], "y": subset["sales"], "region": [region] * len(subset)} |
| 72 | + ) |
54 | 73 |
|
55 | 74 | # Create figure for each cell |
56 | | - p = figure(width=1600, height=620, x_range=(5, 50), y_range=(20, 130), tools="") |
| 75 | + p = figure( |
| 76 | + width=1600, height=620, x_range=(5, 50), y_range=(20, 130), tools="", title="", toolbar_location=None |
| 77 | + ) |
57 | 78 |
|
58 | 79 | # Add scatter points |
59 | | - p.scatter( |
60 | | - x="x", y="y", source=source, size=18, color=colors[region], alpha=0.7, line_color="#333333", line_width=1 |
| 80 | + scatter = p.scatter( |
| 81 | + x="x", y="y", source=source, size=18, color=color_map[region], alpha=0.7, line_color=PAGE_BG, line_width=1 |
61 | 82 | ) |
62 | 83 |
|
| 84 | + # Add hover tooltip |
| 85 | + hover = HoverTool( |
| 86 | + renderers=[scatter], tooltips=[("Region", "@region"), ("Spend", "@x{0.0}"), ("Sales", "@y{0.0}")] |
| 87 | + ) |
| 88 | + p.add_tools(hover) |
| 89 | + |
63 | 90 | # Add facet label in top-left corner |
64 | 91 | p.text( |
65 | | - x=[8], |
66 | | - y=[122], |
67 | | - text=[f"{region} · {season}"], |
68 | | - text_font_size="18pt", |
69 | | - text_color="#333333", |
70 | | - text_font_style="bold", |
| 92 | + x=[8], y=[122], text=[f"{region} · {season}"], text_font_size="18pt", text_color=INK, text_font_style="bold" |
71 | 93 | ) |
72 | 94 |
|
73 | | - # Style axes |
| 95 | + # Style axes - only show labels on edges |
74 | 96 | p.xaxis.axis_label = "Marketing Spend ($K)" if season == seasons[-1] else "" |
75 | 97 | p.yaxis.axis_label = "Sales ($K)" if region == regions[0] else "" |
76 | | - p.xaxis.axis_label_text_font_size = "20pt" |
77 | | - p.yaxis.axis_label_text_font_size = "20pt" |
78 | | - p.xaxis.major_label_text_font_size = "16pt" |
79 | | - p.yaxis.major_label_text_font_size = "16pt" |
| 98 | + p.xaxis.axis_label_text_font_size = "22pt" |
| 99 | + p.yaxis.axis_label_text_font_size = "22pt" |
| 100 | + p.xaxis.major_label_text_font_size = "18pt" |
| 101 | + p.yaxis.major_label_text_font_size = "18pt" |
| 102 | + |
| 103 | + # Theme-adaptive styling |
| 104 | + p.background_fill_color = PAGE_BG |
| 105 | + p.border_fill_color = PAGE_BG |
| 106 | + p.outline_line_color = INK_SOFT |
| 107 | + p.title.text_color = INK |
| 108 | + p.xaxis.axis_label_text_color = INK |
| 109 | + p.yaxis.axis_label_text_color = INK |
| 110 | + p.xaxis.major_label_text_color = INK_SOFT |
| 111 | + p.yaxis.major_label_text_color = INK_SOFT |
| 112 | + p.xaxis.axis_line_color = INK_SOFT |
| 113 | + p.yaxis.axis_line_color = INK_SOFT |
80 | 114 |
|
81 | 115 | # Grid styling |
82 | | - p.xgrid.grid_line_alpha = 0.3 |
83 | | - p.ygrid.grid_line_alpha = 0.3 |
84 | | - p.xgrid.grid_line_dash = "dashed" |
85 | | - p.ygrid.grid_line_dash = "dashed" |
86 | | - |
87 | | - # Background |
88 | | - p.background_fill_color = "#fafafa" |
| 116 | + p.xgrid.grid_line_color = INK |
| 117 | + p.ygrid.grid_line_color = INK |
| 118 | + p.xgrid.grid_line_alpha = 0.15 |
| 119 | + p.ygrid.grid_line_alpha = 0.15 |
89 | 120 |
|
90 | 121 | row_plots.append(p) |
91 | 122 | plots.append(row_plots) |
92 | 123 |
|
93 | 124 | # Create gridplot layout |
94 | 125 | grid = gridplot(plots, toolbar_location=None, merge_tools=False) |
95 | 126 |
|
96 | | -# Add overall title using Div for cleaner implementation |
| 127 | +# Add overall title using Div |
97 | 128 | title_div = Div( |
98 | | - text="<h1 style='text-align: center; color: #333333; font-size: 32pt; margin: 20px 0;'>" |
99 | | - "facet-grid · bokeh · pyplots.ai</h1>", |
| 129 | + text=f"<h1 style='text-align: center; color: {INK}; font-size: 32pt; margin: 20px 0;'>" |
| 130 | + f"facet-grid · bokeh · anyplot.ai</h1>", |
100 | 131 | width=4800, |
101 | 132 | height=120, |
102 | 133 | ) |
103 | 134 |
|
104 | 135 | # Create legend using Div for region-color mapping |
105 | 136 | legend_html = ( |
106 | | - "<div style='text-align: center; font-size: 18pt; padding: 20px 0;'>" |
107 | | - "<span style='font-weight: bold; margin-right: 30px;'>Region:</span>" |
| 137 | + f"<div style='text-align: center; font-size: 18pt; padding: 20px 0; color: {INK};'>" |
| 138 | + f"<span style='font-weight: bold; margin-right: 30px;'>Region:</span>" |
108 | 139 | ) |
109 | | -for region, color in colors.items(): |
| 140 | +for region in regions: |
| 141 | + color = color_map[region] |
110 | 142 | legend_html += ( |
111 | 143 | f"<span style='margin-right: 40px;'>" |
112 | 144 | f"<span style='display: inline-block; width: 20px; height: 20px; " |
113 | | - f"background-color: {color}; border: 1px solid #333; border-radius: 50%; " |
| 145 | + f"background-color: {color}; border: 1px solid {INK_SOFT}; border-radius: 50%; " |
114 | 146 | f"vertical-align: middle; margin-right: 8px;'></span>" |
115 | | - f"{region}</span>" |
| 147 | + f"<span style='color: {INK_SOFT};'>{region}</span></span>" |
116 | 148 | ) |
117 | 149 | legend_html += "</div>" |
118 | 150 |
|
119 | | -legend_div = Div(text=legend_html, width=4800, height=80) |
| 151 | +legend_div = Div(text=legend_html, width=4800, height=100) |
120 | 152 |
|
121 | 153 | # Combine title, grid, and legend using column layout |
122 | 154 | final_layout = column(title_div, grid, legend_div) |
123 | 155 |
|
124 | | -# Save |
125 | | -export_png(final_layout, filename="plot.png") |
| 156 | +# Save HTML |
| 157 | +output_file(f"plot-{THEME}.html") |
| 158 | +save(final_layout) |
| 159 | + |
| 160 | +# Screenshot with headless Chrome |
| 161 | +W, H = 4800, 2700 |
| 162 | +opts = Options() |
| 163 | +for arg in ( |
| 164 | + "--headless=new", |
| 165 | + "--no-sandbox", |
| 166 | + "--disable-dev-shm-usage", |
| 167 | + "--disable-gpu", |
| 168 | + f"--window-size={W},{H}", |
| 169 | + "--hide-scrollbars", |
| 170 | +): |
| 171 | + opts.add_argument(arg) |
| 172 | +driver = webdriver.Chrome(options=opts) |
| 173 | +driver.set_window_size(W, H) |
| 174 | +driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}") |
| 175 | +time.sleep(3) |
| 176 | +driver.save_screenshot(f"plot-{THEME}.png") |
| 177 | +driver.quit() |
0 commit comments