|
| 1 | +""" |
| 2 | +_electron.py |
| 3 | +============ |
| 4 | +Electron app bridge for anyplotlib figures. |
| 5 | +
|
| 6 | +Registers figures so their trait changes are forwarded to the Electron |
| 7 | +renderer via stdout, and provides dispatch_event() so the renderer can |
| 8 | +send interaction events back to Python. |
| 9 | +""" |
| 10 | +from __future__ import annotations |
| 11 | + |
| 12 | +import json |
| 13 | +import sys |
| 14 | +import uuid |
| 15 | + |
| 16 | +_figures: dict[str, object] = {} # fig_id -> Figure |
| 17 | + |
| 18 | + |
| 19 | +def register(fig) -> str: |
| 20 | + """Register *fig* for bidirectional state sync and return its fig_id.""" |
| 21 | + fig_id = uuid.uuid4().hex[:8] |
| 22 | + _figures[fig_id] = fig |
| 23 | + |
| 24 | + def _on_change(change): |
| 25 | + name = change["name"] |
| 26 | + value = change["new"] |
| 27 | + if isinstance(value, (bytes, bytearray)): |
| 28 | + import base64 |
| 29 | + value = {"buffer": base64.b64encode(value).decode()} |
| 30 | + emit({"type": "state_update", "fig_id": fig_id, "key": name, "value": value}) |
| 31 | + |
| 32 | + for name in fig.traits(sync=True): |
| 33 | + if not name.startswith("_"): |
| 34 | + try: |
| 35 | + fig.observe(_on_change, names=[name]) |
| 36 | + except Exception: |
| 37 | + pass |
| 38 | + |
| 39 | + return fig_id |
| 40 | + |
| 41 | + |
| 42 | +def resize_figure(fig_id: str, width: int, height: int) -> None: |
| 43 | + """Update fig_width / fig_height and push new layout to the iframe.""" |
| 44 | + fig = _figures.get(fig_id) |
| 45 | + if fig is None: |
| 46 | + return |
| 47 | + try: |
| 48 | + # Batch both trait changes so _on_resize fires only once each. |
| 49 | + with fig.hold_trait_notifications(): |
| 50 | + fig.fig_width = int(width) |
| 51 | + fig.fig_height = int(height) |
| 52 | + except Exception: |
| 53 | + pass |
| 54 | + |
| 55 | + |
| 56 | +def dispatch_event(fig_id: str, event_json: str) -> None: |
| 57 | + """Apply a frontend interaction event to the registered figure.""" |
| 58 | + fig = _figures.get(fig_id) |
| 59 | + if fig is None: |
| 60 | + return |
| 61 | + try: |
| 62 | + # Figure.show() registers Figure objects which use _dispatch_event(raw_json_str). |
| 63 | + # Standalone widgets use _update_from_js(dict, event_type). |
| 64 | + if hasattr(fig, "_dispatch_event"): |
| 65 | + fig._dispatch_event(event_json) |
| 66 | + elif hasattr(fig, "_update_from_js"): |
| 67 | + fig._update_from_js(json.loads(event_json)) |
| 68 | + except Exception: |
| 69 | + pass |
| 70 | + |
| 71 | + |
| 72 | +def emit(obj: dict) -> None: |
| 73 | + sys.stdout.write(f"PLOTAPP:{json.dumps(obj, default=str)}\n") |
| 74 | + sys.stdout.flush() |
0 commit comments