From e4ba763cab5417965d4655eaf3387d61bf62a878 Mon Sep 17 00:00:00 2001 From: Sriman Selvakumaran Date: Fri, 17 Jul 2026 13:42:44 -0700 Subject: [PATCH 1/3] docs: add interactive XY SDF plot grid --- docs/app/pyproject.toml | 1 + docs/app/tests/test_docs_site.py | 91 +++- docs/app/uv.lock | 133 ++++- .../xy_docs/demos/instrument_sans_density.py | 183 ------- docs/app/xy_docs/demos/xy_sdf_plots.py | 493 ++++++++++++++++++ docs/index.md | 25 +- 6 files changed, 713 insertions(+), 213 deletions(-) delete mode 100644 docs/app/xy_docs/demos/instrument_sans_density.py create mode 100644 docs/app/xy_docs/demos/xy_sdf_plots.py diff --git a/docs/app/pyproject.toml b/docs/app/pyproject.toml index 995aca2..a0d8bdf 100644 --- a/docs/app/pyproject.toml +++ b/docs/app/pyproject.toml @@ -6,6 +6,7 @@ requires-python = ">=3.11" dependencies = [ "numpy", "pillow", + "scipy", "reflex @ git+https://github.com/reflex-dev/reflex@carlos/reusable-docs-components", "reflex-components-internal @ git+https://github.com/reflex-dev/reflex@carlos/reusable-docs-components#subdirectory=packages/reflex-components-internal", "reflex-docgen @ git+https://github.com/reflex-dev/reflex@carlos/reusable-docs-components#subdirectory=packages/reflex-docgen", diff --git a/docs/app/tests/test_docs_site.py b/docs/app/tests/test_docs_site.py index 9447609..cac66fe 100644 --- a/docs/app/tests/test_docs_site.py +++ b/docs/app/tests/test_docs_site.py @@ -32,6 +32,7 @@ ) from reflex_site_shared.docs import render_markdown from reflex_site_shared.docs.content import discover_docs + from rxconfig import config from xy_docs.api_reference import ( AXES_AND_ANNOTATIONS, @@ -230,31 +231,35 @@ def test_docs_app_configures_the_reflex_xy_adapter() -> None: assert any(isinstance(plugin, reflex_xy.XYPlugin) for plugin in config.plugins) -def test_what_is_xy_shows_density_hero_without_inline_code() -> None: - """Keep the signature example visible without exposing its long source.""" +def test_what_is_xy_shows_sdf_plot_grid_without_inline_code() -> None: + """Keep the four signature plots visible without exposing their source.""" content = (DOCS_ROOT / "index.md").read_text(encoding="utf-8") - demo_source = ( - DOCS_APP_ROOT / "xy_docs/demos/instrument_sans_density.py" - ).read_text(encoding="utf-8") + demo_source = (DOCS_APP_ROOT / "xy_docs/demos/xy_sdf_plots.py").read_text( + encoding="utf-8" + ) font = DOCS_APP_ROOT / "xy_docs/assets/InstrumentSans-wdth-wght.ttf" license_file = DOCS_APP_ROOT / "xy_docs/assets/OFL.txt" hero = content.index("~~~python demo-only exec") - introduction = content.index("XY is an experimental Python charting library") + introduction = content.index("xy is an experimental Python charting library") styling = content.index("**Styling uses familiar web vocabulary.**") early_alpha = content.index("### Early alpha") assert introduction < styling < hero < early_alpha assert "demo exec toggle" not in content + assert "from xy_docs.demos.xy_sdf_plots import xy_sdf_plot_grid" in content + assert "View the customizable Python source" in content + assert "sample_points: int = 50_000" in demo_source + assert "density_points: int = 1_000_000" in demo_source + assert "density_display_points: int = 250_000" in demo_source + assert "@lru_cache(maxsize=4)" in demo_source assert ( - "from xy_docs.demos.instrument_sans_density import xy_density_hero" in content + 'class_name="grid w-full grid-cols-1 gap-0 overflow-hidden md:grid-cols-2"' + in demo_source ) - assert "View the complete Python source" in content - assert "N_POINTS = 40_000" in demo_source - assert "N_INLIERS = round(N_POINTS * 0.97)" in demo_source assert "InstrumentSans-wdth-wght.ttf" in demo_source assert 'width="100%"' in demo_source - assert 'height="min(74vw, 560px)"' in demo_source + assert 'height="auto"' in demo_source assert hashlib.sha256(font.read_bytes()).hexdigest() == ( "b24f1812584816958afcf22e22d08e44318c5e51651e25d2438efdde389b33b1" ) @@ -263,11 +268,45 @@ def test_what_is_xy_shows_density_hero_without_inline_code() -> None: ) -def test_density_hero_toolbar_follows_reflex_color_mode() -> None: - """Theme the hero toolbar from the host site's adaptive Radix tokens.""" - from xy_docs.demos.instrument_sans_density import xy_density_hero +def test_sdf_plot_grid_is_cached_and_uses_reflex_toolbar_tokens() -> None: + """Build once while keeping every chart toolbar tied to Reflex tokens.""" + from dataclasses import replace - rendered = str(xy_density_hero()) + from xy_docs.demos.xy_sdf_plots import ( + DEFAULT_CONFIG, + build_sdf_plots, + xy_sdf_plot_grid, + ) + + config = replace( + DEFAULT_CONFIG, + font_size=180, + x_height_bins=12, + sample_points=1_000, + density_points=2_000, + density_display_points=1_500, + bin_min_count=2, + heatmap_stride=4, + contour_cells_per_bin=4, + ) + first = build_sdf_plots(config) + assert build_sdf_plots(config) is first + assert first.reading_order == ( + first.bins_scatter, + first.heatmap, + first.contours, + first.million_scatter, + ) + density_mark = first.million_scatter.children[0] + assert density_mark.kind == "scatter" + assert len(density_mark.x) == config.density_display_points + assert density_mark.props["density"] is False + sizes = density_mark.props["size"] + assert density_mark.props["size_range"] == (float(sizes.min()), float(sizes.max())) + assert sizes.min() >= config.density_size_offset + assert sizes.max() <= config.density_size_offset + config.density_size_scale + + rendered = str(xy_sdf_plot_grid(config)) expected_tokens = { "--chart-modebar-bg": "var(--secondary-2)", "--chart-modebar-active": "var(--primary-a4)", @@ -276,7 +315,27 @@ def test_density_hero_toolbar_follows_reflex_color_mode() -> None: } for name, value in expected_tokens.items(): - assert f'["{name}"] : "{value}"' in rendered + assert rendered.count(f'["{name}"] : "{value}"') == 4 + assert 'id:"xy-sdf-plot-grid"' in rendered + assert "gap-0" in rendered + + +def test_sdf_heatmap_outside_support_uses_page_background() -> None: + """Use the page surface outside support without changing the PDF ramp.""" + from xy_docs.demos import xy_sdf_plots as demo + + colors = demo._heatmap_colors( + demo.np.array([[0.0, 1e-12, 1.0]]), + demo.DEFAULT_CONFIG, + demo.DEFAULT_PALETTE, + ) + background = demo.DEFAULT_PALETTE.dark_background.removeprefix("#") + expected = [int(background[index : index + 2], 16) / 255 for index in (0, 2, 4)] + assert demo.DEFAULT_PALETTE.dark_background == "#090A0B" + assert colors[0, 0, :3] == pytest.approx(expected) + assert colors[0, 1, :3] != pytest.approx(expected) + assert colors[0, 2, :3] != pytest.approx(expected) + assert demo.np.allclose(colors[..., 3], 1) def test_public_docs_use_the_xy_namespace_without_the_legacy_alias() -> None: diff --git a/docs/app/uv.lock b/docs/app/uv.lock index 612f72c..807eb32 100644 --- a/docs/app/uv.lock +++ b/docs/app/uv.lock @@ -1395,6 +1395,134 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/3e/630b222b816fdde6721e9cc8d63985483e349ae26b0dba32d68f75c12765/ruff_format-0.5.2-cp38-abi3-win_arm64.whl", hash = "sha256:e51e89568ac0d216f8a57942fbf3059d802a4bd4baa8c6ba7f9b36eb76d16806", size = 1950501, upload-time = "2026-04-22T22:28:59.12Z" }, ] +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + [[package]] name = "simple-websocket" version = "1.1.0" @@ -1690,7 +1818,7 @@ wheels = [ [[package]] name = "xy" -version = "0.1.0" +version = "0.0.1" source = { editable = "../../" } dependencies = [ { name = "anywidget" }, @@ -1725,6 +1853,8 @@ dependencies = [ { name = "reflex-docgen" }, { name = "reflex-site-shared" }, { name = "reflex-xy" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "xy" }, ] @@ -1745,6 +1875,7 @@ requires-dist = [ { name = "reflex-docgen", git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-docgen&rev=carlos%2Freusable-docs-components" }, { name = "reflex-site-shared", git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-site-shared&rev=carlos%2Freusable-docs-components" }, { name = "reflex-xy", editable = "../../python/reflex-xy" }, + { name = "scipy" }, { name = "xy", editable = "../../" }, ] diff --git a/docs/app/xy_docs/demos/instrument_sans_density.py b/docs/app/xy_docs/demos/instrument_sans_density.py deleted file mode 100644 index 54ac448..0000000 --- a/docs/app/xy_docs/demos/instrument_sans_density.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Build the Instrument Sans point-density hero for the XY overview.""" - -from importlib.resources import files - -import numpy as np -import reflex_xy -import xy -from PIL import Image, ImageDraw, ImageFont - -N_POINTS = 40_000 -N_INLIERS = round(N_POINTS * 0.97) -_RNG = np.random.default_rng(42) -_FONT = ImageFont.truetype( - str(files("xy_docs").joinpath("assets/InstrumentSans-wdth-wght.ttf")), - 720, -) - - -def _glyph_mask(letter, x): - image = Image.new("L", (1600, 1100)) - ImageDraw.Draw(image).text((x, 760), letter, font=_FONT, fill=255, anchor="ls") - return np.asarray(image, dtype=float) / 255, image.getbbox() - - -_x_mask, _x_box = _glyph_mask("x", 140) -_y_mask, _y_box = _glyph_mask( - "y", - 140 + _FONT.getlength("xy") - _FONT.getlength("y"), -) -_glyph_mask_xy = np.maximum(_x_mask, _y_mask) -_rows, _columns = np.nonzero(_glyph_mask_xy) -_left, _top, _right, _bottom = ( - _columns.min(), - _rows.min(), - _columns.max() + 1, - _rows.max() + 1, -) -_pixels_per_unit = (_x_box[3] - _x_box[1]) / 16 - - -def _inside(mask, points): - pixels = np.rint( - np.array([_left, _bottom]) + points * [_pixels_per_unit, -_pixels_per_unit] - ).astype(int) - valid = ( - (pixels[:, 0] >= 0) - & (pixels[:, 0] < mask.shape[1]) - & (pixels[:, 1] >= 0) - & (pixels[:, 1] < mask.shape[0]) - ) - result = np.zeros(len(points), dtype=bool) - result[valid] = mask[pixels[valid, 1], pixels[valid, 0]] >= 0.06 - return result - - -def _sample_glyph(mask, box, count, correlation): - x0, y0, x1, y1 = box - bounds = np.array([ - (x0 - _left) / _pixels_per_unit, - (_bottom - y1) / _pixels_per_unit, - (x1 - _left) / _pixels_per_unit, - (_bottom - y0) / _pixels_per_unit, - ]) - center = bounds.reshape(2, 2).mean(axis=0) - width, height = bounds[2:] - bounds[:2] - sx, sy = width * 0.42, height * 0.44 - covariance = [ - [sx**2, correlation * sx * sy], - [correlation * sx * sy, sy**2], - ] - - accepted = [] - accepted_count = 0 - while accepted_count < count: - batch = max(2048, (count - accepted_count) * 4) - candidates = _RNG.multivariate_normal(center, covariance, batch) - kept = candidates[_inside(mask, candidates)] - accepted.append(kept) - accepted_count += len(kept) - return np.concatenate(accepted)[:count] - - -_n_x = round(N_INLIERS * 0.48) -_inliers = np.vstack(( - _sample_glyph(_x_mask, _x_box, _n_x, 0.08), - _sample_glyph(_y_mask, _y_box, N_INLIERS - _n_x, -0.10), -)) -_xlim = (-3.5, (_right - _left) / _pixels_per_unit + 3.5) -_ylim = (-3.5, (_bottom - _top) / _pixels_per_unit + 3.5) - -_outlier_batches = [] -_outlier_count = 0 -while _outlier_count < N_POINTS - N_INLIERS: - _batch = max(1024, (N_POINTS - N_INLIERS - _outlier_count) * 5) - _source = _RNG.integers(0, N_INLIERS, _batch) - _spread = _RNG.choice([0.65, 1.45, 2.75], _batch, p=[0.55, 0.32, 0.13]) - _spread *= np.sqrt(0.60) - _candidates = np.column_stack(( - _inliers[_source, 0] + _RNG.normal(0, _spread), - _inliers[_source, 1] + _RNG.normal(0, _spread), - )) - _keep = ~_inside(_glyph_mask_xy, _candidates) - _keep &= np.all( - (_candidates >= [_xlim[0], _ylim[0]]) & (_candidates <= [_xlim[1], _ylim[1]]), - axis=1, - ) - _outlier_batches.append(_candidates[_keep]) - _outlier_count += _keep.sum() - -_outliers = np.concatenate(_outlier_batches)[: N_POINTS - N_INLIERS] -_points = np.vstack((_inliers, _outliers))[_RNG.permutation(N_POINTS)] - -_x_edges = np.arange(np.floor(_xlim[0]), np.ceil(_xlim[1]) + 1) -_y_edges = np.arange(np.floor(_ylim[0]), np.ceil(_ylim[1]) + 1) -_density = np.histogram2d( - _inliers[:, 0], - _inliers[:, 1], - bins=(_x_edges, _y_edges), -)[0].T -_density = np.where(_density >= 8, _density, np.nan) - -_stops = np.array([ - [0.06, 0.055, 0.07], - [0.10, 0.075, 0.13], - [0.17, 0.12, 0.27], - [0.26, 0.18, 0.43], - [0.34, 0.25, 0.62], - [0.431, 0.337, 0.812], - [0.61, 0.53, 0.91], -]) -_finite = np.isfinite(_density) -_lo, _hi = np.nanmin(_density), np.nanpercentile(_density, 98) -_scaled = np.clip((_density - _lo) / (_hi - _lo), 0, 1) ** 1.25 -_position = np.nan_to_num(_scaled) * (len(_stops) - 1) -_lower = np.floor(_position).astype(int) -_upper = np.minimum(_lower + 1, len(_stops) - 1) -_mix = (_position - _lower)[..., None] -_rgb = _stops[_lower] * (1 - _mix) + _stops[_upper] * _mix -_rgba = np.concatenate( - (_rgb, np.where(_finite, 0.96, 0)[..., None]), - axis=-1, -) - -_axis_style = { - "grid_color": "rgba(76, 29, 149, 0.10)", - "grid_width": 1, - "axis_color": "rgba(17, 17, 19, 0.58)", - "tick_color": "rgba(17, 17, 19, 0.58)", - "tick_label_color": "#111113", -} -_chart = xy.chart( - xy.scatter(*_points.T, size=1.85, color="#111113", opacity=1), - xy.heatmap( - _rgba, - x=(_x_edges[:-1] + _x_edges[1:]) / 2, - y=(_y_edges[:-1] + _y_edges[1:]) / 2, - opacity=1, - ), - xy.x_axis(domain=_xlim, tick_count=8, style=_axis_style), - xy.y_axis(domain=_ylim, tick_count=7, style=_axis_style), - width=760, - height=560, - padding=(20, 22, 46, 54), - hover=False, - select=False, - brush=False, - crosshair=False, -) - - -def xy_density_hero(): - """Render the responsive Instrument Sans point-density hero.""" - return reflex_xy.chart( - _chart, - width="100%", - height="min(74vw, 560px)", - style={ - "--chart-modebar-bg": "var(--secondary-2)", - "--chart-modebar-active": "var(--primary-a4)", - "--chart-text": "var(--secondary-11)", - "--chart-focus": "var(--primary-9)", - }, - ) diff --git a/docs/app/xy_docs/demos/xy_sdf_plots.py b/docs/app/xy_docs/demos/xy_sdf_plots.py new file mode 100644 index 0000000..916cdbe --- /dev/null +++ b/docs/app/xy_docs/demos/xy_sdf_plots.py @@ -0,0 +1,493 @@ +"""Interactive Instrument Sans ``xy`` plots derived from one signed-distance PDF.""" + +from __future__ import annotations + +from dataclasses import dataclass +from functools import lru_cache +from importlib.resources import files + +import numpy as np +import reflex as rx +import reflex_xy +import xy +from PIL import Image, ImageDraw, ImageFont +from scipy.ndimage import distance_transform_edt + + +@dataclass(frozen=True) +class SDFPlotConfig: + """Parameters shared by all four plots.""" + + text: str = "xy" + font_size: int = 720 + x_height_bins: float = 18.0 + sample_points: int = 50_000 + density_points: int = 1_000_000 + density_display_points: int = 250_000 + core_sigma: float = 1.0 + halo_sigma: float = 3.0 + halo_weight: float = 0.125 + max_sdf: float = 10.0 + view_padding: float = 10.0 + bin_size: float = 1.0 + bin_min_count: int = 25 + bin_gradient_gamma: float = 1.25 + heatmap_gamma: float = 0.25 + heatmap_stride: int = 16 + contour_cells_per_bin: float = 13.5 + contour_high_level: float = 0.85 + contour_level_ratio: float = 0.46 + contour_width: float = 2.2 + density_size_offset: float = 1.0 + density_size_scale: float = 0.5 + chart_height: int = 560 + seed: int = 42 + + +@dataclass(frozen=True) +class SDFPalette: + """Colors shared across the light and dark plot variants.""" + + light_background: str = "#FCFCFD" + dark_background: str = "#090A0B" + point: str = "#111113" + purple: str = "#6E56CF" + bin_gradient: tuple[tuple[float, float, float], ...] = ( + (0.06, 0.055, 0.07), + (0.10, 0.075, 0.13), + (0.17, 0.12, 0.27), + (0.26, 0.18, 0.43), + (0.34, 0.25, 0.62), + (0.431, 0.337, 0.812), + (0.61, 0.53, 0.91), + ) + heatmap_gradient: tuple[tuple[float, float, float], ...] = ( + (0.035, 0.039, 0.043), + (0.055, 0.040, 0.085), + (0.10, 0.04, 0.18), + (0.24, 0.07, 0.45), + (0.48, 0.13, 0.82), + (0.73, 0.32, 1.00), + (0.90, 0.72, 1.00), + ) + + +@dataclass(frozen=True) +class SDFPlots: + """The four charts in documentation reading order.""" + + bins_scatter: xy.Chart + heatmap: xy.Chart + contours: xy.Chart + million_scatter: xy.Chart + + @property + def reading_order(self) -> tuple[xy.Chart, ...]: + """Return bins/scatter, heatmap, contours, then one-million scatter.""" + return ( + self.bins_scatter, + self.heatmap, + self.contours, + self.million_scatter, + ) + + +@dataclass(frozen=True) +class _Raster: + inside: np.ndarray + x_bbox: tuple[int, int, int, int] + xy_bbox: tuple[int, int, int, int] + pixels_per_bin: float + + +@dataclass(frozen=True) +class _Model: + raster: _Raster + pdf: np.ndarray + xlim: tuple[float, float] + ylim: tuple[float, float] + + +DEFAULT_CONFIG = SDFPlotConfig() +DEFAULT_PALETTE = SDFPalette() +_FONT_PATH = files("xy_docs").joinpath("assets/InstrumentSans-wdth-wght.ttf") +_CHART_TOKENS = { + "--chart-modebar-bg": "var(--secondary-2)", + "--chart-modebar-active": "var(--primary-a4)", + "--chart-text": "var(--secondary-11)", + "--chart-focus": "var(--primary-9)", +} + + +class _SDFPlotError(ValueError): + """Invalid demo configuration or font rasterization.""" + + @classmethod + def halo_weight(cls) -> _SDFPlotError: + return cls("halo_weight must be between 0 and 1") + + @classmethod + def positive(cls, names: list[str]) -> _SDFPlotError: + return cls(f"{', '.join(names)} must be positive") + + @classmethod + def sample_count(cls) -> _SDFPlotError: + return cls("display sample counts cannot exceed density_points") + + @classmethod + def empty_glyph(cls) -> _SDFPlotError: + return cls("Instrument Sans produced an empty glyph mask") + + +def _validate(config: SDFPlotConfig) -> None: + if not 0 <= config.halo_weight <= 1: + raise _SDFPlotError.halo_weight() + positive = { + "font_size": config.font_size, + "x_height_bins": config.x_height_bins, + "sample_points": config.sample_points, + "density_points": config.density_points, + "density_display_points": config.density_display_points, + "core_sigma": config.core_sigma, + "halo_sigma": config.halo_sigma, + "max_sdf": config.max_sdf, + "bin_size": config.bin_size, + "heatmap_stride": config.heatmap_stride, + "contour_cells_per_bin": config.contour_cells_per_bin, + "chart_height": config.chart_height, + } + invalid = [name for name, value in positive.items() if value <= 0] + if invalid: + raise _SDFPlotError.positive(invalid) + if max(config.sample_points, config.density_display_points) > config.density_points: + raise _SDFPlotError.sample_count() + + +def _bbox(mask: np.ndarray) -> tuple[int, int, int, int]: + rows, columns = np.nonzero(mask) + if not len(columns): + raise _SDFPlotError.empty_glyph() + return ( + int(columns.min()), + int(rows.min()), + int(columns.max() + 1), + int(rows.max() + 1), + ) + + +def _rasterize(config: SDFPlotConfig) -> _Raster: + font = ImageFont.truetype(str(_FONT_PATH), config.font_size) + text_bbox = font.getbbox(config.text, anchor="ls") + x_font_bbox = font.getbbox("x", anchor="ls") + pixels_per_bin = (x_font_bbox[3] - x_font_bbox[1]) / config.x_height_bins + pad = int(np.ceil((config.max_sdf + 2) * pixels_per_bin)) + left = min(text_bbox[0], x_font_bbox[0]) + top = min(text_bbox[1], x_font_bbox[1]) + right = max(text_bbox[2], x_font_bbox[2]) + bottom = max(text_bbox[3], x_font_bbox[3]) + canvas_size = (right - left + 2 * pad + 2, bottom - top + 2 * pad + 2) + origin = (pad - left, pad - top) + + xy_image = Image.new("L", canvas_size, 0) + ImageDraw.Draw(xy_image).text(origin, config.text, font=font, fill=255, anchor="ls") + inside = np.asarray(xy_image, dtype=np.float32) >= 127.5 + + x_image = Image.new("L", canvas_size, 0) + ImageDraw.Draw(x_image).text(origin, "x", font=font, fill=255, anchor="ls") + return _Raster( + inside=inside, + x_bbox=_bbox(np.asarray(x_image) >= 127.5), + xy_bbox=_bbox(inside), + pixels_per_bin=pixels_per_bin, + ) + + +def _build_model(config: SDFPlotConfig) -> _Model: + raster = _rasterize(config) + inside_distance = distance_transform_edt(raster.inside) + outside_distance = distance_transform_edt(~raster.inside) + sdf = ( + np.where( + raster.inside, + -(inside_distance - 0.5), + outside_distance - 0.5, + ) + / raster.pixels_per_bin + ) + + outside = np.maximum(sdf, 0) + core = np.exp(-0.5 * (outside / config.core_sigma) ** 2) + halo = np.exp(-0.5 * (outside / config.halo_sigma) ** 2) + pdf = np.where( + sdf <= 0, + 1.0, + (1 - config.halo_weight) * core + config.halo_weight * halo, + ) + pdf[sdf >= config.max_sdf] = 0 + + x_left = raster.x_bbox[0] - 0.5 + x_bottom = raster.x_bbox[3] - 0.5 + xy_left, xy_top, xy_right, xy_bottom = raster.xy_bbox + mask_xlim = ( + (xy_left - 0.5 - x_left) / raster.pixels_per_bin, + (xy_right - 0.5 - x_left) / raster.pixels_per_bin, + ) + mask_ylim = ( + (x_bottom - (xy_bottom - 0.5)) / raster.pixels_per_bin, + (x_bottom - (xy_top - 0.5)) / raster.pixels_per_bin, + ) + return _Model( + raster=raster, + pdf=pdf, + xlim=(mask_xlim[0] - config.view_padding, mask_xlim[1] + config.view_padding), + ylim=(mask_ylim[0] - config.view_padding, mask_ylim[1] + config.view_padding), + ) + + +def _sample(model: _Model, count: int, seed: int) -> tuple[np.ndarray, ...]: + rng = np.random.default_rng(seed) + probabilities = (model.pdf / model.pdf.sum()).ravel() + chosen = rng.choice(probabilities.size, count, replace=True, p=probabilities) + row, column = np.divmod(chosen, model.pdf.shape[1]) + raster = model.raster + x = ( + column + rng.uniform(-0.5, 0.5, count) - (raster.x_bbox[0] - 0.5) + ) / raster.pixels_per_bin + y = ( + raster.x_bbox[3] - 0.5 - row - rng.uniform(-0.5, 0.5, count) + ) / raster.pixels_per_bin + return x, y, model.pdf[row, column] + + +def _grid(model: _Model, stride: int) -> tuple[np.ndarray, ...]: + row = np.arange(0, model.pdf.shape[0], stride) + column = np.arange(0, model.pdf.shape[1], stride) + raster = model.raster + x = (column - (raster.x_bbox[0] - 0.5)) / raster.pixels_per_bin + y_descending = (raster.x_bbox[3] - 0.5 - row) / raster.pixels_per_bin + return model.pdf[np.ix_(row, column)][::-1], x, y_descending[::-1] + + +def _interpolate(values: np.ndarray, stops: np.ndarray) -> np.ndarray: + position = values * (len(stops) - 1) + lower = np.floor(position).astype(int) + upper = np.minimum(lower + 1, len(stops) - 1) + mix = (position - lower)[..., None] + return stops[lower] * (1 - mix) + stops[upper] * mix + + +def _bin_colors( + counts: np.ndarray, config: SDFPlotConfig, palette: SDFPalette +) -> np.ndarray: + finite = np.isfinite(counts) + rgba = np.zeros((*counts.shape, 4), dtype=float) + if not finite.any(): + return rgba + visible = counts[finite] + low = float(visible.min()) + high = float(np.percentile(visible, 98)) + scaled = np.zeros_like(counts, dtype=float) + scaled[finite] = np.clip((visible - low) / max(high - low, 1e-12), 0, 1) + rgba[..., :3] = _interpolate( + scaled**config.bin_gradient_gamma, + np.asarray(palette.bin_gradient), + ) + rgba[..., 3] = np.where(finite, 0.96, 0) + return rgba + + +def _heatmap_colors( + pdf: np.ndarray, config: SDFPlotConfig, palette: SDFPalette +) -> np.ndarray: + clipped = np.clip(pdf, 0, 1) + background = palette.dark_background.removeprefix("#") + background_rgb = ( + np.array([int(background[index : index + 2], 16) for index in (0, 2, 4)]) / 255 + ) + rgba = np.empty((*pdf.shape, 4), dtype=float) + rgba[..., :3] = _interpolate( + clipped**config.heatmap_gamma, + np.asarray(palette.heatmap_gradient), + ) + rgba[clipped == 0, :3] = background_rgb + rgba[..., 3] = 1 + return rgba + + +def _chart( + *marks: xy.Mark, + model: _Model, + config: SDFPlotConfig, +) -> xy.Chart: + x_span = model.xlim[1] - model.xlim[0] + y_span = model.ylim[1] - model.ylim[0] + width = round(config.chart_height * x_span / y_span) + return xy.chart( + *marks, + xy.x_axis(domain=model.xlim, tick_label_strategy="none"), + xy.y_axis(domain=model.ylim, tick_label_strategy="none"), + xy.legend(show=False), + width=width, + height=config.chart_height, + padding=(0, 0, 0, 0), + hover=False, + select=False, + brush=False, + crosshair=False, + ) + + +@lru_cache(maxsize=4) +def build_sdf_plots( + config: SDFPlotConfig = DEFAULT_CONFIG, + palette: SDFPalette = DEFAULT_PALETTE, +) -> SDFPlots: + """Compute the shared model and build every chart in one cached pass.""" + _validate(config) + model = _build_model(config) + x, y, sampled_pdf = _sample(model, config.density_points, config.seed) + sample_slice = slice(0, config.sample_points) + + x_edges = np.arange( + np.floor(model.xlim[0] / config.bin_size) * config.bin_size, + np.ceil(model.xlim[1] / config.bin_size) * config.bin_size + config.bin_size, + config.bin_size, + ) + y_edges = np.arange( + np.floor(model.ylim[0] / config.bin_size) * config.bin_size, + np.ceil(model.ylim[1] / config.bin_size) * config.bin_size + config.bin_size, + config.bin_size, + ) + counts = np.histogram2d(x[sample_slice], y[sample_slice], bins=(x_edges, y_edges))[ + 0 + ].T + visible_counts = np.where(counts >= config.bin_min_count, counts, np.nan) + bins_scatter = _chart( + xy.scatter( + x[sample_slice], + y[sample_slice], + size=1.65, + color=palette.point, + opacity=0.48, + density=False, + ), + xy.heatmap( + _bin_colors(visible_counts, config, palette), + x=(x_edges[:-1] + x_edges[1:]) / 2, + y=(y_edges[:-1] + y_edges[1:]) / 2, + ), + model=model, + config=config, + ) + + heatmap_pdf, heatmap_x, heatmap_y = _grid(model, config.heatmap_stride) + heatmap = _chart( + xy.heatmap( + _heatmap_colors(heatmap_pdf, config, palette), + x=heatmap_x, + y=heatmap_y, + ), + model=model, + config=config, + ) + + contour_stride = max( + 1, + round(model.raster.pixels_per_bin / config.contour_cells_per_bin), + ) + contour_pdf, contour_x, contour_y = _grid(model, contour_stride) + contour_levels = tuple( + config.contour_high_level * config.contour_level_ratio**index + for index in range(len(palette.bin_gradient)) + ) + contour_colors = tuple( + "#" + "".join(f"{round(channel * 255):02X}" for channel in stop) + for stop in reversed(palette.bin_gradient) + ) + contours = _chart( + *( + xy.contour( + contour_pdf, + x=contour_x, + y=contour_y, + levels=(level,), + color=color, + width=config.contour_width, + opacity=0.98, + ) + for level, color in zip(contour_levels, contour_colors, strict=True) + ), + model=model, + config=config, + ) + + display_slice = slice(0, config.density_display_points) + display_pdf = sampled_pdf[display_slice] + sizes = config.density_size_offset + config.density_size_scale * np.clip( + display_pdf, 0, 1 + ) + million_scatter = _chart( + xy.scatter( + x[display_slice], + y[display_slice], + size=sizes, + size_range=(float(sizes.min()), float(sizes.max())), + color=palette.purple, + opacity=1, + density=False, + ), + model=model, + config=config, + ) + return SDFPlots(bins_scatter, heatmap, contours, million_scatter) + + +def _responsive_chart(chart: xy.Chart, background: str) -> rx.Component: + return reflex_xy.chart( + chart, + width="100%", + height="auto", + aspect_ratio=f"{chart.width} / {chart.height}", + style={ + **_CHART_TOKENS, + "--chart-bg": background, + "background": background, + }, + ) + + +def xy_sdf_plot_grid( + config: SDFPlotConfig = DEFAULT_CONFIG, + palette: SDFPalette = DEFAULT_PALETTE, +) -> rx.Component: + """Render all four cached charts as a zero-gap responsive grid.""" + plots = build_sdf_plots(config, palette) + backgrounds = ( + palette.light_background, + palette.dark_background, + palette.dark_background, + palette.light_background, + ) + return rx.el.div( + *( + _responsive_chart(chart, background) + for chart, background in zip( + plots.reading_order, + backgrounds, + strict=True, + ) + ), + id="xy-sdf-plot-grid", + class_name="grid w-full grid-cols-1 gap-0 overflow-hidden md:grid-cols-2", + ) + + +__all__ = [ + "DEFAULT_CONFIG", + "DEFAULT_PALETTE", + "SDFPalette", + "SDFPlotConfig", + "SDFPlots", + "build_sdf_plots", + "xy_sdf_plot_grid", +] diff --git a/docs/index.md b/docs/index.md index a71b4b0..6f7e89b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,16 +1,14 @@ --- -title: What is XY? +title: What is `xy`? description: Understand XY's screen-bounded rendering and CSS-first styling model. --- -# What is XY? +# What is `xy`? -XY is an experimental Python charting library for responsive, interactive 2D +xy is an experimental Python charting library for responsive, interactive 2D visualizations. It combines a native Rust compute core, binary column transport, and a WebGL2 client with a declarative Python API that works in notebooks, -applications, and standalone exports. - -Two ideas shape the library: +applications, and standalone exports. Two ideas shape the library: - **Rendered output is bounded by the visible result.** Long ordered series are decimated and dense point clouds become fixed-resolution density surfaces, @@ -24,16 +22,17 @@ Two ideas shape the library: selectors can target pixels inside a canvas. ~~~python demo-only exec -from xy_docs.demos.instrument_sans_density import xy_density_hero +from xy_docs.demos.xy_sdf_plots import xy_sdf_plot_grid -density_demo = xy_density_hero +sdf_plots = xy_sdf_plot_grid ~~~ -This chart is generated from 40,000 points: 97% sampled inside lowercase -Instrument Sans glyphs and 3% retained as a sparse outlier halo. Dense cells -become a screen-bounded RGBA heatmap while the source points remain available. -Pillow is used only to rasterize the font mask for this example; it is not an -XY runtime dependency. [View the complete Python source](https://github.com/reflex-dev/xy/blob/main/docs/app/xy_docs/demos/instrument_sans_density.py). +All four interactive charts come from one cached signed-distance probability +field for lowercase Instrument Sans `xy`. The builder samples one million +points once, reuses the first 50,000 for the binned scatter view and 250,000 for +the final direct scatter, and shares the same PDF with the direct heatmap and +contours. Pillow and SciPy are documentation demo dependencies, not XY runtime +dependencies. [View the customizable Python source](https://github.com/reflex-dev/xy/blob/main/docs/app/xy_docs/demos/xy_sdf_plots.py). ~~~md alert warning ### Early alpha From 7c443af83e327c5d24c257488608df5935aa3ee3 Mon Sep 17 00:00:00 2001 From: Sriman Selvakumaran Date: Fri, 17 Jul 2026 13:59:15 -0700 Subject: [PATCH 2/3] remove scipy dependency --- docs/app/pyproject.toml | 1 - docs/app/tests/test_docs_site.py | 29 ++++++ docs/app/uv.lock | 131 ------------------------- docs/app/xy_docs/demos/xy_sdf_plots.py | 55 ++++++++++- docs/index.md | 4 +- uv.lock | 2 +- 6 files changed, 84 insertions(+), 138 deletions(-) diff --git a/docs/app/pyproject.toml b/docs/app/pyproject.toml index a0d8bdf..995aca2 100644 --- a/docs/app/pyproject.toml +++ b/docs/app/pyproject.toml @@ -6,7 +6,6 @@ requires-python = ">=3.11" dependencies = [ "numpy", "pillow", - "scipy", "reflex @ git+https://github.com/reflex-dev/reflex@carlos/reusable-docs-components", "reflex-components-internal @ git+https://github.com/reflex-dev/reflex@carlos/reusable-docs-components#subdirectory=packages/reflex-components-internal", "reflex-docgen @ git+https://github.com/reflex-dev/reflex@carlos/reusable-docs-components#subdirectory=packages/reflex-docgen", diff --git a/docs/app/tests/test_docs_site.py b/docs/app/tests/test_docs_site.py index cac66fe..4fb47ea 100644 --- a/docs/app/tests/test_docs_site.py +++ b/docs/app/tests/test_docs_site.py @@ -4,6 +4,7 @@ import importlib.util import inspect import re +import tomllib import xml.etree.ElementTree as ET from collections.abc import Iterator from pathlib import Path @@ -338,6 +339,34 @@ def test_sdf_heatmap_outside_support_uses_page_background() -> None: assert demo.np.allclose(colors[..., 3], 1) +def test_sdf_distance_transform_is_exact_without_scipy() -> None: + """Keep the signed-distance model exact without a heavyweight dependency.""" + from xy_docs.demos import xy_sdf_plots as demo + + mask = demo.np.array([ + [True, True, True, True, True], + [True, False, True, False, True], + [True, True, True, True, True], + [True, True, False, True, True], + ]) + zero_cells = demo.np.argwhere(~mask) + expected = demo.np.empty(mask.shape, dtype=float) + for cell in demo.np.ndindex(mask.shape): + expected[cell] = demo.np.sqrt( + demo.np.min(demo.np.sum((zero_cells - cell) ** 2, axis=1)) + ) + + assert demo.np.allclose(demo._distance_transform_edt(mask), expected) + + manifest = tomllib.loads( + (DOCS_APP_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + assert not any( + dependency.lower().startswith("scipy") + for dependency in manifest["project"]["dependencies"] + ) + + def test_public_docs_use_the_xy_namespace_without_the_legacy_alias() -> None: """Keep examples, generated references, and docs tests on the public name.""" legacy_alias = "".join(("f", "c")) diff --git a/docs/app/uv.lock b/docs/app/uv.lock index 807eb32..7a1ee01 100644 --- a/docs/app/uv.lock +++ b/docs/app/uv.lock @@ -1395,134 +1395,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/3e/630b222b816fdde6721e9cc8d63985483e349ae26b0dba32d68f75c12765/ruff_format-0.5.2-cp38-abi3-win_arm64.whl", hash = "sha256:e51e89568ac0d216f8a57942fbf3059d802a4bd4baa8c6ba7f9b36eb76d16806", size = 1950501, upload-time = "2026-04-22T22:28:59.12Z" }, ] -[[package]] -name = "scipy" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.12'", -] -dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, - { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, - { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, - { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, - { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, - { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, - { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, - { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, - { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, - { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, - { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, - { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, - { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, - { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, - { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, - { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, - { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, - { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, - { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, - { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, - { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, - { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, - { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, - { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, - { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, - { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, - { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, - { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, - { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, - { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, - { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, - { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, - { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, - { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, - { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, - { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, - { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, -] - -[[package]] -name = "scipy" -version = "1.18.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", -] -dependencies = [ - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" } }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, - { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, - { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, - { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, - { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, - { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, - { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, - { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, - { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, - { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, - { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, - { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, - { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, - { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, - { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, - { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, - { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, - { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, - { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, - { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, - { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, - { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, - { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, - { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, -] - [[package]] name = "simple-websocket" version = "1.1.0" @@ -1853,8 +1725,6 @@ dependencies = [ { name = "reflex-docgen" }, { name = "reflex-site-shared" }, { name = "reflex-xy" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "xy" }, ] @@ -1875,7 +1745,6 @@ requires-dist = [ { name = "reflex-docgen", git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-docgen&rev=carlos%2Freusable-docs-components" }, { name = "reflex-site-shared", git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-site-shared&rev=carlos%2Freusable-docs-components" }, { name = "reflex-xy", editable = "../../python/reflex-xy" }, - { name = "scipy" }, { name = "xy", editable = "../../" }, ] diff --git a/docs/app/xy_docs/demos/xy_sdf_plots.py b/docs/app/xy_docs/demos/xy_sdf_plots.py index 916cdbe..e50f1cd 100644 --- a/docs/app/xy_docs/demos/xy_sdf_plots.py +++ b/docs/app/xy_docs/demos/xy_sdf_plots.py @@ -11,7 +11,6 @@ import reflex_xy import xy from PIL import Image, ImageDraw, ImageFont -from scipy.ndimage import distance_transform_edt @dataclass(frozen=True) @@ -202,10 +201,60 @@ def _rasterize(config: SDFPlotConfig) -> _Raster: ) +def _squared_distance_transform_1d(values: np.ndarray) -> np.ndarray: + """Compute the lower envelope of squared-distance parabolas in linear time.""" + finite_sites = np.flatnonzero(np.isfinite(values)) + if not finite_sites.size: + return np.full(values.shape, np.inf) + + size = values.size + sites = np.empty(size, dtype=np.intp) + boundaries = np.empty(size + 1, dtype=float) + envelope_size = 0 + sites[0] = finite_sites[0] + boundaries[0] = -np.inf + boundaries[1] = np.inf + + for site in finite_sites[1:]: + previous = sites[envelope_size] + boundary = ( + values[site] + site * site - values[previous] - previous * previous + ) / (2 * (site - previous)) + while boundary <= boundaries[envelope_size]: + envelope_size -= 1 + previous = sites[envelope_size] + boundary = ( + values[site] + site * site - values[previous] - previous * previous + ) / (2 * (site - previous)) + envelope_size += 1 + sites[envelope_size] = site + boundaries[envelope_size] = boundary + boundaries[envelope_size + 1] = np.inf + + result = np.empty(size, dtype=float) + envelope_index = 0 + for position in range(size): + while boundaries[envelope_index + 1] < position: + envelope_index += 1 + offset = position - sites[envelope_index] + result[position] = offset * offset + values[sites[envelope_index]] + return result + + +def _distance_transform_edt(mask: np.ndarray) -> np.ndarray: + """Return exact Euclidean distances from true cells to the nearest false cell.""" + squared = np.where(mask, np.inf, 0.0) + for row in range(squared.shape[0]): + squared[row] = _squared_distance_transform_1d(squared[row]) + for column in range(squared.shape[1]): + squared[:, column] = _squared_distance_transform_1d(squared[:, column]) + return np.sqrt(squared) + + def _build_model(config: SDFPlotConfig) -> _Model: raster = _rasterize(config) - inside_distance = distance_transform_edt(raster.inside) - outside_distance = distance_transform_edt(~raster.inside) + inside_distance = _distance_transform_edt(raster.inside) + outside_distance = _distance_transform_edt(~raster.inside) sdf = ( np.where( raster.inside, diff --git a/docs/index.md b/docs/index.md index 6f7e89b..12d8ee1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,8 +31,8 @@ All four interactive charts come from one cached signed-distance probability field for lowercase Instrument Sans `xy`. The builder samples one million points once, reuses the first 50,000 for the binned scatter view and 250,000 for the final direct scatter, and shares the same PDF with the direct heatmap and -contours. Pillow and SciPy are documentation demo dependencies, not XY runtime -dependencies. [View the customizable Python source](https://github.com/reflex-dev/xy/blob/main/docs/app/xy_docs/demos/xy_sdf_plots.py). +contours. The demo's only extra dependency is Pillow; its exact Euclidean +distance transform is implemented directly with NumPy. [View the customizable Python source](https://github.com/reflex-dev/xy/blob/main/docs/app/xy_docs/demos/xy_sdf_plots.py). ~~~md alert warning ### Early alpha diff --git a/uv.lock b/uv.lock index 53cdcf6..53b9093 100644 --- a/uv.lock +++ b/uv.lock @@ -755,7 +755,7 @@ wheels = [ [[package]] name = "xy" -version = "0.1.0" +version = "0.0.1" source = { editable = "." } dependencies = [ { name = "anywidget" }, From beb161d7dc6f020643a853ba0fb8f0435fb7b436 Mon Sep 17 00:00:00 2001 From: Sriman Selvakumaran Date: Fri, 17 Jul 2026 14:38:22 -0700 Subject: [PATCH 3/3] formatting fixes --- docs/app/xy_docs/demos/xy_sdf_plots.py | 43 +++++++++----------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/docs/app/xy_docs/demos/xy_sdf_plots.py b/docs/app/xy_docs/demos/xy_sdf_plots.py index e50f1cd..f11a921 100644 --- a/docs/app/xy_docs/demos/xy_sdf_plots.py +++ b/docs/app/xy_docs/demos/xy_sdf_plots.py @@ -9,9 +9,10 @@ import numpy as np import reflex as rx import reflex_xy -import xy from PIL import Image, ImageDraw, ImageFont +import xy + @dataclass(frozen=True) class SDFPlotConfig: @@ -217,15 +218,15 @@ def _squared_distance_transform_1d(values: np.ndarray) -> np.ndarray: for site in finite_sites[1:]: previous = sites[envelope_size] - boundary = ( - values[site] + site * site - values[previous] - previous * previous - ) / (2 * (site - previous)) + boundary = (values[site] + site * site - values[previous] - previous * previous) / ( + 2 * (site - previous) + ) while boundary <= boundaries[envelope_size]: envelope_size -= 1 previous = sites[envelope_size] - boundary = ( - values[site] + site * site - values[previous] - previous * previous - ) / (2 * (site - previous)) + boundary = (values[site] + site * site - values[previous] - previous * previous) / ( + 2 * (site - previous) + ) envelope_size += 1 sites[envelope_size] = site boundaries[envelope_size] = boundary @@ -299,12 +300,8 @@ def _sample(model: _Model, count: int, seed: int) -> tuple[np.ndarray, ...]: chosen = rng.choice(probabilities.size, count, replace=True, p=probabilities) row, column = np.divmod(chosen, model.pdf.shape[1]) raster = model.raster - x = ( - column + rng.uniform(-0.5, 0.5, count) - (raster.x_bbox[0] - 0.5) - ) / raster.pixels_per_bin - y = ( - raster.x_bbox[3] - 0.5 - row - rng.uniform(-0.5, 0.5, count) - ) / raster.pixels_per_bin + x = (column + rng.uniform(-0.5, 0.5, count) - (raster.x_bbox[0] - 0.5)) / raster.pixels_per_bin + y = (raster.x_bbox[3] - 0.5 - row - rng.uniform(-0.5, 0.5, count)) / raster.pixels_per_bin return x, y, model.pdf[row, column] @@ -325,9 +322,7 @@ def _interpolate(values: np.ndarray, stops: np.ndarray) -> np.ndarray: return stops[lower] * (1 - mix) + stops[upper] * mix -def _bin_colors( - counts: np.ndarray, config: SDFPlotConfig, palette: SDFPalette -) -> np.ndarray: +def _bin_colors(counts: np.ndarray, config: SDFPlotConfig, palette: SDFPalette) -> np.ndarray: finite = np.isfinite(counts) rgba = np.zeros((*counts.shape, 4), dtype=float) if not finite.any(): @@ -345,14 +340,10 @@ def _bin_colors( return rgba -def _heatmap_colors( - pdf: np.ndarray, config: SDFPlotConfig, palette: SDFPalette -) -> np.ndarray: +def _heatmap_colors(pdf: np.ndarray, config: SDFPlotConfig, palette: SDFPalette) -> np.ndarray: clipped = np.clip(pdf, 0, 1) background = palette.dark_background.removeprefix("#") - background_rgb = ( - np.array([int(background[index : index + 2], 16) for index in (0, 2, 4)]) / 255 - ) + background_rgb = np.array([int(background[index : index + 2], 16) for index in (0, 2, 4)]) / 255 rgba = np.empty((*pdf.shape, 4), dtype=float) rgba[..., :3] = _interpolate( clipped**config.heatmap_gamma, @@ -407,9 +398,7 @@ def build_sdf_plots( np.ceil(model.ylim[1] / config.bin_size) * config.bin_size + config.bin_size, config.bin_size, ) - counts = np.histogram2d(x[sample_slice], y[sample_slice], bins=(x_edges, y_edges))[ - 0 - ].T + counts = np.histogram2d(x[sample_slice], y[sample_slice], bins=(x_edges, y_edges))[0].T visible_counts = np.where(counts >= config.bin_min_count, counts, np.nan) bins_scatter = _chart( xy.scatter( @@ -472,9 +461,7 @@ def build_sdf_plots( display_slice = slice(0, config.density_display_points) display_pdf = sampled_pdf[display_slice] - sizes = config.density_size_offset + config.density_size_scale * np.clip( - display_pdf, 0, 1 - ) + sizes = config.density_size_offset + config.density_size_scale * np.clip(display_pdf, 0, 1) million_scatter = _chart( xy.scatter( x[display_slice],