|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | andrews-curves: Andrews Curves for Multivariate Data |
3 | | -Library: bokeh 3.8.1 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-30 |
| 3 | +Library: bokeh 3.9.0 | Python 3.13.13 |
| 4 | +Quality: 92/100 | Updated: 2026-05-15 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | +import time |
| 9 | +from pathlib import Path |
| 10 | + |
7 | 11 | import numpy as np |
8 | | -from bokeh.io import export_png, output_file, save |
9 | | -from bokeh.models import ColumnDataSource, Legend |
| 12 | +from bokeh.io import output_file, save |
| 13 | +from bokeh.models import ColumnDataSource, HoverTool, Legend |
10 | 14 | from bokeh.plotting import figure |
| 15 | +from selenium import webdriver |
| 16 | +from selenium.webdriver.chrome.options import Options |
11 | 17 | from sklearn.datasets import load_iris |
12 | 18 | from sklearn.preprocessing import StandardScaler |
13 | 19 |
|
14 | 20 |
|
15 | | -# Load and prepare data |
| 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" |
| 27 | + |
| 28 | +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2"] |
| 29 | + |
| 30 | +# Data |
16 | 31 | iris = load_iris() |
17 | 32 | X = iris.data |
18 | 33 | y = iris.target |
|
22 | 37 | scaler = StandardScaler() |
23 | 38 | X_scaled = scaler.fit_transform(X) |
24 | 39 |
|
25 | | - |
26 | | -# Andrews curve function: f(t) = x1/sqrt(2) + x2*sin(t) + x3*cos(t) + x4*sin(2t) + ... |
27 | | -def andrews_curve(coeffs, t): |
28 | | - n = len(coeffs) |
29 | | - result = coeffs[0] / np.sqrt(2) |
30 | | - for i in range(1, n): |
31 | | - if i % 2 == 1: |
32 | | - result += coeffs[i] * np.sin((i // 2 + 1) * t) |
33 | | - else: |
34 | | - result += coeffs[i] * np.cos((i // 2) * t) |
35 | | - return result |
36 | | - |
37 | | - |
38 | 40 | # Generate t values from -π to π |
39 | 41 | t_values = np.linspace(-np.pi, np.pi, 200) |
40 | 42 |
|
41 | | -# Colors for each species (Python Blue, Python Yellow, and a colorblind-safe teal) |
42 | | -colors = ["#306998", "#FFD43B", "#2AA198"] |
43 | | - |
44 | 43 | # Create figure |
45 | 44 | p = figure( |
46 | 45 | width=4800, |
47 | 46 | height=2700, |
48 | | - title="andrews-curves · bokeh · pyplots.ai", |
| 47 | + title="andrews-curves · bokeh · anyplot.ai", |
49 | 48 | x_axis_label="t (radians)", |
50 | 49 | y_axis_label="f(t)", |
| 50 | + background_fill_color=PAGE_BG, |
| 51 | + border_fill_color=PAGE_BG, |
51 | 52 | ) |
52 | 53 |
|
| 54 | +# Style text |
| 55 | +p.title.text_font_size = "28pt" |
| 56 | +p.title.text_color = INK |
| 57 | +p.xaxis.axis_label_text_font_size = "22pt" |
| 58 | +p.xaxis.axis_label_text_color = INK |
| 59 | +p.yaxis.axis_label_text_font_size = "22pt" |
| 60 | +p.yaxis.axis_label_text_color = INK |
| 61 | +p.xaxis.major_label_text_font_size = "18pt" |
| 62 | +p.xaxis.major_label_text_color = INK_SOFT |
| 63 | +p.yaxis.major_label_text_font_size = "18pt" |
| 64 | +p.yaxis.major_label_text_color = INK_SOFT |
| 65 | + |
| 66 | +# Style axes and grid |
| 67 | +p.outline_line_color = INK_SOFT |
| 68 | +p.xaxis.axis_line_color = INK_SOFT |
| 69 | +p.yaxis.axis_line_color = INK_SOFT |
| 70 | +p.xaxis.major_tick_line_color = INK_SOFT |
| 71 | +p.yaxis.major_tick_line_color = INK_SOFT |
| 72 | +p.xgrid.grid_line_color = INK |
| 73 | +p.ygrid.grid_line_color = INK |
| 74 | +p.xgrid.grid_line_alpha = 0.10 |
| 75 | +p.ygrid.grid_line_alpha = 0.10 |
| 76 | + |
53 | 77 | # Store legend items |
54 | 78 | legend_items = [] |
55 | 79 |
|
56 | | -# Plot curves for each species |
| 80 | +# Plot curves for each species using vectorized Andrews curves |
57 | 81 | for species_idx in range(3): |
58 | 82 | species_mask = y == species_idx |
59 | 83 | X_species = X_scaled[species_mask] |
60 | 84 |
|
61 | | - # Track first line for legend |
62 | 85 | first_line = None |
63 | 86 |
|
64 | 87 | for coeffs in X_species: |
65 | | - curve_values = andrews_curve(coeffs, t_values) |
| 88 | + # Vectorized Andrews curve: f(t) = x1/sqrt(2) + x2*sin(t) + x3*cos(t) + x4*sin(2t) + ... |
| 89 | + n = len(coeffs) |
| 90 | + curve_values = coeffs[0] / np.sqrt(2) |
| 91 | + for i in range(1, n): |
| 92 | + if i % 2 == 1: |
| 93 | + curve_values += coeffs[i] * np.sin((i // 2 + 1) * t_values) |
| 94 | + else: |
| 95 | + curve_values += coeffs[i] * np.cos((i // 2) * t_values) |
66 | 96 |
|
67 | | - source = ColumnDataSource(data={"x": t_values, "y": curve_values}) |
| 97 | + source = ColumnDataSource( |
| 98 | + data={"x": t_values, "y": curve_values, "species": [species_names[species_idx]] * len(t_values)} |
| 99 | + ) |
68 | 100 |
|
69 | | - line = p.line(x="x", y="y", source=source, line_color=colors[species_idx], line_alpha=0.4, line_width=2) |
| 101 | + line = p.line(x="x", y="y", source=source, line_color=OKABE_ITO[species_idx], line_alpha=0.4, line_width=3) |
70 | 102 |
|
71 | 103 | if first_line is None: |
72 | 104 | first_line = line |
73 | 105 |
|
74 | 106 | legend_items.append((species_names[species_idx], [first_line])) |
75 | 107 |
|
| 108 | +# Add hover tool |
| 109 | +hover = HoverTool(tooltips=[("t", "@x{0.00}"), ("f(t)", "@y{0.00}"), ("Species", "@species")]) |
| 110 | +p.add_tools(hover) |
| 111 | + |
76 | 112 | # Create and add legend |
77 | 113 | legend = Legend(items=legend_items, location="top_right") |
78 | | -legend.label_text_font_size = "20pt" |
79 | | -legend.background_fill_alpha = 0.8 |
| 114 | +legend.label_text_font_size = "18pt" |
| 115 | +legend.label_text_color = INK_SOFT |
| 116 | +legend.background_fill_color = ELEVATED_BG |
| 117 | +legend.background_fill_alpha = 0.95 |
| 118 | +legend.border_line_color = INK_SOFT |
80 | 119 | p.add_layout(legend, "right") |
81 | 120 |
|
82 | | -# Style the plot |
83 | | -p.title.text_font_size = "28pt" |
84 | | -p.xaxis.axis_label_text_font_size = "22pt" |
85 | | -p.yaxis.axis_label_text_font_size = "22pt" |
86 | | -p.xaxis.major_label_text_font_size = "18pt" |
87 | | -p.yaxis.major_label_text_font_size = "18pt" |
88 | | - |
89 | | -# Grid styling |
90 | | -p.xgrid.grid_line_alpha = 0.3 |
91 | | -p.ygrid.grid_line_alpha = 0.3 |
92 | | -p.xgrid.grid_line_dash = "dashed" |
93 | | -p.ygrid.grid_line_dash = "dashed" |
94 | | - |
95 | | -# Save as PNG and HTML |
96 | | -export_png(p, filename="plot.png") |
97 | | -output_file("plot.html", title="Andrews Curves - Bokeh") |
| 121 | +# Save HTML |
| 122 | +output_file(f"plot-{THEME}.html") |
98 | 123 | save(p) |
| 124 | + |
| 125 | +# Screenshot with headless Chrome via Selenium |
| 126 | +W, H = 4800, 2700 |
| 127 | +opts = Options() |
| 128 | +for arg in ( |
| 129 | + "--headless=new", |
| 130 | + "--no-sandbox", |
| 131 | + "--disable-dev-shm-usage", |
| 132 | + "--disable-gpu", |
| 133 | + f"--window-size={W},{H}", |
| 134 | + "--hide-scrollbars", |
| 135 | +): |
| 136 | + opts.add_argument(arg) |
| 137 | +driver = webdriver.Chrome(options=opts) |
| 138 | +driver.set_window_size(W, H) |
| 139 | +driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}") |
| 140 | +time.sleep(3) |
| 141 | +driver.save_screenshot(f"plot-{THEME}.png") |
| 142 | +driver.quit() |
0 commit comments