Skip to content

Commit 6184baa

Browse files
feat(bokeh): implement spectrogram-basic (#2969)
## Implementation: `spectrogram-basic` - bokeh Implements the **bokeh** version of `spectrogram-basic`. **File:** `plots/spectrogram-basic/implementations/bokeh.py` **Parent Issue:** #2927 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20612805934)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 1e5245e commit 6184baa

File tree

2 files changed

+397
-0
lines changed

2 files changed

+397
-0
lines changed
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
""" pyplots.ai
2+
spectrogram-basic: Spectrogram Time-Frequency Heatmap
3+
Library: bokeh 3.8.1 | Python 3.13.11
4+
Quality: 92/100 | Created: 2025-12-31
5+
"""
6+
7+
import numpy as np
8+
from bokeh.io import export_png, output_file, save
9+
from bokeh.models import BasicTicker, ColorBar, LinearColorMapper
10+
from bokeh.plotting import figure
11+
from scipy import signal
12+
13+
14+
# Data - Generate chirp signal with increasing frequency
15+
np.random.seed(42)
16+
sample_rate = 8000 # 8 kHz sampling rate
17+
duration = 2.0 # 2 seconds
18+
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
19+
20+
# Create a chirp signal: frequency increases from 200 Hz to 2000 Hz
21+
f0 = 200 # Start frequency
22+
f1 = 2000 # End frequency
23+
chirp_signal = signal.chirp(t, f0, duration, f1, method="linear")
24+
25+
# Add some noise for realism
26+
chirp_signal += 0.1 * np.random.randn(len(chirp_signal))
27+
28+
# Compute spectrogram using scipy.signal
29+
nperseg = 256 # Window size
30+
noverlap = 192 # Overlap (75%)
31+
frequencies, times, Sxx = signal.spectrogram(
32+
chirp_signal, fs=sample_rate, nperseg=nperseg, noverlap=noverlap, scaling="density"
33+
)
34+
35+
# Convert to dB scale for better visualization
36+
Sxx_db = 10 * np.log10(Sxx + 1e-10) # Add small value to avoid log(0)
37+
38+
# Viridis palette (256 colors) - perceptually uniform
39+
viridis = [
40+
"#440154",
41+
"#440256",
42+
"#450457",
43+
"#450559",
44+
"#46075a",
45+
"#46085c",
46+
"#460a5d",
47+
"#460b5e",
48+
"#470d60",
49+
"#470e61",
50+
"#471063",
51+
"#471164",
52+
"#471365",
53+
"#481467",
54+
"#481668",
55+
"#481769",
56+
"#48186a",
57+
"#481a6c",
58+
"#481b6d",
59+
"#481c6e",
60+
"#481d6f",
61+
"#481f70",
62+
"#482071",
63+
"#482173",
64+
"#482374",
65+
"#482475",
66+
"#482576",
67+
"#482677",
68+
"#482878",
69+
"#482979",
70+
"#472a79",
71+
"#472c7a",
72+
"#472d7b",
73+
"#472e7c",
74+
"#472f7d",
75+
"#46307e",
76+
"#46327e",
77+
"#46337f",
78+
"#463480",
79+
"#453581",
80+
"#453681",
81+
"#453882",
82+
"#443983",
83+
"#443a83",
84+
"#443b84",
85+
"#433d84",
86+
"#433e85",
87+
"#423f85",
88+
"#424086",
89+
"#424186",
90+
"#414287",
91+
"#414487",
92+
"#404588",
93+
"#404688",
94+
"#3f4788",
95+
"#3f4889",
96+
"#3e4989",
97+
"#3e4a89",
98+
"#3d4b89",
99+
"#3d4c89",
100+
"#3c4d8a",
101+
"#3c4e8a",
102+
"#3b508a",
103+
"#3b518a",
104+
"#3a528b",
105+
"#3a538b",
106+
"#39548b",
107+
"#39558b",
108+
"#38568b",
109+
"#38578c",
110+
"#37588c",
111+
"#37598c",
112+
"#365a8c",
113+
"#365b8c",
114+
"#355c8c",
115+
"#355d8c",
116+
"#345e8d",
117+
"#345f8d",
118+
"#33608d",
119+
"#33618d",
120+
"#32628d",
121+
"#32638d",
122+
"#31648d",
123+
"#31658d",
124+
"#31668d",
125+
"#30678d",
126+
"#30688d",
127+
"#2f698d",
128+
"#2f6a8d",
129+
"#2e6b8e",
130+
"#2e6c8e",
131+
"#2e6d8e",
132+
"#2d6e8e",
133+
"#2d6f8e",
134+
"#2c708e",
135+
"#2c718e",
136+
"#2c728e",
137+
"#2b738e",
138+
"#2b748e",
139+
"#2a758e",
140+
"#2a768e",
141+
"#2a778e",
142+
"#29788e",
143+
"#29798e",
144+
"#297a8e",
145+
"#287b8e",
146+
"#287c8e",
147+
"#277d8e",
148+
"#277e8e",
149+
"#277f8e",
150+
"#26808e",
151+
"#26818e",
152+
"#26828e",
153+
"#25838e",
154+
"#25848e",
155+
"#25858e",
156+
"#24868e",
157+
"#24878e",
158+
"#23888e",
159+
"#23898e",
160+
"#238a8d",
161+
"#228b8d",
162+
"#228c8d",
163+
"#228d8d",
164+
"#218e8d",
165+
"#218f8d",
166+
"#21908d",
167+
"#21918c",
168+
"#20928c",
169+
"#20938c",
170+
"#20948c",
171+
"#1f958b",
172+
"#1f968b",
173+
"#1f978b",
174+
"#1f988a",
175+
"#1f998a",
176+
"#1f9a8a",
177+
"#1e9b89",
178+
"#1e9c89",
179+
"#1e9d88",
180+
"#1f9e88",
181+
"#1f9f88",
182+
"#1fa087",
183+
"#1fa187",
184+
"#1fa286",
185+
"#20a386",
186+
"#20a485",
187+
"#21a585",
188+
"#21a684",
189+
"#22a784",
190+
"#22a883",
191+
"#23a982",
192+
"#24aa82",
193+
"#24ab81",
194+
"#25ac81",
195+
"#26ad80",
196+
"#27ae80",
197+
"#27af7f",
198+
"#28b07e",
199+
"#29b17e",
200+
"#2ab27d",
201+
"#2cb37c",
202+
"#2db47c",
203+
"#2eb57b",
204+
"#2fb67a",
205+
"#30b77a",
206+
"#32b879",
207+
"#33b978",
208+
"#35ba78",
209+
"#36bb77",
210+
"#37bc76",
211+
"#39bd75",
212+
"#3abe75",
213+
"#3cbf74",
214+
"#3ec073",
215+
"#3fc172",
216+
"#41c272",
217+
"#43c371",
218+
"#44c470",
219+
"#46c56f",
220+
"#48c66e",
221+
"#4ac76d",
222+
"#4cc86d",
223+
"#4ec96c",
224+
"#50ca6b",
225+
"#51cb6a",
226+
"#53cc69",
227+
"#55cd68",
228+
"#57ce67",
229+
"#59cf66",
230+
"#5bd066",
231+
"#5dd165",
232+
"#5fd264",
233+
"#61d363",
234+
"#63d462",
235+
"#65d561",
236+
"#67d660",
237+
"#69d75f",
238+
"#6cd85e",
239+
"#6ed95d",
240+
"#70da5c",
241+
"#72db5b",
242+
"#74dc5a",
243+
"#76dd59",
244+
"#78de58",
245+
"#7bdf57",
246+
"#7de056",
247+
"#7fe155",
248+
"#81e254",
249+
"#83e353",
250+
"#86e452",
251+
"#88e551",
252+
"#8ae64f",
253+
"#8ce74e",
254+
"#8fe84d",
255+
"#91e94c",
256+
"#93ea4b",
257+
"#96eb4a",
258+
"#98ec49",
259+
"#9aed48",
260+
"#9dee47",
261+
"#9fef46",
262+
"#a1f045",
263+
"#a4f144",
264+
"#a6f243",
265+
"#a8f342",
266+
"#abf341",
267+
"#adf440",
268+
"#b0f540",
269+
"#b2f63f",
270+
"#b5f73e",
271+
"#b7f83d",
272+
"#baf93c",
273+
"#bcfa3c",
274+
"#bffb3b",
275+
"#c1fc3b",
276+
"#c4fd3a",
277+
"#c7fd3a",
278+
"#c9fe39",
279+
"#ccfe39",
280+
"#cfff38",
281+
"#d1ff38",
282+
"#d4ff38",
283+
"#d7ff37",
284+
"#d9ff37",
285+
"#dcff37",
286+
"#dfff36",
287+
"#e1ff36",
288+
"#e4ff36",
289+
"#e7ff36",
290+
"#e9ff36",
291+
"#ecff36",
292+
"#efff36",
293+
"#f1ff36",
294+
"#f4ff36",
295+
"#f6ff36",
296+
]
297+
298+
# Create figure with appropriate size
299+
p = figure(
300+
width=4800,
301+
height=2700,
302+
title="spectrogram-basic · bokeh · pyplots.ai",
303+
x_axis_label="Time (seconds)",
304+
y_axis_label="Frequency (Hz)",
305+
x_range=(times.min(), times.max()),
306+
y_range=(frequencies.min(), frequencies.max()),
307+
tools="",
308+
toolbar_location=None,
309+
)
310+
311+
# Create color mapper
312+
color_mapper = LinearColorMapper(palette=viridis, low=Sxx_db.min(), high=Sxx_db.max())
313+
314+
# Render spectrogram as image
315+
p.image(
316+
image=[Sxx_db],
317+
x=times.min(),
318+
y=frequencies.min(),
319+
dw=times.max() - times.min(),
320+
dh=frequencies.max() - frequencies.min(),
321+
color_mapper=color_mapper,
322+
level="image",
323+
)
324+
325+
# Add colorbar with larger text
326+
color_bar = ColorBar(
327+
color_mapper=color_mapper,
328+
ticker=BasicTicker(),
329+
label_standoff=20,
330+
border_line_color=None,
331+
location=(0, 0),
332+
title="Power (dB)",
333+
title_text_font_size="36pt",
334+
major_label_text_font_size="28pt",
335+
width=80,
336+
padding=40,
337+
)
338+
p.add_layout(color_bar, "right")
339+
340+
# Style text sizes for 4800x2700 canvas - enlarged for visibility
341+
p.title.text_font_size = "48pt"
342+
p.xaxis.axis_label_text_font_size = "36pt"
343+
p.yaxis.axis_label_text_font_size = "36pt"
344+
p.xaxis.major_label_text_font_size = "28pt"
345+
p.yaxis.major_label_text_font_size = "28pt"
346+
347+
# Axis styling
348+
p.xaxis.axis_line_width = 3
349+
p.yaxis.axis_line_width = 3
350+
p.xaxis.major_tick_line_width = 3
351+
p.yaxis.major_tick_line_width = 3
352+
353+
# Grid styling - subtle
354+
p.xgrid.grid_line_alpha = 0.3
355+
p.ygrid.grid_line_alpha = 0.3
356+
p.xgrid.grid_line_dash = "dashed"
357+
p.ygrid.grid_line_dash = "dashed"
358+
359+
# Background
360+
p.background_fill_color = None
361+
p.border_fill_color = None
362+
363+
# Outline
364+
p.outline_line_color = "#333333"
365+
p.outline_line_width = 2
366+
367+
# Save PNG
368+
export_png(p, filename="plot.png")
369+
370+
# Save HTML for interactive viewing
371+
output_file("plot.html", title="spectrogram-basic · bokeh · pyplots.ai")
372+
save(p)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
library: bokeh
2+
specification_id: spectrogram-basic
3+
created: '2025-12-31T05:36:31Z'
4+
updated: '2025-12-31T05:49:59Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20612805934
7+
issue: 2927
8+
python_version: 3.13.11
9+
library_version: 3.8.1
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/spectrogram-basic/bokeh/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/spectrogram-basic/bokeh/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/spectrogram-basic/bokeh/plot.html
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Excellent use of viridis colormap for perceptually uniform and colorblind-accessible
17+
visualization
18+
- Clean implementation with proper dB scale conversion for power spectral density
19+
- Clear chirp signal demonstrates time-frequency analysis effectively
20+
- Proper scipy.signal integration for spectrogram computation
21+
- Well-sized text for the 4800x2700 canvas
22+
weaknesses:
23+
- Grid lines (dashed) are somewhat distracting over the heatmap image; consider
24+
disabling or reducing opacity
25+
- Could leverage Bokeh interactive hover tools to show frequency/time/power values

0 commit comments

Comments
 (0)