Skip to content
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ in the README).

## [Unreleased]

### Added
- Funnel charts joined the core declarative API (protocol v13):
`xy.funnel_chart(stages, values)` / `xy.funnel(...)` draw one centered
segment per stage in declared order — never sorted — with explicit
`geometry="area"|"bar"` modes, `neck="rect"|"taper"`, per-geometry segment
gaps, and a `min_width` floor that keeps zero/tiny stages visible without
touching their reported values. Conversion arithmetic (value, prior, overall
share, previous-stage conversion, drop-off; `None` over zero denominators)
rides labels with a documented inside/outside/hidden collision ladder,
hover tooltips, click events, and ordered keyboard traversal with
screen-reader announcements. Per-stage colors are a categorical channel over
the stage names (theme `palette={...}` mappings pin by stage name; legend
rows opt in via `xy.legend(...)`), and per-trace `stroke`/`stroke-width`/
opacity style compiles to all three renderers. The client draws
antialiased quads through a dedicated funnel program sharing the ribbon
fragment stage; SVG/PNG/PDF exports emit the same `_scene.funnel_quad`
geometry, pinned by golden tests.

## [0.0.5] - 2026-07-31

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/app/scripts/check_html_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
ROUTES_ROOT = APP_ROOT / ".web" / "app" / "routes"
LIVE_PREVIEW_MARKERS = ("python demo exec", "python demo-only exec")
INLINE_SVG_PREVIEW_ROUTES = {"/overview/gallery/"}
INLINE_SVG_PREVIEW_COUNT = 34
INLINE_SVG_PREVIEW_COUNT = 35
XY_PAYLOAD_PATTERN = re.compile(r'["\'](?P<url>/docs/xy/xy/[a-f0-9]+\.xyf)["\']')
XY_PAYLOAD_MAGIC = b"XYBF"
LLMS_DIRECTIVE = "For AI agents: the complete XY documentation index is at"
Expand Down
35 changes: 18 additions & 17 deletions docs/app/tests/test_docs_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,18 +1279,18 @@ def test_chart_gallery_grid_renders_every_type_as_inline_svg(
chart_section = next(
leaves for title, _landing_route, _icon, leaves in DOCS_SECTIONS if title == "Chart Gallery"
)
assert len(chart_section) == 20
assert len(chart_section) == 21
assert "XYChart" not in rendered
assert rendered.count("dangerouslySetInnerHTML") == 34
assert rendered.count("dangerouslySetInnerHTML") == 35
assert rendered.count('id:"xy-chart-gallery"') == 1
assert rendered.count("main:has(#xy-chart-gallery) > div:has(#toc-navigation)") == 1
assert rendered.count("main:has(#xy-chart-gallery) > div:has(article #xy-chart-gallery)") == 1
assert rendered.count("display: none") == 1
assert rendered.count("max-width: 88rem") == 1
assert rendered.count("2xl:grid-cols-3") == 9
assert rendered.count("aspect-[320/232]") == 34
assert rendered.count("shadow-large") == 34
assert rendered.count("transition-bg") == 34
assert rendered.count("aspect-[320/232]") == 35
assert rendered.count("shadow-large") == 35
assert rendered.count("transition-bg") == 35
Comment thread
coderabbitai[bot] marked this conversation as resolved.
assert "--gallery-preview-surface: #fff" in rendered
assert "--gallery-preview-fill: #efeaff" in rendered
assert "--gallery-preview-soft: #dccfff" in rendered
Expand All @@ -1301,9 +1301,10 @@ def test_chart_gallery_grid_renders_every_type_as_inline_svg(
assert "object-contain" not in rendered
assert "object-center" not in rendered
assert "xy-tailwind-bridge" not in rendered
assert rendered.count("size:14") == 34
assert rendered.count("size:14") == 35
assert "size:6" not in rendered
for chart_type in (
"Funnel",
"Line",
"Area",
"Step",
Expand Down Expand Up @@ -1372,7 +1373,7 @@ def test_chart_gallery_inline_svgs_share_the_component_preview_style() -> None:
for item in group.items
}

assert len(previews) == 34
assert len(previews) == 35
for svg in previews.values():
assert 'viewBox="0 0 320 232"' in svg
assert '<rect x="52" y="62" width="216" height="108" rx="12"' in svg
Expand Down Expand Up @@ -1538,7 +1539,7 @@ def test_chart_gallery_combines_only_the_requested_related_tiles() -> None:
titles = {item.title for group in _GALLERY_GROUPS for item in group.items}
section_titles = [group.title for group in _GALLERY_GROUPS]

assert len(titles) == 34
assert len(titles) == 35
assert section_titles[:3] == [
"Line and Area",
"Distributions",
Expand Down Expand Up @@ -1566,6 +1567,7 @@ def test_chart_gallery_combines_only_the_requested_related_tiles() -> None:
("Stem", "/charts/stem-plot/"),
("Segments", "/charts/segments/"),
("Sankey", "/charts/sankey/"),
("Funnel", "/charts/funnel-chart/"),
Comment thread
Alek99 marked this conversation as resolved.
("Triangle Mesh", "/components/triangle-mesh/"),
]
assert {"Step + Stairs", "Bar + Column"} <= titles
Expand Down Expand Up @@ -1828,18 +1830,17 @@ def test_inline_svg_gallery_validator_requires_every_styled_preview(tmp_path: Pa
"""Accept only the complete code-native gallery in the production route."""
module_path = tmp_path / "route.jsx"
preview = 'viewBox=\\"0 0 320 232\\"'
module_path.write_text(
preview * 34 + "gallery-preview-surface aspect-[320/232] shadow-large",
encoding="utf-8",
)
# Derived from the validator's own constant, not a second copy of the
# number: adding a gallery tile already updates that constant, and a
# hardcoded fixture here just failed the build a second time.
expected = check_html_routes.INLINE_SVG_PREVIEW_COUNT
surface = "gallery-preview-surface aspect-[320/232] shadow-large"
module_path.write_text(preview * expected + surface, encoding="utf-8")

check_html_routes.validate_inline_svg_gallery("/overview/gallery/", module_path)

module_path.write_text(
preview * 33 + "gallery-preview-surface aspect-[320/232] shadow-large",
encoding="utf-8",
)
with pytest.raises(RuntimeError, match="33 previews, expected 34"):
module_path.write_text(preview * (expected - 1) + surface, encoding="utf-8")
with pytest.raises(RuntimeError, match=f"{expected - 1} previews, expected {expected}"):
check_html_routes.validate_inline_svg_gallery("/overview/gallery/", module_path)


Expand Down
2 changes: 2 additions & 0 deletions docs/app/xy_docs/api_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
xy.stem_chart,
xy.segments_chart,
xy.sankey_chart,
xy.funnel_chart,
xy.triangle_mesh_chart,
),
),
Expand Down Expand Up @@ -114,6 +115,7 @@
xy.segments,
xy.ribbon,
xy.sankey,
xy.funnel,
xy.triangle_mesh,
)

Expand Down
1 change: 1 addition & 0 deletions docs/app/xy_docs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
("Stem", "/charts/stem-plot/"),
("Segments", "/charts/segments/"),
("Sankey", "/charts/sankey/"),
("Funnel", "/charts/funnel-chart/"),
),
),
(
Expand Down
4 changes: 4 additions & 0 deletions docs/app/xy_docs/gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ class GalleryGroup:
GalleryItem("Stem", route="/charts/stem-plot/"),
GalleryItem("Segments", route="/charts/segments/"),
GalleryItem("Sankey", route="/charts/sankey/"),
GalleryItem("Funnel", route="/charts/funnel-chart/"),
GalleryItem("Triangle Mesh", route="/components/triangle-mesh/"),
),
),
Expand Down Expand Up @@ -275,6 +276,9 @@ class GalleryGroup:
""",
"Sankey": """
<path d="M85 82C122 82 124 91 157 91V108C124 108 122 99 85 99Z" class="preview-fill"/><path d="M85 102C123 102 124 130 157 130V151C124 151 122 123 85 123Z" class="preview-fill-soft"/><path d="M174 92C207 92 208 76 239 76V97C208 97 207 113 174 113Z" class="preview-fill"/><path d="M174 118C207 118 208 125 239 125V148C208 148 207 141 174 141Z" class="preview-fill-soft"/><rect x="72" y="78" width="13" height="49" rx="3" class="preview-fill-strong"/><rect x="157" y="87" width="17" height="68" rx="3" class="preview-fill-strong"/><rect x="239" y="72" width="13" height="29" rx="3" class="preview-fill-strong"/><rect x="239" y="121" width="13" height="31" rx="3" class="preview-fill-strong"/>
""",
"Funnel": """
<path d="M76 74H244L216 98H104Z" class="preview-fill"/><path d="M106 102H214L192 126H128Z" class="preview-fill-strong"/><path d="M130 130H190L178 154H142Z" class="preview-fill-soft"/><path d="M148 158H172V174H148Z" class="preview-fill"/><path d="M138 86H182M144 112H176M150 140H170" class="preview-label-line"/>
""",
"Triangle Mesh": """
<path d="M72 147L105 87L143 139L175 73L214 121L249 82L262 151H72Z" class="preview-fill"/><path d="M72 147L105 87L143 139L175 73L214 121L249 82L262 151M72 147L143 139L214 121L262 151M105 87L175 73L249 82M105 87L143 139L175 73L214 121L249 82" class="preview-line preview-line-soft"/>
Expand Down
2 changes: 1 addition & 1 deletion docs/app/xy_docs/sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"shapes",
tuple(
(title, _chart_gallery_routes[title])
for title in ("Uncertainty", "Stem", "Segments", "Sankey")
for title in ("Uncertainty", "Stem", "Segments", "Sankey", "Funnel")
),
),
)
Expand Down
Loading
Loading