{"spec_id":"ecdf-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\necdf-basic: Basic ECDF Plot\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-25\n\"\"\"\n\nimport os\nimport sys\n\n\n# The file is named altair.py; remove its own directory from sys.path so\n# `import altair` resolves to the library, not this script.\n_HERE = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if not p or os.path.abspath(p) != _HERE]\nos.chdir(_HERE)  # saves (plot-*.png, plot-*.html) land in the implementations dir\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nBRAND = \"#009E73\"  # Imprint palette position 1 — always first series\n\n# Data: API response latency from a production web service\nnp.random.seed(42)\nresponse_times_ms = np.random.normal(loc=120, scale=35, size=250)\nresponse_times_ms = np.clip(response_times_ms, 20, None)\n\n# Raw data frame — ECDF computed declaratively via Vega-Lite window transform\ndf = pd.DataFrame({\"latency_ms\": response_times_ms})\n\n# Reference values at quartiles for focal emphasis and text annotations\np25_ms = float(np.percentile(response_times_ms, 25))\np50_ms = float(np.median(response_times_ms))\np75_ms = float(np.percentile(response_times_ms, 75))\nref_df = pd.DataFrame(\n    {\n        \"latency_ms\": [p25_ms, p50_ms, p75_ms],\n        \"cumulative\": [0.25, 0.50, 0.75],\n        \"label\": [f\"~{p25_ms:.0f}ms\", f\"~{p50_ms:.0f}ms\", f\"~{p75_ms:.0f}ms\"],\n    }\n)\n\n# Title\ntitle_str = \"ecdf-basic · python · altair · anyplot.ai\"\n\n# ECDF step function — cume_dist() window transform computes the ECDF declaratively\n# in Vega-Lite without numpy preprocessing; step-after gives the correct step shape\necdf_line = (\n    alt.Chart(df)\n    .transform_window(ecdf=\"cume_dist()\", sort=[alt.SortField(\"latency_ms\")])\n    .mark_line(interpolate=\"step-after\", strokeWidth=3.5, color=BRAND)\n    .encode(\n        x=alt.X(\"latency_ms:Q\", title=\"API Response Time (ms)\", scale=alt.Scale(nice=True)),\n        y=alt.Y(\n            \"ecdf:Q\",\n            title=\"Cumulative Proportion\",\n            scale=alt.Scale(domain=[0, 1]),\n            axis=alt.Axis(format=\".0%\", tickCount=11),\n        ),\n        tooltip=[\n            alt.Tooltip(\"latency_ms:Q\", title=\"Latency (ms)\", format=\".1f\"),\n            alt.Tooltip(\"ecdf:Q\", title=\"Proportion\", format=\".3f\"),\n        ],\n    )\n)\n\n# Dashed reference lines spanning the full axes at Q1, median, Q3\nh_rules = (\n    alt.Chart(ref_df)\n    .mark_rule(strokeDash=[5, 4], strokeWidth=1.5, color=INK_MUTED, opacity=0.75)\n    .encode(y=\"cumulative:Q\")\n)\n\nv_rules = (\n    alt.Chart(ref_df)\n    .mark_rule(strokeDash=[5, 4], strokeWidth=1.5, color=INK_MUTED, opacity=0.75)\n    .encode(x=\"latency_ms:Q\")\n)\n\n# Focal markers at quartile intersections on the ECDF\nfocal_pts = (\n    alt.Chart(ref_df)\n    .mark_point(size=120, filled=True, color=BRAND, opacity=1.0)\n    .encode(\n        x=\"latency_ms:Q\",\n        y=\"cumulative:Q\",\n        tooltip=[\n            alt.Tooltip(\"latency_ms:Q\", title=\"Latency (ms)\", format=\".1f\"),\n            alt.Tooltip(\"cumulative:Q\", title=\"Quartile\", format=\".0%\"),\n        ],\n    )\n)\n\n# Text annotations at focal points for at-a-glance percentile reading without hover\nfocal_labels = (\n    alt.Chart(ref_df)\n    .mark_text(align=\"left\", dx=8, dy=-5, fontSize=9, color=INK_SOFT, fontWeight=\"bold\")\n    .encode(x=\"latency_ms:Q\", y=\"cumulative:Q\", text=\"label:N\")\n)\n\n# Compose layers and configure theme-adaptive chrome\nchart = (\n    alt.layer(ecdf_line, h_rules, v_rules, focal_pts, focal_labels)\n    .interactive()\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n        title=alt.Title(title_str, fontSize=16, color=INK),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0, continuousWidth=620, continuousHeight=320)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=12,\n    )\n    .configure_axisX(grid=False)\n    .configure_axisY(gridColor=INK, gridOpacity=0.13)\n    .configure_title(color=INK)\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n\n# Canvas: pad to exactly 3200×1800 with PAGE_BG (vl-convert inner-view padding lands short)\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n"}