"""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._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_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()