better-rtplot 0.4.15__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.
Files changed (36) hide show
  1. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/PKG-INFO +6 -5
  2. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/README.md +5 -4
  3. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/pyproject.toml +1 -1
  4. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/client.py +23 -0
  5. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/server_browser.py +72 -36
  6. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/index.html +21 -6
  7. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/LICENSE +0 -0
  8. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/interactive_test.py +0 -0
  9. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/saved_plots/.gitignore +0 -0
  10. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/server.py +0 -0
  11. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/server_browser_gui.py +0 -0
  12. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/auto-render.min.js +0 -0
  13. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  14. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  15. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  16. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  17. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  18. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  19. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  20. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  21. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  22. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  23. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  24. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  25. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  26. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  27. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  28. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  29. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  30. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  31. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  32. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  33. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/katex.min.css +0 -0
  34. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/katex/katex.min.js +0 -0
  35. {better_rtplot-0.4.15 → better_rtplot-0.4.16}/rtplot/static/uPlot.iife.min.js +0 -0
  36. {better_rtplot-0.4.15 → 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.15
3
+ Version: 0.4.16
4
4
  Summary:
5
5
  License: MIT
6
6
  Author: jmontp
@@ -67,10 +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/).
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.
74
75
 
75
76
  **Client** — pip install in the env that runs your script:
76
77
 
@@ -43,10 +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/).
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.
50
51
 
51
52
  **Client** — pip install in the env that runs your script:
52
53
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "better-rtplot"
3
- version = "0.4.15"
3
+ version = "0.4.16"
4
4
  description = ""
5
5
  authors = ["jmontp <jmontp@umich.edu>"]
6
6
  license = "MIT"
@@ -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
@@ -509,34 +509,53 @@ def save_persisted_tabs():
509
509
  # Config parsing (per tab) #
510
510
  ###############################
511
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
+
512
542
  def parse_config(tab: Tab, json_config):
513
543
  """Translate a client plot config into the tab's buffer/trace metadata."""
514
544
  traces_per_plot = []
515
545
  trace_info = []
516
- control_rows = []
517
546
  slider_values = {}
518
547
  text_values = {}
519
- layout = []
548
+ plot_entries, control_rows, layout = _expand_config(json_config)
520
549
  num_datapoints_in_plot = DEFAULT_NUM_DATAPOINTS_IN_PLOT
521
550
 
522
- plot_counter = 0
523
- for plot_description in json_config.values():
524
- # Legacy non_plot_labels rows save-to-parquet is gone, but we
525
- # accept the shape so old client scripts don't crash.
526
- if "non_plot_labels" in plot_description:
527
- continue
528
-
529
- if "controls" in plot_description:
530
- row = plot_description["controls"]
531
- for element in row:
532
- if element.get("type") in ("slider", "dial") and "value" in element:
533
- slider_values[element["id"]] = float(element["value"])
534
- elif element.get("type") == "text_input":
535
- text_values[element["id"]] = str(element.get("value", ""))
536
- layout.append({"kind": "controls", "index": len(control_rows)})
537
- control_rows.append(row)
538
- 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", ""))
539
557
 
558
+ for plot_counter, (_, plot_description) in enumerate(plot_entries):
540
559
  trace_names = plot_description["names"]
541
560
  traces_per_plot.append(len(trace_names))
542
561
 
@@ -546,9 +565,6 @@ def parse_config(tab: Tab, json_config):
546
565
  for name in trace_names:
547
566
  trace_info.append((name, plot_counter))
548
567
 
549
- layout.append({"kind": "plot", "index": plot_counter})
550
- plot_counter += 1
551
-
552
568
  tab.traces_per_plot = traces_per_plot
553
569
  tab.trace_labels = trace_info
554
570
  tab.control_rows = control_rows
@@ -589,11 +605,7 @@ def parse_config(tab: Tab, json_config):
589
605
  def build_config_message(tab: Tab, config_dict) -> dict:
590
606
  """Convert the client-supplied OrderedDict into a JSON-friendly message."""
591
607
  plots = []
592
- for key, plot_description in config_dict.items():
593
- if "non_plot_labels" in plot_description:
594
- continue
595
- if "controls" in plot_description:
596
- continue
608
+ for key, plot_description in _expand_config(config_dict)[0]:
597
609
  plots.append(
598
610
  {
599
611
  "key": key,
@@ -1529,8 +1541,10 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
1529
1541
  <style>
1530
1542
  html, body { margin: 0; padding: 0; background: #fafafa; color: #222;
1531
1543
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
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; }
1544
+ #plots { display: flex; flex-direction: column; gap: 12px;
1545
+ max-width: 900px; margin: 0 auto; padding: 20px 16px; }
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; }
1534
1548
  .plot-wrap { background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 8px; min-width: 0; }
1535
1549
  .snap-footer { text-align: center; font-size: 12px; color: #999; padding: 8px 0 20px; }
1536
1550
  .snap-footer a { color: #2a5db0; text-decoration: none; }
@@ -1551,8 +1565,17 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
1551
1565
  (function () {
1552
1566
  const SNAP = __SNAPSHOT_JSON__;
1553
1567
  const plotsDiv = document.getElementById('plots');
1554
- plotsDiv.style.setProperty('--plot-columns', SNAP.columns || 1);
1555
- plotsDiv.style.maxWidth = `${900 * (SNAP.columns || 1)}px`;
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
+ }
1556
1579
  const COLOR_MAP = { r:'rgb(255,0,0)', g:'rgb(0,200,0)', b:'rgb(0,0,255)',
1557
1580
  c:'rgb(0,200,200)', m:'rgb(200,0,200)',
1558
1581
  y:'rgb(200,200,0)', k:'rgb(0,0,0)' };
@@ -1565,7 +1588,7 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
1565
1588
  }
1566
1589
  const plots = [];
1567
1590
  let traceOffset = 0;
1568
- SNAP.plots.forEach(function (pcfg) {
1591
+ function buildPlot(pcfg, parent) {
1569
1592
  const xrange = SNAP.num_samples;
1570
1593
  const xs = new Float64Array(xrange);
1571
1594
  for (let i = 0; i < xrange; i++) xs[i] = i;
@@ -1585,7 +1608,7 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
1585
1608
  }
1586
1609
  const wrap = document.createElement('div');
1587
1610
  wrap.className = 'plot-wrap';
1588
- plotsDiv.appendChild(wrap);
1611
+ parent.appendChild(wrap);
1589
1612
  const data = [xs];
1590
1613
  for (let t = 0; t < traceCount; t++) {
1591
1614
  const src = SNAP.trace_data[traceOffset + t];
@@ -1608,6 +1631,20 @@ _SNAPSHOT_TEMPLATE = """<!doctype html>
1608
1631
  plots.push({ uplot: u, wrap: wrap, data: data, xs: xs, xrange: xrange,
1609
1632
  traceCount: traceCount, startIdx: traceOffset });
1610
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
+ }
1611
1648
  });
1612
1649
  renderMathInElement(plotsDiv, {
1613
1650
  delimiters: [
@@ -1699,9 +1736,7 @@ def _build_snapshot_html(tab: Tab, animate: bool) -> str:
1699
1736
 
1700
1737
  plots = []
1701
1738
  cfg = tab.config_dict or OrderedDict()
1702
- for key, plot_description in cfg.items():
1703
- if "non_plot_labels" in plot_description or "controls" in plot_description:
1704
- continue
1739
+ for key, plot_description in _expand_config(cfg)[0]:
1705
1740
  plots.append({
1706
1741
  "key": key,
1707
1742
  "names": plot_description.get("names", []),
@@ -1722,6 +1757,7 @@ def _build_snapshot_html(tab: Tab, animate: bool) -> str:
1722
1757
  payload = {
1723
1758
  "plots": plots,
1724
1759
  "columns": PLOT_COLUMNS,
1760
+ "layout": tab.layout,
1725
1761
  "num_samples": int(hi - lo),
1726
1762
  "trace_data": trace_data,
1727
1763
  "animate": bool(animate),
@@ -62,9 +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: grid; grid-template-columns: repeat(var(--plot-columns, 1), minmax(320px, 1fr)); padding: 12px; gap: 12px; overflow-x: auto; }
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; }
66
67
  .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; }
68
68
  .plot-title { font-size: 13px; font-weight: 600; margin: 0 0 4px 4px; color: #333; }
69
69
  #empty-state { padding: 28px 32px; color: #888; font-size: calc(14px * var(--ui-scale)); font-style: italic; }
70
70
  /* Persistent banner the server raises when a client-supplied plot
@@ -822,7 +822,7 @@
822
822
  plotsDiv.appendChild(d);
823
823
  }
824
824
 
825
- function buildOnePlot(pcfg, rowLayout, traceOffset) {
825
+ function buildOnePlot(pcfg, rowLayout, traceOffset, parent) {
826
826
  const xrange = pcfg.xrange || 200;
827
827
  const xs = new Float64Array(xrange);
828
828
  for (let i = 0; i < xrange; i++) xs[i] = i;
@@ -854,7 +854,7 @@
854
854
  : baseHeight;
855
855
  const wrap = document.createElement('div');
856
856
  wrap.className = 'plot-wrap';
857
- plotsDiv.appendChild(wrap);
857
+ parent.appendChild(wrap);
858
858
 
859
859
  const opts = {
860
860
  width: Math.max(240, wrap.clientWidth - 16),
@@ -900,7 +900,13 @@
900
900
  function buildPlots(cfg) {
901
901
  destroyPlots();
902
902
  const columns = Number(cfg.columns) || (cfg.row_layout === false ? 2 : 1);
903
- plotsDiv.style.setProperty('--plot-columns', Math.max(1, Math.floor(columns)));
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
- traceOffset += buildOnePlot(pcfg, columns === 1, traceOffset);
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));
File without changes