Skip to content

Commit bd27128

Browse files
Merge branch 'main' into implementation/scatter-marginal/pygal
2 parents 93fe808 + 0a37ee9 commit bd27128

8 files changed

Lines changed: 884 additions & 641 deletions

File tree

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
scatter-marginal: Scatter Plot with Marginal Distributions
3-
Library: altair 6.0.0 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-26
3+
Library: altair 6.1.0 | Python 3.13.13
4+
Quality: 94/100 | Updated: 2026-05-09
55
"""
66

7-
import altair as alt
8-
import numpy as np
9-
import pandas as pd
7+
import os
8+
import sys
9+
from pathlib import Path
1010

1111

12+
current_dir = Path(__file__).parent
13+
sys.path = [p for p in sys.path if p != str(current_dir)]
14+
15+
import altair as alt # noqa: E402
16+
import numpy as np # noqa: E402
17+
import pandas as pd # noqa: E402
18+
19+
20+
# Theme tokens
21+
THEME = os.getenv("ANYPLOT_THEME", "light")
22+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
23+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
24+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
25+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
26+
27+
# Okabe-Ito palette
28+
BRAND = "#009E73"
29+
1230
# Data - bivariate distribution with correlation
1331
np.random.seed(42)
1432
n = 150
@@ -21,40 +39,47 @@
2139
x_domain = [df["X Value"].min() - 2, df["X Value"].max() + 2]
2240
y_domain = [df["Y Value"].min() - 2, df["Y Value"].max() + 2]
2341

24-
# Interactive brush selection
25-
brush = alt.param(name="brush", select="interval")
26-
2742
# Base chart
2843
base = alt.Chart(df)
2944

30-
# Main scatter plot with selection
45+
# Main scatter plot with grid
3146
scatter = (
32-
base.mark_circle(size=120, opacity=0.65)
47+
base.mark_circle(size=120, opacity=0.65, color=BRAND)
3348
.encode(
3449
x=alt.X("X Value:Q", title="X Value (units)", scale=alt.Scale(domain=x_domain)),
3550
y=alt.Y("Y Value:Q", title="Y Value (units)", scale=alt.Scale(domain=y_domain)),
36-
color=alt.condition(brush, alt.value("#306998"), alt.value("lightgray")),
3751
tooltip=["X Value:Q", "Y Value:Q"],
3852
)
3953
.properties(width=1000, height=600)
40-
.add_params(brush)
4154
)
4255

4356
# Top marginal histogram with matching X scale
4457
top_hist = (
45-
base.mark_bar(color="#306998", opacity=0.5)
58+
base.mark_bar(color=BRAND, opacity=0.5)
4659
.encode(
47-
x=alt.X("X Value:Q", bin=alt.Bin(maxbins=25), title=None, scale=alt.Scale(domain=x_domain), axis=None),
60+
x=alt.X(
61+
"X Value:Q",
62+
bin=alt.Bin(maxbins=25),
63+
title=None,
64+
scale=alt.Scale(domain=x_domain),
65+
axis=alt.Axis(labels=False, ticks=False),
66+
),
4867
y=alt.Y("count()", title=None, axis=alt.Axis(labels=False, ticks=False)),
4968
)
5069
.properties(width=1000, height=120)
5170
)
5271

5372
# Right marginal histogram with matching Y scale
5473
right_hist = (
55-
base.mark_bar(color="#306998", opacity=0.5)
74+
base.mark_bar(color=BRAND, opacity=0.5)
5675
.encode(
57-
y=alt.Y("Y Value:Q", bin=alt.Bin(maxbins=25), title=None, scale=alt.Scale(domain=y_domain), axis=None),
76+
y=alt.Y(
77+
"Y Value:Q",
78+
bin=alt.Bin(maxbins=25),
79+
title=None,
80+
scale=alt.Scale(domain=y_domain),
81+
axis=alt.Axis(labels=False, ticks=False),
82+
),
5883
x=alt.X("count()", title=None, axis=alt.Axis(labels=False, ticks=False)),
5984
)
6085
.properties(width=120, height=600)
@@ -63,13 +88,24 @@
6388
# Combine: top histogram above, scatter with right histogram below
6489
combined = (
6590
alt.vconcat(top_hist, alt.hconcat(scatter, right_hist, spacing=5), spacing=5)
66-
.properties(title=alt.Title(text="scatter-marginal · altair · pyplots.ai", fontSize=28, anchor="middle"))
67-
.configure_axis(labelFontSize=16, titleFontSize=20)
68-
.configure_title(fontSize=28)
69-
.configure_view(strokeWidth=0)
91+
.properties(
92+
background=PAGE_BG,
93+
title=alt.Title(text="scatter-marginal · altair · anyplot.ai", fontSize=28, anchor="middle", color=INK),
94+
)
95+
.configure_axis(
96+
domainColor=INK_SOFT,
97+
tickColor=INK_SOFT,
98+
gridColor=INK,
99+
gridOpacity=0.10,
100+
labelColor=INK_SOFT,
101+
labelFontSize=16,
102+
titleColor=INK,
103+
titleFontSize=20,
104+
)
105+
.configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=1)
70106
.configure_concat(spacing=5)
71107
)
72108

73109
# Save outputs
74-
combined.save("plot.png", scale_factor=3.0)
75-
combined.save("plot.html")
110+
combined.save(f"plot-{THEME}.png", scale_factor=3.0)
111+
combined.save(f"plot-{THEME}.html")
Lines changed: 88 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
scatter-marginal: Scatter Plot with Marginal Distributions
3-
Library: bokeh 3.8.1 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-26
3+
Library: bokeh 3.9.0 | Python 3.13.13
4+
Quality: 90/100 | Updated: 2026-05-09
55
"""
66

7+
import os
8+
import time
9+
from pathlib import Path
10+
711
import numpy as np
8-
from bokeh.io import export_png
12+
from bokeh.io import output_file, save
913
from bokeh.layouts import column, row
1014
from bokeh.models import ColumnDataSource
1115
from bokeh.plotting import figure
16+
from selenium import webdriver
17+
from selenium.webdriver.chrome.options import Options
18+
1219

20+
# Theme tokens
21+
THEME = os.getenv("ANYPLOT_THEME", "light")
22+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
23+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
24+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
25+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
26+
BRAND = "#009E73" # Okabe-Ito position 1
1327

1428
# Data - bivariate normal with correlation
1529
np.random.seed(42)
@@ -19,15 +33,10 @@
1933

2034
source = ColumnDataSource(data={"x": x, "y": y})
2135

22-
# Colors
23-
python_blue = "#306998"
24-
python_yellow = "#FFD43B"
25-
2636
# Calculate dimensions for 4800x2700 total with marginal plots
27-
# Main scatter: ~80% of each dimension, marginals: ~20%
2837
main_width = 3800
2938
main_height = 2100
30-
marginal_width = 3800 # Same as main for alignment
39+
marginal_width = 3800
3140
marginal_height = 550
3241
side_marginal_width = 950
3342
side_marginal_height = 2100
@@ -41,84 +50,107 @@
4150
p_scatter = figure(
4251
width=main_width,
4352
height=main_height,
44-
x_axis_label="X Value",
45-
y_axis_label="Y Value",
46-
title="scatter-marginal · bokeh · pyplots.ai",
53+
x_axis_label="Height (cm)",
54+
y_axis_label="Weight (kg)",
55+
title="scatter-marginal · bokeh · anyplot.ai",
4756
)
4857

49-
p_scatter.scatter(x="x", y="y", source=source, size=18, color=python_blue, alpha=0.65, line_color=None)
58+
p_scatter.scatter(x="x", y="y", source=source, size=20, color=BRAND, alpha=0.65, line_color=None)
5059

5160
# Style main scatter
5261
p_scatter.title.text_font_size = "28pt"
62+
p_scatter.title.text_color = INK
5363
p_scatter.xaxis.axis_label_text_font_size = "22pt"
5464
p_scatter.yaxis.axis_label_text_font_size = "22pt"
65+
p_scatter.xaxis.axis_label_text_color = INK
66+
p_scatter.yaxis.axis_label_text_color = INK
5567
p_scatter.xaxis.major_label_text_font_size = "18pt"
5668
p_scatter.yaxis.major_label_text_font_size = "18pt"
57-
p_scatter.grid.grid_line_alpha = 0.3
58-
p_scatter.grid.grid_line_dash = [6, 4]
69+
p_scatter.xaxis.major_label_text_color = INK_SOFT
70+
p_scatter.yaxis.major_label_text_color = INK_SOFT
71+
p_scatter.xaxis.axis_line_color = INK_SOFT
72+
p_scatter.yaxis.axis_line_color = INK_SOFT
73+
p_scatter.xaxis.major_tick_line_color = INK_SOFT
74+
p_scatter.yaxis.major_tick_line_color = INK_SOFT
75+
p_scatter.background_fill_color = PAGE_BG
76+
p_scatter.border_fill_color = PAGE_BG
77+
p_scatter.outline_line_color = INK_SOFT
78+
p_scatter.grid.grid_line_color = INK
79+
p_scatter.grid.grid_line_alpha = 0.10
80+
p_scatter.toolbar_location = None
5981

6082
# Top marginal histogram (X distribution)
61-
p_top = figure(
62-
width=main_width,
63-
height=marginal_height,
64-
x_range=p_scatter.x_range, # Align with scatter
65-
title=None,
66-
)
67-
p_top.quad(
68-
top=x_hist,
69-
bottom=0,
70-
left=x_edges[:-1],
71-
right=x_edges[1:],
72-
fill_color=python_yellow,
73-
line_color=python_blue,
74-
alpha=0.7,
75-
)
83+
p_top = figure(width=marginal_width, height=marginal_height, x_range=p_scatter.x_range, title=None)
84+
p_top.quad(top=x_hist, bottom=0, left=x_edges[:-1], right=x_edges[1:], fill_color=BRAND, line_color=None, alpha=0.6)
7685
p_top.xaxis.visible = False
7786
p_top.yaxis.axis_label = "Count"
7887
p_top.yaxis.axis_label_text_font_size = "18pt"
88+
p_top.yaxis.axis_label_text_color = INK
7989
p_top.yaxis.major_label_text_font_size = "14pt"
80-
p_top.grid.grid_line_alpha = 0.3
90+
p_top.yaxis.major_label_text_color = INK_SOFT
91+
p_top.yaxis.axis_line_color = INK_SOFT
92+
p_top.yaxis.major_tick_line_color = INK_SOFT
93+
p_top.background_fill_color = PAGE_BG
94+
p_top.border_fill_color = PAGE_BG
95+
p_top.outline_line_color = INK_SOFT
96+
p_top.grid.grid_line_color = INK
97+
p_top.grid.grid_line_alpha = 0.10
8198
p_top.min_border_bottom = 0
8299
p_top.min_border_left = p_scatter.min_border_left
100+
p_top.toolbar_location = None
83101

84102
# Right marginal histogram (Y distribution)
85-
p_right = figure(
86-
width=side_marginal_width,
87-
height=main_height,
88-
y_range=p_scatter.y_range, # Align with scatter
89-
title=None,
90-
)
91-
p_right.quad(
92-
top=y_edges[1:],
93-
bottom=y_edges[:-1],
94-
left=0,
95-
right=y_hist,
96-
fill_color=python_yellow,
97-
line_color=python_blue,
98-
alpha=0.7,
99-
)
103+
p_right = figure(width=side_marginal_width, height=main_height, y_range=p_scatter.y_range, title=None)
104+
p_right.quad(top=y_edges[1:], bottom=y_edges[:-1], left=0, right=y_hist, fill_color=BRAND, line_color=None, alpha=0.6)
100105
p_right.yaxis.visible = False
101106
p_right.xaxis.axis_label = "Count"
102107
p_right.xaxis.axis_label_text_font_size = "18pt"
108+
p_right.xaxis.axis_label_text_color = INK
103109
p_right.xaxis.major_label_text_font_size = "14pt"
104-
p_right.grid.grid_line_alpha = 0.3
110+
p_right.xaxis.major_label_text_color = INK_SOFT
111+
p_right.xaxis.axis_line_color = INK_SOFT
112+
p_right.xaxis.major_tick_line_color = INK_SOFT
113+
p_right.background_fill_color = PAGE_BG
114+
p_right.border_fill_color = PAGE_BG
115+
p_right.outline_line_color = INK_SOFT
116+
p_right.grid.grid_line_color = INK
117+
p_right.grid.grid_line_alpha = 0.10
105118
p_right.min_border_left = 0
106119
p_right.min_border_bottom = p_scatter.min_border_bottom
120+
p_right.toolbar_location = None
107121

108122
# Empty corner placeholder
109123
p_corner = figure(width=side_marginal_width, height=marginal_height, toolbar_location=None)
110124
p_corner.outline_line_color = None
111125
p_corner.xaxis.visible = False
112126
p_corner.yaxis.visible = False
113127
p_corner.grid.visible = False
128+
p_corner.background_fill_color = PAGE_BG
129+
p_corner.border_fill_color = PAGE_BG
114130

115-
# Remove toolbars for clean look
116-
p_scatter.toolbar_location = None
117-
p_top.toolbar_location = None
118-
p_right.toolbar_location = None
119-
120-
# Layout: top marginal + corner on top row, scatter + right marginal on bottom row
131+
# Layout
121132
layout = column(row(p_top, p_corner), row(p_scatter, p_right))
122133

123-
# Save
124-
export_png(layout, filename="plot.png")
134+
# Save HTML
135+
output_file(f"plot-{THEME}.html")
136+
save(layout)
137+
138+
# Screenshot with Selenium
139+
W, H = 4800, 2700
140+
opts = Options()
141+
for arg in (
142+
"--headless=new",
143+
"--no-sandbox",
144+
"--disable-dev-shm-usage",
145+
"--disable-gpu",
146+
f"--window-size={W},{H}",
147+
"--hide-scrollbars",
148+
):
149+
opts.add_argument(arg)
150+
151+
driver = webdriver.Chrome(options=opts)
152+
driver.set_window_size(W, H)
153+
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
154+
time.sleep(3)
155+
driver.save_screenshot(f"plot-{THEME}.png")
156+
driver.quit()

0 commit comments

Comments
 (0)