Skip to content

Commit d87d7ba

Browse files
Merge branch 'main' into implementation/density-basic/plotnine
2 parents 405024b + 1863880 commit d87d7ba

12 files changed

Lines changed: 1085 additions & 760 deletions

File tree

plots/density-basic/implementations/bokeh.py

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
""" pyplots.ai
22
density-basic: Basic Density Plot
3-
Library: bokeh 3.8.1 | Python 3.13.11
4-
Quality: 92/100 | Created: 2025-12-23
3+
Library: bokeh 3.8.2 | Python 3.14.3
4+
Quality: 91/100 | Updated: 2026-02-23
55
"""
66

77
import numpy as np
88
from bokeh.io import export_png
9-
from bokeh.models import ColumnDataSource
9+
from bokeh.models import ColumnDataSource, HoverTool, Label, NumeralTickFormatter
1010
from bokeh.plotting import figure
1111

1212

@@ -32,20 +32,32 @@
3232
density += np.exp(-0.5 * ((x_range - xi) / bandwidth) ** 2)
3333
density /= n * bandwidth * np.sqrt(2 * np.pi)
3434

35+
# Identify peaks for visual storytelling
36+
peak1_idx = np.argmax(density[:250])
37+
peak2_idx = 250 + np.argmax(density[250:])
38+
peak1_x, peak1_y = x_range[peak1_idx], density[peak1_idx]
39+
peak2_x, peak2_y = x_range[peak2_idx], density[peak2_idx]
40+
3541
# Create data source
3642
source = ColumnDataSource(data={"x": x_range, "density": density})
3743

44+
# Highlight regions around each peak (for visual emphasis)
45+
mask1 = (x_range > peak1_x - 60) & (x_range < peak1_x + 60)
46+
mask2 = (x_range > peak2_x - 55) & (x_range < peak2_x + 55)
47+
highlight1 = ColumnDataSource(data={"x": x_range[mask1], "density": density[mask1]})
48+
highlight2 = ColumnDataSource(data={"x": x_range[mask2], "density": density[mask2]})
49+
3850
# Rug plot data (individual observations)
39-
rug_y_pos = -0.0004 # Position below x-axis
51+
rug_y_pos = -0.0006
4052
rug_source = ColumnDataSource(
4153
data={
4254
"x": response_times,
4355
"y0": np.full_like(response_times, rug_y_pos),
44-
"y1": np.full_like(response_times, rug_y_pos + 0.0004),
56+
"y1": np.full_like(response_times, rug_y_pos + 0.0008),
4557
}
4658
)
4759

48-
# Create figure (4800 x 2700 px for 16:9 landscape)
60+
# Create figure
4961
p = figure(
5062
width=4800,
5163
height=2700,
@@ -55,36 +67,82 @@
5567
)
5668

5769
# Fill under the curve
58-
p.varea(x="x", y1=0, y2="density", source=source, fill_color="#306998", fill_alpha=0.35)
70+
p.varea(x="x", y1=0, y2="density", source=source, fill_color="#306998", fill_alpha=0.25)
5971

6072
# Density curve
61-
p.line(x="x", y="density", source=source, line_color="#306998", line_width=5, line_alpha=0.95)
73+
density_line = p.line(x="x", y="density", source=source, line_color="#306998", line_width=6, line_alpha=0.9)
74+
75+
# Emphasize peak regions with darker fill
76+
p.varea(x="x", y1=0, y2="density", source=highlight1, fill_color="#306998", fill_alpha=0.15)
77+
p.varea(x="x", y1=0, y2="density", source=highlight2, fill_color="#306998", fill_alpha=0.15)
78+
79+
# Annotate peaks for data storytelling
80+
p.add_layout(
81+
Label(
82+
x=peak1_x,
83+
y=peak1_y,
84+
text="Fast Responses",
85+
text_font_size="22pt",
86+
text_color="#1a3d5c",
87+
text_font_style="bold",
88+
text_align="center",
89+
y_offset=18,
90+
)
91+
)
92+
p.add_layout(
93+
Label(
94+
x=peak2_x,
95+
y=peak2_y,
96+
text="Slower Responses",
97+
text_font_size="22pt",
98+
text_color="#1a3d5c",
99+
text_font_style="bold",
100+
text_align="center",
101+
y_offset=18,
102+
)
103+
)
104+
105+
# Hover tool showing density values at cursor position
106+
hover = HoverTool(
107+
renderers=[density_line],
108+
tooltips=[("Response Time", "@x{0.0} ms"), ("Density", "@density{0.00000}")],
109+
mode="vline",
110+
line_policy="nearest",
111+
)
112+
p.add_tools(hover)
62113

63114
# Rug plot - vertical segments at bottom
64-
p.segment(x0="x", y0="y0", x1="x", y1="y1", source=rug_source, line_color="#306998", line_width=2, line_alpha=0.5)
115+
p.segment(x0="x", y0="y0", x1="x", y1="y1", source=rug_source, line_color="#306998", line_width=3, line_alpha=0.65)
65116

66-
# Style text sizes for large canvas (scaled up)
117+
# Text sizes for large canvas
67118
p.title.text_font_size = "36pt"
68119
p.xaxis.axis_label_text_font_size = "28pt"
69120
p.yaxis.axis_label_text_font_size = "28pt"
70121
p.xaxis.major_label_text_font_size = "22pt"
71122
p.yaxis.major_label_text_font_size = "22pt"
72123

73-
# Axis styling
74-
p.xaxis.axis_line_width = 2
75-
p.yaxis.axis_line_width = 2
76-
p.xaxis.major_tick_line_width = 2
77-
p.yaxis.major_tick_line_width = 2
124+
# Y-axis tick format
125+
p.yaxis.formatter = NumeralTickFormatter(format="0.0000")
126+
127+
# Axis styling - softened to match minimalist chrome
128+
p.xaxis.axis_line_width = 1
129+
p.yaxis.axis_line_width = 1
130+
p.xaxis.axis_line_alpha = 0.5
131+
p.yaxis.axis_line_alpha = 0.5
132+
p.xaxis.minor_tick_line_color = None
133+
p.yaxis.minor_tick_line_color = None
134+
p.xaxis.major_tick_line_color = None
135+
p.yaxis.major_tick_line_color = None
78136

79-
# Grid styling
80-
p.xgrid.grid_line_alpha = 0.3
81-
p.ygrid.grid_line_alpha = 0.3
82-
p.xgrid.grid_line_dash = "dashed"
83-
p.ygrid.grid_line_dash = "dashed"
137+
# Grid - y-axis only, subtle
138+
p.xgrid.grid_line_color = None
139+
p.ygrid.grid_line_alpha = 0.15
140+
p.ygrid.grid_line_width = 1
84141

85142
# Background
86143
p.background_fill_color = "#fafafa"
87144
p.border_fill_color = "#ffffff"
145+
p.outline_line_color = None
88146

89147
# Remove toolbar
90148
p.toolbar_location = None
Lines changed: 103 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" pyplots.ai
22
density-basic: Basic Density Plot
3-
Library: highcharts unknown | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: highcharts 1.10.3 | Python 3.14.3
4+
Quality: 92/100 | Updated: 2026-02-23
55
"""
66

77
import tempfile
@@ -18,28 +18,34 @@
1818
from selenium.webdriver.chrome.options import Options
1919

2020

21-
# Data - simulating heights (cm) with realistic distribution
21+
# Data - simulating heights (cm) with bimodal distribution
2222
np.random.seed(42)
23-
# Mix of two normal distributions to show bimodal feature (male/female heights)
2423
n_samples = 500
25-
values_a = np.random.normal(165, 7, n_samples // 2) # Female heights
26-
values_b = np.random.normal(178, 8, n_samples // 2) # Male heights
24+
values_a = np.random.normal(162, 6, n_samples // 2) # Female heights
25+
values_b = np.random.normal(178, 6, n_samples // 2) # Male heights
2726
values = np.concatenate([values_a, values_b])
2827

29-
# Kernel Density Estimation (Gaussian kernel) - inline calculation
30-
x_min, x_max = values.min() - 10, values.max() + 10
31-
x_grid = np.linspace(x_min, x_max, 200)
28+
# Kernel Density Estimation (Gaussian kernel)
29+
x_min, x_max = values.min() - 12, values.max() + 12
30+
x_grid = np.linspace(x_min, x_max, 300)
3231

3332
# Silverman's rule of thumb for bandwidth
3433
n = len(values)
35-
bandwidth = 1.06 * np.std(values) * n ** (-1 / 5)
34+
bandwidth = 0.9 * min(np.std(values), np.subtract(*np.percentile(values, [75, 25])) / 1.34) * n ** (-1 / 5)
3635

3736
# Compute Gaussian KDE
3837
density = np.zeros_like(x_grid)
3938
for xi in values:
4039
density += np.exp(-0.5 * ((x_grid - xi) / bandwidth) ** 2)
4140
density /= n * bandwidth * np.sqrt(2 * np.pi)
4241

42+
# Find the two peak positions for annotation
43+
midpoint = len(x_grid) // 2
44+
peak1_idx = np.argmax(density[:midpoint])
45+
peak2_idx = midpoint + np.argmax(density[midpoint:])
46+
peak1_x = float(x_grid[peak1_idx])
47+
peak2_x = float(x_grid[peak2_idx])
48+
4349
# Create chart
4450
chart = Chart(container="container")
4551
chart.options = HighchartsOptions()
@@ -50,79 +56,131 @@
5056
"width": 4800,
5157
"height": 2700,
5258
"backgroundColor": "#ffffff",
53-
"marginBottom": 200,
54-
"marginLeft": 200,
59+
"marginBottom": 180,
60+
"marginLeft": 180,
61+
"marginRight": 60,
62+
"marginTop": 140,
63+
"style": {"fontFamily": "Arial, Helvetica, sans-serif"},
5564
}
5665

5766
# Title
5867
chart.options.title = {
59-
"text": "density-basic · highcharts · pyplots.ai",
60-
"style": {"fontSize": "72px", "fontWeight": "bold"},
68+
"text": "density-basic \u00b7 highcharts \u00b7 pyplots.ai",
69+
"style": {"fontSize": "64px", "fontWeight": "600", "color": "#2c3e50"},
70+
"margin": 40,
6171
}
6272

63-
# X-axis
73+
# Disable credits
74+
chart.options.credits = {"enabled": False}
75+
76+
# X-axis - clean L-shaped frame with plotBands/plotLines for peak emphasis
6477
chart.options.x_axis = {
65-
"title": {"text": "Height (cm)", "style": {"fontSize": "48px"}},
66-
"labels": {"style": {"fontSize": "36px"}},
67-
"gridLineWidth": 1,
68-
"gridLineColor": "rgba(0, 0, 0, 0.25)",
78+
"title": {"text": "Height (cm)", "style": {"fontSize": "48px", "color": "#444444"}, "margin": 24},
79+
"labels": {"style": {"fontSize": "36px", "color": "#666666"}},
80+
"lineColor": "#cccccc",
81+
"lineWidth": 2,
82+
"tickWidth": 0,
83+
"tickInterval": 5,
84+
"gridLineWidth": 0,
85+
"plotBands": [
86+
{"from": peak1_x - 8, "to": peak1_x + 8, "color": "rgba(48, 105, 152, 0.06)", "zIndex": 0},
87+
{"from": peak2_x - 8, "to": peak2_x + 8, "color": "rgba(48, 105, 152, 0.06)", "zIndex": 0},
88+
],
89+
"plotLines": [
90+
{
91+
"value": peak1_x,
92+
"color": "rgba(48, 105, 152, 0.45)",
93+
"width": 3,
94+
"dashStyle": "Dash",
95+
"zIndex": 4,
96+
"label": {
97+
"text": f"Peak: {peak1_x:.0f} cm",
98+
"style": {"fontSize": "32px", "color": "#306998", "fontWeight": "600"},
99+
"rotation": 0,
100+
"y": 16,
101+
},
102+
},
103+
{
104+
"value": peak2_x,
105+
"color": "rgba(48, 105, 152, 0.45)",
106+
"width": 3,
107+
"dashStyle": "Dash",
108+
"zIndex": 4,
109+
"label": {
110+
"text": f"Peak: {peak2_x:.0f} cm",
111+
"style": {"fontSize": "32px", "color": "#306998", "fontWeight": "600"},
112+
"rotation": 0,
113+
"y": 16,
114+
},
115+
},
116+
],
69117
}
70118

71-
# Y-axis - start at 0 for proper density representation
119+
# Y-axis - subtle horizontal grid only
72120
chart.options.y_axis = {
73-
"title": {"text": "Density", "style": {"fontSize": "48px"}},
74-
"labels": {"style": {"fontSize": "36px"}},
121+
"title": {"text": "Density", "style": {"fontSize": "48px", "color": "#444444"}, "margin": 24},
122+
"labels": {"style": {"fontSize": "36px", "color": "#666666"}},
75123
"gridLineWidth": 1,
76-
"gridLineColor": "rgba(0, 0, 0, 0.25)",
124+
"gridLineColor": "rgba(0, 0, 0, 0.10)",
125+
"lineColor": "#cccccc",
126+
"lineWidth": 2,
127+
"tickAmount": 7,
77128
"min": 0,
78129
}
79130

80-
# Plot options with semi-transparent fill
131+
# Plot options
81132
chart.options.plot_options = {
82133
"area": {
83134
"fillColor": {
84135
"linearGradient": {"x1": 0, "y1": 0, "x2": 0, "y2": 1},
85-
"stops": [[0, "rgba(48, 105, 152, 0.6)"], [1, "rgba(48, 105, 152, 0.1)"]],
136+
"stops": [[0, "rgba(48, 105, 152, 0.45)"], [1, "rgba(48, 105, 152, 0.03)"]],
86137
},
87138
"lineWidth": 5,
88139
"marker": {"enabled": False},
89140
"color": "#306998",
141+
"states": {"hover": {"lineWidth": 5}},
90142
},
91143
"scatter": {
92-
"marker": {"radius": 8, "fillColor": "#FFD43B", "symbol": "diamond", "lineWidth": 2, "lineColor": "#D4AA00"}
144+
"marker": {"radius": 15, "fillColor": "rgba(48, 105, 152, 0.55)", "symbol": "diamond", "lineWidth": 0},
145+
"states": {"hover": {"enabled": False}},
93146
},
147+
"series": {"animation": False},
94148
}
95149

96-
# Legend - enabled with clear styling
150+
# Disable tooltip for static export
151+
chart.options.tooltip = {"enabled": False}
152+
153+
# Legend
97154
chart.options.legend = {
98155
"enabled": True,
99156
"layout": "horizontal",
100157
"align": "right",
101158
"verticalAlign": "top",
102159
"floating": True,
103-
"x": -50,
104-
"y": 80,
105-
"itemStyle": {"fontSize": "36px"},
106-
"symbolHeight": 24,
107-
"symbolWidth": 40,
160+
"x": -40,
161+
"y": 60,
162+
"itemStyle": {"fontSize": "34px", "fontWeight": "normal", "color": "#555555"},
163+
"symbolHeight": 20,
164+
"symbolWidth": 32,
165+
"itemDistance": 40,
166+
"borderWidth": 0,
108167
}
109168

110169
# Add density curve as area series
111170
area_series = AreaSeries()
112-
area_series.data = [[float(x), float(y)] for x, y in zip(x_grid, density, strict=True)]
113-
area_series.name = "Density Curve"
171+
area_series.data = [[round(float(x), 2), round(float(y), 6)] for x, y in zip(x_grid, density, strict=True)]
172+
area_series.name = "Density"
114173
chart.add_series(area_series)
115174

116-
# Add rug plot as small vertical tick marks at y=0
117-
# Sample every 5th point to show distribution without overcrowding
118-
rug_sample = values[::5]
119-
rug_y = 0.0005 # Small positive value just above x-axis
120-
rug_data = [[float(v), rug_y] for v in sorted(rug_sample)]
175+
# Add rug plot - vertical tick marks along x-axis
176+
rug_sample = values[::3] # Show every 3rd observation for good coverage
177+
rug_y = max(density) * 0.008 # Small positive value near axis
178+
rug_data = [[round(float(v), 2), round(float(rug_y), 6)] for v in sorted(rug_sample)]
121179

122180
rug_series = ScatterSeries()
123181
rug_series.data = rug_data
124-
rug_series.name = "Observations (Rug)"
125-
rug_series.marker = {"symbol": "diamond", "fillColor": "#FFD43B", "lineColor": "#D4AA00", "lineWidth": 2, "radius": 10}
182+
rug_series.name = "Observations"
183+
rug_series.marker = {"symbol": "diamond", "fillColor": "rgba(48, 105, 152, 0.55)", "lineWidth": 0, "radius": 15}
126184
chart.add_series(rug_series)
127185

128186
# Download Highcharts JS for inline embedding
@@ -149,9 +207,8 @@
149207
f.write(html_content)
150208
temp_path = f.name
151209

152-
# Also save HTML for interactive version
210+
# Save standalone HTML for interactive version
153211
with open("plot.html", "w", encoding="utf-8") as f:
154-
# For standalone HTML, use CDN link
155212
standalone_html = f"""<!DOCTYPE html>
156213
<html>
157214
<head>
@@ -170,15 +227,14 @@
170227
chrome_options.add_argument("--no-sandbox")
171228
chrome_options.add_argument("--disable-dev-shm-usage")
172229
chrome_options.add_argument("--disable-gpu")
173-
chrome_options.add_argument("--window-size=5000,3000")
230+
chrome_options.add_argument("--window-size=4900,2800")
174231

175232
driver = webdriver.Chrome(options=chrome_options)
176233
driver.get(f"file://{temp_path}")
177-
time.sleep(5) # Wait for chart to render
234+
time.sleep(5)
178235

179-
# Screenshot the chart element specifically for exact 4800x2700
180236
container = driver.find_element("id", "container")
181237
container.screenshot("plot.png")
182238
driver.quit()
183239

184-
Path(temp_path).unlink() # Clean up temp file
240+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)