diff --git a/README.md b/README.md index fa6925b..c8df866 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Documentation is scarce for now, but here are some tips for using the program: - The modes and fileformat drop down menus at the top can usually be kept on `auto`. If a file cannot be read, pay attention to the file extension used, and possibly select a specific file format in the dropdown menu instead of `auto`. - Above the taskbar is the "Pipeline" which lists the different actions (e.g. binning, filtering, mask) that are applied to the different tables before being plotted. The pipeline actions will be reapplied on reload, and python code for them will be generated when exporting a script. - Different plot styling options can be found below the plot area. The button next to the "Save" icon can be used to customize the esthetics of the plot (e.g. fontsize, linewidth, legend location). + - **Third variable / Color scale**: in the column selection panel, a `Z/C:` dropdown is shown below the X-axis selector. Select any column to use it as a color variable — the plot automatically switches to a scatter plot colored by that column. A control bar appears below the canvas with options for the colormap, a colorbar toggle, and a "3D view" checkbox for a 3D scatter visualization. - Live plotting can be disabled using the check box "Live plot". This is useful when manipulating large datasets, and potentially wanting to delete some columns without plotting them. @@ -130,6 +131,8 @@ Different kind of plots: - Multiple plots using sub-figures or a different colors - Probability density function (PDF) plot - Fast Fourier Transform (FFT) plot +- **Scatter plot with color scale**: select a third variable (Z/C) to color scatter points by that variable, with a choice of colormap and optional colorbar +- **3D scatter plot**: when a Z/C variable is selected, enable "3D view" to visualize data as a 3D scatter plot with the Z variable as the height axis Plot options: - Logarithmic scales on x and y axis @@ -137,6 +140,8 @@ Plot options: - Synchronization of the x-axis of the sub-figures while zooming - Markers annotations and Measurements - Plot styling options +- **Z/color variable** (third variable): select a Z/C column in the column panel to color scatter points; choose from a range of colormaps (viridis, coolwarm, jet, etc.) and optionally display a colorbar +- **3D view**: when a Z/C variable is selected, enable the "3D view" checkbox to switch to an interactive 3D scatter plot Data manipulation options: - Remove columns in a table, add columns using a given formula, and export the table to csv diff --git a/pydatview/GUIPlotPanel.py b/pydatview/GUIPlotPanel.py index 24f8f91..3670b8c 100644 --- a/pydatview/GUIPlotPanel.py +++ b/pydatview/GUIPlotPanel.py @@ -34,6 +34,10 @@ raise e # from matplotlib.figure import Figure from pydatview.figure import SwappyFigure as Figure +try: + from mpl_toolkits.mplot3d import Axes3D # noqa: F401 - registers 3d projection +except ImportError: + pass from matplotlib.pyplot import rcParams as pyplot_rc from matplotlib import font_manager from pandas.plotting import register_matplotlib_converters @@ -53,6 +57,26 @@ pyplot_rc['agg.path.chunksize'] = 20000 +def _patch_3d_ctrl_rotate(ax, canvas): + """Require Ctrl+left-click to rotate a 3D axis; plain left-click is free for zoom/pan. + + Strategy: matplotlib's built-in _button_press sets ax.button_pressed = event.button. + Our handler fires afterwards (registered later = called later) and resets + ax.button_pressed to None when Ctrl is not held, so _on_move skips rotation. + wx.GetKeyState gives reliable Ctrl detection in the wx backend. + """ + def _on_3d_press(event): + if event.inaxes != ax or event.button != 1: + return + if not wx.GetKeyState(wx.WXK_CONTROL): + try: + ax.button_pressed = None + except Exception: + pass + + canvas.mpl_connect('button_press_event', _on_3d_press) + + class PDFCtrlPanel(wx.Panel): def __init__(self, parent): super(PDFCtrlPanel,self).__init__(parent) @@ -209,6 +233,65 @@ def _GUI2Data(self): data = {'type': self.rbType.GetString(self.rbType.GetSelection())} return data +class ColorCtrlPanel(wx.Panel): + """Control panel shown when a Z/color variable is selected.""" + COLORMAP = 'viridis' + + def __init__(self, parent): + super(ColorCtrlPanel, self).__init__(parent) + self.parent = parent + self.cb3D = wx.CheckBox(self, -1, '3D view') + self.cb3D.SetValue(False) + # View buttons (shown only in 3D mode) + self.btXY = wx.Button(self, -1, 'x-y plane', style=wx.BU_EXACTFIT) + self.btYZ = wx.Button(self, -1, 'y-z plane', style=wx.BU_EXACTFIT) + self.btXZ = wx.Button(self, -1, 'x-z plane', style=wx.BU_EXACTFIT) + self.btXY.Hide() + self.btYZ.Hide() + self.btXZ.Hide() + dummy_sizer = wx.BoxSizer(wx.HORIZONTAL) + dummy_sizer.Add(self.cb3D , 0, flag=wx.CENTER|wx.LEFT, border=8) + dummy_sizer.Add(self.btXY , 0, flag=wx.CENTER|wx.LEFT, border=8) + dummy_sizer.Add(self.btYZ , 0, flag=wx.CENTER|wx.LEFT, border=4) + dummy_sizer.Add(self.btXZ , 0, flag=wx.CENTER|wx.LEFT, border=4) + self.SetSizer(dummy_sizer) + self.Bind(wx.EVT_CHECKBOX, self.on3DChange, self.cb3D) + self.Bind(wx.EVT_BUTTON, self.onViewXY, self.btXY) + self.Bind(wx.EVT_BUTTON, self.onViewYZ, self.btYZ) + self.Bind(wx.EVT_BUTTON, self.onViewXZ, self.btXZ) + self.Hide() + + def on3DChange(self, event=None): + is3D = self.cb3D.IsChecked() + self.btXY.Show(is3D) + self.btYZ.Show(is3D) + self.btXZ.Show(is3D) + self.GetSizer().Layout() + self.parent.load_and_draw() + + def _setView(self, elev, azim): + for ax in self.parent.fig.axes: + if hasattr(ax, 'view_init'): + ax.view_init(elev=elev, azim=azim) + self.parent.canvas.draw() + + def onViewXY(self, event=None): + self._setView(elev=90, azim=-90) + + def onViewYZ(self, event=None): + self._setView(elev=0, azim=0) + + def onViewXZ(self, event=None): + self._setView(elev=0, azim=-90) + + def _GUI2Data(self): + return { + 'colormap': self.COLORMAP, + 'colorbar': True, + 'view3D': self.cb3D.IsChecked(), + } + + class SpectralCtrlPanel(wx.Panel): def __init__(self, parent): super(SpectralCtrlPanel,self).__init__(parent) @@ -593,11 +676,12 @@ def __init__(self, parent, selPanel, pipeLike=None, infoPanel=None, data=None): # --- Tool Panel self.toolSizer= wx.BoxSizer(wx.VERTICAL) # --- Plot type specific options - self.spcPanel = SpectralCtrlPanel(self) - self.pdfPanel = PDFCtrlPanel(self) - self.cmpPanel = CompCtrlPanel(self) - self.mmxPanel = MinMaxPanel(self) - self.polPanel = PolarPanel(self) + self.spcPanel = SpectralCtrlPanel(self) + self.pdfPanel = PDFCtrlPanel(self) + self.cmpPanel = CompCtrlPanel(self) + self.mmxPanel = MinMaxPanel(self) + self.polPanel = PolarPanel(self) + self.colorPanel = ColorCtrlPanel(self) # --- PlotType Panel (Needs the different pansel above) self.pltTypePanel= PlotTypePanel(self); @@ -702,18 +786,19 @@ def __init__(self, parent, selPanel, pipeLike=None, infoPanel=None, data=None): self.slEsth = wx.StaticLine(self, -1, size=wx.Size(-1,1), style=wx.LI_HORIZONTAL) self.slEsth.Hide() sl1 = wx.StaticLine(self, -1, size=wx.Size(-1,1), style=wx.LI_HORIZONTAL) - plotsizer.Add(self.toolSizer,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) - plotsizer.Add(self.canvas ,1,flag = wx.EXPAND,border = 5 ) - plotsizer.Add(sl1 ,0,flag = wx.EXPAND,border = 0) - plotsizer.Add(self.spcPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) - plotsizer.Add(self.pdfPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) - plotsizer.Add(self.cmpPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) - plotsizer.Add(self.mmxPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) - plotsizer.Add(self.polPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) - plotsizer.Add(self.slEsth ,0,flag = wx.EXPAND,border = 0) - plotsizer.Add(self.esthPanel,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) - plotsizer.Add(self.slCtrl ,0,flag = wx.EXPAND,border = 0) - plotsizer.Add(row_sizer ,0,flag = wx.EXPAND|wx.NORTH ,border = 2) + plotsizer.Add(self.toolSizer ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) + plotsizer.Add(self.canvas ,1,flag = wx.EXPAND,border = 5 ) + plotsizer.Add(sl1 ,0,flag = wx.EXPAND,border = 0) + plotsizer.Add(self.spcPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) + plotsizer.Add(self.pdfPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) + plotsizer.Add(self.cmpPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) + plotsizer.Add(self.mmxPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) + plotsizer.Add(self.polPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) + plotsizer.Add(self.colorPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) + plotsizer.Add(self.slEsth ,0,flag = wx.EXPAND,border = 0) + plotsizer.Add(self.esthPanel ,0,flag = wx.EXPAND|wx.CENTER|wx.TOP|wx.BOTTOM,border = 10) + plotsizer.Add(self.slCtrl ,0,flag = wx.EXPAND,border = 0) + plotsizer.Add(row_sizer ,0,flag = wx.EXPAND|wx.NORTH ,border = 2) self.SetSizer(plotsizer) self.plotsizer=plotsizer; @@ -957,13 +1042,19 @@ def sharex(self): return self.cbSync.IsChecked() and (not self.pltTypePanel.cbPDF.GetValue()) def set_subplots(self,nPlots): + # Determine if 3D view is requested + hasZ = any(pd.z is not None for pd in self.plotData) + use3D = hasZ and self.colorPanel.cb3D.IsChecked() # Creating subplots for ax in self.fig.axes: self.fig.delaxes(ax) sharex=None for i in range(nPlots): # Vertical stack - if i==0: + if use3D: + ax = self.fig.add_subplot(nPlots, 1, i+1, projection='3d') + _patch_3d_ctrl_rotate(ax, self.canvas) + elif i==0: ax=self.fig.add_subplot(nPlots,1,i+1) # Store first axis to share with other if self.sharex: @@ -1178,13 +1269,21 @@ def getPlotData(self, plotType=None): for i,idx in enumerate(ID): # Initialize each plotdata based on selected table and selected id channels PD = PlotData(); - PD.fromIDs(tabs, i, idx, SameCol, pipeline=self.pipeLike) + PD.fromIDs(tabs, i, idx, SameCol, pipeline=self.pipeLike) self.transformPlotData(PD, firstCall=i==0) self.plotData.append(PD) except Exception as e: self.plotData=[] raise e + # Show/hide colorPanel based on whether any plot has a Z/color variable + hasZ = any(pd.z is not None for pd in self.plotData) + if hasZ: + self.colorPanel.Show() + else: + self.colorPanel.Hide() + self.plotsizer.Layout() + def PD_Compare(self,mode): """ Perform comparison of the selected PlotData, returns new plotData with the comparison. """ sComp = self.cmpPanel.rbType.GetStringSelection() @@ -1528,6 +1627,11 @@ def plot_all(self, autoscale=True): ax_right.set_ylabel(' and '.join(yright_labels), **font_options) elif ax_right is not None: ax_right.set_ylabel('') + # Z label for 3D plots + if hasattr(ax_left, 'set_zlabel'): + z_labels = unique([PD[i].sz for i in ax_left.iPD if PD[i].z is not None]) + if len(z_labels) > 0 and len(z_labels) <= 3: + ax_left.set_zlabel(' and '.join(z_labels), **font_options) # Legends lgdLoc = plotStyle['LegendPosition'].lower() @@ -1562,9 +1666,15 @@ def plot_all(self, autoscale=True): # NOTE: cursors needs to be stored in the object! #for ax_left in self.fig.axes: # self.cursors.append(MyCursor(ax_left,horizOn=True, vertOn=False, useblit=True, color='gray', linewidth=0.5, linestyle=':')) - # Vertical cusor for all, commonly + # Vertical cusor for all, commonly (not supported for 3D axes) bXHair = self.cbXHair.GetValue() - self.multiCursors = MyMultiCursor(self.canvas, tuple(self.fig.axes), useblit=True, horizOn=bXHair, vertOn=bXHair, color='gray', linewidth=0.5, linestyle=':') + hasZ = any(pd.z is not None for pd in PD) + use3D = hasZ and self.colorPanel.cb3D.IsChecked() + if not use3D: + try: + self.multiCursors = MyMultiCursor(self.canvas, tuple(self.fig.axes), useblit=True, horizOn=bXHair, vertOn=bXHair, color='gray', linewidth=0.5, linestyle=':') + except Exception: + pass def plotSignals(self, ax, axis_idx, PD, pm, left_right, opts): axis = None @@ -1574,6 +1684,12 @@ def plotSignals(self, ax, axis_idx, PD, pm, left_right, opts): else: loop_range = range(len(PD)) + # Gather color-panel options once + colorOpts = self.colorPanel._GUI2Data() + colormap = colorOpts['colormap'] + showColorBar = colorOpts['colorbar'] + use3D = colorOpts['view3D'] + iPlot=-1 for signal_idx in loop_range: do_plot = False @@ -1589,23 +1705,51 @@ def plotSignals(self, ax, axis_idx, PD, pm, left_right, opts): axis._get_lines.prop_cycler = ax._get_lines.prop_cycler pd=PD[signal_idx] if do_plot: - iPlot+=1 - # --- styling per plot - if len(pd.x)==1: - marker='o'; ls='' - else: - # TODO allow PlotData to override for "per plot" options in the future - marker = opts['Markers'][np.mod(iPlot,len(opts['Markers']))] - ls = opts['LineStyles'][np.mod(iPlot,len(opts['LineStyles']))] - if opts['step']: - plot = axis.step + iPlot+=1 + hasZ = pd.z is not None and not pd.zIsString + if hasZ and use3D: + # 3D scatter: x, y, z as spatial axes + try: + sc = axis.scatter(pd.x, pd.y, pd.z, label=pd.syl, s=opts['ms']**2, + cmap=colormap, c=pd.z) + if showColorBar: + cb = self.fig.colorbar(sc, ax=axis, label=pd.sz, shrink=0.7, pad=0.1) + except Exception: + axis.scatter(pd.x, pd.y, pd.z, label=pd.syl, s=opts['ms']**2) + try: + bAllNeg = bAllNeg and all(pd.y<=0) + except Exception: + pass + elif hasZ: + # 2D scatter colored by Z variable + try: + sc = axis.scatter(pd.x, pd.y, c=pd.z, cmap=colormap, + label=pd.syl, s=opts['ms']**2) + if showColorBar: + cb = self.fig.colorbar(sc, ax=axis, label=pd.sz) + except Exception: + axis.scatter(pd.x, pd.y, label=pd.syl, s=opts['ms']**2) + try: + bAllNeg = bAllNeg and all(pd.y<=0) + except Exception: + pass else: - plot = axis.plot - plot(pd.x,pd.y,label=pd.syl,ms=opts['ms'], lw=opts['lw'], marker=marker, ls=ls) - try: - bAllNeg = bAllNeg and all(pd.y<=0) - except: - pass # Dates or strings + # --- styling per plot + if len(pd.x)==1: + marker='o'; ls='' + else: + # TODO allow PlotData to override for "per plot" options in the future + marker = opts['Markers'][np.mod(iPlot,len(opts['Markers']))] + ls = opts['LineStyles'][np.mod(iPlot,len(opts['LineStyles']))] + if opts['step']: + plot = axis.step + else: + plot = axis.plot + plot(pd.x,pd.y,label=pd.syl,ms=opts['ms'], lw=opts['lw'], marker=marker, ls=ls) + try: + bAllNeg = bAllNeg and all(pd.y<=0) + except: + pass # Dates or strings return axis, bAllNeg def findPlotMode(self,PD): @@ -1865,13 +2009,20 @@ def _store_limits(self): self.xlim_prev = [] self.ylim_prev = [] for ax in self.fig.axes: - self.xlim_prev.append(ax.get_xlim_()) - self.ylim_prev.append(ax.get_ylim_()) + try: + self.xlim_prev.append(ax.get_xlim_()) + self.ylim_prev.append(ax.get_ylim_()) + except AttributeError: + self.xlim_prev.append((0, 1)) + self.ylim_prev.append((0, 1)) def _restore_limits(self): for ax, xlim, ylim in zip(self.fig.axes, self.xlim_prev, self.ylim_prev): - ax.set_xlim_(xlim) - ax.set_ylim_(ylim) + try: + ax.set_xlim_(xlim) + ax.set_ylim_(ylim) + except AttributeError: + pass if __name__ == '__main__': import pandas as pd; diff --git a/pydatview/GUISelectionPanel.py b/pydatview/GUISelectionPanel.py index 69433a5..9e8f045 100644 --- a/pydatview/GUISelectionPanel.py +++ b/pydatview/GUISelectionPanel.py @@ -744,6 +744,11 @@ def __init__(self, parent, selPanel): self.comboX.SetFont(getMonoFont(self)) self.lbColumns=wx.ListBox(self, -1, choices=[], style=wx.LB_EXTENDED ) self.lbColumns.SetFont(getMonoFont(self)) + # Z/Color variable selector + self.lbZ = wx.StaticText(self, -1, 'z-axis:') + self.comboZ = wx.ComboBox(self, choices=['None'], style=wx.CB_READONLY) + self.comboZ.SetFont(getMonoFont(self)) + self.comboZ.SetSelection(0) # Events self.lbColumns.Bind(wx.EVT_RIGHT_DOWN, self.OnColPopup) self.lbColumns.Bind(wx.EVT_MOTION, self.OnColMotion) @@ -751,6 +756,9 @@ def __init__(self, parent, selPanel): # Layout sizerX = wx.BoxSizer(wx.HORIZONTAL) sizerX.Add(self.comboX , 1, flag=wx.TOP | wx.BOTTOM, border=2) + sizerZ = wx.BoxSizer(wx.HORIZONTAL) + sizerZ.Add(self.lbZ , 0, flag=wx.CENTER|wx.RIGHT, border=2) + sizerZ.Add(self.comboZ , 1, flag=wx.TOP | wx.BOTTOM, border=2) sizerF = wx.BoxSizer(wx.HORIZONTAL) sizerF.Add(self.tFilter, 1, flag= wx.CENTER|wx.TOP , border=0) @@ -762,6 +770,7 @@ def __init__(self, parent, selPanel): sizerCol.Add(tb , 0, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM,border=1) #sizerCol.Add(self.comboX , 0, flag=wx.TOP|wx.RIGHT|wx.BOTTOM|wx.TOP,border=2) sizerCol.Add(sizerX , 0, flag=wx.EXPAND, border=0) + sizerCol.Add(sizerZ , 0, flag=wx.EXPAND, border=0) sizerCol.Add(sizerF , 0, flag=wx.EXPAND|wx.TOP|wx.BOTTOM, border=0) sizerCol.Add(self.lbColumns, 2, flag=wx.EXPAND, border=0) self.SetSizer(sizerCol) @@ -827,11 +836,13 @@ def getGUIcolumns(self): def _setReadOnly(self): self.bReadOnly=True self.comboX.Enable(False) + self.comboZ.Enable(False) self.lbColumns.Enable(False) def _unsetReadOnly(self): self.bReadOnly=False self.comboX.Enable(True) + self.comboZ.Enable(True) self.lbColumns.Enable(True) def setReadOnly(self, tabLabel=None, cols=[]): @@ -970,6 +981,15 @@ def setGUIColumns(self, xSel=-1, ySel=[], selInFull=True): columnsX_show=columnsX self.comboX.Set(columnsX_show) # non filtered + # Populate comboZ with None + same columns as comboX (full, non-filtered) + prevZSel = self.comboZ.GetSelection() + columnsZ_show = np.append(['None'], columnsX_show if len(columnsX) > MAX_X_COLUMNS else columnsX) + self.comboZ.Set(columnsZ_show) + if prevZSel >= 0 and prevZSel < len(columnsZ_show): + self.comboZ.SetSelection(prevZSel) + else: + self.comboZ.SetSelection(0) # default: None + # Set selection for y, if any, and considering filtering if selInFull: for iFull in ySel: @@ -1006,10 +1026,14 @@ def forceZeroSelection(self): def empty(self): self.lbColumns.Clear() self.comboX.Clear() + self.comboZ.Clear() + self.comboZ.Append('None') + self.comboZ.SetSelection(0) self.lb.SetLabel('') self.bReadOnly=False self.lbColumns.Enable(False) self.comboX.Enable(False) + self.comboZ.Enable(False) self.bt.Enable(False) self.tab=None self.columns=[] @@ -1045,6 +1069,20 @@ def getColumnSelection(self): self.setGUIColumns(xSel=iXFull, ySel=IYFull) return iXFull,IYFull,sX,SY + def getZColumnSelection(self): + """ + Return the Z/color column selection. + iZ: index in full table (-1 means 'None'/no Z variable) + sZ: column name string + """ + iZ = self.comboZ.GetSelection() + if iZ <= 0: # 0 = 'None', -1 = nothing selected + return -1, '' + # iZ-1 because first item is 'None' + iZFull = iZ - 1 + sZ = self.comboZ.GetStringSelection() + return iZFull, sZ + def onClearFilter(self, event=None): self.tFilter.SetValue('') self.onFilterChange() @@ -1111,10 +1149,13 @@ def __init__(self, parent, tabList, mode='auto', mainframe=None): # BINDINGS self.Bind(wx.EVT_COMBOBOX, self.onColSelectionChange, self.colPanel1.comboX ) self.Bind(wx.EVT_LISTBOX , self.onColSelectionChange, self.colPanel1.lbColumns) + self.Bind(wx.EVT_COMBOBOX, self.onColSelectionChange, self.colPanel1.comboZ ) self.Bind(wx.EVT_COMBOBOX, self.onColSelectionChange, self.colPanel2.comboX ) self.Bind(wx.EVT_LISTBOX , self.onColSelectionChange, self.colPanel2.lbColumns) + self.Bind(wx.EVT_COMBOBOX, self.onColSelectionChange, self.colPanel2.comboZ ) self.Bind(wx.EVT_COMBOBOX, self.onColSelectionChange, self.colPanel3.comboX ) self.Bind(wx.EVT_LISTBOX , self.onColSelectionChange, self.colPanel3.lbColumns) + self.Bind(wx.EVT_COMBOBOX, self.onColSelectionChange, self.colPanel3.comboZ ) self.Bind(wx.EVT_LISTBOX, self.onTabSelectionChange, self.tabPanel.lbTab) # TRIGGERS @@ -1555,6 +1596,7 @@ def getPlotDataSelection(self): ITab,STab = self.getSelectedTables() if self.currentMode=='simColumnsMode' and len(ITab)>1: iiX1,IY1,ssX1,SY1 = self.colPanel1.getColumnSelection() + iZ1,sZ1 = self.colPanel1.getZColumnSelection() SameCol=False for i,(itab,stab) in enumerate(zip(ITab,STab)): IKeep=self.IKeepPerTab[i] @@ -1563,26 +1605,31 @@ def getPlotDataSelection(self): sy = self.tabList[itab].columns[IKeep[iiy]] iX1 = IKeep[iiX1] sX1 = self.tabList[itab].columns[IKeep[iiX1]] - ID.append([itab,iX1,iy,sX1,sy,stab]) + iZ = IKeep[iZ1] if iZ1 >= 0 and iZ1 < len(IKeep) else -1 + szZ = self.tabList[itab].columns[iZ] if iZ >= 0 else '' + ID.append([itab,iX1,iy,sX1,sy,stab,iZ,szZ]) else: iX1,IY1,sX1,SY1 = self.colPanel1.getColumnSelection() + iZ1,sZ1 = self.colPanel1.getZColumnSelection() SameCol=self.tabList.haveSameColumns(ITab) if self.nSplits in [0,1] or SameCol: for i,(itab,stab) in enumerate(zip(ITab,STab)): for j,(iy,sy) in enumerate(zip(IY1,SY1)): - ID.append([itab,iX1,iy,sX1,sy,stab]) + ID.append([itab,iX1,iy,sX1,sy,stab,iZ1,sZ1]) elif self.nSplits in [2,3]: if len(ITab)>=1: for j,(iy,sy) in enumerate(zip(IY1,SY1)): - ID.append([ITab[0],iX1,iy,sX1,sy,STab[0]]) + ID.append([ITab[0],iX1,iy,sX1,sy,STab[0],iZ1,sZ1]) if len(ITab)>=2: iX2,IY2,sX2,SY2 = self.colPanel2.getColumnSelection() + iZ2,sZ2 = self.colPanel2.getZColumnSelection() for j,(iy,sy) in enumerate(zip(IY2,SY2)): - ID.append([ITab[1],iX2,iy,sX2,sy,STab[1]]) + ID.append([ITab[1],iX2,iy,sX2,sy,STab[1],iZ2,sZ2]) if len(ITab)>=3: - iX2,IY2,sX2,SY2 = self.colPanel3.getColumnSelection() - for j,(iy,sy) in enumerate(zip(IY2,SY2)): - ID.append([ITab[2],iX2,iy,sX2,sy,STab[2]]) + iX3,IY3,sX3,SY3 = self.colPanel3.getColumnSelection() + iZ3,sZ3 = self.colPanel3.getZColumnSelection() + for j,(iy,sy) in enumerate(zip(IY3,SY3)): + ID.append([ITab[2],iX3,iy,sX3,sy,STab[2],iZ3,sZ3]) else: raise Exception('Wrong number of splits {}'.format(self.nSplits)) return ID,SameCol,self.currentMode diff --git a/pydatview/figure.py b/pydatview/figure.py index 802a60e..af1f1cd 100644 --- a/pydatview/figure.py +++ b/pydatview/figure.py @@ -11,7 +11,16 @@ def add_subplot(self, *args, projection='swappy', swap=False, **kwargs): # See matplotlib.projections/__init__.py projection_registry.register kwargs.update({'projection':projection}) ax = super().add_subplot(*args, **kwargs) - ax.setSwap(swap) + if hasattr(ax, 'setSwap'): + ax.setSwap(swap) + else: + # Non-SwappyAxes (e.g. Axes3D): add compatibility shims + ax.setSwap = lambda s: None + ax.set_xlim_ = lambda *a, **kw: ax.set_xlim(*a, **kw) + ax.set_ylim_ = lambda *a, **kw: ax.set_ylim(*a, **kw) + ax.get_xlim_ = lambda *a, **kw: ax.get_xlim(*a, **kw) + ax.get_ylim_ = lambda *a, **kw: ax.get_ylim(*a, **kw) + ax.axvline_ = lambda x, *a, **kw: None return ax class SwappyAxes(plt.Axes): diff --git a/pydatview/plotdata.py b/pydatview/plotdata.py index 820dd82..1b835fd 100644 --- a/pydatview/plotdata.py +++ b/pydatview/plotdata.py @@ -47,6 +47,11 @@ def __init__(PD, x=None, y=None, sx='', sy=''): PD.xIsDate =False # true if dates PD.yIsString=False # true if strings PD.yIsDate =False # true if dates + PD.iz =-1 # z/color column index (-1 = no Z variable) + PD.sz ='' # z/color label + PD.z =None # z/color data (None when not used) + PD.zIsString=False # true if strings + PD.zIsDate =False # true if dates # Misc data PD._xMin = None PD._xMax = None @@ -91,6 +96,17 @@ def fromIDs(PD, tabs, i, idx, SameCol, pipeline=None): PD.x, PD.xIsString, PD.xIsDate,_ = tabs[PD.it].getColumn(PD.ix) # actual x data, with info PD.y, PD.yIsString, PD.yIsDate,c = tabs[PD.it].getColumn(PD.iy) # actual y data, with info PD.c =c # raw values, used by PDF + # Z/color variable (optional, idx[6] and idx[7] if provided) + if len(idx) >= 8 and idx[6] >= 0: + PD.iz = idx[6] + PD.sz = idx[7].replace('_', ' ') if idx[7] else '' + PD.z, PD.zIsString, PD.zIsDate, _ = tabs[PD.it].getColumn(PD.iz) + else: + PD.iz = -1 + PD.sz = '' + PD.z = None + PD.zIsString = False + PD.zIsDate = False PD._post_init(pipeline=pipeline) diff --git a/setup.py b/setup.py index 60bbb15..401234d 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ name='pydatview', version='0.5', description='GUI to display tabulated data from files or pandas dataframes', - url='http://github.com/ebranlard/pyDatView/', + url='https://github.com/SimonHH/pyDatView/', author='Emmanuel Branlard', author_email='lastname@gmail.com', license='MIT',