better-rtplot 0.4.14__tar.gz → 0.4.16__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 → better_rtplot-0.4.16}/PKG-INFO +7 -1
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/README.md +6 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/pyproject.toml +1 -1
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/client.py +23 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/server_browser.py +91 -40
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/index.html +29 -14
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/LICENSE +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/interactive_test.py +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/saved_plots/.gitignore +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/server.py +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/server_browser_gui.py +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/auto-render.min.js +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/katex.min.css +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/katex.min.js +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/uPlot.iife.min.js +0 -0
- {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/uPlot.min.css +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: better-rtplot
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.16
|
|
4
4
|
Summary:
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: jmontp
|
|
@@ -67,6 +67,12 @@ 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
|
+
Set columns per row in your sender's layout with
|
|
71
|
+
`PlotRow([Plot(names=["a"]), Plot(names=["b"])], columns=2)`.
|
|
72
|
+
Start the server normally; each row can use a different column count.
|
|
73
|
+
See the [multi-column example](examples/05_multi_column/) for typed and
|
|
74
|
+
dict forms, including a layout with two, one, and three columns.
|
|
75
|
+
|
|
70
76
|
**Client** — pip install in the env that runs your script:
|
|
71
77
|
|
|
72
78
|
```bash
|
|
@@ -43,6 +43,12 @@ 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
|
+
Set columns per row in your sender's layout with
|
|
47
|
+
`PlotRow([Plot(names=["a"]), Plot(names=["b"])], columns=2)`.
|
|
48
|
+
Start the server normally; each row can use a different column count.
|
|
49
|
+
See the [multi-column example](examples/05_multi_column/) for typed and
|
|
50
|
+
dict forms, including a layout with two, one, and three columns.
|
|
51
|
+
|
|
46
52
|
**Client** — pip install in the env that runs your script:
|
|
47
53
|
|
|
48
54
|
```bash
|
|
@@ -164,6 +164,28 @@ class Plot:
|
|
|
164
164
|
})
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
@dataclass
|
|
168
|
+
class PlotRow:
|
|
169
|
+
"""A group of plots with its own column count in the browser."""
|
|
170
|
+
plots: List[Union[Plot, dict]]
|
|
171
|
+
columns: int
|
|
172
|
+
|
|
173
|
+
def to_dict(self):
|
|
174
|
+
if isinstance(self.columns, bool) or not isinstance(self.columns, int) or self.columns < 1:
|
|
175
|
+
raise ValueError("PlotRow.columns must be a positive integer")
|
|
176
|
+
if not self.plots:
|
|
177
|
+
raise ValueError("PlotRow.plots must contain at least one plot")
|
|
178
|
+
plots = [
|
|
179
|
+
p.to_dict() if hasattr(p, "to_dict") else p
|
|
180
|
+
for p in self.plots
|
|
181
|
+
]
|
|
182
|
+
if any(not isinstance(p, dict) or "names" not in p
|
|
183
|
+
or any(k in p for k in ("plots", "controls", "non_plot_labels"))
|
|
184
|
+
for p in plots):
|
|
185
|
+
raise ValueError("PlotRow can contain only plot descriptions")
|
|
186
|
+
return {"columns": self.columns, "plots": plots}
|
|
187
|
+
|
|
188
|
+
|
|
167
189
|
@dataclass
|
|
168
190
|
class Button:
|
|
169
191
|
id: str
|
|
@@ -455,6 +477,7 @@ def initialize_plots(plot_descriptions=1, handshake_timeout=2.0):
|
|
|
455
477
|
- list of str: one plot, one trace per name
|
|
456
478
|
- list of list of str: one plot per sublist
|
|
457
479
|
- list of dict: multiple plots, each with full styling
|
|
480
|
+
- PlotRow or {"plots": [...], "columns": N}: a row with N columns
|
|
458
481
|
handshake_timeout:
|
|
459
482
|
Max seconds to wait for the config_ack. On timeout we print a
|
|
460
483
|
warning and return (so an old server without the handshake
|
|
@@ -98,8 +98,17 @@ parser.add_argument(
|
|
|
98
98
|
parser.add_argument(
|
|
99
99
|
"-c",
|
|
100
100
|
"--column",
|
|
101
|
-
|
|
102
|
-
|
|
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)",
|
|
103
112
|
)
|
|
104
113
|
|
|
105
114
|
parser.add_argument(
|
|
@@ -180,7 +189,9 @@ parser.add_argument(
|
|
|
180
189
|
|
|
181
190
|
args = parser.parse_args()
|
|
182
191
|
|
|
183
|
-
|
|
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")
|
|
184
195
|
DEBUG_TEXT_ENABLED = args.debug
|
|
185
196
|
SKIP_PLOT_DATAPOINTS = args.skip
|
|
186
197
|
ADAPT_SKIP_PLOT_DATAPOINTS = args.adaptable
|
|
@@ -498,34 +509,53 @@ def save_persisted_tabs():
|
|
|
498
509
|
# Config parsing (per tab) #
|
|
499
510
|
###############################
|
|
500
511
|
|
|
512
|
+
def _expand_config(config):
|
|
513
|
+
"""Flatten plots in wire order while retaining explicit row boundaries."""
|
|
514
|
+
plots, controls, layout = [], [], []
|
|
515
|
+
for key, entry in config.items():
|
|
516
|
+
if "non_plot_labels" in entry:
|
|
517
|
+
continue
|
|
518
|
+
if "plots" in entry:
|
|
519
|
+
columns = entry.get("columns")
|
|
520
|
+
children = entry["plots"]
|
|
521
|
+
if isinstance(columns, bool) or not isinstance(columns, int) or columns < 1:
|
|
522
|
+
raise ValueError("Plot row columns must be a positive integer")
|
|
523
|
+
if not isinstance(children, list) or not children:
|
|
524
|
+
raise ValueError("Plot row plots must be a non-empty list")
|
|
525
|
+
indices = []
|
|
526
|
+
for i, child in enumerate(children):
|
|
527
|
+
if (not isinstance(child, dict) or "names" not in child
|
|
528
|
+
or any(k in child for k in ("plots", "controls", "non_plot_labels"))):
|
|
529
|
+
raise ValueError("Plot rows can contain only plot descriptions")
|
|
530
|
+
indices.append(len(plots))
|
|
531
|
+
plots.append((f"{key}/{i}", child))
|
|
532
|
+
layout.append({"kind": "row", "columns": columns, "plots": indices})
|
|
533
|
+
elif "controls" in entry:
|
|
534
|
+
layout.append({"kind": "controls", "index": len(controls)})
|
|
535
|
+
controls.append(entry["controls"])
|
|
536
|
+
else:
|
|
537
|
+
layout.append({"kind": "plot", "index": len(plots)})
|
|
538
|
+
plots.append((key, entry))
|
|
539
|
+
return plots, controls, layout
|
|
540
|
+
|
|
541
|
+
|
|
501
542
|
def parse_config(tab: Tab, json_config):
|
|
502
543
|
"""Translate a client plot config into the tab's buffer/trace metadata."""
|
|
503
544
|
traces_per_plot = []
|
|
504
545
|
trace_info = []
|
|
505
|
-
control_rows = []
|
|
506
546
|
slider_values = {}
|
|
507
547
|
text_values = {}
|
|
508
|
-
layout =
|
|
548
|
+
plot_entries, control_rows, layout = _expand_config(json_config)
|
|
509
549
|
num_datapoints_in_plot = DEFAULT_NUM_DATAPOINTS_IN_PLOT
|
|
510
550
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if "controls" in plot_description:
|
|
519
|
-
row = plot_description["controls"]
|
|
520
|
-
for element in row:
|
|
521
|
-
if element.get("type") in ("slider", "dial") and "value" in element:
|
|
522
|
-
slider_values[element["id"]] = float(element["value"])
|
|
523
|
-
elif element.get("type") == "text_input":
|
|
524
|
-
text_values[element["id"]] = str(element.get("value", ""))
|
|
525
|
-
layout.append({"kind": "controls", "index": len(control_rows)})
|
|
526
|
-
control_rows.append(row)
|
|
527
|
-
continue
|
|
551
|
+
for row in control_rows:
|
|
552
|
+
for element in row:
|
|
553
|
+
if element.get("type") in ("slider", "dial") and "value" in element:
|
|
554
|
+
slider_values[element["id"]] = float(element["value"])
|
|
555
|
+
elif element.get("type") == "text_input":
|
|
556
|
+
text_values[element["id"]] = str(element.get("value", ""))
|
|
528
557
|
|
|
558
|
+
for plot_counter, (_, plot_description) in enumerate(plot_entries):
|
|
529
559
|
trace_names = plot_description["names"]
|
|
530
560
|
traces_per_plot.append(len(trace_names))
|
|
531
561
|
|
|
@@ -535,9 +565,6 @@ def parse_config(tab: Tab, json_config):
|
|
|
535
565
|
for name in trace_names:
|
|
536
566
|
trace_info.append((name, plot_counter))
|
|
537
567
|
|
|
538
|
-
layout.append({"kind": "plot", "index": plot_counter})
|
|
539
|
-
plot_counter += 1
|
|
540
|
-
|
|
541
568
|
tab.traces_per_plot = traces_per_plot
|
|
542
569
|
tab.trace_labels = trace_info
|
|
543
570
|
tab.control_rows = control_rows
|
|
@@ -578,11 +605,7 @@ def parse_config(tab: Tab, json_config):
|
|
|
578
605
|
def build_config_message(tab: Tab, config_dict) -> dict:
|
|
579
606
|
"""Convert the client-supplied OrderedDict into a JSON-friendly message."""
|
|
580
607
|
plots = []
|
|
581
|
-
for key, plot_description in config_dict
|
|
582
|
-
if "non_plot_labels" in plot_description:
|
|
583
|
-
continue
|
|
584
|
-
if "controls" in plot_description:
|
|
585
|
-
continue
|
|
608
|
+
for key, plot_description in _expand_config(config_dict)[0]:
|
|
586
609
|
plots.append(
|
|
587
610
|
{
|
|
588
611
|
"key": key,
|
|
@@ -607,7 +630,8 @@ def build_config_message(tab: Tab, config_dict) -> dict:
|
|
|
607
630
|
"slider_values": dict(tab.slider_values),
|
|
608
631
|
"text_values": dict(tab.text_values),
|
|
609
632
|
"display_values": dict(tab.display_values),
|
|
610
|
-
"
|
|
633
|
+
"columns": PLOT_COLUMNS,
|
|
634
|
+
"row_layout": PLOT_COLUMNS == 1,
|
|
611
635
|
}
|
|
612
636
|
|
|
613
637
|
|
|
@@ -1519,7 +1543,9 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1519
1543
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
|
1520
1544
|
#plots { display: flex; flex-direction: column; gap: 12px;
|
|
1521
1545
|
max-width: 900px; margin: 0 auto; padding: 20px 16px; }
|
|
1522
|
-
.plot-
|
|
1546
|
+
.plot-row { display: grid; grid-template-columns: repeat(var(--plot-columns, 1), minmax(320px, 1fr));
|
|
1547
|
+
gap: 12px; min-width: 0; overflow-x: auto; }
|
|
1548
|
+
.plot-wrap { background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 8px; min-width: 0; }
|
|
1523
1549
|
.snap-footer { text-align: center; font-size: 12px; color: #999; padding: 8px 0 20px; }
|
|
1524
1550
|
.snap-footer a { color: #2a5db0; text-decoration: none; }
|
|
1525
1551
|
.snap-footer a:hover { text-decoration: underline; }
|
|
@@ -1539,6 +1565,17 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1539
1565
|
(function () {
|
|
1540
1566
|
const SNAP = __SNAPSHOT_JSON__;
|
|
1541
1567
|
const plotsDiv = document.getElementById('plots');
|
|
1568
|
+
const layout = SNAP.layout || SNAP.plots.map((_, i) => ({kind: 'plot', index: i}));
|
|
1569
|
+
const maxColumns = Math.max(1, ...layout.map(entry =>
|
|
1570
|
+
entry.kind === 'row' ? entry.columns : (entry.kind === 'plot' ? SNAP.columns || 1 : 1)));
|
|
1571
|
+
plotsDiv.style.maxWidth = `${900 * maxColumns}px`;
|
|
1572
|
+
function newRow(columns) {
|
|
1573
|
+
const row = document.createElement('div');
|
|
1574
|
+
row.className = 'plot-row';
|
|
1575
|
+
row.style.setProperty('--plot-columns', columns);
|
|
1576
|
+
plotsDiv.appendChild(row);
|
|
1577
|
+
return row;
|
|
1578
|
+
}
|
|
1542
1579
|
const COLOR_MAP = { r:'rgb(255,0,0)', g:'rgb(0,200,0)', b:'rgb(0,0,255)',
|
|
1543
1580
|
c:'rgb(0,200,200)', m:'rgb(200,0,200)',
|
|
1544
1581
|
y:'rgb(200,200,0)', k:'rgb(0,0,0)' };
|
|
@@ -1551,7 +1588,7 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1551
1588
|
}
|
|
1552
1589
|
const plots = [];
|
|
1553
1590
|
let traceOffset = 0;
|
|
1554
|
-
|
|
1591
|
+
function buildPlot(pcfg, parent) {
|
|
1555
1592
|
const xrange = SNAP.num_samples;
|
|
1556
1593
|
const xs = new Float64Array(xrange);
|
|
1557
1594
|
for (let i = 0; i < xrange; i++) xs[i] = i;
|
|
@@ -1571,14 +1608,14 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1571
1608
|
}
|
|
1572
1609
|
const wrap = document.createElement('div');
|
|
1573
1610
|
wrap.className = 'plot-wrap';
|
|
1574
|
-
|
|
1611
|
+
parent.appendChild(wrap);
|
|
1575
1612
|
const data = [xs];
|
|
1576
1613
|
for (let t = 0; t < traceCount; t++) {
|
|
1577
1614
|
const src = SNAP.trace_data[traceOffset + t];
|
|
1578
1615
|
data.push(Float64Array.from(src));
|
|
1579
1616
|
}
|
|
1580
1617
|
const opts = {
|
|
1581
|
-
width: Math.max(
|
|
1618
|
+
width: Math.max(240, wrap.clientWidth - 16),
|
|
1582
1619
|
height: 260,
|
|
1583
1620
|
title: pcfg.title || '',
|
|
1584
1621
|
scales: {
|
|
@@ -1591,9 +1628,23 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1591
1628
|
cursor: { drag: { x: true, y: false } },
|
|
1592
1629
|
};
|
|
1593
1630
|
const u = new uPlot(opts, data, wrap);
|
|
1594
|
-
plots.push({ uplot: u, data: data, xs: xs, xrange: xrange,
|
|
1631
|
+
plots.push({ uplot: u, wrap: wrap, data: data, xs: xs, xrange: xrange,
|
|
1595
1632
|
traceCount: traceCount, startIdx: traceOffset });
|
|
1596
1633
|
traceOffset += traceCount;
|
|
1634
|
+
}
|
|
1635
|
+
let implicitRow = null;
|
|
1636
|
+
layout.forEach(entry => {
|
|
1637
|
+
if (entry.kind === 'plot') {
|
|
1638
|
+
if (!implicitRow) implicitRow = newRow(SNAP.columns || 1);
|
|
1639
|
+
buildPlot(SNAP.plots[entry.index], implicitRow);
|
|
1640
|
+
} else if (entry.kind === 'row') {
|
|
1641
|
+
implicitRow = null;
|
|
1642
|
+
const row = newRow(entry.columns);
|
|
1643
|
+
entry.plots.forEach(index => buildPlot(SNAP.plots[index], row));
|
|
1644
|
+
} else {
|
|
1645
|
+
// Snapshots omit controls, but retain the row break around them.
|
|
1646
|
+
implicitRow = null;
|
|
1647
|
+
}
|
|
1597
1648
|
});
|
|
1598
1649
|
renderMathInElement(plotsDiv, {
|
|
1599
1650
|
delimiters: [
|
|
@@ -1625,7 +1676,7 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
|
|
|
1625
1676
|
window.addEventListener('resize', function () {
|
|
1626
1677
|
plots.forEach(function (p) {
|
|
1627
1678
|
p.uplot.setSize({
|
|
1628
|
-
width: Math.max(
|
|
1679
|
+
width: Math.max(240, p.wrap.clientWidth - 16),
|
|
1629
1680
|
height: 260,
|
|
1630
1681
|
});
|
|
1631
1682
|
});
|
|
@@ -1685,9 +1736,7 @@ def _build_snapshot_html(tab: Tab, animate: bool) -> str:
|
|
|
1685
1736
|
|
|
1686
1737
|
plots = []
|
|
1687
1738
|
cfg = tab.config_dict or OrderedDict()
|
|
1688
|
-
for key, plot_description in cfg
|
|
1689
|
-
if "non_plot_labels" in plot_description or "controls" in plot_description:
|
|
1690
|
-
continue
|
|
1739
|
+
for key, plot_description in _expand_config(cfg)[0]:
|
|
1691
1740
|
plots.append({
|
|
1692
1741
|
"key": key,
|
|
1693
1742
|
"names": plot_description.get("names", []),
|
|
@@ -1707,6 +1756,8 @@ def _build_snapshot_html(tab: Tab, animate: bool) -> str:
|
|
|
1707
1756
|
|
|
1708
1757
|
payload = {
|
|
1709
1758
|
"plots": plots,
|
|
1759
|
+
"columns": PLOT_COLUMNS,
|
|
1760
|
+
"layout": tab.layout,
|
|
1710
1761
|
"num_samples": int(hi - lo),
|
|
1711
1762
|
"trace_data": trace_data,
|
|
1712
1763
|
"animate": bool(animate),
|
|
@@ -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: flex; padding: 12px; gap: 12px; }
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.plot-wrap { background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 8px; flex: 1 1 auto; min-width: 320px; }
|
|
65
|
+
#plots { display: flex; flex-direction: column; padding: 12px; gap: 12px; }
|
|
66
|
+
.plot-row { display: grid; grid-template-columns: repeat(var(--plot-columns, 1), minmax(320px, 1fr)); gap: 12px; min-width: 0; overflow-x: auto; }
|
|
67
|
+
.plot-wrap { background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 8px; min-width: 0; }
|
|
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
|
|
@@ -823,7 +822,7 @@
|
|
|
823
822
|
plotsDiv.appendChild(d);
|
|
824
823
|
}
|
|
825
824
|
|
|
826
|
-
function buildOnePlot(pcfg, rowLayout, traceOffset) {
|
|
825
|
+
function buildOnePlot(pcfg, rowLayout, traceOffset, parent) {
|
|
827
826
|
const xrange = pcfg.xrange || 200;
|
|
828
827
|
const xs = new Float64Array(xrange);
|
|
829
828
|
for (let i = 0; i < xrange; i++) xs[i] = i;
|
|
@@ -853,8 +852,12 @@
|
|
|
853
852
|
const plotHeight = (Number.isFinite(heightMul) && heightMul > 0)
|
|
854
853
|
? Math.round(baseHeight * heightMul)
|
|
855
854
|
: baseHeight;
|
|
855
|
+
const wrap = document.createElement('div');
|
|
856
|
+
wrap.className = 'plot-wrap';
|
|
857
|
+
parent.appendChild(wrap);
|
|
858
|
+
|
|
856
859
|
const opts = {
|
|
857
|
-
width: Math.max(
|
|
860
|
+
width: Math.max(240, wrap.clientWidth - 16),
|
|
858
861
|
height: plotHeight,
|
|
859
862
|
title: pcfg.title || '',
|
|
860
863
|
scales: {
|
|
@@ -870,10 +873,6 @@
|
|
|
870
873
|
cursor: { drag: { x: false, y: false } },
|
|
871
874
|
};
|
|
872
875
|
|
|
873
|
-
const wrap = document.createElement('div');
|
|
874
|
-
wrap.className = 'plot-wrap';
|
|
875
|
-
plotsDiv.appendChild(wrap);
|
|
876
|
-
|
|
877
876
|
const buffers = [];
|
|
878
877
|
for (let t = 0; t < traceCount; t++) {
|
|
879
878
|
const buf = new Float32Array(xrange);
|
|
@@ -887,6 +886,7 @@
|
|
|
887
886
|
const u = new uPlot(opts, initialData, wrap);
|
|
888
887
|
plots.push({
|
|
889
888
|
uplot: u,
|
|
889
|
+
wrap: wrap,
|
|
890
890
|
xs: xs,
|
|
891
891
|
traceCount: traceCount,
|
|
892
892
|
startIdx: traceOffset,
|
|
@@ -899,8 +899,14 @@
|
|
|
899
899
|
|
|
900
900
|
function buildPlots(cfg) {
|
|
901
901
|
destroyPlots();
|
|
902
|
-
|
|
903
|
-
|
|
902
|
+
const columns = Number(cfg.columns) || (cfg.row_layout === false ? 2 : 1);
|
|
903
|
+
function newRow(count) {
|
|
904
|
+
const row = document.createElement('div');
|
|
905
|
+
row.className = 'plot-row';
|
|
906
|
+
row.style.setProperty('--plot-columns', Math.max(1, Math.floor(count)));
|
|
907
|
+
plotsDiv.appendChild(row);
|
|
908
|
+
return row;
|
|
909
|
+
}
|
|
904
910
|
|
|
905
911
|
const controls = cfg.controls || [];
|
|
906
912
|
const layout = (cfg.layout && cfg.layout.length)
|
|
@@ -909,12 +915,21 @@
|
|
|
909
915
|
.concat(controls.map((_, idx) => ({ kind: 'controls', index: idx })));
|
|
910
916
|
|
|
911
917
|
let traceOffset = 0;
|
|
918
|
+
let implicitRow = null;
|
|
912
919
|
layout.forEach(entry => {
|
|
913
920
|
if (entry.kind === 'plot') {
|
|
914
921
|
const pcfg = cfg.plots[entry.index];
|
|
915
922
|
if (!pcfg) return;
|
|
916
|
-
|
|
923
|
+
if (!implicitRow) implicitRow = newRow(columns);
|
|
924
|
+
traceOffset += buildOnePlot(pcfg, columns === 1, traceOffset, implicitRow);
|
|
925
|
+
} else if (entry.kind === 'row') {
|
|
926
|
+
implicitRow = null;
|
|
927
|
+
const row = newRow(entry.columns);
|
|
928
|
+
entry.plots.forEach(index => {
|
|
929
|
+
traceOffset += buildOnePlot(cfg.plots[index], entry.columns === 1, traceOffset, row);
|
|
930
|
+
});
|
|
917
931
|
} else if (entry.kind === 'controls') {
|
|
932
|
+
implicitRow = null;
|
|
918
933
|
const row = controls[entry.index];
|
|
919
934
|
if (!row) return;
|
|
920
935
|
plotsDiv.appendChild(buildControlRow(row));
|
|
@@ -1366,7 +1381,7 @@
|
|
|
1366
1381
|
window.addEventListener('resize', () => {
|
|
1367
1382
|
plots.forEach(p => {
|
|
1368
1383
|
p.uplot.setSize({
|
|
1369
|
-
width: Math.max(
|
|
1384
|
+
width: Math.max(240, p.wrap.clientWidth - 16),
|
|
1370
1385
|
height: p.height,
|
|
1371
1386
|
});
|
|
1372
1387
|
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2
RENAMED
|
File without changes
|
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2
RENAMED
|
File without changes
|
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2
RENAMED
|
File without changes
|
{better_rtplot-0.4.14 → better_rtplot-0.4.16}/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
|