|
1 | | -""" anyplot.ai |
| 1 | +"""anyplot.ai |
2 | 2 | ecdf-basic: Basic ECDF Plot |
3 | 3 | Library: altair 6.2.2 | Python 3.13.14 |
4 | 4 | Quality: 89/100 | Updated: 2026-06-25 |
|
34 | 34 | response_times_ms = np.random.normal(loc=120, scale=35, size=250) |
35 | 35 | response_times_ms = np.clip(response_times_ms, 20, None) |
36 | 36 |
|
37 | | -sorted_latency = np.sort(response_times_ms) |
38 | | -cumulative_proportion = np.arange(1, len(sorted_latency) + 1) / len(sorted_latency) |
39 | | -df = pd.DataFrame({"latency_ms": sorted_latency, "cumulative": cumulative_proportion}) |
| 37 | +# Raw data frame — ECDF computed declaratively via Vega-Lite window transform |
| 38 | +df = pd.DataFrame({"latency_ms": response_times_ms}) |
40 | 39 |
|
41 | | -# Reference values at quartiles for focal emphasis |
| 40 | +# Reference values at quartiles for focal emphasis and text annotations |
42 | 41 | p25_ms = float(np.percentile(response_times_ms, 25)) |
43 | 42 | p50_ms = float(np.median(response_times_ms)) |
44 | 43 | p75_ms = float(np.percentile(response_times_ms, 75)) |
45 | | -ref_df = pd.DataFrame({"latency_ms": [p25_ms, p50_ms, p75_ms], "cumulative": [0.25, 0.50, 0.75]}) |
| 44 | +ref_df = pd.DataFrame( |
| 45 | + { |
| 46 | + "latency_ms": [p25_ms, p50_ms, p75_ms], |
| 47 | + "cumulative": [0.25, 0.50, 0.75], |
| 48 | + "label": [f"~{p25_ms:.0f}ms", f"~{p50_ms:.0f}ms", f"~{p75_ms:.0f}ms"], |
| 49 | + } |
| 50 | +) |
46 | 51 |
|
47 | 52 | # Title |
48 | 53 | title_str = "ecdf-basic · python · altair · anyplot.ai" |
49 | 54 |
|
50 | | -# ECDF step function — mark_line with step-after interpolation |
| 55 | +# ECDF step function — cume_dist() window transform computes the ECDF declaratively |
| 56 | +# in Vega-Lite without numpy preprocessing; step-after gives the correct step shape |
51 | 57 | ecdf_line = ( |
52 | 58 | alt.Chart(df) |
| 59 | + .transform_window(ecdf="cume_dist()", sort=[alt.SortField("latency_ms")]) |
53 | 60 | .mark_line(interpolate="step-after", strokeWidth=3.5, color=BRAND) |
54 | 61 | .encode( |
55 | 62 | x=alt.X("latency_ms:Q", title="API Response Time (ms)", scale=alt.Scale(nice=True)), |
56 | 63 | y=alt.Y( |
57 | | - "cumulative:Q", |
| 64 | + "ecdf:Q", |
58 | 65 | title="Cumulative Proportion", |
59 | 66 | scale=alt.Scale(domain=[0, 1]), |
60 | 67 | axis=alt.Axis(format=".0%", tickCount=11), |
61 | 68 | ), |
62 | 69 | tooltip=[ |
63 | 70 | alt.Tooltip("latency_ms:Q", title="Latency (ms)", format=".1f"), |
64 | | - alt.Tooltip("cumulative:Q", title="Proportion", format=".3f"), |
| 71 | + alt.Tooltip("ecdf:Q", title="Proportion", format=".3f"), |
65 | 72 | ], |
66 | 73 | ) |
67 | 74 | ) |
|
93 | 100 | ) |
94 | 101 | ) |
95 | 102 |
|
| 103 | +# Text annotations at focal points for at-a-glance percentile reading without hover |
| 104 | +focal_labels = ( |
| 105 | + alt.Chart(ref_df) |
| 106 | + .mark_text(align="left", dx=8, dy=-5, fontSize=9, color=INK_SOFT, fontWeight="bold") |
| 107 | + .encode(x="latency_ms:Q", y="cumulative:Q", text="label:N") |
| 108 | +) |
| 109 | + |
96 | 110 | # Compose layers and configure theme-adaptive chrome |
97 | 111 | chart = ( |
98 | | - alt.layer(ecdf_line, h_rules, v_rules, focal_pts) |
| 112 | + alt.layer(ecdf_line, h_rules, v_rules, focal_pts, focal_labels) |
99 | 113 | .interactive() |
100 | 114 | .properties( |
101 | 115 | width=620, |
|
114 | 128 | titleFontSize=12, |
115 | 129 | ) |
116 | 130 | .configure_axisX(grid=False) |
117 | | - .configure_axisY(gridColor=INK, gridOpacity=0.10) |
| 131 | + .configure_axisY(gridColor=INK, gridOpacity=0.13) |
118 | 132 | .configure_title(color=INK) |
119 | 133 | ) |
120 | 134 |
|
|
0 commit comments