|
| 1 | +""" pyplots.ai |
| 2 | +line-confidence: Line Plot with Confidence Interval |
| 3 | +Library: altair 6.0.0 | Python 3.13.11 |
| 4 | +Quality: 78/100 | Created: 2025-12-26 |
| 5 | +""" |
| 6 | + |
| 7 | +import altair as alt |
| 8 | +import numpy as np |
| 9 | +import pandas as pd |
| 10 | + |
| 11 | + |
| 12 | +# Data: Simulating a time series forecast with 95% confidence interval |
| 13 | +np.random.seed(42) |
| 14 | + |
| 15 | +# Generate time points (50 days of daily data) |
| 16 | +n_points = 50 |
| 17 | +days = np.arange(n_points) |
| 18 | + |
| 19 | +# Create a trend with some curvature (e.g., a model prediction) |
| 20 | +trend = 100 + 0.5 * days + 0.02 * days**2 + np.sin(days / 5) * 5 |
| 21 | + |
| 22 | +# Add noise for the central line |
| 23 | +noise = np.random.normal(0, 3, n_points) |
| 24 | +y_mean = trend + noise |
| 25 | + |
| 26 | +# Confidence interval that widens over time (typical for forecasts) |
| 27 | +uncertainty = 5 + 0.15 * days # Uncertainty grows with time |
| 28 | +y_lower = y_mean - 1.96 * uncertainty / 2 |
| 29 | +y_upper = y_mean + 1.96 * uncertainty / 2 |
| 30 | + |
| 31 | +# Create DataFrame |
| 32 | +df = pd.DataFrame({"Day": days, "Value": y_mean, "Lower": y_lower, "Upper": y_upper}) |
| 33 | + |
| 34 | +# Calculate appropriate y-axis domain with padding (avoid unnecessary whitespace from 0) |
| 35 | +y_min = df["Lower"].min() |
| 36 | +y_max = df["Upper"].max() |
| 37 | +y_padding = (y_max - y_min) * 0.1 |
| 38 | +y_domain = [y_min - y_padding, y_max + y_padding] |
| 39 | + |
| 40 | +# Create shared y-scale for consistent rendering across all layers |
| 41 | +y_scale = alt.Scale(domain=y_domain) |
| 42 | + |
| 43 | +# Create the confidence band (area) with fill encoding for legend |
| 44 | +band = ( |
| 45 | + alt.Chart(df) |
| 46 | + .transform_calculate(legend_label='"95% Confidence Interval"') |
| 47 | + .mark_area(opacity=0.3) |
| 48 | + .encode( |
| 49 | + x=alt.X("Day:Q", title="Day", axis=alt.Axis(values=list(range(0, 51, 5)))), |
| 50 | + y=alt.Y("Lower:Q", title="Predicted Value", scale=y_scale), |
| 51 | + y2="Upper:Q", |
| 52 | + fill=alt.Fill( |
| 53 | + "legend_label:N", |
| 54 | + scale=alt.Scale(domain=["95% Confidence Interval"], range=["#306998"]), |
| 55 | + legend=alt.Legend( |
| 56 | + title="Legend", |
| 57 | + orient="right", |
| 58 | + labelFontSize=16, |
| 59 | + titleFontSize=18, |
| 60 | + symbolType="square", |
| 61 | + symbolSize=300, |
| 62 | + symbolOpacity=0.3, |
| 63 | + ), |
| 64 | + ), |
| 65 | + ) |
| 66 | +) |
| 67 | + |
| 68 | +# Create the central line with stroke encoding for legend |
| 69 | +line = ( |
| 70 | + alt.Chart(df) |
| 71 | + .transform_calculate(legend_label='"Predicted Mean"') |
| 72 | + .mark_line(strokeWidth=4) |
| 73 | + .encode( |
| 74 | + x="Day:Q", |
| 75 | + y=alt.Y("Value:Q", scale=y_scale), |
| 76 | + stroke=alt.Stroke( |
| 77 | + "legend_label:N", |
| 78 | + scale=alt.Scale(domain=["Predicted Mean"], range=["#306998"]), |
| 79 | + legend=alt.Legend( |
| 80 | + title="Legend", orient="right", labelFontSize=16, titleFontSize=18, symbolStrokeWidth=4, symbolSize=300 |
| 81 | + ), |
| 82 | + ), |
| 83 | + ) |
| 84 | +) |
| 85 | + |
| 86 | +# Add point markers on the line for clarity (increased size for better visibility) |
| 87 | +points = ( |
| 88 | + alt.Chart(df) |
| 89 | + .mark_point(size=100, filled=True, color="#306998") |
| 90 | + .encode(x="Day:Q", y=alt.Y("Value:Q", scale=y_scale)) |
| 91 | +) |
| 92 | + |
| 93 | +# Combine band, line, and points with resolved legends |
| 94 | +chart = ( |
| 95 | + alt.layer(band, line, points) |
| 96 | + .resolve_legend(fill="independent", stroke="independent") |
| 97 | + .properties( |
| 98 | + width=1600, height=900, title=alt.Title("line-confidence · altair · pyplots.ai", fontSize=28, anchor="middle") |
| 99 | + ) |
| 100 | + .configure_axis(labelFontSize=18, titleFontSize=22, gridOpacity=0.3) |
| 101 | + .configure_view(strokeWidth=0) |
| 102 | +) |
| 103 | + |
| 104 | +# Save as PNG (scale_factor=3 gives 4800x2700) |
| 105 | +chart.save("plot.png", scale_factor=3.0) |
| 106 | + |
| 107 | +# Save as HTML for interactivity |
| 108 | +interactive_band = ( |
| 109 | + alt.Chart(df) |
| 110 | + .mark_area(opacity=0.3, color="#306998") |
| 111 | + .encode( |
| 112 | + x=alt.X("Day:Q", title="Day"), |
| 113 | + y=alt.Y("Lower:Q", title="Predicted Value", scale=y_scale), |
| 114 | + y2="Upper:Q", |
| 115 | + tooltip=[ |
| 116 | + alt.Tooltip("Day:Q", title="Day"), |
| 117 | + alt.Tooltip("Lower:Q", title="Lower Bound", format=".1f"), |
| 118 | + alt.Tooltip("Upper:Q", title="Upper Bound", format=".1f"), |
| 119 | + ], |
| 120 | + ) |
| 121 | +) |
| 122 | + |
| 123 | +interactive_line = ( |
| 124 | + alt.Chart(df).mark_line(strokeWidth=3, color="#306998").encode(x="Day:Q", y=alt.Y("Value:Q", scale=y_scale)) |
| 125 | +) |
| 126 | + |
| 127 | +interactive_points = ( |
| 128 | + alt.Chart(df) |
| 129 | + .mark_point(size=60, filled=True, color="#306998") |
| 130 | + .encode( |
| 131 | + x="Day:Q", |
| 132 | + y=alt.Y("Value:Q", scale=y_scale), |
| 133 | + tooltip=[alt.Tooltip("Day:Q", title="Day"), alt.Tooltip("Value:Q", title="Predicted Value", format=".1f")], |
| 134 | + ) |
| 135 | +) |
| 136 | + |
| 137 | +interactive_chart = ( |
| 138 | + alt.layer(interactive_band, interactive_line, interactive_points) |
| 139 | + .properties( |
| 140 | + width=800, height=450, title=alt.Title("line-confidence · altair · pyplots.ai", fontSize=20, anchor="middle") |
| 141 | + ) |
| 142 | + .configure_axis(labelFontSize=14, titleFontSize=16, gridOpacity=0.3) |
| 143 | + .configure_view(strokeWidth=0) |
| 144 | + .interactive() |
| 145 | +) |
| 146 | + |
| 147 | +interactive_chart.save("plot.html") |
0 commit comments