Source code for wolfhece.assets.boxplot.editor

"""Graphical editor for boxplot chart asset configuration.

Provides a wx.Frame-based UI for interactively editing multi-series boxplot
data and display options.  Changes are applied to a BoxplotZonesController
and reflected in real-time on the map.
"""

from __future__ import annotations

import numpy as np
import wx
import wx.grid as wxgrid
import wx.propgrid as pg

from ...CpGrid import CpGrid
from ...PyParams import Type_Param, Wolf_Param
from .zones_asset import BoxplotZonesAsset
from .controller import BoxplotZonesController


[docs] GROUP_CANVAS = "Boxplot - Canvas"
[docs] GROUP_AREA = "Boxplot - Plot area"
[docs] GROUP_PLOT = "Boxplot - Plot options"
[docs] GROUP_STYLE = "Boxplot - Style"
[docs] GROUP_COLORS = "Boxplot - Colors"
[docs] class BoxplotZonesEditor(wx.Frame): """wx editor window for BoxplotZonesController. Grid has one row per data series (Label | Values | Color | Count). The Wolf_Param panel controls canvas geometry, area fractions, plot options, and per-series colours/alpha. """ def __init__(self, parent, controller: BoxplotZonesController): super().__init__(parent, title=f"Boxplot Editor - {controller.id}", size=(1020, 640))
[docs] self.controller = controller
[docs] self._updating_ui = False
[docs] self._colors: list[tuple[int, int, int, int]] = []
panel = wx.Panel(self) main = wx.BoxSizer(wx.VERTICAL) splitter = wx.SplitterWindow(panel, style=wx.SP_LIVE_UPDATE | wx.SP_3D) left_panel = wx.Panel(splitter) right_panel = wx.Panel(splitter) left = wx.BoxSizer(wx.VERTICAL) right = wx.BoxSizer(wx.VERTICAL) # Property grid
[docs] self._wp = Wolf_Param( right_panel, to_read=False, withbuttons=False, init_GUI=False, force_even_if_same_default=True, )
self._wp.ensure_prop(wxparent=right_panel, show_in_active_if_default=True, height=300) self._init_wolf_param_schema() if self._wp.prop is not None: self._wp.prop.SetMinSize((400, -1))
[docs] self._live = wx.CheckBox(right_panel, label="Live update")
self._live.SetValue(True) # Data grid (one row per series)
[docs] self._grid = CpGrid(left_panel, wx.ID_ANY, wx.WANTS_CHARS)
self._grid.CreateGrid(0, 4) self._grid.SetColLabelValue(0, "Label") self._grid.SetColLabelValue(1, "Values (comma-separated)") self._grid.SetColLabelValue(2, "Color") self._grid.SetColLabelValue(3, "Count") self._grid.EnableEditing(True) self._grid.SetColSize(1, 340) # Color picker row colorbar = wx.BoxSizer(wx.HORIZONTAL)
[docs] self._transform_map = wx.Button(right_panel, label="Transform on map")
[docs] self._colorpicker = wx.ColourPickerCtrl(right_panel, colour=wx.Colour(41, 98, 255))
[docs] self._alpha = wx.SpinCtrl(right_panel, min=0, max=255, initial=180, size=(80, -1))
colorbar.Add(self._transform_map, 0, wx.RIGHT, 12) colorbar.Add(wx.StaticText(right_panel, label="Selected row color"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 6) colorbar.Add(self._colorpicker, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 8) colorbar.Add(wx.StaticText(right_panel, label="Alpha"), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 4) colorbar.Add(self._alpha, 0, wx.ALIGN_CENTER_VERTICAL) # Buttons btns = wx.BoxSizer(wx.HORIZONTAL)
[docs] self._add = wx.Button(left_panel, label="Row+")
[docs] self._del = wx.Button(left_panel, label="Row-")
[docs] self._apply = wx.Button(left_panel, label="Apply")
[docs] self._save_json = wx.Button(left_panel, label="Save JSON")
[docs] self._load_json = wx.Button(left_panel, label="Load JSON")
for b in (self._add, self._del, self._apply, self._save_json, self._load_json): btns.Add(b, 0, wx.RIGHT, 6) left.Add(self._grid, 1, wx.EXPAND | wx.ALL, 8) left.Add(btns, 0, wx.ALL, 8) if self._wp.prop is not None: right.Add(self._wp.prop, 1, wx.EXPAND | wx.ALL, 8) right.Add(self._live, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 8) right.Add(colorbar, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8) left_panel.SetSizer(left) right_panel.SetSizer(right) right_panel.SetMinSize((420, -1)) splitter.SplitVertically(left_panel, right_panel, sashPosition=580) splitter.SetSashGravity(1.0) splitter.SetMinimumPaneSize(300) main.Add(splitter, 1, wx.EXPAND) panel.SetSizer(main) self._bind_events() self.refresh_from_controller() # ------------------------------------------------------------------ # Event binding # ------------------------------------------------------------------
[docs] def _bind_events(self) -> None: self._transform_map.Bind(wx.EVT_BUTTON, self.on_transform_map) self._add.Bind(wx.EVT_BUTTON, self.on_add_row) self._del.Bind(wx.EVT_BUTTON, self.on_del_row) self._apply.Bind(wx.EVT_BUTTON, self.on_apply) self._save_json.Bind(wx.EVT_BUTTON, self.on_save_json) self._load_json.Bind(wx.EVT_BUTTON, self.on_load_json) self._grid.Bind(wxgrid.EVT_GRID_CELL_CHANGED, self.on_grid_changed) self._grid.Bind(wxgrid.EVT_GRID_SELECT_CELL, self.on_grid_select) self._grid.Bind(wxgrid.EVT_GRID_EDITOR_HIDDEN, self.on_grid_editor_hidden) self._colorpicker.Bind(wx.EVT_COLOURPICKER_CHANGED, self.on_picker_changed) self._alpha.Bind(wx.EVT_SPINCTRL, self.on_picker_changed) if self._wp.prop is not None: self._wp.prop.Bind(pg.EVT_PG_CHANGED, self.on_prop_changed)
# ------------------------------------------------------------------ # Wolf_Param schema # ------------------------------------------------------------------
[docs] def _init_wolf_param_schema(self) -> None: wp = self._wp # Canvas geometry wp.add_param(GROUP_CANVAS, "X", 0.0, Type_Param.Float, "Canvas origin X", whichdict="All") wp.add_param(GROUP_CANVAS, "Y", 0.0, Type_Param.Float, "Canvas origin Y", whichdict="All") wp.add_param(GROUP_CANVAS, "Width", 100.0, Type_Param.Float, "Canvas width", whichdict="All") wp.add_param(GROUP_CANVAS, "Height", 80.0, Type_Param.Float, "Canvas height", whichdict="All") # Plot area fractions (0..1) wp.add_param(GROUP_AREA, "Left frac", 0.10, Type_Param.Float, "Left margin fraction [0-1]", whichdict="All") wp.add_param(GROUP_AREA, "Bottom frac", 0.08, Type_Param.Float, "Bottom margin fraction [0-1]", whichdict="All") wp.add_param(GROUP_AREA, "Right frac", 0.92, Type_Param.Float, "Right margin fraction [0-1]", whichdict="All") wp.add_param(GROUP_AREA, "Top frac", 0.88, Type_Param.Float, "Top margin fraction [0-1]", whichdict="All") # Plot options wp.add_param(GROUP_PLOT, "Y range mode", "auto", Type_Param.Enum, "Y axis range mode", enum_choices=["auto", "fixed"], whichdict="All") wp.add_param(GROUP_PLOT, "Y min", 0.0, Type_Param.Float, "Y minimum (fixed mode only)", whichdict="All") wp.add_param(GROUP_PLOT, "Y max", 1.0, Type_Param.Float, "Y maximum (fixed mode only)", whichdict="All") wp.add_param(GROUP_PLOT, "Whisker coeff", 1.5, Type_Param.Float, "Whisker coefficient (IQR multiplier)", whichdict="All") wp.add_param(GROUP_PLOT, "Box width fraction", 0.55, Type_Param.Float, "Box width as fraction of slot width", whichdict="All") wp.add_param(GROUP_PLOT, "Show mean", "false", Type_Param.Enum, "Show mean diamond marker", enum_choices=["false", "true"], whichdict="All") wp.add_param(GROUP_PLOT, "Show outliers", "true", Type_Param.Enum, "Show outlier points", enum_choices=["false", "true"], whichdict="All") wp.add_param(GROUP_PLOT, "Show labels", "true", Type_Param.Enum, "Show series labels below boxplots", enum_choices=["false", "true"], whichdict="All") wp.add_param(GROUP_PLOT, "Show frame", "true", Type_Param.Enum, "Show canvas/area frames", enum_choices=["false", "true"], whichdict="All") # Style wp.add_param(GROUP_STYLE, "Median color", (20, 20, 20), Type_Param.Color, "Median line color", whichdict="All") wp.add_param(GROUP_STYLE, "Border color", (25, 25, 25), Type_Param.Color, "Box border color", whichdict="All") wp.add_param(GROUP_STYLE, "Frame color", (40, 40, 40), Type_Param.Color, "Frame color", whichdict="All") wp.add_param(GROUP_STYLE, "Legend text color", (0, 0, 0), Type_Param.Color, "Series label color", whichdict="All") wp.add_param(GROUP_STYLE, "Box line width", 1.0, Type_Param.Float, "Box border line width", whichdict="All") wp.add_param(GROUP_STYLE, "Whisker line width", 1.0, Type_Param.Float, "Whisker/cap line width", whichdict="All") wp.add_param(GROUP_STYLE, "Median line width", 2.0, Type_Param.Float, "Median line width", whichdict="All") wp.add_param(GROUP_STYLE, "Frame line width", 1.0, Type_Param.Float, "Canvas/area frame width", whichdict="All")
[docs] def _purge_color_params(self) -> None: for dict_name in ("myparams", "myparams_default"): d = getattr(self._wp, dict_name, None) if not isinstance(d, dict): continue grp = d.get(GROUP_COLORS) if not isinstance(grp, dict): continue for name in list(grp.keys()): if str(name).startswith("Series "): del grp[name]
@staticmethod
[docs] def _color_key(idx: int) -> str: return f"Series {idx} Color"
@staticmethod
[docs] def _alpha_key(idx: int) -> str: return f"Series {idx} Alpha"
[docs] def _sync_color_param_schema(self, count: int) -> None: self._purge_color_params() for i in range(count): idx = i + 1 self._wp.add_param( GROUP_COLORS, self._color_key(idx), (0, 0, 0), Type_Param.Color, f"Series {idx} color", whichdict="All", ) self._wp.add_param( GROUP_COLORS, self._alpha_key(idx), 180, Type_Param.Integer, f"Series {idx} alpha [0-255]", whichdict="All", )
[docs] def _set_wp_colors(self, colors: list[tuple[int, int, int, int]]) -> None: for i, rgba in enumerate(colors): idx = i + 1 self._wp[(GROUP_COLORS, self._color_key(idx))] = (int(rgba[0]), int(rgba[1]), int(rgba[2])) self._wp[(GROUP_COLORS, self._alpha_key(idx))] = int(max(0, min(255, rgba[3])))
[docs] def _read_wp_colors(self, count: int) -> list[tuple[int, int, int, int]]: out: list[tuple[int, int, int, int]] = [] for i in range(count): idx = i + 1 rgb = tuple(int(v) for v in self._wp[(GROUP_COLORS, self._color_key(idx))]) a = int(self._wp[(GROUP_COLORS, self._alpha_key(idx))]) out.append(( max(0, min(255, rgb[0])), max(0, min(255, rgb[1])), max(0, min(255, rgb[2])), max(0, min(255, a)), )) return out
[docs] def _colors_from_controller(self, count: int) -> list[tuple[int, int, int, int]]: colors: list[tuple[int, int, int, int]] = [] for i in range(count): if self.controller.colors and i < len(self.controller.colors): c = self.controller.colors[i] if len(c) == 3: colors.append((int(c[0]), int(c[1]), int(c[2]), 180)) else: colors.append((int(c[0]), int(c[1]), int(c[2]), int(c[3]))) else: colors.append(tuple(BoxplotZonesAsset.DEFAULT_COLORS[i % len(BoxplotZonesAsset.DEFAULT_COLORS)])) return colors
[docs] def _sync_wp_colors_to_grid(self) -> None: rows = self._grid.GetNumberRows() if rows <= 0: return try: colors = self._read_wp_colors(rows) except Exception: return for i, rgba in enumerate(colors): self._set_row_color(i, rgba) cur = self._grid.GetGridCursorRow() if cur >= 0: self._sync_picker_from_row(min(cur, rows - 1))
# ------------------------------------------------------------------ # Grid helpers # ------------------------------------------------------------------
[docs] def _color_text(self, rgba: tuple) -> str: return f"{rgba[0]},{rgba[1]},{rgba[2]},{rgba[3]}"
[docs] def _set_row_color(self, row: int, rgba: tuple) -> None: if row < 0: return while len(self._colors) <= row: base = BoxplotZonesAsset.DEFAULT_COLORS[len(self._colors) % len(BoxplotZonesAsset.DEFAULT_COLORS)] self._colors.append(tuple(base)) rgba = tuple(max(0, min(255, int(v))) for v in rgba) self._colors[row] = rgba self._grid.SetCellValue(row, 2, self._color_text(rgba)) self._grid.SetReadOnly(row, 2, True)
[docs] def _sync_picker_from_row(self, row: int) -> None: if row < 0 or row >= len(self._colors): return rgba = self._colors[row] self._updating_ui = True try: self._colorpicker.SetColour(wx.Colour(rgba[0], rgba[1], rgba[2])) self._alpha.SetValue(int(rgba[3])) finally: self._updating_ui = False
@staticmethod
[docs] def _parse_values(raw: str) -> list[float]: """Parse comma-/space-separated floats from a grid cell.""" parts = raw.replace(",", " ").split() out = [] for p in parts: p = p.strip() if p: out.append(float(p)) return out
[docs] def _collect_grid_data(self) -> tuple[list[str], list[list[float]], list[tuple[int, int, int, int]]]: n = self._grid.GetNumberRows() labels: list[str] = [] series: list[list[float]] = [] colors: list[tuple[int, int, int, int]] = [] for i in range(n): label = self._grid.GetCellValue(i, 0).strip() or f"Series {i + 1}" raw = self._grid.GetCellValue(i, 1).strip() try: vals = self._parse_values(raw) if not vals: vals = [0.0, 0.5, 1.0, 1.5, 2.0] # fallback non-empty except Exception: vals = [0.0, 0.5, 1.0, 1.5, 2.0] col = self._colors[i] if i < len(self._colors) else BoxplotZonesAsset.DEFAULT_COLORS[i % len(BoxplotZonesAsset.DEFAULT_COLORS)] labels.append(label) series.append(vals) colors.append(col) return labels, series, colors
[docs] def _read_wp(self) -> dict: self._wp.apply_changes_to_memory(verbosity=False) return { "x": float(self._wp[(GROUP_CANVAS, "X")]), "y": float(self._wp[(GROUP_CANVAS, "Y")]), "width": float(self._wp[(GROUP_CANVAS, "Width")]), "height": float(self._wp[(GROUP_CANVAS, "Height")]), "fx0": float(self._wp[(GROUP_AREA, "Left frac")]), "fy0": float(self._wp[(GROUP_AREA, "Bottom frac")]), "fx1": float(self._wp[(GROUP_AREA, "Right frac")]), "fy1": float(self._wp[(GROUP_AREA, "Top frac")]), "y_range_mode": str(self._wp[(GROUP_PLOT, "Y range mode")]), "y_min": float(self._wp[(GROUP_PLOT, "Y min")]), "y_max": float(self._wp[(GROUP_PLOT, "Y max")]), "whis": float(self._wp[(GROUP_PLOT, "Whisker coeff")]), "box_width_fraction":float(self._wp[(GROUP_PLOT, "Box width fraction")]), "show_mean": str(self._wp[(GROUP_PLOT, "Show mean")]) == "true", "show_outliers": str(self._wp[(GROUP_PLOT, "Show outliers")]) == "true", "show_labels": str(self._wp[(GROUP_PLOT, "Show labels")]) == "true", "show_frame": str(self._wp[(GROUP_PLOT, "Show frame")]) == "true", "median_color": tuple(int(v) for v in self._wp[(GROUP_STYLE, "Median color")])[:3], "border_color": tuple(int(v) for v in self._wp[(GROUP_STYLE, "Border color")])[:3], "frame_color": tuple(int(v) for v in self._wp[(GROUP_STYLE, "Frame color")])[:3], "legend_text_color": tuple(int(v) for v in self._wp[(GROUP_STYLE, "Legend text color")])[:3], "box_line_width": float(self._wp[(GROUP_STYLE, "Box line width")]), "whisker_line_width": float(self._wp[(GROUP_STYLE, "Whisker line width")]), "median_line_width": float(self._wp[(GROUP_STYLE, "Median line width")]), "frame_line_width": float(self._wp[(GROUP_STYLE, "Frame line width")]), }
# ------------------------------------------------------------------ # Apply / refresh # ------------------------------------------------------------------
[docs] def _apply_controller(self) -> None: try: if self._grid.IsCellEditControlShown(): self._grid.SaveEditControlValue() self._grid.HideCellEditControl() except Exception: pass cfg = self._read_wp() labels, series, grid_colors = self._collect_grid_data() try: colors = self._read_wp_colors(len(series)) except Exception: colors = grid_colors # Canvas self.controller.canvas_origin_x = cfg["x"] self.controller.canvas_origin_y = cfg["y"] self.controller.canvas_size_w = max(1e-9, cfg["width"]) self.controller.canvas_size_h = max(1e-9, cfg["height"]) self.controller.area_fraction = (cfg["fx0"], cfg["fy0"], cfg["fx1"], cfg["fy1"]) # Plot options self.controller.y_range_mode = cfg["y_range_mode"] self.controller.y_min = cfg["y_min"] if cfg["y_range_mode"] == "fixed" else None self.controller.y_max = cfg["y_max"] if cfg["y_range_mode"] == "fixed" else None self.controller.whis = cfg["whis"] self.controller.box_width_fraction = cfg["box_width_fraction"] self.controller.show_mean = cfg["show_mean"] self.controller.show_outliers = cfg["show_outliers"] self.controller.show_labels = cfg["show_labels"] self.controller.show_area_frame = cfg["show_frame"] # Style self.controller.median_color = cfg["median_color"] self.controller.border_color = cfg["border_color"] self.controller.frame_color = cfg["frame_color"] self.controller.legend_text_color = cfg["legend_text_color"] self.controller.box_line_width = cfg["box_line_width"] self.controller.whisker_line_width = cfg["whisker_line_width"] self.controller.median_line_width = cfg["median_line_width"] self.controller.frame_line_width = cfg["frame_line_width"] # Data self.controller.series = series self.controller.labels = labels self.controller.colors = [list(c) for c in colors] self.controller.rebuild(ToCheck=True) self.refresh_from_controller()
[docs] def refresh_from_controller(self) -> None: """Reload all UI state from controller.""" self._updating_ui = True try: c = self.controller # Canvas self._wp[(GROUP_CANVAS, "X")] = c.canvas_origin_x self._wp[(GROUP_CANVAS, "Y")] = c.canvas_origin_y self._wp[(GROUP_CANVAS, "Width")] = c.canvas_size_w self._wp[(GROUP_CANVAS, "Height")] = c.canvas_size_h # Area self._wp[(GROUP_AREA, "Left frac")] = c.area_fraction[0] self._wp[(GROUP_AREA, "Bottom frac")] = c.area_fraction[1] self._wp[(GROUP_AREA, "Right frac")] = c.area_fraction[2] self._wp[(GROUP_AREA, "Top frac")] = c.area_fraction[3] # Plot options self._wp[(GROUP_PLOT, "Y range mode")] = c.y_range_mode self._wp[(GROUP_PLOT, "Y min")] = c.y_min if c.y_min is not None else 0.0 self._wp[(GROUP_PLOT, "Y max")] = c.y_max if c.y_max is not None else 1.0 self._wp[(GROUP_PLOT, "Whisker coeff")] = c.whis self._wp[(GROUP_PLOT, "Box width fraction")]= c.box_width_fraction self._wp[(GROUP_PLOT, "Show mean")] = "true" if c.show_mean else "false" self._wp[(GROUP_PLOT, "Show outliers")] = "true" if c.show_outliers else "false" self._wp[(GROUP_PLOT, "Show labels")] = "true" if c.show_labels else "false" self._wp[(GROUP_PLOT, "Show frame")] = "true" if c.show_area_frame else "false" # Style self._wp[(GROUP_STYLE, "Median color")] = c.median_color self._wp[(GROUP_STYLE, "Border color")] = c.border_color self._wp[(GROUP_STYLE, "Frame color")] = c.frame_color self._wp[(GROUP_STYLE, "Legend text color")] = c.legend_text_color self._wp[(GROUP_STYLE, "Box line width")] = c.box_line_width self._wp[(GROUP_STYLE, "Whisker line width")] = c.whisker_line_width self._wp[(GROUP_STYLE, "Median line width")] = c.median_line_width self._wp[(GROUP_STYLE, "Frame line width")] = c.frame_line_width n = len(c.series) base_colors = self._colors_from_controller(n) self._sync_color_param_schema(n) self._set_wp_colors(base_colors) self._wp.Populate(sorted_groups=False) # Resize grid cur = self._grid.GetNumberRows() if cur < n: self._grid.AppendRows(n - cur) elif cur > n: self._grid.DeleteRows(0, cur - n) self._colors = [] labels = c.labels if c.labels is not None else [f"Series {i + 1}" for i in range(n)] for i in range(n): label = labels[i] if i < len(labels) else f"Series {i + 1}" vals_str = ", ".join(str(v) for v in c.series[i]) self._grid.SetCellValue(i, 0, label) self._grid.SetCellValue(i, 1, vals_str) self._set_row_color(i, base_colors[i]) self._grid.SetCellValue(i, 3, str(len(c.series[i]))) self._grid.SetReadOnly(i, 3, True) if n > 0: self._sync_picker_from_row(0) finally: self._updating_ui = False
[docs] def _maybe_live(self) -> None: if self._updating_ui: return if self._live.GetValue(): try: self._apply_controller() except Exception: pass
# ------------------------------------------------------------------ # Event handlers # ------------------------------------------------------------------
[docs] def on_prop_changed(self, event): self._sync_wp_colors_to_grid() self._maybe_live() event.Skip()
[docs] def on_grid_changed(self, event): row = event.GetRow() col = event.GetCol() # Keep Count column up to date when Values column is edited. if col == 1: raw = self._grid.GetCellValue(row, 1).strip() try: n = len(self._parse_values(raw)) except Exception: n = 0 self._grid.SetCellValue(row, 3, str(n)) self._grid.SetReadOnly(row, 3, True) self._maybe_live() event.Skip()
[docs] def on_grid_select(self, event): self._sync_picker_from_row(event.GetRow()) event.Skip()
[docs] def on_grid_editor_hidden(self, event): wx.CallAfter(self._grid.SetFocus) event.Skip()
[docs] def on_picker_changed(self, event): if self._updating_ui: event.Skip() return row = self._grid.GetGridCursorRow() if row < 0 or row >= self._grid.GetNumberRows(): event.Skip() return c = self._colorpicker.GetColour() self._set_row_color(row, (c.Red(), c.Green(), c.Blue(), int(self._alpha.GetValue()))) self._maybe_live() event.Skip()
[docs] def on_transform_map(self, event): mv = self.controller.mapviewer if mv is None: wx.MessageBox("Boxplot is not attached to a mapviewer", "Boxplot Editor", wx.OK | wx.ICON_WARNING) event.Skip() return mv._assets.transform_controller = self.controller mv._assets.transform_editor = self mv.start_action('transform asset bounds', 'Transform asset bounds on map') wx.MessageBox( "Left-click a handle (corners/sides/center), then drag to move/resize the asset.\n" "Shift: keep ratio (corners) | Ctrl: resize from center | Alt: enable snap.", "Boxplot Editor", wx.OK | wx.ICON_INFORMATION, ) event.Skip()
[docs] def on_add_row(self, event): self._grid.AppendRows(1) r = self._grid.GetNumberRows() - 1 base = BoxplotZonesAsset.DEFAULT_COLORS[r % len(BoxplotZonesAsset.DEFAULT_COLORS)] self._grid.SetCellValue(r, 0, f"Series {r + 1}") self._grid.SetCellValue(r, 1, "0, 1, 2, 3, 4") self._set_row_color(r, base) self._grid.SetCellValue(r, 3, "5") self._grid.SetReadOnly(r, 3, True) colors = self._colors_from_controller(self._grid.GetNumberRows()) colors[r] = tuple(base) self._sync_color_param_schema(self._grid.GetNumberRows()) self._set_wp_colors(colors) self._wp.Populate(sorted_groups=False) self._sync_picker_from_row(r) self._maybe_live() event.Skip()
[docs] def on_del_row(self, event): if self._grid.GetNumberRows() > 1: r = self._grid.GetNumberRows() - 1 self._grid.DeleteRows(r, 1) if r < len(self._colors): self._colors.pop(r) colors = list(self._colors) self._sync_color_param_schema(self._grid.GetNumberRows()) self._set_wp_colors(colors) self._wp.Populate(sorted_groups=False) cur = self._grid.GetGridCursorRow() self._sync_picker_from_row(max(0, min(cur, self._grid.GetNumberRows() - 1))) self._maybe_live() event.Skip()
[docs] def on_apply(self, event): try: self._apply_controller() except Exception as exc: wx.MessageBox(str(exc), "Boxplot Editor", wx.OK | wx.ICON_ERROR) event.Skip()
[docs] def on_save_json(self, event): dlg = wx.FileDialog( self, "Save boxplot JSON", wildcard="JSON (*.json)|*.json", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, ) if dlg.ShowModal() == wx.ID_OK: try: self._apply_controller() self.controller.save_json(dlg.GetPath()) except Exception as exc: wx.MessageBox(str(exc), "Boxplot Editor", wx.OK | wx.ICON_ERROR) dlg.Destroy() event.Skip()
[docs] def on_load_json(self, event): dlg = wx.FileDialog( self, "Load boxplot JSON", wildcard="JSON (*.json)|*.json", style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST, ) if dlg.ShowModal() == wx.ID_OK: try: loaded = BoxplotZonesController.load_json(dlg.GetPath()) loaded.mapviewer = self.controller.mapviewer loaded.editor = self self.controller = loaded self.controller.rebuild(ToCheck=True) self.refresh_from_controller() except Exception as exc: wx.MessageBox(str(exc), "Boxplot Editor", wx.OK | wx.ICON_ERROR) dlg.Destroy() event.Skip()