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.
Files changed (36) hide show
  1. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/PKG-INFO +7 -1
  2. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/README.md +6 -0
  3. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/pyproject.toml +1 -1
  4. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/client.py +23 -0
  5. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/server_browser.py +91 -40
  6. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/index.html +29 -14
  7. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/LICENSE +0 -0
  8. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/interactive_test.py +0 -0
  9. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/saved_plots/.gitignore +0 -0
  10. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/server.py +0 -0
  11. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/server_browser_gui.py +0 -0
  12. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/auto-render.min.js +0 -0
  13. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  14. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  15. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  16. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  17. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  18. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  19. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  20. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  21. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  22. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  23. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  24. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  25. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  26. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  27. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  28. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  29. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  30. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  31. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  32. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  33. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/katex.min.css +0 -0
  34. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/katex/katex.min.js +0 -0
  35. {better_rtplot-0.4.14 → better_rtplot-0.4.16}/rtplot/static/uPlot.iife.min.js +0 -0
  36. {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.14
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "better-rtplot"
3
- version = "0.4.14"
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
@@ -98,8 +98,17 @@ parser.add_argument(
98
98
  parser.add_argument(
99
99
  "-c",
100
100
  "--column",
101
- help="Create new plots in separate columns instead of rows",
102
- action="store_false",
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
- NEW_SUBPLOT_IN_ROW = args.column
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
- plot_counter = 0
512
- for plot_description in json_config.values():
513
- # Legacy non_plot_labels rows save-to-parquet is gone, but we
514
- # accept the shape so old client scripts don't crash.
515
- if "non_plot_labels" in plot_description:
516
- continue
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.items():
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
- "row_layout": bool(NEW_SUBPLOT_IN_ROW),
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-wrap { background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 8px; }
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
- SNAP.plots.forEach(function (pcfg) {
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
- plotsDiv.appendChild(wrap);
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(640, plotsDiv.clientWidth - 40),
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(640, plotsDiv.clientWidth - 40),
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.items():
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
- #plots.row { flex-direction: column; }
67
- #plots.col { flex-direction: row; flex-wrap: wrap; }
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(640, plotsDiv.clientWidth - 40),
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
- plotsDiv.classList.toggle('row', cfg.row_layout);
903
- plotsDiv.classList.toggle('col', !cfg.row_layout);
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
- traceOffset += buildOnePlot(pcfg, cfg.row_layout, 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));
@@ -1366,7 +1381,7 @@
1366
1381
  window.addEventListener('resize', () => {
1367
1382
  plots.forEach(p => {
1368
1383
  p.uplot.setSize({
1369
- width: Math.max(640, plotsDiv.clientWidth - 40),
1384
+ width: Math.max(240, p.wrap.clientWidth - 16),
1370
1385
  height: p.height,
1371
1386
  });
1372
1387
  });
File without changes