Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,80 @@ See [examples/datatool_dashboard.py](examples/datatool_dashboard.py) for a full

To regenerate the screenshots after UI changes, see [docs/generate_screenshots.py](docs/generate_screenshots.py).

## ProcessObjectView (Process/Object Dashboard UI)

Where `DataToolView` is centered on data tools, `ProcessObjectView` is centered
on the **objects** (samples, specimens, ... - any `Item`) that pass through
processes. It answers *"how did measurement X compare across the runs my objects
went through?"* by overlaying repeated runs on a common, time-normalized axis.

![Process Demo](docs/process_demo.gif)

*Pick objects (tree 1) and a channel under a process type (tree 2); each run is
overlaid from t=0.*

Two trees drive the plot:

1. **Objects** - the `Item` instances to compare.
2. **Process types -> channels** - for each process type the objects went
through, the channels of the data tools used in those runs.

Tree 2 is **aggregated for selection only** (tick once instead of ticking the
same channel on every run and tool). The aggregation is **co-presence aware**:

- data tools of the same type that run **together** in a process stay as
**separate** entries (distinct measurement points);
- data tools that only ever appear in **different** runs are treated as drop-in
replacements and **merged** into one `… [n channels]` entry (the actual
channels are listed in its tooltip).

![Process trees](docs/screenshot_process_trees.png)

*Evacuation ran both probes together (separate per-instance entries); Heating
swapped the probe between runs (merged `DataTool/… [2 channels]` entries).*

Selecting an object + a channel entry plots **every real channel that object has
data on** - fanning out across process runs and co-present tools. Each line is
normalized to its own run (first data point at t=0, x-axis in seconds), grouped
by characteristic, and gets a distinct legend entry
(`object / process / data tool / channel`).

![Process overlay](docs/screenshot_process_overlay.png)

*One channel selection fans out to two runs of the same sample, overlaid from
t=0 for comparison.*

![Heating drop-in](docs/screenshot_process_heating.png)

*Adding `Heating/DataTool/temp` and `…/pressure` on top of the Evacuation
selection overlays both processes, grouped into separate Temperature and Pressure
plots; the merged Heating entries resolve to probe A for Sample 1 and probe B for
Sample 2 (drop-in replacements compared across objects).*

```python
from opensemantic.base.view import ProcessObjectView
from opensemantic.base.view._config import DashboardConfig

view = ProcessObjectView(
objects=objects, # list[Item]
processes=processes, # list[Process] (filtered to those with start+end
# time and >=1 DataTool whose data you can load)
controllers=controllers, # list[DataToolController], matched to process tools
config=DashboardConfig(lang="en"),
title="Process / Object Archive View",
)
view.servable() # for panel serve
```

`DataToolView` and `ProcessObjectView` share their plot/unit/config machinery via
`BaseDataView`, and both support `embeddable=True` (exposing `sidebar_cards` /
`main_cards`) so a host app can combine them.

See [examples/process_dashboard.py](examples/process_dashboard.py) for a full
working example, and
[docs/generate_process_screenshots.py](docs/generate_process_screenshots.py) to
regenerate these screenshots.

## Installation

```bash
Expand Down
217 changes: 217 additions & 0 deletions docs/generate_process_screenshots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
"""Generate screenshots and demo GIF for the ProcessObjectView README.

Prerequisites:
pip install playwright imageio
playwright install chromium

Usage:
python docs/generate_process_screenshots.py
"""

import glob
import io
import os
import subprocess
import sys
import time

import imageio.v3 as iio
from playwright.sync_api import sync_playwright

DOCS_DIR = os.path.dirname(os.path.abspath(__file__))
PACKAGE_DIR = os.path.dirname(DOCS_DIR)
EXAMPLE = os.path.join(PACKAGE_DIR, "examples", "process_dashboard.py")
PORT = 5012
URL = f"http://localhost:{PORT}/process_dashboard"
VIEWPORT = {"width": 1400, "height": 900}


def all_wb_shadows_js():
"""JS that returns all Wunderbaum shadow roots in DOM order."""
return """
function allWbShadows(root) {
const out = [];
function rec(r) {
for (const el of r.querySelectorAll('*')) {
if (el.shadowRoot) {
if (el.shadowRoot.querySelectorAll('.wb-row').length > 0)
out.push(el.shadowRoot);
rec(el.shadowRoot);
}
}
}
rec(root);
return out;
}
"""


def click_tree_checkbox(page, tree_idx, cb_idx):
"""Click the cb_idx-th checkbox of the tree_idx-th Wunderbaum (0=objects)."""
page.evaluate(
f"""() => {{
{all_wb_shadows_js()}
const shadows = allWbShadows(document);
const sh = shadows[{tree_idx}];
if (sh) {{
const cbs = sh.querySelectorAll('i.wb-checkbox');
if (cbs[{cb_idx}]) cbs[{cb_idx}].click();
}}
}}"""
)


def switch_temperature_unit(page):
"""Switch the Temperature unit dropdown to Celsius."""
page.evaluate(
"""() => {
function findAll(root) {
const sels = root.querySelectorAll('select.bk-input');
for (const s of sels) {
const label = s.closest('.bk-input-group')
?.querySelector('label')?.textContent || '';
if (label.includes('Temperature')) {
for (let i = 0; i < s.options.length; i++) {
const t = s.options[i].text;
if (t.includes('C') && !t.includes('K') && t.length < 5) {
s.value = s.options[i].value;
s.dispatchEvent(new Event('change', {bubbles: true}));
return true;
}
}
}
}
for (const el of root.querySelectorAll('*')) {
if (el.shadowRoot) {
const r = findAll(el.shadowRoot);
if (r) return r;
}
}
return false;
}
return findAll(document);
}"""
)


def capture(page, frames, delay=500):
page.wait_for_timeout(delay)
buf = page.screenshot()
frames.append(iio.imread(io.BytesIO(buf)))


def start_server():
"""Start the Panel server, cleaning old archive DBs for fresh data."""
for db in glob.glob(os.path.join(PACKAGE_DIR, "*_db.sqlite")):
os.remove(db)
proc = subprocess.Popen(
[sys.executable, "-m", "panel", "serve", EXAMPLE, "--port", str(PORT)],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=PACKAGE_DIR,
)
# Let the import + first module execution settle. panel serve re-runs the
# script per session (storing the demo data each time), so the first page
# load is slow - handled by a long goto timeout below.
time.sleep(10)
return proc


def stop_server(proc):
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()


def main():
print("Starting Panel server...")
proc = start_server()
try:
frames = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport=VIEWPORT)
# panel serve rebuilds the demo data per session -> slow first load.
page.goto(URL, timeout=120000, wait_until="domcontentloaded")
page.wait_for_timeout(12000)

# Frame 0-1: initial view (both trees, process tree shows
# per-instance Evacuation entries and merged Heating entries).
capture(page, frames, 800)
capture(page, frames, 500)
tree_shot = len(frames) - 1

# Select object "Sample 1" (objects tree, checkbox 0).
click_tree_checkbox(page, 0, 0)
capture(page, frames, 1500)

# Select Evacuation / FurnaceProbe-A / temp
# (process tree: 0=Evac root, 1=A/temp, 2=B/temp, 3=A/press, ...).
click_tree_checkbox(page, 1, 1)
capture(page, frames, 3000)
overlay_shot = len(frames) - 1 # Sample 1: two Evacuation runs

# Add FurnaceProbe-B / temp -> co-present fan-out.
click_tree_checkbox(page, 1, 2)
capture(page, frames, 2500)

# Add object "Sample 2" -> compare across objects.
click_tree_checkbox(page, 0, 1)
capture(page, frames, 3000)
fanout_shot = len(frames) - 1

# Switch Temperature unit to Celsius.
switch_temperature_unit(page)
capture(page, frames, 2500)
units_shot = len(frames) - 1
capture(page, frames, 1000)

# Also add the Heating process so BOTH processes are selected at the
# end. Evacuation stays selected; the merged Heating entry is a
# drop-in pool resolving to probe A for Sample 1 and probe B for
# Sample 2 (process tree: 6 = Heating DataTool/temp).
click_tree_checkbox(page, 1, 6) # check Heating DataTool/temp
capture(page, frames, 3000)

# Finally add the Heating pressure channel (process tree: 7) -> a
# second plot group (Pressure) appears alongside Temperature.
click_tree_checkbox(page, 1, 7) # check Heating DataTool/pressure
capture(page, frames, 3000)
heating_shot = len(frames) - 1
capture(page, frames, 1500)

browser.close()

gif_path = os.path.join(DOCS_DIR, "process_demo.gif")
iio.imwrite(gif_path, frames, duration=1500, loop=0)
print(f"process_demo.gif: {len(frames)} frames")

iio.imwrite(
os.path.join(DOCS_DIR, "screenshot_process_trees.png"), frames[tree_shot]
)
iio.imwrite(
os.path.join(DOCS_DIR, "screenshot_process_overlay.png"),
frames[overlay_shot],
)
iio.imwrite(
os.path.join(DOCS_DIR, "screenshot_process_fanout.png"),
frames[fanout_shot],
)
iio.imwrite(
os.path.join(DOCS_DIR, "screenshot_process_units.png"),
frames[units_shot],
)
iio.imwrite(
os.path.join(DOCS_DIR, "screenshot_process_heating.png"),
frames[heating_shot],
)
print("Static screenshots saved")
finally:
print("Stopping server...")
stop_server(proc)


if __name__ == "__main__":
main()
Binary file added docs/process_demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshot_process_fanout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshot_process_heating.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshot_process_overlay.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshot_process_trees.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshot_process_units.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading