diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 10cf638..059e365 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -96,14 +96,18 @@ jobs: retention-days: 5 if-no-files-found: error - integration: - name: PR / Linux integration + integration_shards: + name: PR / Linux integration shard ${{ matrix.shard }} needs: build - # Keep the required check red when preflight/build fails or is skipped. - if: always() + # Fail closed on upstream failure, but let superseded runs be cancelled. + if: ${{ !cancelled() }} runs-on: ubuntu-24.04 - # Measured test workload is ~39m; build time has its own job budget. + # Disjoint execution preserves every assertion and the per-job time budget. timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + shard: [0, 1, 2, 3] env: WGPU_BACKEND: vulkan FIGGY_TEST_JUPYTER: '1' @@ -113,6 +117,7 @@ jobs: FIGGY_REQUIRE_INTERACTION_PARITY: '1' FIGGYPLOT_MATURIN_ACTION: .ci/maturin-action/action.yml PYTHONUNBUFFERED: '1' + CI_SHARD: ${{ matrix.shard }} steps: - name: Require the verified wheel env: @@ -151,7 +156,7 @@ jobs: run: | .venv/bin/python -m playwright install --with-deps chromium .venv/bin/python scripts/ci_gpu_env.py - - name: Test previous failures first, then native and Jupyter integration + - name: Execute disjoint native and Jupyter test shard run: | mkdir -p ci-results xvfb-run -a -s '-screen 0 1600x1000x24' bash <<'SCRIPT' @@ -159,23 +164,10 @@ jobs: openbox > /tmp/figgyplot-openbox.log 2>&1 & wm_pid=$! trap 'kill "$wm_pid" 2>/dev/null || true' EXIT - # Reject the observed stale-font/box failures before the full suite. - # The queue-order cases must fail on the original a1e79a8 wheel. - .venv/bin/python -m pytest -vv --capture=tee-sys --durations=20 \ - --basetemp=ci-results/layout-probe --junitxml=ci-results/layout-probe.xml \ - 'tests/test_interaction_parity.py::test_inline_editor_resize_uses_current_units_and_applies_real_edits[1]' \ - 'tests/test_interaction_parity.py::test_inline_editor_resize_uses_current_units_and_applies_real_edits[2]' \ - 'tests/test_interaction_parity.py::test_inline_text_resize_multiline_and_existing_format_in_both_hosts[cartesian]' \ - 'tests/test_interaction_parity.py::test_inactive_overlay_objects_can_be_edited_and_dragged_directly[native]' \ - 2>&1 | tee ci-results/layout-probe.log - # Measure the last failed radial screenshot before the full suite. - # Await current surface completion before the unchanged capture deadline. - .venv/bin/python -m pytest -vv --capture=tee-sys --durations=20 \ - --basetemp=ci-results/priority --junitxml=ci-results/priority.xml \ - tests/test_packaged_assets.py \ - 'tests/test_interaction_parity.py::test_annotation_drawing_editing_and_cancel_in_both_hosts[radial]' \ - 2>&1 | tee ci-results/priority.log - .venv/bin/python -m pytest -vv --capture=tee-sys --durations=30 \ + # Collect the entire suite; execute each node ID on one runner. + # The required integration job audits exact JUnit execution IDs. + .venv/bin/python scripts/run_ci_tests.py -x -vv --capture=tee-sys --durations=30 \ + --ci-shard-count=4 --ci-shard-index="$CI_SHARD" \ --basetemp=ci-results/full --junitxml=ci-results/junit.xml \ 2>&1 | tee ci-results/full.log SCRIPT @@ -183,8 +175,40 @@ jobs: if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: pr-linux-integration-results + name: pr-linux-integration-shard-${{ matrix.shard }} path: ci-results retention-days: 5 if-no-files-found: ignore + + integration: + name: PR / Linux integration + needs: integration_shards + if: ${{ !cancelled() }} + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require every shard to succeed + env: + SHARD_RESULT: ${{ needs.integration_shards.result }} + run: test "$SHARD_RESULT" = success + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: pr-linux-integration-shard-* + path: ci-results + - name: Verify complete passing execution without duplicates or skips + run: | + for shard in 0 1 2 3; do + mv "ci-results/pr-linux-integration-shard-$shard" "ci-results/shard-$shard" + done + python3 scripts/check_ci_execution.py ci-results --shards=4 + - name: Save execution audit + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: pr-linux-integration-audit + path: ci-results/execution-audit.json + retention-days: 5 + if-no-files-found: error diff --git a/README.md b/README.md index 894514f..6b23626 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,13 @@ in [Editor commands and history](docs/EDITOR_COMMANDS.md). ## Edit once, reuse across datasets +For an optional multi-Figure agent review workflow, use +[`figgyplot.review.Session`](docs/REVIEW_MODE.md) and the +[review controller example](examples/review_workflow.py). Review comments are +separate from chart annotations. Approval checkpoints exact SSOT edits and binary +source data before closing the view; the complete next round is published after +source execution and rendering validation. The ordinary editor needs no session. + Run the example shown in the GIF: ```bash @@ -432,7 +439,8 @@ rates are reported separately; they are not monitor FPS measurements. The native extension uses the published **figgy-renderer 0.18.1** and **figgy-model 0.12.1** crates from crates.io, pinned in `Cargo.lock`. The packaged browser kernel is built from their upstream release commit -`3a0a6a3` with a recorded web-binding patch for non-borrowing GPU completion. +`f3f569c4c0e32d3d0682632444b6fe7ce84cba8d` with a recorded web-binding patch +for non-borrowing GPU completion and current annotation hit geometry. Asset provenance checks pair the registry versions and checksums with that source revision and verify the binding patch hash. Rust owns chart layout, rendering, picking and drag policies; this repository adds the NumPy-facing API, diff --git a/docs/HOSTS.md b/docs/HOSTS.md index 1b88c9f..66588c3 100644 --- a/docs/HOSTS.md +++ b/docs/HOSTS.md @@ -2,7 +2,8 @@ The figgyplot 0.3.0 source uses the published `figgy-renderer 0.18.1` and `figgy-model 0.12.1` crates from crates.io. `Cargo.lock` pins their versions -and checksums; packaged WASM uses their release commit `3a0a6a3`. +and checksums; packaged WASM uses their release commit +`f3f569c4c0e32d3d0682632444b6fe7ce84cba8d`. Provenance checks preserve this source pairing; see [publishing](PUBLISHING.md). Chart layout, styles, data rendering, hit maps and drag policies remain renderer APIs. No renderer source is @@ -96,6 +97,13 @@ presentation or completed GPU execution. `benchmarks/ui_surface.py` measures surface submission rate; the existing `compare.py` measures completed RGBA readback. Do not compare the two as the same FPS metric. +Both native chart families also report `pointer_sequence` and `pointer_input` +for the last cursor-motion or mouse-button event included in a submitted surface frame. The event +records its kind, physical client coordinates, button/state and egui's consumed +flag. These diagnose delivered input independently of chart SSOT or selection; +a surface-frame increment alone does not identify which input was processed. +The UI harness retains bounded before/after input records and timestamps. + ## Jupyter Native and Jupyter WebGPU hosts support Cartesian, radial, categorical and boxplot diff --git a/docs/REVIEW_MODE.md b/docs/REVIEW_MODE.md new file mode 100644 index 0000000..7b28651 --- /dev/null +++ b/docs/REVIEW_MODE.md @@ -0,0 +1,136 @@ +# Review sessions (protocol 1) + +Status: unreleased implementation for #18, pending API review. Use a wheel built +from this branch; the previously published figgyplot 0.3.0 does not include it. +See the [validation record](REVIEW_MODE_VALIDATION.md) for measured test coverage. + +Issue #18 adds an explicit `figgyplot.review.Session` lifecycle. Ordinary +`Figure`, `show`, notebook editing and Copy Python do not create review files, +start agents or require an LLM runtime. + +## API and lifecycle + +```python +from figgyplot.review import Session + +session = Session(source={"entrypoint": "plots.py", "function": "build", "arguments": {}}) +fig = session.figure(id="signal") +fig.axes[0].plot([0, 1], [1, 3], id="measurement") +session.present() # native windows; use session.widget(fig) in a notebook +session.run_events() # handles review requests while pumping native windows +``` + +Native and notebook review panels contain a dedicated **Review comments** input, +**Request changes**, and **Accept**. Chart text/annotations stay chart data. +Approval flushes input, checks the displayed revision, publishes durable feedback, +then closes the approved view. Failed publication keeps the view editable with +the comment intact. Closing a window manually leaves its disposition pending. +`session.submit(figure_id, disposition, comments=..., expected_revision=...)` +is the synchronous native/headless interface; `await session.submit_async(...)` +first acknowledges notebook edits. `skip` is an explicit programmatic disposition. +`session.start()` seals a complete set for headless use without opening views. +`session.comment(figure_id, text, target=...)` appends a targeted API comment. +The editable UI comment has its own channel; changing or clearing it preserves +API comments and their targets. Both channels are included in feedback. +Runtime revision counters are scoped to recorded Figure instance IDs. Recovery +creates a new instance; baseline and approved instance/revision pairs therefore +remain explicit. Persistent hashes and stable IDs identify the compared states. +The session round number, rather than a process-local Figure counter, drives +successor publication and cleanup. + +Figure IDs use the existing document identity namespace. Choose a Figure ID +different from its panel IDs (the default panel is `main`). +Review mode supports one active interactive view per Figure in a round. A second +native window is rejected; duplicate notebook views must be closed before +approval, so another view's pending input cannot be discarded. Native PNG +fallback editors must also be closed before review approval. Ordinary manual +Figures retain their existing multiple-view behavior. + +All required Figures form one sealed review set. Only when all have an explicit +disposition can `session.events()` emit the batch's stable work ID and manifest +location. Repeated polling returns the same pending work. Accepting every Figure +without edits/comments finishes the round without requesting code changes. +Source writes belong to one agent/staging workspace, outside the UI event loop. + +`Session.open(cache_dir)` resumes a session. `restore(figure_id)` reconstructs a +Figure from the last complete settings and binary payloads. It does not depend +on the original Python process, GPU resources, notebook transfer buffers or an +unbounded undo journal. `cancel()` preserves the last recoverable round. + +`regenerate(work_id, superseded=...)` loads the recorded source entry point, +calls its `build(session, **arguments)` function, validates every candidate by +rendering, and publishes a complete new round. Build/render failure preserves +the pending work and previous checkpoint. The source hash must stay unchanged +during validation. Stable Figure IDs must remain present unless explicitly +superseded. Human property changes must be reproduced by source, or explicitly +superseded with a reason. Merely acknowledging an operation is insufficient. +The next complete set is shown only after publication. Agent integration can +use the CLI and filesystem protocol without importing private Python objects. +Source imports, recorded working directory and rendering execute in an isolated +Python worker process, with a configurable timeout. They cannot change the +controller's working directory, module state or UI event loop. + +## Feedback and persistence + +The versioned session manifest carries session/round/work IDs, source entry point, +function, arguments and digest, per-Figure schema/renderer versions, baseline and +approved revisions, full baseline/approved settings, canonical persistent hashes, +exact edit operations, separately entered comments, and binary references. +Runtime hover/selection is removed using the same Rust projection as edit history. +Operations use JSON Pointer paths, before/after existence and values, and stable +identity selectors. Object creation, removal and ordering are explicit operations; +there is no guessed gesture delta. The baseline and approved snapshots remain +available to interpret derived layout/link changes. + +Numeric input is cached at upload time as content-addressed NumPy binary files +(`allow_pickle=False`), with dtype, shape, byte size and SHA-256. Matrix rows and +vector/mapping columns retain their source IDs; bounded-family values reside in +the complete settings document. Settings-only revisions reuse immutable files. +No permanent native CPU array copy or GPU readback is introduced. + +Create sessions in a dedicated empty directory, or omit `cache_dir` to allocate +one. `Session.open` requires an existing manifest. Cache creation rejects +nonempty foreign directories before creating files or running cleanup. + +Writers and readers hold a process/file lock. Payloads are flushed before a +temporary manifest is flushed and atomically replaced; the directory is flushed +where supported. Only the published manifest defines a complete revision. +Interrupted writes cannot replace the previous complete state. Garbage collection +runs after commits and on reopen/access. It retains the newest complete round, +unfinished work and every referenced baseline/data file. Higher complete rounds +allow reclamation of superseded completed rounds in the same session lineage. +Readers and active writers are protected by the same lock. Cleanup never depends +on an agent acknowledgement or compares unrelated Figure revision counters. + +The filesystem adapter is local and agent-neutral. A disconnected agent resumes +by polling `python -m figgyplot.review events CACHE`; raw arrays never enter the +feedback text. `feedback CACHE WORK` reads structured feedback. Before editing +source, `claim CACHE WORK --worker ID` obtains a renewable writer lease; +`regenerate CACHE WORK --claim-token TOKEN` validates updated source and publishes +its next round. Expired/stale leases cannot publish; duplicate completion returns +the existing successor. `status CACHE` reports pending Figures and rerun errors. +The bundled companion skill documents protocol/version discovery and this loop. +Jupyter controllers use `await session.watch()` to display published successors; +native controllers use `run_events(on_status=...)` to observe progress while +their approved windows are closed. + +The [bundled skill](../python/figgyplot/skills/figgyplot-review/SKILL.md) is also +available through `python -m figgyplot.review skill`. Agent hosts use their +existing terminal/tool runner to poll events while a human reviews the figures. +The adapter does not require a vendor model SDK. A disconnected agent resumes +the same durable work ID instead of relying on an in-memory notification. + +Run adapter commands on the rendering host through the agent's execution tool. +Remote agents need an explicit execution bridge with access to the cache, +recorded source paths and Python environment. A manifest location alone does not +connect a remote chat service. The validation worker needs a native GPU or a +software GPU runtime, including for notebook-built candidates; the notebook +controller itself continues to use its GPU-free document model. + +The cache preserves registered renderer columns and canonical bounded values. +For histograms and boxes these include counts/edges or computed statistics; +original samples, external computation inputs and random seeds belong to the +source builder's reproducible inputs/arguments. The checkpoint restores the +chart without inferring those computations from its rendered result. + + diff --git a/docs/REVIEW_MODE_VALIDATION.md b/docs/REVIEW_MODE_VALIDATION.md new file mode 100644 index 0000000..5e0e9d1 --- /dev/null +++ b/docs/REVIEW_MODE_VALIDATION.md @@ -0,0 +1,330 @@ +# Review mode validation + +This records local validation of the unreleased #18 implementation. Required +GitHub checks still run on the draft PR's exact commit. + +## Initial review implementation wheel + +- CPython 3.12.14, NumPy 2.5.4, Linux, Mesa software Vulkan, Chromium 154. +- Wheel SHA-256: `93e14460b0430002f65d284b11dd99532864c52bc2ec6006f8fd215ff16103b2`. +- Every packaged Python, JavaScript, CSS, schema and skill file matches source. +- **46 review tests passed** in 134.33 seconds: 37 core cases and nine real native + or Jupyter input cases. UI tests ran with Xvfb/Openbox at 1600 x 1000 and the + required interaction/timing diagnostics enabled. +- **Four existing editor/layout probes passed** in 72.51 seconds, including + resize at two scales, multiline text formatting and inactive overlay editing. +- **117 CI preflight tests passed**. Cargo format/check, JavaScript syntax, + installed-package checks and editor-control inventory passed. + +Run the review cases with the installed wheel and the normal CI UI environment: + +```sh +python -m pytest -vv tests/test_review.py tests/test_interaction_parity.py -k review +``` + +The review cases compare the actual accepted baseline and approved settings, +then restore from the manifest and binary files. They cover all chart families +on both backends, unchanged binary reuse, corruption, stale revisions/instances, +writer leases, disconnect, duplicate delivery, concurrent comments, failed and +timed-out source/render workers, explicit edit supersession and completed-round +cleanup. Native and browser UI cases submit through real controls, retain pending +annotations and comments on storage failure, retry approval, and display every +Figure in the validated next round. + +Additional regressions first failed against the preceding wheel, then passed: + +- UI comments no longer erase separately targeted API comments. +- A close observer may read session state after a post-rename durability retry; + approval does not close views while holding the manifest file lock. +- Cache creation cannot claim a nonempty foreign directory, and opening a missing + cache creates no files. +- Native review buttons remain inside the surface with long comments. The final + input check pastes 40 actual lines and verifies the complete stored text. +- Duplicate notebook views cannot approve while another view has pending input; + duplicate native windows and reopening approved native Figures are rejected. + +## Earlier full-suite run + +Before those final review-specific fixes and six added regressions, one complete +UI-enabled invocation passed **633 tests in 2460.86 seconds**. The then-current wheel was +then rebuilt and the 46 review cases, four existing layout probes and preflight +checks above were rerun. Those results belong to the initial implementation; +the follow-up artifact and complete run are recorded separately below. + +No thresholds, gesture assertions, pixel tolerances or CI time limits were relaxed. +The notebook harness now accepts an explicit expected initial view count and +requires every such view to reach Ready within the existing startup budget. + +## Native input and popup follow-up + +The same draft PR also fixes a short-click failure exposed while diagnosing +CI #51. Its workflow/assets check and wheel build passed, but integration stopped +near the existing 45-minute budget after recording a native radial-detail failure. +The job log did not reach the final failure traceback before cancellation. + +A local reproduction of that scenario measured the old input driver's button +hold at 0.8216 seconds on failure and 0.7937 seconds on success. Egui 0.36.2's +default click limit is 0.8 seconds. The driver now sends a complete X11 click +before waiting for rendering; intentional drag input remains separate. A shared +helper covers the native test drivers. An injected 0.9-second frame delay fails +the preceding driver and passes the corrected driver. [The recorded input and +timing evidence](measurements/ci51-native-click-duration-20261022.json) separates +the local cause from what the cancelled CI log establishes. + +The first full run with the corrected click driver stopped with **219 passed +and one failed** in 772.35 seconds, exposing a separate application issue. The Cartesian +inspector shared the Foreground order used by popup menus and could intercept a +reopened menu's item click. The inspector now uses Middle, keeping menus above +it. Review controls reserve their space before inspector sizing. [The actual +menu hit layers, before/after settings and review bounds](measurements/native-inspector-popup-layer-20261022.json) +record both fixes. Raw screenshots of the synthetic fixture show the [occluded menu before](measurements/native-inspector-menu-before-20261022.png) and the [visible menu after](measurements/native-inspector-menu-after-20261022.png). Five focused cases passed in 124.92 seconds, including native +and browser opposite-axis editing, properties with long review comments, and +the slow-frame click regression. + +A subsequent full run stopped with **243 passed and one failed** in 1201.72 +seconds. A native boxplot text-placement click exposed another input assumption: +egui clears `press_origin` after processing a release, even when both press and +release are buffered in one frame. Bounded drawing now recovers that frame's +actual primary-button press coordinate from the ordered raw events. Existing +image hit tests, drag previews and release commits remain intact. [The failed +placement, successful placement and paired raw events](measurements/native-buffered-click-20261022.json) +show `clicked=true`, a cleared snapshot origin and the recovered coordinate. +Opening the text draft leaves the complete chart SSOT identical to its armed +state. All **eight chart-family cases passed in 352.12 seconds**, each exercising +native and browser creation, text editing, cancellation, drawing and export. + +Native diagnostics retain the last submitted pointer event and bounded input +records. Browser diagnostics separate server/browser startup, view readiness, +the test body, collection and shutdown in `harness-timing.json`. CI stops at the +first failure to print its traceback immediately, and runs the two click/detail +regressions in the priority group. The full collection, existing 45-minute budget, +pixel assertions and gesture deadlines remain unchanged. The opposite-axis SSOT +assertions that detected the application failure are unchanged. + +The final follow-up wheel's SHA-256 is +`88e7f7cc4bfa8edf93ae9240f289144b530fc493dbe1f73a66355fb7b1537c2b`. +The complete UI-enabled suite passed **640 tests in 2444.04 seconds (40m 44s)** +on that wheel, with **zero failures, errors or skips**. This includes the full +review workflow and the existing native/Jupyter interaction, layout, pixel and +SSOT assertions, with all CI interaction/timing flags enabled. Current preflight +passed all **117 cases in 3.02 seconds**; Cargo format/check passed. All 33 installed runtime +files match the wheel byte for byte, including the native extension; its 32 +non-native runtime files also match their packaged source files. Installed-wheel +smoke and editor-control inventory checks passed. + +## CI #52 annotation-input follow-up + +The preceding local 640-case pass did not reproduce CI #52's browser input +failure. CI passed four layout probes and 51 priority cases, then stopped at +**250 passed and one failed** in 1472.42 seconds. The radial multiline-text +case sent its reopening double-click to **(1243, 873.94)** in a **980 x 820** +viewport. Its separately read protocol bounding box still described a +960 x 600 canvas, while the subsequent DOM record was 511 x 319.375. No text +hit was recorded; all three GPU submissions had completed and the host queue +was idle. This establishes an invalid test input, not a renderer/SSOT failure. +The evidence does not determine whether the earlier protocol box was stale +when returned or the DOM layout changed between the separate reads. + +The shared annotation-input helper now prepares annotation geometry and reads +the live DOM rectangle and coordinate units in one browser operation. It rejects +points outside the visible canvas before sending real Playwright mouse input. +Both existing-text helpers use this path. No timing deadline or post-input +assertion changed. A controlled browser case reproduces the recorded resize: +the old point delivers no canvas double-click, while the current point delivers +a trusted mouse event. [The CI request, accepted SSOT and settled host state](measurements/ci52-annotation-input-20261022.json) +retain the measured failure. + +All **five affected cases passed in 94.48 seconds**: the controlled input +regression, Cartesian/radial multiline editing, and scale 1/2 editing. Runtime +files and the release wheel are unchanged. The collection now contains **641 +cases**; no complete local 641-case pass is claimed. Required CI validates the +complete updated collection. + +## CI #53 resize-readiness correction + +CI #53 failed the same radial multiline case after **251 passed and one failed** +in 1726.82 seconds. Unlike the preceding separate observations, its input helper +read annotation geometry and live DOM bounds together. The DOM itself was still +960 x 600 in the new 980 x 820 viewport; the requested point remained +(1243, 873.94). Thus the preceding coordinate-read correction did not fix the +underlying readiness error: matching CSS and backing dimensions can both be old. +[The measured input, host and GPU state](measurements/ci53-surface-readiness-20261022.json) +preserve this distinction. No renderer or SSOT failure is established here. + +A controlled browser regression using the exact CI allocation **failed against +the old predicate** (one failed in 1.69 seconds). Readiness now also requires a +positive canvas allocation that fits the requested horizontal viewport, while +preserving CSS/backing/DPR agreement. The regression withholds the new allocation +for 200 ms and checks that readiness waits for its delivery. It does not wait for +the asserted text content, font or inline-editor bounds. This is an input +precondition, not a claim that all automatic layout or GPU work has completed; +notebook output may still scroll vertically. + +All **six affected cases passed in 96.74 seconds** on the unchanged runtime +wheel: that readiness regression, the trusted annotation-input regression, +Cartesian/radial multiline editing in both hosts and queued resize/editing at +scales 1 and 2. CI orders the exact failed radial case and both controlled input +regressions first in the full suite. The collection now has **642 cases**. The +existing full suite, assertions and time limits remain in place; no complete +local 642-case pass is claimed. Local Chromium is 154; CI #53 used Chromium 156. + +## Acceptance gaps observed before the oracle correction + +Counts include packaging and release guards and do not measure feature or UX +coverage. Before the correction below, isolated assertion audits found two gaps: + +- All nine native feature round-trip cases accept a substituted white RGBA + image with one black pixel. Their model/replay assertions still execute, but + nonblank output plus replay-image equality is not an independent visual oracle. + A targeted data/colorbar pixel-change case rejects that same substituted image. +- All ten column-backed restoration family/backend cases accept arrays zeroed + only during restoration, while preserving shape and dtype. Settings equality + and a valid PNG signature do not verify every restored data value. A separate + input-snapshot test checks one stored float32 payload, not every restore handoff. + +These were demonstrated test gaps, not observed production data corruption. +The independent scene and restored-array assertions below address these cases. +Finite acceptance scenes still do not establish every feature combination or +general usability. + +## Execute the complete collection once + +The historical 640-case local JUnit record contains 2443.445 seconds of case +time. Running the priority groups separately repeats 55 of those cases, adding +246.020 seconds: **2689.465 seconds (44m 49s)** before collection, job setup and +shutdown. That leaves no useful margin within the unchanged 45-minute job +budget. These are measured local costs, not a prediction of the CI runner's +exact runtime. [The per-case costs and collection comparison](measurements/ci-test-order-20261022.json) +retain the calculation. + +`scripts/run_ci_tests.py` now runs one complete pytest collection and orders the +past failures first. A comparison against ordinary pytest collection found +**642 original IDs, 642 ordered IDs and 642 unique IDs**, with identical +multisets: zero omissions or additions. The runner records both lists in the +CI artifact `test-plan.json`. It rejects duplicated node IDs and missing required +priority cases with exit code 4; both rejection paths were exercised locally. +The workflow retains every test, its assertions, fail-fast behavior and the +45-minute job budget. Only duplicate execution is removed. + +The first **57 cases passed together in 254.33 seconds** in the recorded order, +including the exact failed radial editor case, two resize/input regressions, +both scale cases, inactive-overlay editing, packaged assets, slow native clicks, +radial detail selection and radial annotation drawing in both hosts. Preflight +passed **117 cases in 2.03 seconds**. The runtime wheel is unchanged. + +The integration job also used `if: always()`. GitHub's [cancellation reference](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-cancellation) +explicitly states that a running job with this condition continues on +cancellation. The job now uses `if: ${{ !cancelled() }}`: an upstream failed or +skipped build still runs the existing result guard and fails the required check, +while a superseded workflow can stop. Preflight passed **117 cases in 2.10 +seconds** after this workflow-only change. An already-running job retains the +workflow definition with which it started; this change does not alter that job +retroactively. + +## Independent feature and recovery oracles + +The original API/combination replay fixtures remain intact. Each of the nine +native feature replay cases now also renders an independently authored reference +scene. Its expected probes come from declared input geometry and colors, never +from another rendered image or renderer-reported bounds. The document is +960 x 600 with a fixed 400 x 400 plot at (100,100), hidden Cartesian decoration +and fixed [0,4] ranges. The assertions check: + +- Mapped points: gray values, positions, different marker areas and empty space. +- Point/grid vectors: mapped colors, shafts, directional heads and empty tails. +- Steps: required horizontal/vertical segments and absence of linear diagonals. +- Baseline/series fills: filled interiors and empty regions below/above the + declared boundaries. +- Polar coordinates: asymmetric radii, radians, clockwise direction and chords. +- Radar: two differently colored polygons, declared range and empty interiors. +- Annotations: reference-line color/position, span opacity and text in its + authored region. + +The identical oracle checks nine actual displayed Jupyter WebGPU canvases. A +fractional Jupyter output origin otherwise creates a 601-row enclosing capture +for a 600px canvas. The fixture translates the entire widget onto the device +pixel grid, retaining exactly 960 x 600 CSS/backing dimensions at DPR 1. No rows +are cropped, pixels resampled or dimension assertions relaxed. + +Recovery tests retain independent copies of the original renderer inputs in +test code only, before the cache writer or renderer can mutate them. They compare every baseline/approved NPY payload and every +restored upload by column identity, dtype, shape, all values and payload bits. +The NPY oracle reads files directly rather than reusing the restore reader. +Bounded numeric checkpoints are also compared with literal expectations for +pie/bar/box/radar inputs, including box statistics and sample-count dtype. +Coverage includes mapped values/sizes, matrices and edge coordinates, point/grid +vector components and mapped grids, shared-X fill pairs and panel/overlay data. + +Native inspection reads every restored column through the production GPU +selection-readback path using temporary test-only probe series referencing the +existing buffers; sample-upload counters must remain unchanged. These probes +run after complete approved-SSOT assertions and are discarded with the test +Figure. Browser inspection compares actual transfer snapshot bytes. Additional +float32/float64 cases include signed zero, nontrivial finite precision, a custom +NaN payload and both infinities. Cache/upload/transfer bits are exact; native +finite GPU values use the packed pair-of-f32 representation's 2e-14 relative +bound, with zero absolute tolerance. Native selection JSON represents nonfinite +values as null, so exact nonfinite bit preservation is asserted at the lossless +boundaries rather than claimed for that JSON observation. + +Negative controls are part of ordinary CI: **45 wrong image cases**, **nine +single-pixel substitutions through the actual replay test**, **42 wrong restore +payloads** (zero/reversed/wrong dtype), and **14 wrong device/transfer contents** +must all be rejected. Two additional controls require rejection when the cache +writer mutates its original input in place. Recording the expectation after the +writer incorrectly accepted both faults; pre-write snapshots now reject them. +In the resident-data cases, settings, cache and observed upload +arguments remain correct: only the final native upload or browser transfer bytes +are zeroed. Re-running the original baseline test accepted all ten original +family/backend resident-data faults; the strengthened checks reject them. + +Local validation passed **209 focused cases in 11.14 seconds**, **nine headful +browser cases in 127.57 seconds**, and **131 preflight cases in 2.90 seconds**. +The unchanged runtime wheel is the previously verified +`88e7f7cc4bfa8edf93ae9240f289144b530fc493dbe1f73a66355fb7b1537c2b` artifact. +Ordinary and priority collection both contain **785 unique cases**: all previous +642 IDs remain, with 143 additions and zero duplicates. Each runner collects the complete plan, orders the priority cases first and +executes one disjoint quarter. No full local 785-case pass is claimed; required +CI must validate the final commit. +[The before/after contract and measurements](measurements/acceptance-oracles-20261022.json) +distinguish these results. These finite scenes do not claim every feature +combination, exact font rasterization or general usability. + + +### Complete execution is a required assertion + +[CI #58](https://github.com/shim9610/figgy-python/actions/runs/39734785159) +passed 470 of 771 cases before its configured 45-minute job limit cancelled the +run. It reported no assertion failure; it did not establish a full-suite pass. +The nine added browser oracles occupied approximately 125 seconds of the logged +test interval. Existing radial GPU work was substantially slower: the same +radial-detail WebGPU test's first submission increased from 11.622 seconds in +CI #56 to 42.342 seconds, while pipeline preparation increased from 0.501 to +0.660 seconds. This observation locates the larger stall in GPU completion, +without attributing it to a particular runner CPU or renderer change. +[The measurements](measurements/ci58-execution-deadline-20261022.json) preserve +that distinction. + +The suite now runs on four independent software-GPU runners. Each collects the +identical full ordered plan, then selects `ordered[index::4]`. Individual test +deadlines, correctness assertions and the 45-minute per-job limit are retained. +No test is intentionally run twice. The existing required check name, +`PR / Linux integration`, now verifies that every shard succeeded and audits +all four JUnit reports against their complete plans. Missing artifacts, changed +plans, wrong partitions, missing/duplicate execution IDs, failures, errors and +skips fail this check. Exact pytest node IDs are written into JUnit properties; +Python class names are not used to guess them. + +Fourteen local execution-contract tests passed, including a real two-shard +pytest/JUnit run and twelve deliberately invalid reports. Collection proves +that the four selected lists contain all 785 current IDs exactly once, including +all original 642 cases. This audit is also part of CI preflight. + + +The first sharded run (CI #59) exposed a nested-test isolation error: its sample +pytest subprocess inherited the parent project's collection root when CI's +`--basetemp` lived inside the repository. The same repository-local layout +reproduced the failure locally. The sample subprocess now explicitly fixes its +root to its own temporary directory; the full audit remains strict. All 14 +execution-contract cases pass in that repository-local layout (0.64 seconds), +and preflight passes all 131 cases (2.90 seconds). diff --git a/docs/images/review-mode-browser.png b/docs/images/review-mode-browser.png new file mode 100644 index 0000000..3656a5d Binary files /dev/null and b/docs/images/review-mode-browser.png differ diff --git a/docs/measurements/acceptance-oracles-20261022.json b/docs/measurements/acceptance-oracles-20261022.json new file mode 100644 index 0000000..a516311 --- /dev/null +++ b/docs/measurements/acceptance-oracles-20261022.json @@ -0,0 +1,95 @@ +{ + "baseline": "c2f3703a85682fd485e8d5c0579601bf75bbe775", + "runtime_changed": false, + "before": { + "single_pixel_feature_tests_accepted": 9, + "zero_restore_payload_tests_accepted": 10, + "zero_resident_data_tests_accepted": 10, + "writer_mutation_tests_accepted_before_source_snapshot_fix": 2 + }, + "after": { + "native_feature_scenes": 9, + "browser_feature_scenes": 9, + "wrong_image_checks_rejected": 45, + "actual_roundtrip_single_pixel_checks_rejected": 9, + "wrong_restore_payload_checks_rejected": 42, + "wrong_device_or_transfer_checks_rejected": 14, + "normal_restore_cases": 22, + "float_precision_cases": 4, + "focused_cases_passed": 209, + "focused_seconds": 11.14, + "headful_browser_cases_passed": 9, + "headful_browser_seconds": 127.57, + "preflight_cases_passed": 131, + "preflight_seconds": 2.9, + "wrong_cache_writer_input_mutation_checks_rejected": 2, + "execution_contract_cases_passed": 14, + "execution_contract_seconds": 0.64 + }, + "collection": { + "previous": 642, + "current": 785, + "added": 143, + "omitted": 0, + "duplicates": 0, + "shards": 4, + "per_shard": [ + 197, + 196, + 196, + 196 + ] + }, + "visual_contract": { + "document": [ + 960, + 600 + ], + "plot": [ + 100, + 100, + 400, + 400 + ], + "cartesian_domain": [ + 0, + 4 + ], + "rgb_quantization_tolerance": 3, + "pixel_neighborhood": 1, + "marker_solid_area_radius_band": 2, + "oracle": "Authored scene geometry and colors; no rendered image or renderer bounds are used to generate expected probes.", + "semantic_faults": [ + "wrong colormap", + "swapped vector components", + "linear interpolation replacing steps", + "wrong fill baseline/reference", + "reversed polar direction", + "wrong radar value range", + "missing text annotation" + ] + }, + "recovery_contract": { + "lossless_boundaries": [ + "original renderer input", + "baseline and approved NPY files", + "restored upload arguments", + "browser transfer snapshots" + ], + "comparisons": [ + "complete column identities", + "dtype", + "shape", + "all values", + "payload bits" + ], + "gpu": "Read every native column via GPU selection readback, using temporary test-only probe series without any sample upload.", + "native_finite_rtol": 2e-14, + "native_finite_atol": 0, + "nonfinite": "Native selection JSON represents nonfinite values as null. NaN payloads, infinities and signed-zero bits are checked exactly at lossless boundaries, not promised as bit-identical GPU representation.", + "original_capture": "Copy the original array before delegating to the cache writer or renderer; restoration capture remains before the renderer upload." + }, + "browser_capture": "The complete widget is translated by the fractional part of its DOM origin to align canvas pixels with the device grid. CSS and backing remain exactly 960x600 at DPR 1. No cropping, resampling or relaxed image shape is used.", + "scope": "These close the demonstrated oracle gaps for the listed scenes and recovery families. They do not establish every feature combination, font rasterization or usability.", + "required_ci": "Pending validation of the final commit. CI #58 passed 470 of 771 tests before its deadline. CI #59 caught a repository-local subprocess root isolation error, reproduced locally and fixed with an explicit sample-test root. The required integration check audits exact passing JUnit IDs across all four disjoint shards." +} diff --git a/docs/measurements/ci-test-order-20261022.json b/docs/measurements/ci-test-order-20261022.json new file mode 100644 index 0000000..847e445 --- /dev/null +++ b/docs/measurements/ci-test-order-20261022.json @@ -0,0 +1,259 @@ +{ + "historical_local_run": { + "cases": 640, + "seconds": 2444.04, + "junit_sha256": "dba1bfa4ae739ef881e342e26430f70fcd0c2be252c1f98e362d117e410ad090", + "summed_case_seconds": 2443.445 + }, + "duplicate_work": { + "cases": 55, + "summed_seconds": 246.02, + "full_plus_duplicate_case_seconds": 2689.465, + "items": [ + { + "nodeid": "tests.test_interaction_parity::test_annotation_drawing_editing_and_cancel_in_both_hosts[radial]", + "seconds": 128.761 + }, + { + "nodeid": "tests.test_interaction_parity::test_inline_text_resize_multiline_and_existing_format_in_both_hosts[radial]", + "seconds": 37.09 + }, + { + "nodeid": "tests.test_interaction_parity::test_inline_editor_resize_uses_current_units_and_applies_real_edits[1]", + "seconds": 14.95 + }, + { + "nodeid": "tests.test_interaction_parity::test_inline_editor_resize_uses_current_units_and_applies_real_edits[2]", + "seconds": 15.994 + }, + { + "nodeid": "tests.test_interaction_parity::test_inactive_overlay_objects_can_be_edited_and_dragged_directly[native]", + "seconds": 12.473 + }, + { + "nodeid": "tests.test_interaction_parity::test_radial_detail_tree_selection_edits_only_the_chosen_child[native]", + "seconds": 29.857 + }, + { + "nodeid": "tests.test_packaged_assets::test_source_asset_provenance", + "seconds": 0.012 + }, + { + "nodeid": "tests.test_packaged_assets::test_runtime_asset_check_does_not_require_tomllib", + "seconds": 0.011 + }, + { + "nodeid": "tests.test_packaged_assets::test_crlf_changes_are_rejected_with_asset_diagnostics", + "seconds": 0.006 + }, + { + "nodeid": "tests.test_packaged_assets::test_git_autocrlf_checkout_preserves_manifest_bytes[False]", + "seconds": 0.34 + }, + { + "nodeid": "tests.test_packaged_assets::test_git_autocrlf_checkout_preserves_manifest_bytes[True]", + "seconds": 0.326 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_check_does_not_depend_on_callers_git_subdirectory", + "seconds": 0.014 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_inside_another_checkout", + "seconds": 0.013 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_must_match_built_asset_provenance[bytes]", + "seconds": 0.019 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_must_match_built_asset_provenance[missing]", + "seconds": 0.018 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_must_match_built_asset_provenance[hash]", + "seconds": 0.018 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_must_match_built_asset_provenance[path]", + "seconds": 0.019 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_must_match_built_asset_provenance[files]", + "seconds": 0.019 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_cannot_change_native_sources_or_build_configuration[renderer]", + "seconds": 0.012 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_cannot_change_native_sources_or_build_configuration[model]", + "seconds": 0.012 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_cannot_change_native_sources_or_build_configuration[manifest]", + "seconds": 0.012 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_cannot_change_native_sources_or_build_configuration[mode]", + "seconds": 0.008 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_cannot_change_native_sources_or_build_configuration[crlf]", + "seconds": 0.009 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_cannot_change_native_sources_or_build_configuration[deletion]", + "seconds": 0.013 + }, + { + "nodeid": "tests.test_packaged_assets::test_host_patch_cannot_change_native_sources_or_build_configuration[invalid]", + "seconds": 0.013 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_lockfile_changes_are_rejected[checksum-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc-figgy-model]", + "seconds": 0.015 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_lockfile_changes_are_rejected[checksum-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc-figgy-renderer]", + "seconds": 0.015 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_lockfile_changes_are_rejected[version-9.0.0-figgy-model]", + "seconds": 0.015 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_lockfile_changes_are_rejected[version-9.0.0-figgy-renderer]", + "seconds": 0.014 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_lockfile_changes_are_rejected[source-git+https://github.com/shim9610/figgy#7777777777777777777777777777777777777777-figgy-model]", + "seconds": 0.016 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_lockfile_changes_are_rejected[source-git+https://github.com/shim9610/figgy#7777777777777777777777777777777777777777-figgy-renderer]", + "seconds": 0.016 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_pairing_metadata_changes_are_rejected[native_revision]", + "seconds": 0.016 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_pairing_metadata_changes_are_rejected[wasm_revision]", + "seconds": 0.015 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_pairing_metadata_changes_are_rejected[script_revision]", + "seconds": 0.015 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_pairing_metadata_changes_are_rejected[path]", + "seconds": 0.014 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_pairing_metadata_changes_are_rejected[renderer_version]", + "seconds": 0.014 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_pairing_metadata_changes_are_rejected[missing]", + "seconds": 0.014 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_pairing_metadata_changes_are_rejected[duplicate]", + "seconds": 0.015 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_overrides_rejected_even_with_unchanged_lock[[patch.crates-io]\\nfiggy-renderer = { git = \"https://github.com/shim9610/figgy\", rev = \"7777777777777777777777777777777777777777\" }]", + "seconds": 0.008 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_overrides_rejected_even_with_unchanged_lock[[patch.crates-io]\\nmodel_alias = { package = \"figgy-model\", path = \"../model\" }]", + "seconds": 0.008 + }, + { + "nodeid": "tests.test_packaged_assets::test_registry_overrides_rejected_even_with_unchanged_lock[[replace]\\n\"figgy-model:0.11.0\" = { path = \"../model\" }]", + "seconds": 0.009 + }, + { + "nodeid": "tests.test_packaged_assets::test_direct_nonregistry_renderer_rejected[, path = \"../renderer\"]", + "seconds": 0.009 + }, + { + "nodeid": "tests.test_packaged_assets::test_direct_nonregistry_renderer_rejected[, git = \"https://github.com/shim9610/figgy\"]", + "seconds": 0.009 + }, + { + "nodeid": "tests.test_packaged_assets::test_direct_nonregistry_renderer_rejected[, registry = \"other\"]", + "seconds": 0.009 + }, + { + "nodeid": "tests.test_packaged_assets::test_dirty_wasm_checkout_is_rejected_before_download[False]", + "seconds": 0.019 + }, + { + "nodeid": "tests.test_packaged_assets::test_dirty_wasm_checkout_is_rejected_before_download[True]", + "seconds": 0.021 + }, + { + "nodeid": "tests.test_packaged_assets::test_published_archive_matches_exact_wasm_source[None]", + "seconds": 0.004 + }, + { + "nodeid": "tests.test_packaged_assets::test_published_archive_matches_exact_wasm_source[checksum]", + "seconds": 0.003 + }, + { + "nodeid": "tests.test_packaged_assets::test_published_archive_matches_exact_wasm_source[revision]", + "seconds": 0.003 + }, + { + "nodeid": "tests.test_packaged_assets::test_published_archive_matches_exact_wasm_source[dirty]", + "seconds": 0.004 + }, + { + "nodeid": "tests.test_packaged_assets::test_published_archive_matches_exact_wasm_source[path]", + "seconds": 0.003 + }, + { + "nodeid": "tests.test_packaged_assets::test_published_archive_matches_exact_wasm_source[manifest]", + "seconds": 0.003 + }, + { + "nodeid": "tests.test_packaged_assets::test_published_archive_matches_exact_wasm_source[source]", + "seconds": 0.003 + }, + { + "nodeid": "tests.test_packaged_assets::test_published_archive_matches_exact_wasm_source[inventory]", + "seconds": 0.003 + }, + { + "nodeid": "tests.test_radial_desktop::test_click_remains_a_short_press_when_frames_are_slow", + "seconds": 5.689 + } + ] + }, + "unchanged_job_budget_seconds": 2700, + "scope": "Historical local case times, excluding setup/collection/shutdown overhead. These are not a CI runtime guarantee.", + "collection_comparison": { + "baseline_cases": 642, + "ordered_cases": 642, + "unique_cases": 642, + "same_multiset": true, + "missing": [], + "added": [], + "priority_cases": 57 + }, + "failure_guards": { + "missing_required_cases_exit": 4, + "duplicate_nodeids_exit": 4 + }, + "correction": "One complete pytest collection, stable priority ordering, one execution per node ID. Preserve all assertions, fail-fast behavior and the existing 45-minute budget.", + "ordered_priority_execution": { + "passed": 57, + "failed": 0, + "seconds": 254.33, + "preflight_passed": 117, + "preflight_seconds": 2.03, + "runtime_changed": false + } +} diff --git a/docs/measurements/ci51-native-click-duration-20261022.json b/docs/measurements/ci51-native-click-duration-20261022.json new file mode 100644 index 0000000..e950ff2 --- /dev/null +++ b/docs/measurements/ci51-native-click-duration-20261022.json @@ -0,0 +1,170 @@ +{ + "scenario": "Native radial detail series selection through real X11 input", + "ci": { + "run": 39656619970, + "job": 119421653668, + "full_collection": 639, + "observed_failed_case": "test_radial_detail_tree_selection_edits_only_the_chosen_child[native]", + "started_utc": "2026-10-21T18:58:40Z", + "cancelled_utc": "2026-10-21T19:43:57Z", + "job_budget_minutes": 45, + "note": "CI log records the failed case but no final traceback before cancellation. Detailed cause below is reproduced locally for the same case." + }, + "local_reproduction": { + "failed": { + "press_to_release_command_seconds": 0.8215802909980994, + "target_rect": [ + 1069.0, + 224.0, + 243.0, + 24.0 + ], + "press": { + "button": "Left", + "consumed": true, + "kind": "button", + "position": [ + 1190.0, + 236.0 + ], + "state": "Pressed" + }, + "release": { + "button": "Left", + "consumed": true, + "kind": "button", + "position": [ + 1190.0, + 236.0 + ], + "state": "Released" + }, + "selected_after_release": null, + "surface_frames": [ + 45, + 52 + ] + }, + "passed": { + "press_to_release_command_seconds": 0.793658811977366, + "target_rect": [ + 1069.0, + 224.0, + 243.0, + 24.0 + ], + "press": { + "button": "Left", + "consumed": true, + "kind": "button", + "position": [ + 1190.0, + 236.0 + ], + "state": "Pressed" + }, + "release": { + "button": "Left", + "consumed": true, + "kind": "button", + "position": [ + 1190.0, + 236.0 + ], + "state": "Released" + }, + "selected_after_release": { + "group": 1, + "index": 1 + }, + "surface_frames": [ + 50, + 57 + ] + }, + "failure": "KeyError: section:Series style; the detail slice remained unselected" + }, + "input_contract": { + "egui_version": "0.36.2", + "max_click_duration_seconds": 0.8, + "source": "https://docs.rs/egui/0.36.2/egui/struct.InputOptions.html#structfield.max_click_duration" + }, + "regression": { + "test": "test_click_remains_a_short_press_when_frames_are_slow", + "injected_frame_delay_seconds": 0.9, + "before_fix": "failed: properties_open was false", + "fix": "Send X11 button press/release as one xdotool click before waiting for rendered frames.", + "after_fix": "passed with the same injected frame delay" + }, + "validation": { + "focused_passed": 3, + "focused_seconds": 94.07, + "cases": [ + "test_click_remains_a_short_press_when_frames_are_slow", + "test_radial_detail_tree_selection_edits_only_the_chosen_child[native]", + "test_radial_detail_tree_selection_edits_only_the_chosen_child[webgpu]" + ], + "full_suite": { + "tests": 640, + "failures": 0, + "errors": 0, + "skipped": 0, + "seconds": 2444.032, + "wheel_sha256": "88e7f7cc4bfa8edf93ae9240f289144b530fc493dbe1f73a66355fb7b1537c2b" + }, + "full_suite_attempts": [ + { + "collected": 640, + "passed": 219, + "failed": 1, + "seconds": 772.35, + "failed_case": "test_matching_opposite_axes_share_only_ui_edited_fields[x]", + "cause": "The Cartesian inspector intercepted its reopened popup menu." + }, + { + "collected": 640, + "passed": 243, + "failed": 1, + "seconds": 1201.72, + "failed_case": "test_annotation_drawing_editing_and_cancel_in_both_hosts[boxplot]", + "cause": "Native bounded drawing relied on press_origin, which egui clears after a press and release buffered in the same frame." + }, + { + "collected": 640, + "passed": 640, + "failed": 0, + "seconds": 2444.04, + "wheel_sha256": "88e7f7cc4bfa8edf93ae9240f289144b530fc493dbe1f73a66355fb7b1537c2b" + } + ] + }, + "ci_runtime_comparison": { + "run50": { + "run_id": 39630843161, + "full_suite_passed": 593, + "full_suite_seconds": 1781.99, + "seconds_from_full_start_to_native_detail_result": 1603 + }, + "run51": { + "run_id": 39656619970, + "full_collection": 639, + "seconds_from_full_start_to_native_detail_result": 2250, + "full_suite_complete": false + }, + "common_browser_fixtures": 77, + "host": { + "logical_cpus": 4, + "affinity_cpus": 4, + "chromium_version": "156.0.8078.4" + }, + "median_fixture_max_pipeline_ms": { + "run50": 432.9, + "run51": 542.3 + }, + "median_fixture_page_time_ms": { + "run50": 8975.8, + "run51": 11939.7 + }, + "interpretation": "Slower software-GPU/host timing is measured. This comparison does not identify all causes of the wall-time increase. New harness phase records separate startup, test, diagnostics and shutdown. No deadline is increased." + } +} diff --git a/docs/measurements/ci52-annotation-input-20261022.json b/docs/measurements/ci52-annotation-input-20261022.json new file mode 100644 index 0000000..e3f15c4 --- /dev/null +++ b/docs/measurements/ci52-annotation-input-20261022.json @@ -0,0 +1,745 @@ +{ + "run": 39711546907, + "job": 119605700627, + "commit": "9ceb223d81aa58eb6a6ffe5acfb70b47fa9360ea", + "case": "test_inline_text_resize_multiline_and_existing_format_in_both_hosts[radial]", + "full_suite": { + "passed": 250, + "failed": 1, + "seconds": 1472.42 + }, + "request": { + "annotation": [ + 846, + 506.0000305175781, + 62.265625, + 38.400001525878906 + ], + "canvas": { + "x": 389, + "y": 361.9375, + "width": 960, + "height": 600 + }, + "units": [ + 960, + 600 + ], + "point": [ + 1243, + 873.9375305175781 + ] + }, + "observed": { + "canvas": { + "x": 389, + "y": 438.9375, + "width": 511, + "height": 319.375 + }, + "backing": [ + 511, + 319 + ], + "viewport": [ + 980, + 820, + 1 + ], + "box": { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + }, + "input": { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + }, + "hint": { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + }, + "font": 16, + "value": "First\nSecond", + "focused": false, + "selection": [ + 12, + 12 + ], + "accepted": { + "schema_version": 2, + "renderer_version": "0.18.1", + "config": { + "chart_area": { + "x": 0, + "y": 0, + "width": 960, + "height": 600 + }, + "top_x": { + "scale": "Linear", + "min": 0, + "max": 1, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "font_size": 18, + "label_visible": false, + "label_font": "", + "label_offset_x": 0, + "label_offset_y": 0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "font_size": 22, + "font": "" + }, + "visible": false, + "offset_x": 0, + "offset_y": 0 + }, + "out_margin": 25, + "line_offset": 0, + "line_visible": true, + "line_color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "line_width": 1, + "line_style": "Solid", + "major_tick_length": 5, + "minor_tick_length": 3 + }, + "bottom_x": { + "scale": "Linear", + "min": 0, + "max": 1, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "font_size": 18, + "label_visible": true, + "label_font": "", + "label_offset_x": 0, + "label_offset_y": 0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "font_size": 22, + "font": "" + }, + "visible": true, + "offset_x": 0, + "offset_y": 0 + }, + "out_margin": 80, + "line_offset": 0, + "line_visible": true, + "line_color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "line_width": 1, + "line_style": "Solid", + "major_tick_length": 5, + "minor_tick_length": 3 + }, + "left_y": { + "scale": "Linear", + "min": 0, + "max": 1, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "font_size": 18, + "label_visible": true, + "label_font": "", + "label_offset_x": 0, + "label_offset_y": 0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "font_size": 22, + "font": "" + }, + "visible": true, + "offset_x": 0, + "offset_y": 0 + }, + "out_margin": 110, + "line_offset": 0, + "line_visible": true, + "line_color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "line_width": 1, + "line_style": "Solid", + "major_tick_length": 5, + "minor_tick_length": 3 + }, + "right_y": { + "scale": "Linear", + "min": 0, + "max": 1, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "font_size": 18, + "label_visible": false, + "label_font": "", + "label_offset_x": 0, + "label_offset_y": 0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "font_size": 22, + "font": "" + }, + "visible": false, + "offset_x": 0, + "offset_y": 0 + }, + "out_margin": 25, + "line_offset": 0, + "line_visible": true, + "line_color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "line_width": 1, + "line_style": "Solid", + "major_tick_length": 5, + "minor_tick_length": 3 + }, + "chart_title": { + "text": { + "segments": [], + "color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "font_size": 28, + "font": "" + }, + "visible": true, + "offset_x": 0, + "offset_y": 0, + "top_margin": 32 + }, + "grid": { + "show_major_x": true, + "major_x_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1 + }, + "major_x_width": 1, + "major_x_style": "Solid", + "show_major_y": true, + "major_y_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1 + }, + "major_y_width": 1, + "major_y_style": "Solid", + "show_minor_x": false, + "minor_x_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1 + }, + "minor_x_width": 0.5, + "minor_x_style": "Dot", + "show_minor_y": false, + "minor_y_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1 + }, + "minor_y_width": 0.5, + "minor_y_style": "Dot" + }, + "legend": { + "visible": false, + "content": { + "segments": [], + "color": { + "r": 0, + "g": 0, + "b": 0, + "a": 1 + }, + "font_size": 14, + "font": "" + }, + "corner": "TopRight", + "offset_x": 0, + "offset_y": 0, + "padding": 8, + "bg_color": { + "r": 1, + "g": 1, + "b": 1, + "a": 0.85 + }, + "border_color": { + "r": 0.6, + "g": 0.6, + "b": 0.6, + "a": 1 + } + } + }, + "series": [], + "radial": { + "decorations": { + "data_area": null, + "offsets": [], + "selected": { + "Annotation": 0 + } + }, + "title": "", + "annotations": { + "visible": true, + "items": [ + { + "id": "annotation_b4a941a9-9f48-43a1-9cfe-47374db14348", + "kind": { + "Text": { + "anchor": { + "Ratio": { + "x": 0.90570176, + "y": 0.8804348 + } + }, + "text": { + "segments": [ + { + "text": "F", + "bold": true, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "i", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "r", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "s", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "t", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "\n", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "S", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "e", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "c", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "o", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "n", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "d", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + } + ], + "color": { + "r": 0.137, + "g": 0.176, + "b": 0.235, + "a": 1 + }, + "font_size": 16, + "font": "" + }, + "box_style": null + } + } + } + ] + }, + "slices": [ + { + "label": "Slice 1", + "value": 2, + "color": { + "r": 0.12156863, + "g": 0.46666667, + "b": 0.7058824, + "a": 1 + }, + "explode": 0, + "style": null, + "labels": null, + "label_format": null, + "label_color": null + }, + { + "label": "Slice 2", + "value": 3, + "color": { + "r": 1, + "g": 0.49803922, + "b": 0.05490196, + "a": 1 + }, + "explode": 0, + "style": null, + "labels": null, + "label_format": null, + "label_color": null + }, + { + "label": "Slice 3", + "value": 4, + "color": { + "r": 0.17254902, + "g": 0.627451, + "b": 0.17254902, + "a": 1 + }, + "explode": 0, + "style": null, + "labels": null, + "label_format": null, + "label_color": null + } + ], + "kind": { + "Donut": { + "inner_radius": 0.5 + } + }, + "start_angle_degrees": -90, + "split": null, + "style": { + "material": "Flat", + "tilt_degrees": 0, + "depth": 0, + "bevel": 0.015, + "gap_degrees": 0, + "roughness": 0.35, + "texture_strength": 0.2, + "texture_scale": 1, + "light": [ + -0.5, + -0.6, + 1 + ], + "shadow": false, + "inner_corner": 0, + "outer_corner": 0, + "outline": { + "rim": false, + "separators": false, + "emphasis": false, + "width": 1, + "color": { + "r": 0.08, + "g": 0.12, + "b": 0.2, + "a": 1 + } + }, + "hover_lift": 0.08, + "hover_brightness": 0.04, + "gloss": 0.55, + "texture_angle_degrees": 0 + }, + "labels": "Outside", + "label_format": "NamePercent", + "label_decimals": 1, + "value_suffix": "", + "interaction": { + "hovered": null, + "selected": null, + "hover_progress": 0 + }, + "font_family": "sans-serif", + "font_size": 18.333334, + "label_color": { + "r": 0.12, + "g": 0.16, + "b": 0.22, + "a": 1 + }, + "background": { + "r": 1, + "g": 1, + "b": 1, + "a": 1 + } + }, + "identity": { + "figure": "c592a4d1-1cd3-4725-bed2-ebdc552b9d07", + "groups": {}, + "layout": null, + "panels": [ + { + "id": "main", + "overlays": [] + } + ], + "retired": [] + } + }, + "host": { + "kind": "radial", + "busy": 0, + "queued": 0, + "dirty": false, + "frame_pending": false, + "frame_error": null, + "edit_inflight": null, + "edit_pending": false, + "canvas": [ + 511, + 319 + ], + "disposed": false + } + }, + "hit": null, + "annotation_geometry_after": [ + { + "element": "annotation_0", + "handles": [], + "id": "annotation_b4a941a9-9f48-43a1-9cfe-47374db14348", + "index": 0, + "kind": "Text", + "rect": [ + 846, + 506.0000305175781, + 62.265625, + 39.56614303588867 + ], + "selected": true + } + ], + "interpretation": "The recorded input point is outside the 980x820 viewport. The failure does not establish a renderer hit-test or SSOT error. Request-time DOM coordinates must be read together with prepared annotation geometry; a separately captured protocol bounding box may describe an earlier layout.", + "controlled_regression": { + "stale_point": [ + 1243.0, + 873.9375 + ], + "current": { + "annotation": [ + 846, + 506, + 62, + 38 + ], + "canvas": { + "x": 389, + "y": 438.9375, + "width": 511, + "height": 319.375 + }, + "units": [ + 960, + 600 + ], + "point": [ + 843.5770833333333, + 711.4708333333333 + ], + "viewport": [ + 980, + 820 + ], + "backing": [ + 960, + 600 + ] + }, + "delivered": [ + { + "trusted": true, + "x": 843, + "y": 711 + } + ] + }, + "validation": { + "affected_passed": 5, + "seconds": 94.48, + "complete_collection": 641, + "runtime_changed": false, + "deadlines_or_post_input_assertions_changed": false, + "complete_updated_ci": "pending" + } +} diff --git a/docs/measurements/ci53-surface-readiness-20261022.json b/docs/measurements/ci53-surface-readiness-20261022.json new file mode 100644 index 0000000..9ce5ce1 --- /dev/null +++ b/docs/measurements/ci53-surface-readiness-20261022.json @@ -0,0 +1,95 @@ +{ + "ci": { + "run": 53, + "url": "https://github.com/shim9610/figgy-python/actions/runs/39716629618", + "commit": "819692ac1ba362bb9061fd881b05b44be7f7e32a", + "result": { + "passed": 251, + "failed": 1, + "seconds": 1726.82 + }, + "case": "test_inline_text_resize_multiline_and_existing_format_in_both_hosts[radial]", + "browser": "156.0.8078.4", + "input": { + "annotation": [ + 846, + 506.0000305175781, + 62.265625, + 38.400001525878906 + ], + "canvas": { + "x": 389, + "y": 361.9375, + "width": 960, + "height": 600 + }, + "units": [ + 960, + 600 + ], + "point": [ + 1243, + 873.9375305175781 + ], + "viewport": [ + 980, + 820 + ], + "backing": [ + 960, + 600 + ] + }, + "host": { + "kind": "radial", + "busy": 0, + "queued": 0, + "dirty": true, + "frame_pending": false, + "frame_error": null, + "edit_inflight": null, + "edit_pending": false, + "canvas": [ + 960, + 600 + ], + "disposed": false + }, + "gpu": { + "submitted": 1, + "completed": 1, + "completedCanvas": [ + 960, + 600 + ] + } + }, + "conclusion": "Matching CSS and backing dimensions can both describe the preceding allocation after viewport shrink. The readiness predicate accepted that state and subsequent input was outside the viewport. This observation corrects the earlier incomplete coordinate-read hypothesis; it does not establish a renderer or SSOT failure.", + "correction": "Require a positive CSS canvas allocation fitting the requested horizontal viewport as well as CSS/backing/DPR agreement. Keep the input helper's live geometry and canvas hit check. Editor font, containment, value and post-input assertions remain independent.", + "limitations": "This is an input precondition, not proof that all automatic layout or GPU rendering has completed. Full vertical containment is not required because notebook output may scroll.", + "local": { + "browser": "154.0.8037.57", + "negative": { + "cases": 1, + "failed": 1, + "seconds": 1.69, + "condition": "The recorded CI allocation must be rejected, but the old predicate returned true." + }, + "positive": { + "cases": 6, + "passed": 6, + "seconds": 96.74, + "coverage": [ + "controlled delayed CSS/backing resize", + "trusted annotation input after resize", + "radial multiline native/Jupyter editing", + "Cartesian multiline native/Jupyter editing", + "queued resize and real edits at DPR 1", + "queued resize and real edits at DPR 2" + ] + }, + "wheel_sha256": "88e7f7cc4bfa8edf93ae9240f289144b530fc493dbe1f73a66355fb7b1537c2b", + "runtime_changed": false + }, + "status": "Local correction validated; required CI must run on the new commit before it can be described as passing." +} diff --git a/docs/measurements/ci58-execution-deadline-20261022.json b/docs/measurements/ci58-execution-deadline-20261022.json new file mode 100644 index 0000000..388954e --- /dev/null +++ b/docs/measurements/ci58-execution-deadline-20261022.json @@ -0,0 +1,18 @@ +{ + "run": 39734785159, + "head": "25e015710fe1802ee2dccde43c78d2a65ef3478a", + "completed_cases": 470, + "total": 771, + "assertion_failures": 0, + "termination": "Job cancelled exactly at its configured 45-minute limit; 470 tests passed, no assertion failure reported.", + "new_visual_browser_cases": 9, + "new_visual_browser_interval_seconds": 125.166, + "radial_comparison": { + "nodeid": "tests/test_interaction_parity.py::test_radial_detail_tree_selection_edits_only_the_chosen_child[webgpu]", + "baseline_run": 39723351628, + "previous_first_gpu_submit_ms": 11621.7, + "current_first_gpu_submit_ms": 42342.3, + "previous_pipeline_ms": 500.7, + "current_pipeline_ms": 659.8 + } +} diff --git a/docs/measurements/native-buffered-click-20261022.json b/docs/measurements/native-buffered-click-20261022.json new file mode 100644 index 0000000..a886a2b --- /dev/null +++ b/docs/measurements/native-buffered-click-20261022.json @@ -0,0 +1,4581 @@ +{ + "scenario": "A short native text-placement click on a boxplot is buffered into one egui frame", + "before": { + "wheel_sha256": "f5de068f208dbdefec7564cf5b07be418f329538fb07348ee8e6b48272972ed8", + "test": "test_annotation_drawing_editing_and_cancel_in_both_hosts[boxplot]", + "result": "failed: inline_text was absent after a complete X11 click", + "seconds": 4.9, + "armed": { + "stage": "text-armed", + "settings": { + "schema_version": 2, + "renderer_version": "0.18.1", + "config": { + "chart_area": { + "x": 0, + "y": 0, + "width": 960, + "height": 600 + }, + "top_x": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "bottom_x": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 80.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "left_y": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 110.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "right_y": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "chart_title": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 28.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0, + "top_margin": 32.0 + }, + "grid": { + "show_major_x": true, + "major_x_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_x_width": 1.0, + "major_x_style": "Solid", + "show_major_y": true, + "major_y_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_y_width": 1.0, + "major_y_style": "Solid", + "show_minor_x": false, + "minor_x_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_x_width": 0.5, + "minor_x_style": "Dot", + "show_minor_y": false, + "minor_y_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_y_width": 0.5, + "minor_y_style": "Dot" + }, + "legend": { + "visible": false, + "content": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 14.0, + "font": "" + }, + "corner": "TopRight", + "offset_x": 0.0, + "offset_y": 0.0, + "padding": 8.0, + "bg_color": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 0.85 + }, + "border_color": { + "r": 0.6, + "g": 0.6, + "b": 0.6, + "a": 1.0 + } + } + }, + "series": [], + "boxplot": { + "decorations": { + "data_area": null, + "offsets": [], + "selected": null + }, + "title": "", + "categories": [ + { + "id": "category_0", + "label": "Box 1" + }, + { + "id": "category_1", + "label": "Box 2" + } + ], + "series": [ + { + "id": "series_0", + "label": "Distribution", + "values": [ + { + "q1": 1.5, + "median": 2.0, + "q3": 3.0, + "whisker_low": 1.0, + "whisker_high": 4.0, + "mean": 2.3333333333333335, + "sample_count": 3, + "median_ci": null, + "outliers": [] + }, + { + "q1": 3.5, + "median": 5.0, + "q3": 7.0, + "whisker_low": 2.0, + "whisker_high": 9.0, + "mean": 5.333333333333333, + "sample_count": 3, + "median_ci": null, + "outliers": [] + } + ], + "color": { + "r": 0.12156863, + "g": 0.46666667, + "b": 0.7058824, + "a": 1.0 + }, + "style": null + } + ], + "direction": "Vertical", + "style": { + "material": "Matte", + "corner_radius": 0.0, + "outline": true, + "outline_width": 1.2, + "outline_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "median_width": 1.2, + "median_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "whisker_width": 1.2, + "whisker_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "caps": true, + "cap_ratio": 0.5, + "notched": false, + "notch_depth": 0.22, + "show_mean": false, + "mean_size": 7.0, + "mean_color": { + "r": 0.7058824, + "g": 0.1764706, + "b": 0.1764706, + "a": 1.0 + }, + "show_outliers": true, + "outlier_size": 7.0, + "outlier_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "outlier_shape": "Circle", + "outlier_filled": false, + "texture_strength": 0.2, + "texture_scale": 1.0, + "gloss": 0.45, + "emphasis_brightness": 0.08 + }, + "overrides": [], + "hovered": null, + "selected": null, + "labels": "None", + "label_decimals": 1, + "value_suffix": "", + "value_title": "", + "category_title": "", + "value_range": null, + "grid": true, + "legend": true, + "group_width": 0.65, + "box_gap": 8.0, + "font_family": "sans-serif", + "font_size": 18.333334, + "text_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "background": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 1.0 + } + }, + "identity": { + "figure": "2c2545dd-b73b-4701-b515-8bf79e7f47f4", + "groups": {}, + "layout": null, + "panels": [ + { + "id": "main", + "overlays": [] + } + ], + "retired": [] + } + }, + "host": { + "chart_family": "boxplot", + "chart_panel": [ + 45, + 135, + 901, + 563 + ], + "column_uploads": 0, + "controls": { + "advanced": [ + 448.59375, + 13.5, + 152.15625, + 30.0 + ], + "copy": [ + 610.75, + 10.5, + 36.0, + 36.0 + ], + "draw": [ + 335.5625, + 13.5, + 57.03125, + 30.0 + ], + "draw_arrow": [ + 139.125, + 60.0, + 60.4375, + 30.0 + ], + "draw_data_anchor": [ + 495.0625, + 60.0, + 90.59375, + 30.0 + ], + "draw_hline": [ + 209.5625, + 60.0, + 57.40625, + 30.0 + ], + "draw_hspan": [ + 343.4375, + 60.0, + 66.28125, + 30.0 + ], + "draw_select": [ + 8.0, + 60.0, + 60.0, + 30.0 + ], + "draw_text": [ + 78.0, + 60.0, + 51.125, + 30.0 + ], + "draw_vline": [ + 276.96875, + 60.0, + 56.46875, + 30.0 + ], + "draw_vspan": [ + 419.71875, + 60.0, + 65.34375, + 30.0 + ], + "edit_area": [ + 289.5625, + 10.5, + 36.0, + 36.0 + ], + "fit": [ + 402.59375, + 10.5, + 36.0, + 36.0 + ], + "layout_mode": [ + 289.5625, + 10.5, + 36.0, + 36.0 + ], + "properties": [ + 197.5625, + 10.5, + 36.0, + 36.0 + ], + "redo": [ + 130.9375, + 13.5, + 56.625, + 30.0 + ], + "save": [ + 656.75, + 10.5, + 36.0, + 36.0 + ], + "select_mode": [ + 243.5625, + 10.5, + 36.0, + 36.0 + ], + "undo": [ + 63.15625, + 13.5, + 57.78125, + 30.0 + ] + }, + "decoration_selected": null, + "decorations": { + "axis_bottom": [ + 41.39225387573242, + 508.3333740234375, + 897.6077270507812, + 6.0 + ], + "axis_left": [ + 41.39225387573242, + 29.0, + 6.0, + 485.3333740234375 + ], + "data_area": [ + 44.39225387573242, + 32.0, + 891.6077270507812, + 479.3333740234375 + ], + "legend": [ + 417.14874267578125, + 556.8780517578125, + 117.03582763671875, + 12.78863525390625 + ], + "tick_labels_bottom": [ + 243.8538360595703, + 524.8779907226562, + 492.6845703125, + 12.78863525390625 + ], + "tick_labels_left": [ + 14.0, + 25.07803726196289, + 20.392257690429688, + 492.1219787597656 + ] + }, + "drawing_tool": "text", + "error": "", + "figure_id": 1, + "gpu_context_id": 1, + "host": "winit-wgpu", + "inspector_overlay": false, + "layout_mode": false, + "png_encodes": 0, + "pointer_input": { + "button": "Left", + "consumed": true, + "kind": "button", + "position": [ + 105.0, + 75.0 + ], + "state": "Released" + }, + "pointer_sequence": 22, + "properties_open": false, + "review": null, + "revision": 2, + "rgba_readbacks": 0, + "scale_factor": 1.0, + "selected": null, + "surface_frames": 77, + "surface_size": [ + 992, + 744 + ], + "window_role": "chart" + } + }, + "placed": { + "stage": "text-placed", + "settings": { + "schema_version": 2, + "renderer_version": "0.18.1", + "config": { + "chart_area": { + "x": 0, + "y": 0, + "width": 960, + "height": 600 + }, + "top_x": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "bottom_x": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 80.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "left_y": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 110.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "right_y": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "chart_title": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 28.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0, + "top_margin": 32.0 + }, + "grid": { + "show_major_x": true, + "major_x_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_x_width": 1.0, + "major_x_style": "Solid", + "show_major_y": true, + "major_y_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_y_width": 1.0, + "major_y_style": "Solid", + "show_minor_x": false, + "minor_x_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_x_width": 0.5, + "minor_x_style": "Dot", + "show_minor_y": false, + "minor_y_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_y_width": 0.5, + "minor_y_style": "Dot" + }, + "legend": { + "visible": false, + "content": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 14.0, + "font": "" + }, + "corner": "TopRight", + "offset_x": 0.0, + "offset_y": 0.0, + "padding": 8.0, + "bg_color": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 0.85 + }, + "border_color": { + "r": 0.6, + "g": 0.6, + "b": 0.6, + "a": 1.0 + } + } + }, + "series": [], + "boxplot": { + "decorations": { + "data_area": null, + "offsets": [], + "selected": null + }, + "title": "", + "categories": [ + { + "id": "category_0", + "label": "Box 1" + }, + { + "id": "category_1", + "label": "Box 2" + } + ], + "series": [ + { + "id": "series_0", + "label": "Distribution", + "values": [ + { + "q1": 1.5, + "median": 2.0, + "q3": 3.0, + "whisker_low": 1.0, + "whisker_high": 4.0, + "mean": 2.3333333333333335, + "sample_count": 3, + "median_ci": null, + "outliers": [] + }, + { + "q1": 3.5, + "median": 5.0, + "q3": 7.0, + "whisker_low": 2.0, + "whisker_high": 9.0, + "mean": 5.333333333333333, + "sample_count": 3, + "median_ci": null, + "outliers": [] + } + ], + "color": { + "r": 0.12156863, + "g": 0.46666667, + "b": 0.7058824, + "a": 1.0 + }, + "style": null + } + ], + "direction": "Vertical", + "style": { + "material": "Matte", + "corner_radius": 0.0, + "outline": true, + "outline_width": 1.2, + "outline_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "median_width": 1.2, + "median_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "whisker_width": 1.2, + "whisker_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "caps": true, + "cap_ratio": 0.5, + "notched": false, + "notch_depth": 0.22, + "show_mean": false, + "mean_size": 7.0, + "mean_color": { + "r": 0.7058824, + "g": 0.1764706, + "b": 0.1764706, + "a": 1.0 + }, + "show_outliers": true, + "outlier_size": 7.0, + "outlier_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "outlier_shape": "Circle", + "outlier_filled": false, + "texture_strength": 0.2, + "texture_scale": 1.0, + "gloss": 0.45, + "emphasis_brightness": 0.08 + }, + "overrides": [], + "hovered": null, + "selected": null, + "labels": "None", + "label_decimals": 1, + "value_suffix": "", + "value_title": "", + "category_title": "", + "value_range": null, + "grid": true, + "legend": true, + "group_width": 0.65, + "box_gap": 8.0, + "font_family": "sans-serif", + "font_size": 18.333334, + "text_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "background": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 1.0 + } + }, + "identity": { + "figure": "2c2545dd-b73b-4701-b515-8bf79e7f47f4", + "groups": {}, + "layout": null, + "panels": [ + { + "id": "main", + "overlays": [] + } + ], + "retired": [] + } + }, + "host": { + "chart_family": "boxplot", + "chart_panel": [ + 45, + 135, + 901, + 563 + ], + "column_uploads": 0, + "controls": { + "advanced": [ + 448.59375, + 13.5, + 152.15625, + 30.0 + ], + "copy": [ + 610.75, + 10.5, + 36.0, + 36.0 + ], + "draw": [ + 335.5625, + 13.5, + 57.03125, + 30.0 + ], + "draw_arrow": [ + 139.125, + 60.0, + 60.4375, + 30.0 + ], + "draw_data_anchor": [ + 495.0625, + 60.0, + 90.59375, + 30.0 + ], + "draw_hline": [ + 209.5625, + 60.0, + 57.40625, + 30.0 + ], + "draw_hspan": [ + 343.4375, + 60.0, + 66.28125, + 30.0 + ], + "draw_select": [ + 8.0, + 60.0, + 60.0, + 30.0 + ], + "draw_text": [ + 78.0, + 60.0, + 51.125, + 30.0 + ], + "draw_vline": [ + 276.96875, + 60.0, + 56.46875, + 30.0 + ], + "draw_vspan": [ + 419.71875, + 60.0, + 65.34375, + 30.0 + ], + "edit_area": [ + 289.5625, + 10.5, + 36.0, + 36.0 + ], + "fit": [ + 402.59375, + 10.5, + 36.0, + 36.0 + ], + "layout_mode": [ + 289.5625, + 10.5, + 36.0, + 36.0 + ], + "properties": [ + 197.5625, + 10.5, + 36.0, + 36.0 + ], + "redo": [ + 130.9375, + 13.5, + 56.625, + 30.0 + ], + "save": [ + 656.75, + 10.5, + 36.0, + 36.0 + ], + "select_mode": [ + 243.5625, + 10.5, + 36.0, + 36.0 + ], + "undo": [ + 63.15625, + 13.5, + 57.78125, + 30.0 + ] + }, + "decoration_selected": null, + "decorations": { + "axis_bottom": [ + 41.39225387573242, + 508.3333740234375, + 897.6077270507812, + 6.0 + ], + "axis_left": [ + 41.39225387573242, + 29.0, + 6.0, + 485.3333740234375 + ], + "data_area": [ + 44.39225387573242, + 32.0, + 891.6077270507812, + 479.3333740234375 + ], + "legend": [ + 417.14874267578125, + 556.8780517578125, + 117.03582763671875, + 12.78863525390625 + ], + "tick_labels_bottom": [ + 243.8538360595703, + 524.8779907226562, + 492.6845703125, + 12.78863525390625 + ], + "tick_labels_left": [ + 14.0, + 25.07803726196289, + 20.392257690429688, + 492.1219787597656 + ] + }, + "drawing_tool": "text", + "error": "", + "figure_id": 1, + "gpu_context_id": 1, + "host": "winit-wgpu", + "inspector_overlay": false, + "layout_mode": false, + "png_encodes": 0, + "pointer_input": { + "button": "Left", + "consumed": true, + "kind": "button", + "position": [ + 355.0, + 360.0 + ], + "state": "Released" + }, + "pointer_sequence": 26, + "properties_open": false, + "review": null, + "revision": 2, + "rgba_readbacks": 0, + "scale_factor": 1.0, + "selected": null, + "surface_frames": 86, + "surface_size": [ + 992, + 744 + ], + "window_role": "chart" + } + } + }, + "after": { + "wheel_sha256": "88e7f7cc4bfa8edf93ae9240f289144b530fc493dbe1f73a66355fb7b1537c2b", + "same_frame_probe": { + "button_down": false, + "clicked": true, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": false + } + ], + "frame": 85, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 355.0, + 360.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 355.0, + 360.0 + ], + "tool": "text" + }, + "armed": { + "stage": "text-armed", + "settings": { + "schema_version": 2, + "renderer_version": "0.18.1", + "config": { + "chart_area": { + "x": 0, + "y": 0, + "width": 960, + "height": 600 + }, + "top_x": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "bottom_x": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 80.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "left_y": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 110.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "right_y": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "chart_title": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 28.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0, + "top_margin": 32.0 + }, + "grid": { + "show_major_x": true, + "major_x_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_x_width": 1.0, + "major_x_style": "Solid", + "show_major_y": true, + "major_y_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_y_width": 1.0, + "major_y_style": "Solid", + "show_minor_x": false, + "minor_x_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_x_width": 0.5, + "minor_x_style": "Dot", + "show_minor_y": false, + "minor_y_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_y_width": 0.5, + "minor_y_style": "Dot" + }, + "legend": { + "visible": false, + "content": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 14.0, + "font": "" + }, + "corner": "TopRight", + "offset_x": 0.0, + "offset_y": 0.0, + "padding": 8.0, + "bg_color": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 0.85 + }, + "border_color": { + "r": 0.6, + "g": 0.6, + "b": 0.6, + "a": 1.0 + } + } + }, + "series": [], + "boxplot": { + "decorations": { + "data_area": null, + "offsets": [], + "selected": null + }, + "title": "", + "categories": [ + { + "id": "category_0", + "label": "Box 1" + }, + { + "id": "category_1", + "label": "Box 2" + } + ], + "series": [ + { + "id": "series_0", + "label": "Distribution", + "values": [ + { + "q1": 1.5, + "median": 2.0, + "q3": 3.0, + "whisker_low": 1.0, + "whisker_high": 4.0, + "mean": 2.3333333333333335, + "sample_count": 3, + "median_ci": null, + "outliers": [] + }, + { + "q1": 3.5, + "median": 5.0, + "q3": 7.0, + "whisker_low": 2.0, + "whisker_high": 9.0, + "mean": 5.333333333333333, + "sample_count": 3, + "median_ci": null, + "outliers": [] + } + ], + "color": { + "r": 0.12156863, + "g": 0.46666667, + "b": 0.7058824, + "a": 1.0 + }, + "style": null + } + ], + "direction": "Vertical", + "style": { + "material": "Matte", + "corner_radius": 0.0, + "outline": true, + "outline_width": 1.2, + "outline_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "median_width": 1.2, + "median_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "whisker_width": 1.2, + "whisker_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "caps": true, + "cap_ratio": 0.5, + "notched": false, + "notch_depth": 0.22, + "show_mean": false, + "mean_size": 7.0, + "mean_color": { + "r": 0.7058824, + "g": 0.1764706, + "b": 0.1764706, + "a": 1.0 + }, + "show_outliers": true, + "outlier_size": 7.0, + "outlier_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "outlier_shape": "Circle", + "outlier_filled": false, + "texture_strength": 0.2, + "texture_scale": 1.0, + "gloss": 0.45, + "emphasis_brightness": 0.08 + }, + "overrides": [], + "hovered": null, + "selected": null, + "labels": "None", + "label_decimals": 1, + "value_suffix": "", + "value_title": "", + "category_title": "", + "value_range": null, + "grid": true, + "legend": true, + "group_width": 0.65, + "box_gap": 8.0, + "font_family": "sans-serif", + "font_size": 18.333334, + "text_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "background": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 1.0 + } + }, + "identity": { + "figure": "1bccaa56-20df-4f2f-8d3f-56725fbe3656", + "groups": {}, + "layout": null, + "panels": [ + { + "id": "main", + "overlays": [] + } + ], + "retired": [] + } + }, + "host": { + "chart_family": "boxplot", + "chart_panel": [ + 45, + 135, + 901, + 563 + ], + "column_uploads": 0, + "controls": { + "advanced": [ + 448.59375, + 13.5, + 152.15625, + 30.0 + ], + "copy": [ + 610.75, + 10.5, + 36.0, + 36.0 + ], + "draw": [ + 335.5625, + 13.5, + 57.03125, + 30.0 + ], + "draw_arrow": [ + 139.125, + 60.0, + 60.4375, + 30.0 + ], + "draw_data_anchor": [ + 495.0625, + 60.0, + 90.59375, + 30.0 + ], + "draw_hline": [ + 209.5625, + 60.0, + 57.40625, + 30.0 + ], + "draw_hspan": [ + 343.4375, + 60.0, + 66.28125, + 30.0 + ], + "draw_select": [ + 8.0, + 60.0, + 60.0, + 30.0 + ], + "draw_text": [ + 78.0, + 60.0, + 51.125, + 30.0 + ], + "draw_vline": [ + 276.96875, + 60.0, + 56.46875, + 30.0 + ], + "draw_vspan": [ + 419.71875, + 60.0, + 65.34375, + 30.0 + ], + "edit_area": [ + 289.5625, + 10.5, + 36.0, + 36.0 + ], + "fit": [ + 402.59375, + 10.5, + 36.0, + 36.0 + ], + "layout_mode": [ + 289.5625, + 10.5, + 36.0, + 36.0 + ], + "properties": [ + 197.5625, + 10.5, + 36.0, + 36.0 + ], + "redo": [ + 130.9375, + 13.5, + 56.625, + 30.0 + ], + "save": [ + 656.75, + 10.5, + 36.0, + 36.0 + ], + "select_mode": [ + 243.5625, + 10.5, + 36.0, + 36.0 + ], + "undo": [ + 63.15625, + 13.5, + 57.78125, + 30.0 + ] + }, + "decoration_selected": null, + "decorations": { + "axis_bottom": [ + 41.39225387573242, + 508.3333740234375, + 897.6077270507812, + 6.0 + ], + "axis_left": [ + 41.39225387573242, + 29.0, + 6.0, + 485.3333740234375 + ], + "data_area": [ + 44.39225387573242, + 32.0, + 891.6077270507812, + 479.3333740234375 + ], + "legend": [ + 417.14874267578125, + 556.8780517578125, + 117.03582763671875, + 12.78863525390625 + ], + "tick_labels_bottom": [ + 243.8538360595703, + 524.8779907226562, + 492.6845703125, + 12.78863525390625 + ], + "tick_labels_left": [ + 14.0, + 25.07803726196289, + 20.392257690429688, + 492.1219787597656 + ] + }, + "drawing_tool": "text", + "error": "", + "figure_id": 3, + "gpu_context_id": 1, + "host": "winit-wgpu", + "inspector_overlay": false, + "layout_mode": false, + "layout_probe": [ + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 169.0, + 75.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 169.0, + 75.0 + ], + "pressed": false + } + ], + "frame": 30, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 169.0, + 75.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 169.0, + 75.0 + ], + "tool": "arrow" + }, + { + "button_down": true, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": true + } + ], + "frame": 36, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 355.0, + 360.0 + ], + "press_origin": [ + 355.0, + 360.0 + ], + "released": false, + "resolved_origin": [ + 355.0, + 360.0 + ], + "tool": "arrow" + }, + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 105.0, + 75.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 105.0, + 75.0 + ], + "pressed": false + } + ], + "frame": 76, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 105.0, + 75.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 105.0, + 75.0 + ], + "tool": "text" + } + ], + "png_encodes": 0, + "pointer_input": { + "button": "Left", + "consumed": true, + "kind": "button", + "position": [ + 105.0, + 75.0 + ], + "state": "Released" + }, + "pointer_sequence": 23, + "properties_open": false, + "review": null, + "revision": 2, + "rgba_readbacks": 0, + "scale_factor": 1.0, + "selected": null, + "surface_frames": 79, + "surface_size": [ + 992, + 744 + ], + "window_role": "chart" + } + }, + "placed": { + "stage": "text-placed", + "settings": { + "schema_version": 2, + "renderer_version": "0.18.1", + "config": { + "chart_area": { + "x": 0, + "y": 0, + "width": 960, + "height": 600 + }, + "top_x": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "bottom_x": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 80.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "left_y": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 110.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "right_y": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "chart_title": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 28.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0, + "top_margin": 32.0 + }, + "grid": { + "show_major_x": true, + "major_x_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_x_width": 1.0, + "major_x_style": "Solid", + "show_major_y": true, + "major_y_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_y_width": 1.0, + "major_y_style": "Solid", + "show_minor_x": false, + "minor_x_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_x_width": 0.5, + "minor_x_style": "Dot", + "show_minor_y": false, + "minor_y_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_y_width": 0.5, + "minor_y_style": "Dot" + }, + "legend": { + "visible": false, + "content": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 14.0, + "font": "" + }, + "corner": "TopRight", + "offset_x": 0.0, + "offset_y": 0.0, + "padding": 8.0, + "bg_color": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 0.85 + }, + "border_color": { + "r": 0.6, + "g": 0.6, + "b": 0.6, + "a": 1.0 + } + } + }, + "series": [], + "boxplot": { + "decorations": { + "data_area": null, + "offsets": [], + "selected": null + }, + "title": "", + "categories": [ + { + "id": "category_0", + "label": "Box 1" + }, + { + "id": "category_1", + "label": "Box 2" + } + ], + "series": [ + { + "id": "series_0", + "label": "Distribution", + "values": [ + { + "q1": 1.5, + "median": 2.0, + "q3": 3.0, + "whisker_low": 1.0, + "whisker_high": 4.0, + "mean": 2.3333333333333335, + "sample_count": 3, + "median_ci": null, + "outliers": [] + }, + { + "q1": 3.5, + "median": 5.0, + "q3": 7.0, + "whisker_low": 2.0, + "whisker_high": 9.0, + "mean": 5.333333333333333, + "sample_count": 3, + "median_ci": null, + "outliers": [] + } + ], + "color": { + "r": 0.12156863, + "g": 0.46666667, + "b": 0.7058824, + "a": 1.0 + }, + "style": null + } + ], + "direction": "Vertical", + "style": { + "material": "Matte", + "corner_radius": 0.0, + "outline": true, + "outline_width": 1.2, + "outline_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "median_width": 1.2, + "median_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "whisker_width": 1.2, + "whisker_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "caps": true, + "cap_ratio": 0.5, + "notched": false, + "notch_depth": 0.22, + "show_mean": false, + "mean_size": 7.0, + "mean_color": { + "r": 0.7058824, + "g": 0.1764706, + "b": 0.1764706, + "a": 1.0 + }, + "show_outliers": true, + "outlier_size": 7.0, + "outlier_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "outlier_shape": "Circle", + "outlier_filled": false, + "texture_strength": 0.2, + "texture_scale": 1.0, + "gloss": 0.45, + "emphasis_brightness": 0.08 + }, + "overrides": [], + "hovered": null, + "selected": null, + "labels": "None", + "label_decimals": 1, + "value_suffix": "", + "value_title": "", + "category_title": "", + "value_range": null, + "grid": true, + "legend": true, + "group_width": 0.65, + "box_gap": 8.0, + "font_family": "sans-serif", + "font_size": 18.333334, + "text_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "background": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 1.0 + } + }, + "identity": { + "figure": "1bccaa56-20df-4f2f-8d3f-56725fbe3656", + "groups": {}, + "layout": null, + "panels": [ + { + "id": "main", + "overlays": [] + } + ], + "retired": [] + } + }, + "host": { + "chart_family": "boxplot", + "chart_panel": [ + 45, + 135, + 901, + 563 + ], + "column_uploads": 0, + "controls": { + "advanced": [ + 448.59375, + 13.5, + 152.15625, + 30.0 + ], + "copy": [ + 610.75, + 10.5, + 36.0, + 36.0 + ], + "draw": [ + 335.5625, + 13.5, + 57.03125, + 30.0 + ], + "draw_arrow": [ + 139.125, + 60.0, + 60.4375, + 30.0 + ], + "draw_data_anchor": [ + 495.0625, + 60.0, + 90.59375, + 30.0 + ], + "draw_hline": [ + 209.5625, + 60.0, + 57.40625, + 30.0 + ], + "draw_hspan": [ + 343.4375, + 60.0, + 66.28125, + 30.0 + ], + "draw_select": [ + 8.0, + 60.0, + 62.0, + 30.0 + ], + "draw_text": [ + 80.0, + 60.0, + 49.125, + 30.0 + ], + "draw_vline": [ + 276.96875, + 60.0, + 56.46875, + 30.0 + ], + "draw_vspan": [ + 419.71875, + 60.0, + 65.34375, + 30.0 + ], + "edit_area": [ + 289.5625, + 10.5, + 36.0, + 36.0 + ], + "fit": [ + 402.59375, + 10.5, + 36.0, + 36.0 + ], + "inline_text": [ + 355.0, + 361.0, + 40.0, + 32.0 + ], + "layout_mode": [ + 289.5625, + 10.5, + 36.0, + 36.0 + ], + "properties": [ + 197.5625, + 10.5, + 36.0, + 36.0 + ], + "redo": [ + 130.9375, + 13.5, + 56.625, + 30.0 + ], + "save": [ + 656.75, + 10.5, + 36.0, + 36.0 + ], + "select_mode": [ + 243.5625, + 10.5, + 36.0, + 36.0 + ], + "undo": [ + 63.15625, + 13.5, + 57.78125, + 30.0 + ] + }, + "decoration_selected": null, + "decorations": { + "axis_bottom": [ + 41.39225387573242, + 508.3333740234375, + 897.6077270507812, + 6.0 + ], + "axis_left": [ + 41.39225387573242, + 29.0, + 6.0, + 485.3333740234375 + ], + "data_area": [ + 44.39225387573242, + 32.0, + 891.6077270507812, + 479.3333740234375 + ], + "legend": [ + 417.14874267578125, + 556.8780517578125, + 117.03582763671875, + 12.78863525390625 + ], + "tick_labels_bottom": [ + 243.8538360595703, + 524.8779907226562, + 492.6845703125, + 12.78863525390625 + ], + "tick_labels_left": [ + 14.0, + 25.07803726196289, + 20.392257690429688, + 492.1219787597656 + ] + }, + "drawing_tool": "select", + "error": "", + "figure_id": 3, + "gpu_context_id": 1, + "host": "winit-wgpu", + "inspector_overlay": false, + "layout_mode": false, + "layout_probe": [ + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 169.0, + 75.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 169.0, + 75.0 + ], + "pressed": false + } + ], + "frame": 30, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 169.0, + 75.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 169.0, + 75.0 + ], + "tool": "arrow" + }, + { + "button_down": true, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": true + } + ], + "frame": 36, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 355.0, + 360.0 + ], + "press_origin": [ + 355.0, + 360.0 + ], + "released": false, + "resolved_origin": [ + 355.0, + 360.0 + ], + "tool": "arrow" + }, + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 105.0, + 75.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 105.0, + 75.0 + ], + "pressed": false + } + ], + "frame": 76, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 105.0, + 75.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 105.0, + 75.0 + ], + "tool": "text" + }, + { + "button_down": false, + "clicked": true, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": false + } + ], + "frame": 85, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 355.0, + 360.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 355.0, + 360.0 + ], + "tool": "text" + } + ], + "png_encodes": 0, + "pointer_input": { + "button": "Left", + "consumed": true, + "kind": "button", + "position": [ + 355.0, + 360.0 + ], + "state": "Released" + }, + "pointer_sequence": 27, + "properties_open": false, + "review": null, + "revision": 2, + "rgba_readbacks": 0, + "scale_factor": 1.0, + "selected": null, + "surface_frames": 97, + "surface_size": [ + 992, + 744 + ], + "window_role": "chart" + } + }, + "completed": { + "stage": "closing", + "settings": { + "schema_version": 2, + "renderer_version": "0.18.1", + "config": { + "chart_area": { + "x": 0, + "y": 0, + "width": 960, + "height": 600 + }, + "top_x": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "bottom_x": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 80.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "left_y": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 110.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "right_y": { + "scale": "Linear", + "min": 0.0, + "max": 1.0, + "major_spacing": 0.2, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "chart_title": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 28.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0, + "top_margin": 32.0 + }, + "grid": { + "show_major_x": true, + "major_x_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_x_width": 1.0, + "major_x_style": "Solid", + "show_major_y": true, + "major_y_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_y_width": 1.0, + "major_y_style": "Solid", + "show_minor_x": false, + "minor_x_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_x_width": 0.5, + "minor_x_style": "Dot", + "show_minor_y": false, + "minor_y_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_y_width": 0.5, + "minor_y_style": "Dot" + }, + "legend": { + "visible": false, + "content": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 14.0, + "font": "" + }, + "corner": "TopRight", + "offset_x": 0.0, + "offset_y": 0.0, + "padding": 8.0, + "bg_color": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 0.85 + }, + "border_color": { + "r": 0.6, + "g": 0.6, + "b": 0.6, + "a": 1.0 + } + } + }, + "series": [], + "boxplot": { + "decorations": { + "data_area": null, + "offsets": [], + "selected": null + }, + "title": "", + "active_layer": "Annotations", + "annotations": { + "visible": true, + "items": [ + { + "id": "annotation_18e0b5fed1825545_f", + "kind": { + "Text": { + "anchor": { + "Ratio": { + "x": 0.32066503, + "y": 0.43349132 + } + }, + "text": { + "segments": [ + { + "text": "R", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "e", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "v", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "i", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "e", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "w", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": " ", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "t", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "h", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "i", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "s", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": " ", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "r", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "e", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "g", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "i", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "o", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "n", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": " ", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "a", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "g", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "a", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "i", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + }, + { + "text": "n", + "bold": false, + "italic": false, + "underline": false, + "superscript": false, + "subscript": false, + "greek": false + } + ], + "color": { + "r": 0.13725491, + "g": 0.1764706, + "b": 0.23529412, + "a": 1.0 + }, + "font_size": 16.0, + "font": "" + }, + "box_style": null + } + } + }, + { + "id": "annotation_18e0b5ffed1445b9_10", + "kind": { + "Arrow": { + "from": { + "Data": { + "x": 0.16284030675888062, + "y": 5.242652893066406 + } + }, + "to": { + "Data": { + "x": 0.5452444553375244, + "y": 3.1527167558670044 + } + }, + "style": { + "line": { + "color": { + "r": 0.23529412, + "g": 0.23529412, + "b": 0.23529412, + "a": 1.0 + }, + "width": 1.5, + "style": "Solid" + }, + "head_length": 10.0, + "head_width": 8.0 + }, + "text": null + } + } + }, + { + "id": "annotation_18e0b60056466cbe_11", + "kind": { + "ReferenceLine": { + "axis": "Y", + "value": 3.1527167558670044, + "style": { + "color": { + "r": 0.23529412, + "g": 0.23529412, + "b": 0.23529412, + "a": 1.0 + }, + "width": 1.5, + "style": "Solid" + }, + "label": null + } + } + } + ] + }, + "categories": [ + { + "id": "category_0", + "label": "Box 1" + }, + { + "id": "category_1", + "label": "Box 2" + } + ], + "series": [ + { + "id": "series_0", + "label": "Distribution", + "values": [ + { + "q1": 1.5, + "median": 2.0, + "q3": 3.0, + "whisker_low": 1.0, + "whisker_high": 4.0, + "mean": 2.3333333333333335, + "sample_count": 3, + "median_ci": null, + "outliers": [] + }, + { + "q1": 3.5, + "median": 5.0, + "q3": 7.0, + "whisker_low": 2.0, + "whisker_high": 9.0, + "mean": 5.333333333333333, + "sample_count": 3, + "median_ci": null, + "outliers": [] + } + ], + "color": { + "r": 0.12156863, + "g": 0.46666667, + "b": 0.7058824, + "a": 1.0 + }, + "style": null + } + ], + "direction": "Vertical", + "style": { + "material": "Matte", + "corner_radius": 0.0, + "outline": true, + "outline_width": 1.2, + "outline_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "median_width": 1.2, + "median_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "whisker_width": 1.2, + "whisker_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "caps": true, + "cap_ratio": 0.5, + "notched": false, + "notch_depth": 0.22, + "show_mean": false, + "mean_size": 7.0, + "mean_color": { + "r": 0.7058824, + "g": 0.1764706, + "b": 0.1764706, + "a": 1.0 + }, + "show_outliers": true, + "outlier_size": 7.0, + "outlier_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "outlier_shape": "Circle", + "outlier_filled": false, + "texture_strength": 0.2, + "texture_scale": 1.0, + "gloss": 0.45, + "emphasis_brightness": 0.08 + }, + "overrides": [], + "hovered": null, + "selected": null, + "labels": "None", + "label_decimals": 1, + "value_suffix": "", + "value_title": "", + "category_title": "", + "value_range": null, + "grid": true, + "legend": true, + "group_width": 0.65, + "box_gap": 8.0, + "font_family": "sans-serif", + "font_size": 18.333334, + "text_color": { + "r": 0.1254902, + "g": 0.18039216, + "b": 0.23529412, + "a": 1.0 + }, + "background": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 1.0 + } + }, + "identity": { + "figure": "1bccaa56-20df-4f2f-8d3f-56725fbe3656", + "groups": {}, + "layout": null, + "panels": [ + { + "id": "main", + "overlays": [] + } + ], + "retired": [ + "annotation/annotation_18e0b600bef55f4a_12" + ] + } + }, + "host": { + "chart_family": "boxplot", + "chart_panel": [ + 45, + 135, + 901, + 563 + ], + "column_uploads": 0, + "controls": { + "advanced": [ + 448.59375, + 13.5, + 152.15625, + 30.0 + ], + "auto_range": [ + 1001.0, + 449.0, + 122.09375, + 30.0 + ], + "bounded_category_title": [ + 1001.0, + 775.25, + 280.0, + 20.125 + ], + "bounded_title": [ + 1001.0, + 719.125, + 280.0, + 20.125 + ], + "bounded_value_title": [ + 1001.0, + 831.375, + 280.0, + 20.125 + ], + "copy": [ + 610.75, + 10.5, + 36.0, + 36.0 + ], + "draw": [ + 335.5625, + 13.5, + 57.03125, + 30.0 + ], + "draw_arrow": [ + 139.125, + 60.0, + 60.4375, + 30.0 + ], + "draw_data_anchor": [ + 495.0625, + 60.0, + 90.59375, + 30.0 + ], + "draw_hline": [ + 209.5625, + 60.0, + 57.40625, + 30.0 + ], + "draw_hspan": [ + 343.4375, + 60.0, + 66.28125, + 30.0 + ], + "draw_select": [ + 8.0, + 60.0, + 62.0, + 30.0 + ], + "draw_text": [ + 80.0, + 60.0, + 49.125, + 30.0 + ], + "draw_vline": [ + 276.96875, + 60.0, + 56.46875, + 30.0 + ], + "draw_vspan": [ + 419.71875, + 60.0, + 65.34375, + 30.0 + ], + "edit_area": [ + 289.5625, + 10.5, + 36.0, + 36.0 + ], + "fit": [ + 402.59375, + 10.5, + 36.0, + 36.0 + ], + "font_family": [ + 1001.0, + 623.0, + 315.0, + 20.125 + ], + "font_size": [ + 1211.0, + 653.125, + 105.0, + 30.0 + ], + "layout_mode": [ + 289.5625, + 10.5, + 36.0, + 36.0 + ], + "object_search": [ + 1001.0, + 145.9375, + 273.0, + 20.125 + ], + "properties": [ + 197.5625, + 10.5, + 36.0, + 36.0 + ], + "properties_header": [ + 1001.0, + 397.0, + 209.28125, + 16.0 + ], + "properties_viewport": [ + 1001.0, + 423.0, + 315.0, + 289.0 + ], + "range_max": [ + 1123.875, + 489.0, + 51.125, + 30.0 + ], + "range_min": [ + 1034.40625, + 489.0, + 43.25, + 30.0 + ], + "redo": [ + 130.9375, + 13.5, + 56.625, + 30.0 + ], + "save": [ + 656.75, + 10.5, + 36.0, + 36.0 + ], + "select_mode": [ + 243.5625, + 10.5, + 36.0, + 36.0 + ], + "tree:bounded/annotation/annotation_18e0b5fed1825545_f": [ + 1057.0, + 295.0, + 255.0, + 24.0 + ], + "tree:bounded/annotation/annotation_18e0b5ffed1445b9_10": [ + 1057.0, + 321.0, + 255.0, + 24.0 + ], + "tree:bounded/annotation/annotation_18e0b60056466cbe_11": [ + 1057.0, + 347.0, + 255.0, + 24.0 + ], + "tree:bounded/annotations": [ + 1048.0, + 269.0, + 264.0, + 24.0 + ], + "tree:bounded/axes": [ + 1048.0, + 191.0, + 264.0, + 24.0 + ], + "tree:bounded/data": [ + 1048.0, + 217.0, + 264.0, + 24.0 + ], + "tree:bounded/legend": [ + 1045.0, + 181.0, + 267.0, + 8.0 + ], + "tree:bounded/series/series_0": [ + 1057.0, + 243.0, + 255.0, + 24.0 + ], + "tree_toggle:bounded": [ + 1013.0, + 113.0, + 20.0, + 24.0 + ], + "tree_toggle:bounded/annotations": [ + 1025.0, + 269.0, + 20.0, + 24.0 + ], + "tree_toggle:bounded/axes": [ + 1025.0, + 191.0, + 20.0, + 24.0 + ], + "tree_toggle:bounded/data": [ + 1025.0, + 217.0, + 20.0, + 24.0 + ], + "tree_toggle:figure/1bccaa56-20df-4f2f-8d3f-56725fbe3656": [ + 1001.0, + 87.0, + 20.0, + 24.0 + ], + "undo": [ + 63.15625, + 13.5, + 57.78125, + 30.0 + ] + }, + "decoration_selected": null, + "decorations": { + "axis_bottom": [ + 41.39225387573242, + 508.3333740234375, + 897.6077270507812, + 6.0 + ], + "axis_left": [ + 41.39225387573242, + 29.0, + 6.0, + 485.3333740234375 + ], + "data_area": [ + 44.39225387573242, + 32.0, + 891.6077270507812, + 479.3333740234375 + ], + "legend": [ + 417.14874267578125, + 556.8780517578125, + 117.03582763671875, + 12.78863525390625 + ], + "tick_labels_bottom": [ + 243.8538360595703, + 524.8779907226562, + 492.6845703125, + 12.78863525390625 + ], + "tick_labels_left": [ + 14.0, + 25.07803726196289, + 20.392257690429688, + 492.1219787597656 + ] + }, + "drawing_tool": "select", + "error": "", + "figure_id": 3, + "gpu_context_id": 1, + "host": "winit-wgpu", + "inspector_overlay": false, + "layout_mode": false, + "layout_probe": [ + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 169.0, + 75.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 169.0, + 75.0 + ], + "pressed": false + } + ], + "frame": 30, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 169.0, + 75.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 169.0, + 75.0 + ], + "tool": "arrow" + }, + { + "button_down": true, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": true + } + ], + "frame": 36, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 355.0, + 360.0 + ], + "press_origin": [ + 355.0, + 360.0 + ], + "released": false, + "resolved_origin": [ + 355.0, + 360.0 + ], + "tool": "arrow" + }, + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 105.0, + 75.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 105.0, + 75.0 + ], + "pressed": false + } + ], + "frame": 76, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 105.0, + 75.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 105.0, + 75.0 + ], + "tool": "text" + }, + { + "button_down": false, + "clicked": true, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": false + } + ], + "frame": 85, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 355.0, + 360.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 355.0, + 360.0 + ], + "tool": "text" + }, + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 105.0, + 75.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 105.0, + 75.0 + ], + "pressed": false + } + ], + "frame": 109, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 105.0, + 75.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 105.0, + 75.0 + ], + "tool": "text" + }, + { + "button_down": false, + "clicked": true, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 355.0, + 360.0 + ], + "pressed": false + } + ], + "frame": 118, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 355.0, + 360.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 355.0, + 360.0 + ], + "tool": "text" + }, + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 169.0, + 75.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 169.0, + 75.0 + ], + "pressed": false + } + ], + "frame": 201, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 169.0, + 75.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 169.0, + 75.0 + ], + "tool": "arrow" + }, + { + "button_down": true, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 364.0, + 379.0 + ], + "pressed": true + } + ], + "frame": 207, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 364.0, + 379.0 + ], + "press_origin": [ + 364.0, + 379.0 + ], + "released": false, + "resolved_origin": [ + 364.0, + 379.0 + ], + "tool": "arrow" + }, + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 524.0, + 473.0 + ], + "pressed": false + } + ], + "frame": 235, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 524.0, + 473.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": null, + "tool": "arrow" + }, + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 238.0, + 75.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 238.0, + 75.0 + ], + "pressed": false + } + ], + "frame": 254, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 238.0, + 75.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 238.0, + 75.0 + ], + "tool": "hline" + }, + { + "button_down": true, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 364.0, + 379.0 + ], + "pressed": true + } + ], + "frame": 260, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 364.0, + 379.0 + ], + "press_origin": [ + 364.0, + 379.0 + ], + "released": false, + "resolved_origin": [ + 364.0, + 379.0 + ], + "tool": "hline" + }, + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 524.0, + 473.0 + ], + "pressed": false + } + ], + "frame": 288, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 524.0, + 473.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": null, + "tool": "hline" + }, + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 377.0, + 75.0 + ], + "pressed": true + }, + { + "button": "Primary", + "position": [ + 377.0, + 75.0 + ], + "pressed": false + } + ], + "frame": 302, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 377.0, + 75.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": [ + 377.0, + 75.0 + ], + "tool": "hspan" + }, + { + "button_down": true, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 364.0, + 379.0 + ], + "pressed": true + } + ], + "frame": 308, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 364.0, + 379.0 + ], + "press_origin": [ + 364.0, + 379.0 + ], + "released": false, + "resolved_origin": [ + 364.0, + 379.0 + ], + "tool": "hspan" + }, + { + "button_down": false, + "clicked": false, + "drag_started": false, + "events": [ + { + "button": "Primary", + "position": [ + 524.0, + 473.0 + ], + "pressed": false + } + ], + "frame": 336, + "kind": "drawing_input", + "pass": 0, + "pointer": [ + 524.0, + 473.0 + ], + "press_origin": null, + "released": true, + "resolved_origin": null, + "tool": "hspan" + } + ], + "png_encodes": 0, + "pointer_input": { + "button": "Left", + "consumed": true, + "kind": "button", + "position": [ + 1070.0, + 664.0 + ], + "state": "Released" + }, + "pointer_sequence": 76, + "properties_open": true, + "review": null, + "revision": 8, + "rgba_readbacks": 0, + "scale_factor": 1.0, + "selected": null, + "surface_frames": 362, + "surface_size": [ + 1312, + 744 + ], + "window_role": "chart" + } + }, + "draft_settings_identical": true + }, + "input_contract": { + "egui_version": "0.36.2", + "source": "https://docs.rs/egui/0.36.2/src/egui/input_state/mod.rs.html", + "behavior": "PointerState clears press_origin when processing PointerButton release, including when a press and release are buffered in the same frame." + }, + "fix": "If the held-pointer snapshot has no origin, use the last primary-button press position in the current ordered raw events. Preserve the existing image hit test, drag preview and release commit.", + "scope": "Native wrapper input handling; no renderer, chart SSOT or timing threshold changes.", + "validation": { + "focused_passed": 8, + "seconds": 352.12, + "coverage": "Cartesian, logarithmic/inverted, vertical/horizontal boxplot, categorical, radial, radar and polar; each includes native and browser creation, editing, cancellation and export.", + "full_suite": { + "tests": 640, + "failures": 0, + "errors": 0, + "skipped": 0, + "seconds": 2444.032, + "wheel_sha256": "88e7f7cc4bfa8edf93ae9240f289144b530fc493dbe1f73a66355fb7b1537c2b" + } + } +} diff --git a/docs/measurements/native-inspector-menu-after-20261022.png b/docs/measurements/native-inspector-menu-after-20261022.png new file mode 100644 index 0000000..c9c2f6d Binary files /dev/null and b/docs/measurements/native-inspector-menu-after-20261022.png differ diff --git a/docs/measurements/native-inspector-menu-before-20261022.png b/docs/measurements/native-inspector-menu-before-20261022.png new file mode 100644 index 0000000..38361d6 Binary files /dev/null and b/docs/measurements/native-inspector-menu-before-20261022.png differ diff --git a/docs/measurements/native-inspector-popup-layer-20261022.json b/docs/measurements/native-inspector-popup-layer-20261022.json new file mode 100644 index 0000000..2ee9bb8 --- /dev/null +++ b/docs/measurements/native-inspector-popup-layer-20261022.json @@ -0,0 +1,1450 @@ +{ + "scenario": "Reopen the native axis scale menu after editing divergent opposite-axis ranges", + "before": { + "menu_probe": { + "button_clicked": false, + "frame": 999, + "id": "axis_scale", + "items": [ + { + "clicked": false, + "clip": [ + 0.0, + 533.0, + 1312.0, + 70.0 + ], + "contains_pointer": false, + "enabled": true, + "hovered": false, + "id": "id_0BCE", + "interact_rect": [ + 1005.0, + 533.0, + 162.0, + 30.0 + ], + "layer": "LayerId { Foreground id_40E5 }", + "layer_at_pointer": "LayerId { Foreground id_2B95 }", + "name": "Linear", + "pointer": [ + 1086.0, + 548.0 + ], + "rect": [ + 1005.0, + 533.0, + 162.0, + 30.0 + ], + "transform": null + }, + { + "clicked": false, + "clip": [ + 0.0, + 533.0, + 1312.0, + 70.0 + ], + "contains_pointer": false, + "enabled": true, + "hovered": false, + "id": "id_08C5", + "interact_rect": [ + 1005.0, + 573.0, + 162.0, + 30.0 + ], + "layer": "LayerId { Foreground id_40E5 }", + "layer_at_pointer": "LayerId { Foreground id_2B95 }", + "name": "Logarithmic", + "pointer": [ + 1086.0, + 548.0 + ], + "rect": [ + 1005.0, + 573.0, + 162.0, + 30.0 + ], + "transform": null + } + ], + "kind": "scale_control", + "pass": 0, + "popup_open": false, + "previous": "Logarithmic", + "value": "Logarithmic" + }, + "requested_scale": "Linear", + "accepted_before_config": { + "chart_area": { + "x": 0, + "y": 0, + "width": 960, + "height": 600 + }, + "top_x": { + "scale": "Logarithmic", + "min": 1.0, + "max": 201.0, + "major_spacing": 1.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 11.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Scientific", + "significant_digits": 2 + }, + "tick": "Both", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 3.0, + "offset_y": 4.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "bottom_x": { + "scale": "Logarithmic", + "min": 1.0, + "max": 101.0, + "major_spacing": 3.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Scientific", + "significant_digits": 2 + }, + "tick": "Both", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 80.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "left_y": { + "scale": "Linear", + "min": 1.0, + "max": 100.0, + "major_spacing": 10.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 110.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "right_y": { + "scale": "Linear", + "min": 1.0, + "max": 100.0, + "major_spacing": 10.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": false, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 11.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 6 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 3.0, + "offset_y": 4.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "chart_title": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 28.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0, + "top_margin": 32.0 + }, + "grid": { + "show_major_x": true, + "major_x_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_x_width": 1.0, + "major_x_style": "Solid", + "show_major_y": true, + "major_y_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_y_width": 1.0, + "major_y_style": "Solid", + "show_minor_x": false, + "minor_x_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_x_width": 0.5, + "minor_x_style": "Dot", + "show_minor_y": false, + "minor_y_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_y_width": 0.5, + "minor_y_style": "Dot" + }, + "legend": { + "visible": false, + "content": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 14.0, + "font": "" + }, + "corner": "TopRight", + "offset_x": 0.0, + "offset_y": 0.0, + "padding": 8.0, + "bg_color": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 0.85 + }, + "border_color": { + "r": 0.6, + "g": 0.6, + "b": 0.6, + "a": 1.0 + } + } + }, + "accepted_after_config": { + "chart_area": { + "x": 0, + "y": 0, + "width": 960, + "height": 600 + }, + "top_x": { + "scale": "Logarithmic", + "min": 1.0, + "max": 201.0, + "major_spacing": 1.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 11.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Scientific", + "significant_digits": 2 + }, + "tick": "Both", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 3.0, + "offset_y": 4.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "bottom_x": { + "scale": "Logarithmic", + "min": 1.0, + "max": 101.0, + "major_spacing": 3.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Scientific", + "significant_digits": 2 + }, + "tick": "Both", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 80.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "left_y": { + "scale": "Linear", + "min": 1.0, + "max": 100.0, + "major_spacing": 10.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 110.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "right_y": { + "scale": "Linear", + "min": 1.0, + "max": 100.0, + "major_spacing": 10.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": false, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 11.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 6 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 3.0, + "offset_y": 4.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "chart_title": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 28.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0, + "top_margin": 32.0 + }, + "grid": { + "show_major_x": true, + "major_x_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_x_width": 1.0, + "major_x_style": "Solid", + "show_major_y": true, + "major_y_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_y_width": 1.0, + "major_y_style": "Solid", + "show_minor_x": false, + "minor_x_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_x_width": 0.5, + "minor_x_style": "Dot", + "show_minor_y": false, + "minor_y_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_y_width": 0.5, + "minor_y_style": "Dot" + }, + "legend": { + "visible": false, + "content": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 14.0, + "font": "" + }, + "corner": "TopRight", + "offset_x": 0.0, + "offset_y": 0.0, + "padding": 8.0, + "bg_color": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 0.85 + }, + "border_color": { + "r": 0.6, + "g": 0.6, + "b": 0.6, + "a": 1.0 + } + } + } + }, + "after": { + "menu_probe": { + "button_clicked": false, + "frame": 1001, + "id": "axis_scale", + "items": [ + { + "clicked": true, + "clip": [ + 0.0, + 533.0, + 1312.0, + 70.0 + ], + "contains_pointer": true, + "enabled": true, + "hovered": true, + "id": "id_0BCE", + "interact_rect": [ + 1005.0, + 533.0, + 162.0, + 30.0 + ], + "layer": "LayerId { Foreground id_40E5 }", + "layer_at_pointer": "LayerId { Foreground id_40E5 }", + "name": "Linear", + "pointer": [ + 1086.0, + 548.0 + ], + "rect": [ + 1005.0, + 533.0, + 162.0, + 30.0 + ], + "transform": null + }, + { + "clicked": false, + "clip": [ + 0.0, + 533.0, + 1312.0, + 70.0 + ], + "contains_pointer": false, + "enabled": true, + "hovered": false, + "id": "id_08C5", + "interact_rect": [ + 1005.0, + 573.0, + 162.0, + 30.0 + ], + "layer": "LayerId { Foreground id_40E5 }", + "layer_at_pointer": "LayerId { Foreground id_40E5 }", + "name": "Logarithmic", + "pointer": [ + 1086.0, + 548.0 + ], + "rect": [ + 1005.0, + 573.0, + 162.0, + 30.0 + ], + "transform": null + } + ], + "kind": "scale_control", + "parent_layer": "LayerId { Middle id_2B95 }", + "pass": 0, + "popup_open": false, + "previous": "Logarithmic", + "value": "Linear" + }, + "requested_scale": "Linear", + "accepted_before_config": { + "chart_area": { + "x": 0, + "y": 0, + "width": 960, + "height": 600 + }, + "top_x": { + "scale": "Logarithmic", + "min": 1.0, + "max": 201.0, + "major_spacing": 1.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 11.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Scientific", + "significant_digits": 2 + }, + "tick": "Both", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 3.0, + "offset_y": 4.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "bottom_x": { + "scale": "Logarithmic", + "min": 1.0, + "max": 101.0, + "major_spacing": 3.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Scientific", + "significant_digits": 2 + }, + "tick": "Both", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 80.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "left_y": { + "scale": "Linear", + "min": 1.0, + "max": 100.0, + "major_spacing": 10.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 110.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "right_y": { + "scale": "Linear", + "min": 1.0, + "max": 100.0, + "major_spacing": 10.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": false, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 11.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 6 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 3.0, + "offset_y": 4.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "chart_title": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 28.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0, + "top_margin": 32.0 + }, + "grid": { + "show_major_x": true, + "major_x_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_x_width": 1.0, + "major_x_style": "Solid", + "show_major_y": true, + "major_y_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_y_width": 1.0, + "major_y_style": "Solid", + "show_minor_x": false, + "minor_x_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_x_width": 0.5, + "minor_x_style": "Dot", + "show_minor_y": false, + "minor_y_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_y_width": 0.5, + "minor_y_style": "Dot" + }, + "legend": { + "visible": false, + "content": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 14.0, + "font": "" + }, + "corner": "TopRight", + "offset_x": 0.0, + "offset_y": 0.0, + "padding": 8.0, + "bg_color": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 0.85 + }, + "border_color": { + "r": 0.6, + "g": 0.6, + "b": 0.6, + "a": 1.0 + } + } + }, + "accepted_after_config": { + "chart_area": { + "x": 0, + "y": 0, + "width": 960, + "height": 600 + }, + "top_x": { + "scale": "Logarithmic", + "min": 1.0, + "max": 201.0, + "major_spacing": 1.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 11.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Scientific", + "significant_digits": 2 + }, + "tick": "Both", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 3.0, + "offset_y": 4.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "bottom_x": { + "scale": "Linear", + "min": 1.0, + "max": 101.0, + "major_spacing": 20.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Scientific", + "significant_digits": 2 + }, + "tick": "Both", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 80.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "left_y": { + "scale": "Linear", + "min": 1.0, + "max": 100.0, + "major_spacing": 10.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": true, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 18.0, + "label_visible": true, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 3 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0 + }, + "out_margin": 110.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "right_y": { + "scale": "Linear", + "min": 1.0, + "max": 100.0, + "major_spacing": 10.0, + "minor_count": 4, + "inverted": false, + "label_style": { + "visible": false, + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 11.0, + "label_visible": false, + "label_font": "", + "label_offset_x": 0.0, + "label_offset_y": 0.0, + "format": "Decimal", + "significant_digits": 6 + }, + "tick": "Inside", + "title_option": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 22.0, + "font": "" + }, + "visible": false, + "offset_x": 3.0, + "offset_y": 4.0 + }, + "out_margin": 25.0, + "line_offset": 0.0, + "line_visible": true, + "line_color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "line_width": 1.0, + "line_style": "Solid", + "major_tick_length": 5.0, + "minor_tick_length": 3.0 + }, + "chart_title": { + "text": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 28.0, + "font": "" + }, + "visible": true, + "offset_x": 0.0, + "offset_y": 0.0, + "top_margin": 32.0 + }, + "grid": { + "show_major_x": true, + "major_x_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_x_width": 1.0, + "major_x_style": "Solid", + "show_major_y": true, + "major_y_color": { + "r": 0.78431374, + "g": 0.78431374, + "b": 0.78431374, + "a": 1.0 + }, + "major_y_width": 1.0, + "major_y_style": "Solid", + "show_minor_x": false, + "minor_x_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_x_width": 0.5, + "minor_x_style": "Dot", + "show_minor_y": false, + "minor_y_color": { + "r": 0.9019608, + "g": 0.9019608, + "b": 0.9019608, + "a": 1.0 + }, + "minor_y_width": 0.5, + "minor_y_style": "Dot" + }, + "legend": { + "visible": false, + "content": { + "segments": [], + "color": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + }, + "font_size": 14.0, + "font": "" + }, + "corner": "TopRight", + "offset_x": 0.0, + "offset_y": 0.0, + "padding": 8.0, + "bg_color": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 0.85 + }, + "border_color": { + "r": 0.6, + "g": 0.6, + "b": 0.6, + "a": 1.0 + } + } + } + }, + "fix": { + "inspector_order_before": "Foreground", + "inspector_order_after": "Middle", + "popup_order": "Foreground", + "reason": "The inspector must not intercept input for its popup menus" + }, + "review_layout": { + "before": { + "properties": [ + 998.0, + 448.0, + 296.0, + 174.0 + ], + "review": [ + 12.0, + 605.0, + 1288.0, + 36.25 + ] + }, + "after_cartesian": { + "properties": [ + 998.0, + 397.1875, + 296.0, + 100.0 + ], + "review": [ + 12.0, + 605.0, + 1288.0, + 36.25 + ] + }, + "fix": "Reserve the review panel before sizing the inspector from remaining space" + }, + "focused_validation": { + "passed": 5, + "seconds": 124.92, + "cases": [ + "paired axes x (native/browser)", + "paired axes y (native/browser)", + "long comments with Cartesian properties", + "long comments with radial properties", + "short click under injected slow frames" + ] + } +} diff --git a/examples/review_workflow.py b/examples/review_workflow.py new file mode 100644 index 0000000..e101bad --- /dev/null +++ b/examples/review_workflow.py @@ -0,0 +1,31 @@ +"""Native review controller; an external agent polls the printed cache path.""" +import argparse +from pathlib import Path + +import numpy as np +from figgyplot.review import Session + + +def build(session): + x=np.linspace(0,10,101) + density=session.figure(id='density') + density.axes[0].plot(x,np.exp(-x/3),id='density-values',linewidth=2.5) + density.axes[0].set_title('Density profile') + field=session.figure(id='field-plot') + field.axes[0].imshow(np.sin(x[:20,None])*np.cos(x[None,:20]),id='field-values') + field.axes[0].set_title('Spatial distribution') + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--cache',help='Resume an existing session cache') + args=parser.parse_args() + session=Session.open(args.cache) if args.cache else Session(source={'entrypoint':str(Path(__file__).resolve())}) + with session: + if not args.cache:build(session) + print('Review cache:',session.cache_dir,flush=True) + print('Agent command: python -m figgyplot.review events '+str(session.cache_dir),flush=True) + session.present();session.run_events(on_status=lambda status:print(status,flush=True)) + + +if __name__=='__main__':main() diff --git a/pyproject.toml b/pyproject.toml index 59b9d51..cf08eae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ test = ["pytest>=8", "pillow>=10", "anywidget>=0.9,<1"] [tool.maturin] python-source = "python" module-name = "figgyplot._native" -include = ["python/figgyplot/*.js", "python/figgyplot/*.css", "python/figgyplot/licenses/*.txt", "python/figgyplot/assets/*", "THIRD_PARTY_NOTICES.md", { path = "docs/*.md", format = "sdist" }, { path = "scripts/build-wasm.sh", format = "sdist" }, { path = "scripts/wasm-host.patch", format = "sdist" }, { path = "scripts/check_assets.py", format = "sdist" }, { path = "scripts/record_registry_provenance.py", format = "sdist" }, { path = "scripts/repro_upstream_colorbar/Cargo.toml", format = "sdist" }, { path = "scripts/repro_upstream_colorbar/README.md", format = "sdist" }, { path = "scripts/repro_upstream_colorbar/src/*.rs", format = "sdist" }] +include = ["python/figgyplot/review-feedback-v1.schema.json", "python/figgyplot/skills/figgyplot-review/SKILL.md", "python/figgyplot/*.js", "python/figgyplot/*.css", "python/figgyplot/licenses/*.txt", "python/figgyplot/assets/*", "THIRD_PARTY_NOTICES.md", { path = "docs/*.md", format = "sdist" }, { path = "scripts/build-wasm.sh", format = "sdist" }, { path = "scripts/wasm-host.patch", format = "sdist" }, { path = "scripts/check_assets.py", format = "sdist" }, { path = "scripts/record_registry_provenance.py", format = "sdist" }, { path = "scripts/repro_upstream_colorbar/Cargo.toml", format = "sdist" }, { path = "scripts/repro_upstream_colorbar/README.md", format = "sdist" }, { path = "scripts/repro_upstream_colorbar/src/*.rs", format = "sdist" }] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/python/figgyplot/_review_diff.py b/python/figgyplot/_review_diff.py new file mode 100644 index 0000000..653f6c8 --- /dev/null +++ b/python/figgyplot/_review_diff.py @@ -0,0 +1,121 @@ +"""Exact persistent changes with identity selectors for source reconciliation.""" +import copy +import json + +from ._review_store import digest + + +def persistent(state): + from ._native import persistent_settings_json + return json.loads(persistent_settings_json(json.dumps(state, allow_nan=False))) + + +def escape(value): + return str(value).replace('~', '~0').replace('/', '~1') + + +def _selector(path, item, index, document): + if not isinstance(item, dict): + return None + for key in ('series_id', 'id'): + if isinstance(item.get(key), str): + return {'key': key, 'value': item[key]} + if path == '/config/subplots': + return {'panel': document['identity']['panels'][index+1]['id']} + if path.endswith('/overlays') and path.startswith('/config'): + pieces = path.split('/') + owner = int(pieces[3])+1 if len(pieces)>3 and pieces[2]=='subplots' else 0 + return {'overlay': document['identity']['panels'][owner]['overlays'][index]} + return None + + +def resolve(document, path, selectors): + parts = path.split('/')[1:] + value = document + actual = [] + for encoded in parts: + key = encoded.replace('~1', '/').replace('~0', '~') + if isinstance(value, list): + selection = selectors.get('/'+'/'.join(actual)) + if selection: + if 'key' in selection: + key = next(i for i,v in enumerate(value) if v.get(selection['key']) == selection['value']) + elif 'panel' in selection: + key = next(i-1 for i,v in enumerate(document['identity']['panels']) if v['id']==selection['panel']) + else: + owner = int(actual[2])+1 if len(actual)>2 and actual[1]=='subplots' else 0 + key = document['identity']['panels'][owner]['overlays'].index(selection['overlay']) + else: + key = int(key) + value = value[key] + actual.append(escape(key)) + return value + + +def changes(before, after): + before, after = persistent(before), persistent(after) + result = [] + + def emit(op, path, a, b, selectors, **extra): + change = dict(op=op, path=path, before_exists=op!='add', after_exists=op!='remove', + before=copy.deepcopy(a), after=copy.deepcopy(b), selectors=copy.deepcopy(selectors), **extra) + change['id'] = digest(change) + change['object_id'] = 'figure/'+after['identity']['figure'] + for selector in selectors.values(): + change['object_id'] += '/'+str(selector.get('value', selector.get('panel', selector.get('overlay')))) + result.append(change) + + def walk(a, b, path, selectors): + if a == b: + return + if isinstance(a, dict) and isinstance(b, dict): + for key in sorted(set(a)|set(b)): + child = path+'/'+escape(key) + if key not in a: emit('add',child,None,b[key],selectors) + elif key not in b: emit('remove',child,a[key],None,selectors) + else: walk(a[key],b[key],child,selectors) + elif isinstance(a, list) and isinstance(b, list): + ka = [_selector(path,v,i,before) for i,v in enumerate(a)] + kb = [_selector(path,v,i,after) for i,v in enumerate(b)] + if (a or b) and all(v is not None for v in ka+kb) and len({json.dumps(v,sort_keys=True) for v in ka})==len(ka) and len({json.dumps(v,sort_keys=True) for v in kb})==len(kb): + current, keys = copy.deepcopy(a), ka[:] + for i in range(len(keys)-1,-1,-1): + if keys[i] not in kb: + emit('remove',path+'/'+str(i),current[i],None,dict(selectors, **{path:keys[i]})) + keys.pop(i);current.pop(i) + for i,key in enumerate(kb): + selected = dict(selectors, **{path:key}) + if key not in keys: + emit('add',path+'/'+str(i),None,b[i],selected) + keys.insert(i,key);current.insert(i,copy.deepcopy(b[i])) + else: + old = keys.index(key) + if old != i: + emit('move',path+'/'+str(i),current[old],current[old],selected, from_path=path+'/'+str(old)) + keys.insert(i,keys.pop(old));current.insert(i,current.pop(old)) + walk(current[i],b[i],path+'/'+str(i),selected) + elif len(a)==len(b): + for i,(old,new) in enumerate(zip(a,b)):walk(old,new,path+'/'+str(i),selectors) + else: + emit('replace',path,a,b,selectors) + else: + emit('replace',path,a,b,selectors) + walk(before,after,'',{}) + return result + + +def preserved(change, document): + try: + value = resolve(persistent(document), change['path'], change['selectors']) + except (KeyError, IndexError, StopIteration, ValueError, TypeError): + return not change['after_exists'] + if not change['after_exists']: + return False + if change['op'] == 'move': + # Selection resolves the item; order is checked against the new index. + parent, index = change['path'].rsplit('/',1) + try: + return resolve(persistent(document),parent,change['selectors'])[int(index)] == value + except (KeyError, IndexError, ValueError, TypeError): + return False + return value == change['after'] diff --git a/python/figgyplot/_review_runner.py b/python/figgyplot/_review_runner.py new file mode 100644 index 0000000..15f98ee --- /dev/null +++ b/python/figgyplot/_review_runner.py @@ -0,0 +1,30 @@ +"""Isolated review builder: source imports/cwd and GPU work stay out of the UI.""" +import hashlib +import os +from pathlib import Path +import runpy +import sys + +from .review import Session + + +def build(cache): + with Session.open(cache) as session: + session._validation=True + source=session.snapshot()['rounds'][-1]['source'] + os.chdir(source['cwd']) + sys.path.insert(0,str(Path(source['entrypoint']).parent)) + namespace=runpy.run_path(source['entrypoint'],run_name='__figgy_review__') + namespace[source['function']](session,**source['arguments']) + session.start() + for fid in list(session._figures): + fig=session._figures[fid] + if fig.backend!='native':fig=session.restore(fid,backend='native') + png=fig.to_png() + with session._store.locked(): + state=session._store.read() + state['rounds'][-1]['documents'][fid]['validated_png_sha256']=hashlib.sha256(png).hexdigest() + session._store.write(state) + + +if __name__=='__main__':build(sys.argv[1]) diff --git a/python/figgyplot/_review_store.py b/python/figgyplot/_review_store.py new file mode 100644 index 0000000..5f15421 --- /dev/null +++ b/python/figgyplot/_review_store.py @@ -0,0 +1,150 @@ +"""Crash-safe review manifests and immutable numeric payloads.""" +from contextlib import contextmanager +import hashlib +import io +import json +import os +from pathlib import Path +import re +import shutil +import tempfile +import threading +import time + +import numpy as np + + +def encoded(value): + return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode('utf-8') + + +def digest(value): + return hashlib.sha256(encoded(value)).hexdigest() + + +def _sync_directory(path): + if hasattr(os, 'O_DIRECTORY'): + fd = os.open(str(path), os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(fd) + finally: + os.close(fd) + + +def atomic_write(path, data): + fd, temporary = tempfile.mkstemp(prefix='.writing-', dir=str(path.parent)) + try: + with os.fdopen(fd, 'wb') as stream: + stream.write(data) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + _sync_directory(path.parent) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + + +class Store: + @staticmethod + def references(doc): + for key in ('columns','baseline_columns','bounded_data','baseline_bounded_data'): + yield from doc.get(key,{}).values() + + def __init__(self, path): + self.path = Path(path).resolve() + self.path.mkdir(parents=True, exist_ok=True) + (self.path/'data').mkdir(exist_ok=True) + self.mutex = threading.RLock() + self._seen_generation = None + + @contextmanager + def locked(self): + with self.mutex, (self.path/'.lock').open('a+b') as stream: + if os.name == 'nt': + import msvcrt + stream.seek(0) + if not stream.read(1): + stream.write(b'0'); stream.flush() + stream.seek(0) + msvcrt.locking(stream.fileno(), msvcrt.LK_LOCK, 1) + else: + import fcntl + fcntl.flock(stream, fcntl.LOCK_EX) + try: + yield + finally: + if os.name == 'nt': + stream.seek(0) + msvcrt.locking(stream.fileno(), msvcrt.LK_UNLCK, 1) + else: + fcntl.flock(stream, fcntl.LOCK_UN) + + def read(self): + state = json.loads((self.path/'manifest.json').read_text(encoding='utf-8')) + if state.get('protocol') != 1: + raise ValueError('Unsupported review protocol; expected version 1') + if state['generation'] != self._seen_generation: + # Also complete durability after a writer lost its fsync response + # following rename. Every controller confirms a newly observed head. + _sync_directory(self.path/'data') + _sync_directory(self.path) + self._seen_generation=state['generation'] + return state + + def write(self, state): + state['generation'] += 1 + _sync_directory(self.path/'data') + atomic_write(self.path/'manifest.json', encoded(state)) + self._seen_generation=state['generation'] + + def put(self, array): + array = np.ascontiguousarray(array) + if array.dtype.kind not in 'fiu' or array.ndim != 1: + raise ValueError('Review columns must be one-dimensional numeric arrays') + array = array.astype(array.dtype.newbyteorder('<'), copy=False) + stream = io.BytesIO() + np.save(stream, array, allow_pickle=False) + data = stream.getvalue() + sha = hashlib.sha256(data).hexdigest() + path = self.path/'data'/(sha+'.npy') + if not path.exists(): + atomic_write(path, data) + elif hashlib.sha256(path.read_bytes()).hexdigest() != sha: + raise ValueError('Corrupt existing review data: '+sha) + return dict(file='data/'+path.name, sha256=sha, bytes=len(data), dtype=array.dtype.str, shape=list(array.shape)) + + def get(self, reference): + sha = reference['sha256'] + if not isinstance(sha, str) or not re.fullmatch('[0-9a-f]{64}', sha) or reference['file'] != 'data/'+sha+'.npy': + raise ValueError('Invalid review data reference') + data = (self.path/reference['file']).read_bytes() + if len(data) != reference['bytes'] or hashlib.sha256(data).hexdigest() != sha: + raise ValueError('Review data integrity check failed: '+sha) + array = np.load(io.BytesIO(data), allow_pickle=False) + if array.dtype.str != reference['dtype'] or list(array.shape) != reference['shape']: + raise ValueError('Review data dtype/shape differs from manifest') + return array + + def collect(self, state): + # Unfinished work owns its baseline and approval; retain it until a + # complete successor is committed. Each retained round is self-contained. + rounds = state['rounds'] + latest = max((r['number'] for r in rounds), default=0) + retained = [r for r in rounds if r['number'] == latest or r['status'] not in ('completed', 'cancelled')] + if len(retained) != len(rounds): + state['rounds'] = retained + self.write(state) + live = {ref['file'] for r in retained for doc in r['documents'].values() for ref in self.references(doc)} + for path in (self.path/'data').iterdir(): + if path.name.startswith('.writing-') or ('data/'+path.name not in live and re.fullmatch('[0-9a-f]{64}\\.npy', path.name)): + path.unlink() + for path in self.path.glob('.writing-*'): + path.unlink() + active={r.get('claim',{}).get('token') for r in retained + if r['status']=='ready' and r.get('claim',{}).get('expires',0)>time.time()} + for path in self.path.glob('candidate-*'): + match=re.fullmatch('candidate-([0-9a-f]{32})-.+',path.name) + if match and match[1] not in active and path.is_dir(): + try:shutil.rmtree(path) + except OSError:pass # Retry on access if an expired writer still has files open. diff --git a/python/figgyplot/editor.css b/python/figgyplot/editor.css index 9a3f268..065096b 100644 --- a/python/figgyplot/editor.css +++ b/python/figgyplot/editor.css @@ -93,3 +93,9 @@ .figgy-choice-swatch{display:inline-block;flex:0 0 18px;height:3px} .figgy-webgpu .figgy-choice-preview{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:4} .figgy-switch-chart{position:absolute;z-index:5;transform:translate(calc(-100% - 6px),6px);font:12px system-ui,sans-serif;border:1px solid #9bb8dc;border-radius:5px;padding:4px 7px;background:#fff;color:#234d82} +.figgy-review {border-top:1px solid #d2d8e1;padding:12px 16px;background:#f5f8fc;} +.figgy-review[hidden] {display:none;} +.figgy-review label {display:block;} +.figgy-review textarea {display:block;box-sizing:border-box;width:100%;margin:6px 0;resize:vertical;font:inherit;} +.figgy-review button {margin-right:8px;} +.figgy-review [data-review-status] {font-size:13px;} diff --git a/python/figgyplot/notebook.py b/python/figgyplot/notebook.py index 17a5ed4..b569d4c 100644 --- a/python/figgyplot/notebook.py +++ b/python/figgyplot/notebook.py @@ -21,7 +21,7 @@ def upload_column(self, id, array): little = np.asarray(array, dtype=array.dtype.newbyteorder("<")) self.columns[id] = (str(array.dtype), little.tobytes(order="C")) - def add_series_fitted(self, spec_json, config=None, bounds=None): + def cache_series_extents(self, spec_json): # Only bounded extent metadata survives transfer release, never sample arrays. spec = json.loads(spec_json) style = next(iter(spec['render_type'].values())) @@ -67,6 +67,9 @@ def array(name): positive = values[values>0] extents.append((float(values.min()), float(values.max()), float(positive.min()) if positive.size else None)) self.model.cache_fit_extent(spec['series_id'], *extents) + + def add_series_fitted(self, spec_json, config=None, bounds=None): + self.cache_series_extents(spec_json) return self.model.add_series_fitted(spec_json, config, bounds) def remove_column(self, id): diff --git a/python/figgyplot/review-feedback-v1.schema.json b/python/figgyplot/review-feedback-v1.schema.json new file mode 100644 index 0000000..41445e2 --- /dev/null +++ b/python/figgyplot/review-feedback-v1.schema.json @@ -0,0 +1,257 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Figgyplot review feedback protocol 1", + "type": "object", + "required": [ + "protocol", + "session_id", + "round" + ], + "properties": { + "protocol": { + "const": 1 + }, + "session_id": { + "type": "string", + "minLength": 1 + }, + "round": { + "type": "object", + "required": [ + "id", + "number", + "status", + "source", + "documents", + "work_id" + ], + "properties": { + "id": { + "type": "string" + }, + "number": { + "type": "integer", + "minimum": 1 + }, + "status": { + "const": "ready" + }, + "work_id": { + "type": "string" + }, + "source": { + "type": "object" + }, + "documents": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/document" + } + } + } + } + }, + "$defs": { + "document": { + "type": "object", + "required": [ + "baseline", + "baseline_revision", + "baseline_hash", + "dpi", + "backend", + "columns", + "comments", + "approval" + ], + "properties": { + "baseline": { + "type": "object" + }, + "baseline_revision": { + "type": "integer", + "minimum": 0 + }, + "baseline_hash": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "columns": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/binary" + } + }, + "comments": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "text", + "target" + ], + "properties": { + "channel": { + "enum": [ + "api", + "ui" + ] + }, + "text": { + "type": "string" + }, + "id": { + "type": "string" + }, + "target": {} + } + } + }, + "approval": { + "type": "object", + "required": [ + "id", + "disposition", + "revision", + "settings", + "persistent_hash", + "changes" + ], + "properties": { + "disposition": { + "enum": [ + "accept", + "request_changes", + "skip" + ] + }, + "changes": { + "type": "array", + "items": { + "$ref": "#/$defs/change" + } + } + } + } + } + }, + "binary": { + "type": "object", + "required": [ + "file", + "sha256", + "bytes", + "dtype", + "shape" + ], + "properties": { + "file": { + "type": "string", + "pattern": "^data/[0-9a-f]{64}\\.npy$" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "bytes": { + "type": "integer", + "minimum": 0 + }, + "dtype": { + "type": "string" + }, + "shape": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + } + } + } + }, + "change": { + "type": "object", + "required": [ + "id", + "op", + "path", + "object_id", + "selectors", + "before_exists", + "after_exists", + "before", + "after" + ], + "properties": { + "id": { + "type": "string" + }, + "op": { + "enum": [ + "add", + "remove", + "replace", + "move" + ] + }, + "path": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "selectors": { + "type": "object" + }, + "before_exists": { + "type": "boolean" + }, + "after_exists": { + "type": "boolean" + } + } + }, + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/event" + } + }, + "event": { + "type": "object", + "required": [ + "protocol", + "session_id", + "round_id", + "work_id", + "manifest", + "source" + ], + "properties": { + "protocol": { + "const": 1 + }, + "session_id": { + "type": "string", + "minLength": 1 + }, + "round_id": { + "type": "string", + "minLength": 1 + }, + "work_id": { + "type": "string", + "minLength": 1 + }, + "manifest": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "object" + } + } + } + } +} diff --git a/python/figgyplot/review.py b/python/figgyplot/review.py new file mode 100644 index 0000000..48eaa48 --- /dev/null +++ b/python/figgyplot/review.py @@ -0,0 +1,638 @@ +"""Opt-in, durable, turn-based chart review. No LLM runtime is required.""" +from __future__ import annotations + +import argparse +from contextlib import ExitStack, contextmanager +import asyncio +import copy +import hashlib +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import threading +import time +import uuid +import numpy as np + +from ._review_diff import changes, persistent, preserved +from ._review_store import Store, digest + + +def _id(): + return uuid.uuid4().hex + + +def _ui_comment(doc): + return next((c['text'] for c in doc['comments'] if c.get('channel')=='ui'),'') + + +def _replace_ui_comment(doc, text): + doc['comments']=[c for c in doc['comments'] if c.get('channel')!='ui'] + if text:doc['comments'].append(dict(id=_id(),text=text,target=None,channel='ui')) + + +def _source(value): + value = copy.deepcopy(value or {}) + if value.get('entrypoint'): + path = Path(value['entrypoint']).resolve() + value.update(entrypoint=str(path), digest=hashlib.sha256(path.read_bytes()).hexdigest()) + value.setdefault('function', 'build') + value.setdefault('arguments', {}) + value.setdefault('cwd',str(Path.cwd())) + return value + + +class _CapturedData: + def __init__(self, native, session, figure_id, round_id, instance_id): + self.native, self.session, self.figure_id = native, session, figure_id + self.round_id = round_id + self.instance_id = instance_id + + def __getattr__(self, name): + value = getattr(self.native, name) + if name=='show': + def show(*args, **kwargs): + self.session._assert_mutable(self.figure_id,self.round_id,self.instance_id) + if self.session._validation: + raise RuntimeError('Source builders create Figures; the controller presents review windows') + if self.session.status['state']=='building': + raise RuntimeError('Seal the review set with start() or present() before opening windows') + fig=self.session._figures[self.figure_id] + if any(not handle.closed for handle in fig._windows): + raise RuntimeError('A review Figure already has an open native view') + return value(*args, **kwargs) + return show + mutations = {'apply_settings','add_series','add_series_fitted','add_series_batch', + 'set_radial','set_bounded','fit','fit_active','drag','resize_handle', + 'undo','redo','reset_edits'} + if name not in mutations: + return value + def mutate(*args, **kwargs): + self.session._assert_mutable(self.figure_id,self.round_id,self.instance_id) + return value(*args, **kwargs) + return mutate + + def upload_column(self, name, array): + with self.session._store.locked(): + state = self.session._store.read() + if state['rounds'][-1]['id']!=self.round_id:raise ValueError('Stale Figure from a previous review round') + doc = self.session._mutable(state, self.figure_id) + if doc['instance_id']!=self.instance_id:raise ValueError('Stale Figure instance; restore the current checkpoint') + reference = self.session._store.put(array) + self.native.upload_column(name, array) + try: + doc['columns'][name] = reference + self.session._store.write(state) + except Exception: + self.native.remove_column(name) + raise + + def remove_column(self, name): + with self.session._store.locked(): + state = self.session._store.read() + if state['rounds'][-1]['id']!=self.round_id:raise ValueError('Stale Figure from a previous review round') + doc = self.session._mutable(state, self.figure_id) + if doc['instance_id']!=self.instance_id:raise ValueError('Stale Figure instance; restore the current checkpoint') + self.native.remove_column(name) + doc['columns'].pop(name, None) + self.session._store.write(state) + + +class Session: + """A recoverable review round, with explicit approval and separate comments. + + Create source data through ``session.figure`` so it is captured before native + uploads or notebook transfer release. Sessions and their cache are opt-in. + """ + protocol = 1 + + def __init__(self, cache_dir=None, *, source=None, backend='native'): + cache=Path(cache_dir) if cache_dir is not None else Path(tempfile.mkdtemp(prefix='figgyplot-review-')) + if cache.exists() and (not cache.is_dir() or any(cache.iterdir())): + raise ValueError('Use a dedicated empty review cache directory; use Session.open() for an existing cache') + self._store = Store(cache) + self._figures = {} + self._handles = {} + self._widgets = {} + self._live_round = None + self._validation = False + self._worker_id = _id() + with self._store.locked(): + if (self.cache_dir/'manifest.json').exists(): + raise ValueError('Review cache already exists; use Session.open(cache_dir)') + if backend not in ('native','webgpu'): + raise ValueError('Review backend must be native or webgpu') + state = dict(protocol=1, session_id=_id(), generation=0, completed_work={}, + rounds=[dict(id=_id(),number=1,status='building',source=_source(source), + backend=backend,documents={},work_id=None)]) + self._store.write(state) + + @classmethod + def open(cls, cache_dir): + if not (Path(cache_dir)/'manifest.json').is_file(): + raise ValueError('Review cache manifest does not exist') + session = cls.__new__(cls) + session._store = Store(cache_dir) + session._figures,session._handles,session._widgets = {},{},{} + session._live_round,session._validation = None,False + session._worker_id = _id() + with session._store.locked(): + state = session._store.read() + # Check retained data before maintenance so corrupt recovery cannot + # delete another complete checkpoint's only available payload. + for r in state['rounds']: + for doc in r['documents'].values(): + for ref in session._store.references(doc):session._store.get(ref) + session._store.collect(state) + return session + + @property + def cache_dir(self): + return self._store.path + + def snapshot(self): + with self._store.locked(): + state = self._store.read() + self._store.collect(state) + return copy.deepcopy(state) + + def _mutable(self, state, figure_id): + r = state['rounds'][-1] + if r['status'] not in ('building','reviewing'): + raise RuntimeError('Review round is sealed; wait for the next round or reopen a Figure') + doc = r['documents'][figure_id] + if doc.get('approval') is not None: + raise RuntimeError('Figure is approved; reopen it before editing') + return doc + + def _assert_mutable(self, figure_id, round_id, instance_id): + with self._store.locked(): + state=self._store.read() + if state['rounds'][-1]['id']!=round_id:raise ValueError('Stale Figure from a previous review round') + doc=self._mutable(state,figure_id) + if doc['instance_id']!=instance_id:raise ValueError('Stale Figure instance; restore the current checkpoint') + + def figure(self, *, id, **kwargs): + from .figure import Figure + if not isinstance(id,str) or not id: + raise ValueError('Review Figures require a non-empty stable ID') + with self._store.locked(): + state = self._store.read();r = state['rounds'][-1] + if r['status'] != 'building': + raise RuntimeError('The review set is sealed; create Figures before present()') + if id in r['documents']:raise ValueError('Duplicate review Figure ID: '+id) + fig = Figure(id=id,backend=kwargs.pop('backend',r['backend']),**kwargs) + settings=fig.get_settings() + r['documents'][id]=dict(baseline=settings,baseline_revision=fig.revision, + baseline_hash=digest(persistent(settings)),draft=settings,draft_revision=fig.revision, + dpi=fig.dpi,backend=fig.backend,columns={},comments=[],approval=None,instance_id=_id()) + try:self._store.write(state) + except Exception:fig.close();raise + self._bind(fig,id) + return fig + + def _bind(self, fig, figure_id): + previous=self._figures.get(figure_id) + if previous is not None and previous is not fig:previous.close() + with self._store.locked(): + r=self._store.read()['rounds'][-1];round_id=r['id'];instance_id=r['documents'][figure_id]['instance_id'] + fig._native = _CapturedData(fig._native,self,figure_id,round_id,instance_id) + fig._review_session = self + self._figures[figure_id] = fig + + def _seal(self): + with self._figure_transaction(): + state=self._store.read();r=state['rounds'][-1] + if r['status']=='building': + if not r['documents']:raise ValueError('A review round needs at least one Figure') + for fid,doc in r['documents'].items(): + fig=self._figures[fid];settings=fig.get_settings() + doc.update(baseline=settings,baseline_revision=fig.revision, + baseline_hash=digest(persistent(settings)),draft=settings,draft_revision=fig.revision) + doc['baseline_columns']=copy.deepcopy(doc['columns']) + doc['baseline_instance_id']=doc['instance_id'] + doc['bounded_data']=self._bounded_data(settings) + doc['baseline_bounded_data']=copy.deepcopy(doc['bounded_data']) + r['status']='reviewing';self._store.write(state) + return r['number'] + + def _bounded_data(self, settings): + # Bounded models own canonical f64/statistical values in SSOT, rather + # than GPU columns. Cache those values separately without uploading them. + result={} + def collect(value,path): + if isinstance(value,dict): + for key,item in value.items():collect(item,path+'/'+key) + elif isinstance(value,list): + if value and all(isinstance(v,(int,float)) and not isinstance(v,bool) for v in value): + result[path]=self._store.put(np.asarray(value,dtype=np.float64)) + else: + for index,item in enumerate(value):collect(item,path+'/'+str(index)) + elif isinstance(value,(int,float)) and not isinstance(value,bool): + result[path]=self._store.put(np.asarray([value])) + for family in ('radial','categorical','boxplot','radar'): + if family not in settings:continue + chart=settings[family] + if family=='radial': + for index,item in enumerate(chart['slices']):collect(item['value'],f'/radial/slices/{index}/value') + for index,item in enumerate((chart.get('split') or {}).get('children',[])): + collect(item['value'],f'/radial/split/children/{index}/value') + else: + for index,item in enumerate(chart['series']):collect(item['values'],f'/{family}/series/{index}/values') + return result + + @contextmanager + def _figure_transaction(self): + # Figure mutations already acquire Figure -> store locks. Preserve this + # order while collecting multiple native/browser snapshots as well. + with ExitStack() as stack: + for fid in sorted(self._figures):stack.enter_context(self._figures[fid]._lock) + stack.enter_context(self._store.locked()) + yield + + def start(self): + """Seal all Figures and their baselines without opening a view.""" + self._seal() + return self + + def present(self): + """Seal the review set and show its complete pending native/notebook set.""" + number=self._seal() + if self._validation:return self + if threading.current_thread() is not threading.main_thread(): + raise RuntimeError('Review presentation runs on the main thread') + state=self.snapshot();r=state['rounds'][-1] + prepared=[] + try: + for fid,doc in r['documents'].items(): + if doc['approval'] is not None:continue + fig=self._figures.get(fid) or self.restore(fid) + if fig.backend=='native':fig.prepare() + prepared.append(fig) + for fig in prepared: + if fig.backend=='native': + old=self._handles.get(fig.id) + if old is None or old.closed: + handle=fig.show(block=False) + handle.configure_review(json.dumps(dict(figure_id=fig.id,comments=self._comment_text(fig.id)))) + self._handles[fig.id]=handle + else: + from IPython.display import display + display(self.widget(fig)) + self._live_round=number + except Exception: + for handle in self._handles.values():handle.close() + raise + return self + + def widget(self, figure): + """The ordinary live editor plus a separate review panel (not PNG fallback).""" + self._live_round=self._seal() + if isinstance(figure,str):figure=self._figures.get(figure) or self.restore(figure,backend='webgpu') + if figure.backend!='webgpu':raise ValueError('Notebook review requires backend=webgpu') + if figure.id not in self._figures or self._figures[figure.id] is not figure: + raise ValueError('Figure does not belong to this review session') + widget=figure.widget();widget.review=dict(enabled=True,figure_id=figure.id,comments=self._comment_text(figure.id)) + self._widgets[figure.id]=widget + return widget + + def _comment_text(self, figure_id): + return _ui_comment(self.snapshot()['rounds'][-1]['documents'][figure_id]) + + def comment(self, figure_id, text, *, target=None): + if not isinstance(text,str):raise ValueError('Review comment must be text') + with self._store.locked(): + state=self._store.read();doc=self._mutable(state,figure_id) + doc['comments'].append(dict(id=_id(),text=text,target=target,channel='api')) + self._store.write(state) + + def _set_comment_text(self, figure_id, text): + if not isinstance(text,str):raise ValueError('Review comments must be text') + with self._store.locked(): + state=self._store.read();doc=self._mutable(state,figure_id) + previous=_ui_comment(doc) + if previous!=text: + _replace_ui_comment(doc,text) + self._store.write(state) + + def submit(self, figure_id, disposition, *, comments=None, expected_revision=None): + """Durably approve accepted state, then close views; never approve on close.""" + if disposition not in ('accept','request_changes','skip'): + raise ValueError('Disposition must be accept, request_changes or skip') + fig=self._figures.get(figure_id) + if fig is None:raise ValueError('Restore the Figure before submitting it') + if fig.backend=='webgpu' and any(not w._closed for w in fig._widgets): + raise RuntimeError('Use await session.submit_async() to flush notebook edits') + return self._submit(figure_id,disposition,comments,expected_revision) + + async def submit_async(self, figure_id, disposition, *, comments=None, expected_revision=None): + fig=self._figures[figure_id] + if fig.backend=='webgpu':await fig.sync() + return self._submit(figure_id,disposition,comments,expected_revision) + + def _submit(self, figure_id, disposition, comments, expected_revision): + if disposition not in ('accept','request_changes','skip'): + raise ValueError('Unknown review disposition') + if comments is not None and not isinstance(comments,str):raise ValueError('Review comments must be text') + fig=self._figures[figure_id] + with fig._lock, self._store.locked(): + approval=self._commit_approval(figure_id,disposition,comments,expected_revision) + # Closing a view may call observers that read the session. Release the + # manifest lock before closing, on both first delivery and retries. + self._close_views(fig,disposition) + return copy.deepcopy(approval) + + def _commit_approval(self, figure_id, disposition, comments, expected_revision): + state=self._store.read();r=state['rounds'][-1];doc=r['documents'][figure_id] + # Repeated delivery of the same approval never creates another work ID. + if doc['approval'] is not None: + if doc['approval']['disposition']!=disposition or (comments is not None and doc['approval']['comment_text']!=comments): + raise ValueError('Figure already approved with different feedback') + self._store.write(state) + return doc['approval'] + self._mutable(state,figure_id) + fig=self._figures[figure_id] + if doc['instance_id']!=fig._native.instance_id:raise ValueError('Stale Figure instance; restore the current checkpoint') + if any(not hasattr(widget,'_clients') for widget in fig._widgets): + raise RuntimeError('Close the PNG fallback view before approving; use a live review view') + clients=sum(len(widget._clients) for widget in fig._widgets if not widget._closed) + if clients>1 or sum(not handle.closed for handle in fig._windows)>1: + raise RuntimeError('Close duplicate review views before approving this Figure') + if r['status']=='building':raise RuntimeError('Present/seal the complete review set before approval') + fig._native.end_edit() + revision=fig.revision + if expected_revision is not None and revision!=expected_revision: + raise ValueError('Stale review revision; refresh before approving') + approved=fig.get_settings() + doc['bounded_data']=self._bounded_data(approved) + if comments is not None: + _replace_ui_comment(doc,comments) + for ref in self._store.references(doc):self._store.get(ref) + approval=dict(id=_id(),disposition=disposition,revision=revision,settings=approved, + instance_id=doc['instance_id'], + persistent_hash=digest(persistent(approved)),changes=changes(doc['baseline'],approved), + comment_text=_ui_comment(doc)) + doc.update(approval=approval,draft=approved,draft_revision=revision) + if all(d['approval'] is not None for d in r['documents'].values()): + needs_work=any(d['approval']['disposition']=='request_changes' or d['approval']['changes'] or d['comments'] for d in r['documents'].values()) + r.update(status='ready' if needs_work else 'completed',work_id=_id() if needs_work else None) + self._store.write(state) + self._store.collect(state) + return approval + + def _close_views(self, fig, disposition): + for window in list(fig._windows):window.close() + for widget in list(fig._widgets): + widget.review=dict(enabled=True,approved=True,disposition=disposition) + widget.close() + + def events(self): + state=self.snapshot() + return [dict(protocol=1,session_id=state['session_id'],round_id=r['id'],work_id=r['work_id'], + manifest=str(self.cache_dir/'manifest.json'),source=r['source']) + for r in state['rounds'] if r['status']=='ready'] + + @property + def status(self): + r=self.snapshot()['rounds'][-1] + return dict(round=r['number'],state=r['status'],work_id=r['work_id'], + pending=[fid for fid,doc in r['documents'].items() if doc['approval'] is None], + error=r.get('error')) + + def feedback(self, work_id): + state=self.snapshot() + r=next((r for r in state['rounds'] if r['work_id']==work_id and r['status']=='ready'),None) + if r is None:raise ValueError('Stale or unknown review work ID') + return dict(protocol=1,session_id=state['session_id'],round=copy.deepcopy(r)) + + def claim(self, work_id, *, worker_id=None, seconds=1800): + """Lease one staging writer before an agent edits source for this work.""" + from .figure import _timeout + seconds=_timeout(seconds);worker_id=worker_id or self._worker_id + if not isinstance(worker_id,str) or not worker_id:raise ValueError('worker_id must be non-empty text') + with self._store.locked(): + state=self._store.read();r=state['rounds'][-1] + if r['status']!='ready' or r['work_id']!=work_id:raise ValueError('Stale or unknown review work ID') + claim=r.get('claim');now=time.time() + if claim and claim['expires']>now and claim['worker_id']!=worker_id: + raise RuntimeError('Review work is already claimed by another source writer') + if not claim or claim['worker_id']!=worker_id or claim['expires']<=now: + claim=dict(token=_id(),worker_id=worker_id) + claim['expires']=now+seconds;r['claim']=claim;self._store.write(state) + return copy.deepcopy(claim) + + def restore(self, figure_id, *, backend=None): + from .figure import Figure + with self._store.locked(): + state=self._store.read();doc=state['rounds'][-1]['documents'][figure_id] + settings=doc['draft'];rect=settings['config']['chart_area'];dpi=doc['dpi'] + fig=Figure(figsize=(rect['width']/dpi,rect['height']/dpi),dpi=dpi, + backend=backend or doc['backend'],id=figure_id, + pool_bytes=max(8*1024**2,sum(ref['shape'][0]*8 for ref in doc['columns'].values())*2)) + try: + for name,ref in doc['columns'].items():fig._native.upload_column(name,self._store.get(ref)) + family=next((k for k in ('radial','categorical','boxplot','radar') if k in settings),None) + if family=='radial':fig._native.set_radial(json.dumps(settings[family])) + elif family:fig._native.set_bounded(family,json.dumps(settings[family])) + else: + # Establish panel/overlay identities before registering the + # complete series batch; no positional owner is guessed. + bootstrap=copy.deepcopy(settings);bootstrap['series']=[] + def empty_membership(config): + for overlay in config.get('overlays',[]):overlay['series']=[] + for panel in config.get('subplots',[]): + panel['series']=[];empty_membership(panel['config']) + empty_membership(bootstrap['config']) + fig.apply_settings(bootstrap) + if not family and settings['series']: + if fig.backend=='webgpu': + for spec in settings['series']:fig._native.cache_series_extents(json.dumps(spec)) + fig._native.add_series_batch(json.dumps(settings['series']),json.dumps(settings['config'])) + fig.apply_settings(settings) + if persistent(fig.get_settings())!=persistent(settings): + raise ValueError('Restored settings differ from the complete checkpoint') + fig._next_series=len(settings['series']) + doc['instance_id']=_id() + self._store.write(state) + except Exception: + fig.close();raise + self._bind(fig,figure_id) + return fig + + def reopen(self, figure_id): + with self._store.locked(): + state=self._store.read();r=state['rounds'][-1] + if r['status'] not in ('reviewing','ready'):raise RuntimeError('Round cannot be reopened') + r['documents'][figure_id]['approval']=None + r.pop('claim',None) + r.update(status='reviewing',work_id=None) + self._store.write(state) + return self._figures.get(figure_id) or self.restore(figure_id) + + def cancel(self): + with self._store.locked(): + state=self._store.read();state['rounds'][-1]['status']='cancelled' + self._store.write(state) + self.close() + + def poll(self): + """Pump native input and durable submissions; call only on the main thread.""" + from .desktop import poll_events + poll_events() + state=self.snapshot();r=state['rounds'][-1] + if self._live_round is not None and r['number']!=self._live_round: + self.close();self.present();return + failed=False + for fid,handle in list(self._handles.items()): + request=json.loads(handle.review_request_json()) + if request is None:continue + try: + self.submit(fid,request['disposition'],comments=request['comments'],expected_revision=request['revision']) + except Exception as exc: + handle.review_result(str(exc)) + failed=True + if failed:return + # Native editor changes bypass Python callbacks. Capture accepted drafts + # here for disconnect recovery, without waiting for a GPU readback. + with self._figure_transaction(): + state=self._store.read();r=state['rounds'][-1];changed=False + for fid,fig in self._figures.items(): + doc=r['documents'].get(fid) + if doc and fig._native is not None and doc['instance_id']!=fig._native.instance_id:continue + if doc and doc['approval'] is None and fig._native is not None and doc['draft_revision']!=fig.revision: + doc.update(draft=fig.get_settings(),draft_revision=fig.revision);changed=True + handle=self._handles.get(fid) + if doc and doc['approval'] is None and handle is not None: + text=json.loads(handle.info_json())['review'].get('comments','') + if text!=_ui_comment(doc): + _replace_ui_comment(doc,text);changed=True + if changed:self._store.write(state) + + def run_events(self, *, on_status=None): + """Wait through review and disconnected-agent periods until accept/cancel.""" + previous=None + while True: + self.poll() + status=self.status + if status!=previous and on_status is not None:on_status(status) + previous=status + if status['state'] in ('completed','cancelled'):break + time.sleep(.02) + + async def watch(self): + """Notebook controller: publish the next complete set after agent work.""" + if self._live_round is None:self.present() + while True: + r=self.snapshot()['rounds'][-1] + if r['status'] in ('completed','cancelled'):return + if r['number']!=self._live_round: + self.close();self.present() + await asyncio.sleep(.1) + + def regenerate(self, work_id, *, superseded=None, claim_token=None, present=False, timeout=180): + """Run updated source, render every result, reconcile edits, publish once.""" + superseded=dict(superseded or {}) + if any(not isinstance(reason,str) or not reason.strip() for reason in superseded.values()): + raise ValueError('Superseded edits/Figures require non-empty reasons') + state=self.snapshot() + if work_id in state['completed_work']:return state['completed_work'][work_id] + if claim_token is None:claim_token=self.claim(work_id)['token'] + feedback=self.feedback(work_id);old=feedback['round'];source=_source(old['source']) + if old.get('claim',{}).get('token')!=claim_token or old['claim']['expires']<=time.time(): + raise ValueError('Source writer lease is invalid or expired') + known={c['id'] for doc in old['documents'].values() for c in doc['approval']['changes']}|{'figure:'+fid for fid in old['documents']} + if set(superseded)-known:raise ValueError('Unknown superseded edit or Figure ID') + if not source.get('entrypoint'):raise ValueError('Record a source entrypoint before regeneration') + from .figure import _timeout + timeout=_timeout(timeout) + with tempfile.TemporaryDirectory(prefix='candidate-'+claim_token+'-',dir=str(self.cache_dir)) as candidate_path: + candidate=Session(candidate_path,source=source,backend=old['backend']);candidate._validation=True + try: + result=subprocess.run([sys.executable,'-m','figgyplot._review_runner',candidate_path], + capture_output=True,text=True,timeout=timeout) + if result.returncode: + raise RuntimeError('Candidate execution/render failed:\n'+result.stderr[-4000:]) + current=candidate.snapshot()['rounds'][-1] + if not all(doc.get('validated_png_sha256') for doc in current['documents'].values()): + raise ValueError('Candidate set lacks complete render validation') + for fid,doc in old['documents'].items(): + if fid not in current['documents']: + if 'figure:'+fid not in superseded:raise ValueError('Candidate omitted Figure '+fid) + continue + for change in doc['approval']['changes']: + if change['id'] not in superseded and not preserved(change,current['documents'][fid]['baseline']): + raise ValueError('Source did not reproduce approved edit: '+fid+' '+change['path']) + if _source(source)['digest']!=source['digest']:raise ValueError('Source changed during candidate validation') + with self._store.locked(): + state=self._store.read() + if work_id in state['completed_work']:return state['completed_work'][work_id] + active=state['rounds'][-1] + if active['status']!='ready' or active['work_id']!=work_id: + raise ValueError('Stale round; candidate was not published') + if active.get('claim',{}).get('token')!=claim_token or active['claim']['expires']<=time.time(): + raise ValueError('Source writer lease expired during validation') + for doc in current['documents'].values(): + for key in ('columns','baseline_columns','bounded_data','baseline_bounded_data'): + for name,ref in list(doc.get(key,{}).items()): + doc[key][name]=self._store.put(candidate._store.get(ref)) + current.update(number=active['number']+1,source=source,superseded=superseded) + active['status']='completed';state['rounds'].append(current) + state['completed_work'][work_id]=current['number'] + self._store.write(state);self._store.collect(state) + number=current['number'] + except Exception as exc: + try: + with self._store.locked(): + state=self._store.read();active=state['rounds'][-1] + if active['status']=='ready' and active['work_id']==work_id: + active['error']=dict(type=type(exc).__name__,message=str(exc)) + self._store.write(state) + except OSError:pass + raise + finally: + candidate.close() + if present: + self.close();self.present() + return number + + def close(self): + for fig in list(self._figures.values()):fig.close() + self._figures.clear();self._handles.clear();self._widgets.clear() + + def __enter__(self):return self + def __exit__(self,*args):self.close() + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('action',choices=['protocol','skill','status','events','feedback','claim','regenerate','show','cancel']) + parser.add_argument('cache',nargs='?') + parser.add_argument('work',nargs='?') + parser.add_argument('--superseded',help='JSON file mapping edit IDs or figure:ID to reasons') + parser.add_argument('--worker',help='Stable agent/staging-workspace identifier for a writer lease') + parser.add_argument('--claim-token',help='Writer lease token returned by claim') + parser.add_argument('--timeout',type=float,default=180,help='Maximum candidate execution/render seconds') + args=parser.parse_args() + package=Path(__file__).parent + if args.action=='protocol': + print(json.dumps(dict(protocol=1,skill=str(package/'skills/figgyplot-review/SKILL.md'),schema=str(package/'review-feedback-v1.schema.json'),event_schema=str(package/'review-feedback-v1.schema.json')+'#/$defs/events')));return + if args.action=='skill': + print((package/'skills/figgyplot-review/SKILL.md').read_text(encoding='utf-8'));return + if not args.cache:parser.error('cache is required for this action') + with Session.open(args.cache) as session: + if args.action=='status':result=session.status + elif args.action=='events':result=session.events() + elif args.action=='feedback':result=session.feedback(args.work) + elif args.action=='claim':result=session.claim(args.work,worker_id=args.worker) + elif args.action=='regenerate': + if not args.claim_token:parser.error('regenerate requires --claim-token from claim') + superseded=json.loads(Path(args.superseded).read_text()) if args.superseded else None + result=dict(round=session.regenerate(args.work,superseded=superseded,claim_token=args.claim_token,timeout=args.timeout)) + elif args.action=='show':session.present();session.run_events();result=session.events() + else:session.cancel();result={'cancelled':True} + print(json.dumps(result,ensure_ascii=False,allow_nan=False)) + + +if __name__=='__main__':main() diff --git a/python/figgyplot/skills/figgyplot-review/SKILL.md b/python/figgyplot/skills/figgyplot-review/SKILL.md new file mode 100644 index 0000000..ca9bf1d --- /dev/null +++ b/python/figgyplot/skills/figgyplot-review/SKILL.md @@ -0,0 +1,46 @@ +--- +name: figgyplot-review +description: Consume figgyplot protocol-1 review batches, reconcile exact human chart edits and separate comments into runnable source, validate and publish the next complete round. +--- + +# Figgyplot review adapter + +This skill ships with figgyplot. It uses `python -m figgyplot.review` and protocol +1. Run `protocol` to discover the installed schema and skill paths. A host may +copy this directory into its skill search path; no particular agent vendor, +model SDK or background service is required. Ordinary plotting remains usable +without installing this skill. + +1. Obtain the session cache path from the user/controller. Poll `events CACHE`. + No event means no complete batch is ready. Do not work on individual Figure + approval before the whole round is submitted. +2. Read `feedback CACHE WORK_ID`. Use the baseline and approved settings, exact + change operations and stable selectors. Read natural-language review comments + separately. They are instructions, not chart annotations. Binary references + are recovery/input files; do not paste raw arrays into model messages. +3. Before editing source, run `claim CACHE WORK_ID --worker YOUR_STABLE_ID`. + Keep its token and use one staging workspace/source writer for this session. + If another worker holds the lease, wait or resume that worker. Reclaim expired + leases after disconnect; never overwrite another active worker's source. +4. Inspect the recorded entry point/function, arguments, inputs and source digest. + Preserve stable Figure IDs. Update runnable source to retain each exact human + edit and satisfy the comments. Source builders use `build(session, **arguments)` + and `session.figure(id=...)`; they do not start an agent or approve themselves. +5. Run `regenerate CACHE WORK_ID --claim-token TOKEN`. This executes the builder, + renders every candidate, checks retained edits and source stability, and + atomically publishes one complete successor. The live controller displays it + after publication. Renew the lease with the same worker ID for long work. +6. Only explicitly supersede an approved edit when feedback requires it. Supply + `--superseded FILE` containing a JSON map from the operation's `id` (or + `figure:ID` for a removed Figure) to a concrete reason. Never waive all edits + merely to make validation pass. +7. On execution/render/stale-base failure, inspect the error and correct source. + The previous checkpoint and pending work remain recoverable. Do not delete + manifests or binary files. Repeated events and completion of the same work ID + are idempotent. Application code owns revision-driven cleanup. + +The human controller uses `Session.present()` / `run_events()` in native mode or +live widgets with `await session.watch()` in Jupyter. Agent regeneration belongs +outside that UI loop. `show CACHE` can resume a disconnected native controller. +Manual window closure is not approval. `accept` without edits/comments finishes +the round; `request_changes` submits another work batch after every disposition. diff --git a/python/figgyplot/webgpu.js b/python/figgyplot/webgpu.js index 837e830..2215406 100644 --- a/python/figgyplot/webgpu.js +++ b/python/figgyplot/webgpu.js @@ -54,6 +54,40 @@ export function render({model,el}) { let revision=model.get('state').revision,sequence=0,inflight=null,pending=null,barriers=[],local=clone(model.get('state').settings); let editHistory=model.get('state').edit_history??{},historyBusy=false; let propertyColumns=clone(model.get('state').property_columns??[]),propertyCatalog=clone(model.get('state').property_catalog??[]),controlQueue=Promise.resolve(),controlFinish=null; + // Session metadata is separate from chart SSOT and ordinary inspector fields. + const reviewPanel=document.createElement('section');reviewPanel.className='figgy-review';reviewPanel.hidden=true; + reviewPanel.innerHTML='