Source code for wolfhece.matplotlib_fig

from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavigationToolbar
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from typing import Literal
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
import numpy as np
from matplotlib.axes import Axes
from wolfhece.CpGrid import CpGrid
from wolfhece.PyParams import Wolf_Param, new_json
from wolfhece.PyTranslate import _
from wolfhece.PyVertex import getRGBfromI


import wx
from matplotlib.backend_bases import KeyEvent, MouseEvent
from matplotlib.lines import Line2D


import logging
import json
from pathlib import Path
from enum import Enum


[docs] def sanitize_fmt(fmt): """ Sanitizes the given format string for numerical formatting. This function ensures that the format string is in a valid format for floating-point number representation. If the input format string is 'None' or an empty string, it defaults to '.2f'. Otherwise, it ensures that the format string contains a decimal point and ends with 'f' for floating-point representation. If the format string is '.f', it defaults to '.2f'. :param fmt: The format string to be sanitized. :type fmt: str :return: A sanitized format string suitable for floating-point number formatting. :rtype: str """ if fmt in ['None', '']: return '.2f' else: if not '.' in fmt: fmt = '.' + fmt if not 'f' in fmt: fmt = fmt + 'f' if fmt == '.f': fmt = '.2f' return fmt
[docs] class Matplotlib_ax_properties(): def __init__(self, ax =None) -> None: self._ax = ax self._myprops = None self._lines:list[Matplolib_line_properties] = [] self._tmp_line_prop:Matplolib_line_properties = None self._selected_line = -1 self.title = 'Figure' self.xtitle = 'X [m]' self.ytitle = 'Y [m]' self.legend = False self.xmin = 0. self.xmax = 1. self.ymin = 0. self.ymax = 1. self.gridx_major = False self.gridy_major = False self.gridx_minor = False self.gridy_minor = False self._equal_axis = 0 self.scaling_factor = 1. self.ticks_x = 1. self.ticks_y = 1. self.ticks_label_x = 1. self.ticks_label_y = 1. self.format_x = '.2f' self.format_y = '.2f' self._set_props() @property
[docs] def is_equal(self): if self._equal_axis == 1: return 'equal' elif self._equal_axis == 0: return 'auto' else: return self.scaling_factor
[docs] def reset_selection(self): if self._selected_line>=0: for curline in self._lines: curline.selected = False self._selected_line = -1
[docs] def select_line(self, idx:int): if self._selected_line>=0: self.reset_selection() if idx>=0 and idx<len(self._lines): self._selected_line = idx self._lines[idx].selected = True
[docs] def set_ax(self, ax:Axes): self._ax = ax if ax is None: return self.get_properties() self._lines = [Matplolib_line_properties(line, self) for line in ax.get_lines()] return self
[docs] def _set_props(self): """ Set the properties UI """ if self._myprops is not None: return self._myprops = Wolf_Param(title='Figure properties', w= 500, h= 400, to_read= False, ontop= False, init_GUI= False) self._myprops.set_callbacks(None, self.destroyprop) # self._myprops.hide_selected_buttons() # only 'Apply' button self._myprops.addparam('Draw','Title',self.title,'String','Title') self._myprops.addparam('Draw','X title',self.xtitle,'String','X title') self._myprops.addparam('Draw','Y title',self.ytitle,'String','Y title') self._myprops.addparam('Draw','Legend',self.legend,'Logical','Legend') self._myprops.addparam('Bounds','X min',self.xmin,'Float','X min') self._myprops.addparam('Bounds','X max',self.xmax,'Float','X max') self._myprops.addparam('Bounds','Y min',self.ymin,'Float','Y min') self._myprops.addparam('Bounds','Y max',self.ymax,'Float','Y max') self._myprops.addparam('Ticks X','Positions',self.ticks_x,'String','X ticks') self._myprops.addparam('Ticks X','Labels',self.ticks_label_x,'String','X ticks labels') self._myprops.addparam('Ticks Y','Positions',self.ticks_y,'String','Y ticks') self._myprops.addparam('Ticks Y','Labels',self.ticks_label_y,'String','Y ticks labels') self._myprops.addparam('Formats','Ticks X',self.format_x,'String','X format') self._myprops.addparam('Formats','Ticks Y',self.format_y,'String','Y format') self._myprops.addparam('Formats','Shape',self._equal_axis,'Integer','Shape', jsonstr= new_json({'auto':0, 'equal':1, 'specific':2})) self._myprops.addparam('Formats','Scaling factor',self.scaling_factor,'Float','Scaling factor') self._myprops.add_param('Grid','Major X', self.gridx_major, 'Logical', 'Major grid X') self._myprops.add_param('Grid','Major Y', self.gridy_major, 'Logical', 'Major grid Y') self._myprops.Populate()
[docs] def populate(self): """ Populate the properties UI """ if self._myprops is None: self._set_props() self._myprops[('Draw','Title')] = self.title self._myprops[('Draw','X title')] = self.xtitle self._myprops[('Draw','Y title')] = self.ytitle self._myprops[('Draw','Legend')] = self.legend self._myprops[('Bounds','X min')] = self.xmin self._myprops[('Bounds','X max')] = self.xmax self._myprops[('Bounds','Y min')] = self.ymin self._myprops[('Bounds','Y max')] = self.ymax self._myprops[('Grid','Major X')] = self.gridx_major self._myprops[('Grid','Major Y')] = self.gridy_major self._myprops[('Ticks X','Positions')] = self.ticks_x self._myprops[('Ticks X','Labels')] = self.ticks_label_x self._myprops[('Ticks Y','Positions')] = self.ticks_y self._myprops[('Ticks Y','Labels')] = self.ticks_label_y self._myprops[('Formats','Ticks X')] = self.format_x self._myprops[('Formats','Ticks Y')] = self.format_y self._myprops[('Formats','Shape')] = self._equal_axis self._myprops[('Formats','Scaling factor')] = self.scaling_factor self._myprops.Populate()
[docs] def ui(self): if self._myprops is not None: self._myprops.CenterOnScreen() self._myprops.Raise() self._myprops.Show() return self._set_props() self._myprops.Show() self._myprops.SetTitle(_('Ax properties')) icon = wx.Icon() icon_path = Path(__file__).parent / "apps/wolf_logo2.bmp" icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY)) self._myprops.SetIcon(icon) self._myprops.Center() self._myprops.Raise()
[docs] def destroyprop(self): self._myprops=None
[docs] def bounds_lines(self): if self._ax is None: logging.warning('No axes found') return lines = self._ax.get_lines() if len(lines) == 0: logging.warning('No lines found') return xmin = np.inf xmax = -np.inf ymin = np.inf ymax = -np.inf for line in lines: x = line.get_xdata() y = line.get_ydata() xmin = min(xmin, np.min(x)) xmax = max(xmax, np.max(x)) ymin = min(ymin, np.min(y)) ymax = max(ymax, np.max(y)) return xmin, xmax, ymin, ymax
[docs] def fill_property(self, verbosity= True): if self._myprops is None: logging.warning('Properties UI not found') return self._myprops.apply_changes_to_memory(verbosity= verbosity) self.title = self._myprops[('Draw','Title')] self.xtitle = self._myprops[('Draw','X title')] self.ytitle = self._myprops[('Draw','Y title')] self.legend = self._myprops[('Draw','Legend')] self.xmin = self._myprops[('Bounds','X min')] self.xmax = self._myprops[('Bounds','X max')] self.ymin = self._myprops[('Bounds','Y min')] self.ymax = self._myprops[('Bounds','Y max')] self.gridx_major = self._myprops[('Grid','Major X')] self.gridy_major = self._myprops[('Grid','Major Y')] xmin, xmax, ymin, ymax = self.bounds_lines() if self.xmin == -99999.: self.xmin = xmin if self.xmax == -99999.: self.xmax = xmax if self.ymin == -99999.: self.ymin = ymin if self.ymax == -99999.: self.ymax = ymax self.format_x = sanitize_fmt(self._myprops[('Formats','Ticks X')]) self.format_y = sanitize_fmt(self._myprops[('Formats','Ticks Y')]) def format_value(value, fmt): return '{value:{fmt}}'.format(value=value, fmt=fmt) ticks_x = self._myprops[('Ticks X','Positions')] if '[' in ticks_x: self.ticks_x = [float(cur.replace("'",'').replace(',','')) for cur in self._myprops[('Ticks X','Positions')].replace('[','').replace(']','').split()] else: try: self.ticks_x = float(ticks_x) self.ticks_x = np.linspace(self.xmin, self.xmax, int(np.ceil((self.xmax-self.xmin)/self.ticks_x)+1), endpoint=True).tolist() except: self.ticks_x = np.linspace(self.xmin, self.xmax, 5).tolist() ticks_label_x = self._myprops[('Ticks X','Labels')] if '[' in ticks_label_x: self.ticks_label_x = [cur.replace("'",'').replace(',','') for cur in self._myprops[('Ticks X','Labels')].replace('[','').replace(']','').split()] if len(self.ticks_label_x) != len(self.ticks_x): self.ticks_label_x = [format_value(cur, self.format_x) for cur in self.ticks_x] else: self.ticks_label_x = [format_value(cur, self.format_x) for cur in self.ticks_x] ticks_y = self._myprops[('Ticks Y','Positions')] if '[' in ticks_y: self.ticks_y = [float(cur.replace("'",'').replace(',','')) for cur in self._myprops[('Ticks Y','Positions')].replace('[','').replace(']','').split()] else: try: self.ticks_y = float(ticks_y) self.ticks_y = np.linspace(self.ymin, self.ymax, int(np.ceil((self.ymax-self.ymin)/self.ticks_y)+1), endpoint= True).tolist() except: self.ticks_y = np.linspace(self.ymin, self.ymax, 5).tolist() ticks_label_y = self._myprops[('Ticks Y','Labels')] if '[' in ticks_label_y: self.ticks_label_y = [cur.replace("'",'').replace(',','') for cur in self._myprops[('Ticks Y','Labels')].replace('[','').replace(']','').split()] if len(self.ticks_label_y) != len(self.ticks_y): self.ticks_label_y = [format_value(cur, self.format_y) for cur in self.ticks_y] else: self.ticks_label_y = [format_value(cur, self.format_y) for cur in self.ticks_y] self._equal_axis = self._myprops[('Formats','Shape')] self.scaling_factor = self._myprops[('Formats','Scaling factor')] self.set_properties()
[docs] def set_properties(self, ax:Axes = None): if ax is None: ax = self._ax ax.set_title(self.title) ax.set_xlabel(self.xtitle) ax.set_ylabel(self.ytitle) ax.xaxis.grid(self.gridx_major) ax.yaxis.grid(self.gridy_major) ax.set_xticks(self.ticks_x, self.ticks_label_x) ax.set_yticks(self.ticks_y, self.ticks_label_y) if self.legend: update = any(line.update_legend for line in self._lines) if update: ax.legend().set_visible(False) for line in self._lines: line.update_legend = True ax.legend().set_visible(True) else: ax.legend().set_visible(False) ax.set_aspect(self.is_equal) ax.set_xlim(self.xmin, self.xmax) ax.set_ylim(self.ymin, self.ymax) ax.figure.canvas.draw() self.get_properties()
[docs] def get_properties(self, ax:Axes = None): if ax is None: ax = self._ax self.title = ax.get_title() self.xtitle = ax.get_xlabel() self.ytitle = ax.get_ylabel() self.legend = ax.legend().get_visible() self.xmin, self.xmax = ax.get_xlim() self.ymin, self.ymax = ax.get_ylim() self.gridx_major = any(line.get_visible() for line in ax.get_xgridlines()) self.gridy_major = any(line.get_visible() for line in ax.get_ygridlines()) self.ticks_x = [str(cur) for cur in ax.get_xticks()] self.ticks_y = [str(cur) for cur in ax.get_yticks()] self.ticks_label_x = [label.get_text().replace("'",'').replace(',','') for label in ax.get_xticklabels()] self.ticks_label_y = [label.get_text().replace("'",'').replace(',','') for label in ax.get_yticklabels()] if ax.get_aspect() == 'auto': self._equal_axis = 0 self.scaling_factor = 1. elif ax.get_aspect() == 1.: self._equal_axis = 1 self.scaling_factor = 1. else: self._equal_axis = 2 self.scaling_factor = ax.get_aspect() logging.warning('Aspect ratio not found, set to auto') self.populate()
[docs] def to_dict(self) -> str: """ properties to dict """ props= {'title':self.title, 'xtitle':self.xtitle, 'ytitle':self.ytitle, 'legend':self.legend, 'xmin':self.xmin, 'xmax':self.xmax, 'ymin':self.ymin, 'ymax':self.ymax, 'ticks_x':self.ticks_x, 'ticks_y':self.ticks_y, 'ticks_label_x':self.ticks_label_x, 'ticks_label_y':self.ticks_label_y} if self._lines is not None: props['lines'] = [line.to_dict() for line in self._lines] else: props['lines'] = [] return props
[docs] def from_dict(self, props:dict, frame:wx.Frame = None): """ properties from dict """ keys = ['title', 'xtitle', 'ytitle', 'legend', 'xmin', 'xmax', 'ymin', 'ymax', 'ticks_x', 'ticks_y', 'ticks_label_x', 'ticks_label_y'] for key in keys: try: setattr(self, key, props[key]) except: logging.warning('Key not found in properties dict') pass if isinstance(self.ticks_x,list): self.ticks_x = [float(cur) for cur in props['ticks_x']] elif isinstance(self.ticks_x,float): self.ticks_x = [self.ticks_x] elif isinstance(self.ticks_x,str): self.ticks_x = [float(self.ticks_x)] if isinstance(self.ticks_y,list): self.ticks_y = [float(cur) for cur in props['ticks_y']] elif isinstance(self.ticks_y,float): self.ticks_y = [self.ticks_y] elif isinstance(self.ticks_y,str): self.ticks_y = [float(self.ticks_y)] if isinstance(self.ticks_label_x,list): pass elif isinstance(self.ticks_label_x,float): self.ticks_label_x = [self.ticks_label_x] elif isinstance(self.ticks_label_x,str): self.ticks_label_x = [self.ticks_label_x] if isinstance(self.ticks_label_y,list): pass elif isinstance(self.ticks_label_y,float): self.ticks_label_y = [self.ticks_label_y] elif isinstance(self.ticks_label_y,str): self.ticks_label_y = [self.ticks_label_y] assert len(self.ticks_x) == len(self.ticks_label_x), f'{len(self.ticks_x)} != {len(self.ticks_label_x)}' assert len(self.ticks_y) == len(self.ticks_label_y), f'{len(self.ticks_y)} != {len(self.ticks_label_y)}' for line in props['lines']: if 'xdata' in line and 'ydata' in line: xdata = line['xdata'] ydata = line['ydata'] self._ax.plot(xdata, ydata) self.populate() self._lines = [Matplolib_line_properties(line, self).from_dict(line_props) for line_props, line in zip(props['lines'], self._ax.get_lines())] return self
[docs] def serialize(self): """ Serialize the properties """ return json.dumps(self.to_dict(), indent=4)
[docs] def deserialize(self, props:str): """ Deserialize the properties """ self.from_dict(json.loads(props))
[docs] def add_props_to_sizer(self, frame:wx.Frame, sizer:wx.BoxSizer): """ Add the properties to a sizer """ self._myprops.ensure_prop(frame, show_in_active_if_default=True, height=300) sizer.Add(self._myprops.prop, proportion= 1, flag= wx.EXPAND) self._myprops.prop.Hide()
[docs] def show_props(self): """ Show the properties """ self._myprops.prop.Show()
[docs] def hide_props(self): """ Hide the properties """ self._myprops.prop.Hide()
[docs] def hide_all_props(self): """ Hide all properties """ self.hide_props() for line in self._lines: line.hide_props()
[docs] def del_line(self, idx:int): """ Delete a line """ if idx>=0 and idx<len(self._lines): self._lines[idx].delete() self._lines.pop(idx) self._ax.lines.pop(idx)
[docs] MARKERS_MPL = ['None','o', 'v', '^', '<', '>', 's', 'p', 'P', '*', 'h', 'H', '+', 'x', 'X', 'D', 'd', '|', '_']
[docs] LINESTYLE_MPL = ['-', '--', '-.', ':', 'solid', 'dashed', 'dashdot', 'dotted', 'None']
[docs] def convert_colorname_rgb(color:str) -> str: """ Convert a given color name or abbreviation to its corresponding RGB tuple. :param color: The color name or abbreviation to convert. Supported colors are 'b'/'blue', 'g'/'green', 'r'/'red', 'c'/'cyan', 'm'/'magenta', 'y'/'yellow', 'k'/'black', 'w'/'white', and 'o'/'orange'. :type color: str :return: A tuple representing the RGB values of the color. If the color is not recognized, returns (0, 0, 0) which corresponds to black. :rtype: tuple """ if color in COLORS_MPL: if color in ['b', 'blue']: return (0,0,255) elif color in ['g', 'green']: return (0,128,0) elif color in ['r', 'red']: return (255,0,0) elif color in ['c', 'cyan']: return (0,255,255) elif color in ['m', 'magenta']: return (255,0,255) elif color in ['y', 'yellow']: return (255,255,0) elif color in ['k', 'black']: return (0,0,0) elif color in ['w', 'white']: return (255,255,255) elif color in ['o', 'orange']: return (255,165,0) else: return(0,0,0)
[docs] def convert_color(value:str | tuple) -> tuple: """ Convert a hex color to RGB """ if isinstance(value, tuple): return tuple([int(cur*255) for cur in value]) elif isinstance(value, str): if value.startswith('#'): value = value.lstrip('#') return tuple(int(value[i:i+2], 16) for i in (0, 2, 4)) else: return convert_colorname_rgb(value) else: return (0,0,0)
[docs] class Matplolib_line_properties(): def __init__(self, line:Line2D=None, ax_props:"Matplotlib_ax_properties"= None) -> None: self.wx_exits = wx.App.Get() is not None self._ax_props = ax_props self.color = (0,0,255) self.linewidth = 1.5 self._linestyle = 0 self._marker = 0 self.markersize = 6 self.alpha = 1.0 self.label = 'Line' self.markerfacecolor = (0,0,255) self.markeredgecolor = (0,0,255) self.markeredgewidth = 1.5 self.visible = True self.zorder = 1 self.picker:bool = False self.picker_radius:float = 5.0 self._selected = False self._selected_prop:Matplolib_line_properties = None self._myprops = None self._line = line self.update_legend = False self._set_props() if self._line is not None: self.get_properties() @property
[docs] def ax_props(self): return self._ax_props
@ax_props.setter def ax_props(self, value): self._ax_props = value @property
[docs] def ax(self): return self._ax_props._ax
@property
[docs] def fig(self): return self._ax_props._ax.figure
[docs] def copy(self): new_prop = Matplolib_line_properties() new_prop._ax_props = self._ax_props new_prop.color = self.color new_prop.linewidth = self.linewidth new_prop._linestyle = self._linestyle new_prop._marker = self._marker new_prop.markersize = self.markersize new_prop.alpha = self.alpha new_prop.label = self.label new_prop.markerfacecolor = self.markerfacecolor new_prop.markeredgecolor = self.markeredgecolor new_prop.markeredgewidth = self.markeredgewidth new_prop.visible = self.visible new_prop.zorder = self.zorder new_prop.picker = self.picker new_prop.picker_radius = self.picker_radius return new_prop
[docs] def presets(self, preset:str): """ Set the properties to a preset """ self.color = (0,0,255) self.linewidth = 1.5 self._linestyle = 0 self._marker = 0 self.markersize = 6 self.alpha = 1.0 self.label = 'Line' self.markerfacecolor = (0,0,255) self.markeredgecolor = (0,0,255) self.markeredgewidth = 1.5 self.visible = True self.zorder = 1 self.picker = False self.picker_radius = 5.0 if preset == 'default': pass elif preset == 'water': self.color = (0,0,255) self.linewidth = 2.5 self.label = 'Water' elif preset == 'land': self.color = (0,255,0) self.linewidth = 2.5 self.label = 'Land' elif preset == 'banks': self.color = (128,128,128) self.linestyle = 1 self.linewidth = 1.0 self.set_properties()
@property
[docs] def selected(self): return self._selected
@selected.setter def selected(self, value): self._selected = value self.set_properties() @property
[docs] def linestyle(self): return LINESTYLE_MPL[self._linestyle]
@linestyle.setter def linestyle(self, value): if isinstance(value, str): if value in LINESTYLE_MPL: self._linestyle = LINESTYLE_MPL.index(value) else: logging.warning('Line style not found, set to default') self._linestyle = 0 elif isinstance(value, int): self._linestyle = value else: logging.warning('Line style not found, set to default') self._linestyle = 0 @property
[docs] def marker(self): return MARKERS_MPL[self._marker]
@marker.setter def marker(self, value): if isinstance(value, str): if value in MARKERS_MPL: self._marker = MARKERS_MPL.index(value) else: logging.warning('Marker not found, set to default') self._marker = 0 elif isinstance(value, int): self._marker = value else: logging.warning('Marker not found, set to default') self._marker = 0
[docs] def set_line(self, line:Line2D): self._line = line if line is None: return self.get_properties() return self
[docs] def on_pick(self, line:Line2D, mouseevent:MouseEvent): if mouseevent.button == 1: pass print(mouseevent.xdata, mouseevent.ydata) # line.set_color('r') # line.figure.canvas.draw() return True, dict()
[docs] def get_properties(self, line:Line2D= None): if line is None: line = self._line if line is None: logging.warning('Line not found/defined') return self.color = convert_color(line.get_color()) self.linewidth = line.get_linewidth() self.linestyle = line.get_linestyle() if self.linestyle not in LINESTYLE_MPL: self.linestyle = '-' logging.warning('Line style not found, set to default') self.marker = line.get_marker() if self.marker not in MARKERS_MPL: self.marker = 'o' logging.warning('Marker not found, set to default') self.markersize = line.get_markersize() self.alpha = line.get_alpha() if line.get_alpha() is not None else 1.0 self.label = line.get_label() self.markerfacecolor = convert_color(line.get_markerfacecolor()) self.markeredgecolor = convert_color(line.get_markeredgecolor()) self.markeredgewidth = line.get_markeredgewidth() self.visible = line.get_visible() self.zorder = line.get_zorder() self.picker = line.get_picker() is not None self.picker_radius = line.get_pickradius()
[docs] def _set_props(self): """ Set the properties UI """ if self._myprops is not None: return self._myprops = Wolf_Param(title='Line properties', w= 500, h= 400, to_read= False, ontop= False, init_GUI= False) self._myprops.set_callbacks(None, self.destroyprop) # self._myprops.hide_selected_buttons() # only 'Apply' button self._myprops.addparam('Draw','Color',self.color,'Color','Drawing color') self._myprops.addparam('Draw','Width',self.linewidth,'Float','Drawing width') self._myprops.addparam('Draw','Style',self._linestyle,'Integer','Drawing style', jsonstr= new_json({'-':0, '--':1, '-.':2, ':':3, 'None':8, 'solid': 0, 'dashed': 1, 'dashdot': 2, 'dotted': 3})) self._myprops.addparam('Draw', 'Alpha', self.alpha, 'Float', 'Transparency') self._myprops.addparam('Draw', 'Label', self.label, 'String', 'Label') self._myprops.addparam('Draw', 'Visible', self.visible, 'Logical', 'Visible') self._myprops.addparam('Draw', 'Zorder', self.zorder, 'Integer', 'Zorder') self._myprops.addparam('Marker', 'Marker', self._marker, 'Integer', 'Marker style', jsonstr= new_json({'None':0, 'o': 1, 'v': 2, '^': 3, '<': 4, '>': 5, 's': 6, 'p': 7, 'P': 8, '*': 9, 'h': 10, 'H': 11, '+': 12, 'x': 13, 'X': 14, 'D': 15, 'd': 16, '|': 17, '_': 18})) self._myprops.addparam('Marker', 'Markersize', self.markersize, 'Float', 'Marker size') self._myprops.addparam('Marker', 'Markerfacecolor', self.markerfacecolor, 'Color', 'Marker face color') self._myprops.addparam('Marker', 'Markeredgecolor', self.markeredgecolor, 'Color', 'Marker edge color') self._myprops.addparam('Marker', 'Markeredgewidth', self.markeredgewidth, 'Float', 'Marker edge width') self._myprops.addparam('Picker', 'Picker', self.picker, 'Logical', 'Picker') self._myprops.addparam('Picker', 'Picker radius', self.picker_radius, 'Float', 'Picker radius') self._myprops.Populate()
# self._myprops.Layout() # self._myprops.SetSizeHints(500,500)
[docs] def populate(self): """ Populate the properties UI """ if self._myprops is None: self._set_props() self._myprops[('Draw','Color')] = self.color self._myprops[('Draw','Width')] = self.linewidth self._myprops[('Draw','Style')] = self._linestyle self._myprops[('Draw','Alpha')] = self.alpha self._myprops[('Draw','Label')] = self.label self._myprops[('Draw','Visible')] = self.visible self._myprops[('Draw','Zorder')] = self.zorder self._myprops[('Marker', 'Marker')] = self._marker self._myprops[('Marker', 'Markersize')] = self.markersize self._myprops[('Marker', 'Markeredgecolor')] = self.markeredgecolor self._myprops[('Marker', 'Markerfacecolor')] = self.markerfacecolor self._myprops[('Marker', 'Markeredgewidth')] = self.markeredgewidth self._myprops[('Picker', 'Picker')] = self.picker self._myprops[('Picker', 'Picker radius')] = self.picker_radius self._myprops.Populate()
[docs] def ui(self): if self._myprops is not None: self._myprops.CenterOnScreen() self._myprops.Raise() self._myprops.Show() return self._set_props() self._myprops.Show() self._myprops.SetTitle(_('Line properties')) icon = wx.Icon() icon_path = Path(__file__).parent / "apps/wolf_logo2.bmp" icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY)) self._myprops.SetIcon(icon) self._myprops.Center() self._myprops.Raise()
[docs] def destroyprop(self): self._myprops=None
[docs] def fill_property(self, verbosity:bool= True): if self._myprops is None: logging.warning('Properties UI not found') return self._myprops.apply_changes_to_memory(verbosity= verbosity) self.color = getRGBfromI(self._myprops[('Draw','Color')]) self.linewidth = self._myprops[('Draw','Width')] self.linestyle = self._myprops[('Draw','Style')] self.alpha = self._myprops[('Draw', 'Alpha')] self.update_legend = self.label == self._myprops[('Draw', 'Label')] self.label = self._myprops[('Draw', 'Label')] self.visible = self._myprops[('Draw', 'Visible')] self.zorder = self._myprops[('Draw', 'Zorder')] self.marker = self._myprops[('Marker', 'Marker')] self.markersize = self._myprops[('Marker', 'Markersize')] self.markeredgecolor = getRGBfromI(self._myprops[('Marker', 'Markeredgecolor')]) self.markerfacecolor = getRGBfromI(self._myprops[('Marker', 'Markerfacecolor')]) self.markeredgewidth = self._myprops[('Marker', 'Markeredgewidth')] self.picker = self._myprops[('Picker', 'Picker')] self.picker_radius = self._myprops[('Picker', 'Picker radius')] self.set_properties()
[docs] def set_properties(self, line:Line2D = None): if line is None: line = self._line if line is None: logging.warning('Line not found/defined') return def check_color(color): if isinstance(color, str): color = convert_colorname_rgb(color) color = tuple([c/255. for c in color]) return color line.set_color(check_color(self.color if not self.selected else (255,0,0))) line.set_linewidth(self.linewidth if not self.selected else 3.0) line.set_linestyle(self.linestyle if not self.selected else '-') line.set_marker(self.marker) line.set_markersize(self.markersize) line.set_alpha(self.alpha) line.set_label(self.label) line.set_markerfacecolor(check_color(self.markerfacecolor if not self.selected else (255,0,0))) line.set_markeredgecolor(check_color(self.markeredgecolor)) line.set_markeredgewidth(self.markeredgewidth) line.set_visible(self.visible) line.set_zorder(self.zorder) line.set_pickradius(self.picker_radius) line.set_picker(self.on_pick if self.picker else lambda line,mouseevent: (False, dict())) if self._ax_props is not None: self._ax_props.fill_property(verbosity= False) else: line.axes.figure.canvas.draw()
[docs] def show_properties(self): self.ui()
[docs] def to_dict(self) -> str: """ properties to dict """ xdata = self._line.get_xdata().tolist() ydata = self._line.get_ydata().tolist() return {'color':self.color, 'linewidth':self.linewidth, 'linestyle':self.linestyle, 'marker':self.marker, 'markersize':self.markersize, 'alpha':self.alpha, 'label':self.label, 'markerfacecolor':self.markerfacecolor, 'markeredgecolor':self.markeredgecolor, 'markeredgewidth':self.markeredgewidth, 'visible':self.visible, 'zorder':self.zorder, 'picker':self.picker, 'picker_radius':self.picker_radius, 'xdata':xdata, 'ydata':ydata}
[docs] def from_dict(self, props:dict): """ properties from dict """ keys = ['color', 'linewidth', 'linestyle', 'marker', 'markersize', 'alpha', 'label', 'markerfacecolor', 'markeredgecolor', 'markeredgewidth', 'visible', 'zorder', 'picker', 'picker_radius'] for key in keys: try: setattr(self, key, props[key]) except: logging.warning('Key not found in properties dict') pass self.populate() self.set_properties() return self
[docs] def add_props_to_sizer(self, frame:wx.Frame, sizer:wx.BoxSizer): """ Add the properties to a sizer """ self._myprops.ensure_prop(frame, show_in_active_if_default=True, height=300) sizer.Add(self._myprops.prop, proportion= 1, flag= wx.EXPAND) self._myprops.prop.Hide()
[docs] def show_props(self): """ Show the properties """ self.populate() self._myprops.prop.Show()
[docs] def hide_props(self): """ Hide the properties """ self._myprops.prop.Hide()
[docs] def delete(self): """ Delete the properties """ self._myprops.prop.Hide() self._myprops.prop.Destroy() self._myprops = None self._line = None
[docs] class PRESET_LAYOUTS(Enum):
[docs] DEFAULT = (1,1)
[docs] MAT2X2 = (2,2)
[docs] class Matplotlib_Figure(wx.Frame): """ Matplotlib Figure with wx Frame """ def __init__(self, layout:tuple | list | dict | PRESET_LAYOUTS = None) -> None: """ Layout can be a tuple, a list, a dict or a string. If a string, it must be a list of strings or a list of lists. It will be used in fig.subplot_mosaic. If a tuple or a list of 2 integers. It will be used in fig.subplots. if a dict, it must contain 'nrows' and 'ncols' and 'ax_cells' (list of tuples with row_start, row_end, col_start, col_end, key). It will be used in fig.add_gridspec. The class has: - fig: the figure - ax_dict: a dict of axes --> key: name of the axes, value: axes - ax: a list of axes --> always flatten The properties of the figure can be accessed by self.fig_properties. The properties of the axes can be accessed by self._axes_properties. The current Axes can be accessed by self.cur_ax. A plot can be added by self.add_plot(xdata, ydata, label, color, linestyle, linewidth, marker, markersize, markerfacecolor, markeredgecolor, markeredgewidth, alpha, visible, zorder, picker, picker_radius) :param layout: layout of the figure :type layout: tuple | list | dict | str """ self.wx_exists = wx.App.Get() is not None self.fig = plt.figure() dpi = self.fig.get_dpi() size_x, size_y = self.fig.get_size_inches() if self.wx_exists: wx.Frame.__init__(self, None, -1, 'Matplotlib Figure', size=(size_x*dpi+16, size_y*dpi+240), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER) self.ax_dict:dict[str,Axes] = {} # dict of axes self.ax:list[Axes] = [] # list of axes -- always flatten self.shown_props = None # shown properties self.apply_layout(layout) # apply the layout pass
[docs] def presets(self, which:PRESET_LAYOUTS = PRESET_LAYOUTS.DEFAULT): """ Presets """ if which not in PRESET_LAYOUTS: logging.warning('Preset not found') return self.apply_layout(which)
@property
[docs] def layout(self): return self._layout
[docs] def apply_layout(self, layout:tuple | list | dict | PRESET_LAYOUTS): """ Apply the layout Choose between (subplots, subplot_mosaic, gridspec) according to the type of layout (tuple, list[str], dict) """ self._layout = layout if self._layout is None: logging.info('No layout defined') return if isinstance(layout, PRESET_LAYOUTS): self.apply_layout(layout.value) return if isinstance(layout, tuple | list): # check is the first element is a string - layout can be a list of lists tmp_layout = [] for cur in layout: if isinstance(cur, list): tmp_layout.extend(cur) else: tmp_layout.append(cur) if isinstance(tmp_layout[0], str): # List of strings - subplot_mosaic returns a dict of Axes self.ax_dict = self.fig.subplot_mosaic(layout) # store the axes in a list -- So we can access them by index, not only by name self.ax = [ax for ax in self.ax_dict.values()] else: # Tuple or list of 2 elements - subplots if len(layout) != 2: logging.warning('Layout must be a tuple or a list of 2 elements') return self.nbrows, self.nbcols = layout if self.nbrows*self.nbcols == 1: # Convert to list -- subplots returns a single Axes but we want a list self.ax = [self.fig.subplots(self.nbrows, self.nbcols)] else: # Flatten the axes -- sbplots returns a 2D array of Axes but we want a list self.ax = self.fig.subplots(self.nbrows, self.nbcols).flatten() # store the axes in a dict -- So we can access them by name, not only by index self.ax_dict = {f'{i}':ax for i, ax in enumerate(self.ax)} for key,ax in self.ax_dict.items(): ax._label = key elif isinstance(layout, dict): # dict --> Gridspec # Check if nrows and ncols are defined if 'nrows' not in layout or 'ncols' not in layout: logging.warning('nrows and ncols must be defined in the layout') return if 'ax_cells' not in layout: logging.warning('ax_cells must be defined in the layout') return gs:GridSpec = self.fig.add_gridspec(nrows= layout['nrows'], ncols= layout['ncols']) ax_cells = layout['ax_cells'] for row_start, row_end, col_start, col_end, key in ax_cells: self.ax_dict[key] = self.fig.add_subplot(gs[row_start:row_end, col_start:col_end]) self.ax_dict[key]._label = key self.ax = [ax for ax in self.ax_dict.values()] self._fig_properties = Matplotlib_figure_properties(self, self.fig) if self.wx_exists: self.set_wx()
@property
[docs] def fig_properties(self) -> "Matplotlib_figure_properties": return self._fig_properties
@property
[docs] def _axes_properties(self) -> list[Matplotlib_ax_properties]: return self._fig_properties._axes
@property
[docs] def nbrows(self): return self._nbrows
@nbrows.setter def nbrows(self, value:int): self._nbrows = value @property
[docs] def nbcols(self): return self._nbcols
@nbcols.setter def nbcols(self, value:int): self._nbcols = value @property
[docs] def nb_axes(self): return len(self.ax)
[docs] def set_wx(self): """ Set the wx Frame Design """ self.SetIcon(wx.Icon(str(Path(__file__).parent / "apps/wolf_logo2.bmp"))) self._sizer = wx.BoxSizer(wx.VERTICAL) # Matplotlib canvas interacting with wx # -------------------------------------- self._canvas = FigureCanvas(self, -1, self.fig) self._sizer.Add(self._canvas, 1, wx.EXPAND | wx.ALL) # Bind events self._canvas.Bind(wx.EVT_ENTER_WINDOW, self.ChangeCursor) self._canvas.mpl_connect('motion_notify_event', self.UpdateStatusBar) self._canvas.mpl_connect('button_press_event', self.OnClickCanvas) self._canvas.mpl_connect('key_press_event', self.OnKeyCanvas) # Toolbar - Matplotlib # -------------------- self._toolbar = NavigationToolbar(self._canvas, self) # Buttons - Figure, Axes, Lines properties # --------- ------------------------------ self._prop_but = wx.Button(self, -1, 'Figure Properties') self._ax_sizer = wx.BoxSizer(wx.HORIZONTAL) self._ax_current = wx.Choice(self, -1, choices=[ax._label for ax in self.ax]) self._ax_current.SetToolTip('Select the current ax -- Axes are enumerated from left to right and top to bottom') self._ax_current.SetSelection(0) self._ax_but = wx.Button(self, -1, 'Ax Properties') self._ax_but.SetToolTip('Choosing the properties of the current ax -- Axes are enumerated from left to right and top to bottom') self._ax_current.Bind(wx.EVT_CHOICE, self.on_ax_choice) self._ax_but.Bind(wx.EVT_BUTTON, self.on_ax_properties) self._ax_sizer.Add(self._ax_current, 1, wx.EXPAND) self._ax_sizer.Add(self._ax_but, 1, wx.EXPAND) self._line_sizer = wx.BoxSizer(wx.HORIZONTAL) self._line_current = wx.Choice(self, -1, choices=[str(i) for i in range(len(self.cur_ax.get_lines()))]) self._line_current.SetSelection(0) self._line_but = wx.Button(self, -1, 'Line Properties') self._line_but.Bind(wx.EVT_BUTTON, self.on_line_properties) self._line_current.Bind(wx.EVT_CHOICE, self.on_line_choose) self._line_sizer.Add(self._line_current, 1, wx.EXPAND) self._line_sizer.Add(self._line_but, 1, wx.EXPAND) self.Bind(wx.EVT_CLOSE, self.on_close) self._prop_but.Bind(wx.EVT_BUTTON, self.on_fig_properties) self._sizer.Add(self._toolbar, 0, wx.EXPAND) self._sizer.Add(self._prop_but, 0, wx.EXPAND) self._sizer.Add(self._ax_sizer, 0, wx.EXPAND) self._sizer.Add(self._line_sizer, 0, wx.EXPAND) self._statusbar = wx.StatusBar(self) self._sizer.Add(self._statusbar, 0, wx.EXPAND) # Buttons - Save, Load # -------------------- self._save_but = wx.Button(self, -1, 'Save') self._load_but = wx.Button(self, -1, 'Load') self._save_but.Bind(wx.EVT_BUTTON, self.on_save) self._load_but.Bind(wx.EVT_BUTTON, self.on_load) self._sizer_save_load = wx.BoxSizer(wx.HORIZONTAL) self._sizer_save_load.Add(self._save_but, 1, wx.EXPAND) self._sizer_save_load.Add(self._load_but, 1, wx.EXPAND) self._sizer.Add(self._sizer_save_load, 0, wx.EXPAND) self._applyt_but = wx.Button(self, -1, 'Apply Properties') self._applyt_but.Bind(wx.EVT_BUTTON, self.onapply_properties) self._sizer.Add(self._applyt_but, 0, wx.EXPAND) # Collapsible pane -- Grid Xls, Properties # ---------------------------------- self._collaps_pane = wx.CollapsiblePane(self, label='Properties', style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE) self._collaps_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.on_collaps_pane) win = self._collaps_pane.GetPane() self._sizer_grid_props = wx.BoxSizer(wx.HORIZONTAL) win.SetSizer(self._sizer_grid_props) self._sizer_grid_props.SetSizeHints(win) # XLS sizer # --------- self._sizer_xls = wx.BoxSizer(wx.VERTICAL) self._xls = CpGrid(win, -1, wx.WANTS_CHARS) self._update_xy = wx.Button(win, -1, 'Update XY') self._update_xy.Bind(wx.EVT_BUTTON, self.update_line_from_grid) self._add_row = wx.Button(win, -1, 'Add rows') self._add_row.Bind(wx.EVT_BUTTON, self.add_row_to_grid) self._add_line = wx.Button(win, -1, 'Add line') self._add_line.Bind(wx.EVT_BUTTON, self.onadd_line) self._del_line = wx.Button(win, -1, 'Remove line') self._del_line.Bind(wx.EVT_BUTTON, self.ondel_line) self._sizer_xls.Add(self._xls, 1, wx.EXPAND) self._sizer_xls.Add(self._update_xy, 0, wx.EXPAND) self._sizer_xls.Add(self._add_row, 0, wx.EXPAND) self._sizer_xls.Add(self._add_line, 0, wx.EXPAND) self._sizer_xls.Add(self._del_line, 0, wx.EXPAND) # Properties sizer # --------------- # Add all props from axes self._fig_properties.add_props_to_sizer(win, self._sizer_grid_props) self._sizer_grid_props.Add(self._sizer_xls, 1, wx.GROW | wx.ALL) # self._sizer.Add(self._sizer_grid_props, 1, wx.EXPAND) self._collaps_pane.Expand() self._sizer.Add(self._collaps_pane, 0, wx.EXPAND | wx.ALL) self._xls.CreateGrid(10, 2) self._xls.SetColLabelValue(0, 'X') self._xls.SetColLabelValue(1, 'Y') self._xls.SetMaxSize((-1, 400)) self.SetSizer(self._sizer) self.SetAutoLayout(True) # self.Layout() self.Fit() self.Bind(wx.EVT_SIZE, self.on_size) self.Show() self._collapsible_size = self._collaps_pane.GetSize()
[docs] def on_save(self, event): """ Save the figure """ with wx.FileDialog(self, "Save figure", wildcard="JSON files (*.json)|*.json", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog: if fileDialog.ShowModal() == wx.ID_CANCEL: return path = fileDialog.GetPath() self.save(str(path))
[docs] def on_load(self, event): """ Load the figure """ with wx.FileDialog(self, "Open figure", wildcard="JSON files (*.json)|*.json", style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fileDialog: if fileDialog.ShowModal() == wx.ID_CANCEL: return path = fileDialog.GetPath() self.load(str(path))
[docs] def ChangeCursor(self, event:MouseEvent): self._canvas.SetCursor(wx.Cursor(wx.CURSOR_BULLSEYE))
[docs] def UpdateStatusBar(self, event:MouseEvent): if event.inaxes: idx= event.inaxes.get_figure().axes.index(event.inaxes) x, y = event.xdata, event.ydata self._statusbar.SetStatusText("Axes index= " + str(idx) + " -- x= "+str(x)+" -- y="+str(y))
[docs] def _mask_all_axes_props(self): for ax_prop in self._axes_properties: ax_prop._myprops.prop.Hide()
[docs] def _show_axes_props(self, idx:int): self._mask_all_axes_props() self._axes_properties[idx]._myprops.prop.Show()
@property
[docs] def cur_ax(self) -> Axes: return self.ax[int(self._ax_current.GetSelection())]
@cur_ax.setter def cur_ax(self, idx:int): if idx < 0 or idx >= len(self.ax): logging.warning('Index out of range') return self._ax_current.SetSelection(idx) self._fill_lines_ax() @property
[docs] def cur_ax_properties(self) -> Matplotlib_ax_properties: return self._axes_properties[int(self._ax_current.GetSelection())]
@cur_ax_properties.setter def cur_ax_properties(self, idx:int): if idx < 0 or idx >= len(self._axes_properties): logging.warning('Index out of range') return self._ax_current.SetSelection(idx) self._fill_lines_ax() @property
[docs] def cur_line_properties(self) -> Matplolib_line_properties: if self._line_current.GetSelection() == -1: return None return self.cur_ax_properties._lines[int(self._line_current.GetSelection())]
@property
[docs] def cur_line(self) -> Line2D: return self.cur_ax.get_lines()[int(self._line_current.GetSelection())]
[docs] def get_figax(self): if len(self.ax) == 1: return self.fig, self.ax[0] else: return self.fig, self.ax
[docs] def on_close(self, event): self.Destroy()
[docs] def on_fig_properties(self, event): """ Show the figure properties """ self.show_fig_properties()
[docs] def show_fig_properties(self): # self._fig_properties.ui() self._hide_all_props() self._fig_properties.show_props() self.Layout() self.shown_props = self._fig_properties
[docs] def _fill_lines_ax(self, idx:int = None): self._line_current.SetItems([line.get_label() for line in self.cur_ax.get_lines()]) self._line_current.SetSelection(0) if idx is not None: self._line_current.SetSelection(idx)
[docs] def on_ax_choice(self, event): self._fill_lines_ax()
[docs] def on_ax_properties(self, event): """ Show the ax properties """ self.show_curax_properties()
[docs] def show_curax_properties(self): # self.cur_ax_properties.ui() self._hide_all_props() self.cur_ax_properties.show_props() self.Layout() self.shown_props = self.cur_ax_properties
[docs] def on_line_properties(self, event): """ Show the line properties """ self.show_curline_properties()
[docs] def show_curline_properties(self): # self.cur_line_properties.ui() self._hide_all_props() self.cur_line_properties.show_props() # self.Layout() self._sizer_grid_props.Layout() self.shown_props = self.cur_line_properties
[docs] def onapply_properties(self, event): """ Apply the properties """ if self.shown_props is not None: self.shown_props.fill_property() self.update_layout()
[docs] def _hide_all_props(self): self._fig_properties.hide_props() for ax_prop in self._axes_properties: ax_prop.hide_all_props()
[docs] def on_line_choose(self, event): self.cur_ax_properties.reset_selection() self.cur_ax_properties.select_line(self._line_current.GetSelection()) self.fill_grid_with_xy()
[docs] def on_size(self, event): """ Resize event """ width, height = self.fig.get_size_inches() dpi = self.fig.get_dpi() width_pix = int(width * dpi) height_pix = int(height * dpi) self._canvas.MinSize = (width_pix, height_pix) self._collapsible_size = self._collaps_pane.GetSize() event.Skip()
[docs] def update_layout(self): if not self.wx_exists: return width, height = self.fig.get_size_inches() dpi = self.fig.get_dpi() width_pix = int(width * dpi) height_pix = int(height * dpi) self._canvas.MinSize = (width_pix, height_pix) self.SetSize((width_pix + 16, height_pix + 210 + self._collapsible_size[1])) self.Fit()
[docs] def on_collaps_pane(self, event): """ Collapsible pane event """ if event.GetCollapsed(): self._collaps_pane.Collapse() else: self._collaps_pane.Expand() if self._collapsible_size != self._collaps_pane.GetSize(): self.SetSize((self.GetSize()[0], self.GetSize()[1] + self._collaps_pane.GetSize()[1]-self._collapsible_size[1])) self._collapsible_size = self._collaps_pane.GetSize() self.Fit()
[docs] def OnKeyCanvas(self, event:KeyEvent): if event.key == 'escape': self._axes_properties[int(self._ax_current.GetSelection())].reset_selection()
[docs] def OnClickCanvas(self, event:MouseEvent): rclick = event.button == 3 lclick = event.button == 1 if not rclick: return if event.inaxes: ax:Axes = event.inaxes idx= ax.get_figure().axes.index(event.inaxes) x, y = event.xdata, event.ydata dist_min = 1e6 line_min = None for line in ax.get_lines(): xy = line.get_xydata() dist = np.linalg.norm(xy - np.array([x,y]), axis=1) idx_min = np.argmin(dist) if dist[idx_min] < dist_min: dist_min = dist[idx_min] line_min = line self._ax_current.SetSelection(idx) self._fill_lines_ax(idx = ax.get_lines().index(line_min)) self._axes_properties[idx].select_line(ax.get_lines().index(line_min)) self.fill_grid_with_xy(line_min) self.show_curline_properties()
[docs] def fill_grid_with_xy(self, line:Line2D= None, grid:CpGrid= None, colx:int= 0, coly:int= 1): if line is None: line = self.cur_line if grid is None: grid = self._xls xy = line.get_xydata() grid.ClearGrid() if grid.GetNumberRows() < len(xy): grid.AppendRows(len(xy)-grid.GetNumberRows()) elif grid.GetNumberRows() > len(xy): grid.DeleteRows(len(xy), grid.GetNumberRows()-len(xy)) for i in range(len(xy)): grid.SetCellValue(i, colx, str(xy[i,0])) grid.SetCellValue(i, coly, str(xy[i,1]))
[docs] def update_line_from_grid(self, event): line = self.cur_line #count not null values n = 0 for i in range(self._xls.GetNumberRows()): if self._xls.GetCellValue(i, 0) != '' and self._xls.GetCellValue(i, 1) != '': n += 1 xy = np.zeros((n, 2)) for i in range(n): xy[i,0] = float(self._xls.GetCellValue(i, 0)) xy[i,1] = float(self._xls.GetCellValue(i, 1)) line.set_data(xy[:,0], xy[:,1]) self.update_layout()
[docs] def add_row_to_grid(self, event): dlg = wx.TextEntryDialog(self, 'Number of rows to add', 'Add rows', '1') dlg.ShowModal() try: n = int(dlg.GetValue()) except: n = 1 self._xls.AppendRows(n)
[docs] def onadd_line(self, event): """ Add a plot to the current ax """ xy = self._get_xy_from_grid(self._xls) self.add_line(xy, self.cur_ax)
[docs] def _get_xy_from_grid(self, grid:CpGrid, colx:int= 0, coly:int= 1): """ Get the xy from a grid """ #Searching xy in the grid #count not null values n = 0 for i in range(grid.GetNumberRows()): if grid.GetCellValue(i, colx) != '' and grid.GetCellValue(i, coly) != '': n += 1 xy = np.zeros((n, 2)) for i in range(n): xy[i,0] = float(grid.GetCellValue(i, colx)) xy[i,1] = float(grid.GetCellValue(i, coly)) return xy
[docs] def add_line(self, xy:np.ndarray, ax:Axes=None, **kwargs): """ Add a plot to the current ax """ ax, idx_ax = self.get_ax_idx(ax) ax.plot(xy[:,0], xy[:,1], **kwargs) cur_ax_prop:Matplotlib_ax_properties = self._axes_properties[idx_ax] cur_ax_prop._lines.append(Matplolib_line_properties(ax.get_lines()[-1], cur_ax_prop)) cur_ax_prop._lines[-1].add_props_to_sizer(self._collaps_pane.GetPane(), self._sizer_grid_props) self.update_layout()
[docs] def ondel_line(self, event): """ Remove a plot from the current ax """ dlg = wx.MessageDialog(self, _('Do you want to remove the selected line?\n\nSuch action is irrevocable !\n\nPlease consider to set "Visible" to "False" to hide data'), _('Remove line'), wx.YES_NO | wx.ICON_QUESTION | wx.NO_DEFAULT) ret = dlg.ShowModal() if ret == wx.ID_NO: return if self._line_current.GetSelection() == -1: return idx = self._line_current.GetSelection() self.del_line(idx)
[docs] def del_line(self, idx:int): """ Delete a line """ self.cur_ax_properties.del_line(idx) self.update_layout()
[docs] def get_ax_idx(self, key:str | int | Axes= None) -> Axes: if key is None: return self.cur_ax, self._ax_current.GetSelection() if isinstance(key, str): if key in self.ax_dict: return self.ax_dict[key], list(self.ax_dict.keys()).index(key) else: logging.warning('Key not found') return None elif isinstance(key, int): if key >= 0 and key < len(self.ax): return self.ax[key], key else: logging.warning('Index out of range') return None elif isinstance(key, Axes): return key, list(self.ax_dict.values()).index(key)
[docs] def plot(self, x:np.ndarray, y:np.ndarray, ax:Axes | int | str= None, **kwargs): ax, idx_ax = self.get_ax_idx(ax) ax.plot(x, y, **kwargs) new_props = Matplolib_line_properties(ax.get_lines()[-1], self._axes_properties[idx_ax]) if self.wx_exists: new_props.add_props_to_sizer(self._collaps_pane.GetPane(), self._sizer_grid_props) ax_prop:Matplotlib_ax_properties = self._axes_properties[idx_ax] ax_prop._lines.append(new_props) ax_prop.get_properties() if self.wx_exists: if ax == self.cur_ax: self._line_current.SetItems([line.get_label() for line in ax.get_lines()]) self._line_current.SetSelection(len(ax.get_lines())-1) self.fig.tight_layout() self.update_layout()
[docs] def to_dict(self) -> dict: """ properties to dict """ ret = {} if self.wx_exists: ret['frame_name'] = self.GetName() ret['frame_size_x'] = self.GetSize()[0] ret['frame_size_y'] = self.GetSize()[1] ret['layout'] = self._layout ret['fig'] = self._fig_properties.to_dict() ret['axes'] = [ax.to_dict() for ax in self._axes_properties] return ret
[docs] def from_dict(self, props:dict): """ properties from dict """ if 'layout' not in props: logging.error('No layout found in properties') return self.apply_layout(props['layout']) self._fig_properties.from_dict(props['fig']) for ax_props, ax in zip(props['axes'], self._axes_properties): ax:Matplotlib_ax_properties ax.from_dict(ax_props) if self.wx_exists: for ax_props, ax in zip(props['axes'], self._axes_properties): for line in ax._lines: line.add_props_to_sizer(self._collaps_pane.GetPane(), self._sizer_grid_props) if 'frame_name' in props: self.SetName(props['frame_name']) if 'frame_size_x' in props and 'frame_size_y' in props: self.SetSize(props['frame_size_x'], props['frame_size_y']) self.Layout() return self
[docs] def serialize(self): """ Serialize the properties """ return json.dumps(self.to_dict(), indent=4)
[docs] def deserialize(self, props:str): """ Deserialize the properties """ self.from_dict(json.loads(props))
[docs] def save(self, filename:str): with open(filename, 'w') as f: f.write(self.serialize())
[docs] def load(self, filename:str): with open(filename, 'r') as f: self.deserialize(f.read())
[docs] def save_image(self, filename:str, dpi:int= 100): self.fig.savefig(filename, dpi= dpi)
[docs] def set_x_bounds(self, xmin:float, xmax:float, ax:Axes | int | str= None): ax, idx_ax = self.get_ax_idx(ax) ax.set_xlim(xmin, xmax) self._axes_properties[idx_ax].get_properties() self.fig.tight_layout() self._canvas.draw()
[docs] def set_y_bounds(self, ymin:float, ymax:float, ax:Axes | int | str= None): ax, idx_ax = self.get_ax_idx(ax) ax.set_ylim(ymin, ymax) self._axes_properties[idx_ax].get_properties() self.fig.tight_layout() self._canvas.draw()
[docs] class Matplotlib_figure_properties(): def __init__(self, parent:Matplotlib_Figure = None, fig:Figure = None) -> None: self.wx_exists = wx.App.Get() is not None self.parent = parent self._myprops = None self._fig:Figure = None self._axes = None self.title = 'Figure' self.size_width = 8 self.size_height = 6 self.dpi = 100 self._filename = None self.set_fig(fig) self._set_props()
[docs] def set_fig(self, fig:Figure): self._fig = fig if fig is None: return self._axes:list[Matplotlib_ax_properties] = [Matplotlib_ax_properties(ax) for ax in fig.get_axes()] self.get_properties() return self
[docs] def _set_props(self): """ Set the properties UI """ if self._myprops is not None: return self._myprops = Wolf_Param(title='Figure properties', w= 500, h= 400, to_read= False, ontop= False, init_GUI= False) self._myprops.set_callbacks(None, self.destroyprop) # self._myprops.hide_selected_buttons() self._myprops.addparam('Draw','Title',self.title,'String','SupTitle of the figure') self._myprops.addparam('Draw','Width',self.size_width,'Float','Width in inches') self._myprops.addparam('Draw','Height',self.size_height,'Float','Height in inches') self._myprops.addparam('Draw','DPI',self.dpi,'Integer','DPI - Dots per inch') self._myprops.addparam('Draw','Filename',self._filename,'File','Filename') self._myprops.Populate()
# self._myprops.Layout() # self._myprops.SetSizeHints(500,500)
[docs] def populate(self): """ Populate the properties UI """ if self._myprops is None: self._set_props() self._myprops[('Draw','Title')] = self.title self._myprops[('Draw','Width')] = self.size_width self._myprops[('Draw','Height')] = self.size_height self._myprops[('Draw','DPI')] = self.dpi self._myprops[('Draw','Filename')] = self._filename self._myprops.Populate()
[docs] def ui(self): """ Create the properties UI """ if not self.wx_exists: return if self._myprops is not None: self._myprops.CenterOnScreen() self._myprops.Raise() self._myprops.Show() return self._set_props() self._myprops.Show() self._myprops.SetTitle(_('Figure properties')) icon = wx.Icon() icon_path = Path(__file__).parent / "apps/wolf_logo2.bmp" icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY)) self._myprops.SetIcon(icon) self._myprops.Center() self._myprops.Raise()
[docs] def destroyprop(self): self._myprops=None
[docs] def fill_property(self): if self._myprops is None: logging.warning('Properties UI not found') return self._myprops.apply_changes_to_memory() self.title = self._myprops[('Draw','Title')] self.size_width = self._myprops[('Draw','Width')] self.size_height = self._myprops[('Draw','Height')] self.dpi = self._myprops[('Draw','DPI')] self._filename = self._myprops[('Draw','Filename')] self.set_properties()
[docs] def set_properties(self, fig:Figure = None): if fig is None: fig = self._fig if self.size_height == 0 or self.size_width == 0: logging.warning('Size is 0') return fig.set_dpi(self.dpi) fig.set_size_inches(self.size_width, self.size_height) fig.suptitle(self.title) fig.tight_layout() fig.canvas.draw() self.get_properties()
[docs] def get_properties(self, fig:Figure = None): if fig is None: fig = self._fig self.title = '' self.size_width, self.size_height = fig.get_size_inches() self.dpi = fig.get_dpi()
[docs] def to_dict(self) -> str: """ properties to dict """ return {'title':self.title if self.title != 'Figure' else '', 'size_width':self.size_width, 'size_height':self.size_height, 'dpi':self.dpi}
[docs] def from_dict(self, props:dict): """ properties from dict """ keys = ['title', 'size_width', 'size_height', 'dpi'] for key in keys: try: setattr(self, key, props[key]) except: logging.warning('Key not found in properties dict') pass self.set_properties() return self
[docs] def add_props_to_sizer(self, frame:wx.Frame, sizer:wx.BoxSizer): """ Add the properties to a sizer """ self._myprops.ensure_prop(frame, show_in_active_if_default=True, height=300) sizer.Add(self._myprops.prop, proportion= 1, flag= wx.EXPAND) self._myprops.prop.Hide() for ax in self._axes: ax.add_props_to_sizer(frame, sizer) pass
[docs] def show_props(self): """ Show the properties """ self._myprops.prop.Show()
[docs] def hide_props(self): """ Hide the properties """ self._myprops.prop.Hide()
[docs] COLORS_MPL = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'w', 'blue', 'green', 'red', 'cyan', 'magenta', 'yellow', 'black', 'white', 'orange']