diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fb833f..4f2a276 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,25 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version-file: webview/.nvmrc + cache: npm + cache-dependency-path: webview/package-lock.json - name: Setup Python Env uses: ./.github/actions/setup-python-env with: uv-extra-groups: ci + - name: Install frontend dependencies + run: npm ci --prefix webview + - name: Check frontend quality + run: | + npm --prefix webview run typecheck + npm --prefix webview run lint + npm --prefix webview run format:check + npm --prefix webview test + npm --prefix webview run build - name: Check formatting run: | source .venv/bin/activate @@ -39,10 +54,26 @@ jobs: steps: - name: Check out Git repository uses: actions/checkout@v7 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version-file: webview/.nvmrc + cache: npm + cache-dependency-path: webview/package-lock.json - name: Setup Python Env uses: ./.github/actions/setup-python-env with: uv-extra-groups: ci + - name: Install frontend dependencies + run: npm ci --prefix webview + - name: Build frontend assets + run: npm --prefix webview run build + - name: Install browser for end-to-end tests + run: npm --prefix webview run playwright:install + - name: Run browser tests + run: npm --prefix webview run test:e2e + - name: Build Python distributions + run: uv build - name: Run tests run: | source .venv/bin/activate diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e27f9c5..b834331 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -exclude: ^proflow/tests/snapshots/|^experiments/ +exclude: ^proflow/tests/snapshots/|^experiments/|(^|/)(package\.json|pnpm-lock\.yaml|tsconfig\.json)$ repos: diff --git a/AGENTS.md b/AGENTS.md index cea9722..b595171 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,3 +76,8 @@ When responding, explicitly confirm completion of each requirement. ## Testing - Use `.venv/bin/python -m pytest` for running all tests in the project. + +## Webview specific instructions + +- Use nvm to manage Node.js versions for the webview project. +- Use pnpm to manage project dependencies for the webview project. diff --git a/CHANGELOG.md b/CHANGELOG.md index ecbca50..8cce380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,188 @@ # Changelog +## 2026-11-05 + +- Date: 2026-11-05 +- Summary: Restore graph zoom and pan controls in the relocated webview frontend. +- Added: + - Mouse/touch zoom, canvas panning, bounded zoom controls, and a reset button. +- Changed: + - Graph content shares an SVG transform while controls remain fixed and node + dragging continues to work when zoomed. +- Fixed: + - Restored navigation behavior lost during the frontend directory refactor. +- Docs: + - Documented navigation controls +- Tests: + - Frontend typecheck, unit tests, and zoom/pan browser coverage passed. +- Lint/Format: + - Passed ESLint and Prettier checks. +- Credits: Copilot SDK in VS Code. + +## 2026-11-04 + +- Date: 2026-11-04 +- Summary: Isolate the process graph webview in a compiled Node package. +- Added: + - A private TypeScript/Vite ES-module frontend package with a locked npm + dependency tree. + - Vitest unit tests, Playwright browser tests, and Flask route/distribution + tests. +- Changed: + - The Flask host now serves compiled frontend assets while continuing to + provide graph data through `/data`. + - CI runs frontend checks and verifies compiled assets in Python + distributions. +- Fixed: + - Frontend graph requests and invalid graph responses now surface accessible + errors instead of failing silently. +- Docs: + - Documented frontend development, tests, build output, and Python runtime + boundaries. +- Tests: + - 123 Python tests passed and 7 were skipped; 9 Vitest unit tests and 3 + Playwright browser tests passed. + - Verified compiled frontend assets are present in both wheel and source + distributions. +- Lint/Format: + - Passed ESLint, Prettier, Ruff lint, and Ruff formatting checks. +- Credits: Copilot SDK in VS Code. + +## 2026-11-04 + +- Date: 2026-11-04 +- Summary: Add zooming and panning to the analysis web view. +- Added: + - Mouse/touch zoom, canvas panning, and accessible zoom-in, zoom-out, and + reset buttons. +- Changed: + - Graph elements share a transformed SVG group while controls stay fixed. +- Fixed: + - None. +- Docs: + - Documented graph navigation controls and gestures in the analysis guide. +- Tests: + - Browser-verified button and wheel zoom, zoom bounds, reset, canvas panning, + and stationary controls. +- Lint/Format: + - Not applicable to JavaScript, CSS, and Markdown changes. +- Credits: Copilot SDK in VS Code. + +## 2026-11-04 + +- Date: 2026-11-04 +- Summary: Add a control-panel toggle for the graph forces. +- Added: + - An accessible checkbox to pause and resume the force-directed layout. +- Changed: + - The control-panel heading now describes the graph controls. + - Dragging nodes while forces are off moves them without restarting the + simulation. +- Fixed: + - None. +- Docs: + - Documented the force toggle in the process analysis guide. +- Tests: + - Verified force simulation pause/resume and node dragging in the browser. +- Lint/Format: + - Passed the configured pre-commit checks. +- Credits: Copilot SDK in VS Code. + +## 2026-11-04 + +- Date: 2026-11-04 +- Summary: Render web-view links as perpendicular Bézier curves. +- Added: + - Cubic Bézier paths for process-network links. +- Changed: + - Box links leave the source's right edge and meet the target's left edge + horizontally. +- Fixed: + - Link paths no longer meet box edges at an angle. +- Docs: + - Documented perpendicular Bézier links in the process analysis guide. +- Tests: + - Verified rendered box-link endpoints and horizontal endpoint tangents in the browser. +- Lint/Format: + - Not applicable to JavaScript and Markdown changes. +- Credits: Copilot SDK in VS Code. + +## 2026-11-04 + +- Date: 2026-11-04 +- Summary: Keep node labels visible above their shapes in the web view. +- Added: + - None. +- Changed: + - Node shapes are inserted before labels in SVG rendering order. +- Fixed: + - Rectangle nodes no longer cover their labels. +- Docs: + - Clarified that labels remain visible in both node-shape modes. +- Tests: + - Verified node SVG stacking and visible labels in the browser. +- Lint/Format: + - Not applicable to JavaScript and Markdown changes. +- Credits: Copilot SDK in VS Code. + +## 2026-11-03 + +- Date: 2026-11-03 +- Summary: Add a control-panel switch for box and dot graph nodes. +- Added: + - Accessible radio controls for selecting the node shape. +- Changed: + - The graph redraws node shapes, labels, and link endpoints when the selection + changes. +- Fixed: + - None. +- Docs: + - Documented the node-shape switch in the process analysis guide. +- Tests: + - Verified switching between boxes and dots, plus their link endpoints, in + the browser. +- Lint/Format: + - Not applicable to JavaScript, CSS, and Markdown changes. +- Credits: Copilot SDK in VS Code. + +## 2026-11-03 + +- Date: 2026-11-03 +- Summary: Render web-view process nodes as boxes with directional edge ports. +- Added: + - Rectangular process nodes with centered labels. +- Changed: + - Incoming links connect to the left side of a node and outgoing links leave + from its right side. +- Fixed: + - None. +- Docs: + - Documented the box and edge direction in the process analysis guide. +- Tests: + - Verified rendered node shapes and directional link endpoints in the browser. +- Lint/Format: + - Not applicable to JavaScript, CSS, and Markdown changes. +- Credits: Copilot SDK in VS Code. + +## 2026-11-03 + +- Date: 2026-11-03 +- Summary: Add node search controls to the process network web view. +- Added: + - An accessible node search panel with a live count of visible nodes. +- Changed: + - The web view hides nonmatching nodes and links connected to hidden nodes. +- Fixed: + - None. +- Docs: + - Documented how to run the web view and use its node filter. +- Tests: + - Verified case-insensitive name matching, ID matching, edge filtering, and clearing the search in the browser. + - Project tests passed: 116 passed, 7 skipped. +- Lint/Format: + - Ruff checks and formatting checks passed for the related Python modules. +- Credits: Copilot SDK in VS Code. + ## 2026-11-03 - Date: 2026-11-03 diff --git a/README.md b/README.md index d8b84f5..2d13e12 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,7 @@ inputs and outputs connect to the surrounding data: `I(value, as_="argument_name")` entries; the `as_` value becomes the corresponding keyword argument to the function. - **Outputs** are provided by `state_outputs`. The callable receives the - function result and returns `(value, "target.path")` pairs to write into - state. + function result and returns `(value, "target.path")` pairs to write int state. - **Other input sources** include `config_inputs`, `parameters_inputs`, `external_state_inputs`, and `additional_inputs`. - **Process options** include `gate` to skip a process, `comment` and `group` @@ -114,3 +113,11 @@ ruff format . --check ## License proFlow is distributed under the terms of the [MIT License](./LICENSE). + +## Build and Publish + +The publish workflow builds the distributions and publishes them to PyPI using trusted publishing. + +Use `build_and_deploy.sh patch|minor|major` to bump the version and trigger the publish workflow. + +Configure a PyPI trusted publisher for this GitHub repository with workflow filename `publish.yml` and environment `pypi` before pushing the tag. No PyPI API token is needed. diff --git a/proflow/analysis/README.md b/proflow/analysis/README.md index 064770e..bd2cf8c 100644 --- a/proflow/analysis/README.md +++ b/proflow/analysis/README.md @@ -122,6 +122,73 @@ though an `Edge` record's `source` field identifies the consumer and its (`target == -1`) are omitted from the exported graph. The function returns the same node-link data it writes. +## Explore a network in the web view + +The optional Flask web view displays the process network as an interactive +force-directed graph. Install Flask if needed, then run it from the project root: + +```bash +python -m pip install flask +python -m proflow.analysis.webview.run_app +``` + +Open the printed local URL and use the **Search nodes** field to match node IDs, +function names, or process descriptions without regard to case. The graph hides +nonmatching nodes and links that connect to hidden nodes; clearing the search +restores the full network. Use **Enable forces** to pause or resume the +force-directed layout, and **Node shape** to switch between labeled boxes (the +default) and dots. Dragging nodes remains available while forces are paused. +Use the mouse wheel or a touch gesture to zoom, and drag empty canvas to pan. +The **Zoom in**, **Zoom out**, and **Reset** buttons provide equivalent controls; +reset returns to the original view. Zoom is bounded so the graph cannot become +arbitrarily small or large. Panning and zooming do not interfere with dragging +nodes. +In box mode, incoming links connect to the left edge and outgoing links leave +the right edge, with cubic Bézier curves that leave and meet the boxes +perpendicularly. Node labels remain visible in both modes. + +### Develop the webview frontend + +The frontend is a private Node ES-module package in +[`proflow_webview`](../../proflow_webview). +It uses TypeScript for strict type checking, Vite to build, Vitest for unit +tests, and Playwright for browser tests. The Python package serves the compiled +assets and provides graph data at `/data`; Node is needed only to develop and +build the frontend, not to run an installed Python package. + +Use Node 22 or newer and install frontend dependencies from the repository root: + +```bash +pnpm --dir proflow_webview install --frozen-lockfile +``` + +For frontend development, start the Flask server in one terminal and Vite in +another. Vite proxies `/data` to the Flask server on port 8000: + +```bash +uv sync --locked --group web +uv run --locked python -m proflow.analysis.webview.run_app +pnpm --dir proflow_webview dev +``` + +Run the frontend checks and build compiled assets for the Flask server: + +```bash +pnpm --dir proflow_webview typecheck +pnpm --dir proflow_webview lint +pnpm --dir proflow_webview format:check +pnpm --dir proflow_webview test +pnpm --dir proflow_webview build +``` + +The build output is stored in +`proflow/analysis/webview/static/force/frontend-dist` and included in Python +source and binary distributions. The Playwright suite starts the Flask server +automatically; install Chromium first with +`pnpm --dir proflow_webview playwright:install`, then run +`pnpm --dir proflow_webview test:e2e`. Python route tests are included in the +project pytest suite. + ## Dependencies The canvas workflow requires `ipycanvas`. JSON export requires `networkx`. diff --git a/proflow/analysis/export.py b/proflow/analysis/export.py new file mode 100644 index 0000000..b19aa91 --- /dev/null +++ b/proflow/analysis/export.py @@ -0,0 +1,37 @@ +"""Export process analysis results to the force webview JSON format.""" + +from dataclasses import asdict + +from proflow.analysis.network.extract_nodes_and_edges import Edge, Node + + +def processes_to_json( + nodes_and_edges: tuple[list[Node], list[Edge]], +) -> dict[str, object]: + """Write process nodes and edges as force webview node-link JSON. + + The returned graph data is also written to ``output_file``. Unresolved + edges, whose ``target`` is ``-1``, are omitted from the links. + """ + nodes, edges = nodes_and_edges + graph_data: dict[str, object] = { + "directed": True, + "multigraph": True, + "graph": {}, + "nodes": [{**asdict(node), "id": node.index} for node in nodes], + "links": [ + { + "source": edge.target, + "target": edge.source, + "name": edge.name, + "key": 0, + "id": link_id, + } + for link_id, edge in enumerate( + (edge for edge in edges if edge.target != -1), + start=1, + ) + ], + } + + return graph_data diff --git a/proflow/analysis/network/extract_nodes_and_edges_test.py b/proflow/analysis/network/extract_nodes_and_edges_test.py index 9e097b9..df95cba 100644 --- a/proflow/analysis/network/extract_nodes_and_edges_test.py +++ b/proflow/analysis/network/extract_nodes_and_edges_test.py @@ -1,4 +1,4 @@ -from proflow.tests.demodata import DEMO_PROCESSES +from proflow.tests.mock_data import DEMO_PROCESSES from .extract_nodes_and_edges import ( Edge, diff --git a/proflow/analysis/webview/README.md b/proflow/analysis/webview/README.md new file mode 100644 index 0000000..9bafb1d --- /dev/null +++ b/proflow/analysis/webview/README.md @@ -0,0 +1,8 @@ +# ProFlow Webview Analysis + +This directory contains the webview analysis components for ProFlow, including the handling and validation of graph data used within the webview. + +## Static JS assets + +The static JavaScript assets required for the webview are located in this directory. These assets include scripts for rendering and interacting with the graph data within the webview. +They are created from the proflow_webview source files, which are then processed and optimized for use within the webview environment. diff --git a/proflow/analysis/webview/demo_data_generation.py b/proflow/analysis/webview/demo_data_generation.py new file mode 100644 index 0000000..833a914 --- /dev/null +++ b/proflow/analysis/webview/demo_data_generation.py @@ -0,0 +1,96 @@ +# %% +import json + +from proflow.analysis.export import processes_to_json +from proflow.analysis.network.extract_nodes_and_edges import processes_to_nodes_and_edges +from proflow.analysis.webview.run_app import run +from proflow.objects.interface import I +from proflow.objects.process import Process + +DEMO_PROCESSES = [ + Process( + func=lambda x, y, z: x + y + z, + comment="Demo process a", + config_inputs=lambda config: [ + I(config.foo.bar, as_="x"), + ], + state_inputs=lambda state: [I(state.info.today, as_="y"), I(state.info.hour, as_="z")], + state_outputs=lambda result: [ + (result, "info.tomorrow"), + ], + ), + Process( + func=lambda x, y: x + y, + comment="Demo process b", + config_inputs=lambda config: [ + I(config.foo.bar, as_="x"), + ], + state_inputs=lambda state: [ + I(state.info.tomorrow, as_="y"), + ], + state_outputs=lambda result: [ + (result, "info.today"), + ], + ), + Process( + func=lambda x, y: x + y, + comment="Demo process c", + config_inputs=lambda config: [ + I(config.foo.bar, as_="x"), + ], + state_inputs=lambda state: [ + I(state.info.tomorrow, as_="y"), + ], + state_outputs=lambda result: [ + (result, "info.today"), + ], + ), + Process( + func=lambda x, y: x + y, + comment="Demo process d", + config_inputs=lambda config: [ + I(config.foo.bar, as_="x"), + ], + state_inputs=lambda state: [ + I(state.info.today, as_="y"), + ], + state_outputs=lambda result: [ + (result, "info.tomorrow"), + ], + ), +] + + +def DEMO_FUNC(x, y, z): + return x + y + z + + +DEMO_PROCESS = Process( + func=DEMO_FUNC, + comment="This is the process comment", + gate=True, + config_inputs=lambda config: [ + I(config.a, as_="x"), + ], + state_inputs=lambda state: [ + I(state.a, as_="y"), + ], + additional_inputs=lambda: [ + I(10, as_="z"), + ], + state_outputs=lambda result: [ + (result, "x"), + ], +) + + +graph_data = processes_to_json(processes_to_nodes_and_edges(DEMO_PROCESSES)) + +# %% +with open("proflow/analysis/webview/static/force/data.json", "w") as output: + json.dump(graph_data, output, indent=4) + output.write("\n") + +# %% +run(port=8002, datalocation="proflow/analysis/webview/static/force/data.json") +# %% diff --git a/proflow/analysis/webview/run_app.py b/proflow/analysis/webview/run_app.py index febfbb8..167a2ef 100644 --- a/proflow/analysis/webview/run_app.py +++ b/proflow/analysis/webview/run_app.py @@ -1,23 +1,67 @@ +"""Serve compiled process-network assets and graph data with Flask.""" + +import json +import os +from pathlib import Path + import flask +WEBVIEW_STATIC_FOLDER = Path(__file__).resolve().parent / "static" +DEFAULT_DATA_LOCATION = WEBVIEW_STATIC_FOLDER / "force" / "data.json" + + +def create_app(datalocation: str | Path = DEFAULT_DATA_LOCATION) -> flask.Flask: + """Create the Flask app that hosts compiled assets and graph JSON. -def run(port=8000, datalocation="force/data.json"): - app = flask.Flask(__name__, static_folder="static") + Args: + datalocation: JSON file containing the exported process graph. + + Returns: + The configured Flask app. + """ + app = flask.Flask(__name__, static_folder=str(WEBVIEW_STATIC_FOLDER)) @app.route("/") - def static_proxy(): - return app.send_static_file("force/force.html") + def root() -> flask.Response: + """Serve the Vite-built webview entry point.""" + return app.send_static_file("force/frontend-dist/index.html") @app.route("/force/") - def static_proxy_b(filename): + def static_force_assets(filename: str) -> flask.Response: + """Serve compiled JavaScript, CSS, and other webview assets.""" return app.send_static_file("force/" + filename) - @app.route("/data/") - def static_proxy_c(text): - return app.send_static_file(datalocation) + @app.route("/data") + def data_file() -> tuple[flask.Response, int] | flask.Response: + """Read and return the configured graph JSON document.""" + try: + with Path(datalocation).open(encoding="utf-8") as data_file_handle: + data = json.load(data_file_handle) + except FileNotFoundError: + app.logger.error("Graph data file not found: %s", datalocation) + return flask.jsonify({"error": "Graph data file not found"}), 404 + except json.JSONDecodeError: + app.logger.exception("Graph data file contains invalid JSON: %s", datalocation) + return flask.jsonify({"error": "Graph data file is invalid"}), 500 + except OSError: + app.logger.exception("Unable to read graph data file: %s", datalocation) + return flask.jsonify({"error": "Graph data file could not be read"}), 500 + return flask.jsonify(data) + + return app + + +def run(port: int = 8000, datalocation: str | Path = DEFAULT_DATA_LOCATION) -> None: + """Start the local webview server on the requested port. + Args: + port: Local TCP port for the Flask development server. + datalocation: JSON file containing the graph displayed by the webview. + """ + app = create_app(datalocation) print(f"\nGo to http://localhost:{port} to see the example\n") app.run(port=port) -run() +if __name__ == "__main__": + run(port=int(os.environ.get("PROFLOW_WEBVIEW_PORT", "8000"))) diff --git a/proflow/analysis/webview/run_app_test.py b/proflow/analysis/webview/run_app_test.py new file mode 100644 index 0000000..ffd9a48 --- /dev/null +++ b/proflow/analysis/webview/run_app_test.py @@ -0,0 +1,102 @@ +"""Tests for the Flask adapter that serves the compiled process webview.""" + +import json +import tarfile +import zipfile +from pathlib import Path + +import pytest + +flask = pytest.importorskip("flask") + +from proflow.analysis.webview.run_app import create_app + +FRONTEND_DIRECTORY = Path("proflow/analysis/webview/static/force/frontend-dist") + + +def frontend_asset_paths() -> set[str]: + """Return repository-relative paths for all generated frontend assets.""" + return {path.as_posix() for path in FRONTEND_DIRECTORY.rglob("*") if path.is_file()} + + +def test_root_serves_compiled_frontend() -> None: + """The root route serves the generated frontend entry point.""" + client = create_app().test_client() + + response = client.get("/") + + assert response.status_code == 200 + assert b"Process Network" in response.data + assert b"/assets/" in response.data + + +def test_frontend_assets_are_served() -> None: + """The Flask adapter serves generated JavaScript and stylesheet assets.""" + client = create_app().test_client() + html = client.get("/").get_data(as_text=True) + asset_path = html.split('src="', 1)[1].split('"', 1)[0] + + response = client.get(asset_path) + + assert response.status_code == 200 + assert response.mimetype == "text/javascript" + assert response.data + + +def test_data_route_returns_graph_json(tmp_path) -> None: + """The data route returns the configured graph JSON document.""" + graph_data = {"nodes": [], "links": []} + data_file = tmp_path / "graph.json" + data_file.write_text(json.dumps(graph_data), encoding="utf-8") + client = create_app(datalocation=data_file).test_client() + + response = client.get("/data") + + assert response.status_code == 200 + assert response.get_json() == graph_data + + +def test_data_route_reports_missing_file(tmp_path) -> None: + """The data route reports a missing graph file as a not-found response.""" + client = create_app(datalocation=tmp_path / "missing.json").test_client() + + response = client.get("/data") + + assert response.status_code == 404 + assert response.get_json() == {"error": "Graph data file not found"} + + +def test_data_route_reports_invalid_json(tmp_path) -> None: + """The data route reports malformed JSON as a server error.""" + data_file = tmp_path / "invalid.json" + data_file.write_text("{", encoding="utf-8") + client = create_app(datalocation=data_file).test_client() + + response = client.get("/data") + + assert response.status_code == 500 + assert response.get_json() == {"error": "Graph data file is invalid"} + + +def test_wheel_contains_compiled_frontend_assets() -> None: + """The built Python wheel contains the frontend files used by Flask.""" + wheel_files = list(Path("dist").glob("*.whl")) + if not wheel_files: + pytest.skip("Run `uv build` before testing wheel contents.") + + with zipfile.ZipFile(wheel_files[0]) as wheel: + assert frontend_asset_paths() <= set(wheel.namelist()) + + +def test_source_distribution_contains_compiled_frontend_assets() -> None: + """The source archive contains the compiled frontend files used by Flask.""" + source_archives = list(Path("dist").glob("*.tar.gz")) + if not source_archives: + pytest.skip("Run `uv build` before testing source distribution contents.") + + with tarfile.open(source_archives[0], "r:gz") as source_archive: + archive_paths = {member.name for member in source_archive.getmembers()} + assert all( + any(member_path.endswith(asset_path) for member_path in archive_paths) + for asset_path in frontend_asset_paths() + ) diff --git a/proflow/analysis/webview/static/force/data.json b/proflow/analysis/webview/static/force/data.json index b2d53d4..27bfe45 100644 --- a/proflow/analysis/webview/static/force/data.json +++ b/proflow/analysis/webview/static/force/data.json @@ -34,73 +34,29 @@ "name": "", "text": "Demo process d", "id": 3 - }, - { - "index": 4, - "x": 3, - "y": 0, - "name": "", - "text": "Demo process e", - "id": 4 - }, - { - "index": 5, - "x": 3, - "y": 0, - "name": "", - "text": "Demo process f", - "id": 5 } ], "links": [ { "source": 0, "target": 1, - "name": "info.tomorrow", + "name": "state.info.tomorrow", "key": 0, "id": 1 }, { "source": 0, "target": 2, - "name": "info.tomorrow", + "name": "state.info.tomorrow", "key": 0, "id": 2 }, - { - "source": 1, - "target": 4, - "name": "info.other", - "key": 0, - "id": 3 - }, { "source": 2, "target": 3, - "name": "info.today", + "name": "state.info.today", "key": 0, - "id": 4 - }, - { - "source": 2, - "target": 4, - "name": "info.other", - "key": 0, - "id": 5 - }, - { - "source": 2, - "target": 4, - "name": "info.other.extra", - "key": 0, - "id": 6 - }, - { - "source": 4, - "target": 5, - "name": "info.other", - "key": 0, - "id": 7 + "id": 3 } ] } diff --git a/proflow/analysis/webview/static/force/force.css b/proflow/analysis/webview/static/force/force.css deleted file mode 100644 index f0d5bbb..0000000 --- a/proflow/analysis/webview/static/force/force.css +++ /dev/null @@ -1,21 +0,0 @@ -.nodes circle { - cursor: pointer; - fill: #3ace15; - stroke: #000; - stroke-width: .5px; -} - -.links line{ - fill: none; - stroke: #9ecae1; - stroke-width: .5px; -} - -.pathInvis { - stroke: none; - fill: none; -} - -.link { stroke: #999; stroke-opacity: .6; stroke-width: 1px; fill: none} -.linkPerp { stroke: #999; stroke-opacity: .6; stroke-width: 1px; } -/* .linkPerp { stroke: #999; stroke-opacity: .6; stroke-width: 1px; } */ diff --git a/proflow/analysis/webview/static/force/force.html b/proflow/analysis/webview/static/force/force.html deleted file mode 100644 index e74633e..0000000 --- a/proflow/analysis/webview/static/force/force.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - Proflow Process Runner Network Analysis - - - - - - - - - - - - - - - - diff --git a/proflow/analysis/webview/static/force/force.js b/proflow/analysis/webview/static/force/force.js deleted file mode 100644 index 377a748..0000000 --- a/proflow/analysis/webview/static/force/force.js +++ /dev/null @@ -1,223 +0,0 @@ -/* force directed graph with direction arrows and multiple edges - -References - -http://jsfiddle.net/zhanghuancs/a2QpA/ -https://bl.ocks.org/mattkohl/146d301c0fc20d89d85880df537de7b0 -http://bl.ocks.org/fancellu/2c782394602a93921faff74e594d1bb1 -*/ - - -function sortLinks(lks) { - lks.sort(function (a, b) { - if (a.source > b.source) { - return 1; - } - else if (a.source < b.source) { - return -1; - } - else { - if (a.target > b.target) { - return 1; - } - if (a.target < b.target) { - return -1; - } - else { - return 0; - } - } - }); -} - -var mLinkNum = {}; -function setLinkIndexAndNum(linksData) { - //any links with duplicate source and target get an incremented 'linknum' - for (var i = 0; i < linksData.length; i++) { - if (i != 0 && - linksData[i].source == linksData[i - 1].source && - linksData[i].target == linksData[i - 1].target) { - linksData[i].linkindex = linksData[i - 1].linkindex + 1; - } - else { - linksData[i].linkindex = 1; - } - // save the total number of links between two nodes - if (mLinkNum[linksData[i].target + "," + linksData[i].source] !== undefined) { - mLinkNum[linksData[i].target + "," + linksData[i].source] = linksData[i].linkindex; - } - else { - mLinkNum[linksData[i].source + "," + linksData[i].target] = linksData[i].linkindex; - } - } -} - -var getSiblingLinks = function (source, target) { - var siblings = []; - for (var i = 0; i < mLinkNum.length; ++i) { - if ((mLinkNum[i].source == source && mLinkNum[i].target == target) || (mLinkNum[i].source == target && mLinkNum[i].target == source)) - siblings.push(mLinkNum[i].id); - }; - return siblings; -}; -function arcPath(leftHand, d) { - // var isOddLink = d.linkindex % 2 === 0; - var x1 = (leftHand) ? d.source.x : d.target.x, - y1 = (leftHand) ? d.source.y : d.target.y, - x2 = (leftHand) ? d.target.x : d.source.x, - y2 = (leftHand) ? d.target.y : d.source.y, - dx = x2 - x1, - dy = y2 - y1, - dr = Math.sqrt(dx * dx + dy * dy); - - var sweep = (leftHand) ? 0 : 1; - - xRotation = 0, - largeArc = 0; - - var dx = d.target.x - d.source.x, - dy = d.target.y - d.source.y, - dr = Math.sqrt(dx * dx + dy * dy); - - // get the total link numbers between source and target node - var siblingCount = mLinkNum[d.source.id + "," + d.target.id] || mLinkNum[d.target.id + "," + d.source.id]; - if (siblingCount > 1) { - // if there are multiple links between these two nodes, we need generate different dr for each path - dr = dr / (1 + (1 / (siblingCount)) * (d.linkindex - 1 - sweep)); - } - - return "M" + x1 + "," + y1 + "A" + dr + ", " + dr + " " + xRotation + ", " + largeArc + ", " + sweep + " " + x2 + "," + y2; -} - -var colors = d3.scaleOrdinal(d3.schemeCategory10); - -var svg = d3.select("svg"), - width = +svg.attr("width"), - height = +svg.attr("height"), - node, - link, - pathInvis; - -svg.append('defs') - .append('marker') - .attrs({ - 'id': 'arrowhead', - 'viewBox': '-0 -5 10 10', - 'refX': 13, - 'refY': 0, - 'orient': 'auto', - 'markerWidth': 13, - 'markerHeight': 13, - 'xoverflow': 'visible' - }) - .append('svg:path') - .attr('d', 'M 0,-5 L 10 ,0 L 0,5') - .attr('fill', '#999') - .style('stroke', 'none'); - - -var simulation = d3.forceSimulation() - .force("link", d3.forceLink().id(function (d) { return d.id; }).distance(500).strength(0.5)) - .force("charge", d3.forceManyBody()) - .force("center", d3.forceCenter(width / 2, height / 2)); - - -d3.json("/data/force.json", function (error, graph) { - // d3.json("force/force.json", function (error, graph) { - if (error) throw error; - var linksData = graph.links - sortLinks(linksData) - setLinkIndexAndNum(linksData) - update(linksData, graph.nodes); -}); - -function update(links, nodes) { - link = svg.selectAll(".link") - .data(links) - .enter() - // .append("line") - .append("svg:path") - .attr("class", "link") - .attr('marker-end', 'url(#arrowhead)') - - link.append("title") - .text(function (d) { return d.name; }); - - pathInvis = svg.selectAll(".pathInvis") - .data(links) - .enter() - // .append("line") - .append("svg:path") - .attr("id", function (d) { - return 'invis_' + d.source + "-" + d.linkindex + "-" + d.target; - }) - .attr("class", "pathInvis") - - var pathLabel = svg.selectAll(".pathLabel") - .data(links) - .enter() - // .append("g") - .append("svg:text") - .attr("class", "pathLabel") - .attr("text-anchor", "middle") - .append("svg:textPath") - .attr("startOffset", "50%") - .attr("xlink:href", function (d) { - return '#invis_' + d.source + "-" + d.linkindex + "-" + d.target;; - }) - .style("fill", "#cccccc") - .style("font-size", 10) - .text(function (d) { return d.name; }); - node = svg.selectAll(".node") - .data(nodes) - .enter() - .append("g") - .attr("class", "node") - .call(d3.drag() - .on("start", dragstarted) - .on("drag", dragged) - //.on("end", dragended) - ); - - node.append("circle") - .attr("r", 5) - .style("fill", function (d, i) { return colors(i); }) - - node.append("title") - .text(function (d) { return d.id; }); - - node.append("text") - .attr("dy", -3) - .text(function (d) { return d.name + ":" + d.text; }); - - simulation - .nodes(nodes) - .on("tick", ticked); - - simulation.force("link") - .links(links); -} - - -function ticked() { - link.attr("d", function (d) { - return arcPath(true, d); - }); - - pathInvis.attr("d", function (d) { - return arcPath(d.source.x < d.target.x, d); - }); - - node.attr("transform", function (d) { return "translate(" + d.x + ", " + d.y + ")"; }); -} - -function dragstarted(d) { - if (!d3.event.active) simulation.alphaTarget(0.02).restart() - d.fx = d.x; - d.fy = d.y; -} - -function dragged(d) { - d.fx = d3.event.x; - d.fy = d3.event.y; -} diff --git a/proflow/analysis/webview/static/force/frontend-dist/assets/index-BojTQZrE.css b/proflow/analysis/webview/static/force/frontend-dist/assets/index-BojTQZrE.css new file mode 100644 index 0000000..78aa49e --- /dev/null +++ b/proflow/analysis/webview/static/force/frontend-dist/assets/index-BojTQZrE.css @@ -0,0 +1 @@ +html,body{width:100%;height:100%;margin:0}body{overflow:hidden;font-family:system-ui,sans-serif}svg{display:block;width:100%;height:100%}.control-panel{position:fixed;z-index:1;top:16px;left:16px;width:min(320px,calc(100vw - 64px));padding:16px;border:1px solid #d5d9de;border-radius:8px;background:#fff;box-shadow:0 2px 10px #00000024}.control-panel h1{margin:0 0 12px;font-size:16px}.control-panel label{display:block;margin-bottom:6px;font-size:13px;font-weight:600}.control-panel input[type=search]{box-sizing:border-box;width:100%;padding:9px 10px;border:1px solid #aeb5bd;border-radius:4px;font:inherit}.zoom-controls,.node-style-options,.force-options{margin:14px 0 0;padding:0;border:0}.zoom-controls legend,.node-style-options legend,.force-options legend{margin-bottom:6px;font-size:13px;font-weight:600}.zoom-control-buttons{display:flex;gap:8px}.zoom-control-buttons button{flex:1;padding:6px 8px;border:1px solid #aeb5bd;border-radius:4px;background:#f8fafc;color:inherit;cursor:pointer;font:inherit}.node-style-options label,.force-options label{display:inline-flex;align-items:center;gap:6px;margin:0 12px 0 0;font-weight:400}.node-style-options input[type=radio],.force-options input[type=checkbox]{width:auto;margin:0;padding:0}.control-panel input:focus-visible,.control-panel button:focus-visible{outline:2px solid #2563eb;outline-offset:2px}.control-panel p{margin:10px 0 0;color:#555;font-size:12px}#webview-error:not([hidden]){color:#b42318}.node rect,.node circle{cursor:pointer;stroke:#000;stroke-width:.5px}.node text{font-size:12px;pointer-events:none}.pathInvis{fill:none;stroke:none}.link{fill:none;stroke:#999;stroke-opacity:.6;stroke-width:1px} diff --git a/proflow/analysis/webview/static/force/frontend-dist/assets/index-DWYGgMTr.js b/proflow/analysis/webview/static/force/frontend-dist/assets/index-DWYGgMTr.js new file mode 100644 index 0000000..1edb003 --- /dev/null +++ b/proflow/analysis/webview/static/force/frontend-dist/assets/index-DWYGgMTr.js @@ -0,0 +1 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const f of document.querySelectorAll('link[rel="modulepreload"]'))r(f);new MutationObserver(f=>{for(const a of f)if(a.type==="childList")for(const i of a.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(f){const a={};return f.integrity&&(a.integrity=f.integrity),f.referrerPolicy&&(a.referrerPolicy=f.referrerPolicy),f.crossOrigin==="use-credentials"?a.credentials="include":f.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(f){if(f.ep)return;f.ep=!0;const a=n(f);fetch(f.href,a)}})();class St extends Error{constructor(t){super(t),this.name="GraphDataError"}}function zt(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function ze(e){return typeof e=="number"&&Number.isInteger(e)}function ur(e){if(!zt(e)||!Array.isArray(e.nodes)||!Array.isArray(e.links))throw new St("Graph data must contain node and link arrays.");return{nodes:e.nodes,links:e.links}}function dr(e,t){if(!zt(e)||!ze(e.id)||typeof e.x!="number"||!Number.isFinite(e.x)||typeof e.y!="number"||!Number.isFinite(e.y)||typeof e.name!="string"||typeof e.text!="string")throw new St(`Graph node at index ${t} is invalid.`)}function lr(e,t,n){if(!zt(e)||!ze(e.source)||!ze(e.target)||!n.has(e.source)||!n.has(e.target)||typeof e.name!="string"||!ze(e.key)||!ze(e.id))throw new St(`Graph link at index ${t} has an invalid or unknown node endpoint.`)}function hr(e){return e.nodes.map((t,n)=>(dr(t,n),{id:t.id,x:t.x,y:t.y,name:t.name,text:t.text}))}function br(e,t){return e.links.map((n,r)=>(lr(n,r,t),{source:n.source,target:n.target,name:n.name,key:n.key,id:n.id}))}function pr(e){const t=ur(e),n=hr(t),r=new Set(n.map(a=>a.id)),f=br(t,r);return{nodes:n,links:f}}async function gr(e="/data",t=fetch){const n=await t(e);if(!n.ok)throw new Error(`Graph data request failed with status ${n.status}.`);return pr(await n.json())}function We(e,t){return e==null||t==null?NaN:et?1:e>=t?0:NaN}function yr(e,t){return e==null||t==null?NaN:te?1:t>=e?0:NaN}function Mn(e){let t,n,r;e.length!==2?(t=We,n=(c,o)=>We(e(c),o),r=(c,o)=>e(c)-o):(t=e===We||e===yr?e:mr,n=e,r=e);function f(c,o,s=0,u=c.length){if(s>>1;n(c[l],o)<0?s=l+1:u=l}while(s>>1;n(c[l],o)<=0?s=l+1:u=l}while(ss&&r(c[l-1],o)>-r(c[l],o)?l-1:l}return{left:f,center:i,right:a}}function mr(){return 0}function vr(e){return e===null?NaN:+e}Mn(We);Mn(vr).center;class Kt extends Map{constructor(t,n=_r){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),t!=null)for(const[r,f]of t)this.set(r,f)}get(t){return super.get(Wt(this,t))}has(t){return super.has(Wt(this,t))}set(t,n){return super.set(xr(this,t),n)}delete(t){return super.delete(wr(this,t))}}function Wt({_intern:e,_key:t},n){const r=t(n);return e.has(r)?e.get(r):n}function xr({_intern:e,_key:t},n){const r=t(n);return e.has(r)?e.get(r):(e.set(r,n),n)}function wr({_intern:e,_key:t},n){const r=t(n);return e.has(r)&&(n=e.get(r),e.delete(r)),n}function _r(e){return e!==null&&typeof e=="object"?e.valueOf():e}var Nr={value:()=>{}};function Xe(){for(var e=0,t=arguments.length,n={},r;e=0&&(r=n.slice(f+1),n=n.slice(0,f)),n&&!t.hasOwnProperty(n))throw new Error("unknown type: "+n);return{type:n,name:r}})}Qe.prototype=Xe.prototype={constructor:Qe,on:function(e,t){var n=this._,r=Ar(e+"",n),f,a=-1,i=r.length;if(arguments.length<2){for(;++a0)for(var n=new Array(f),r=0,f,a;r=0&&(t=e.slice(0,n))!=="xmlns"&&(e=e.slice(n+1)),Zt.hasOwnProperty(t)?{space:Zt[t],local:e}:e}function $r(e){return function(){var t=this.ownerDocument,n=this.namespaceURI;return n===wt&&t.documentElement.namespaceURI===wt?t.createElement(e):t.createElementNS(n,e)}}function Er(e){return function(){return this.ownerDocument.createElementNS(e.space,e.local)}}function $n(e){var t=ct(e);return(t.local?Er:$r)(t)}function kr(){}function Tt(e){return e==null?kr:function(){return this.querySelector(e)}}function Sr(e){typeof e!="function"&&(e=Tt(e));for(var t=this._groups,n=t.length,r=new Array(n),f=0;f=$&&($=x+1);!(A=y[$])&&++$=0;)(i=r[f])&&(a&&i.compareDocumentPosition(a)^4&&a.parentNode.insertBefore(i,a),a=i);return this}function ef(e){e||(e=tf);function t(l,p){return l&&p?e(l.__data__,p.__data__):!l-!p}for(var n=this._groups,r=n.length,f=new Array(r),a=0;at?1:e>=t?0:NaN}function nf(){var e=arguments[0];return arguments[0]=this,e.apply(null,arguments),this}function rf(){return Array.from(this)}function ff(){for(var e=this._groups,t=0,n=e.length;t1?this.each((t==null?gf:typeof t=="function"?mf:yf)(e,t,n??"")):_e(this.node(),e)}function _e(e,t){return e.style.getPropertyValue(t)||Tn(e).getComputedStyle(e,null).getPropertyValue(t)}function xf(e){return function(){delete this[e]}}function wf(e,t){return function(){this[e]=t}}function _f(e,t){return function(){var n=t.apply(this,arguments);n==null?delete this[e]:this[e]=n}}function Nf(e,t){return arguments.length>1?this.each((t==null?xf:typeof t=="function"?_f:wf)(e,t)):this.node()[e]}function In(e){return e.trim().split(/^|\s+/)}function It(e){return e.classList||new Cn(e)}function Cn(e){this._node=e,this._names=In(e.getAttribute("class")||"")}Cn.prototype={add:function(e){var t=this._names.indexOf(e);t<0&&(this._names.push(e),this._node.setAttribute("class",this._names.join(" ")))},remove:function(e){var t=this._names.indexOf(e);t>=0&&(this._names.splice(t,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(e){return this._names.indexOf(e)>=0}};function Dn(e,t){for(var n=It(e),r=-1,f=t.length;++r=0&&(n=t.slice(r+1),t=t.slice(0,r)),{type:t,name:n}})}function Zf(e){return function(){var t=this.__on;if(t){for(var n=0,r=-1,f=t.length,a;n()=>e;function _t(e,{sourceEvent:t,subject:n,target:r,identifier:f,active:a,x:i,y:c,dx:o,dy:s,dispatch:u}){Object.defineProperties(this,{type:{value:e,enumerable:!0,configurable:!0},sourceEvent:{value:t,enumerable:!0,configurable:!0},subject:{value:n,enumerable:!0,configurable:!0},target:{value:r,enumerable:!0,configurable:!0},identifier:{value:f,enumerable:!0,configurable:!0},active:{value:a,enumerable:!0,configurable:!0},x:{value:i,enumerable:!0,configurable:!0},y:{value:c,enumerable:!0,configurable:!0},dx:{value:o,enumerable:!0,configurable:!0},dy:{value:s,enumerable:!0,configurable:!0},_:{value:u}})}_t.prototype.on=function(){var e=this._.on.apply(this._,arguments);return e===this._?this:e};function oa(e){return!e.ctrlKey&&!e.button}function sa(){return this.parentNode}function ua(e,t){return t??{x:e.x,y:e.y}}function da(){return navigator.maxTouchPoints||"ontouchstart"in this}function la(){var e=oa,t=sa,n=ua,r=da,f={},a=Xe("start","drag","end"),i=0,c,o,s,u,l=0;function p(b){b.on("mousedown.drag",h).filter(r).on("touchstart.drag",y).on("touchmove.drag",g,ca).on("touchend.drag touchcancel.drag",x).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function h(b,A){if(!(u||!e.call(this,b,A))){var E=$(this,t.call(this,b,A),b,A,"mouse");E&&(J(b.view).on("mousemove.drag",w,De).on("mouseup.drag",m,De),Ln(b.view),ht(b),s=!1,c=b.clientX,o=b.clientY,E("start",b))}}function w(b){if(me(b),!s){var A=b.clientX-c,E=b.clientY-o;s=A*A+E*E>l}f.mouse("drag",b)}function m(b){J(b.view).on("mousemove.drag mouseup.drag",null),On(b.view,s),me(b),f.mouse("end",b)}function y(b,A){if(e.call(this,b,A)){var E=b.changedTouches,z=t.call(this,b,A),C=E.length,q,O;for(q=0;q>8&15|t>>4&240,t>>4&15|t&240,(t&15)<<4|t&15,1):n===8?He(t>>24&255,t>>16&255,t>>8&255,(t&255)/255):n===4?He(t>>12&15|t>>8&240,t>>8&15|t>>4&240,t>>4&15|t&240,((t&15)<<4|t&15)/255):null):(t=ba.exec(e))?new F(t[1],t[2],t[3],1):(t=pa.exec(e))?new F(t[1]*255/100,t[2]*255/100,t[3]*255/100,1):(t=ga.exec(e))?He(t[1],t[2],t[3],t[4]):(t=ya.exec(e))?He(t[1]*255/100,t[2]*255/100,t[3]*255/100,t[4]):(t=ma.exec(e))?an(t[1],t[2]/100,t[3]/100,1):(t=va.exec(e))?an(t[1],t[2]/100,t[3]/100,t[4]):jt.hasOwnProperty(e)?nn(jt[e]):e==="transparent"?new F(NaN,NaN,NaN,0):null}function nn(e){return new F(e>>16&255,e>>8&255,e&255,1)}function He(e,t,n,r){return r<=0&&(e=t=n=NaN),new F(e,t,n,r)}function Ct(e){return e instanceof de||(e=Pe(e)),e?(e=e.rgb(),new F(e.r,e.g,e.b,e.opacity)):new F}function qe(e,t,n,r){return arguments.length===1?Ct(e):new F(e,t,n,r??1)}function F(e,t,n,r){this.r=+e,this.g=+t,this.b=+n,this.opacity=+r}Ee(F,qe,Ye(de,{brighter(e){return e=e==null?Ne:Math.pow(Ne,e),new F(this.r*e,this.g*e,this.b*e,this.opacity)},darker(e){return e=e==null?pe:Math.pow(pe,e),new F(this.r*e,this.g*e,this.b*e,this.opacity)},rgb(){return this},clamp(){return new F(he(this.r),he(this.g),he(this.b),tt(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:rn,formatHex:rn,formatHex8:_a,formatRgb:fn,toString:fn}));function rn(){return`#${le(this.r)}${le(this.g)}${le(this.b)}`}function _a(){return`#${le(this.r)}${le(this.g)}${le(this.b)}${le((isNaN(this.opacity)?1:this.opacity)*255)}`}function fn(){const e=tt(this.opacity);return`${e===1?"rgb(":"rgba("}${he(this.r)}, ${he(this.g)}, ${he(this.b)}${e===1?")":`, ${e})`}`}function tt(e){return isNaN(e)?1:Math.max(0,Math.min(1,e))}function he(e){return Math.max(0,Math.min(255,Math.round(e)||0))}function le(e){return e=he(e),(e<16?"0":"")+e.toString(16)}function an(e,t,n,r){return r<=0?e=t=n=NaN:n<=0||n>=1?e=t=NaN:t<=0&&(e=NaN),new W(e,t,n,r)}function Fn(e){if(e instanceof W)return new W(e.h,e.s,e.l,e.opacity);if(e instanceof de||(e=Pe(e)),!e)return new W;if(e instanceof W)return e;e=e.rgb();var t=e.r/255,n=e.g/255,r=e.b/255,f=Math.min(t,n,r),a=Math.max(t,n,r),i=NaN,c=a-f,o=(a+f)/2;return c?(t===a?i=(n-r)/c+(n0&&o<1?0:i,new W(i,c,o,e.opacity)}function Na(e,t,n,r){return arguments.length===1?Fn(e):new W(e,t,n,r??1)}function W(e,t,n,r){this.h=+e,this.s=+t,this.l=+n,this.opacity=+r}Ee(W,Na,Ye(de,{brighter(e){return e=e==null?Ne:Math.pow(Ne,e),new W(this.h,this.s,this.l*e,this.opacity)},darker(e){return e=e==null?pe:Math.pow(pe,e),new W(this.h,this.s,this.l*e,this.opacity)},rgb(){var e=this.h%360+(this.h<0)*360,t=isNaN(e)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*t,f=2*n-r;return new F(bt(e>=240?e-240:e+120,f,r),bt(e,f,r),bt(e<120?e+240:e-120,f,r),this.opacity)},clamp(){return new W(cn(this.h),Ve(this.s),Ve(this.l),tt(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const e=tt(this.opacity);return`${e===1?"hsl(":"hsla("}${cn(this.h)}, ${Ve(this.s)*100}%, ${Ve(this.l)*100}%${e===1?")":`, ${e})`}`}}));function cn(e){return e=(e||0)%360,e<0?e+360:e}function Ve(e){return Math.max(0,Math.min(1,e||0))}function bt(e,t,n){return(e<60?t+(n-t)*e/60:e<180?n:e<240?t+(n-t)*(240-e)/60:t)*255}const Xn=Math.PI/180,Bn=180/Math.PI,nt=18,Yn=.96422,Gn=1,Hn=.82521,Vn=4/29,xe=6/29,Un=3*xe*xe,Aa=xe*xe*xe;function Kn(e){if(e instanceof ee)return new ee(e.l,e.a,e.b,e.opacity);if(e instanceof ce)return Wn(e);e instanceof F||(e=Ct(e));var t=mt(e.r),n=mt(e.g),r=mt(e.b),f=pt((.2225045*t+.7168786*n+.0606169*r)/Gn),a,i;return t===n&&n===r?a=i=f:(a=pt((.4360747*t+.3850649*n+.1430804*r)/Yn),i=pt((.0139322*t+.0971045*n+.7141733*r)/Hn)),new ee(116*f-16,500*(a-f),200*(f-i),e.opacity)}function Ma(e,t,n,r){return arguments.length===1?Kn(e):new ee(e,t,n,r??1)}function ee(e,t,n,r){this.l=+e,this.a=+t,this.b=+n,this.opacity=+r}Ee(ee,Ma,Ye(de,{brighter(e){return new ee(this.l+nt*(e??1),this.a,this.b,this.opacity)},darker(e){return new ee(this.l-nt*(e??1),this.a,this.b,this.opacity)},rgb(){var e=(this.l+16)/116,t=isNaN(this.a)?e:e+this.a/500,n=isNaN(this.b)?e:e-this.b/200;return t=Yn*gt(t),e=Gn*gt(e),n=Hn*gt(n),new F(yt(3.1338561*t-1.6168667*e-.4906146*n),yt(-.9787684*t+1.9161415*e+.033454*n),yt(.0719453*t-.2289914*e+1.4052427*n),this.opacity)}}));function pt(e){return e>Aa?Math.pow(e,1/3):e/Un+Vn}function gt(e){return e>xe?e*e*e:Un*(e-Vn)}function yt(e){return 255*(e<=.0031308?12.92*e:1.055*Math.pow(e,1/2.4)-.055)}function mt(e){return(e/=255)<=.04045?e/12.92:Math.pow((e+.055)/1.055,2.4)}function $a(e){if(e instanceof ce)return new ce(e.h,e.c,e.l,e.opacity);if(e instanceof ee||(e=Kn(e)),e.a===0&&e.b===0)return new ce(NaN,0=1?(n=1,t-1):Math.floor(n*t),f=e[r],a=e[r+1],i=r>0?e[r-1]:2*f-a,c=r()=>e;function Zn(e,t){return function(n){return e+n*t}}function Ta(e,t,n){return e=Math.pow(e,n),t=Math.pow(t,n)-e,n=1/n,function(r){return Math.pow(e+r*t,n)}}function Ia(e,t){var n=t-e;return n?Zn(e,n>180||n<-180?n-360*Math.round(n/360):n):Pt(isNaN(e)?t:e)}function Ca(e){return(e=+e)==1?we:function(t,n){return n-t?Ta(t,n,e):Pt(isNaN(t)?n:t)}}function we(e,t){var n=t-e;return n?Zn(e,n):Pt(isNaN(e)?t:e)}var dn=(function e(t){var n=Ca(t);function r(f,a){var i=n((f=qe(f)).r,(a=qe(a)).r),c=n(f.g,a.g),o=n(f.b,a.b),s=we(f.opacity,a.opacity);return function(u){return f.r=i(u),f.g=c(u),f.b=o(u),f.opacity=s(u),f+""}}return r.gamma=e,r})(1);function Da(e){return function(t){var n=t.length,r=new Array(n),f=new Array(n),a=new Array(n),i,c;for(i=0;in&&(a=t.slice(n,a),c[i]?c[i]+=a:c[++i]=a),(r=r[0])===(f=f[0])?c[i]?c[i]+=f:c[++i]=f:(c[++i]=null,o.push({i,x:ue(r,f)})),n=vt.lastIndex;return n180?u+=360:u-s>180&&(s+=360),p.push({i:l.push(f(l)+"rotate(",null,r)-2,x:ue(s,u)})):u&&l.push(f(l)+"rotate("+u+r)}function c(s,u,l,p){s!==u?p.push({i:l.push(f(l)+"skewX(",null,r)-2,x:ue(s,u)}):u&&l.push(f(l)+"skewX("+u+r)}function o(s,u,l,p,h,w){if(s!==l||u!==p){var m=h.push(f(h)+"scale(",null,",",null,")");w.push({i:m-4,x:ue(s,l)},{i:m-2,x:ue(u,p)})}else(l!==1||p!==1)&&h.push(f(h)+"scale("+l+","+p+")")}return function(s,u){var l=[],p=[];return s=e(s),u=e(u),a(s.translateX,s.translateY,u.translateX,u.translateY,l,p),i(s.rotate,u.rotate,l,p),c(s.skewX,u.skewX,l,p),o(s.scaleX,s.scaleY,u.scaleX,u.scaleY,l,p),s=u=null,function(h){for(var w=-1,m=p.length,y;++w=0&&e._call.call(void 0,t),e=e._next;--Ae}function bn(){ge=(ft=Oe.now())+st,Ae=Te=0;try{Ka()}finally{Ae=0,Qa(),ge=0}}function Wa(){var e=Oe.now(),t=e-ft;t>tr&&(st-=t,ft=e)}function Qa(){for(var e,t=rt,n,r=1/0;t;)t._call?(r>t._time&&(r=t._time),e=t,t=t._next):(n=t._next,t._next=null,t=e?e._next=n:rt=n);Ie=e,Mt(r)}function Mt(e){if(!Ae){Te&&(Te=clearTimeout(Te));var t=e-ge;t>24?(e<1/0&&(Te=setTimeout(bn,Math.ceil(e-Oe.now()-st)+1)),ke&&(ke=clearInterval(ke))):(ke||(ft=Oe.now(),ke=setInterval(Wa,tr)),Ae=1,nr(bn))}}function pn(e,t,n){var r=new at;return t=t==null?0:+t,r.restart(f=>{r.stop(),e(f+t)},t,n),r}function Za(e){return((e*=2)<=1?e*e*e:(e-=2)*e*e+2)/2}var Ft=3;(function e(t){t=+t;function n(r){return Math.pow(r,t)}return n.exponent=e,n})(Ft);(function e(t){t=+t;function n(r){return 1-Math.pow(1-r,t)}return n.exponent=e,n})(Ft);(function e(t){t=+t;function n(r){return((r*=2)<=1?Math.pow(r,t):2-Math.pow(2-r,t))/2}return n.exponent=e,n})(Ft);function it(e){return(Math.pow(2,-10*e)-.0009765625)*1.0009775171065494}var Xt=1.70158;(function e(t){t=+t;function n(r){return(r=+r)*r*(t*(r-1)+r)}return n.overshoot=e,n})(Xt);(function e(t){t=+t;function n(r){return--r*r*((r+1)*t+r)+1}return n.overshoot=e,n})(Xt);(function e(t){t=+t;function n(r){return((r*=2)<1?r*r*((t+1)*r-t):(r-=2)*r*((t+1)*r+t)+2)/2}return n.overshoot=e,n})(Xt);var Me=2*Math.PI,Bt=1,Yt=.3;(function e(t,n){var r=Math.asin(1/(t=Math.max(1,t)))*(n/=Me);function f(a){return t*it(- --a)*Math.sin((r-a)/n)}return f.amplitude=function(a){return e(a,n*Me)},f.period=function(a){return e(t,a)},f})(Bt,Yt);(function e(t,n){var r=Math.asin(1/(t=Math.max(1,t)))*(n/=Me);function f(a){return 1-t*it(a=+a)*Math.sin((a+r)/n)}return f.amplitude=function(a){return e(a,n*Me)},f.period=function(a){return e(t,a)},f})(Bt,Yt);(function e(t,n){var r=Math.asin(1/(t=Math.max(1,t)))*(n/=Me);function f(a){return((a=a*2-1)<0?t*it(-a)*Math.sin((r-a)/n):2-t*it(a)*Math.sin((r+a)/n))/2}return f.amplitude=function(a){return e(a,n*Me)},f.period=function(a){return e(t,a)},f})(Bt,Yt);var Ja=Xe("start","end","cancel","interrupt"),ja=[],rr=0,gn=1,$t=2,Ze=3,yn=4,Et=5,Je=6;function ut(e,t,n,r,f,a){var i=e.__transition;if(!i)e.__transition={};else if(n in i)return;ei(e,n,{name:t,index:r,group:f,on:Ja,tween:ja,time:a.time,delay:a.delay,duration:a.duration,ease:a.ease,timer:null,state:rr})}function Gt(e,t){var n=Q(e,t);if(n.state>rr)throw new Error("too late; already scheduled");return n}function ne(e,t){var n=Q(e,t);if(n.state>Ze)throw new Error("too late; already running");return n}function Q(e,t){var n=e.__transition;if(!n||!(n=n[t]))throw new Error("transition not found");return n}function ei(e,t,n){var r=e.__transition,f;r[t]=n,n.timer=Ot(a,0,n.time);function a(s){n.state=gn,n.timer.restart(i,n.delay,n.time),n.delay<=s&&i(s-n.delay)}function i(s){var u,l,p,h;if(n.state!==gn)return o();for(u in r)if(h=r[u],h.name===n.name){if(h.state===Ze)return pn(i);h.state===yn?(h.state=Je,h.timer.stop(),h.on.call("interrupt",e,e.__data__,h.index,h.group),delete r[u]):+u$t&&r.state=0&&(t=t.slice(0,n)),!t||t==="start"})}function zi(e,t,n){var r,f,a=Si(t)?Gt:ne;return function(){var i=a(this,e),c=i.on;c!==r&&(f=(r=c).copy()).on(t,n),i.on=f}}function Ti(e,t){var n=this._id;return arguments.length<2?Q(this.node(),n).on.on(e):this.each(zi(n,e,t))}function Ii(e){return function(){var t=this.parentNode;for(var n in this.__transition)if(+n!==e)return;t&&t.removeChild(this)}}function Ci(){return this.on("end.remove",Ii(this._id))}function Di(e){var t=this._name,n=this._id;typeof e!="function"&&(e=Tt(e));for(var r=this._groups,f=r.length,a=new Array(f),i=0;i=(l=(c+s)/2))?c=l:s=l,(y=n>=(p=(o+u)/2))?o=p:u=p,f=a,!(a=a[g=y<<1|m]))return f[g]=i,e;if(h=+e._x.call(null,a.data),w=+e._y.call(null,a.data),t===h&&n===w)return i.next=a,f?f[g]=i:e._root=i,e;do f=f?f[g]=new Array(4):e._root=new Array(4),(m=t>=(l=(c+s)/2))?c=l:s=l,(y=n>=(p=(o+u)/2))?o=p:u=p;while((g=y<<1|m)===(x=(w>=p)<<1|h>=l));return f[x]=a,f[g]=i,e}function ac(e){Array.isArray(e)||(e=Array.from(e));const t=e.length,n=new Float64Array(t),r=new Float64Array(t);let f=1/0,a=f,i=-f,c=i;for(let o=0,s,u,l;oi&&(i=u),lc&&(c=l)}if(f>i||a>c)return this;this.cover(f,a).cover(i,c);for(let o=0;oe||e>=f||r>t||t>=a;)switch(s=(tu||(c=w.y0)>l||(o=w.x1)=g)<<1|e>=y)&&(w=p[p.length-1],p[p.length-1]=p[p.length-1-m],p[p.length-1-m]=w)}else{var x=e-+this._x.call(null,h.data),$=t-+this._y.call(null,h.data),b=x*x+$*$;if(b=(p=(i+o)/2))?i=p:o=p,(m=l>=(h=(c+s)/2))?c=h:s=h,t=n,!(n=n[y=m<<1|w]))return this;if(!n.length)break;(t[y+1&3]||t[y+2&3]||t[y+3&3])&&(r=t,g=y)}for(;n.data!==e;)if(f=n,!(n=n.next))return this;return(a=n.next)&&delete n.next,f?(a?f.next=a:delete f.next,this):t?(a?t[y]=a:delete t[y],(n=t[0]||t[1]||t[2]||t[3])&&n===(t[3]||t[2]||t[1]||t[0])&&!n.length&&(r?r[g]=n:this._root=n),this):(this._root=a,this)}function dc(e){for(var t=0,n=e.length;t[t(A,E,i),A])),b;for(y=0,c=new Array(g);y(e=(Nc*e+Ac)%xn)/xn}function $c(e){return e.x}function Ec(e){return e.y}var kc=10,Sc=Math.PI*(3-Math.sqrt(5));function zc(e){var t,n=1,r=.001,f=1-Math.pow(r,1/300),a=0,i=.6,c=new Map,o=Ot(l),s=Xe("tick","end"),u=Mc();e==null&&(e=[]);function l(){p(),s.call("tick",t),n1?(y==null?c.delete(m):c.set(m,w(y)),t):c.get(m)},find:function(m,y,g){var x=0,$=e.length,b,A,E,z,C;for(g==null?g=1/0:g*=g,x=0;x<$;++x)z=e[x],b=m-z.x,A=y-z.y,E=b*b+A*A,E1?(s.on(m,y),t):s.on(m)}}}function Tc(){var e,t,n,r,f=Ce(-30),a,i=1,c=1/0,o=.81;function s(h){var w,m=e.length,y=or(e,$c,Ec).visitAfter(l);for(r=h,w=0;w=c)return;(h.data!==t||h.next)&&(g===0&&(g=ye(n),b+=g*g),x===0&&(x=ye(n),b+=x*x),bRa(e[e.length-1]),Dc=new Array(3).concat("d8b365f5f5f55ab4ac","a6611adfc27d80cdc1018571","a6611adfc27df5f5f580cdc1018571","8c510ad8b365f6e8c3c7eae55ab4ac01665e","8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e","8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e","8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e","5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30","5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30").map(S);D(Dc);var Rc=new Array(3).concat("af8dc3f7f7f77fbf7b","7b3294c2a5cfa6dba0008837","7b3294c2a5cff7f7f7a6dba0008837","762a83af8dc3e7d4e8d9f0d37fbf7b1b7837","762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837","762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837","762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837","40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b","40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b").map(S);D(Rc);var Pc=new Array(3).concat("e9a3c9f7f7f7a1d76a","d01c8bf1b6dab8e1864dac26","d01c8bf1b6daf7f7f7b8e1864dac26","c51b7de9a3c9fde0efe6f5d0a1d76a4d9221","c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221","c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221","c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221","8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419","8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419").map(S);D(Pc);var qc=new Array(3).concat("998ec3f7f7f7f1a340","5e3c99b2abd2fdb863e66101","5e3c99b2abd2f7f7f7fdb863e66101","542788998ec3d8daebfee0b6f1a340b35806","542788998ec3d8daebf7f7f7fee0b6f1a340b35806","5427888073acb2abd2d8daebfee0b6fdb863e08214b35806","5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806","2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08","2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08").map(S);D(qc);var Lc=new Array(3).concat("ef8a62f7f7f767a9cf","ca0020f4a58292c5de0571b0","ca0020f4a582f7f7f792c5de0571b0","b2182bef8a62fddbc7d1e5f067a9cf2166ac","b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac","b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac","b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac","67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061","67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061").map(S);D(Lc);var Oc=new Array(3).concat("ef8a62ffffff999999","ca0020f4a582bababa404040","ca0020f4a582ffffffbababa404040","b2182bef8a62fddbc7e0e0e09999994d4d4d","b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d","b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d","b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d","67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a","67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a").map(S);D(Oc);var Fc=new Array(3).concat("fc8d59ffffbf91bfdb","d7191cfdae61abd9e92c7bb6","d7191cfdae61ffffbfabd9e92c7bb6","d73027fc8d59fee090e0f3f891bfdb4575b4","d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4","d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4","d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4","a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695","a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695").map(S);D(Fc);var Xc=new Array(3).concat("fc8d59ffffbf91cf60","d7191cfdae61a6d96a1a9641","d7191cfdae61ffffbfa6d96a1a9641","d73027fc8d59fee08bd9ef8b91cf601a9850","d73027fc8d59fee08bffffbfd9ef8b91cf601a9850","d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850","d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850","a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837","a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837").map(S);D(Xc);var Bc=new Array(3).concat("fc8d59ffffbf99d594","d7191cfdae61abdda42b83ba","d7191cfdae61ffffbfabdda42b83ba","d53e4ffc8d59fee08be6f59899d5943288bd","d53e4ffc8d59fee08bffffbfe6f59899d5943288bd","d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd","d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd","9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2","9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2").map(S);D(Bc);var Yc=new Array(3).concat("e5f5f999d8c92ca25f","edf8fbb2e2e266c2a4238b45","edf8fbb2e2e266c2a42ca25f006d2c","edf8fbccece699d8c966c2a42ca25f006d2c","edf8fbccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824","f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b").map(S);D(Yc);var Gc=new Array(3).concat("e0ecf49ebcda8856a7","edf8fbb3cde38c96c688419d","edf8fbb3cde38c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68856a7810f7c","edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b","f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b").map(S);D(Gc);var Hc=new Array(3).concat("e0f3dba8ddb543a2ca","f0f9e8bae4bc7bccc42b8cbe","f0f9e8bae4bc7bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc443a2ca0868ac","f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e","f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081").map(S);D(Hc);var Vc=new Array(3).concat("fee8c8fdbb84e34a33","fef0d9fdcc8afc8d59d7301f","fef0d9fdcc8afc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59e34a33b30000","fef0d9fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000","fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000").map(S);D(Vc);var Uc=new Array(3).concat("ece2f0a6bddb1c9099","f6eff7bdc9e167a9cf02818a","f6eff7bdc9e167a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf1c9099016c59","f6eff7d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450","fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636").map(S);D(Uc);var Kc=new Array(3).concat("ece7f2a6bddb2b8cbe","f1eef6bdc9e174a9cf0570b0","f1eef6bdc9e174a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d","f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b","fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858").map(S);D(Kc);var Wc=new Array(3).concat("e7e1efc994c7dd1c77","f1eef6d7b5d8df65b0ce1256","f1eef6d7b5d8df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0dd1c77980043","f1eef6d4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f","f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f").map(S);D(Wc);var Qc=new Array(3).concat("fde0ddfa9fb5c51b8a","feebe2fbb4b9f768a1ae017e","feebe2fbb4b9f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1c51b8a7a0177","feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177","fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a").map(S);D(Qc);var Zc=new Array(3).concat("edf8b17fcdbb2c7fb8","ffffcca1dab441b6c4225ea8","ffffcca1dab441b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c42c7fb8253494","ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84","ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58").map(S);D(Zc);var Jc=new Array(3).concat("f7fcb9addd8e31a354","ffffccc2e69978c679238443","ffffccc2e69978c67931a354006837","ffffccd9f0a3addd8e78c67931a354006837","ffffccd9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32","ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529").map(S);D(Jc);var jc=new Array(3).concat("fff7bcfec44fd95f0e","ffffd4fed98efe9929cc4c02","ffffd4fed98efe9929d95f0e993404","ffffd4fee391fec44ffe9929d95f0e993404","ffffd4fee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04","ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506").map(S);D(jc);var eo=new Array(3).concat("ffeda0feb24cf03b20","ffffb2fecc5cfd8d3ce31a1c","ffffb2fecc5cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cf03b20bd0026","ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026","ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026").map(S);D(eo);var to=new Array(3).concat("deebf79ecae13182bd","eff3ffbdd7e76baed62171b5","eff3ffbdd7e76baed63182bd08519c","eff3ffc6dbef9ecae16baed63182bd08519c","eff3ffc6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594","f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b").map(S);D(to);var no=new Array(3).concat("e5f5e0a1d99b31a354","edf8e9bae4b374c476238b45","edf8e9bae4b374c47631a354006d2c","edf8e9c7e9c0a1d99b74c47631a354006d2c","edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32","f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b").map(S);D(no);var ro=new Array(3).concat("f0f0f0bdbdbd636363","f7f7f7cccccc969696525252","f7f7f7cccccc969696636363252525","f7f7f7d9d9d9bdbdbd969696636363252525","f7f7f7d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525","fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000").map(S);D(ro);var fo=new Array(3).concat("efedf5bcbddc756bb1","f2f0f7cbc9e29e9ac86a51a3","f2f0f7cbc9e29e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8756bb154278f","f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486","fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d").map(S);D(fo);var ao=new Array(3).concat("fee0d2fc9272de2d26","fee5d9fcae91fb6a4acb181d","fee5d9fcae91fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4ade2d26a50f15","fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d","fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d").map(S);D(ao);var io=new Array(3).concat("fee6cefdae6be6550d","feeddefdbe85fd8d3cd94701","feeddefdbe85fd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3ce6550da63603","feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04","fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704").map(S);D(io);qt(te(300,.5,0),te(-240,.5,1));qt(te(-100,.75,.35),te(80,1.5,.8));qt(te(260,.75,.35),te(80,1.5,.8));te();qe();function dt(e){var t=e.length;return function(n){return e[Math.max(0,Math.min(t-1,Math.floor(n*t)))]}}dt(S("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725"));dt(S("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf"));dt(S("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4"));dt(S("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));var Ke=e=>()=>e;function co(e,{sourceEvent:t,target:n,transform:r,dispatch:f}){Object.defineProperties(this,{type:{value:e,enumerable:!0,configurable:!0},sourceEvent:{value:t,enumerable:!0,configurable:!0},target:{value:n,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:f}})}function oe(e,t,n){this.k=e,this.x=t,this.y=n}oe.prototype={constructor:oe,scale:function(e){return e===1?this:new oe(this.k*e,this.x,this.y)},translate:function(e,t){return e===0&t===0?this:new oe(this.k,this.x+this.k*e,this.y+this.k*t)},apply:function(e){return[e[0]*this.k+this.x,e[1]*this.k+this.y]},applyX:function(e){return e*this.k+this.x},applyY:function(e){return e*this.k+this.y},invert:function(e){return[(e[0]-this.x)/this.k,(e[1]-this.y)/this.k]},invertX:function(e){return(e-this.x)/this.k},invertY:function(e){return(e-this.y)/this.k},rescaleX:function(e){return e.copy().domain(e.range().map(this.invertX,this).map(e.invert,e))},rescaleY:function(e){return e.copy().domain(e.range().map(this.invertY,this).map(e.invert,e))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Ut=new oe(1,0,0);oe.prototype;function xt(e){e.stopImmediatePropagation()}function Se(e){e.preventDefault(),e.stopImmediatePropagation()}function oo(e){return(!e.ctrlKey||e.type==="wheel")&&!e.button}function so(){var e=this;return e instanceof SVGElement?(e=e.ownerSVGElement||e,e.hasAttribute("viewBox")?(e=e.viewBox.baseVal,[[e.x,e.y],[e.x+e.width,e.y+e.height]]):[[0,0],[e.width.baseVal.value,e.height.baseVal.value]]):[[0,0],[e.clientWidth,e.clientHeight]]}function _n(){return this.__zoom||Ut}function uo(e){return-e.deltaY*(e.deltaMode===1?.05:e.deltaMode?1:.002)*(e.ctrlKey?10:1)}function lo(){return navigator.maxTouchPoints||"ontouchstart"in this}function ho(e,t,n){var r=e.invertX(t[0][0])-n[0][0],f=e.invertX(t[1][0])-n[1][0],a=e.invertY(t[0][1])-n[0][1],i=e.invertY(t[1][1])-n[1][1];return e.translate(f>r?(r+f)/2:Math.min(0,r)||Math.max(0,f),i>a?(a+i)/2:Math.min(0,a)||Math.max(0,i))}function bo(){var e=oo,t=so,n=ho,r=uo,f=lo,a=[0,1/0],i=[[-1/0,-1/0],[1/0,1/0]],c=250,o=Va,s=Xe("start","zoom","end"),u,l,p,h=500,w=150,m=0,y=10;function g(d){d.property("__zoom",_n).on("wheel.zoom",C,{passive:!1}).on("mousedown.zoom",q).on("dblclick.zoom",O).filter(f).on("touchstart.zoom",B).on("touchmove.zoom",N).on("touchend.zoom touchcancel.zoom",P).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}g.transform=function(d,_,v,M){var k=d.selection?d.selection():d;k.property("__zoom",_n),d!==k?A(d,_,v,M):k.interrupt().each(function(){E(this,arguments).event(M).start().zoom(null,typeof _=="function"?_.apply(this,arguments):_).end()})},g.scaleBy=function(d,_,v,M){g.scaleTo(d,function(){var k=this.__zoom.k,T=typeof _=="function"?_.apply(this,arguments):_;return k*T},v,M)},g.scaleTo=function(d,_,v,M){g.transform(d,function(){var k=t.apply(this,arguments),T=this.__zoom,I=v==null?b(k):typeof v=="function"?v.apply(this,arguments):v,R=T.invert(I),L=typeof _=="function"?_.apply(this,arguments):_;return n($(x(T,L),I,R),k,i)},v,M)},g.translateBy=function(d,_,v,M){g.transform(d,function(){return n(this.__zoom.translate(typeof _=="function"?_.apply(this,arguments):_,typeof v=="function"?v.apply(this,arguments):v),t.apply(this,arguments),i)},null,M)},g.translateTo=function(d,_,v,M,k){g.transform(d,function(){var T=t.apply(this,arguments),I=this.__zoom,R=M==null?b(T):typeof M=="function"?M.apply(this,arguments):M;return n(Ut.translate(R[0],R[1]).scale(I.k).translate(typeof _=="function"?-_.apply(this,arguments):-_,typeof v=="function"?-v.apply(this,arguments):-v),T,i)},M,k)};function x(d,_){return _=Math.max(a[0],Math.min(a[1],_)),_===d.k?d:new oe(_,d.x,d.y)}function $(d,_,v){var M=_[0]-v[0]*d.k,k=_[1]-v[1]*d.k;return M===d.x&&k===d.y?d:new oe(d.k,M,k)}function b(d){return[(+d[0][0]+ +d[1][0])/2,(+d[0][1]+ +d[1][1])/2]}function A(d,_,v,M){d.on("start.zoom",function(){E(this,arguments).event(M).start()}).on("interrupt.zoom end.zoom",function(){E(this,arguments).event(M).end()}).tween("zoom",function(){var k=this,T=arguments,I=E(k,T).event(M),R=t.apply(k,T),L=v==null?b(R):typeof v=="function"?v.apply(k,T):v,Z=Math.max(R[1][0]-R[0][0],R[1][1]-R[0][1]),X=k.__zoom,U=typeof _=="function"?_.apply(k,T):_,re=o(X.invert(L).concat(Z/X.k),U.invert(L).concat(Z/U.k));return function(K){if(K===1)K=U;else{var fe=re(K),lt=Z/fe[2];K=new oe(lt,L[0]-fe[0]*lt,L[1]-fe[1]*lt)}I.zoom(null,K)}})}function E(d,_,v){return!v&&d.__zooming||new z(d,_)}function z(d,_){this.that=d,this.args=_,this.active=0,this.sourceEvent=null,this.extent=t.apply(d,_),this.taps=0}z.prototype={event:function(d){return d&&(this.sourceEvent=d),this},start:function(){return++this.active===1&&(this.that.__zooming=this,this.emit("start")),this},zoom:function(d,_){return this.mouse&&d!=="mouse"&&(this.mouse[1]=_.invert(this.mouse[0])),this.touch0&&d!=="touch"&&(this.touch0[1]=_.invert(this.touch0[0])),this.touch1&&d!=="touch"&&(this.touch1[1]=_.invert(this.touch1[0])),this.that.__zoom=_,this.emit("zoom"),this},end:function(){return--this.active===0&&(delete this.that.__zooming,this.emit("end")),this},emit:function(d){var _=J(this.that).datum();s.call(d,this.that,new co(d,{sourceEvent:this.sourceEvent,target:g,transform:this.that.__zoom,dispatch:s}),_)}};function C(d,..._){if(!e.apply(this,arguments))return;var v=E(this,_).event(d),M=this.__zoom,k=Math.max(a[0],Math.min(a[1],M.k*Math.pow(2,r.apply(this,arguments)))),T=ie(d);if(v.wheel)(v.mouse[0][0]!==T[0]||v.mouse[0][1]!==T[1])&&(v.mouse[1]=M.invert(v.mouse[0]=T)),clearTimeout(v.wheel);else{if(M.k===k)return;v.mouse=[T,M.invert(T)],je(this),v.start()}Se(d),v.wheel=setTimeout(I,w),v.zoom("mouse",n($(x(M,k),v.mouse[0],v.mouse[1]),v.extent,i));function I(){v.wheel=null,v.end()}}function q(d,..._){if(p||!e.apply(this,arguments))return;var v=d.currentTarget,M=E(this,_,!0).event(d),k=J(d.view).on("mousemove.zoom",L,!0).on("mouseup.zoom",Z,!0),T=ie(d,v),I=d.clientX,R=d.clientY;Ln(d.view),xt(d),M.mouse=[T,this.__zoom.invert(T)],je(this),M.start();function L(X){if(Se(X),!M.moved){var U=X.clientX-I,re=X.clientY-R;M.moved=U*U+re*re>m}M.event(X).zoom("mouse",n($(M.that.__zoom,M.mouse[0]=ie(X,v),M.mouse[1]),M.extent,i))}function Z(X){k.on("mousemove.zoom mouseup.zoom",null),On(X.view,M.moved),Se(X),M.event(X).end()}}function O(d,..._){if(e.apply(this,arguments)){var v=this.__zoom,M=ie(d.changedTouches?d.changedTouches[0]:d,this),k=v.invert(M),T=v.k*(d.shiftKey?.5:2),I=n($(x(v,T),M,k),t.apply(this,_),i);Se(d),c>0?J(this).transition().duration(c).call(A,I,M,d):J(this).call(g.transform,I,M,d)}}function B(d,..._){if(e.apply(this,arguments)){var v=d.touches,M=v.length,k=E(this,_,d.changedTouches.length===M).event(d),T,I,R,L;for(xt(d),I=0;Ipo(n,t)).map(n=>n.id))}function yo(e,t){const n=$e(e.source),r=$e(e.target),f=r.x-n.x,a=r.y-n.y,i=Math.hypot(f,a);if(t==="dot"){const c=i===0?0:kt/i;return{source:{x:n.x+f*c,y:n.y+a*c},target:{x:r.x-f*c,y:r.y-a*c}}}return{source:{x:n.x+Fe/2,y:n.y},target:{x:r.x-Fe/2,y:r.y}}}function An(e,t,n){const r=yo(t,n),f=e?r.source:r.target,a=e?r.target:r.source,i=$e(e?t.source:t.target),c=$e(e?t.target:t.source),o=c.x-i.x,s=c.y-i.y,u=Math.hypot(o,s)||1,l=Math.max(Fe/2,Math.abs(o)/2)+((t.linkindex??1)-1)*12,p=n==="box"?{x:f.x+(e?1:-1)*l,y:f.y}:{x:f.x+o/u*l,y:f.y+s/u*l},h=n==="box"?{x:a.x-(e?1:-1)*l,y:a.y}:{x:a.x-o/u*l,y:a.y-s/u*l};return`M${f.x},${f.y}C${p.x},${p.y} ${h.x},${h.y} ${a.x},${a.y}`}function mo(e){e.sort((r,f)=>V(r.source)-V(f.source)||V(r.target)-V(f.target));let t="",n=0;for(const r of e){const f=`${V(r.source)},${V(r.target)}`;n=f===t?n+1:1,r.linkindex=n,t=f}}function vo(e){const t=document.querySelector("svg"),n=document.querySelector("#node-filter"),r=document.querySelector("#forces-toggle"),f=document.querySelector("#zoom-in"),a=document.querySelector("#zoom-out"),i=document.querySelector("#zoom-reset"),c=document.querySelector("#filter-summary"),o=Array.from(document.querySelectorAll('input[name="node-shape"]'));if(!t||!n||!r||!f||!a||!i||!c||o.length<2)throw new Error("The webview page is missing required graph controls.");const s=n,u=c,l=J(t),p=Number(l.attr("width")),h=Number(l.attr("height")),w=sr(Cc);let m="box",y=r.checked;mo(e.links),l.append("defs").append("marker").attr("id","arrowhead").attr("viewBox","-0 -5 10 10").attr("refX",13).attr("refY",0).attr("orient","auto").attr("markerWidth",13).attr("markerHeight",13).attr("overflow","visible").append("path").attr("d","M 0,-5 L 10,0 L 0,5").attr("fill","#999").style("stroke","none");const g=l.append("g").attr("class","graph-content"),x=bo().scaleExtent([.2,4]).filter(N=>{const P=(N.type==="mousedown"||N.type==="touchstart")&&N.target instanceof Element&&N.target.closest(".node")!==null,d="button"in N&&typeof N.button=="number"&&N.button!==0,_=!("ctrlKey"in N&&N.ctrlKey===!0)||N.type==="wheel";return!d&&_&&!P}).on("zoom",N=>{g.attr("transform",N.transform)});l.call(x),f.addEventListener("click",()=>{l.call(x.scaleBy,1.2)}),a.addEventListener("click",()=>{l.call(x.scaleBy,1/1.2)}),i.addEventListener("click",()=>{l.call(x.transform,Ut)});const $=_c().id(N=>N.id).distance(500).strength(.5),b=zc().force("link",$).force("charge",Tc()).force("center",xc(p/2,h/2)),A=g.selectAll(".link").data(e.links).enter().append("path").attr("class","link").attr("marker-end","url(#arrowhead)");A.append("title").text(N=>N.name);const E=g.selectAll(".pathInvis").data(e.links).enter().append("path").attr("id",N=>`invis_${V(N.source)}-${N.linkindex}-${V(N.target)}`).attr("class","pathInvis"),z=g.selectAll(".pathLabel").data(e.links).enter().append("text").attr("class","pathLabel").attr("text-anchor","middle");z.append("textPath").attr("startOffset","50%").attr("href",N=>`#invis_${V(N.source)}-${N.linkindex}-${V(N.target)}`).style("fill","#ccc").style("font-size",10).text(N=>N.name);const C=g.selectAll(".node").data(e.nodes).enter().append("g").attr("class","node").call(la().on("start",(N,P)=>{y&&!N.active&&b.alphaTarget(.02).restart(),P.fx=P.x,P.fy=P.y}).on("drag",(N,P)=>{P.fx=N.x,P.fy=N.y,y||(P.x=N.x,P.y=N.y,q())}));C.append("title").text(N=>String(N.id)),C.append("text").text(N=>`${N.name}:${N.text}`),b.nodes(e.nodes).on("tick",q),$.links(e.links);for(const N of o)N.addEventListener("change",O);s.addEventListener("input",B),r.addEventListener("change",()=>{y=r.checked,y?b.alpha(1).restart():b.stop()}),O(),B();function q(){A.attr("d",N=>An(!0,N,m)),E.attr("d",N=>{const P=$e(N.source),d=$e(N.target);return An(P.x`translate(${N.x}, ${N.y})`)}function O(){var P;const N=(P=o.find(d=>d.checked))==null?void 0:P.value;if(N!=="box"&&N!=="dot")throw new Error("A valid node shape must be selected.");m=N,C.selectAll(".node-shape").remove(),m==="box"?C.insert("rect","title").attr("class","node-shape").attr("x",-Fe/2).attr("y",-Nn/2).attr("width",Fe).attr("height",Nn).attr("rx",4).style("fill",(d,_)=>w(_)):C.insert("circle","title").attr("class","node-shape").attr("r",kt).style("fill",(d,_)=>w(_)),C.select("text").attr("x",m==="box"?0:kt+4).attr("dy",m==="box"?4:-3).attr("text-anchor",m==="box"?"middle":"start"),q()}function B(){const N=go(e.nodes,s.value);C.style("display",d=>N.has(d.id)?null:"none");function P(d){return N.has(V(d.source))&&N.has(V(d.target))}A.style("display",d=>P(d)?null:"none"),E.style("display",d=>P(d)?null:"none"),z.style("display",d=>P(d)?null:"none"),u.textContent=`${N.size} of ${e.nodes.length} nodes shown`}}async function xo(){try{vo(await gr())}catch(e){const t=document.querySelector("#webview-error"),n=e instanceof Error?e.message:"An unknown error occurred.";console.error("Unable to load process graph:",e),t&&(t.textContent=`Unable to load process graph: ${n}`,t.hidden=!1)}}xo(); diff --git a/proflow/analysis/webview/static/force/frontend-dist/index.html b/proflow/analysis/webview/static/force/frontend-dist/index.html new file mode 100644 index 0000000..1ea4726 --- /dev/null +++ b/proflow/analysis/webview/static/force/frontend-dist/index.html @@ -0,0 +1,67 @@ + + + + + + proFlow Process Network + + + + + + + + diff --git a/proflow/process_inspector.py b/proflow/process_inspector.py index a4a72dd..42e7046 100644 --- a/proflow/process_inspector.py +++ b/proflow/process_inspector.py @@ -676,9 +676,14 @@ def parse_input_map_to_arg(inp: ast.Call) -> str: return "" # TODO: Should throw an error! raise NotImplementedError("Starred not implemented") - assert len(inp.keywords) >= 0 and inp.keywords[0].arg == "as_", "first keyword should be as_" - as_arg = inp.keywords[0] - return parse_as_arg(as_arg.value) + + if len(inp.keywords) == 0 or inp.keywords[0].arg != "as_": + # Assume second arg is the as_ value + if len(inp.args) >= 2: + return parse_as_arg(inp.args[1]) + return "" + else: + return parse_as_arg(inp.keywords[0].value) def get_inputs(input_list: ast.List) -> ArgMap: @@ -701,6 +706,8 @@ def parse_output_target(out_el): elif isinstance(out_el, ast.JoinedStr): elts = list(map(parse_output_target, out_el.values)) return ".".join(elts) + elif isinstance(out_el, ast.Name): + return out_el.id else: raise NotImplementedError(f"Ast type not implemented: {type(out_el)}") @@ -796,7 +803,9 @@ def parse_inputs( return "FAILED_TO_PARSE_PROCESS" -def parse_outputs_b(map_inputs_fn: Callable[[any], list[I]]) -> dict: +def parse_outputs_b( + map_inputs_fn: Callable[[any], list[I]], allow_errors: bool = True, silent: bool = True +) -> dict: source_code = textwrap.dedent(inspect.getsource(map_inputs_fn)) try: ast_tree = ast.parse(source_code) @@ -810,7 +819,7 @@ def parse_outputs_b(map_inputs_fn: Callable[[any], list[I]]) -> dict: def parse_outputs( map_outputs_fn: Callable[[any], list[I]], allow_errors: bool = True, - silent=True, + silent: bool = True, ) -> dict: try: output_lines = parse_outputs_to_interface(map_outputs_fn, allow_errors) @@ -830,42 +839,43 @@ def fieldNotEmpty(f: any | Callable) -> bool: return f and callable(f) -def inspect_process(process: "Process"): - Parsed = namedtuple( - "Parsed", "config_inputs state_inputs parameter_inputs additional_inputs state_outputs" - ) +Parsed = namedtuple( + "Parsed", "config_inputs state_inputs parameter_inputs additional_inputs state_outputs" +) + + +def inspect_process(process: "Process", allow_errors: bool = True, silent: bool = True) -> Parsed: try: return Parsed( config_inputs=fieldNotEmpty(process.config_inputs) - and parse_inputs(process.config_inputs), - state_inputs=fieldNotEmpty(process.state_inputs) and parse_inputs(process.state_inputs), + and parse_inputs(process.config_inputs, allow_errors, silent), + state_inputs=fieldNotEmpty(process.state_inputs) + and parse_inputs(process.state_inputs, allow_errors, silent), parameter_inputs=fieldNotEmpty(process.state_inputs) - and parse_inputs(process.state_inputs), + and parse_inputs(process.state_inputs, allow_errors, silent), additional_inputs=fieldNotEmpty(process.additional_inputs) - and parse_inputs(process.additional_inputs), + and parse_inputs(process.additional_inputs, allow_errors, silent), state_outputs=fieldNotEmpty(process.state_outputs) - and parse_outputs(process.state_outputs), + and parse_outputs(process.state_outputs, allow_errors, silent), ) - except Exception as e: # noqa: BLE001 - normalize parsing failures into the process-level error - raise ProflowParsingError(str(e), process) + except Exception as e: + raise ProflowParsingError(str(e), process) from e -def inspect_process_to_interfaces(process: "Process"): - Parsed = namedtuple( - "Parsed", "config_inputs state_inputs parameter_inputs additional_inputs state_outputs" - ) +def inspect_process_to_interfaces(process: "Process", allow_errors: bool = True) -> Parsed: + try: return Parsed( config_inputs=fieldNotEmpty(process.config_inputs) - and parse_inputs_to_interface(process.config_inputs), + and parse_inputs_to_interface(process.config_inputs, allow_errors), state_inputs=fieldNotEmpty(process.state_inputs) - and parse_inputs_to_interface(process.state_inputs), + and parse_inputs_to_interface(process.state_inputs, allow_errors), parameter_inputs=fieldNotEmpty(process.state_inputs) - and parse_inputs_to_interface(process.state_inputs), + and parse_inputs_to_interface(process.state_inputs, allow_errors), additional_inputs=fieldNotEmpty(process.additional_inputs) - and parse_inputs_to_interface(process.additional_inputs), + and parse_inputs_to_interface(process.additional_inputs, allow_errors), state_outputs=fieldNotEmpty(process.state_outputs) - and parse_outputs_to_interface(process.state_outputs), + and parse_outputs_to_interface(process.state_outputs, allow_errors), ) - except Exception as e: # noqa: BLE001 - normalize parsing failures into the process-level error - raise ProflowParsingError(str(e), process) + except Exception as e: + raise ProflowParsingError(str(e), process) from e diff --git a/proflow/tests/demodata.py b/proflow/tests/mock_data.py similarity index 78% rename from proflow/tests/demodata.py rename to proflow/tests/mock_data.py index 0874459..e631d8c 100644 --- a/proflow/tests/demodata.py +++ b/proflow/tests/mock_data.py @@ -53,3 +53,26 @@ ], ), ] + + +def DEMO_FUNC(x, y, z): + return x + y + z + + +DEMO_PROCESS = Process( + func=DEMO_FUNC, + comment="This is the process comment", + gate=True, + config_inputs=lambda config: [ + I(config.a, as_="x"), + ], + state_inputs=lambda state: [ + I(state.a, as_="y"), + ], + additional_inputs=lambda: [ + I(10, as_="z"), + ], + state_outputs=lambda result: [ + (result, "x"), + ], +) diff --git a/proflow/tests/process_inspector_test.py b/proflow/tests/process_inspector_test.py index 0e7348a..6857fdb 100644 --- a/proflow/tests/process_inspector_test.py +++ b/proflow/tests/process_inspector_test.py @@ -19,29 +19,7 @@ reset_id, strip_out_comments, ) - - -def DEMO_FUNC(x, y, z): - return x + y + z - - -DEMO_PROCESS = Process( - func=DEMO_FUNC, - comment="This is the process comment", - gate=True, - config_inputs=lambda config: [ - I(config.a, as_="x"), - ], - state_inputs=lambda state: [ - I(state.a, as_="y"), - ], - additional_inputs=lambda: [ - I(10, as_="z"), - ], - state_outputs=lambda result: [ - (result, "x"), - ], -) +from proflow.tests.mock_data import DEMO_PROCESS @pytest.fixture(autouse=True) @@ -51,11 +29,6 @@ def clear_id(): def test_inspect_process(): process_inputs = inspect_process(DEMO_PROCESS) - # assert process_inputs.additional_inputs == {\"10\": \"z\"} - # assert process_inputs.config_inputs == {\"config.a\": \"x\"} - # assert process_inputs.parameter_inputs == {\"state.a\": \"y\"} - # assert process_inputs.state_inputs == {\"state.a\": \"y\"} - # assert process_inputs.state_outputs == {\"result\": \"state.x\"} assert process_inputs.additional_inputs == {"z": "10"} assert process_inputs.config_inputs == {"x": "config.a"} assert process_inputs.parameter_inputs == {"y": "state.a"} @@ -338,6 +311,17 @@ def test_example(self): "row_count": "state.row_index", } + def test_not_using_as(self): + DEMO_INPUTS = lambda config: [ + I(config.foo, "bar"), + ] + + out = parse_inputs(DEMO_INPUTS, False) + + assert out == { + "bar": "config.foo", + } + # def test_parse_inputs_check(self): # nL = 1 # nLC = 2 @@ -431,47 +415,43 @@ def test_parse_key(): assert out == "config.a.foo.0.[var].bar" -def test_parse_outputs(): - """Test parse_outputs returns correct value.""" - DEMO_OUTPUTS = lambda result: [ - (result.a.foo.bar, "x"), - (result.a.foo[0], "y"), - ] - out = parse_outputs(DEMO_OUTPUTS) - assert out == {"result.a.foo.bar": "state.x", "result.a.foo.0": "state.y"} - - -def test_parse_outputs_list_comprehension(): - """Test parse_outputs returns correct value.""" - DEMO_OUTPUTS = lambda result: [ - (result[iL][iLC], f"foo.{iL}.{iLC}.bar") for iL in range(3) for iLC in range(3) - ] - out = parse_outputs(DEMO_OUTPUTS) - assert out == {"result.[iL].[iLC]": 'state.f"foo.{iL}.{iLC}.bar'} - - -def test_parse_outputs_list_comprehension_inputed_index(): - """Test parse_outputs returns correct value.""" - iLC = 0 - DEMO_OUTPUTS = lambda result, iLC=iLC: [ - (result[iL][iLC], f"foo.{iL}.{iLC}.bar") for iL in range(3) - ] - out = parse_outputs(DEMO_OUTPUTS, False) - assert out == {"result.[iL].[iLC]": 'state.f"foo.{iL}.{iLC}.bar'} - +class TestParseOutputs: + def test_parse_outputs(self): + """Test parse_outputs returns correct value.""" + DEMO_OUTPUTS = lambda result: [ + (result.a.foo.bar, "x"), + (result.a.foo[0], "y"), + ] + out = parse_outputs(DEMO_OUTPUTS) + assert out == {"result.a.foo.bar": "state.x", "result.a.foo.0": "state.y"} -@pytest.mark.skip(reason="functions in output section not currently supported") -def test_parse_outputs_deep_copy(): - """Test parse_outputs returns correct output.""" - deepcopy = lambda: None - DEMO_OUTPUTS = lambda result: [ - (deepcopy(result["a"]), "a"), - ] - out = parse_outputs(DEMO_OUTPUTS, False) - assert out == {"deepcopy(result.a.)": "state.a"} + def test_parse_outputs_list_comprehension(self): + """Test parse_outputs returns correct value.""" + DEMO_OUTPUTS = lambda result: [ + (result[iL][iLC], f"foo.{iL}.{iLC}.bar") for iL in range(3) for iLC in range(3) + ] + out = parse_outputs(DEMO_OUTPUTS) + assert out == {"result.[iL].[iLC]": 'state.f"foo.{iL}.{iLC}.bar'} + + def test_parse_outputs_list_comprehension_inputed_index(self): + """Test parse_outputs returns correct value.""" + iLC = 0 + DEMO_OUTPUTS = lambda result, iLC=iLC: [ + (result[iL][iLC], f"foo.{iL}.{iLC}.bar") for iL in range(3) + ] + out = parse_outputs(DEMO_OUTPUTS, False) + assert out == {"result.[iL].[iLC]": 'state.f"foo.{iL}.{iLC}.bar'} + @pytest.mark.skip(reason="functions in output section not currently supported") + def test_parse_outputs_deep_copy(self): + """Test parse_outputs returns correct output.""" + deepcopy = lambda: None + DEMO_OUTPUTS = lambda result: [ + (deepcopy(result["a"]), "a"), + ] + out = parse_outputs(DEMO_OUTPUTS, False) + assert out == {"deepcopy(result.a.)": "state.a"} -class TestParseOutputs: def test_parse_outputs_to_interface(self): DEMO_OUTPUTS = lambda result: [ (result["a"], "a"), @@ -505,6 +485,19 @@ def test_parse_with_star_arg(self): assert output_interface[0].from_ == 'result["a"]' assert output_interface[0].as_ == "state.a" + def test_parse_outputs_with_dynamic_key(self): + """Test parse_outputs with dynamic key. + + This doesn't work perfectly as we just use the variable name "k" instead of its value. + """ + k = "key1" + state_outputs = lambda result, k=k: [ + (result, k), + ] + + out = parse_outputs(state_outputs, allow_errors=False) + assert out == {"result": "state.k"} + class TestParseArg: def test_parse_max(self): diff --git a/proflow_webview/.gitignore b/proflow_webview/.gitignore new file mode 100644 index 0000000..945fcd0 --- /dev/null +++ b/proflow_webview/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +playwright-report/ +test-results/ diff --git a/proflow_webview/.nvmrc b/proflow_webview/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/proflow_webview/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/proflow_webview/.prettierignore b/proflow_webview/.prettierignore new file mode 100644 index 0000000..1656e65 --- /dev/null +++ b/proflow_webview/.prettierignore @@ -0,0 +1,2 @@ +node_modules/ +../proflow/analysis/webview/static/force/frontend-dist/ diff --git a/proflow_webview/README.md b/proflow_webview/README.md new file mode 100644 index 0000000..6f8bd66 --- /dev/null +++ b/proflow_webview/README.md @@ -0,0 +1,42 @@ +# Process network webview + +This private Node ES-module package contains the isolated TypeScript frontend +for proFlow's process-network graph. It depends only on the JSON response +served at `/data`; it does not import Python modules or package code. + +## Requirements + +- Node.js 22 or newer +- pnpm (the committed `pnpm-lock.yaml` is used with `pnpm install --frozen-lockfile`) +- The proFlow Python environment with the optional Flask dependency to run the + Flask host or browser tests + +## Commands + +Run these commands from the repository root: + +```bash +pnpm --dir proflow_webview install --frozen-lockfile +pnpm --dir proflow_webview typecheck +pnpm --dir proflow_webview lint +pnpm --dir proflow_webview format:check +pnpm --dir proflow_webview test +pnpm --dir proflow_webview build +``` + +The Vite development server proxies the graph data endpoint to the Flask host +on port 8000. Install the optional Flask dependency with +`uv sync --locked --group web`, start the Python server with +`uv run --locked python -m proflow.analysis.webview.run_app`, then run +`pnpm --dir proflow_webview dev`. + +The production build is written to +`proflow/analysis/webview/static/force/frontend-dist`. Commit generated +frontend assets with frontend source changes so source checkouts and Python +distributions can run the webview without Node. + +Install Chromium once with +`pnpm --dir proflow_webview playwright:install`. The +`pnpm --dir proflow_webview test:e2e` command starts the Flask host and +exercises the webview in a browser. Vitest unit tests run with +`pnpm --dir proflow_webview test`. diff --git a/proflow_webview/eslint.config.js b/proflow_webview/eslint.config.js new file mode 100644 index 0000000..5c300ee --- /dev/null +++ b/proflow_webview/eslint.config.js @@ -0,0 +1,24 @@ +/** Apply ESLint's recommended JavaScript and TypeScript rules to the package. */ +import eslint from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["../proflow/analysis/webview/static/force/frontend-dist/**"], + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.{js,ts}"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + "@typescript-eslint/explicit-function-return-type": "error", + }, + }, +); diff --git a/proflow_webview/index.html b/proflow_webview/index.html new file mode 100644 index 0000000..92e23a1 --- /dev/null +++ b/proflow_webview/index.html @@ -0,0 +1,66 @@ + + + + + + proFlow Process Network + + + + + + + diff --git a/proflow_webview/package.json b/proflow_webview/package.json new file mode 100644 index 0000000..500ce52 --- /dev/null +++ b/proflow_webview/package.json @@ -0,0 +1,41 @@ +{ + "name": "@proflow/process-webview", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "dev": "vite", + "typecheck": "tsc --noEmit", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "vitest run", + "playwright:install": "playwright install --with-deps chromium", + "test:e2e": "playwright test", + "build": "pnpm run typecheck && vite build" + }, + "dependencies": { + "d3": "^7.9.0" + }, + "devDependencies": { + "@eslint/js": "^9.30.0", + "@playwright/test": "^1.55.0", + "@types/d3": "^7.4.3", + "@types/node": "^22.15.30", + "eslint": "^9.30.0", + "globals": "^16.3.0", + "prettier": "^3.6.2", + "typescript": "^5.8.3", + "typescript-eslint": "^8.35.1", + "vite": "^6.3.5", + "vitest": "^5.1.0" + }, + "overrides": { + "@typescript-eslint/typescript-estree": { + "minimatch": "^11.0.1" + } + } +} diff --git a/proflow_webview/playwright.config.ts b/proflow_webview/playwright.config.ts new file mode 100644 index 0000000..8c5ad4e --- /dev/null +++ b/proflow_webview/playwright.config.ts @@ -0,0 +1,35 @@ +/** Configure Chromium browser tests against the Flask-hosted webview. */ +import { fileURLToPath } from "node:url"; +import { defineConfig, devices } from "@playwright/test"; + +const repositoryRoot = fileURLToPath(new URL("..", import.meta.url)); + +export default defineConfig({ + testDir: "./tests/e2e", + fullyParallel: false, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: "list", + use: { + baseURL: "http://127.0.0.1:8765", + trace: "retain-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: { + command: + "uv run --locked --no-sync python -m proflow.analysis.webview.run_app", + cwd: repositoryRoot, + url: "http://127.0.0.1:8765", + reuseExistingServer: false, + timeout: 30_000, + env: { + PROFLOW_WEBVIEW_PORT: "8765", + }, + }, +}); diff --git a/proflow_webview/pnpm-lock.yaml b/proflow_webview/pnpm-lock.yaml new file mode 100644 index 0000000..ddf12fe --- /dev/null +++ b/proflow_webview/pnpm-lock.yaml @@ -0,0 +1,3161 @@ +lockfileVersion: "9.0" + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + dependencies: + d3: + specifier: ^7.9.0 + version: 7.9.2 + devDependencies: + "@eslint/js": + specifier: ^9.30.0 + version: 9.39.5 + "@playwright/test": + specifier: ^1.55.0 + version: 1.64.1 + "@types/d3": + specifier: ^7.4.3 + version: 7.4.3 + "@types/node": + specifier: ^22.15.30 + version: 22.20.5 + eslint: + specifier: ^9.30.0 + version: 9.39.5(supports-color@7.2.0) + globals: + specifier: ^16.3.0 + version: 16.5.0 + prettier: + specifier: ^3.6.2 + version: 3.9.10 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.35.1 + version: 8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + vite: + specifier: ^6.3.5 + version: 6.4.5(@types/node@22.20.5) + vitest: + specifier: ^5.1.0 + version: 5.1.0(@types/node@22.20.5)(vite@6.4.5(@types/node@22.20.5)) + +packages: + "@esbuild/aix-ppc64@0.25.12": + resolution: + { + integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==, + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [aix] + + "@esbuild/android-arm64@0.25.12": + resolution: + { + integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [android] + + "@esbuild/android-arm@0.25.12": + resolution: + { + integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==, + } + engines: { node: ">=18" } + cpu: [arm] + os: [android] + + "@esbuild/android-x64@0.25.12": + resolution: + { + integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [android] + + "@esbuild/darwin-arm64@0.25.12": + resolution: + { + integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [darwin] + + "@esbuild/darwin-x64@0.25.12": + resolution: + { + integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [darwin] + + "@esbuild/freebsd-arm64@0.25.12": + resolution: + { + integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [freebsd] + + "@esbuild/freebsd-x64@0.25.12": + resolution: + { + integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [freebsd] + + "@esbuild/linux-arm64@0.25.12": + resolution: + { + integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [linux] + + "@esbuild/linux-arm@0.25.12": + resolution: + { + integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==, + } + engines: { node: ">=18" } + cpu: [arm] + os: [linux] + + "@esbuild/linux-ia32@0.25.12": + resolution: + { + integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==, + } + engines: { node: ">=18" } + cpu: [ia32] + os: [linux] + + "@esbuild/linux-loong64@0.25.12": + resolution: + { + integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==, + } + engines: { node: ">=18" } + cpu: [loong64] + os: [linux] + + "@esbuild/linux-mips64el@0.25.12": + resolution: + { + integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==, + } + engines: { node: ">=18" } + cpu: [mips64el] + os: [linux] + + "@esbuild/linux-ppc64@0.25.12": + resolution: + { + integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==, + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [linux] + + "@esbuild/linux-riscv64@0.25.12": + resolution: + { + integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==, + } + engines: { node: ">=18" } + cpu: [riscv64] + os: [linux] + + "@esbuild/linux-s390x@0.25.12": + resolution: + { + integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==, + } + engines: { node: ">=18" } + cpu: [s390x] + os: [linux] + + "@esbuild/linux-x64@0.25.12": + resolution: + { + integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [linux] + + "@esbuild/netbsd-arm64@0.25.12": + resolution: + { + integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [netbsd] + + "@esbuild/netbsd-x64@0.25.12": + resolution: + { + integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [netbsd] + + "@esbuild/openbsd-arm64@0.25.12": + resolution: + { + integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openbsd] + + "@esbuild/openbsd-x64@0.25.12": + resolution: + { + integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [openbsd] + + "@esbuild/openharmony-arm64@0.25.12": + resolution: + { + integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openharmony] + + "@esbuild/sunos-x64@0.25.12": + resolution: + { + integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [sunos] + + "@esbuild/win32-arm64@0.25.12": + resolution: + { + integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [win32] + + "@esbuild/win32-ia32@0.25.12": + resolution: + { + integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==, + } + engines: { node: ">=18" } + cpu: [ia32] + os: [win32] + + "@esbuild/win32-x64@0.25.12": + resolution: + { + integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [win32] + + "@eslint-community/eslint-utils@4.10.1": + resolution: + { + integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + "@eslint-community/regexpp@4.12.2": + resolution: + { + integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, + } + engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } + + "@eslint/config-array@0.21.2": + resolution: + { + integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/config-helpers@0.4.2": + resolution: + { + integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/core@0.17.0": + resolution: + { + integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/eslintrc@3.3.7": + resolution: + { + integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/js@9.39.5": + resolution: + { + integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/object-schema@2.1.7": + resolution: + { + integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@eslint/plugin-kit@0.4.1": + resolution: + { + integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@humanfs/core@0.19.2": + resolution: + { + integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==, + } + engines: { node: ">=18.18.0" } + + "@humanfs/node@0.16.8": + resolution: + { + integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==, + } + engines: { node: ">=18.18.0" } + + "@humanfs/types@0.15.0": + resolution: + { + integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==, + } + engines: { node: ">=18.18.0" } + + "@humanwhocodes/module-importer@1.0.1": + resolution: + { + integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, + } + engines: { node: ">=12.22" } + + "@humanwhocodes/retry@0.4.3": + resolution: + { + integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, + } + engines: { node: ">=18.18" } + + "@jridgewell/resolve-uri@3.1.2": + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: ">=6.0.0" } + + "@jridgewell/sourcemap-codec@1.6.0": + resolution: + { + integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==, + } + + "@jridgewell/trace-mapping@0.3.31": + resolution: + { + integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, + } + + "@napi-rs/lzma-linux-x64-gnu@1.5.1": + resolution: + { + integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==, + } + engines: { node: ^22.20 || ^24.12 || >=25 } + cpu: [x64] + os: [linux] + libc: [glibc] + + "@playwright/test@1.64.1": + resolution: + { + integrity: sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==, + } + engines: { node: ">=20" } + hasBin: true + + "@rollup/rollup-android-arm-eabi@4.67.4": + resolution: + { + integrity: sha512-/v/pUAPOcwp+r1Hjjdg0cBV1TE89CzGKrKh2FgkIQw2QBcIMmVPOCSfuOZStCn4Xh/98HXfo/ht6ztcssctPNg==, + } + cpu: [arm] + os: [android] + + "@rollup/rollup-android-arm64@4.67.4": + resolution: + { + integrity: sha512-6tmmB/P52Vt167FXXEw5auiEUgTtZz7aqUgWHqYXToIUE0HFDY6fTdAyeekjjsNk6O/8dtpSKtClxSj1mMneKQ==, + } + cpu: [arm64] + os: [android] + + "@rollup/rollup-android-x64@4.67.4": + resolution: + { + integrity: sha512-8Z9Q4RmotVmuL4llOvqX814JNiOK1oqoBv+glG5KbpRfUiypqzz2/fxIzK4Pq129l4dT5a8Gszmpc0/XPYoFuQ==, + } + cpu: [x64] + os: [android] + + "@rollup/rollup-darwin-arm64@4.67.4": + resolution: + { + integrity: sha512-pnq6hssxTtwuRAl6Kdi/+IDY+b1IhkUetjhUkdLAAJCssjfqtL2Oqy/FXJVJFtAWOu1oRlArIvf0Yk1O92x2HQ==, + } + cpu: [arm64] + os: [darwin] + + "@rollup/rollup-darwin-x64@4.67.4": + resolution: + { + integrity: sha512-BeIrGZvHMVdJg/N+ohZfFywG/gF41684YhDj2wBvouj7m9ziP1tZDUNwzXRiDY+3WIKngaC4hiUBqVtBPGP5og==, + } + cpu: [x64] + os: [darwin] + + "@rollup/rollup-freebsd-arm64@4.67.4": + resolution: + { + integrity: sha512-fRFX/oqNg/g780DKoS8RIfYcZD59Ru5O/bSeK1N7gQljBFrTxJY/q5EX4eMs5SrJf32VgVBVVYCdfPpzAwPPnQ==, + } + cpu: [arm64] + os: [freebsd] + + "@rollup/rollup-freebsd-x64@4.67.4": + resolution: + { + integrity: sha512-bSrBRopG1q+0RuOsW0NtYTtIODlvNA3T3ebx5WnQVdSxWIoXFmj+rZdHUHdSLgFdZ5M6VGmekYE7r4RMGQXjfg==, + } + cpu: [x64] + os: [freebsd] + + "@rollup/rollup-linux-arm-gnueabihf@4.67.4": + resolution: + { + integrity: sha512-L8IvdrJVtl65g1ozwDSyjGPG+FmibhCdFUwmqTItJLw0S3+Oj4QzFs7rfgTUiK7rsPjkCLdG1/GApFxoynXFJQ==, + } + cpu: [arm] + os: [linux] + libc: [glibc] + + "@rollup/rollup-linux-arm-musleabihf@4.67.4": + resolution: + { + integrity: sha512-h7PcoQOzC31jshNpQ48J0akB9tsghjkSjSl5AZ6XsVGTxVsln7t7C4SQyw62tNVSaz2SeHgk4VT2ELbL17BSHQ==, + } + cpu: [arm] + os: [linux] + libc: [musl] + + "@rollup/rollup-linux-arm64-gnu@4.67.4": + resolution: + { + integrity: sha512-KSPuOBNHoOl7AsVjeY1KZci8+0up0U4TFAHU5IzBYN8P+Z6cB9DZq28+JQycZW7f6WLLa/tSzhV7Rdy95bVi+g==, + } + cpu: [arm64] + os: [linux] + libc: [glibc] + + "@rollup/rollup-linux-arm64-musl@4.67.4": + resolution: + { + integrity: sha512-0E/mKmVk1PlQeQYygIwQm0jlwjANTzGSiNBMPrFx+rdFGFj/2Axh4p6XwcrystQIqXZVudTaGsdbDpzYgJ4wSA==, + } + cpu: [arm64] + os: [linux] + libc: [musl] + + "@rollup/rollup-linux-loong64-gnu@4.67.4": + resolution: + { + integrity: sha512-Fvw7dOSc2Sa9UOipRZVI0zOgdLCzsjVyD/6JuHXzUHTXFu7mMgmeX/0/bCVBIut1dz+nWz3pV4WpS/Qg0XVnxw==, + } + cpu: [loong64] + os: [linux] + libc: [glibc] + + "@rollup/rollup-linux-loong64-musl@4.67.4": + resolution: + { + integrity: sha512-jzxbVHaE7V3F+P0wEK7UydQtvv7Twfdtx58jI5/3VRx1jJxyrn+qFwfXmqjVFxPhBl+zjaRs+ZJMFJw7vdHaQA==, + } + cpu: [loong64] + os: [linux] + libc: [musl] + + "@rollup/rollup-linux-ppc64-gnu@4.67.4": + resolution: + { + integrity: sha512-fhtrzp9p4GjEJVaigaap9g6X9nP6xgJtKiLLXSDgpE18HXufA5aqfltGn+KAqMtVZReJZfOrXm//y5YSA9BWbA==, + } + cpu: [ppc64] + os: [linux] + libc: [glibc] + + "@rollup/rollup-linux-ppc64-musl@4.67.4": + resolution: + { + integrity: sha512-uynaF7IUHwAckV0S+yC1f5esy9gB+aqCBsPH5BOck32TYdCuAn2lVG5tvUcFnEsrviGEmRZxE1NHSOieUPYJEg==, + } + cpu: [ppc64] + os: [linux] + libc: [musl] + + "@rollup/rollup-linux-riscv64-gnu@4.67.4": + resolution: + { + integrity: sha512-uaqRFdfb8eIa2bVHN/FE3ndCvR0zV2xv0AwjCfxrqSu6l6fp/54VhjfhslKlf2RIik9L1jwlt4E56PeVX6tQtQ==, + } + cpu: [riscv64] + os: [linux] + libc: [glibc] + + "@rollup/rollup-linux-riscv64-musl@4.67.4": + resolution: + { + integrity: sha512-JZRow7kmxUDEASnxHuVG5itTm+nTXi9BQUABcVUnUEngC+OPwMrpx1uJ9hIA/v+IcAEBL52ktCSPXk5OJn9Jqg==, + } + cpu: [riscv64] + os: [linux] + libc: [musl] + + "@rollup/rollup-linux-s390x-gnu@4.67.4": + resolution: + { + integrity: sha512-rdrh4e+Lq9fCNKoDPaCqeHXbQCe7yl77Ukl45tlSClwYdZ1gpFGZz1XLJdYtY++FRlfL5aHvb6/0n3GDW/Nxsw==, + } + cpu: [s390x] + os: [linux] + libc: [glibc] + + "@rollup/rollup-linux-x64-gnu@4.67.4": + resolution: + { + integrity: sha512-wL7MS4r0fYcxluF3ne2plofHZiB/LsqNCnyaUlyiKpAJLb6agPSFz7DEFCfkonWAZXMrLwCmC08b2ZEnEdJkog==, + } + cpu: [x64] + os: [linux] + libc: [glibc] + + "@rollup/rollup-linux-x64-musl@4.67.4": + resolution: + { + integrity: sha512-4H0XHfd2VufGjX7NjUyeue78sD0bRIoKSGjGqO+F5nSvL6jN48NAU7esdmtFEYxZXwObLtt3MJ/kHS0jASMW3g==, + } + cpu: [x64] + os: [linux] + libc: [musl] + + "@rollup/rollup-openbsd-x64@4.67.4": + resolution: + { + integrity: sha512-osRHQbE55pZpCLetpMsFmhkapyZnAUH73qRKMh+npi3boUom6u6hdaTTPSxlInXxwJHsn9r2uDhdz0V7Yoxx2A==, + } + cpu: [x64] + os: [openbsd] + + "@rollup/rollup-openharmony-arm64@4.67.4": + resolution: + { + integrity: sha512-95+exYEyOwrZePsE0czi4q7X5lxKPa21+lmyAZlwbYENITQ0Unk4HAQAFp/p+KhXBHBx9mUABXRYjOC5A1wY3Q==, + } + cpu: [arm64] + os: [openharmony] + + "@rollup/rollup-win32-arm64-msvc@4.67.4": + resolution: + { + integrity: sha512-FOXOcKhNQpO4Po+Ynan6IbtM67SrXZbgpRJWVYwz53CT5veI7sVzDwVBmWS65N5Imv+wK3SVhIPAl+C5DXXNCg==, + } + cpu: [arm64] + os: [win32] + + "@rollup/rollup-win32-ia32-msvc@4.67.4": + resolution: + { + integrity: sha512-cklKh0gJOkAZQcWziQa3hrjxZKN+PCHZ7SPTyGJDAiBVr05ZXlEjGFCG7zFJMaVibxsmzAe1FimN4u/HBns9+Q==, + } + cpu: [ia32] + os: [win32] + + "@rollup/rollup-win32-x64-gnu@4.67.4": + resolution: + { + integrity: sha512-AegwxJPc9bvUp8fQtlsUHkFMOMkGylWU7glyL+YD4vwG+QaYoGXYLSsQoeTD7kvoLZrVlMWNVhTg1dedY5C1VQ==, + } + cpu: [x64] + os: [win32] + + "@rollup/rollup-win32-x64-msvc@4.67.4": + resolution: + { + integrity: sha512-RJZnrwTWj7Iq2ooBwoGbDdC65tDwYawneGcAfFMJqVUrRZMcskka6nTkBpXR/47G7kzn3EW2qupExnaBy11vwg==, + } + cpu: [x64] + os: [win32] + + "@types/chai@5.2.3": + resolution: + { + integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==, + } + + "@types/d3-array@3.2.2": + resolution: + { + integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==, + } + + "@types/d3-axis@3.0.6": + resolution: + { + integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==, + } + + "@types/d3-brush@3.0.6": + resolution: + { + integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==, + } + + "@types/d3-chord@3.0.6": + resolution: + { + integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==, + } + + "@types/d3-color@3.1.3": + resolution: + { + integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==, + } + + "@types/d3-contour@3.0.6": + resolution: + { + integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==, + } + + "@types/d3-delaunay@6.0.4": + resolution: + { + integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==, + } + + "@types/d3-dispatch@3.0.7": + resolution: + { + integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==, + } + + "@types/d3-drag@3.0.7": + resolution: + { + integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==, + } + + "@types/d3-dsv@3.0.7": + resolution: + { + integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==, + } + + "@types/d3-ease@3.0.2": + resolution: + { + integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==, + } + + "@types/d3-fetch@3.0.7": + resolution: + { + integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==, + } + + "@types/d3-force@3.0.10": + resolution: + { + integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==, + } + + "@types/d3-format@3.0.4": + resolution: + { + integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==, + } + + "@types/d3-geo@3.1.1": + resolution: + { + integrity: sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==, + } + + "@types/d3-hierarchy@3.1.7": + resolution: + { + integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==, + } + + "@types/d3-interpolate@3.0.4": + resolution: + { + integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==, + } + + "@types/d3-path@3.1.1": + resolution: + { + integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==, + } + + "@types/d3-polygon@3.0.2": + resolution: + { + integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==, + } + + "@types/d3-quadtree@3.0.6": + resolution: + { + integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==, + } + + "@types/d3-random@3.0.4": + resolution: + { + integrity: sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==, + } + + "@types/d3-scale-chromatic@3.1.0": + resolution: + { + integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==, + } + + "@types/d3-scale@4.0.9": + resolution: + { + integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==, + } + + "@types/d3-selection@3.0.12": + resolution: + { + integrity: sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==, + } + + "@types/d3-shape@3.2.0": + resolution: + { + integrity: sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==, + } + + "@types/d3-time-format@4.0.3": + resolution: + { + integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==, + } + + "@types/d3-time@3.0.4": + resolution: + { + integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==, + } + + "@types/d3-timer@3.0.2": + resolution: + { + integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==, + } + + "@types/d3-transition@3.0.9": + resolution: + { + integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==, + } + + "@types/d3-zoom@3.0.9": + resolution: + { + integrity: sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==, + } + + "@types/d3@7.4.3": + resolution: + { + integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==, + } + + "@types/deep-eql@4.0.2": + resolution: + { + integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==, + } + + "@types/estree@1.0.10": + resolution: + { + integrity: sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==, + } + + "@types/geojson@7946.0.16": + resolution: + { + integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==, + } + + "@types/json-schema@7.0.15": + resolution: + { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, + } + + "@types/node@22.20.5": + resolution: + { + integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==, + } + + "@typescript-eslint/eslint-plugin@8.72.1": + resolution: + { + integrity: sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + "@typescript-eslint/parser": ^8.72.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/parser@8.72.1": + resolution: + { + integrity: sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/project-service@8.72.1": + resolution: + { + integrity: sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/scope-manager@8.72.1": + resolution: + { + integrity: sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/tsconfig-utils@8.72.1": + resolution: + { + integrity: sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/type-utils@8.72.1": + resolution: + { + integrity: sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/types@8.72.1": + resolution: + { + integrity: sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/typescript-estree@8.72.1": + resolution: + { + integrity: sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/utils@8.72.1": + resolution: + { + integrity: sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + + "@typescript-eslint/visitor-keys@8.72.1": + resolution: + { + integrity: sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@vitest/mocker@5.1.0": + resolution: + { + integrity: sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==, + } + peerDependencies: + msw: ^2.4.9 || ^3.0.0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + "@vitest/spy@5.1.0": + resolution: + { + integrity: sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==, + } + + acorn-jsx@5.3.2: + resolution: + { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, + } + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.19.0: + resolution: + { + integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==, + } + engines: { node: ">=0.4.0" } + hasBin: true + + ajv@6.15.0: + resolution: + { + integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==, + } + + ansi-styles@4.3.0: + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, + } + engines: { node: ">=8" } + + argparse@2.0.1: + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } + + assertion-error@2.0.1: + resolution: + { + integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, + } + engines: { node: ">=12" } + + balanced-match@1.0.2: + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } + + balanced-match@4.0.4: + resolution: + { + integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, + } + engines: { node: 18 || 20 || >=22 } + + brace-expansion@1.1.21: + resolution: + { + integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==, + } + + brace-expansion@5.0.12: + resolution: + { + integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==, + } + engines: { node: 20 || >=22 } + + callsites@3.1.0: + resolution: + { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, + } + engines: { node: ">=6" } + + chai@6.3.0: + resolution: + { + integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==, + } + engines: { node: ">=18" } + + chalk@4.1.2: + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, + } + engines: { node: ">=10" } + + color-convert@2.0.1: + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: ">=7.0.0" } + + color-name@1.1.4: + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } + + commander@7.2.0: + resolution: + { + integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==, + } + engines: { node: ">= 10" } + + concat-map@0.0.1: + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } + + cross-spawn@7.0.6: + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: ">= 8" } + + d3-array@3.2.5: + resolution: + { + integrity: sha512-Th8qGXoBalmWsstpkuhmBuTsBQIFqYOJtjlBvNVvBfJ8E3uYusBUwUR8HU20U0qKKQkgCvKyR1YjiozWadk5QA==, + } + engines: { node: ">=12" } + + d3-axis@3.0.1: + resolution: + { + integrity: sha512-E5QCXfaf/uJZEHHiqoWRq+bQEm2PzoVFv2rDC7CZ3wwI0MjfMW3w83J3ubobG4IJj3lShAI5eiPJhOu/8wHofA==, + } + engines: { node: ">=12" } + + d3-brush@3.0.1: + resolution: + { + integrity: sha512-L5Glqa/Gb7LU3ndIOMtA5mDzlJ0aXKp5ELP2X09ctWF23jO7+faIUVDbmaIi+D6k3EemXFWn1F3lkfguExoVhA==, + } + engines: { node: ">=12" } + + d3-chord@3.0.2: + resolution: + { + integrity: sha512-cGmADo5M2PTi3ewJZ2lcE32eURd4JmHtQ5JwRIUcas5L1SXqufG9yPyjWp8fW0gRi7aZi9bgbZPFZO1eyZ+1CQ==, + } + engines: { node: ">=12" } + + d3-color@3.1.1: + resolution: + { + integrity: sha512-lfab8x0Bj0UO7SNsY8dZSbNIA6ipbkE3X0NG9nqjCu6CfCHZseuDnOSJY7hidKrjpP++i1M2qT+FlAdPKYRilA==, + } + engines: { node: ">=12" } + + d3-contour@4.0.3: + resolution: + { + integrity: sha512-5DYURc8KwXPOhSUBblU0P9rVcArsSPxPOxsfO0Eud0s28oU0sB2e80aRHRL9AZW3usT8ikFKyIHpHHLNWLdRVA==, + } + engines: { node: ">=12" } + + d3-delaunay@6.0.6: + resolution: + { + integrity: sha512-DMQIEvSJhZCB6pwedcXaAlQ+MeKU5LlM2XakKtc73b/VM+RBiI1SRXycOEOKg2jsKj4zZ9F0KGYG6nCGwwWu/g==, + } + engines: { node: ">=12" } + + d3-dispatch@3.0.2: + resolution: + { + integrity: sha512-sAzH4Y4nsLo8eSqw5jKiZt+LU2wZmL/qS+OkxQGjhwazB/D7TY34R/QK1eshfGr7cZEKymKZ/Haem7m+lu98Sg==, + } + engines: { node: ">=12" } + + d3-drag@3.0.1: + resolution: + { + integrity: sha512-OWeoayCq8J69hUIjvL+Mb6Q6Oaj/fyHV5XaXByEUAG+JUjIfTGDnk3qGpcXrD3VmQ3BSV96fcbM6fqq7C0tbPA==, + } + engines: { node: ">=12" } + + d3-dsv@3.0.2: + resolution: + { + integrity: sha512-FUnXhX7I4/AXyRCZzwfZTDGSq8PGZ4oR0tiCBqNGOZMsDrQ+6sIWNQnBzcCGhzJKZ6uCvCezTwflUqI209RRiQ==, + } + engines: { node: ">=12" } + hasBin: true + + d3-ease@3.0.2: + resolution: + { + integrity: sha512-B5znKqRmKROAN4oHWQ50W/qisVjA9GXh2WMsVKdFVp4DxWmcrOsweqgUOUVsv5th96kwaZlokU6SlV4J3gak1w==, + } + engines: { node: ">=12" } + + d3-fetch@3.0.2: + resolution: + { + integrity: sha512-1wGF62bgjMRbF6mqbD+6rRWBL2hwWVYQjFO2UP3Wm1nIAvJZKaQxXM5caVY7athoCa8wIq9lCf7g44lPWFk50g==, + } + engines: { node: ">=12" } + + d3-force@3.0.1: + resolution: + { + integrity: sha512-kAaaHLFebBYwgi5cYBXAI8vKrQFMCD2MEswj2QuPKKbdU1wYLc8WXtS7O8F/b6POCIe3gJjY6hkDstHi1yMETQ==, + } + engines: { node: ">=12" } + + d3-format@3.1.3: + resolution: + { + integrity: sha512-+F+/49swqmGuQJmSKnVMIDH/QkEFV5li9QRj0xm/KhyAUBv099O5PrvpQFxBp24PYph17Mnvt9mjKQV0ii0nMA==, + } + engines: { node: ">=12" } + + d3-geo@3.1.2: + resolution: + { + integrity: sha512-5R8ZgDQorIxmauv7R6gpE/ndz+Z0qAZssigq0aXFdvPHdY41RqtW02jrRUQAL724NdPABYXo1+gQEGBBr7uKNg==, + } + engines: { node: ">=12" } + + d3-hierarchy@3.1.3: + resolution: + { + integrity: sha512-GRajN5ZcnEwYFWqqdzhG1qJ3+fDg6WWzk47TJg7ePY7yvYAgRfD2r1mUx+wviBOWuCwamCUzjwMSdato43jbGA==, + } + engines: { node: ">=12" } + + d3-interpolate@3.0.2: + resolution: + { + integrity: sha512-IRzGU62DtxhvsnSTWMQIdnoZ6gA+BCWwBon1EJI6SjoN595799b6z/QRBzXfFhE8iupUPp3042yBNo0STPqnzQ==, + } + engines: { node: ">=12" } + + d3-path@3.1.1: + resolution: + { + integrity: sha512-jsbgV8zIT38ReB9O7X7gcLndTQRONSqNNd+oGIaTBdUug4r5jFmp0N6RY9R5GWbdUDucn0qoTGyfjapT4tDhcg==, + } + engines: { node: ">=12" } + + d3-polygon@3.0.2: + resolution: + { + integrity: sha512-WAUvPudGU1Dn4mDPfb6+JnnbeVEV53hG/BoT+bxts6AkZsOpTEiV1jHYX+2vtwCICNFG9Wxv8QS+eW1zE/Dwfg==, + } + engines: { node: ">=12" } + + d3-quadtree@3.0.2: + resolution: + { + integrity: sha512-9Lu9mqJKRbIk0d6mKDjpvPURaPdwHVK5CYbcdd0/4vBgz4ldz/7TIOse3kNERsByGNfld58KEco507AztJWyTQ==, + } + engines: { node: ">=12" } + + d3-random@3.0.2: + resolution: + { + integrity: sha512-PxJi9LNV+7mulaYO77nd+oGurXGL1fhedXjHnB3pWLUcqDnQtF4VASoqnQGHgoXLByVdv5QJ/2kfwkGn5HTS8g==, + } + engines: { node: ">=12" } + + d3-scale-chromatic@3.1.1: + resolution: + { + integrity: sha512-lA5gfN2yMvmChubmptnDiBfeGpHD7Yb1EbvCz0TZE0WHXhyR6W2allPuhKtzV5ILQRqeOdefF0qYtERReLUBDg==, + } + engines: { node: ">=12" } + + d3-scale@4.0.3: + resolution: + { + integrity: sha512-66cWobCNzcMvlfoaDHCxuIv9TBUWG1BLOWdv9kHHFuCr1JdqAWPvTmsaEYDD5NRMyWZ5+7u0GqiCDfMXMqh3hg==, + } + engines: { node: ">=12" } + + d3-selection@3.0.1: + resolution: + { + integrity: sha512-ODSf35vqCrlCfp7hrm0eZjvOosSXSYLjqvIlhMBV9q9VOx7EBJG6pc+uXBswEZNJgCGJ+qnKYcWutbZDIxe/Xg==, + } + engines: { node: ">=12" } + + d3-shape@3.2.1: + resolution: + { + integrity: sha512-dm2o+lbKdzg9tY7GEag/dB9hcbodLfwkE/DkvqbELJXaMz0yKzWWhZqhbI7CKubukA7HnRyz0avdtvi7EPxPvw==, + } + engines: { node: ">=12" } + + d3-time-format@4.1.1: + resolution: + { + integrity: sha512-22XaqjyCjpqgda20erhsOjGmC3vvYwTNYniKAd45W5UWLDF43moeOuV9qYpRqobIlsnK0UhnKZjUUJm3VgORUA==, + } + engines: { node: ">=12" } + + d3-time@3.1.1: + resolution: + { + integrity: sha512-Ze7rjrhMzdeulh0/hjcfFj7XLAHOfowtEMfsoG6DU866HPynQNW7Wfen6hzXcqKTv8XzxvF6Hzr5XD6bMHr2ug==, + } + engines: { node: ">=12" } + + d3-timer@3.0.2: + resolution: + { + integrity: sha512-1Il1aliroVSErrv5aulRBxAY+7+DnaoLIWGd1C6SsBTWDeobWiP/V69vwb8vT6abR1kKwaNe1196pmaQ1fK90A==, + } + engines: { node: ">=12" } + + d3-transition@3.0.2: + resolution: + { + integrity: sha512-RcjCsdFgyFoDB+QXLC5m60ZkTN0k9zH+ovoLlTdJnDS1AbfbcuowiM4pCKSTOclNibS4f4J4cbv2m7O7y8NTOQ==, + } + engines: { node: ">=12" } + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.1: + resolution: + { + integrity: sha512-kfjD3BDQu/FOatpZ6JdMSyIoDwTxIpI29ySTI2J5ZlDaH1WbuJGQJ2Q9oJkWTEZEW98j6idezoomSD3Cn2zwiw==, + } + engines: { node: ">=12" } + + d3@7.9.2: + resolution: + { + integrity: sha512-ZKb/mONPVKzaO5UlcWcwN8bS4ezvQaYu+bsou+XgNABdGC800kwH651ADTbbTxMPHW13UlvbQafdmJQDk1w7Mg==, + } + engines: { node: ">=12" } + + debug@4.4.3: + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, + } + engines: { node: ">=6.0" } + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: + { + integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, + } + + delaunator@5.1.0: + resolution: + { + integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==, + } + + es-module-lexer@2.3.2: + resolution: + { + integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==, + } + + esbuild@0.25.12: + resolution: + { + integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==, + } + engines: { node: ">=18" } + hasBin: true + + escape-string-regexp@4.0.0: + resolution: + { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, + } + engines: { node: ">=10" } + + eslint-scope@8.4.0: + resolution: + { + integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint-visitor-keys@3.4.3: + resolution: + { + integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + + eslint-visitor-keys@4.2.1: + resolution: + { + integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint-visitor-keys@5.0.1: + resolution: + { + integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + + eslint@9.39.5: + resolution: + { + integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: + { + integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + esquery@1.7.0: + resolution: + { + integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==, + } + engines: { node: ">=0.10" } + + esrecurse@4.3.0: + resolution: + { + integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, + } + engines: { node: ">=4.0" } + + estraverse@5.3.0: + resolution: + { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, + } + engines: { node: ">=4.0" } + + estree-walker@3.0.3: + resolution: + { + integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, + } + + esutils@2.0.3: + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, + } + engines: { node: ">=0.10.0" } + + expect-type@1.4.0: + resolution: + { + integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==, + } + engines: { node: ">=12.0.0" } + + fast-deep-equal@3.1.3: + resolution: + { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, + } + + fast-json-stable-stringify@2.1.0: + resolution: + { + integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, + } + + fast-levenshtein@2.0.6: + resolution: + { + integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, + } + + fdir@6.5.0: + resolution: + { + integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, + } + engines: { node: ">=12.0.0" } + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: + { + integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, + } + engines: { node: ">=16.0.0" } + + find-up@5.0.0: + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, + } + engines: { node: ">=10" } + + flat-cache@4.0.1: + resolution: + { + integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, + } + engines: { node: ">=16" } + + flatted@3.4.4: + resolution: + { + integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==, + } + + fsevents@2.3.3: + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + os: [darwin] + + glob-parent@6.0.2: + resolution: + { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, + } + engines: { node: ">=10.13.0" } + + globals@14.0.0: + resolution: + { + integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==, + } + engines: { node: ">=18" } + + globals@16.5.0: + resolution: + { + integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==, + } + engines: { node: ">=18" } + + has-flag@4.0.0: + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, + } + engines: { node: ">=8" } + + iconv-lite@0.6.3: + resolution: + { + integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==, + } + engines: { node: ">=0.10.0" } + + ignore@5.3.2: + resolution: + { + integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, + } + engines: { node: ">= 4" } + + ignore@7.0.14: + resolution: + { + integrity: sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==, + } + engines: { node: ">= 4" } + + import-fresh@3.3.1: + resolution: + { + integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, + } + engines: { node: ">=6" } + + imurmurhash@0.1.4: + resolution: + { + integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, + } + engines: { node: ">=0.8.19" } + + internmap@2.0.3: + resolution: + { + integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==, + } + engines: { node: ">=12" } + + is-extglob@2.1.1: + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, + } + engines: { node: ">=0.10.0" } + + is-glob@4.0.3: + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: { node: ">=0.10.0" } + + isexe@2.0.0: + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } + + js-yaml@4.3.2: + resolution: + { + integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==, + } + hasBin: true + + json-buffer@3.0.1: + resolution: + { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, + } + + json-schema-traverse@0.4.1: + resolution: + { + integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, + } + + json-stable-stringify-without-jsonify@1.0.1: + resolution: + { + integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, + } + + keyv@4.5.4: + resolution: + { + integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, + } + + levn@0.4.1: + resolution: + { + integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, + } + engines: { node: ">= 0.8.0" } + + locate-path@6.0.0: + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, + } + engines: { node: ">=10" } + + lodash.merge@4.6.2: + resolution: + { + integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, + } + + magic-string@1.4.5: + resolution: + { + integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==, + } + + minimatch@10.2.6: + resolution: + { + integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==, + } + engines: { node: 18 || 20 || >=22 } + + minimatch@3.1.5: + resolution: + { + integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==, + } + + ms@2.1.3: + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } + + nanoid@3.3.20: + resolution: + { + integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + hasBin: true + + natural-compare@1.4.0: + resolution: + { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, + } + + obug@3.0.0: + resolution: + { + integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==, + } + engines: { node: ">=12.20.0" } + + optionator@0.9.4: + resolution: + { + integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, + } + engines: { node: ">= 0.8.0" } + + p-limit@3.1.0: + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: ">=10" } + + p-locate@5.0.0: + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: ">=10" } + + parent-module@1.0.1: + resolution: + { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + } + engines: { node: ">=6" } + + path-exists@4.0.0: + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: ">=8" } + + path-key@3.1.1: + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: ">=8" } + + picocolors@1.1.1: + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } + + picomatch@4.0.7: + resolution: + { + integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==, + } + engines: { node: ">=12" } + + playwright-core@1.64.1: + resolution: + { + integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==, + } + engines: { node: ">=20" } + hasBin: true + + playwright@1.64.1: + resolution: + { + integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==, + } + engines: { node: ">=20" } + hasBin: true + + postcss@8.5.32: + resolution: + { + integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==, + } + engines: { node: ^10 || ^12 || >=14 } + + prelude-ls@1.2.1: + resolution: + { + integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, + } + engines: { node: ">= 0.8.0" } + + prettier@3.9.10: + resolution: + { + integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==, + } + engines: { node: ">=14" } + hasBin: true + + punycode@2.3.1: + resolution: + { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, + } + engines: { node: ">=6" } + + resolve-from@4.0.0: + resolution: + { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, + } + engines: { node: ">=4" } + + robust-predicates@3.0.3: + resolution: + { + integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==, + } + + rollup@4.67.4: + resolution: + { + integrity: sha512-Lsv3oKcz5jgOQJej/p7nfwbVEdAbDT+z6OPf/SCdI+nF+q2ENp0pTKT35ZwXAmH8g45mfrIiJeEyAqKYSYoL9Q==, + } + engines: { node: ">=18.0.0", npm: ">=8.0.0" } + hasBin: true + + rw@1.3.3: + resolution: + { + integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==, + } + + safer-buffer@2.1.2: + resolution: + { + integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, + } + + semver@7.8.6: + resolution: + { + integrity: sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==, + } + engines: { node: ">=10" } + hasBin: true + + shebang-command@2.0.0: + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: ">=8" } + + shebang-regex@3.0.0: + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: ">=8" } + + source-map-js@1.2.2: + resolution: + { + integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==, + } + engines: { node: ">=0.10.0" } + + std-env@4.3.0: + resolution: + { + integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==, + } + + strip-json-comments@3.1.1: + resolution: + { + integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, + } + engines: { node: ">=8" } + + supports-color@7.2.0: + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, + } + engines: { node: ">=8" } + + tinybench@6.2.3: + resolution: + { + integrity: sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw==, + } + engines: { node: ">=20.0.0" } + + tinyexec@1.3.1: + resolution: + { + integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==, + } + engines: { node: ">=18" } + + tinyglobby@0.2.17: + resolution: + { + integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==, + } + engines: { node: ">=12.0.0" } + + ts-api-utils@2.5.0: + resolution: + { + integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==, + } + engines: { node: ">=18.12" } + peerDependencies: + typescript: ">=4.8.4" + + type-check@0.4.0: + resolution: + { + integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, + } + engines: { node: ">= 0.8.0" } + + typescript-eslint@8.72.1: + resolution: + { + integrity: sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + + typescript@5.9.3: + resolution: + { + integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, + } + engines: { node: ">=14.17" } + hasBin: true + + undici-types@6.21.0: + resolution: + { + integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==, + } + + uri-js@4.4.1: + resolution: + { + integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, + } + + vite@6.4.5: + resolution: + { + integrity: sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==, + } + engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } + hasBin: true + peerDependencies: + "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: ">=1.21.0" + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.1.0: + resolution: + { + integrity: sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==, + } + engines: { node: ^22.12.0 || ^24.0.0 || >=26.0.0 } + hasBin: true + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 5.1.0 + "@vitest/browser-preview": 5.1.0 + "@vitest/browser-webdriverio": ^5.0.0-beta.5 || >=5.0.0 + "@vitest/coverage-istanbul": 5.1.0 + "@vitest/coverage-v8": 5.1.0 + "@vitest/ui": 5.1.0 + happy-dom: "*" + jsdom: "*" + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/coverage-istanbul": + optional: true + "@vitest/coverage-v8": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: ">= 8" } + hasBin: true + + why-is-node-running@3.2.1: + resolution: + { + integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==, + } + engines: { node: ">=20.11" } + hasBin: true + + word-wrap@1.2.5: + resolution: + { + integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, + } + engines: { node: ">=0.10.0" } + + yocto-queue@0.1.0: + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, + } + engines: { node: ">=10" } + +snapshots: + "@esbuild/aix-ppc64@0.25.12": + optional: true + + "@esbuild/android-arm64@0.25.12": + optional: true + + "@esbuild/android-arm@0.25.12": + optional: true + + "@esbuild/android-x64@0.25.12": + optional: true + + "@esbuild/darwin-arm64@0.25.12": + optional: true + + "@esbuild/darwin-x64@0.25.12": + optional: true + + "@esbuild/freebsd-arm64@0.25.12": + optional: true + + "@esbuild/freebsd-x64@0.25.12": + optional: true + + "@esbuild/linux-arm64@0.25.12": + optional: true + + "@esbuild/linux-arm@0.25.12": + optional: true + + "@esbuild/linux-ia32@0.25.12": + optional: true + + "@esbuild/linux-loong64@0.25.12": + optional: true + + "@esbuild/linux-mips64el@0.25.12": + optional: true + + "@esbuild/linux-ppc64@0.25.12": + optional: true + + "@esbuild/linux-riscv64@0.25.12": + optional: true + + "@esbuild/linux-s390x@0.25.12": + optional: true + + "@esbuild/linux-x64@0.25.12": + optional: true + + "@esbuild/netbsd-arm64@0.25.12": + optional: true + + "@esbuild/netbsd-x64@0.25.12": + optional: true + + "@esbuild/openbsd-arm64@0.25.12": + optional: true + + "@esbuild/openbsd-x64@0.25.12": + optional: true + + "@esbuild/openharmony-arm64@0.25.12": + optional: true + + "@esbuild/sunos-x64@0.25.12": + optional: true + + "@esbuild/win32-arm64@0.25.12": + optional: true + + "@esbuild/win32-ia32@0.25.12": + optional: true + + "@esbuild/win32-x64@0.25.12": + optional: true + + "@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(supports-color@7.2.0))": + dependencies: + eslint: 9.39.5(supports-color@7.2.0) + eslint-visitor-keys: 3.4.3 + + "@eslint-community/regexpp@4.12.2": {} + + "@eslint/config-array@0.21.2(supports-color@7.2.0)": + dependencies: + "@eslint/object-schema": 2.1.7 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + "@eslint/config-helpers@0.4.2": + dependencies: + "@eslint/core": 0.17.0 + + "@eslint/core@0.17.0": + dependencies: + "@types/json-schema": 7.0.15 + + "@eslint/eslintrc@3.3.7(supports-color@7.2.0)": + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@7.2.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + "@eslint/js@9.39.5": {} + + "@eslint/object-schema@2.1.7": {} + + "@eslint/plugin-kit@0.4.1": + dependencies: + "@eslint/core": 0.17.0 + levn: 0.4.1 + + "@humanfs/core@0.19.2": + dependencies: + "@humanfs/types": 0.15.0 + + "@humanfs/node@0.16.8": + dependencies: + "@humanfs/core": 0.19.2 + "@humanfs/types": 0.15.0 + "@humanwhocodes/retry": 0.4.3 + + "@humanfs/types@0.15.0": {} + + "@humanwhocodes/module-importer@1.0.1": {} + + "@humanwhocodes/retry@0.4.3": {} + + "@jridgewell/resolve-uri@3.1.2": {} + + "@jridgewell/sourcemap-codec@1.6.0": {} + + "@jridgewell/trace-mapping@0.3.31": + dependencies: + "@jridgewell/resolve-uri": 3.1.2 + "@jridgewell/sourcemap-codec": 1.6.0 + + "@napi-rs/lzma-linux-x64-gnu@1.5.1": + optional: true + + "@playwright/test@1.64.1": + dependencies: + playwright: 1.64.1 + + "@rollup/rollup-android-arm-eabi@4.67.4": + optional: true + + "@rollup/rollup-android-arm64@4.67.4": + optional: true + + "@rollup/rollup-android-x64@4.67.4": + optional: true + + "@rollup/rollup-darwin-arm64@4.67.4": + optional: true + + "@rollup/rollup-darwin-x64@4.67.4": + optional: true + + "@rollup/rollup-freebsd-arm64@4.67.4": + optional: true + + "@rollup/rollup-freebsd-x64@4.67.4": + optional: true + + "@rollup/rollup-linux-arm-gnueabihf@4.67.4": + optional: true + + "@rollup/rollup-linux-arm-musleabihf@4.67.4": + optional: true + + "@rollup/rollup-linux-arm64-gnu@4.67.4": + optional: true + + "@rollup/rollup-linux-arm64-musl@4.67.4": + optional: true + + "@rollup/rollup-linux-loong64-gnu@4.67.4": + optional: true + + "@rollup/rollup-linux-loong64-musl@4.67.4": + optional: true + + "@rollup/rollup-linux-ppc64-gnu@4.67.4": + optional: true + + "@rollup/rollup-linux-ppc64-musl@4.67.4": + optional: true + + "@rollup/rollup-linux-riscv64-gnu@4.67.4": + optional: true + + "@rollup/rollup-linux-riscv64-musl@4.67.4": + optional: true + + "@rollup/rollup-linux-s390x-gnu@4.67.4": + optional: true + + "@rollup/rollup-linux-x64-gnu@4.67.4": + optional: true + + "@rollup/rollup-linux-x64-musl@4.67.4": + optional: true + + "@rollup/rollup-openbsd-x64@4.67.4": + optional: true + + "@rollup/rollup-openharmony-arm64@4.67.4": + optional: true + + "@rollup/rollup-win32-arm64-msvc@4.67.4": + optional: true + + "@rollup/rollup-win32-ia32-msvc@4.67.4": + optional: true + + "@rollup/rollup-win32-x64-gnu@4.67.4": + optional: true + + "@rollup/rollup-win32-x64-msvc@4.67.4": + optional: true + + "@types/chai@5.2.3": + dependencies: + "@types/deep-eql": 4.0.2 + assertion-error: 2.0.1 + + "@types/d3-array@3.2.2": {} + + "@types/d3-axis@3.0.6": + dependencies: + "@types/d3-selection": 3.0.12 + + "@types/d3-brush@3.0.6": + dependencies: + "@types/d3-selection": 3.0.12 + + "@types/d3-chord@3.0.6": {} + + "@types/d3-color@3.1.3": {} + + "@types/d3-contour@3.0.6": + dependencies: + "@types/d3-array": 3.2.2 + "@types/geojson": 7946.0.16 + + "@types/d3-delaunay@6.0.4": {} + + "@types/d3-dispatch@3.0.7": {} + + "@types/d3-drag@3.0.7": + dependencies: + "@types/d3-selection": 3.0.12 + + "@types/d3-dsv@3.0.7": {} + + "@types/d3-ease@3.0.2": {} + + "@types/d3-fetch@3.0.7": + dependencies: + "@types/d3-dsv": 3.0.7 + + "@types/d3-force@3.0.10": {} + + "@types/d3-format@3.0.4": {} + + "@types/d3-geo@3.1.1": + dependencies: + "@types/geojson": 7946.0.16 + + "@types/d3-hierarchy@3.1.7": {} + + "@types/d3-interpolate@3.0.4": + dependencies: + "@types/d3-color": 3.1.3 + + "@types/d3-path@3.1.1": {} + + "@types/d3-polygon@3.0.2": {} + + "@types/d3-quadtree@3.0.6": {} + + "@types/d3-random@3.0.4": {} + + "@types/d3-scale-chromatic@3.1.0": {} + + "@types/d3-scale@4.0.9": + dependencies: + "@types/d3-time": 3.0.4 + + "@types/d3-selection@3.0.12": {} + + "@types/d3-shape@3.2.0": + dependencies: + "@types/d3-path": 3.1.1 + + "@types/d3-time-format@4.0.3": {} + + "@types/d3-time@3.0.4": {} + + "@types/d3-timer@3.0.2": {} + + "@types/d3-transition@3.0.9": + dependencies: + "@types/d3-selection": 3.0.12 + + "@types/d3-zoom@3.0.9": + dependencies: + "@types/d3-interpolate": 3.0.4 + "@types/d3-selection": 3.0.12 + + "@types/d3@7.4.3": + dependencies: + "@types/d3-array": 3.2.2 + "@types/d3-axis": 3.0.6 + "@types/d3-brush": 3.0.6 + "@types/d3-chord": 3.0.6 + "@types/d3-color": 3.1.3 + "@types/d3-contour": 3.0.6 + "@types/d3-delaunay": 6.0.4 + "@types/d3-dispatch": 3.0.7 + "@types/d3-drag": 3.0.7 + "@types/d3-dsv": 3.0.7 + "@types/d3-ease": 3.0.2 + "@types/d3-fetch": 3.0.7 + "@types/d3-force": 3.0.10 + "@types/d3-format": 3.0.4 + "@types/d3-geo": 3.1.1 + "@types/d3-hierarchy": 3.1.7 + "@types/d3-interpolate": 3.0.4 + "@types/d3-path": 3.1.1 + "@types/d3-polygon": 3.0.2 + "@types/d3-quadtree": 3.0.6 + "@types/d3-random": 3.0.4 + "@types/d3-scale": 4.0.9 + "@types/d3-scale-chromatic": 3.1.0 + "@types/d3-selection": 3.0.12 + "@types/d3-shape": 3.2.0 + "@types/d3-time": 3.0.4 + "@types/d3-time-format": 4.0.3 + "@types/d3-timer": 3.0.2 + "@types/d3-transition": 3.0.9 + "@types/d3-zoom": 3.0.9 + + "@types/deep-eql@4.0.2": {} + + "@types/estree@1.0.10": {} + + "@types/geojson@7946.0.16": {} + + "@types/json-schema@7.0.15": {} + + "@types/node@22.20.5": + dependencies: + undici-types: 6.21.0 + + "@typescript-eslint/eslint-plugin@8.72.1(@typescript-eslint/parser@8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@eslint-community/regexpp": 4.12.2 + "@typescript-eslint/parser": 8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/scope-manager": 8.72.1 + "@typescript-eslint/type-utils": 8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/utils": 8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/visitor-keys": 8.72.1 + eslint: 9.39.5(supports-color@7.2.0) + ignore: 7.0.14 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/parser@8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/scope-manager": 8.72.1 + "@typescript-eslint/types": 8.72.1 + "@typescript-eslint/typescript-estree": 8.72.1(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/visitor-keys": 8.72.1 + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/project-service@8.72.1(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/tsconfig-utils": 8.72.1(typescript@5.9.3) + "@typescript-eslint/types": 8.72.1 + debug: 4.4.3(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/scope-manager@8.72.1": + dependencies: + "@typescript-eslint/types": 8.72.1 + "@typescript-eslint/visitor-keys": 8.72.1 + + "@typescript-eslint/tsconfig-utils@8.72.1(typescript@5.9.3)": + dependencies: + typescript: 5.9.3 + + "@typescript-eslint/type-utils@8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/types": 8.72.1 + "@typescript-eslint/typescript-estree": 8.72.1(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/utils": 8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/types@8.72.1": {} + + "@typescript-eslint/typescript-estree@8.72.1(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@typescript-eslint/project-service": 8.72.1(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/tsconfig-utils": 8.72.1(typescript@5.9.3) + "@typescript-eslint/types": 8.72.1 + "@typescript-eslint/visitor-keys": 8.72.1 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.6 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/utils@8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)": + dependencies: + "@eslint-community/eslint-utils": 4.10.1(eslint@9.39.5(supports-color@7.2.0)) + "@typescript-eslint/scope-manager": 8.72.1 + "@typescript-eslint/types": 8.72.1 + "@typescript-eslint/typescript-estree": 8.72.1(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/visitor-keys@8.72.1": + dependencies: + "@typescript-eslint/types": 8.72.1 + eslint-visitor-keys: 5.0.1 + + "@vitest/mocker@5.1.0(vite@6.4.5(@types/node@22.20.5))": + dependencies: + "@jridgewell/trace-mapping": 0.3.31 + "@vitest/spy": 5.1.0 + estree-walker: 3.0.3 + magic-string: 1.4.5 + optionalDependencies: + vite: 6.4.5(@types/node@22.20.5) + + "@vitest/spy@5.1.0": {} + + acorn-jsx@5.3.2(acorn@8.19.0): + dependencies: + acorn: 8.19.0 + + acorn@8.19.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + brace-expansion@1.1.21: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + callsites@3.1.0: {} + + chai@6.3.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@7.2.0: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + d3-array@3.2.5: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.1: {} + + d3-brush@3.0.1: + dependencies: + d3-dispatch: 3.0.2 + d3-drag: 3.0.1 + d3-interpolate: 3.0.2 + d3-selection: 3.0.1 + d3-transition: 3.0.2(d3-selection@3.0.1) + + d3-chord@3.0.2: + dependencies: + d3-path: 3.1.1 + + d3-color@3.1.1: {} + + d3-contour@4.0.3: + dependencies: + d3-array: 3.2.5 + + d3-delaunay@6.0.6: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.2: {} + + d3-drag@3.0.1: + dependencies: + d3-dispatch: 3.0.2 + d3-selection: 3.0.1 + + d3-dsv@3.0.2: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.2: {} + + d3-fetch@3.0.2: + dependencies: + d3-dsv: 3.0.2 + + d3-force@3.0.1: + dependencies: + d3-dispatch: 3.0.2 + d3-quadtree: 3.0.2 + d3-timer: 3.0.2 + + d3-format@3.1.3: {} + + d3-geo@3.1.2: + dependencies: + d3-array: 3.2.5 + + d3-hierarchy@3.1.3: {} + + d3-interpolate@3.0.2: + dependencies: + d3-color: 3.1.1 + + d3-path@3.1.1: {} + + d3-polygon@3.0.2: {} + + d3-quadtree@3.0.2: {} + + d3-random@3.0.2: {} + + d3-scale-chromatic@3.1.1: + dependencies: + d3-color: 3.1.1 + d3-interpolate: 3.0.2 + d3-scale: 4.0.3 + + d3-scale@4.0.3: + dependencies: + d3-array: 3.2.5 + d3-format: 3.1.3 + d3-interpolate: 3.0.2 + d3-time: 3.1.1 + d3-time-format: 4.1.1 + + d3-selection@3.0.1: {} + + d3-shape@3.2.1: + dependencies: + d3-path: 3.1.1 + + d3-time-format@4.1.1: + dependencies: + d3-time: 3.1.1 + + d3-time@3.1.1: + dependencies: + d3-array: 3.2.5 + + d3-timer@3.0.2: {} + + d3-transition@3.0.2(d3-selection@3.0.1): + dependencies: + d3-color: 3.1.1 + d3-dispatch: 3.0.2 + d3-ease: 3.0.2 + d3-interpolate: 3.0.2 + d3-selection: 3.0.1 + d3-timer: 3.0.2 + + d3-zoom@3.0.1: + dependencies: + d3-dispatch: 3.0.2 + d3-drag: 3.0.1 + d3-interpolate: 3.0.2 + d3-selection: 3.0.1 + d3-transition: 3.0.2(d3-selection@3.0.1) + + d3@7.9.2: + dependencies: + d3-array: 3.2.5 + d3-axis: 3.0.1 + d3-brush: 3.0.1 + d3-chord: 3.0.2 + d3-color: 3.1.1 + d3-contour: 4.0.3 + d3-delaunay: 6.0.6 + d3-dispatch: 3.0.2 + d3-drag: 3.0.1 + d3-dsv: 3.0.2 + d3-ease: 3.0.2 + d3-fetch: 3.0.2 + d3-force: 3.0.1 + d3-format: 3.1.3 + d3-geo: 3.1.2 + d3-hierarchy: 3.1.3 + d3-interpolate: 3.0.2 + d3-path: 3.1.1 + d3-polygon: 3.0.2 + d3-quadtree: 3.0.2 + d3-random: 3.0.2 + d3-scale: 4.0.3 + d3-scale-chromatic: 3.1.1 + d3-selection: 3.0.1 + d3-shape: 3.2.1 + d3-time: 3.1.1 + d3-time-format: 4.1.1 + d3-timer: 3.0.2 + d3-transition: 3.0.2(d3-selection@3.0.1) + d3-zoom: 3.0.1 + + debug@4.4.3(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + deep-is@0.1.4: {} + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + es-module-lexer@2.3.2: {} + + esbuild@0.25.12: + optionalDependencies: + "@esbuild/aix-ppc64": 0.25.12 + "@esbuild/android-arm": 0.25.12 + "@esbuild/android-arm64": 0.25.12 + "@esbuild/android-x64": 0.25.12 + "@esbuild/darwin-arm64": 0.25.12 + "@esbuild/darwin-x64": 0.25.12 + "@esbuild/freebsd-arm64": 0.25.12 + "@esbuild/freebsd-x64": 0.25.12 + "@esbuild/linux-arm": 0.25.12 + "@esbuild/linux-arm64": 0.25.12 + "@esbuild/linux-ia32": 0.25.12 + "@esbuild/linux-loong64": 0.25.12 + "@esbuild/linux-mips64el": 0.25.12 + "@esbuild/linux-ppc64": 0.25.12 + "@esbuild/linux-riscv64": 0.25.12 + "@esbuild/linux-s390x": 0.25.12 + "@esbuild/linux-x64": 0.25.12 + "@esbuild/netbsd-arm64": 0.25.12 + "@esbuild/netbsd-x64": 0.25.12 + "@esbuild/openbsd-arm64": 0.25.12 + "@esbuild/openbsd-x64": 0.25.12 + "@esbuild/openharmony-arm64": 0.25.12 + "@esbuild/sunos-x64": 0.25.12 + "@esbuild/win32-arm64": 0.25.12 + "@esbuild/win32-ia32": 0.25.12 + "@esbuild/win32-x64": 0.25.12 + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5(supports-color@7.2.0): + dependencies: + "@eslint-community/eslint-utils": 4.10.1(eslint@9.39.5(supports-color@7.2.0)) + "@eslint-community/regexpp": 4.12.2 + "@eslint/config-array": 0.21.2(supports-color@7.2.0) + "@eslint/config-helpers": 0.4.2 + "@eslint/core": 0.17.0 + "@eslint/eslintrc": 3.3.7(supports-color@7.2.0) + "@eslint/js": 9.39.5 + "@eslint/plugin-kit": 0.4.1 + "@humanfs/node": 0.16.8 + "@humanwhocodes/module-importer": 1.0.1 + "@humanwhocodes/retry": 0.4.3 + "@types/estree": 1.0.10 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@7.2.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.19.0 + acorn-jsx: 5.3.2(acorn@8.19.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + "@types/estree": 1.0.10 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + fsevents@2.3.3: + optional: true + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.5.0: {} + + has-flag@4.0.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.14: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internmap@2.0.3: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + magic-string@1.4.5: + dependencies: + "@jridgewell/sourcemap-codec": 1.6.0 + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.21 + + ms@2.1.3: {} + + nanoid@3.3.20: {} + + natural-compare@1.4.0: {} + + obug@3.0.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + playwright-core@1.64.1: {} + + playwright@1.64.1: + dependencies: + playwright-core: 1.64.1 + + postcss@8.5.32: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + prelude-ls@1.2.1: {} + + prettier@3.9.10: {} + + punycode@2.3.1: {} + + resolve-from@4.0.0: {} + + robust-predicates@3.0.3: {} + + rollup@4.67.4: + dependencies: + "@types/estree": 1.0.10 + optionalDependencies: + "@napi-rs/lzma-linux-x64-gnu": 1.5.1 + "@rollup/rollup-android-arm-eabi": 4.67.4 + "@rollup/rollup-android-arm64": 4.67.4 + "@rollup/rollup-android-x64": 4.67.4 + "@rollup/rollup-darwin-arm64": 4.67.4 + "@rollup/rollup-darwin-x64": 4.67.4 + "@rollup/rollup-freebsd-arm64": 4.67.4 + "@rollup/rollup-freebsd-x64": 4.67.4 + "@rollup/rollup-linux-arm-gnueabihf": 4.67.4 + "@rollup/rollup-linux-arm-musleabihf": 4.67.4 + "@rollup/rollup-linux-arm64-gnu": 4.67.4 + "@rollup/rollup-linux-arm64-musl": 4.67.4 + "@rollup/rollup-linux-loong64-gnu": 4.67.4 + "@rollup/rollup-linux-loong64-musl": 4.67.4 + "@rollup/rollup-linux-ppc64-gnu": 4.67.4 + "@rollup/rollup-linux-ppc64-musl": 4.67.4 + "@rollup/rollup-linux-riscv64-gnu": 4.67.4 + "@rollup/rollup-linux-riscv64-musl": 4.67.4 + "@rollup/rollup-linux-s390x-gnu": 4.67.4 + "@rollup/rollup-linux-x64-gnu": 4.67.4 + "@rollup/rollup-linux-x64-musl": 4.67.4 + "@rollup/rollup-openbsd-x64": 4.67.4 + "@rollup/rollup-openharmony-arm64": 4.67.4 + "@rollup/rollup-win32-arm64-msvc": 4.67.4 + "@rollup/rollup-win32-ia32-msvc": 4.67.4 + "@rollup/rollup-win32-x64-gnu": 4.67.4 + "@rollup/rollup-win32-x64-msvc": 4.67.4 + fsevents: 2.3.3 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + + semver@7.8.6: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.2: {} + + std-env@4.3.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinybench@6.2.3: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + "@typescript-eslint/eslint-plugin": 8.72.1(@typescript-eslint/parser@8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/parser": 8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/typescript-estree": 8.72.1(supports-color@7.2.0)(typescript@5.9.3) + "@typescript-eslint/utils": 8.72.1(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@6.4.5(@types/node@22.20.5): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.32 + rollup: 4.67.4 + tinyglobby: 0.2.17 + optionalDependencies: + "@types/node": 22.20.5 + fsevents: 2.3.3 + + vitest@5.1.0(@types/node@22.20.5)(vite@6.4.5(@types/node@22.20.5)): + dependencies: + "@types/chai": 5.2.3 + "@vitest/mocker": 5.1.0(vite@6.4.5(@types/node@22.20.5)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.5 + obug: 3.0.0 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.3 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 6.4.5(@types/node@22.20.5) + why-is-node-running: 3.2.1 + optionalDependencies: + "@types/node": 22.20.5 + transitivePeerDependencies: + - msw + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@3.2.1: {} + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} diff --git a/proflow_webview/pnpm-workspace.yaml b/proflow_webview/pnpm-workspace.yaml new file mode 100644 index 0000000..00f6fc4 --- /dev/null +++ b/proflow_webview/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: set this to true or false diff --git a/proflow_webview/src/graph-data.ts b/proflow_webview/src/graph-data.ts new file mode 100644 index 0000000..4e5cf77 --- /dev/null +++ b/proflow_webview/src/graph-data.ts @@ -0,0 +1,160 @@ +/** Define the typed graph contract shared with the Python JSON exporter. */ +import type { SimulationLinkDatum, SimulationNodeDatum } from "d3"; + +/** Describe a process node and its mutable D3 force-layout coordinates. */ +export interface GraphNode extends SimulationNodeDatum { + id: number; + x: number; + y: number; + name: string; + text: string; +} + +/** Describe a process link before or after D3 resolves endpoint IDs. */ +export interface GraphLink extends SimulationLinkDatum { + source: number | GraphNode; + target: number | GraphNode; + name: string; + key: number; + id: number; + linkindex?: number; +} + +/** Describe the frontend subset of the Python-exported node-link document. */ +export interface GraphData { + nodes: GraphNode[]; + links: GraphLink[]; +} + +/** Signal that the graph response does not satisfy the frontend contract. */ +export class GraphDataError extends Error { + /** Create a graph contract validation error with a useful message. */ + constructor(message: string) { + super(message); + this.name = "GraphDataError"; + } +} + +/** Check whether a JSON value is a non-null object record. */ +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** Check whether a JSON value is an integer. */ +function isInteger(value: unknown): value is number { + return typeof value === "number" && Number.isInteger(value); +} + +/** Validate graph data against the frontend contract. */ +function validateGraphData(value: unknown): { + nodes: unknown[]; + links: unknown[]; +} { + if ( + !isRecord(value) || + !Array.isArray(value.nodes) || + !Array.isArray(value.links) + ) { + throw new GraphDataError("Graph data must contain node and link arrays."); + } + return { nodes: value.nodes, links: value.links }; +} + +/** Validate a graph node against the frontend contract. */ +function validateGraphNode( + node: unknown, + index: number, +): asserts node is GraphNode { + if ( + !isRecord(node) || + !isInteger(node.id) || + typeof node.x !== "number" || + !Number.isFinite(node.x) || + typeof node.y !== "number" || + !Number.isFinite(node.y) || + typeof node.name !== "string" || + typeof node.text !== "string" + ) { + throw new GraphDataError(`Graph node at index ${index} is invalid.`); + } +} + +/** Validate a graph link against the frontend contract. */ +function validateGraphLink( + link: unknown, + index: number, + nodeIds: Set, +): asserts link is GraphLink { + if ( + !isRecord(link) || + !isInteger(link.source) || + !isInteger(link.target) || + !nodeIds.has(link.source) || + !nodeIds.has(link.target) || + typeof link.name !== "string" || + !isInteger(link.key) || + !isInteger(link.id) + ) { + throw new GraphDataError( + `Graph link at index ${index} has an invalid or unknown node endpoint.`, + ); + } +} + +/** + * Extract and validate graph nodes from the given graph data. + * @param value The graph data containing nodes and links. + * @returns An array of validated graph nodes. + */ +function getNodes(value: { nodes: unknown[]; links: unknown[] }): GraphNode[] { + return value.nodes.map((node, index) => { + validateGraphNode(node, index); + return { + id: node.id, + x: node.x, + y: node.y, + name: node.name, + text: node.text, + }; + }); +} + +function getLinks( + value: { nodes: unknown[]; links: unknown[] }, + nodeIds: Set, +): GraphLink[] { + return value.links.map((link, index) => { + validateGraphLink(link, index, nodeIds); + return { + source: link.source, + target: link.target, + name: link.name, + key: link.key, + id: link.id, + }; + }); +} + +/** Validate an unknown response and return the graph data used by D3. */ +export function parseGraphData(value: unknown): GraphData { + const validatedGraphData = validateGraphData(value); + const nodes = getNodes(validatedGraphData); + const nodeIds = new Set(nodes.map((node) => node.id)); + const links = getLinks(validatedGraphData, nodeIds); + return { nodes, links }; +} + +/** Fetch and validate graph data from the webview's JSON endpoint. */ +export async function fetchGraphData( + url = "/data", + fetcher: typeof fetch = fetch, +): Promise { + const response = await fetcher(url); + if (!response.ok) { + throw new Error( + `Graph data request failed with status ${response.status}.`, + ); + } + + return parseGraphData(await response.json()); +} diff --git a/proflow_webview/src/graph.ts b/proflow_webview/src/graph.ts new file mode 100644 index 0000000..843af9a --- /dev/null +++ b/proflow_webview/src/graph.ts @@ -0,0 +1,418 @@ +/** Render and manage the interactive process graph using D3. */ +import { + drag, + forceCenter, + forceLink, + forceManyBody, + forceSimulation, + select, + scaleOrdinal, + schemeCategory10, + type Selection, + zoom, + zoomIdentity, + type ZoomBehavior, +} from "d3"; +import type { GraphData, GraphLink, GraphNode } from "./graph-data"; + +/** Define the node rendering modes supported by the control panel. */ +export type NodeShape = "box" | "dot"; + +const NODE_WIDTH = 180; +const NODE_HEIGHT = 48; +const NODE_DOT_RADIUS = 5; + +/** Return the graph node represented by a D3-resolved link endpoint. */ +function endpointNode(endpoint: number | GraphNode): GraphNode { + if (typeof endpoint === "number") { + throw new Error("Graph link endpoints must be resolved before rendering."); + } + return endpoint; +} + +/** Return an endpoint's numeric ID before or after D3 resolves the link. */ +function endpointId(endpoint: number | GraphNode): number { + return typeof endpoint === "number" ? endpoint : endpoint.id; +} + +/** Match a node's ID, function name, or description against a search query. */ +export function matchesNode(node: GraphNode, query: string): boolean { + const normalizedQuery = query.trim().toLocaleLowerCase(); + return ( + normalizedQuery.length === 0 || + [node.id, node.name, node.text] + .join(" ") + .toLocaleLowerCase() + .includes(normalizedQuery) + ); +} + +/** Return all node IDs visible for the given search query. */ +export function getVisibleNodeIds( + nodes: GraphNode[], + query: string, +): Set { + return new Set( + nodes.filter((node) => matchesNode(node, query)).map((node) => node.id), + ); +} + +/** Calculate link endpoints on the selected node shape's boundary. */ +export function nodeBoundaryPoints( + link: GraphLink, + shape: NodeShape, +): { source: { x: number; y: number }; target: { x: number; y: number } } { + const source = endpointNode(link.source); + const target = endpointNode(link.target); + const dx = target.x - source.x; + const dy = target.y - source.y; + const distance = Math.hypot(dx, dy); + + if (shape === "dot") { + const offset = distance === 0 ? 0 : NODE_DOT_RADIUS / distance; + return { + source: { x: source.x + dx * offset, y: source.y + dy * offset }, + target: { x: target.x - dx * offset, y: target.y - dy * offset }, + }; + } + + return { + source: { x: source.x + NODE_WIDTH / 2, y: source.y }, + target: { x: target.x - NODE_WIDTH / 2, y: target.y }, + }; +} + +/** Build a cubic Bézier path whose box endpoints have horizontal tangents. */ +export function bezierPath( + leftHand: boolean, + link: GraphLink, + shape: NodeShape, +): string { + const endpoints = nodeBoundaryPoints(link, shape); + const start = leftHand ? endpoints.source : endpoints.target; + const end = leftHand ? endpoints.target : endpoints.source; + const startNode = endpointNode(leftHand ? link.source : link.target); + const endNode = endpointNode(leftHand ? link.target : link.source); + const dx = endNode.x - startNode.x; + const dy = endNode.y - startNode.y; + const distance = Math.hypot(dx, dy) || 1; + const controlDistance = + Math.max(NODE_WIDTH / 2, Math.abs(dx) / 2) + + ((link.linkindex ?? 1) - 1) * 12; + + const control1 = + shape === "box" + ? { x: start.x + (leftHand ? 1 : -1) * controlDistance, y: start.y } + : { + x: start.x + (dx / distance) * controlDistance, + y: start.y + (dy / distance) * controlDistance, + }; + const control2 = + shape === "box" + ? { x: end.x - (leftHand ? 1 : -1) * controlDistance, y: end.y } + : { + x: end.x - (dx / distance) * controlDistance, + y: end.y - (dy / distance) * controlDistance, + }; + + return `M${start.x},${start.y}C${control1.x},${control1.y} ${control2.x},${control2.y} ${end.x},${end.y}`; +} + +/** Sort links and assign stable offsets to parallel process dependencies. */ +function sortAndIndexLinks(links: GraphLink[]): void { + links.sort((left, right) => { + const sourceDifference = endpointId(left.source) - endpointId(right.source); + return ( + sourceDifference || endpointId(left.target) - endpointId(right.target) + ); + }); + + let previousPair = ""; + let pairIndex = 0; + for (const link of links) { + const pair = `${endpointId(link.source)},${endpointId(link.target)}`; + pairIndex = pair === previousPair ? pairIndex + 1 : 1; + link.linkindex = pairIndex; + previousPair = pair; + } +} + +/** Render the graph and connect its navigation and layout controls. */ +export function renderGraph(graph: GraphData): void { + const svgElement = document.querySelector("svg"); + const filterElement = + document.querySelector("#node-filter"); + const forceElement = + document.querySelector("#forces-toggle"); + const zoomInElement = document.querySelector("#zoom-in"); + const zoomOutElement = document.querySelector("#zoom-out"); + const zoomResetElement = + document.querySelector("#zoom-reset"); + const summaryElement = document.querySelector("#filter-summary"); + const shapeElements = Array.from( + document.querySelectorAll('input[name="node-shape"]'), + ); + + if ( + !svgElement || + !filterElement || + !forceElement || + !zoomInElement || + !zoomOutElement || + !zoomResetElement || + !summaryElement || + shapeElements.length < 2 + ) { + throw new Error("The webview page is missing required graph controls."); + } + + const nodeFilter = filterElement; + const filterSummary = summaryElement; + const svg = select(svgElement); + const width = Number(svg.attr("width")); + const height = Number(svg.attr("height")); + const colors = scaleOrdinal(schemeCategory10); + let shape: NodeShape = "box"; + let forcesEnabled = forceElement.checked; + + sortAndIndexLinks(graph.links); + svg + .append("defs") + .append("marker") + .attr("id", "arrowhead") + .attr("viewBox", "-0 -5 10 10") + .attr("refX", 13) + .attr("refY", 0) + .attr("orient", "auto") + .attr("markerWidth", 13) + .attr("markerHeight", 13) + .attr("overflow", "visible") + .append("path") + .attr("d", "M 0,-5 L 10,0 L 0,5") + .attr("fill", "#999") + .style("stroke", "none"); + + const graphContent = svg.append("g").attr("class", "graph-content"); + const zoomBehavior: ZoomBehavior = zoom< + SVGSVGElement, + unknown + >() + .scaleExtent([0.2, 4]) + .filter((event: Event) => { + const startsNodeDrag = + (event.type === "mousedown" || event.type === "touchstart") && + event.target instanceof Element && + event.target.closest(".node") !== null; + const hasNonPrimaryButton = + "button" in event && + typeof event.button === "number" && + event.button !== 0; + const isUnmodifiedGesture = + !("ctrlKey" in event && event.ctrlKey === true) || + event.type === "wheel"; + return !hasNonPrimaryButton && isUnmodifiedGesture && !startsNodeDrag; + }) + .on("zoom", (event) => { + graphContent.attr("transform", event.transform); + }); + svg.call(zoomBehavior); + zoomInElement.addEventListener("click", () => { + svg.call(zoomBehavior.scaleBy, 1.2); + }); + zoomOutElement.addEventListener("click", () => { + svg.call(zoomBehavior.scaleBy, 1 / 1.2); + }); + zoomResetElement.addEventListener("click", () => { + svg.call(zoomBehavior.transform, zoomIdentity); + }); + + const linkForce = forceLink() + .id((node) => node.id) + .distance(500) + .strength(0.5); + const simulation = forceSimulation() + .force("link", linkForce) + .force("charge", forceManyBody()) + .force("center", forceCenter(width / 2, height / 2)); + + const linkSelection: Selection< + SVGPathElement, + GraphLink, + SVGGElement, + unknown + > = graphContent + .selectAll(".link") + .data(graph.links) + .enter() + .append("path") + .attr("class", "link") + .attr("marker-end", "url(#arrowhead)"); + linkSelection.append("title").text((link) => link.name); + + const invisiblePathSelection: Selection< + SVGPathElement, + GraphLink, + SVGGElement, + unknown + > = graphContent + .selectAll(".pathInvis") + .data(graph.links) + .enter() + .append("path") + .attr( + "id", + (link) => + `invis_${endpointId(link.source)}-${link.linkindex}-${endpointId(link.target)}`, + ) + .attr("class", "pathInvis"); + + const labelSelection: Selection< + SVGTextElement, + GraphLink, + SVGGElement, + unknown + > = graphContent + .selectAll(".pathLabel") + .data(graph.links) + .enter() + .append("text") + .attr("class", "pathLabel") + .attr("text-anchor", "middle"); + labelSelection + .append("textPath") + .attr("startOffset", "50%") + .attr( + "href", + (link) => + `#invis_${endpointId(link.source)}-${link.linkindex}-${endpointId(link.target)}`, + ) + .style("fill", "#ccc") + .style("font-size", 10) + .text((link) => link.name); + + const nodeSelection: Selection = + graphContent + .selectAll(".node") + .data(graph.nodes) + .enter() + .append("g") + .attr("class", "node") + .call( + drag() + .on("start", (event, node) => { + if (forcesEnabled && !event.active) { + simulation.alphaTarget(0.02).restart(); + } + node.fx = node.x; + node.fy = node.y; + }) + .on("drag", (event, node) => { + node.fx = event.x; + node.fy = event.y; + if (!forcesEnabled) { + node.x = event.x; + node.y = event.y; + ticked(); + } + }), + ); + + nodeSelection.append("title").text((node) => String(node.id)); + nodeSelection.append("text").text((node) => `${node.name}:${node.text}`); + simulation.nodes(graph.nodes).on("tick", ticked); + linkForce.links(graph.links); + + for (const shapeElement of shapeElements) { + shapeElement.addEventListener("change", renderNodeShape); + } + nodeFilter.addEventListener("input", applyNodeFilter); + forceElement.addEventListener("change", () => { + forcesEnabled = forceElement.checked; + if (forcesEnabled) { + simulation.alpha(1).restart(); + } else { + simulation.stop(); + } + }); + + renderNodeShape(); + applyNodeFilter(); + + /** Update graph marks when the simulation advances or a node is dragged. */ + function ticked(): void { + linkSelection.attr("d", (link) => bezierPath(true, link, shape)); + invisiblePathSelection.attr("d", (link) => { + const source = endpointNode(link.source); + const target = endpointNode(link.target); + return bezierPath(source.x < target.x, link, shape); + }); + nodeSelection.attr( + "transform", + (node) => `translate(${node.x}, ${node.y})`, + ); + } + + /** Apply the selected shape and update label placement and link boundaries. */ + function renderNodeShape(): void { + const selectedShape = shapeElements.find( + (element) => element.checked, + )?.value; + if (selectedShape !== "box" && selectedShape !== "dot") { + throw new Error("A valid node shape must be selected."); + } + shape = selectedShape; + nodeSelection.selectAll(".node-shape").remove(); + + if (shape === "box") { + nodeSelection + .insert("rect", "title") + .attr("class", "node-shape") + .attr("x", -NODE_WIDTH / 2) + .attr("y", -NODE_HEIGHT / 2) + .attr("width", NODE_WIDTH) + .attr("height", NODE_HEIGHT) + .attr("rx", 4) + .style("fill", (_node, index) => colors(index)); + } else { + nodeSelection + .insert("circle", "title") + .attr("class", "node-shape") + .attr("r", NODE_DOT_RADIUS) + .style("fill", (_node, index) => colors(index)); + } + + nodeSelection + .select("text") + .attr("x", shape === "box" ? 0 : NODE_DOT_RADIUS + 4) + .attr("dy", shape === "box" ? 4 : -3) + .attr("text-anchor", shape === "box" ? "middle" : "start"); + ticked(); + } + + /** Filter nodes, links, and labels, then announce the visible node count. */ + function applyNodeFilter(): void { + const visibleIds = getVisibleNodeIds(graph.nodes, nodeFilter.value); + nodeSelection.style("display", (node) => + visibleIds.has(node.id) ? null : "none", + ); + + /** Return whether both link endpoints are present in the filtered graph. */ + function linkIsVisible(link: GraphLink): boolean { + return ( + visibleIds.has(endpointId(link.source)) && + visibleIds.has(endpointId(link.target)) + ); + } + + linkSelection.style("display", (link) => + linkIsVisible(link) ? null : "none", + ); + invisiblePathSelection.style("display", (link) => + linkIsVisible(link) ? null : "none", + ); + labelSelection.style("display", (link) => + linkIsVisible(link) ? null : "none", + ); + filterSummary.textContent = `${visibleIds.size} of ${graph.nodes.length} nodes shown`; + } +} diff --git a/proflow_webview/src/main.ts b/proflow_webview/src/main.ts new file mode 100644 index 0000000..52c6ac5 --- /dev/null +++ b/proflow_webview/src/main.ts @@ -0,0 +1,22 @@ +/** Fetch the exported graph and initialize the process network webview. */ +import "./style.css"; +import { fetchGraphData } from "./graph-data"; +import { renderGraph } from "./graph"; + +/** Load the graph and display failures in the accessible alert region. */ +async function startWebview(): Promise { + try { + renderGraph(await fetchGraphData()); + } catch (error) { + const alert = document.querySelector("#webview-error"); + const message = + error instanceof Error ? error.message : "An unknown error occurred."; + console.error("Unable to load process graph:", error); + if (alert) { + alert.textContent = `Unable to load process graph: ${message}`; + alert.hidden = false; + } + } +} + +void startWebview(); diff --git a/proflow_webview/src/style.css b/proflow_webview/src/style.css new file mode 100644 index 0000000..9df7f63 --- /dev/null +++ b/proflow_webview/src/style.css @@ -0,0 +1,140 @@ +/** Define the responsive graph canvas and its accessible control panel. */ +html, +body { + width: 100%; + height: 100%; + margin: 0; +} + +body { + overflow: hidden; + font-family: system-ui, sans-serif; +} + +svg { + display: block; + width: 100%; + height: 100%; +} + +.control-panel { + position: fixed; + z-index: 1; + top: 16px; + left: 16px; + width: min(320px, calc(100vw - 64px)); + padding: 16px; + border: 1px solid #d5d9de; + border-radius: 8px; + background: #fff; + box-shadow: 0 2px 10px rgb(0 0 0 / 14%); +} + +.control-panel h1 { + margin: 0 0 12px; + font-size: 16px; +} + +.control-panel label { + display: block; + margin-bottom: 6px; + font-size: 13px; + font-weight: 600; +} + +.control-panel input[type="search"] { + box-sizing: border-box; + width: 100%; + padding: 9px 10px; + border: 1px solid #aeb5bd; + border-radius: 4px; + font: inherit; +} + +.zoom-controls, +.node-style-options, +.force-options { + margin: 14px 0 0; + padding: 0; + border: 0; +} + +.zoom-controls legend, +.node-style-options legend, +.force-options legend { + margin-bottom: 6px; + font-size: 13px; + font-weight: 600; +} + +.zoom-control-buttons { + display: flex; + gap: 8px; +} + +.zoom-control-buttons button { + flex: 1; + padding: 6px 8px; + border: 1px solid #aeb5bd; + border-radius: 4px; + background: #f8fafc; + color: inherit; + cursor: pointer; + font: inherit; +} + +.node-style-options label, +.force-options label { + display: inline-flex; + align-items: center; + gap: 6px; + margin: 0 12px 0 0; + font-weight: normal; +} + +.node-style-options input[type="radio"], +.force-options input[type="checkbox"] { + width: auto; + margin: 0; + padding: 0; +} + +.control-panel input:focus-visible, +.control-panel button:focus-visible { + outline: 2px solid #2563eb; + outline-offset: 2px; +} + +.control-panel p { + margin: 10px 0 0; + color: #555; + font-size: 12px; +} + +#webview-error:not([hidden]) { + color: #b42318; +} + +.node rect, +.node circle { + cursor: pointer; + stroke: #000; + stroke-width: 0.5px; +} + +.node text { + font-size: 12px; + pointer-events: none; +} + +.pathInvis { + fill: none; + stroke: none; +} + +.link { + fill: none; + stroke: #999; + stroke-opacity: 0.6; + stroke-width: 1px; +} diff --git a/proflow_webview/tests/e2e/webview.spec.ts b/proflow_webview/tests/e2e/webview.spec.ts new file mode 100644 index 0000000..564a016 --- /dev/null +++ b/proflow_webview/tests/e2e/webview.spec.ts @@ -0,0 +1,159 @@ +/** Exercise webview loading and user-facing graph controls in Chromium. */ +import { expect, test } from "@playwright/test"; + +test("loads the graph and filters nodes and links", async ({ page }) => { + await page.goto("/"); + + const visibleNodes = page.locator("svg .node:visible"); + await expect(visibleNodes.first()).toBeVisible(); + const totalNodes = await visibleNodes.count(); + const search = page.getByRole("searchbox", { name: "Search nodes" }); + + await search.fill("no node can match this query"); + await expect(visibleNodes).toHaveCount(0); + await expect(page.locator("#filter-summary")).toHaveText( + `0 of ${totalNodes} nodes shown`, + ); + + await search.fill(""); + await expect(visibleNodes).toHaveCount(totalNodes); +}); + +test("switches node shapes and pauses the force layout", async ({ page }) => { + await page.goto("/"); + + const boxOption = page.getByRole("radio", { name: "Box" }); + const dotOption = page.getByRole("radio", { name: "Dot" }); + await expect(boxOption).toBeChecked(); + await page.locator("svg .node rect").first().waitFor(); + + await dotOption.check(); + await expect(page.locator("svg .node circle").first()).toBeVisible(); + await expect(page.locator("svg .node rect")).toHaveCount(0); + + const forceOption = page.getByRole("checkbox", { name: "Enable forces" }); + await forceOption.uncheck(); + await expect(forceOption).not.toBeChecked(); + await boxOption.check(); + const node = page.locator("svg .node").last(); + const bounds = await page.locator("svg .node rect").last().boundingBox(); + expect(bounds).not.toBeNull(); + if (!bounds) { + throw new Error("The graph node has no browser bounding box."); + } + const beforeDrag = await node.getAttribute("transform"); + await page.mouse.move( + bounds.x + bounds.width / 2, + bounds.y + bounds.height / 2, + ); + await page.mouse.down(); + await page.mouse.move( + bounds.x + bounds.width / 2 + 40, + bounds.y + bounds.height / 2 + 30, + ); + await page.mouse.up(); + await expect(node).not.toHaveAttribute("transform", beforeDrag ?? ""); +}); + +test("zooms and pans the graph without moving the control panel", async ({ + page, +}) => { + await page.goto("/"); + + const graph = page.locator("svg .graph-content"); + const controls = page.locator(".control-panel"); + const controlBoundsBefore = await controls.boundingBox(); + await page.getByRole("button", { name: "Zoom in" }).click(); + await expect(graph).toHaveAttribute("transform", /scale\(1\.2\)/); + + await page.getByRole("checkbox", { name: "Enable forces" }).uncheck(); + const node = page.locator("svg .node").last(); + const nodeBounds = await node.boundingBox(); + expect(nodeBounds).not.toBeNull(); + if (!nodeBounds) { + throw new Error("The graph node has no browser bounding box."); + } + const nodeTransformBeforeDrag = await node.getAttribute("transform"); + await page.mouse.move( + nodeBounds.x + nodeBounds.width / 2, + nodeBounds.y + nodeBounds.height / 2, + ); + await page.mouse.down(); + await page.mouse.move( + nodeBounds.x + nodeBounds.width / 2 + 30, + nodeBounds.y + nodeBounds.height / 2 + 20, + ); + await page.mouse.up(); + await expect(node).not.toHaveAttribute( + "transform", + nodeTransformBeforeDrag ?? "", + ); + + const scaleBeforeWheel = await graph.evaluate((element) => { + const match = element.getAttribute("transform")?.match(/scale\(([^)]+)\)/); + if (!match) { + throw new Error("The graph has no zoom scale."); + } + return Number(match[1]); + }); + const svgBounds = await page.locator("svg").boundingBox(); + expect(svgBounds).not.toBeNull(); + if (!svgBounds) { + throw new Error("The graph has no browser bounding box."); + } + await page.mouse.move( + svgBounds.x + svgBounds.width * 0.8, + svgBounds.y + svgBounds.height * 0.8, + ); + await page.mouse.wheel(0, -120); + + await expect + .poll(() => + graph.evaluate((element) => { + const match = element + .getAttribute("transform") + ?.match(/scale\(([^)]+)\)/); + return match ? Number(match[1]) : 1; + }), + ) + .toBeGreaterThan(scaleBeforeWheel); + await page.mouse.wheel(0, -100000); + await expect(graph).toHaveAttribute("transform", /scale\(4\)/); + + await page.getByRole("button", { name: "Reset" }).click(); + await expect(graph).toHaveAttribute("transform", /scale\(1\)/); + const transformBeforePan = await graph.getAttribute("transform"); + await page.mouse.move( + svgBounds.x + svgBounds.width * 0.9, + svgBounds.y + svgBounds.height * 0.9, + ); + await page.mouse.down(); + await page.mouse.move( + svgBounds.x + svgBounds.width * 0.9 - 40, + svgBounds.y + svgBounds.height * 0.9 - 30, + ); + await page.mouse.up(); + await expect(graph).not.toHaveAttribute( + "transform", + transformBeforePan ?? "", + ); + + const controlBoundsAfter = await controls.boundingBox(); + expect(controlBoundsAfter).toEqual(controlBoundsBefore); +}); + +test("announces graph data failures accessibly", async ({ page }) => { + await page.route("**/data", (route) => + route.fulfill({ + status: 503, + contentType: "application/json", + body: JSON.stringify({ error: "unavailable" }), + }), + ); + + await page.goto("/"); + + await expect(page.getByRole("alert")).toContainText( + "Unable to load process graph: Graph data request failed with status 503", + ); +}); diff --git a/proflow_webview/tests/graph-data.test.ts b/proflow_webview/tests/graph-data.test.ts new file mode 100644 index 0000000..70b39fb --- /dev/null +++ b/proflow_webview/tests/graph-data.test.ts @@ -0,0 +1,51 @@ +/** Verify validation and fetching at the Python-to-frontend graph boundary. */ +import { describe, expect, it } from "vitest"; +import { + fetchGraphData, + GraphDataError, + parseGraphData, +} from "../src/graph-data"; + +const validPayload = { + directed: true, + multigraph: true, + graph: {}, + nodes: [{ id: 1, x: 0, y: 0, name: "process", text: "description" }], + links: [{ source: 1, target: 1, name: "state", key: 0, id: 1 }], +}; + +describe("parseGraphData", () => { + it("accepts graph JSON matching the Python export contract", () => { + expect(parseGraphData(validPayload)).toEqual({ + nodes: [{ id: 1, x: 0, y: 0, name: "process", text: "description" }], + links: [{ source: 1, target: 1, name: "state", key: 0, id: 1 }], + }); + }); + + it("rejects malformed node records", () => { + expect(() => + parseGraphData({ + ...validPayload, + nodes: [{ id: "1", name: "process", text: "description" }], + }), + ).toThrow(GraphDataError); + }); + + it("rejects links whose endpoint is not a known node", () => { + expect(() => + parseGraphData({ + ...validPayload, + links: [{ source: 1, target: 2, name: "state", key: 0, id: 1 }], + }), + ).toThrow(/unknown node/); + }); + + it("reports unsuccessful graph-data requests", async () => { + const fetcher: typeof fetch = async () => + new Response(JSON.stringify({ error: "unavailable" }), { status: 503 }); + + await expect(fetchGraphData("/data", fetcher)).rejects.toThrow( + "Graph data request failed with status 503", + ); + }); +}); diff --git a/proflow_webview/tests/graph.test.ts b/proflow_webview/tests/graph.test.ts new file mode 100644 index 0000000..0d73ed2 --- /dev/null +++ b/proflow_webview/tests/graph.test.ts @@ -0,0 +1,78 @@ +/** Verify graph filtering and SVG geometry helpers independently of the DOM. */ +import { describe, expect, it } from "vitest"; +import { + bezierPath, + getVisibleNodeIds, + matchesNode, + nodeBoundaryPoints, +} from "../src/graph"; +import type { GraphLink, GraphNode } from "../src/graph-data"; + +const source: GraphNode = { + id: 1, + x: 0, + y: 0, + name: "producer", + text: "writes state", +}; +const target: GraphNode = { + id: 2, + x: 300, + y: 100, + name: "consumer", + text: "reads state", +}; +const link: GraphLink = { + source, + target, + name: "state.value", + key: 0, + id: 1, + linkindex: 1, +}; + +describe("node filtering", () => { + it("matches node IDs, names, and descriptions without regard to case", () => { + expect(matchesNode(source, "PRODUCER")).toBe(true); + expect(matchesNode(source, "writes")).toBe(true); + expect(matchesNode(source, "1")).toBe(true); + expect(matchesNode(source, "absent")).toBe(false); + }); + + it("returns only IDs matching the trimmed query", () => { + expect(getVisibleNodeIds([source, target], " consumer ")).toEqual( + new Set([2]), + ); + expect(getVisibleNodeIds([source, target], "")).toEqual(new Set([1, 2])); + }); +}); + +describe("link geometry", () => { + it("connects box links to their left and right edges", () => { + expect(nodeBoundaryPoints(link, "box")).toEqual({ + source: { x: 90, y: 0 }, + target: { x: 210, y: 100 }, + }); + }); + + it("connects dot links to their circumference", () => { + const points = nodeBoundaryPoints(link, "dot"); + const sourceDistance = Math.hypot( + points.source.x - source.x!, + points.source.y - source.y!, + ); + const targetDistance = Math.hypot( + points.target.x - target.x!, + points.target.y - target.y!, + ); + + expect(sourceDistance).toBeCloseTo(5); + expect(targetDistance).toBeCloseTo(5); + }); + + it("uses horizontal endpoint tangents for box links", () => { + const path = bezierPath(true, link, "box"); + + expect(path).toBe("M90,0C240,0 60,100 210,100"); + }); +}); diff --git a/proflow_webview/tsconfig.json b/proflow_webview/tsconfig.json new file mode 100644 index 0000000..cbdd551 --- /dev/null +++ b/proflow_webview/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client", "node", "vitest/globals"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "verbatimModuleSyntax": true + }, + "include": [ + "src", + "tests", + "vite.config.ts", + "vitest.config.ts", + "playwright.config.ts" + ] +} diff --git a/proflow_webview/vite.config.ts b/proflow_webview/vite.config.ts new file mode 100644 index 0000000..6015013 --- /dev/null +++ b/proflow_webview/vite.config.ts @@ -0,0 +1,23 @@ +/** Configure Vite's output and the Flask data proxy for webview development. */ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vite"; + +const pythonStaticOutput = fileURLToPath( + new URL( + "../proflow/analysis/webview/static/force/frontend-dist", + import.meta.url, + ), +); + +export default defineConfig({ + base: "/force/frontend-dist/", + build: { + outDir: pythonStaticOutput, + emptyOutDir: true, + }, + server: { + proxy: { + "/data": "http://127.0.0.1:8000", + }, + }, +}); diff --git a/proflow_webview/vitest.config.ts b/proflow_webview/vitest.config.ts new file mode 100644 index 0000000..e2e38e8 --- /dev/null +++ b/proflow_webview/vitest.config.ts @@ -0,0 +1,6 @@ +/** Configure Vitest to run fast frontend unit tests independently of Playwright. */ +export default { + test: { + include: ["tests/**/*.test.ts"], + }, +}; diff --git a/pyproject.toml b/pyproject.toml index 4bac615..a0f7f5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,7 @@ description = "Functional micro pipeline manager" authors = [{ "name" = "Sam Bland" }] readme = "README.md" requires-python = ">=3.14" -dependencies = [ - "data-helpers>=0.2.1", - "dill>=0.4.1", - "numpy>=1.18.3", -] +dependencies = ["data-helpers>=0.2.1", "dill>=0.4.1", "numpy>=1.18.3"] [dependency-groups] dev = [ "pytest>=8.3.3", @@ -39,12 +35,11 @@ ci = [ "ruff>=0.17.0", "pytest-benchmark>=3.4.1", "decorator>=5.0.0", -] - -web = [ "flask>=2.3.0", ] +web = ["flask>=2.3.0"] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/uv.lock b/uv.lock index 43a193b..c6e88cd 100644 --- a/uv.lock +++ b/uv.lock @@ -372,6 +372,7 @@ dependencies = [ [package.dev-dependencies] ci = [ { name = "decorator" }, + { name = "flask" }, { name = "pytest" }, { name = "pytest-benchmark" }, { name = "pytest-cov" }, @@ -411,6 +412,7 @@ requires-dist = [ [package.metadata.requires-dev] ci = [ { name = "decorator", specifier = ">=5.0.0" }, + { name = "flask", specifier = ">=2.3.0" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-benchmark", specifier = ">=3.4.1" }, { name = "pytest-cov", specifier = ">=5.0.0" },