better-rtplot 0.4.10__tar.gz → 0.4.14__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.14/LICENSE +21 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/PKG-INFO +6 -4
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/README.md +3 -1
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/pyproject.toml +2 -2
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/client.py +47 -10
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/interactive_test.py +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/saved_plots/.gitignore +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/server_browser.py +135 -4
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/server_browser_gui.py +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/index.html +68 -1
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/auto-render.min.js +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/katex.min.css +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/katex.min.js +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/uPlot.iife.min.js +0 -0
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/uPlot.min.css +0 -0
- better_rtplot-0.4.10/LICENSE +0 -674
- {better_rtplot-0.4.10 → better_rtplot-0.4.14}/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.14
|
|
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
|
|
@@ -127,8 +127,10 @@ A rising line now draws itself in the browser tab.
|
|
|
127
127
|
- **Config lives with the data.** The sender declares plot layout.
|
|
128
128
|
- **Interactive controls.** Buttons, sliders, dials, displays — polled
|
|
129
129
|
from your loop, no threads, no callbacks.
|
|
130
|
+
- **LaTeX labels.** Use `$...$` or `\(...\)` in plot and control text;
|
|
131
|
+
the server bundles KaTeX and works offline.
|
|
130
132
|
- **Static HTML snapshots.** `save_snapshot("out.html")` writes a
|
|
131
|
-
self-contained
|
|
133
|
+
self-contained file with the current trace and math rendering embedded.
|
|
132
134
|
|
|
133
135
|
---
|
|
134
136
|
|
|
@@ -103,8 +103,10 @@ A rising line now draws itself in the browser tab.
|
|
|
103
103
|
- **Config lives with the data.** The sender declares plot layout.
|
|
104
104
|
- **Interactive controls.** Buttons, sliders, dials, displays — polled
|
|
105
105
|
from your loop, no threads, no callbacks.
|
|
106
|
+
- **LaTeX labels.** Use `$...$` or `\(...\)` in plot and control text;
|
|
107
|
+
the server bundles KaTeX and works offline.
|
|
106
108
|
- **Static HTML snapshots.** `save_snapshot("out.html")` writes a
|
|
107
|
-
self-contained
|
|
109
|
+
self-contained file with the current trace and math rendering embedded.
|
|
108
110
|
|
|
109
111
|
---
|
|
110
112
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "better-rtplot"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.14"
|
|
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/*"]
|
|
@@ -22,7 +22,7 @@ socket = context.socket(zmq.PUB)
|
|
|
22
22
|
control_socket = context.socket(zmq.PULL)
|
|
23
23
|
control_socket.setsockopt(zmq.RCVHWM, 1000)
|
|
24
24
|
#Non-blocking local state for controls, drained on each poll_controls() call
|
|
25
|
-
|
|
25
|
+
_control_values = {}
|
|
26
26
|
_control_button_events = []
|
|
27
27
|
|
|
28
28
|
# Cached payload of the most recent initialize_plots() call. The server
|
|
@@ -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,
|
|
@@ -247,10 +250,27 @@ class Text:
|
|
|
247
250
|
})
|
|
248
251
|
|
|
249
252
|
|
|
253
|
+
@dataclass
|
|
254
|
+
class TextInput:
|
|
255
|
+
id: str
|
|
256
|
+
label: str
|
|
257
|
+
value: str = ""
|
|
258
|
+
placeholder: Optional[str] = None
|
|
259
|
+
max_length: Optional[int] = None
|
|
260
|
+
height: Optional[float] = None
|
|
261
|
+
|
|
262
|
+
def to_dict(self):
|
|
263
|
+
return _drop_none({
|
|
264
|
+
"type": "text_input", "id": self.id, "label": self.label,
|
|
265
|
+
"value": self.value, "placeholder": self.placeholder,
|
|
266
|
+
"max_length": self.max_length, "height": self.height,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
|
|
250
270
|
@dataclass
|
|
251
271
|
class ControlsRow:
|
|
252
272
|
"""A row of control widgets, rendered in place of a plot."""
|
|
253
|
-
controls: List[Union[Button, Slider, Dial, Display, Text, dict]] = field(default_factory=list)
|
|
273
|
+
controls: List[Union[Button, Slider, Dial, Display, Text, TextInput, dict]] = field(default_factory=list)
|
|
254
274
|
|
|
255
275
|
def to_dict(self):
|
|
256
276
|
return {"controls": [
|
|
@@ -536,7 +556,9 @@ def _send_initialize_with_handshake(cfg, timeout):
|
|
|
536
556
|
elif evtype == "button":
|
|
537
557
|
_control_button_events.append(event.get("id"))
|
|
538
558
|
elif evtype == "slider":
|
|
539
|
-
|
|
559
|
+
_control_values[event.get("id")] = float(event.get("value", 0.0))
|
|
560
|
+
elif evtype == "text":
|
|
561
|
+
_control_values[event.get("id")] = str(event.get("value", ""))
|
|
540
562
|
# Unknown / resend_config: ignore during handshake.
|
|
541
563
|
# Resend if the window elapsed without an ack; PUB/SUB will
|
|
542
564
|
# drop the first send during slow-join but retries land.
|
|
@@ -571,12 +593,24 @@ def set_display(display_id: str, value):
|
|
|
571
593
|
socket.send_json({"id": str(display_id), "value": payload_value})
|
|
572
594
|
|
|
573
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
|
+
|
|
574
608
|
def poll_controls():
|
|
575
609
|
"""Drain the return channel non-blocking and return current control state.
|
|
576
610
|
|
|
577
611
|
Returns a ControlState(values, buttons) where:
|
|
578
|
-
- values: dict of
|
|
579
|
-
|
|
612
|
+
- values: dict of latest control values keyed by id. Sliders/dials
|
|
613
|
+
remain floats; text_input controls remain strings.
|
|
580
614
|
- buttons: list of button ids that fired since the previous poll
|
|
581
615
|
(cleared after this call).
|
|
582
616
|
|
|
@@ -587,7 +621,7 @@ def poll_controls():
|
|
|
587
621
|
Reconnect button on a tab), it asks us to re-publish the cached
|
|
588
622
|
initialize_plots() payload so the plot rewires without the user
|
|
589
623
|
restarting their script. The request is consumed silently — the
|
|
590
|
-
returned ControlState only ever contains
|
|
624
|
+
returned ControlState only ever contains control/button events.
|
|
591
625
|
"""
|
|
592
626
|
global _control_button_events
|
|
593
627
|
while True:
|
|
@@ -601,7 +635,9 @@ def poll_controls():
|
|
|
601
635
|
if evtype == "button":
|
|
602
636
|
_control_button_events.append(event.get("id"))
|
|
603
637
|
elif evtype == "slider":
|
|
604
|
-
|
|
638
|
+
_control_values[event.get("id")] = float(event.get("value", 0.0))
|
|
639
|
+
elif evtype == "text":
|
|
640
|
+
_control_values[event.get("id")] = str(event.get("value", ""))
|
|
605
641
|
elif evtype == "resend_config":
|
|
606
642
|
if plot_desc_dict is not None:
|
|
607
643
|
socket.send_string(SENDING_PLOT_UPDATE)
|
|
@@ -615,7 +651,7 @@ def poll_controls():
|
|
|
615
651
|
|
|
616
652
|
buttons = _control_button_events
|
|
617
653
|
_control_button_events = []
|
|
618
|
-
return ControlState(values=dict(
|
|
654
|
+
return ControlState(values=dict(_control_values), buttons=buttons)
|
|
619
655
|
|
|
620
656
|
|
|
621
657
|
def save_snapshot(path, server_url=None, animate=False, timeout=5.0):
|
|
@@ -623,7 +659,8 @@ def save_snapshot(path, server_url=None, animate=False, timeout=5.0):
|
|
|
623
659
|
|
|
624
660
|
The server exposes a ``/snapshot.html`` endpoint that renders the
|
|
625
661
|
current plot state as a self-contained HTML file with uPlot's JS +
|
|
626
|
-
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
|
|
627
664
|
opens in any browser offline and looks pixel-identical to what the
|
|
628
665
|
live tab was showing. Ideal for committing reproducible example
|
|
629
666
|
previews to a repo or emailing a static "here's what I was seeing"
|
|
@@ -664,4 +701,4 @@ def save_snapshot(path, server_url=None, animate=False, timeout=5.0):
|
|
|
664
701
|
abs_path = _os.path.abspath(path)
|
|
665
702
|
with open(abs_path, "wb") as fh:
|
|
666
703
|
fh.write(body)
|
|
667
|
-
return abs_path
|
|
704
|
+
return abs_path
|
|
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
|
|
@@ -215,6 +217,7 @@ RECEIVED_PLOT_UPDATE = 0
|
|
|
215
217
|
RECEIVED_DATA = 1
|
|
216
218
|
SAVE_PLOT = 3
|
|
217
219
|
RECEIVED_DISPLAY = 4
|
|
220
|
+
RECEIVED_TEXT_INPUT = 5
|
|
218
221
|
|
|
219
222
|
BIND_ME_ID = "bind_me"
|
|
220
223
|
BIND_ME_NAME = "Shared - Bind to me"
|
|
@@ -375,6 +378,8 @@ class Tab:
|
|
|
375
378
|
layout: list = field(default_factory=list)
|
|
376
379
|
control_rows: list = field(default_factory=list)
|
|
377
380
|
slider_values: dict = field(default_factory=dict)
|
|
381
|
+
text_values: dict = field(default_factory=dict)
|
|
382
|
+
text_dirty: set = field(default_factory=set)
|
|
378
383
|
display_values: dict = field(default_factory=dict)
|
|
379
384
|
display_dirty: set = field(default_factory=set)
|
|
380
385
|
fps: float = 0.0
|
|
@@ -499,6 +504,7 @@ def parse_config(tab: Tab, json_config):
|
|
|
499
504
|
trace_info = []
|
|
500
505
|
control_rows = []
|
|
501
506
|
slider_values = {}
|
|
507
|
+
text_values = {}
|
|
502
508
|
layout = []
|
|
503
509
|
num_datapoints_in_plot = DEFAULT_NUM_DATAPOINTS_IN_PLOT
|
|
504
510
|
|
|
@@ -514,6 +520,8 @@ def parse_config(tab: Tab, json_config):
|
|
|
514
520
|
for element in row:
|
|
515
521
|
if element.get("type") in ("slider", "dial") and "value" in element:
|
|
516
522
|
slider_values[element["id"]] = float(element["value"])
|
|
523
|
+
elif element.get("type") == "text_input":
|
|
524
|
+
text_values[element["id"]] = str(element.get("value", ""))
|
|
517
525
|
layout.append({"kind": "controls", "index": len(control_rows)})
|
|
518
526
|
control_rows.append(row)
|
|
519
527
|
continue
|
|
@@ -546,6 +554,18 @@ def parse_config(tab: Tab, json_config):
|
|
|
546
554
|
}
|
|
547
555
|
tab.display_dirty = {d for d in tab.display_dirty if d in active_display_ids}
|
|
548
556
|
tab.slider_values = slider_values
|
|
557
|
+
active_text_ids = {
|
|
558
|
+
el["id"]
|
|
559
|
+
for row in control_rows
|
|
560
|
+
for el in row
|
|
561
|
+
if el.get("type") == "text_input"
|
|
562
|
+
}
|
|
563
|
+
tab.text_values = {
|
|
564
|
+
text_id: tab.text_values.get(text_id, default_value)
|
|
565
|
+
for text_id, default_value in text_values.items()
|
|
566
|
+
if text_id in active_text_ids
|
|
567
|
+
}
|
|
568
|
+
tab.text_dirty = {text_id for text_id in tab.text_dirty if text_id in active_text_ids}
|
|
549
569
|
tab.layout = layout
|
|
550
570
|
|
|
551
571
|
num_traces = sum(traces_per_plot)
|
|
@@ -585,11 +605,20 @@ def build_config_message(tab: Tab, config_dict) -> dict:
|
|
|
585
605
|
"controls": tab.control_rows,
|
|
586
606
|
"layout": tab.layout,
|
|
587
607
|
"slider_values": dict(tab.slider_values),
|
|
608
|
+
"text_values": dict(tab.text_values),
|
|
588
609
|
"display_values": dict(tab.display_values),
|
|
589
610
|
"row_layout": bool(NEW_SUBPLOT_IN_ROW),
|
|
590
611
|
}
|
|
591
612
|
|
|
592
613
|
|
|
614
|
+
def refresh_config_message(tab: Tab):
|
|
615
|
+
"""Rebuild the browser config payload using the tab's latest control state."""
|
|
616
|
+
if tab.config_dict is None:
|
|
617
|
+
tab.config_message = None
|
|
618
|
+
else:
|
|
619
|
+
tab.config_message = build_config_message(tab, tab.config_dict)
|
|
620
|
+
|
|
621
|
+
|
|
593
622
|
###############################
|
|
594
623
|
# Binary payload construction #
|
|
595
624
|
###############################
|
|
@@ -952,7 +981,7 @@ async def zmq_receiver(tab: Tab):
|
|
|
952
981
|
|
|
953
982
|
tab.last_config_error = None # clear: this one was good
|
|
954
983
|
tab.config_dict = cfg
|
|
955
|
-
|
|
984
|
+
refresh_config_message(tab)
|
|
956
985
|
tab.initialized = True
|
|
957
986
|
fps = None
|
|
958
987
|
tab.fps = 0.0
|
|
@@ -976,6 +1005,10 @@ async def zmq_receiver(tab: Tab):
|
|
|
976
1005
|
await send_control_event(
|
|
977
1006
|
tab, {"type": "slider", "id": sid, "value": svalue}
|
|
978
1007
|
)
|
|
1008
|
+
for text_id, text_value in tab.text_values.items():
|
|
1009
|
+
await send_control_event(
|
|
1010
|
+
tab, {"type": "text", "id": text_id, "value": text_value}
|
|
1011
|
+
)
|
|
979
1012
|
|
|
980
1013
|
elif category == RECEIVED_DATA:
|
|
981
1014
|
arr = await _recv_array_async(sock)
|
|
@@ -1028,6 +1061,20 @@ async def zmq_receiver(tab: Tab):
|
|
|
1028
1061
|
tab.display_values[display_id] = value
|
|
1029
1062
|
tab.display_dirty.add(display_id)
|
|
1030
1063
|
|
|
1064
|
+
elif category == RECEIVED_TEXT_INPUT:
|
|
1065
|
+
payload = await sock.recv_json()
|
|
1066
|
+
text_id = payload.get("id")
|
|
1067
|
+
if text_id is None:
|
|
1068
|
+
continue
|
|
1069
|
+
value = str(payload.get("value", ""))
|
|
1070
|
+
if tab.text_values.get(text_id) != value:
|
|
1071
|
+
tab.text_values[text_id] = value
|
|
1072
|
+
tab.text_dirty.add(text_id)
|
|
1073
|
+
refresh_config_message(tab)
|
|
1074
|
+
await send_control_event(
|
|
1075
|
+
tab, {"type": "text", "id": text_id, "value": value}
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1031
1078
|
|
|
1032
1079
|
###############################
|
|
1033
1080
|
# Pusher tasks #
|
|
@@ -1050,6 +1097,23 @@ async def display_pusher():
|
|
|
1050
1097
|
)
|
|
1051
1098
|
|
|
1052
1099
|
|
|
1100
|
+
async def text_input_pusher():
|
|
1101
|
+
"""Push dirty text_input values to viewers at ~30 Hz, per tab."""
|
|
1102
|
+
target_dt = 1.0 / 30.0
|
|
1103
|
+
while True:
|
|
1104
|
+
await asyncio.sleep(target_dt)
|
|
1105
|
+
if not ws_clients:
|
|
1106
|
+
continue
|
|
1107
|
+
for t in list(tabs.values()):
|
|
1108
|
+
if not t.text_dirty:
|
|
1109
|
+
continue
|
|
1110
|
+
values = {k: t.text_values[k] for k in t.text_dirty}
|
|
1111
|
+
t.text_dirty = set()
|
|
1112
|
+
await broadcast_text_tab(
|
|
1113
|
+
t.id, {"type": "text_update", "tab": t.id, "values": values}
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
|
|
1053
1117
|
async def ws_pusher():
|
|
1054
1118
|
"""Push new samples to viewers as binary delta frames, per tab."""
|
|
1055
1119
|
target_dt = 1.0 / max(1, args.rate)
|
|
@@ -1296,8 +1360,21 @@ def _read_static_asset(name: str) -> str:
|
|
|
1296
1360
|
return fh.read()
|
|
1297
1361
|
|
|
1298
1362
|
|
|
1299
|
-
# Loaded once at import time so per-request cost is just the send.
|
|
1363
|
+
# Loaded once at import time so per-request cost is just the send. Version the
|
|
1364
|
+
# asset URLs by their contents: browsers can otherwise keep an old or damaged
|
|
1365
|
+
# KaTeX script after the executable is updated while fetching the new HTML.
|
|
1300
1366
|
_INDEX_HTML = _read_static_asset("index.html")
|
|
1367
|
+
for _asset in (
|
|
1368
|
+
"uPlot.min.css",
|
|
1369
|
+
"uPlot.iife.min.js",
|
|
1370
|
+
"katex/katex.min.css",
|
|
1371
|
+
"katex/katex.min.js",
|
|
1372
|
+
"katex/auto-render.min.js",
|
|
1373
|
+
):
|
|
1374
|
+
_digest = hashlib.sha256(_read_static_asset(_asset).encode("utf-8")).hexdigest()[:12]
|
|
1375
|
+
_INDEX_HTML = _INDEX_HTML.replace(
|
|
1376
|
+
f'"/static/{_asset}"', f'"/static/{_asset}?v={_digest}"'
|
|
1377
|
+
)
|
|
1301
1378
|
|
|
1302
1379
|
|
|
1303
1380
|
async def handle_index(request):
|
|
@@ -1342,6 +1419,7 @@ async def handle_ws(request):
|
|
|
1342
1419
|
},
|
|
1343
1420
|
)
|
|
1344
1421
|
if t.config_message is not None:
|
|
1422
|
+
refresh_config_message(t)
|
|
1345
1423
|
await ws_send_text(ws, t.config_message)
|
|
1346
1424
|
if t.display_values:
|
|
1347
1425
|
await ws_send_text(
|
|
@@ -1402,9 +1480,21 @@ async def handle_ws(request):
|
|
|
1402
1480
|
t = tabs.get(tid)
|
|
1403
1481
|
if sid and t is not None:
|
|
1404
1482
|
t.slider_values[sid] = value
|
|
1483
|
+
refresh_config_message(t)
|
|
1405
1484
|
await send_control_event(
|
|
1406
1485
|
t, {"type": "slider", "id": sid, "value": value}
|
|
1407
1486
|
)
|
|
1487
|
+
elif ptype == "control_text":
|
|
1488
|
+
text_id = payload.get("id")
|
|
1489
|
+
tid = ws_tab.get(ws, BIND_ME_ID)
|
|
1490
|
+
t = tabs.get(tid)
|
|
1491
|
+
if text_id and t is not None:
|
|
1492
|
+
value = str(payload.get("value", ""))
|
|
1493
|
+
t.text_values[text_id] = value
|
|
1494
|
+
refresh_config_message(t)
|
|
1495
|
+
await send_control_event(
|
|
1496
|
+
t, {"type": "text", "id": text_id, "value": value}
|
|
1497
|
+
)
|
|
1408
1498
|
elif msg.type == WSMsgType.ERROR:
|
|
1409
1499
|
break
|
|
1410
1500
|
finally:
|
|
@@ -1423,6 +1513,7 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1423
1513
|
<meta charset="utf-8" />
|
|
1424
1514
|
<title>rtplot snapshot</title>
|
|
1425
1515
|
<style>__UPLOT_CSS__</style>
|
|
1516
|
+
<style>__KATEX_CSS__</style>
|
|
1426
1517
|
<style>
|
|
1427
1518
|
html, body { margin: 0; padding: 0; background: #fafafa; color: #222;
|
|
1428
1519
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
|
@@ -1442,6 +1533,8 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1442
1533
|
· drag to zoom, double-click to reset
|
|
1443
1534
|
</p>
|
|
1444
1535
|
<script>__UPLOT_JS__</script>
|
|
1536
|
+
<script>__KATEX_JS__</script>
|
|
1537
|
+
<script>__KATEX_AUTO_RENDER_JS__</script>
|
|
1445
1538
|
<script>
|
|
1446
1539
|
(function () {
|
|
1447
1540
|
const SNAP = __SNAPSHOT_JSON__;
|
|
@@ -1502,6 +1595,15 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1502
1595
|
traceCount: traceCount, startIdx: traceOffset });
|
|
1503
1596
|
traceOffset += traceCount;
|
|
1504
1597
|
});
|
|
1598
|
+
renderMathInElement(plotsDiv, {
|
|
1599
|
+
delimiters: [
|
|
1600
|
+
{ left: '$$', right: '$$', display: true },
|
|
1601
|
+
{ left: '\\\\[', right: '\\\\]', display: true },
|
|
1602
|
+
{ left: '\\\\(', right: '\\\\)', display: false },
|
|
1603
|
+
{ left: '$', right: '$', display: false },
|
|
1604
|
+
],
|
|
1605
|
+
throwOnError: false,
|
|
1606
|
+
});
|
|
1505
1607
|
if (SNAP.animate) {
|
|
1506
1608
|
let phase = 0;
|
|
1507
1609
|
setInterval(function () {
|
|
@@ -1538,9 +1640,31 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1538
1640
|
try:
|
|
1539
1641
|
_UPLOT_JS = _read_static_asset("uPlot.iife.min.js")
|
|
1540
1642
|
_UPLOT_CSS = _read_static_asset("uPlot.min.css")
|
|
1643
|
+
_KATEX_JS = _read_static_asset(os.path.join("katex", "katex.min.js"))
|
|
1644
|
+
_KATEX_AUTO_RENDER_JS = _read_static_asset(
|
|
1645
|
+
os.path.join("katex", "auto-render.min.js")
|
|
1646
|
+
)
|
|
1647
|
+
_KATEX_CSS = _read_static_asset(os.path.join("katex", "katex.min.css"))
|
|
1648
|
+
# Snapshot files have no sibling font directory. Inline the vendored
|
|
1649
|
+
# WOFF2 faces so equations keep their layout when opened offline.
|
|
1650
|
+
_KATEX_FONT_DIR = os.path.join(
|
|
1651
|
+
os.path.dirname(os.path.abspath(__file__)), "static", "katex", "fonts"
|
|
1652
|
+
)
|
|
1653
|
+
for _font_name in os.listdir(_KATEX_FONT_DIR):
|
|
1654
|
+
if not _font_name.endswith(".woff2"):
|
|
1655
|
+
continue
|
|
1656
|
+
with open(os.path.join(_KATEX_FONT_DIR, _font_name), "rb") as _font_file:
|
|
1657
|
+
_font_data = base64.b64encode(_font_file.read()).decode("ascii")
|
|
1658
|
+
_KATEX_CSS = _KATEX_CSS.replace(
|
|
1659
|
+
f"url(fonts/{_font_name})",
|
|
1660
|
+
f"url(data:font/woff2;base64,{_font_data})",
|
|
1661
|
+
)
|
|
1541
1662
|
except Exception as _exc: # noqa: BLE001
|
|
1542
1663
|
_UPLOT_JS = f"/* uPlot load failed: {_exc} */"
|
|
1543
1664
|
_UPLOT_CSS = ""
|
|
1665
|
+
_KATEX_JS = f"/* KaTeX load failed: {_exc} */"
|
|
1666
|
+
_KATEX_AUTO_RENDER_JS = ""
|
|
1667
|
+
_KATEX_CSS = ""
|
|
1544
1668
|
|
|
1545
1669
|
|
|
1546
1670
|
def _build_snapshot_html(tab: Tab, animate: bool) -> str:
|
|
@@ -1590,7 +1714,13 @@ def _build_snapshot_html(tab: Tab, animate: bool) -> str:
|
|
|
1590
1714
|
html = _SNAPSHOT_TEMPLATE
|
|
1591
1715
|
html = html.replace("__UPLOT_CSS__", _UPLOT_CSS)
|
|
1592
1716
|
html = html.replace("__UPLOT_JS__", _UPLOT_JS)
|
|
1593
|
-
html = html.replace("
|
|
1717
|
+
html = html.replace("__KATEX_CSS__", _KATEX_CSS)
|
|
1718
|
+
html = html.replace("__KATEX_JS__", _KATEX_JS)
|
|
1719
|
+
html = html.replace("__KATEX_AUTO_RENDER_JS__", _KATEX_AUTO_RENDER_JS)
|
|
1720
|
+
# Keep client-supplied labels from closing the script element while
|
|
1721
|
+
# preserving their exact value when JavaScript parses the payload.
|
|
1722
|
+
snapshot_json = json.dumps(payload).replace("<", "\\u003c")
|
|
1723
|
+
html = html.replace("__SNAPSHOT_JSON__", snapshot_json)
|
|
1594
1724
|
return html
|
|
1595
1725
|
|
|
1596
1726
|
|
|
@@ -1641,11 +1771,12 @@ async def on_startup(app):
|
|
|
1641
1771
|
|
|
1642
1772
|
app["ws_task"] = asyncio.create_task(ws_pusher())
|
|
1643
1773
|
app["display_task"] = asyncio.create_task(display_pusher())
|
|
1774
|
+
app["text_input_task"] = asyncio.create_task(text_input_pusher())
|
|
1644
1775
|
app["resources_task"] = asyncio.create_task(resources_pusher())
|
|
1645
1776
|
|
|
1646
1777
|
|
|
1647
1778
|
async def on_cleanup(app):
|
|
1648
|
-
for key in ("ws_task", "display_task", "resources_task"):
|
|
1779
|
+
for key in ("ws_task", "display_task", "text_input_task", "resources_task"):
|
|
1649
1780
|
await _cancel_task(app.get(key))
|
|
1650
1781
|
for t in list(tabs.values()):
|
|
1651
1782
|
await _cancel_task(t.receiver_task)
|
|
File without changes
|
|
@@ -95,6 +95,8 @@
|
|
|
95
95
|
.ctrl-numinput { width: 72px; font-family: monospace; font-size: calc(13px * var(--ui-scale)); padding: 4px 6px; border: 1px solid #b8b8b8; border-radius: 3px; background: #fff; color: #222; text-align: right; -moz-appearance: textfield; }
|
|
96
96
|
.ctrl-numinput::-webkit-outer-spin-button,
|
|
97
97
|
.ctrl-numinput::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
98
|
+
.ctrl-textinput { min-width: 200px; width: min(320px, 100%); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: calc(13px * var(--ui-scale)); padding: 7px 10px; border: 1px solid #b8b8b8; border-radius: 3px; background: #fff; color: #222; text-align: left; }
|
|
99
|
+
.ctrl-textinput:focus { outline: none; border-color: #2a5db0; box-shadow: 0 0 0 2px rgba(42, 93, 176, 0.14); }
|
|
98
100
|
.ctrl-nudgebtn { width: 26px; height: 26px; font-size: calc(15px * var(--ui-scale)); font-weight: 600; line-height: 1; padding: 0; border: 1px solid #b8b8b8; background: #f7f7f7; color: #333; cursor: pointer; border-radius: 3px; }
|
|
99
101
|
.ctrl-nudgebtn:hover { background: #e9e9e9; }
|
|
100
102
|
.ctrl-nudgebtn:active { background: #dcdcdc; }
|
|
@@ -263,6 +265,8 @@
|
|
|
263
265
|
// a typo in someone's label can't blow up the whole page.
|
|
264
266
|
const _latexDelims = [
|
|
265
267
|
{ left: '$$', right: '$$', display: true },
|
|
268
|
+
{ left: '\\[', right: '\\]', display: true },
|
|
269
|
+
{ left: '\\(', right: '\\)', display: false },
|
|
266
270
|
{ left: '$', right: '$', display: false },
|
|
267
271
|
];
|
|
268
272
|
function renderLatexIn(el) {
|
|
@@ -299,6 +303,26 @@
|
|
|
299
303
|
const resTabRates = document.getElementById('res-tab-rates');
|
|
300
304
|
const resUnavail = document.getElementById('res-unavail');
|
|
301
305
|
|
|
306
|
+
// A failed KaTeX download can still leave auto-render defined, but its
|
|
307
|
+
// closure then holds an undefined KaTeX reference. Retry both scripts
|
|
308
|
+
// in order with fresh URLs and render labels already on the page.
|
|
309
|
+
if (typeof katex === 'undefined') {
|
|
310
|
+
const retry = Date.now();
|
|
311
|
+
const core = document.createElement('script');
|
|
312
|
+
core.src = `/static/katex/katex.min.js?retry=${retry}`;
|
|
313
|
+
core.onload = () => {
|
|
314
|
+
if (typeof katex === 'undefined') return;
|
|
315
|
+
const autoRender = document.createElement('script');
|
|
316
|
+
autoRender.src = `/static/katex/auto-render.min.js?retry=${retry}`;
|
|
317
|
+
autoRender.onload = () => {
|
|
318
|
+
renderLatexIn(plotsDiv);
|
|
319
|
+
tabbar.querySelectorAll('.tab-name').forEach(renderLatexIn);
|
|
320
|
+
};
|
|
321
|
+
document.head.appendChild(autoRender);
|
|
322
|
+
};
|
|
323
|
+
document.head.appendChild(core);
|
|
324
|
+
}
|
|
325
|
+
|
|
302
326
|
// ---- Detect the monitor's refresh rate on page load ----
|
|
303
327
|
let monitorHz = 0;
|
|
304
328
|
(function measureMonitorHz() {
|
|
@@ -415,7 +439,7 @@
|
|
|
415
439
|
let socket = null;
|
|
416
440
|
let pendingFrame = false;
|
|
417
441
|
let lastStatus = { fps: 0, statusByte: 0, nonPlot: 0, peers: 0, dirty: true };
|
|
418
|
-
const controlElements = { displays: {}, displayFormats: {}, displayKinds: {}, sliders: {} };
|
|
442
|
+
const controlElements = { displays: {}, displayFormats: {}, displayKinds: {}, sliders: {}, textInputs: {} };
|
|
419
443
|
|
|
420
444
|
function sendCtrl(msg) {
|
|
421
445
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
@@ -684,6 +708,34 @@
|
|
|
684
708
|
item.appendChild(val);
|
|
685
709
|
controlElements.displays[el.id] = val;
|
|
686
710
|
controlElements.displayKinds[el.id] = 'text';
|
|
711
|
+
} else if (el.type === 'text_input') {
|
|
712
|
+
item.classList.add('ctrl-text-input', 'flex');
|
|
713
|
+
if (el.label) {
|
|
714
|
+
const lbl = document.createElement('label');
|
|
715
|
+
lbl.textContent = el.label;
|
|
716
|
+
item.appendChild(lbl);
|
|
717
|
+
}
|
|
718
|
+
const input = document.createElement('input');
|
|
719
|
+
input.type = 'text';
|
|
720
|
+
input.className = 'ctrl-textinput';
|
|
721
|
+
input.value = (el.value !== undefined && el.value !== null) ? String(el.value) : '';
|
|
722
|
+
if (el.placeholder !== undefined && el.placeholder !== null) {
|
|
723
|
+
input.placeholder = String(el.placeholder);
|
|
724
|
+
}
|
|
725
|
+
if (el.max_length !== undefined && Number.isFinite(Number(el.max_length))) {
|
|
726
|
+
input.maxLength = Number(el.max_length);
|
|
727
|
+
}
|
|
728
|
+
const commit = () => sendCtrl({ type: 'control_text', id: el.id, value: input.value });
|
|
729
|
+
input.addEventListener('change', commit);
|
|
730
|
+
input.addEventListener('blur', commit);
|
|
731
|
+
input.addEventListener('keydown', (e) => {
|
|
732
|
+
if (e.key === 'Enter') {
|
|
733
|
+
e.preventDefault();
|
|
734
|
+
commit();
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
item.appendChild(input);
|
|
738
|
+
controlElements.textInputs[el.id] = input;
|
|
687
739
|
}
|
|
688
740
|
return item;
|
|
689
741
|
}
|
|
@@ -722,6 +774,14 @@
|
|
|
722
774
|
}
|
|
723
775
|
}
|
|
724
776
|
|
|
777
|
+
function applyTextValues(values) {
|
|
778
|
+
if (!values) return;
|
|
779
|
+
for (const [id, val] of Object.entries(values)) {
|
|
780
|
+
const input = controlElements.textInputs[id];
|
|
781
|
+
if (input) input.value = (val !== undefined && val !== null) ? String(val) : '';
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
725
785
|
function applyDisplayValues(values) {
|
|
726
786
|
if (!values) return;
|
|
727
787
|
for (const [id, val] of Object.entries(values)) {
|
|
@@ -752,6 +812,7 @@
|
|
|
752
812
|
controlElements.displayFormats = {};
|
|
753
813
|
controlElements.displayKinds = {};
|
|
754
814
|
controlElements.sliders = {};
|
|
815
|
+
controlElements.textInputs = {};
|
|
755
816
|
}
|
|
756
817
|
|
|
757
818
|
function showEmptyState(text) {
|
|
@@ -771,6 +832,7 @@
|
|
|
771
832
|
const colors = pcfg.colors || DEFAULT_COLORS;
|
|
772
833
|
const widths = pcfg.line_width || [];
|
|
773
834
|
const styles = pcfg.line_style || [];
|
|
835
|
+
const shows = pcfg.show || [];
|
|
774
836
|
|
|
775
837
|
const series = [{}];
|
|
776
838
|
for (let t = 0; t < traceCount; t++) {
|
|
@@ -782,6 +844,7 @@
|
|
|
782
844
|
dash: dash || undefined,
|
|
783
845
|
points: { show: false },
|
|
784
846
|
spanGaps: true,
|
|
847
|
+
show: shows[t] !== false,
|
|
785
848
|
});
|
|
786
849
|
}
|
|
787
850
|
|
|
@@ -859,6 +922,7 @@
|
|
|
859
922
|
});
|
|
860
923
|
totalTraces = traceOffset;
|
|
861
924
|
applySliderValues(cfg.slider_values);
|
|
925
|
+
applyTextValues(cfg.text_values);
|
|
862
926
|
applyDisplayValues(cfg.display_values);
|
|
863
927
|
// Render LaTeX in everything we just built: uPlot's title,
|
|
864
928
|
// x/y axis labels, and legend (all rendered as HTML), plus the
|
|
@@ -1280,6 +1344,9 @@
|
|
|
1280
1344
|
} else if (msg.type === 'display_update') {
|
|
1281
1345
|
if (msg.tab && msg.tab !== activeTab) return;
|
|
1282
1346
|
applyDisplayValues(msg.values);
|
|
1347
|
+
} else if (msg.type === 'text_update') {
|
|
1348
|
+
if (msg.tab && msg.tab !== activeTab) return;
|
|
1349
|
+
applyTextValues(msg.values);
|
|
1283
1350
|
} else if (msg.type === 'peer_count') {
|
|
1284
1351
|
const n = Number(msg.count) || 0;
|
|
1285
1352
|
if (lastStatus.peers !== n) {
|
|
File without changes
|
{better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2
RENAMED
|
File without changes
|
|
File without changes
|
{better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.10 → better_rtplot-0.4.14}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2
RENAMED
|
File without changes
|