better-rtplot 0.4.11__tar.gz → 0.4.15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- better_rtplot-0.4.15/LICENSE +21 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/PKG-INFO +11 -4
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/README.md +8 -1
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/pyproject.toml +2 -2
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/client.py +17 -1
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/interactive_test.py +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/saved_plots/.gitignore +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/server_browser.py +118 -13
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/server_browser_gui.py +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/index.html +40 -13
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/auto-render.min.js +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/katex.min.css +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/katex.min.js +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/uPlot.iife.min.js +0 -0
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/uPlot.min.css +0 -0
- better_rtplot-0.4.11/LICENSE +0 -674
- {better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/server.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jmontp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: better-rtplot
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.15
|
|
4
4
|
Summary:
|
|
5
|
-
License:
|
|
5
|
+
License: MIT
|
|
6
6
|
Author: jmontp
|
|
7
7
|
Author-email: jmontp@umich.edu
|
|
8
8
|
Requires-Python: >=3.9,<3.13
|
|
9
|
-
Classifier: License ::
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.9
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -67,6 +67,11 @@ Sender and server don't have to be on the same machine — see the
|
|
|
67
67
|
No Python needed on the viewing machine. On Windows the binary opens
|
|
68
68
|
a small Tk status window showing the listening URL.
|
|
69
69
|
|
|
70
|
+
To arrange several plots side by side, start the server with
|
|
71
|
+
`./rtplot-server --columns 2` (or `--columns 3`). The Python server accepts
|
|
72
|
+
the same option: `python -m rtplot.server_browser --columns 2`. See the
|
|
73
|
+
[multi-column example](examples/05_multi_column/).
|
|
74
|
+
|
|
70
75
|
**Client** — pip install in the env that runs your script:
|
|
71
76
|
|
|
72
77
|
```bash
|
|
@@ -127,8 +132,10 @@ A rising line now draws itself in the browser tab.
|
|
|
127
132
|
- **Config lives with the data.** The sender declares plot layout.
|
|
128
133
|
- **Interactive controls.** Buttons, sliders, dials, displays — polled
|
|
129
134
|
from your loop, no threads, no callbacks.
|
|
135
|
+
- **LaTeX labels.** Use `$...$` or `\(...\)` in plot and control text;
|
|
136
|
+
the server bundles KaTeX and works offline.
|
|
130
137
|
- **Static HTML snapshots.** `save_snapshot("out.html")` writes a
|
|
131
|
-
self-contained
|
|
138
|
+
self-contained file with the current trace and math rendering embedded.
|
|
132
139
|
|
|
133
140
|
---
|
|
134
141
|
|
|
@@ -43,6 +43,11 @@ Sender and server don't have to be on the same machine — see the
|
|
|
43
43
|
No Python needed on the viewing machine. On Windows the binary opens
|
|
44
44
|
a small Tk status window showing the listening URL.
|
|
45
45
|
|
|
46
|
+
To arrange several plots side by side, start the server with
|
|
47
|
+
`./rtplot-server --columns 2` (or `--columns 3`). The Python server accepts
|
|
48
|
+
the same option: `python -m rtplot.server_browser --columns 2`. See the
|
|
49
|
+
[multi-column example](examples/05_multi_column/).
|
|
50
|
+
|
|
46
51
|
**Client** — pip install in the env that runs your script:
|
|
47
52
|
|
|
48
53
|
```bash
|
|
@@ -103,8 +108,10 @@ A rising line now draws itself in the browser tab.
|
|
|
103
108
|
- **Config lives with the data.** The sender declares plot layout.
|
|
104
109
|
- **Interactive controls.** Buttons, sliders, dials, displays — polled
|
|
105
110
|
from your loop, no threads, no callbacks.
|
|
111
|
+
- **LaTeX labels.** Use `$...$` or `\(...\)` in plot and control text;
|
|
112
|
+
the server bundles KaTeX and works offline.
|
|
106
113
|
- **Static HTML snapshots.** `save_snapshot("out.html")` writes a
|
|
107
|
-
self-contained
|
|
114
|
+
self-contained file with the current trace and math rendering embedded.
|
|
108
115
|
|
|
109
116
|
---
|
|
110
117
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "better-rtplot"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.15"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["jmontp <jmontp@umich.edu>"]
|
|
6
|
-
license = "
|
|
6
|
+
license = "MIT"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
packages = [{include = "rtplot"}]
|
|
9
9
|
include = ["rtplot/static/*", "rtplot/static/katex/*", "rtplot/static/katex/fonts/*"]
|
|
@@ -90,6 +90,7 @@ else:
|
|
|
90
90
|
SENDING_PLOT_UPDATE = "0"
|
|
91
91
|
SENDING_DATA = "1"
|
|
92
92
|
SENDING_DISPLAY = "4"
|
|
93
|
+
SENDING_TEXT_INPUT = "5"
|
|
93
94
|
|
|
94
95
|
#Lightweight result type returned by poll_controls()
|
|
95
96
|
ControlState = namedtuple("ControlState", ["values", "buttons"])
|
|
@@ -138,6 +139,7 @@ class Plot:
|
|
|
138
139
|
names: List[str]
|
|
139
140
|
colors: Optional[List[str]] = None
|
|
140
141
|
line_style: Optional[List[str]] = None
|
|
142
|
+
show: Optional[List[bool]] = None
|
|
141
143
|
line_width: Optional[float] = None
|
|
142
144
|
title: Optional[str] = None
|
|
143
145
|
xlabel: Optional[str] = None
|
|
@@ -151,6 +153,7 @@ class Plot:
|
|
|
151
153
|
"names": list(self.names),
|
|
152
154
|
"colors": list(self.colors) if self.colors is not None else None,
|
|
153
155
|
"line_style": list(self.line_style) if self.line_style is not None else None,
|
|
156
|
+
"show": list(self.show) if self.show is not None else None,
|
|
154
157
|
"line_width": self.line_width,
|
|
155
158
|
"title": self.title,
|
|
156
159
|
"xlabel": self.xlabel,
|
|
@@ -590,6 +593,18 @@ def set_display(display_id: str, value):
|
|
|
590
593
|
socket.send_json({"id": str(display_id), "value": payload_value})
|
|
591
594
|
|
|
592
595
|
|
|
596
|
+
def set_text_input(input_id: str, value):
|
|
597
|
+
"""Push a text_input control value to the browser and server state.
|
|
598
|
+
|
|
599
|
+
This is the editable-control companion to set_display(). It updates
|
|
600
|
+
declared 'text_input' controls without rebuilding the full plot config,
|
|
601
|
+
and the server keeps the value as the current control state returned by
|
|
602
|
+
poll_controls().
|
|
603
|
+
"""
|
|
604
|
+
socket.send_string(SENDING_TEXT_INPUT, zmq.SNDMORE)
|
|
605
|
+
socket.send_json({"id": str(input_id), "value": str(value)})
|
|
606
|
+
|
|
607
|
+
|
|
593
608
|
def poll_controls():
|
|
594
609
|
"""Drain the return channel non-blocking and return current control state.
|
|
595
610
|
|
|
@@ -644,7 +659,8 @@ def save_snapshot(path, server_url=None, animate=False, timeout=5.0):
|
|
|
644
659
|
|
|
645
660
|
The server exposes a ``/snapshot.html`` endpoint that renders the
|
|
646
661
|
current plot state as a self-contained HTML file with uPlot's JS +
|
|
647
|
-
CSS inlined plus the most recent window of trace data.
|
|
662
|
+
CSS inlined plus the most recent window of trace data. Vendored KaTeX
|
|
663
|
+
assets are also inlined, so LaTeX plot labels render offline. The result
|
|
648
664
|
opens in any browser offline and looks pixel-identical to what the
|
|
649
665
|
live tab was showing. Ideal for committing reproducible example
|
|
650
666
|
previews to a repo or emailing a static "here's what I was seeing"
|
|
File without changes
|
|
File without changes
|
|
@@ -20,7 +20,9 @@ subscribed tab's PUSH socket.
|
|
|
20
20
|
|
|
21
21
|
import argparse
|
|
22
22
|
import asyncio
|
|
23
|
+
import base64
|
|
23
24
|
import dataclasses
|
|
25
|
+
import hashlib
|
|
24
26
|
import json
|
|
25
27
|
import os
|
|
26
28
|
import socket
|
|
@@ -96,8 +98,17 @@ parser.add_argument(
|
|
|
96
98
|
parser.add_argument(
|
|
97
99
|
"-c",
|
|
98
100
|
"--column",
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
dest="columns",
|
|
102
|
+
help="Shortcut for --columns 2",
|
|
103
|
+
action="store_const",
|
|
104
|
+
const=2,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--columns",
|
|
109
|
+
type=int,
|
|
110
|
+
metavar="N",
|
|
111
|
+
help="Arrange plots in N columns (default: 1)",
|
|
101
112
|
)
|
|
102
113
|
|
|
103
114
|
parser.add_argument(
|
|
@@ -178,7 +189,9 @@ parser.add_argument(
|
|
|
178
189
|
|
|
179
190
|
args = parser.parse_args()
|
|
180
191
|
|
|
181
|
-
|
|
192
|
+
PLOT_COLUMNS = args.columns if args.columns is not None else 1
|
|
193
|
+
if PLOT_COLUMNS < 1:
|
|
194
|
+
parser.error("--columns must be at least 1")
|
|
182
195
|
DEBUG_TEXT_ENABLED = args.debug
|
|
183
196
|
SKIP_PLOT_DATAPOINTS = args.skip
|
|
184
197
|
ADAPT_SKIP_PLOT_DATAPOINTS = args.adaptable
|
|
@@ -215,6 +228,7 @@ RECEIVED_PLOT_UPDATE = 0
|
|
|
215
228
|
RECEIVED_DATA = 1
|
|
216
229
|
SAVE_PLOT = 3
|
|
217
230
|
RECEIVED_DISPLAY = 4
|
|
231
|
+
RECEIVED_TEXT_INPUT = 5
|
|
218
232
|
|
|
219
233
|
BIND_ME_ID = "bind_me"
|
|
220
234
|
BIND_ME_NAME = "Shared - Bind to me"
|
|
@@ -376,6 +390,7 @@ class Tab:
|
|
|
376
390
|
control_rows: list = field(default_factory=list)
|
|
377
391
|
slider_values: dict = field(default_factory=dict)
|
|
378
392
|
text_values: dict = field(default_factory=dict)
|
|
393
|
+
text_dirty: set = field(default_factory=set)
|
|
379
394
|
display_values: dict = field(default_factory=dict)
|
|
380
395
|
display_dirty: set = field(default_factory=set)
|
|
381
396
|
fps: float = 0.0
|
|
@@ -561,6 +576,7 @@ def parse_config(tab: Tab, json_config):
|
|
|
561
576
|
for text_id, default_value in text_values.items()
|
|
562
577
|
if text_id in active_text_ids
|
|
563
578
|
}
|
|
579
|
+
tab.text_dirty = {text_id for text_id in tab.text_dirty if text_id in active_text_ids}
|
|
564
580
|
tab.layout = layout
|
|
565
581
|
|
|
566
582
|
num_traces = sum(traces_per_plot)
|
|
@@ -602,7 +618,8 @@ def build_config_message(tab: Tab, config_dict) -> dict:
|
|
|
602
618
|
"slider_values": dict(tab.slider_values),
|
|
603
619
|
"text_values": dict(tab.text_values),
|
|
604
620
|
"display_values": dict(tab.display_values),
|
|
605
|
-
"
|
|
621
|
+
"columns": PLOT_COLUMNS,
|
|
622
|
+
"row_layout": PLOT_COLUMNS == 1,
|
|
606
623
|
}
|
|
607
624
|
|
|
608
625
|
|
|
@@ -1056,6 +1073,20 @@ async def zmq_receiver(tab: Tab):
|
|
|
1056
1073
|
tab.display_values[display_id] = value
|
|
1057
1074
|
tab.display_dirty.add(display_id)
|
|
1058
1075
|
|
|
1076
|
+
elif category == RECEIVED_TEXT_INPUT:
|
|
1077
|
+
payload = await sock.recv_json()
|
|
1078
|
+
text_id = payload.get("id")
|
|
1079
|
+
if text_id is None:
|
|
1080
|
+
continue
|
|
1081
|
+
value = str(payload.get("value", ""))
|
|
1082
|
+
if tab.text_values.get(text_id) != value:
|
|
1083
|
+
tab.text_values[text_id] = value
|
|
1084
|
+
tab.text_dirty.add(text_id)
|
|
1085
|
+
refresh_config_message(tab)
|
|
1086
|
+
await send_control_event(
|
|
1087
|
+
tab, {"type": "text", "id": text_id, "value": value}
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1059
1090
|
|
|
1060
1091
|
###############################
|
|
1061
1092
|
# Pusher tasks #
|
|
@@ -1078,6 +1109,23 @@ async def display_pusher():
|
|
|
1078
1109
|
)
|
|
1079
1110
|
|
|
1080
1111
|
|
|
1112
|
+
async def text_input_pusher():
|
|
1113
|
+
"""Push dirty text_input values to viewers at ~30 Hz, per tab."""
|
|
1114
|
+
target_dt = 1.0 / 30.0
|
|
1115
|
+
while True:
|
|
1116
|
+
await asyncio.sleep(target_dt)
|
|
1117
|
+
if not ws_clients:
|
|
1118
|
+
continue
|
|
1119
|
+
for t in list(tabs.values()):
|
|
1120
|
+
if not t.text_dirty:
|
|
1121
|
+
continue
|
|
1122
|
+
values = {k: t.text_values[k] for k in t.text_dirty}
|
|
1123
|
+
t.text_dirty = set()
|
|
1124
|
+
await broadcast_text_tab(
|
|
1125
|
+
t.id, {"type": "text_update", "tab": t.id, "values": values}
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
|
|
1081
1129
|
async def ws_pusher():
|
|
1082
1130
|
"""Push new samples to viewers as binary delta frames, per tab."""
|
|
1083
1131
|
target_dt = 1.0 / max(1, args.rate)
|
|
@@ -1324,8 +1372,21 @@ def _read_static_asset(name: str) -> str:
|
|
|
1324
1372
|
return fh.read()
|
|
1325
1373
|
|
|
1326
1374
|
|
|
1327
|
-
# Loaded once at import time so per-request cost is just the send.
|
|
1375
|
+
# Loaded once at import time so per-request cost is just the send. Version the
|
|
1376
|
+
# asset URLs by their contents: browsers can otherwise keep an old or damaged
|
|
1377
|
+
# KaTeX script after the executable is updated while fetching the new HTML.
|
|
1328
1378
|
_INDEX_HTML = _read_static_asset("index.html")
|
|
1379
|
+
for _asset in (
|
|
1380
|
+
"uPlot.min.css",
|
|
1381
|
+
"uPlot.iife.min.js",
|
|
1382
|
+
"katex/katex.min.css",
|
|
1383
|
+
"katex/katex.min.js",
|
|
1384
|
+
"katex/auto-render.min.js",
|
|
1385
|
+
):
|
|
1386
|
+
_digest = hashlib.sha256(_read_static_asset(_asset).encode("utf-8")).hexdigest()[:12]
|
|
1387
|
+
_INDEX_HTML = _INDEX_HTML.replace(
|
|
1388
|
+
f'"/static/{_asset}"', f'"/static/{_asset}?v={_digest}"'
|
|
1389
|
+
)
|
|
1329
1390
|
|
|
1330
1391
|
|
|
1331
1392
|
async def handle_index(request):
|
|
@@ -1464,12 +1525,13 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1464
1525
|
<meta charset="utf-8" />
|
|
1465
1526
|
<title>rtplot snapshot</title>
|
|
1466
1527
|
<style>__UPLOT_CSS__</style>
|
|
1528
|
+
<style>__KATEX_CSS__</style>
|
|
1467
1529
|
<style>
|
|
1468
1530
|
html, body { margin: 0; padding: 0; background: #fafafa; color: #222;
|
|
1469
1531
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
|
1470
|
-
#plots { display:
|
|
1471
|
-
max-width: 900px; margin: 0 auto; padding: 20px 16px; }
|
|
1472
|
-
.plot-wrap { background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 8px; }
|
|
1532
|
+
#plots { display: grid; grid-template-columns: repeat(var(--plot-columns, 1), minmax(320px, 1fr));
|
|
1533
|
+
gap: 12px; overflow-x: auto; max-width: 900px; margin: 0 auto; padding: 20px 16px; }
|
|
1534
|
+
.plot-wrap { background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 8px; min-width: 0; }
|
|
1473
1535
|
.snap-footer { text-align: center; font-size: 12px; color: #999; padding: 8px 0 20px; }
|
|
1474
1536
|
.snap-footer a { color: #2a5db0; text-decoration: none; }
|
|
1475
1537
|
.snap-footer a:hover { text-decoration: underline; }
|
|
@@ -1483,10 +1545,14 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1483
1545
|
· drag to zoom, double-click to reset
|
|
1484
1546
|
</p>
|
|
1485
1547
|
<script>__UPLOT_JS__</script>
|
|
1548
|
+
<script>__KATEX_JS__</script>
|
|
1549
|
+
<script>__KATEX_AUTO_RENDER_JS__</script>
|
|
1486
1550
|
<script>
|
|
1487
1551
|
(function () {
|
|
1488
1552
|
const SNAP = __SNAPSHOT_JSON__;
|
|
1489
1553
|
const plotsDiv = document.getElementById('plots');
|
|
1554
|
+
plotsDiv.style.setProperty('--plot-columns', SNAP.columns || 1);
|
|
1555
|
+
plotsDiv.style.maxWidth = `${900 * (SNAP.columns || 1)}px`;
|
|
1490
1556
|
const COLOR_MAP = { r:'rgb(255,0,0)', g:'rgb(0,200,0)', b:'rgb(0,0,255)',
|
|
1491
1557
|
c:'rgb(0,200,200)', m:'rgb(200,0,200)',
|
|
1492
1558
|
y:'rgb(200,200,0)', k:'rgb(0,0,0)' };
|
|
@@ -1526,7 +1592,7 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1526
1592
|
data.push(Float64Array.from(src));
|
|
1527
1593
|
}
|
|
1528
1594
|
const opts = {
|
|
1529
|
-
width: Math.max(
|
|
1595
|
+
width: Math.max(240, wrap.clientWidth - 16),
|
|
1530
1596
|
height: 260,
|
|
1531
1597
|
title: pcfg.title || '',
|
|
1532
1598
|
scales: {
|
|
@@ -1539,10 +1605,19 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1539
1605
|
cursor: { drag: { x: true, y: false } },
|
|
1540
1606
|
};
|
|
1541
1607
|
const u = new uPlot(opts, data, wrap);
|
|
1542
|
-
plots.push({ uplot: u, data: data, xs: xs, xrange: xrange,
|
|
1608
|
+
plots.push({ uplot: u, wrap: wrap, data: data, xs: xs, xrange: xrange,
|
|
1543
1609
|
traceCount: traceCount, startIdx: traceOffset });
|
|
1544
1610
|
traceOffset += traceCount;
|
|
1545
1611
|
});
|
|
1612
|
+
renderMathInElement(plotsDiv, {
|
|
1613
|
+
delimiters: [
|
|
1614
|
+
{ left: '$$', right: '$$', display: true },
|
|
1615
|
+
{ left: '\\\\[', right: '\\\\]', display: true },
|
|
1616
|
+
{ left: '\\\\(', right: '\\\\)', display: false },
|
|
1617
|
+
{ left: '$', right: '$', display: false },
|
|
1618
|
+
],
|
|
1619
|
+
throwOnError: false,
|
|
1620
|
+
});
|
|
1546
1621
|
if (SNAP.animate) {
|
|
1547
1622
|
let phase = 0;
|
|
1548
1623
|
setInterval(function () {
|
|
@@ -1564,7 +1639,7 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1564
1639
|
window.addEventListener('resize', function () {
|
|
1565
1640
|
plots.forEach(function (p) {
|
|
1566
1641
|
p.uplot.setSize({
|
|
1567
|
-
width: Math.max(
|
|
1642
|
+
width: Math.max(240, p.wrap.clientWidth - 16),
|
|
1568
1643
|
height: 260,
|
|
1569
1644
|
});
|
|
1570
1645
|
});
|
|
@@ -1579,9 +1654,31 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1579
1654
|
try:
|
|
1580
1655
|
_UPLOT_JS = _read_static_asset("uPlot.iife.min.js")
|
|
1581
1656
|
_UPLOT_CSS = _read_static_asset("uPlot.min.css")
|
|
1657
|
+
_KATEX_JS = _read_static_asset(os.path.join("katex", "katex.min.js"))
|
|
1658
|
+
_KATEX_AUTO_RENDER_JS = _read_static_asset(
|
|
1659
|
+
os.path.join("katex", "auto-render.min.js")
|
|
1660
|
+
)
|
|
1661
|
+
_KATEX_CSS = _read_static_asset(os.path.join("katex", "katex.min.css"))
|
|
1662
|
+
# Snapshot files have no sibling font directory. Inline the vendored
|
|
1663
|
+
# WOFF2 faces so equations keep their layout when opened offline.
|
|
1664
|
+
_KATEX_FONT_DIR = os.path.join(
|
|
1665
|
+
os.path.dirname(os.path.abspath(__file__)), "static", "katex", "fonts"
|
|
1666
|
+
)
|
|
1667
|
+
for _font_name in os.listdir(_KATEX_FONT_DIR):
|
|
1668
|
+
if not _font_name.endswith(".woff2"):
|
|
1669
|
+
continue
|
|
1670
|
+
with open(os.path.join(_KATEX_FONT_DIR, _font_name), "rb") as _font_file:
|
|
1671
|
+
_font_data = base64.b64encode(_font_file.read()).decode("ascii")
|
|
1672
|
+
_KATEX_CSS = _KATEX_CSS.replace(
|
|
1673
|
+
f"url(fonts/{_font_name})",
|
|
1674
|
+
f"url(data:font/woff2;base64,{_font_data})",
|
|
1675
|
+
)
|
|
1582
1676
|
except Exception as _exc: # noqa: BLE001
|
|
1583
1677
|
_UPLOT_JS = f"/* uPlot load failed: {_exc} */"
|
|
1584
1678
|
_UPLOT_CSS = ""
|
|
1679
|
+
_KATEX_JS = f"/* KaTeX load failed: {_exc} */"
|
|
1680
|
+
_KATEX_AUTO_RENDER_JS = ""
|
|
1681
|
+
_KATEX_CSS = ""
|
|
1585
1682
|
|
|
1586
1683
|
|
|
1587
1684
|
def _build_snapshot_html(tab: Tab, animate: bool) -> str:
|
|
@@ -1624,6 +1721,7 @@ def _build_snapshot_html(tab: Tab, animate: bool) -> str:
|
|
|
1624
1721
|
|
|
1625
1722
|
payload = {
|
|
1626
1723
|
"plots": plots,
|
|
1724
|
+
"columns": PLOT_COLUMNS,
|
|
1627
1725
|
"num_samples": int(hi - lo),
|
|
1628
1726
|
"trace_data": trace_data,
|
|
1629
1727
|
"animate": bool(animate),
|
|
@@ -1631,7 +1729,13 @@ def _build_snapshot_html(tab: Tab, animate: bool) -> str:
|
|
|
1631
1729
|
html = _SNAPSHOT_TEMPLATE
|
|
1632
1730
|
html = html.replace("__UPLOT_CSS__", _UPLOT_CSS)
|
|
1633
1731
|
html = html.replace("__UPLOT_JS__", _UPLOT_JS)
|
|
1634
|
-
html = html.replace("
|
|
1732
|
+
html = html.replace("__KATEX_CSS__", _KATEX_CSS)
|
|
1733
|
+
html = html.replace("__KATEX_JS__", _KATEX_JS)
|
|
1734
|
+
html = html.replace("__KATEX_AUTO_RENDER_JS__", _KATEX_AUTO_RENDER_JS)
|
|
1735
|
+
# Keep client-supplied labels from closing the script element while
|
|
1736
|
+
# preserving their exact value when JavaScript parses the payload.
|
|
1737
|
+
snapshot_json = json.dumps(payload).replace("<", "\\u003c")
|
|
1738
|
+
html = html.replace("__SNAPSHOT_JSON__", snapshot_json)
|
|
1635
1739
|
return html
|
|
1636
1740
|
|
|
1637
1741
|
|
|
@@ -1682,11 +1786,12 @@ async def on_startup(app):
|
|
|
1682
1786
|
|
|
1683
1787
|
app["ws_task"] = asyncio.create_task(ws_pusher())
|
|
1684
1788
|
app["display_task"] = asyncio.create_task(display_pusher())
|
|
1789
|
+
app["text_input_task"] = asyncio.create_task(text_input_pusher())
|
|
1685
1790
|
app["resources_task"] = asyncio.create_task(resources_pusher())
|
|
1686
1791
|
|
|
1687
1792
|
|
|
1688
1793
|
async def on_cleanup(app):
|
|
1689
|
-
for key in ("ws_task", "display_task", "resources_task"):
|
|
1794
|
+
for key in ("ws_task", "display_task", "text_input_task", "resources_task"):
|
|
1690
1795
|
await _cancel_task(app.get(key))
|
|
1691
1796
|
for t in list(tabs.values()):
|
|
1692
1797
|
await _cancel_task(t.receiver_task)
|
|
File without changes
|
|
@@ -62,10 +62,9 @@
|
|
|
62
62
|
#ws-status { font-size: calc(12px * var(--ui-scale)); color: #666; margin-left: auto; }
|
|
63
63
|
|
|
64
64
|
/* -------- Plots + controls (existing) -------- */
|
|
65
|
-
#plots { display:
|
|
66
|
-
|
|
67
|
-
#plots.
|
|
68
|
-
.plot-wrap { background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 8px; flex: 1 1 auto; min-width: 320px; }
|
|
65
|
+
#plots { display: grid; grid-template-columns: repeat(var(--plot-columns, 1), minmax(320px, 1fr)); padding: 12px; gap: 12px; overflow-x: auto; }
|
|
66
|
+
.plot-wrap { background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 8px; min-width: 0; }
|
|
67
|
+
#plots .ctrl-row, #plots #empty-state { grid-column: 1 / -1; }
|
|
69
68
|
.plot-title { font-size: 13px; font-weight: 600; margin: 0 0 4px 4px; color: #333; }
|
|
70
69
|
#empty-state { padding: 28px 32px; color: #888; font-size: calc(14px * var(--ui-scale)); font-style: italic; }
|
|
71
70
|
/* Persistent banner the server raises when a client-supplied plot
|
|
@@ -265,6 +264,8 @@
|
|
|
265
264
|
// a typo in someone's label can't blow up the whole page.
|
|
266
265
|
const _latexDelims = [
|
|
267
266
|
{ left: '$$', right: '$$', display: true },
|
|
267
|
+
{ left: '\\[', right: '\\]', display: true },
|
|
268
|
+
{ left: '\\(', right: '\\)', display: false },
|
|
268
269
|
{ left: '$', right: '$', display: false },
|
|
269
270
|
];
|
|
270
271
|
function renderLatexIn(el) {
|
|
@@ -301,6 +302,26 @@
|
|
|
301
302
|
const resTabRates = document.getElementById('res-tab-rates');
|
|
302
303
|
const resUnavail = document.getElementById('res-unavail');
|
|
303
304
|
|
|
305
|
+
// A failed KaTeX download can still leave auto-render defined, but its
|
|
306
|
+
// closure then holds an undefined KaTeX reference. Retry both scripts
|
|
307
|
+
// in order with fresh URLs and render labels already on the page.
|
|
308
|
+
if (typeof katex === 'undefined') {
|
|
309
|
+
const retry = Date.now();
|
|
310
|
+
const core = document.createElement('script');
|
|
311
|
+
core.src = `/static/katex/katex.min.js?retry=${retry}`;
|
|
312
|
+
core.onload = () => {
|
|
313
|
+
if (typeof katex === 'undefined') return;
|
|
314
|
+
const autoRender = document.createElement('script');
|
|
315
|
+
autoRender.src = `/static/katex/auto-render.min.js?retry=${retry}`;
|
|
316
|
+
autoRender.onload = () => {
|
|
317
|
+
renderLatexIn(plotsDiv);
|
|
318
|
+
tabbar.querySelectorAll('.tab-name').forEach(renderLatexIn);
|
|
319
|
+
};
|
|
320
|
+
document.head.appendChild(autoRender);
|
|
321
|
+
};
|
|
322
|
+
document.head.appendChild(core);
|
|
323
|
+
}
|
|
324
|
+
|
|
304
325
|
// ---- Detect the monitor's refresh rate on page load ----
|
|
305
326
|
let monitorHz = 0;
|
|
306
327
|
(function measureMonitorHz() {
|
|
@@ -810,6 +831,7 @@
|
|
|
810
831
|
const colors = pcfg.colors || DEFAULT_COLORS;
|
|
811
832
|
const widths = pcfg.line_width || [];
|
|
812
833
|
const styles = pcfg.line_style || [];
|
|
834
|
+
const shows = pcfg.show || [];
|
|
813
835
|
|
|
814
836
|
const series = [{}];
|
|
815
837
|
for (let t = 0; t < traceCount; t++) {
|
|
@@ -821,6 +843,7 @@
|
|
|
821
843
|
dash: dash || undefined,
|
|
822
844
|
points: { show: false },
|
|
823
845
|
spanGaps: true,
|
|
846
|
+
show: shows[t] !== false,
|
|
824
847
|
});
|
|
825
848
|
}
|
|
826
849
|
|
|
@@ -829,8 +852,12 @@
|
|
|
829
852
|
const plotHeight = (Number.isFinite(heightMul) && heightMul > 0)
|
|
830
853
|
? Math.round(baseHeight * heightMul)
|
|
831
854
|
: baseHeight;
|
|
855
|
+
const wrap = document.createElement('div');
|
|
856
|
+
wrap.className = 'plot-wrap';
|
|
857
|
+
plotsDiv.appendChild(wrap);
|
|
858
|
+
|
|
832
859
|
const opts = {
|
|
833
|
-
width: Math.max(
|
|
860
|
+
width: Math.max(240, wrap.clientWidth - 16),
|
|
834
861
|
height: plotHeight,
|
|
835
862
|
title: pcfg.title || '',
|
|
836
863
|
scales: {
|
|
@@ -846,10 +873,6 @@
|
|
|
846
873
|
cursor: { drag: { x: false, y: false } },
|
|
847
874
|
};
|
|
848
875
|
|
|
849
|
-
const wrap = document.createElement('div');
|
|
850
|
-
wrap.className = 'plot-wrap';
|
|
851
|
-
plotsDiv.appendChild(wrap);
|
|
852
|
-
|
|
853
876
|
const buffers = [];
|
|
854
877
|
for (let t = 0; t < traceCount; t++) {
|
|
855
878
|
const buf = new Float32Array(xrange);
|
|
@@ -863,6 +886,7 @@
|
|
|
863
886
|
const u = new uPlot(opts, initialData, wrap);
|
|
864
887
|
plots.push({
|
|
865
888
|
uplot: u,
|
|
889
|
+
wrap: wrap,
|
|
866
890
|
xs: xs,
|
|
867
891
|
traceCount: traceCount,
|
|
868
892
|
startIdx: traceOffset,
|
|
@@ -875,8 +899,8 @@
|
|
|
875
899
|
|
|
876
900
|
function buildPlots(cfg) {
|
|
877
901
|
destroyPlots();
|
|
878
|
-
|
|
879
|
-
plotsDiv.
|
|
902
|
+
const columns = Number(cfg.columns) || (cfg.row_layout === false ? 2 : 1);
|
|
903
|
+
plotsDiv.style.setProperty('--plot-columns', Math.max(1, Math.floor(columns)));
|
|
880
904
|
|
|
881
905
|
const controls = cfg.controls || [];
|
|
882
906
|
const layout = (cfg.layout && cfg.layout.length)
|
|
@@ -889,7 +913,7 @@
|
|
|
889
913
|
if (entry.kind === 'plot') {
|
|
890
914
|
const pcfg = cfg.plots[entry.index];
|
|
891
915
|
if (!pcfg) return;
|
|
892
|
-
traceOffset += buildOnePlot(pcfg,
|
|
916
|
+
traceOffset += buildOnePlot(pcfg, columns === 1, traceOffset);
|
|
893
917
|
} else if (entry.kind === 'controls') {
|
|
894
918
|
const row = controls[entry.index];
|
|
895
919
|
if (!row) return;
|
|
@@ -1320,6 +1344,9 @@
|
|
|
1320
1344
|
} else if (msg.type === 'display_update') {
|
|
1321
1345
|
if (msg.tab && msg.tab !== activeTab) return;
|
|
1322
1346
|
applyDisplayValues(msg.values);
|
|
1347
|
+
} else if (msg.type === 'text_update') {
|
|
1348
|
+
if (msg.tab && msg.tab !== activeTab) return;
|
|
1349
|
+
applyTextValues(msg.values);
|
|
1323
1350
|
} else if (msg.type === 'peer_count') {
|
|
1324
1351
|
const n = Number(msg.count) || 0;
|
|
1325
1352
|
if (lastStatus.peers !== n) {
|
|
@@ -1339,7 +1366,7 @@
|
|
|
1339
1366
|
window.addEventListener('resize', () => {
|
|
1340
1367
|
plots.forEach(p => {
|
|
1341
1368
|
p.uplot.setSize({
|
|
1342
|
-
width: Math.max(
|
|
1369
|
+
width: Math.max(240, p.wrap.clientWidth - 16),
|
|
1343
1370
|
height: p.height,
|
|
1344
1371
|
});
|
|
1345
1372
|
});
|
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2
RENAMED
|
File without changes
|
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2
RENAMED
|
File without changes
|
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.11 → better_rtplot-0.4.15}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|