{"spec_id":"spectrogram-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nspectrogram-basic: Spectrogram Time-Frequency Heatmap\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-15\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Data: Generate a chirp signal with increasing frequency\nnp.random.seed(42)\nsample_rate = 4000  # Hz\nduration = 2.0  # seconds\nn_samples = int(sample_rate * duration)\nt = np.linspace(0, duration, n_samples)\n\n# Chirp signal: frequency sweeps from 100 Hz to 800 Hz (linear chirp)\nf0, f1 = 100, 800\nphase = 2 * np.pi * (f0 * t + (f1 - f0) / (2 * duration) * t**2)\nchirp_signal = np.sin(phase)\n\n# Add some noise for realism\nchirp_signal += np.random.randn(len(chirp_signal)) * 0.1\n\n# Compute spectrogram using numpy FFT (Short-Time Fourier Transform)\nnperseg = 256  # Window size\nhop_length = 32  # Step between windows (higher overlap for smoother result)\nwindow = np.hanning(nperseg)\n\n# Calculate number of frames\nn_frames = (n_samples - nperseg) // hop_length + 1\n\n# Initialize spectrogram matrix\nn_freq = nperseg // 2 + 1\nSxx = np.zeros((n_freq, n_frames))\n\n# Compute STFT\nfor i in range(n_frames):\n    start = i * hop_length\n    segment = chirp_signal[start : start + nperseg] * window\n    fft_result = np.fft.rfft(segment)\n    Sxx[:, i] = np.abs(fft_result) ** 2\n\n# Frequency and time arrays\nfrequencies = np.fft.rfftfreq(nperseg, 1 / sample_rate)\ntimes = (np.arange(n_frames) * hop_length + nperseg / 2) / sample_rate\n\n# Convert power to dB scale\nSxx_db = 10 * np.log10(Sxx + 1e-10)\n\n# Limit frequency range for better visualization (0-1000 Hz)\nfreq_mask = frequencies <= 1000\nfrequencies_subset = frequencies[freq_mask]\nSxx_db_subset = Sxx_db[freq_mask, :]\n\n# Create meshgrid and flatten for DataFrame\ntime_grid, freq_grid = np.meshgrid(times, frequencies_subset)\ndf = pd.DataFrame(\n    {\"Time (s)\": time_grid.flatten(), \"Frequency (Hz)\": freq_grid.flatten(), \"Power (dB)\": Sxx_db_subset.flatten()}\n)\n\n# Calculate bin sizes for proper rectangle rendering\ntime_step = times[1] - times[0] if len(times) > 1 else 0.01\nfreq_step = frequencies_subset[1] - frequencies_subset[0] if len(frequencies_subset) > 1 else 10\n\n# Add bin edges for proper rectangle sizing\ndf[\"time_start\"] = df[\"Time (s)\"] - time_step / 2\ndf[\"time_end\"] = df[\"Time (s)\"] + time_step / 2\ndf[\"freq_start\"] = df[\"Frequency (Hz)\"] - freq_step / 2\ndf[\"freq_end\"] = df[\"Frequency (Hz)\"] + freq_step / 2\n\n# Create spectrogram heatmap with Altair using x2/y2 for proper rectangles\nchart = (\n    alt.Chart(df)\n    .mark_rect()\n    .encode(\n        x=alt.X(\"time_start:Q\", title=\"Time (s)\", scale=alt.Scale(nice=False)),\n        x2=alt.X2(\"time_end:Q\"),\n        y=alt.Y(\"freq_start:Q\", title=\"Frequency (Hz)\", scale=alt.Scale(nice=False)),\n        y2=alt.Y2(\"freq_end:Q\"),\n        color=alt.Color(\n            \"Power (dB):Q\",\n            scale=alt.Scale(scheme=\"viridis\"),\n            legend=alt.Legend(\n                title=\"Power (dB)\",\n                titleFontSize=18,\n                labelFontSize=16,\n                gradientLength=400,\n                gradientThickness=20,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n            ),\n        ),\n        tooltip=[\n            alt.Tooltip(\"Time (s):Q\", format=\".3f\"),\n            alt.Tooltip(\"Frequency (Hz):Q\", format=\".1f\"),\n            alt.Tooltip(\"Power (dB):Q\", format=\".1f\"),\n        ],\n    )\n    .properties(width=1600, height=900, background=PAGE_BG, title=\"spectrogram-basic · altair · anyplot.ai\")\n    .configure_title(fontSize=28, anchor=\"middle\", color=INK)\n    .configure_axis(\n        labelFontSize=18,\n        titleFontSize=22,\n        tickSize=10,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}