Created
August 27, 2025 13:01
-
-
Save sjchoi86/4507590dbe59a71ecba51524b4ce5eb7 to your computer and use it in GitHub Desktop.
Static plot widget
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Plot graph | |
| fig = StaticPlotWidget( | |
| title = 'Constant-velocity Interpolated Joint Position Trajectories', | |
| x_offset = 0, | |
| y_offset = 0, | |
| window_width = int(0.5*env.monitor_width), | |
| window_height = int(1.0*env.monitor_height), | |
| x_label = 'time (sec)', | |
| legend = True, | |
| legend_loc = 'top-right' | |
| ) | |
| colors = get_colors(n_color=len(joint_names),cmap_name='gist_rainbow',alpha=1.0) | |
| for j_idx, name in enumerate(joint_names): | |
| # Linearly interpolated joint trajectories | |
| fig.plot(times, qpos_traj[:, j_idx], fmt='-', color='w', lw=1.0, label=None) | |
| # Smoothed joint trajectories | |
| color = colors[j_idx] | |
| fig.plot(times,qpos_traj_smt[:,j_idx],fmt='-',color=color,lw=2.0,label=name) | |
| # Via poses | |
| fig.plot(times_anchor,qpos_list[:,j_idx],fmt='o',color=color,ms=5,mfc='none',mec=color) | |
| vline = fig.plot_vertical_bar(x=0.0,color=(1,0,0,1),lw=2) | |
| # Legend | |
| fig.legend(loc='top-right',offset=(10,10),ncol=2,font_size=6) | |
| fig.show() | |
| # Initialize | |
| env.reset() | |
| cfgs = {'azimuth':142.7,'distance':2.3,'elevation':-25.6,'lookat':[0,0.08,1.03]} | |
| env.init_viewer(width=0.5,height=1.0,transparent=True,black_sky=True,**cfgs) | |
| # Loop | |
| running,tick = False,0 | |
| while env.is_viewer_alive(): | |
| # Update | |
| time = times[tick] | |
| qpos = qpos_traj_smt[tick,:] | |
| env.forward(qpos) | |
| # Keyboard | |
| if env.is_key_pressed_once(glfw.KEY_SPACE): running = not running | |
| if env.is_key_pressed_once(glfw.KEY_RIGHT): tick = tick + 100 | |
| if env.is_key_pressed_once(glfw.KEY_LEFT): tick = tick - 100 | |
| # Update tick | |
| if running: tick = tick + 1 | |
| if tick >= L: tick = L-1 | |
| if tick <= 0: tick = 0 | |
| fig.move_vertical_bar(x=time) | |
| # Render | |
| env.viewer_text_overlay('Key',loc='top left') | |
| env.viewer_text_overlay('[Space]','Pause/Resume',loc='top left') | |
| env.viewer_text_overlay('[Right Arrow]','Forward',loc='top left') | |
| env.viewer_text_overlay('[Left Arrow]','Backward',loc='top left') | |
| env.viewer_text_overlay('Status','[%s]'%('Running' if running else 'Paused')) | |
| env.viewer_text_overlay('Tick','[%d/%d]'%(tick,L)) | |
| env.viewer_text_overlay('Time','[%.2f]sec'%(time)) | |
| env.plot_global_coordinate_axes() | |
| env.render() | |
| # Close | |
| env.close_viewer() | |
| fig.close() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class StaticPlotWidget(pg.PlotWidget): | |
| """ | |
| Minimal static plotting widget with a matplotlib-like API. | |
| Features: | |
| - Matplotlib-like `plot()` with fmt string (marker + line style). | |
| - Compact, multi-column legend with custom spacing and margins. | |
| - Axis label setters, title setter, x/y limits. | |
| - Vertical indicator bar (single instance) with create/update/remove API. | |
| - "Segment" vertical bar (ymin~ymax only) or infinite vertical line. | |
| Usage: | |
| # Create a static figure window | |
| w = StaticPlotWidget( | |
| title="Demo", | |
| x_label="time (s)", | |
| y_label="position (rad)", | |
| legend=True, | |
| legend_loc="top-right" | |
| ) | |
| # Plot raw data | |
| w.plot(t, y1, fmt='-', color='k', lw=1.0, label='raw') | |
| # Plot smoothed data with markers | |
| w.plot(t, y2, fmt='o-', color=(0.2, 0.5, 0.9), lw=1.5, ms=5, label='smooth') | |
| # Plot anchor points (hollow markers) | |
| w.plot(tx, yx, fmt='o', mfc='none', mec='r', ms=6, label='anchors') | |
| # Legend (multi-column) | |
| w.legend(loc='top-right', ncol=3, font_size=8) | |
| # Vertical bar: create once (infinite mode -> spans current y-view) | |
| w.set_vertical_bar(x=1.25, color=(1, 0, 0, 1), lw=2) | |
| # Later, move it quickly (e.g., in a timer callback) | |
| w.move_vertical_bar(new_time) | |
| # Segment mode (only draw between ymin and ymax) | |
| w.set_vertical_bar(x=2.0, color='c', lw=2, ymin=-1.0, ymax=+1.0) | |
| w.move_vertical_bar(2.5) | |
| # Axis limits | |
| w.set_xlim(0, 10) | |
| w.set_ylim(-2, 2) | |
| # Show window | |
| w.show() | |
| """ | |
| def __init__( | |
| self, | |
| title = "Static Plot", | |
| x_offset = 0, | |
| y_offset = 0, | |
| window_width = 800, | |
| window_height = 500, | |
| x_label = "", | |
| y_label = "", | |
| axis_font_size = 10, | |
| grid = True, | |
| parent = None, | |
| legend = False, | |
| legend_loc = "top-right", # 'top-right','top-left','bottom-right','bottom-left' | |
| legend_offset = (10, 10), | |
| ): | |
| """ | |
| Initialize the widget and optional legend anchor. | |
| Parameters: | |
| title: Window title. | |
| x_offset, y_offset: Window position on screen. | |
| window_width, window_height: Window size in pixels. | |
| x_label, y_label: Axis labels. | |
| axis_font_size: Axis tick font size (pt). | |
| grid: Whether to show major grid lines. | |
| parent: Optional parent widget. | |
| legend: Create legend on init if True. | |
| legend_loc: Legend anchor location. | |
| legend_offset: Legend pixel offset relative to its anchor. | |
| """ | |
| super().__init__(parent=parent) | |
| # Window geometry & title | |
| self.setGeometry(x_offset, y_offset, int(window_width), int(window_height)) | |
| self.title = title | |
| self.setWindowTitle(self.title) | |
| # Axes & grid | |
| self.axis_font_size = axis_font_size | |
| self.plotItem.showGrid(x=grid, y=grid) | |
| axis_style = {"tickFont": QFont("Arial", self.axis_font_size)} | |
| self.plotItem.getAxis("bottom").setStyle(**axis_style) | |
| self.plotItem.getAxis("left").setStyle(**axis_style) | |
| if x_label: | |
| self.set_xlabel(x_label) | |
| if y_label: | |
| self.set_ylabel(y_label) | |
| # State for curves and legend | |
| self._curves = [] | |
| self._legend = None | |
| self._legend_loc = legend_loc | |
| self._legend_offset = legend_offset | |
| self._legend_ncol = 1 | |
| self._legend_entries = [] # list[(curve, name)] | |
| self._legend_names = set() | |
| # State for a single vertical bar | |
| self._vbar_item = None # pg.InfiniteLine or pg.PlotDataItem | |
| self._vbar_mode = None # 'infinite' or 'segment' | |
| self._vbar_range_src = None # 'explicit' or 'view' (segment mode) | |
| self._vbar_ymin = None | |
| self._vbar_ymax = None | |
| if legend: | |
| self.legend(loc=legend_loc, offset=legend_offset, ncol=1) | |
| # ---------------- Public API (matplotlib-like) ---------------- | |
| def plot( | |
| self, | |
| x, | |
| y, | |
| fmt: str = "-", | |
| color=None, | |
| lw: float = 1.5, | |
| label: str = None, | |
| ms: float = 6, | |
| mfc=None, | |
| mec=None, | |
| ): | |
| """ | |
| Add a static curve (and/or markers), similar to matplotlib's plt.plot. | |
| Parameters: | |
| x, y: 1-D data arrays. | |
| fmt: Format string e.g., 'o-', 's--', 'x:', '-', 'o'. | |
| color: '#rrggbb', Qt color name, or (r,g,b[,a]) in [0..1] or [0..255]. | |
| lw: Line width. | |
| label: Legend label (if provided). | |
| ms: Marker size in px. | |
| mfc: Marker face color; use 'none' for hollow markers. | |
| mec: Marker edge color. | |
| Returns: | |
| The created pyqtgraph PlotDataItem. | |
| """ | |
| x = np.asarray(x).ravel() | |
| y = np.asarray(y).ravel() | |
| assert x.shape == y.shape, "Lengths of x and y must match." | |
| style = self._parse_fmt(fmt) | |
| pen, symbol, symbolSize, symbolPen, symbolBrush = self._make_pg_style( | |
| style, color, lw, ms, mfc, mec | |
| ) | |
| curve = self.plotItem.plot( | |
| x=x, | |
| y=y, | |
| pen=pen, | |
| symbol=symbol, | |
| symbolSize=symbolSize, | |
| symbolPen=symbolPen, | |
| symbolBrush=symbolBrush, | |
| name=label if label else None, | |
| ) | |
| self._curves.append(curve) | |
| if label and label not in self._legend_names: | |
| self._legend_names.add(label) | |
| self._legend_entries.append((curve, label)) | |
| if self._legend is not None: | |
| self._rebuild_legend_grid(self._legend_ncol) | |
| return curve | |
| def set_title(self, title: str) -> None: | |
| """Set the plot title.""" | |
| self.title = title | |
| self.plotItem.setTitle(self.title) | |
| def set_xlabel(self, text: str) -> None: | |
| """Set the x-axis label.""" | |
| self.plotItem.setLabel("bottom", text) | |
| def set_ylabel(self, text: str) -> None: | |
| """Set the y-axis label.""" | |
| self.plotItem.setLabel("left", text) | |
| def legend( | |
| self, | |
| loc: str = "top-right", | |
| offset=(10, 10), | |
| clear_existing: bool = True, | |
| ncol: int = 1, | |
| font_size: int = 9, | |
| hspacing: int = 6, | |
| vspacing: int = 2, | |
| margins=(2, 2, 2, 2), | |
| sample_size: int = 10, | |
| ): | |
| """ | |
| Create or move the legend with compact spacing. | |
| Parameters: | |
| loc: 'top-right'|'top-left'|'bottom-right'|'bottom-left' | |
| offset: (x, y) pixel offset. | |
| clear_existing: Remove previous legend item if it exists. | |
| ncol: Number of legend columns (>=1). | |
| font_size: Legend label font size (pt). | |
| hspacing: Horizontal spacing between legend cells (px). | |
| vspacing: Vertical spacing between legend rows (px). | |
| margins: (left, top, right, bottom) content margins (px). | |
| sample_size: Size of line/marker sample box (px). | |
| """ | |
| self._legend_loc = loc | |
| self._legend_offset = offset | |
| self._legend_ncol = max(1, int(ncol)) | |
| self._legend_font_size = int(font_size) | |
| self._legend_hspacing = int(hspacing) | |
| self._legend_vspacing = int(vspacing) | |
| self._legend_margins = tuple(int(x) for x in margins) | |
| self._legend_sample_sz = int(sample_size) | |
| if clear_existing and self._legend is not None: | |
| try: | |
| self._legend.scene().removeItem(self._legend) | |
| except Exception: | |
| pass | |
| self._legend = None | |
| if self._legend is None: | |
| self._legend = pg.LegendItem(offset=offset) | |
| self._legend.setParentItem(self.plotItem) | |
| if loc == "top-left": | |
| self._legend.anchor(itemPos=(0, 0), parentPos=(0, 0)) | |
| elif loc == "bottom-left": | |
| self._legend.anchor(itemPos=(0, 1), parentPos=(0, 1)) | |
| elif loc == "bottom-right": | |
| self._legend.anchor(itemPos=(1, 1), parentPos=(1, 1)) | |
| else: # 'top-right' | |
| self._legend.anchor(itemPos=(1, 0), parentPos=(1, 0)) | |
| # Apply spacing and margins | |
| lay = self._legend.layout | |
| if hasattr(lay, "setHorizontalSpacing"): | |
| lay.setHorizontalSpacing(self._legend_hspacing) | |
| if hasattr(lay, "setVerticalSpacing"): | |
| lay.setVerticalSpacing(self._legend_vspacing) | |
| if hasattr(lay, "setContentsMargins"): | |
| l, t, r, b = self._legend_margins | |
| lay.setContentsMargins(l, t, r, b) | |
| elif hasattr(lay, "setSpacing"): | |
| lay.setSpacing(min(self._legend_hspacing, self._legend_vspacing)) | |
| self._rebuild_legend_grid(self._legend_ncol) | |
| def set_xlim(self, left=None, right=None) -> None: | |
| """ | |
| Set x-limits; pass None to keep the current bound. | |
| Parameters: | |
| left: Left limit (float or None). | |
| right: Right limit (float or None). | |
| """ | |
| r = self.plotItem.viewRange() | |
| ymin, ymax = r[1] | |
| if left is None: | |
| left = r[0][0] | |
| if right is None: | |
| right = r[0][1] | |
| self.plotItem.setRange(xRange=(left, right), yRange=(ymin, ymax), padding=0) | |
| def set_ylim(self, bottom=None, top=None) -> None: | |
| """ | |
| Set y-limits; pass None to keep the current bound. | |
| Parameters: | |
| bottom: Bottom limit (float or None). | |
| top: Top limit (float or None). | |
| """ | |
| r = self.plotItem.viewRange() | |
| xmin, xmax = r[0] | |
| if bottom is None: | |
| bottom = r[1][0] | |
| if top is None: | |
| top = r[1][1] | |
| self.plotItem.setRange(xRange=(xmin, xmax), yRange=(bottom, top), padding=0) | |
| def clear(self, keep_legend: bool = True) -> None: | |
| """ | |
| Clear plotted data. If keep_legend=True, legend entries are kept and re-rendered. | |
| Also resets the vertical bar state. | |
| """ | |
| self.plotItem.clear() | |
| self._curves = [] | |
| if self.title: | |
| self.plotItem.setTitle(self.title) | |
| # Reset vertical bar state | |
| self._vbar_item = None | |
| self._vbar_mode = None | |
| self._vbar_range_src = None | |
| self._vbar_ymin = None | |
| self._vbar_ymax = None | |
| if keep_legend and self._legend is not None: | |
| self._rebuild_legend_grid(self._legend_ncol) | |
| else: | |
| if self._legend is not None: | |
| try: | |
| self._legend.scene().removeItem(self._legend) | |
| except Exception: | |
| pass | |
| self._legend = None | |
| self._legend_entries.clear() | |
| self._legend_names.clear() | |
| # ---------------- Vertical bar API ---------------- | |
| def set_vertical_bar(self, x, color=(1, 0, 0, 1), lw: float = 2, ymin=None, ymax=None): | |
| """ | |
| Create or update a single vertical indicator at x. | |
| Modes: | |
| - Infinite mode (ymin and ymax are None): draws a pg.InfiniteLine, | |
| which spans the current y-view. | |
| - Segment mode (ymin and/or ymax specified): draws only between | |
| [ymin, ymax] using a pg.PlotDataItem. | |
| Repeated calls update the existing bar (no accumulation). | |
| Parameters: | |
| x: X coordinate of the vertical bar. | |
| color: Color spec (#rrggbb, name, or tuple/list). | |
| lw: Line width. | |
| ymin, ymax: Segment vertical bounds; if both None -> infinite mode. | |
| Returns: | |
| The underlying pyqtgraph item (InfiniteLine or PlotDataItem). | |
| """ | |
| pen = pg.mkPen(self._mkColor(color), width=lw) | |
| want_segment = (ymin is not None) or (ymax is not None) | |
| # Infinite mode | |
| if not want_segment: | |
| if isinstance(self._vbar_item, pg.InfiniteLine): | |
| self._vbar_item.setPos(float(x)) | |
| self._vbar_item.setPen(pen) | |
| self._vbar_mode = "infinite" | |
| return self._vbar_item | |
| if self._vbar_item is not None: | |
| try: | |
| self.plotItem.removeItem(self._vbar_item) | |
| except Exception: | |
| pass | |
| self._vbar_item = None | |
| line = pg.InfiniteLine(pos=float(x), angle=90, pen=pen, movable=False) | |
| self.plotItem.addItem(line) | |
| self._vbar_item = line | |
| self._vbar_mode = "infinite" | |
| self._vbar_range_src = None | |
| self._vbar_ymin = None | |
| self._vbar_ymax = None | |
| return line | |
| # Segment mode | |
| _, (cur_ymin, cur_ymax) = self.plotItem.viewRange() | |
| y0 = cur_ymin if ymin is None else float(ymin) | |
| y1 = cur_ymax if ymax is None else float(ymax) | |
| if (self._vbar_item is not None) and (self._vbar_mode == "segment"): | |
| self._vbar_item.setData([x, x], [y0, y1], pen=pen) | |
| # For some pg versions, ensure pen propagation: | |
| if hasattr(self._vbar_item, "curve"): | |
| self._vbar_item.curve.setPen(pen) | |
| self._vbar_ymin, self._vbar_ymax = y0, y1 | |
| self._vbar_range_src = "explicit" if (ymin is not None or ymax is not None) else "view" | |
| return self._vbar_item | |
| if self._vbar_item is not None: | |
| try: | |
| self.plotItem.removeItem(self._vbar_item) | |
| except Exception: | |
| pass | |
| self._vbar_item = None | |
| seg = self.plotItem.plot([x, x], [y0, y1], pen=pen) | |
| self._vbar_item = seg | |
| self._vbar_mode = "segment" | |
| self._vbar_ymin, self._vbar_ymax = y0, y1 | |
| self._vbar_range_src = "explicit" if (ymin is not None or ymax is not None) else "view" | |
| return seg | |
| def move_vertical_bar(self, x): | |
| """ | |
| Move the existing vertical bar to a new x. | |
| If in segment mode and the range source is 'view', the bar tracks the current y-view range. | |
| """ | |
| if self._vbar_item is None: | |
| return None | |
| if self._vbar_mode == "infinite": | |
| self._vbar_item.setPos(float(x)) | |
| else: | |
| if self._vbar_range_src == "view": | |
| _, (cur_ymin, cur_ymax) = self.plotItem.viewRange() | |
| self._vbar_ymin, self._vbar_ymax = cur_ymin, cur_ymax | |
| self._vbar_item.setData([x, x], [self._vbar_ymin, self._vbar_ymax]) | |
| return self._vbar_item | |
| def remove_vertical_bar(self) -> None: | |
| """Remove the vertical bar, if present, and reset its state.""" | |
| if self._vbar_item is not None: | |
| try: | |
| self.plotItem.removeItem(self._vbar_item) | |
| except Exception: | |
| pass | |
| self._vbar_item = None | |
| self._vbar_mode = None | |
| self._vbar_range_src = None | |
| self._vbar_ymin = None | |
| self._vbar_ymax = None | |
| # Backward-compatible alias | |
| def plot_vertical_bar(self, x, color=(1, 0, 0, 1), lw: float = 2, ymin=None, ymax=None): | |
| """Alias for set_vertical_bar().""" | |
| return self.set_vertical_bar(x, color=color, lw=lw, ymin=ymin, ymax=ymax) | |
| # ---------------- Internals ---------------- | |
| def _mkColor(self, c): | |
| """ | |
| Normalize various color specs to pg.mkColor. | |
| Parameters: | |
| c: Color spec (None, tuple/list, name, hex). | |
| Returns: | |
| pg.mkColor-compatible object or None. | |
| """ | |
| if c is None: | |
| return None | |
| if isinstance(c, (tuple, list)): | |
| # Support 0..1 tuples by scaling to 0..255 | |
| if len(c) > 0 and max(float(v) for v in c) <= 1.0: | |
| c = tuple(int(round(255 * float(v))) for v in c) | |
| return pg.mkColor(c) | |
| def _parse_fmt(self, fmt: str): | |
| """ | |
| Parse a matplotlib-like fmt string into (marker, linestyle). | |
| Parameters: | |
| fmt: Format string (e.g., 'o-', 's--', 'x:', '-', 'o'). | |
| Returns: | |
| dict with keys: | |
| 'marker': supported marker or None | |
| 'linestyle': one of {'-', '--', '-.', ':', None} | |
| """ | |
| fmt = fmt or "" | |
| # Line styles | |
| linestyle = None | |
| if "--" in fmt: | |
| linestyle = "--" | |
| elif "-." in fmt: | |
| linestyle = "-." | |
| elif ":" in fmt: | |
| linestyle = ":" | |
| elif "-" in fmt: | |
| linestyle = "-" | |
| # Markers (first match) | |
| supported_markers = ['o', 's', 'd', '^', 'v', '<', '>', 'x', '+', '*', 'p', 'h', 't'] | |
| marker = None | |
| for m in supported_markers: | |
| if m in fmt: | |
| marker = m | |
| break | |
| return {"marker": marker, "linestyle": linestyle} | |
| def _qt_pen_style(self, linestyle: str): | |
| """ | |
| Map matplotlib-like line style to Qt pen style. | |
| Parameters: | |
| linestyle: '-', '--', '-.', ':', or None. | |
| Returns: | |
| Qt.PenStyle | |
| """ | |
| if linestyle == "-": | |
| return Qt.SolidLine | |
| if linestyle == "--": | |
| return Qt.DashLine | |
| if linestyle == "-.": | |
| return Qt.DashDotLine | |
| if linestyle == ":": | |
| return Qt.DotLine | |
| return Qt.SolidLine | |
| def _marker_to_pg_symbol(self, m: str): | |
| """ | |
| Map matplotlib-like marker char to pyqtgraph symbol string. | |
| Parameters: | |
| m: Marker character. | |
| Returns: | |
| pyqtgraph symbol string or None. | |
| """ | |
| if m is None: | |
| return None | |
| mapping = { | |
| "o": "o", # circle | |
| "s": "s", # square | |
| "d": "d", # diamond | |
| "^": "^", # triangle up | |
| "v": "v", # triangle down | |
| "<": "<", # triangle left | |
| ">": ">", # triangle right | |
| "x": "x", # x | |
| "+": "+", # plus | |
| "*": "star", # star | |
| "p": "p", # pentagon | |
| "h": "h", # hexagon | |
| "t": "t", # triangle (generic) | |
| } | |
| return mapping.get(m, None) | |
| def _make_pg_style(self, style, color, lw, ms, mfc, mec): | |
| """ | |
| Build pyqtgraph pen/symbol styles from parsed fmt and colors. | |
| Parameters: | |
| style: Parsed style dict from _parse_fmt(). | |
| color: Color spec for line and default marker colors. | |
| lw: Line width. | |
| ms: Marker size (px). | |
| mfc: Marker face color. | |
| mec: Marker edge color. | |
| Returns: | |
| (pen, symbol, symbolSize, symbolPen, symbolBrush) | |
| """ | |
| # Line | |
| pen = None | |
| qcolor = self._mkColor(color) if color is not None else pg.mkColor("#000000") | |
| if style["linestyle"] is not None: | |
| pen = pg.mkPen(color=qcolor, width=lw, style=self._qt_pen_style(style["linestyle"])) | |
| # Marker | |
| symbol = None | |
| symbolSize = 0 | |
| symbolPen = None | |
| symbolBrush = None | |
| if style["marker"] is not None: | |
| symbol = self._marker_to_pg_symbol(style["marker"]) | |
| symbolSize = ms | |
| edge_color = self._mkColor(mec) if mec is not None else qcolor | |
| symbolPen = pg.mkPen(edge_color, width=max(1, lw * 0.8)) | |
| if mfc is None: | |
| symbolBrush = qcolor | |
| elif mfc == "none": | |
| symbolBrush = None | |
| else: | |
| symbolBrush = self._mkColor(mfc) | |
| return pen, symbol, symbolSize, symbolPen, symbolBrush | |
| def _rebuild_legend_grid(self, ncol: int) -> None: | |
| """ | |
| Rebuild legend layout as an n-column grid. | |
| Each entry consumes two columns (sample, label). | |
| Parameters: | |
| ncol: Number of legend columns. | |
| """ | |
| if self._legend is None: | |
| return | |
| layout = self._legend.layout | |
| # Clear existing items | |
| for it in list(getattr(self._legend, "items", [])): | |
| try: | |
| layout.removeItem(it[0]) | |
| layout.removeItem(it[1]) | |
| except Exception: | |
| pass | |
| self._legend.items = [] | |
| # Reapply spacing/margins | |
| if hasattr(layout, "setHorizontalSpacing"): | |
| layout.setHorizontalSpacing(self._legend_hspacing) | |
| if hasattr(layout, "setVerticalSpacing"): | |
| layout.setVerticalSpacing(self._legend_vspacing) | |
| if hasattr(layout, "setContentsMargins"): | |
| l, t, r, b = self._legend_margins | |
| layout.setContentsMargins(l, t, r, b) | |
| elif hasattr(layout, "setSpacing"): | |
| layout.setSpacing(min(self._legend_hspacing, self._legend_vspacing)) | |
| # Add entries in grid | |
| for idx, (curve, name) in enumerate(self._legend_entries): | |
| row = idx // ncol | |
| col = idx % ncol | |
| sample = _ItemSample(curve) | |
| if hasattr(sample, "setFixedWidth"): | |
| sample.setFixedWidth(self._legend_sample_sz) | |
| if hasattr(sample, "setFixedHeight"): | |
| sample.setFixedHeight(self._legend_sample_sz) | |
| label = _LabelItem(name) | |
| try: | |
| label.setText(name, size=f"{self._legend_font_size}pt") | |
| except Exception: | |
| pass | |
| sample.setParentItem(self._legend) | |
| label.setParentItem(self._legend) | |
| layout.addItem(sample, row, 2 * col) | |
| layout.addItem(label, row, 2 * col + 1) | |
| self._legend.items.append((sample, label)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment