|
| 1 | +""" pyplots.ai |
| 2 | +histogram-density: Density Histogram |
| 3 | +Library: altair 6.0.0 | Python 3.13.11 |
| 4 | +Quality: 93/100 | Created: 2025-12-29 |
| 5 | +""" |
| 6 | + |
| 7 | +import altair as alt |
| 8 | +import numpy as np |
| 9 | +import pandas as pd |
| 10 | +from scipy import stats |
| 11 | + |
| 12 | + |
| 13 | +# Data - Generate bimodal distribution to show density histogram features |
| 14 | +np.random.seed(42) |
| 15 | +# Reaction times from two conditions: baseline and fatigued |
| 16 | +baseline_times = np.random.normal(loc=250, scale=30, size=350) |
| 17 | +fatigued_times = np.random.normal(loc=380, scale=45, size=150) |
| 18 | +reaction_times = np.concatenate([baseline_times, fatigued_times]) |
| 19 | + |
| 20 | +# Compute histogram data manually for density normalization |
| 21 | +num_bins = 25 |
| 22 | +bins = np.linspace(reaction_times.min() - 10, reaction_times.max() + 10, num_bins + 1) |
| 23 | +counts, bin_edges = np.histogram(reaction_times, bins=bins, density=True) |
| 24 | +bin_width = bin_edges[1] - bin_edges[0] |
| 25 | + |
| 26 | +# Create DataFrame with bin ranges for proper bar rendering |
| 27 | +hist_df = pd.DataFrame( |
| 28 | + { |
| 29 | + "bin_start": bin_edges[:-1], |
| 30 | + "bin_end": bin_edges[1:], |
| 31 | + "Density": counts, |
| 32 | + "bin_center": (bin_edges[:-1] + bin_edges[1:]) / 2, |
| 33 | + } |
| 34 | +) |
| 35 | + |
| 36 | +# Create density histogram using rect mark for proper filled bars |
| 37 | +histogram = ( |
| 38 | + alt.Chart(hist_df) |
| 39 | + .mark_rect(color="#306998", opacity=0.75, stroke="#1a3a5c", strokeWidth=1.5) |
| 40 | + .encode( |
| 41 | + x=alt.X("bin_start:Q", scale=alt.Scale(domain=[bins.min(), bins.max()]), title="Reaction Time (ms)"), |
| 42 | + x2="bin_end:Q", |
| 43 | + y=alt.Y("Density:Q", scale=alt.Scale(domain=[0, counts.max() * 1.1]), title="Density (probability per ms)"), |
| 44 | + tooltip=[ |
| 45 | + alt.Tooltip("bin_center:Q", title="Bin Center", format=".0f"), |
| 46 | + alt.Tooltip("Density:Q", title="Density", format=".5f"), |
| 47 | + ], |
| 48 | + ) |
| 49 | +) |
| 50 | + |
| 51 | +# Create KDE overlay for theoretical density reference |
| 52 | +kde = stats.gaussian_kde(reaction_times, bw_method=0.15) |
| 53 | +x_kde = np.linspace(reaction_times.min() - 20, reaction_times.max() + 20, 300) |
| 54 | +y_kde = kde(x_kde) |
| 55 | + |
| 56 | +kde_df = pd.DataFrame({"Reaction Time (ms)": x_kde, "Density": y_kde}) |
| 57 | + |
| 58 | +kde_line = alt.Chart(kde_df).mark_line(color="#FFD43B", strokeWidth=4).encode(x="Reaction Time (ms):Q", y="Density:Q") |
| 59 | + |
| 60 | +# Combine histogram and KDE |
| 61 | +chart = ( |
| 62 | + alt.layer(histogram, kde_line) |
| 63 | + .properties( |
| 64 | + width=1600, |
| 65 | + height=900, |
| 66 | + title=alt.Title("histogram-density · altair · pyplots.ai", fontSize=28, anchor="middle", color="#333333"), |
| 67 | + ) |
| 68 | + .configure_axis(labelFontSize=18, titleFontSize=22, gridColor="#cccccc", gridOpacity=0.3) |
| 69 | + .configure_view(strokeWidth=0) |
| 70 | +) |
| 71 | + |
| 72 | +# Save as PNG (1600 × 900 × 3 = 4800 × 2700 px) |
| 73 | +chart.save("plot.png", scale_factor=3.0) |
| 74 | + |
| 75 | +# Save interactive HTML version |
| 76 | +chart.save("plot.html") |
0 commit comments