Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Expand All @@ -130,13 +131,17 @@ 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
- Scaling data ("min/max") when ranges and means are different
- 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
Expand Down
233 changes: 192 additions & 41 deletions pydatview/GUIPlotPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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;
Expand Down
Loading