plotWidget.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. """
  2. This Module implements the Plot Windows used in KCG
  3. """
  4. import pyqtgraph as pg
  5. try: # on some versions of pyqtgraph useOpenGl is no valid option
  6. pg.setConfigOption('useOpenGl', True)
  7. except:
  8. pass
  9. pg.setConfigOption('background', 'w')
  10. pg.setConfigOption('foreground', 'k')
  11. import numpy as np
  12. from PyQt4 import QtCore, QtGui
  13. from groupedelements import live_plot_windows
  14. from backend import io
  15. from backend import board
  16. from backend.board import available_boards
  17. import kcgwidget as kcgw
  18. from .. import config
  19. tr = kcgw.tr
  20. LIVE = 1
  21. FILE = 2
  22. ERROR = 99
  23. NO_DATA = 98
  24. class Enum():
  25. """
  26. Simple Enum Class (as this is not supported by clean python)
  27. """
  28. def __init__(self, *args):
  29. self.idx = 0
  30. for i in args:
  31. setattr(self, i, self.idx)
  32. self.idx += 1
  33. plotList = [tr("Label", "Heatmap"), tr("Label", "FFT"), tr("Label", "Trains"), tr("Label", "Combined"), tr("Label", "Compare"), tr("Label", "Follow")]
  34. PlotType = Enum(*[str(i) for i in plotList])
  35. # gradient = {'mode': 'rgb',
  36. # 'ticks': [(0.0, (0.37281, 0.11883, 3.53583, 255)),
  37. # (0.0390625, (4.9401150000000005, 3.858915, 22.635585, 255)),
  38. # (0.078125, (15.6417, 9.330449999999999, 45.29871, 255)),
  39. # (0.1171875, (29.74728, 12.13137, 69.44185499999999, 255)),
  40. # (0.15625, (46.774395000000005, 10.283895, 90.51760499999999, 255)),
  41. # (0.1953125, (64.1631, 9.614775, 102.86139, 255)),
  42. # (0.234375, (80.65191, 13.63995, 108.40458, 255)),
  43. # (0.2734375, (96.64525499999999, 19.444515, 110.343345, 255)),
  44. # (0.3125, (112.507785, 25.33119, 110.05646999999999, 255)),
  45. # (0.3515625, (128.390715, 31.001625, 107.95578, 255)),
  46. # (0.390625, (144.29277, 36.609585, 104.10579, 255)),
  47. # (0.4296875, (160.100985, 42.476625, 98.50038, 255)),
  48. # (0.46875, (175.606515, 49.020945, 91.188765, 255)),
  49. # (0.5078125, (190.517385, 56.70639, 82.32827999999999, 255)),
  50. # (0.546875, (204.477105, 65.96187, 72.190245, 255)),
  51. # (0.5859375, (217.10292, 77.07629999999999, 61.10718, 255)),
  52. # (0.625, (228.047775, 90.11674500000001, 49.36392, 255)),
  53. # (0.6640625, (237.05922, 104.927145, 37.068585, 255)),
  54. # (0.703125, (243.99726, 121.21578, 24.147225, 255)),
  55. # (0.7421875, (248.797635, 138.66849, 11.122589999999999, 255)),
  56. # (0.78125, (251.41776000000002, 157.01625, 6.52596, 255)),
  57. # (0.8203125, (251.80332, 176.04333, 20.397450000000003, 255)),
  58. # (0.859375, (249.90816, 195.543435, 42.420015, 255)),
  59. # (0.8984375, (245.92047, 215.18124, 69.714705, 255)),
  60. # (0.9375, (241.63647, 233.936745, 104.719575, 255)),
  61. # (0.9765625, (245.26206, 248.86062, 145.840875, 255)),
  62. # (0.99609375, (252.03231, 254.58282, 164.45562, 255))]}
  63. gradient = {'mode': 'rgb',
  64. 'ticks': [(0.0, (0.37281, 0.11883, 3.53583, 255)),
  65. (0.1171875, (29.74728, 12.13137, 69.44185499999999, 255)),
  66. (0.234375, (80.65191, 13.63995, 108.40458, 255)),
  67. (0.3515625, (128.390715, 31.001625, 107.95578, 255)),
  68. (0.46875, (175.606515, 49.020945, 91.188765, 255)),
  69. (0.5859375, (217.10292, 77.07629999999999, 61.10718, 255)),
  70. (0.703125, (243.99726, 121.21578, 24.147225, 255)),
  71. (0.8203125, (251.80332, 176.04333, 20.397450000000003, 255)),
  72. (0.9375, (241.63647, 233.936745, 104.719575, 255)),
  73. (0.99609375, (252.03231, 254.58282, 164.45562, 255))]}
  74. class CustomGradientEditorItem(pg.GradientEditorItem):
  75. """
  76. A Gradient Editor Item to insert a perception linear gradient
  77. """
  78. def __init__(self, **kwargs):
  79. pg.GradientEditorItem.__init__(self, **kwargs)
  80. self.customGradients = {}
  81. def addGrad(self, name, grad_dic):
  82. """
  83. Add a gradient to the list of gradients in the gui
  84. :param name: the name of the gradient
  85. :param grad_dic: the dictionary containing the gradient data
  86. """
  87. if name not in self.customGradients:
  88. self.customGradients[name] = grad_dic
  89. px = QtGui.QPixmap(100, 15)
  90. p = QtGui.QPainter(px)
  91. self.restoreState(grad_dic)
  92. length_backup = self.length
  93. self.length = 100
  94. grad = self.getGradient()
  95. self.length = length_backup
  96. brush = QtGui.QBrush(grad)
  97. p.fillRect(QtCore.QRect(0, 0, 100, 15), brush)
  98. p.end()
  99. label = QtGui.QLabel()
  100. label.setPixmap(px)
  101. label.setContentsMargins(1, 1, 1, 1)
  102. act = QtGui.QWidgetAction(self)
  103. act.setDefaultWidget(label)
  104. act.triggered.connect(self.contextMenuClicked)
  105. act.name = name
  106. if len(self.customGradients) > 1:
  107. self.menu.insertAction(self.menu.actions()[0], act)
  108. else:
  109. sep = self.menu.insertSeparator(self.menu.actions()[0])
  110. self.menu.insertAction(sep, act)
  111. self.restoreState(grad_dic)
  112. def restoreState(self, state):
  113. """
  114. Reimplemented of pyqtgraph.GradientEditorItem.restoreState to work with our custom perception linear gradient.
  115. :param state: the state to restore to
  116. """
  117. self.setColorMode(state['mode'])
  118. for t in list(self.ticks.keys()):
  119. self.removeTick(t, finish=False)
  120. self.bottomTick = [None, 10]
  121. self.topmostTick = [None, -1]
  122. for t in state['ticks']:
  123. c = QtGui.QColor(*t[1])
  124. tick = self.addTick(t[0], c, finish=False)
  125. if t[0] < self.bottomTick[1]:
  126. self.bottomTick = [tick, t[0]]
  127. if t[0] > self.topmostTick[1]:
  128. self.topmostTick = [tick, t[0]]
  129. self.tickPosInPercent = self.ticks.copy()
  130. for t in self.ticks:
  131. if t is self.topmostTick[0] or t is self.bottomTick[0]:
  132. continue
  133. t.hide()
  134. t.movable = False
  135. # self.bottomTick[1] *= self.length
  136. # self.topmostTick[1] *= self.length
  137. self.updateGradient()
  138. self.sigGradientChangeFinished.emit(self)
  139. def loadPreset(self, name):
  140. """
  141. Reimplemented of pyqtgraph.GradientEditorItem.loadPreset to work with our custom perception linear gradient.
  142. :param name: the name of the preset to load
  143. """
  144. if name in self.customGradients:
  145. self.restoreState(self.customGradients[name])
  146. else:
  147. super(CustomGradientEditorItem, self).loadPreset(name)
  148. def tickMoved(self, tick, pos):
  149. """
  150. Reimplemented of pyqtgraph.GradientEditorItem.tickMoved to work with our custom perception linear gradient,
  151. which has a lot of steps which would create a lot of ticks in the gradient legend. This removes all but 2
  152. ticks and aligns all the internal ticks accordingly.
  153. :param tick: the tick to move
  154. :param pos: the position to move to
  155. """
  156. if tick is self.bottomTick[0]:
  157. # self.bottomTick[1] = pos.x()
  158. pg.TickSliderItem.tickMoved(self, tick, pos)
  159. elif tick is self.topmostTick[0]:
  160. # self.topmostTick[1] = pos.x()
  161. pg.TickSliderItem.tickMoved(self, tick, pos)
  162. if tick in [self.bottomTick[0], self.topmostTick[0]]: # if bottom or topmost tick was moved
  163. if pos.x() < self.length and pos.x() > 0: # if tick is not at top or bottom
  164. newUnit = (self.topmostTick[0].pos().x() - self.bottomTick[0].pos().x()) # create new virtual unit length
  165. for t in self.ticks: # for every tick
  166. if t not in [self.bottomTick[0], self.topmostTick[0]]: # if tick is not bottom or topmost tick
  167. pos = t.pos()
  168. new_x = self.bottomTick[0].pos().x() + self.tickPosInPercent[t] * newUnit
  169. pos.setX(new_x)
  170. t.setPos(pos)
  171. pg.TickSliderItem.tickMoved(self, t, pos)
  172. self.updateGradient()
  173. class SpectrogramColorLegendItem(pg.GraphicsWidget):
  174. """
  175. The Item used as Legend for Heatmap and FFT Plot
  176. """
  177. gradientChanged = QtCore.pyqtSignal()
  178. def __init__(self, img=None):
  179. """
  180. Initialise the Legend
  181. :param img: (ImageItem) the image item this is the legend for
  182. :return: -
  183. """
  184. super(SpectrogramColorLegendItem, self).__init__()
  185. self.layout = QtGui.QGraphicsGridLayout()
  186. self.setLayout(self.layout)
  187. self.img = img
  188. # self.gei = pg.GradientEditorItem(orientation='right')
  189. self.gei = CustomGradientEditorItem(orientation='right', allowAdd=False)
  190. self.legend_axis = pg.AxisItem(orientation='left')
  191. # self.addItem(self.legend_axis)
  192. self.layout.addItem(self.legend_axis, 0, 0, alignment=QtCore.Qt.AlignVCenter)
  193. # self.addItem(self.gei)
  194. self.layout.addItem(self.gei, 0, 1)
  195. self.gei.sigGradientChanged.connect(self.gradient_changed)
  196. # self.gei.loadPreset('spectrum')
  197. self.gei.addGrad('inferno', gradient)
  198. # self.setFixedWidth(80)
  199. self.image_changed()
  200. def gradient_changed(self):
  201. """
  202. Proxy function for the gradient_cnahged event of the GradientEditorItem
  203. :return:
  204. """
  205. self.gradientChanged.emit()
  206. def set_image(self, img):
  207. """
  208. Set the Image this is the legend for (only needed if not already done upon initialisation)
  209. :param img: (ImageItem) The image item to use
  210. :return: -
  211. """
  212. self.img = img
  213. self.image_changed(True)
  214. def update_axis(self):
  215. """
  216. Update the axis of this legend
  217. :return: -
  218. """
  219. if self.img.getLevels():
  220. # min, max = self.img.getLevels()
  221. min, max = (np.min(self.img.image), np.max(self.img.image))
  222. self.legend_axis.setRange(min, max)
  223. def reset_gradient(self):
  224. """
  225. Reset the gradient to the preset "spectrum"
  226. :return: -
  227. """
  228. # self.gei.loadPreset('spectrum')
  229. self.gei.loadPreset('inferno')
  230. # self.gei.restoreState(gradient)
  231. def image_changed(self, reset_gradient=True):
  232. """
  233. Call this to adjust the legend when the image has changed
  234. :param reset_gradient: (bool) whether to reset the gradient or not
  235. :return:
  236. """
  237. if self.img:
  238. self.update_axis()
  239. if reset_gradient:
  240. self.reset_gradient()
  241. def getLookupTable(self, nPts, alpha=None):
  242. """
  243. Get the look up table of the imageitem
  244. :param nPts: (int) number of points the lookup table is defined on
  245. :param alpha: ??
  246. :return: -
  247. """
  248. return self.gei.getLookupTable(nPts, alpha=alpha)
  249. def resizeEvent(self, event):
  250. """
  251. Handle resizing of the window
  252. """
  253. self.legend_axis.setHeight(self.gei.length)
  254. class SubPlotWidget(pg.GraphicsLayoutWidget):
  255. """
  256. The Widget actually containing the plots and images
  257. """
  258. def __init__(self, dType=FILE):
  259. super(SubPlotWidget, self).__init__()
  260. self.dType = dType
  261. self._type_changed = False
  262. # self.setStyleSheet("border: 5px solid black; margin: 10px;")
  263. self.plotType = 1
  264. self.plotItem = pg.PlotItem()
  265. self.img = pg.ImageItem()
  266. self.plotItem2 = pg.PlotItem()
  267. self.img2 = pg.ImageItem()
  268. self.plotItem2.addItem(self.img2)
  269. self.setFrameStyle(self.StyledPanel | self.Sunken)
  270. self.gradient_legend = SpectrogramColorLegendItem(self.img)
  271. def changelut():
  272. """
  273. Handle changing of the lookup table
  274. """
  275. self.img.setLookupTable(self.gradient_legend.getLookupTable(512))
  276. self.img2.setLookupTable(self.gradient_legend.getLookupTable(512))
  277. self.gradient_legend.gradientChanged.connect(changelut)
  278. self.addItem(self.plotItem)
  279. self.plotItem.addItem(self.img)
  280. self.plotItemPlotScatter = pg.PlotDataItem(pen=None, symbolPen=pg.mkPen(50, 50, 255), symbolSize=3, pxMode=True, symbolBrush=pg.mkBrush(50, 50, 255), downsample=200)
  281. self.plotItemPlotScatter1 = pg.PlotDataItem(pen=None, symbolPen=pg.mkPen(255, 50, 50), symbolSize=3, pxMode=True, symbolBrush=pg.mkBrush(255, 50, 50), downsample=200)
  282. # self.plotItemPlotScatter2 = pg.PlotDataItem(pen=None, symbolPen=pg.mkPen(50, 255, 50), symbolSize=3, pxMode=True, symbolBrush=pg.mkBrush(50, 255, 50), downsample=200)
  283. self.plotItemPlot = pg.PlotDataItem() # pen="#000000")
  284. self.plotItem.addItem(self.plotItemPlot)
  285. self.plotItem.addItem(self.plotItemPlotScatter)
  286. self.plotItem.addItem(self.plotItemPlotScatter1)
  287. # self.plotItem.addItem(self.plotItemPlotScatter2)
  288. self.plotItem.vb.origAutoRange = self.plotItem.vb.autoRange
  289. self.error_label = pg.LabelItem("Error during plot.", color=(200, 50, 50))
  290. self.error_label.hide()
  291. self.error_label.scale(1, -0.6)
  292. self.error_label.translate(0, -30)
  293. self.plotItem.addItem(self.error_label)
  294. self.no_data_label = pg.LabelItem("No data to plot.", color=(200, 50, 50))
  295. self.no_data_label.hide()
  296. self.no_data_label.scale(1, -0.6)
  297. self.no_data_label.translate(0, -30)
  298. self.plotItem.addItem(self.no_data_label)
  299. def _enableCustomAutoRange(self, data):
  300. """
  301. Enable custom auto range in this plot
  302. :param data: the data to autorange
  303. :return:
  304. """
  305. def newAutoRange(*args, **kwargs):
  306. ''' function to handle the new autorange '''
  307. if len(data) == 0:
  308. return
  309. bounds = [np.min(data), np.max(data)]
  310. self.plotItem.vb.setRange(xRange=[0, len(data)],
  311. yRange=[bounds[0]-0.1*(bounds[1]-bounds[0])-1, bounds[1]+0.1*(bounds[1]-bounds[0])+1], update=True)
  312. self.plotItem.update()
  313. self.plotItem.vb.autoRange = newAutoRange
  314. self.plotItem.autoBtn.clicked.disconnect()
  315. self.plotItem.autoBtn.clicked.connect(newAutoRange)
  316. def _disableCustomAutoRange(self):
  317. """
  318. Disable the custom autorange and reset it to default
  319. :return: -
  320. """
  321. self.plotItem.vb.autoRange = self.plotItem.vb.origAutoRange
  322. self.plotItem.autoBtn.clicked.disconnect()
  323. self.plotItem.autoBtn.clicked.connect(self.plotItem.autoBtnClicked)
  324. @QtCore.pyqtSlot(np.ndarray, tuple, tuple)
  325. def plot(self, data, xvalueborders=None, yvalueborders=None, autorange=True):
  326. """
  327. Plot Data. The plot type depends on the type property
  328. :param data: (dataset.DataSet) data to plot
  329. :param xvalueborders: (touple) the borders for the xvalues
  330. :param yvalueborders: (touple) the borders for the yvalues
  331. :param autorange: (bool) whether to perform a autorange or not
  332. :return: -
  333. """
  334. self.error_label.hide()
  335. self.no_data_label.hide()
  336. self.plotItemPlot.clear()
  337. self.plotItemPlotScatter.clear()
  338. self.plotItemPlotScatter1.clear()
  339. # self.plotItemPlotScatter2.clear()
  340. self.plotItem.resetTransform()
  341. self.plotItem.getAxis('bottom').setScale()
  342. self.img.show()
  343. if self._type_changed:
  344. self.plotItem.autoBtnClicked()
  345. try:
  346. self.removeItem(self.plotItem2)
  347. except Exception as e:
  348. if "Could not determine index of item" in str(e):
  349. pass
  350. else:
  351. raise
  352. if self.plotType == PlotType.FFT or self.plotType == PlotType.Heatmap:
  353. self.addItem(self.gradient_legend)
  354. self.gradient_legend.show()
  355. self.img.setImage(data, scale=[1, 1/1000.])
  356. self.img.resetTransform()
  357. self.img.setAutoDownsample(True)
  358. if autorange:
  359. self.gradient_legend.image_changed()
  360. if xvalueborders:
  361. self.plotItem.getAxis('bottom').setScale((xvalueborders[1]-xvalueborders[0])/float(data.shape[0]))
  362. self.img.translate(xvalueborders[0]/self.plotItem.getAxis('bottom').scale, 0)
  363. else:
  364. self.plotItem.getAxis('bottom').setScale()
  365. if yvalueborders:
  366. self.img.translate(0, yvalueborders[0])
  367. self.plotItem.getAxis('left').setScale(yvalueborders[1]/float(data.shape[1]))
  368. self._disableCustomAutoRange()
  369. self.plotItem.setClipToView(True)
  370. if self.plotType == PlotType.Compare:
  371. self.addItem(self.plotItem2)
  372. self.plotItem2.vb.linkView(self.plotItem2.vb.XAxis, self.plotItem.vb)
  373. self.plotItem2.vb.linkView(self.plotItem2.vb.YAxis, self.plotItem.vb)
  374. self.addItem(self.gradient_legend)
  375. self.gradient_legend.show()
  376. self.img.setImage(data[0], scale=[1, 1/1000.])
  377. self.img.show()
  378. self.img.resetTransform()
  379. self.img2.setImage(data[1], scale=[1, 1/1000.])
  380. self.img2.show()
  381. self.img2.resetTransform()
  382. self.img.setAutoDownsample(True)
  383. self.img2.setAutoDownsample(True)
  384. if autorange:
  385. self.gradient_legend.image_changed()
  386. pass
  387. if xvalueborders:
  388. self.plotItem.getAxis('bottom').setScale((xvalueborders[1]-xvalueborders[0])/float(data[0].shape[0]))
  389. self.plotItem2.getAxis('bottom').setScale((xvalueborders[1]-xvalueborders[0])/float(data[1].shape[0]))
  390. self.img.translate(xvalueborders[0]/self.plotItem.getAxis('bottom').scale, 0)
  391. self.img2.translate(xvalueborders[0]/self.plotItem.getAxis('bottom').scale, 0)
  392. else:
  393. self.plotItem.getAxis('bottom').setScale()
  394. self.plotItem2.getAxis('bottom').setScale()
  395. if yvalueborders:
  396. self.img.translate(0, yvalueborders[0])
  397. self.img2.translate(0, yvalueborders[0])
  398. self.plotItem.getAxis('left').setScale(yvalueborders[1]/float(data[0].shape[1]))
  399. self.plotItem2.getAxis('left').setScale(yvalueborders[1]/float(data[1].shape[1]))
  400. self._disableCustomAutoRange()
  401. self.plotItem.setClipToView(True)
  402. if self.plotType == PlotType.Trains or self.plotType == PlotType.Follow:
  403. self.img.clear()
  404. self.plotItemPlot.show()
  405. self.gradient_legend.hide()
  406. try:
  407. self.removeItem(self.gradient_legend)
  408. pass
  409. except Exception, e:
  410. if not "Could not determine index of item" in str(e):
  411. raise
  412. self.plotItemPlotScatter.clear()
  413. self.plotItemPlotScatter1.clear()
  414. self.plotItemPlot.setData(data)
  415. self._enableCustomAutoRange(data)
  416. if self.plotType == PlotType.Combined:
  417. self.img.clear()
  418. self.plotItemPlotScatter.show()
  419. self.plotItemPlotScatter1.show()
  420. self.gradient_legend.hide()
  421. try:
  422. self.removeItem(self.gradient_legend)
  423. pass
  424. except Exception, e:
  425. if not "Could not determine index of item" in str(e):
  426. raise
  427. self.plotItemPlot.clear()
  428. self.plotItemPlotScatter.setData(data[0].transpose())
  429. self.plotItemPlotScatter1.setData(data[1].transpose())
  430. self._enableCustomAutoRange(data[0][1])
  431. self.plotItem.setClipToView(False) # NOTE: otherwise only a very small portion of data is visible :wonder:
  432. if self.plotType == ERROR or self.plotType == NO_DATA:
  433. self.img.hide()
  434. self.gradient_legend.hide()
  435. self.img2.hide()
  436. self.plotItemPlot.hide()
  437. self.plotItemPlotScatter.hide()
  438. self.plotItemPlotScatter1.hide()
  439. if self.plotType == ERROR:
  440. self.error_label.show()
  441. else:
  442. self.no_data_label.show()
  443. if autorange:
  444. self.plotItem.getViewBox().autoRange()
  445. self.plotItem.getViewBox().autoRange()
  446. self.labelAxes()
  447. if self.plotType in [PlotType.FFT, PlotType.Heatmap, PlotType.Compare]:
  448. self.gradient_legend.update_axis()
  449. def labelAxes(self):
  450. """
  451. Add Labels to the axis depending on the self.plotType property.
  452. :return: -
  453. """
  454. if self.plotType == PlotType.Heatmap:
  455. self.plotItem.setLabel('bottom', 'Turn', '')
  456. self.plotItem.setLabel('left', 'Bunch Position', '')
  457. elif self.plotType == PlotType.FFT:
  458. self.plotItem.setLabel('left', 'Bunch Position', '')
  459. self.plotItem.setLabel('bottom', 'Frequency', 'Hz')
  460. elif self.plotType == PlotType.Trains or self.plotType == PlotType.Follow:
  461. self.plotItem.setLabel('left', '', '')
  462. self.plotItem.setLabel('bottom', 'Sample Point', '')
  463. elif self.plotType == PlotType.Combined:
  464. self.plotItem.setLabel('left', '', '')
  465. self.plotItem.setLabel('bottom', '', '')
  466. if self.plotType == PlotType.Compare:
  467. self.plotItem.setLabel('bottom', 'Turn', '')
  468. self.plotItem.setLabel('left', 'Bunch Position', '')
  469. self.plotItem2.setLabel('bottom', 'Turn', '')
  470. self.plotItem2.setLabel('left', 'Bunch Position', '')
  471. def changeType(self, type):
  472. """
  473. Change the plot Type
  474. :param type: (int) the new type
  475. :return: -
  476. """
  477. if type != self.plotType:
  478. self._type_changed = True
  479. self.plotType = type
  480. # if type in [PlotType.FFT, PlotType.Compare, PlotType.Heatmap]: # this will generate an error
  481. # self.gradient_legend.image_changed()
  482. class PlotWidget(kcgw.KCGWidgets):
  483. """
  484. The container Class holding various buttons and controls and the actual plots as SubPlotWidgets instance
  485. """
  486. close_signal = QtCore.pyqtSignal()
  487. change_type_signal = QtCore.pyqtSignal(int, int, str)
  488. def __init__(self, board_id, parent=None, name=None, unique_id=None, type=None, datatype=None, prefix=None, fName=None, data=None):
  489. """
  490. Initialise the Plot Widgt
  491. :param parent: (QWidget) the parent widget
  492. :param name: (str) name of this widget
  493. :param unique_id: (int) unique id of this widget
  494. :param type: (int) type of this widget
  495. :param datatype: (int) datatype (LIVE or FILE)
  496. :param prefix: (str) prefix for text in the listview in LeftBar
  497. :param fName: (str) the filename (this is only used if datatype is FILE)
  498. :param data: (dataset.DataSet) the data to be plotted
  499. :return: -
  500. """
  501. super(PlotWidget, self).__init__()
  502. self.board_id = board_id
  503. if name != None:
  504. self.theName = name
  505. if unique_id != None:
  506. self.theId = unique_id
  507. if type != None:
  508. self.theType = type
  509. self._old_type = type
  510. if datatype != None:
  511. self.theDataType = datatype
  512. if prefix != None:
  513. self.thePrefix = prefix
  514. if self.theDataType == FILE:
  515. self.fName = fName
  516. if self.theDataType == FILE:
  517. self.data = data
  518. else:
  519. self.data = None
  520. self.close_silent = False
  521. self.parent = parent
  522. self.adc = 1
  523. self.secadc = 2
  524. self._single_adc_checked = False
  525. self.initUI()
  526. self.changePlotType(self.theType) # initially mark the correct button
  527. self.setWindowTitle(self.theName + " - " + plotList[type] + " - " + str(self.thePrefix))
  528. if self.theDataType == FILE:
  529. self.plot(type)
  530. else:
  531. live_plot_windows.addWindow(board_id, self)
  532. if board.get_board_status(board_id).last_file is not None:
  533. self.data = io.read_from_file(board.get_board_status(board_id).last_file)
  534. self.plot(type)
  535. def initUI(self):
  536. """
  537. Initialise the UI
  538. :return: -
  539. """
  540. self.plot_widget = SubPlotWidget(dType=self.theDataType)
  541. self.layout = QtGui.QVBoxLayout()
  542. self.plot_buttons_layout = QtGui.QHBoxLayout()
  543. self.heatmap_button = self.createButton(text=tr("Button", "Heatmap"), connect=lambda: self.plot(type=PlotType.Heatmap))
  544. self.fft_button = self.createButton(text=tr("Button", "FFT"), connect=lambda: self.plot(type=PlotType.FFT))
  545. self.trains_button = self.createButton(text=tr("Button", "Trains"), connect=lambda: self.plot(type=PlotType.Trains))
  546. self.combined_button = self.createButton(text=tr("Button", "Combined"), connect=lambda: self.plot(type=PlotType.Combined))
  547. self.compare_button = self.createButton(text=tr("Button", "Compare"), connect=lambda: self.plot(type=PlotType.Compare))
  548. self.follow_button = self.createButton(text=tr("Button", "Follow"), connect=lambda: self.plot(type=PlotType.Follow))
  549. self.type_buttons = {PlotType.Heatmap:self.heatmap_button, PlotType.FFT:self.fft_button,
  550. PlotType.Trains:self.trains_button, PlotType.Combined:self.combined_button,
  551. PlotType.Compare:self.compare_button, PlotType.Follow:self.follow_button}
  552. self.defaultButtonStyleSheet = self.heatmap_button.styleSheet()
  553. self.adc1 = self.createCheckbox(text="ADC 1", connect=self.change_adc)
  554. self.adc1.setChecked(True)
  555. self.adc2 = self.createCheckbox(text="ADC 2", connect=self.change_adc)
  556. self.adc3 = self.createCheckbox(text="ADC 3", connect=self.change_adc)
  557. self.adc4 = self.createCheckbox(text="ADC 4", connect=self.change_adc)
  558. self.plot_buttons_layout.addWidget(self.heatmap_button)
  559. self.plot_buttons_layout.addWidget(self.fft_button)
  560. self.plot_buttons_layout.addWidget(self.trains_button)
  561. self.plot_buttons_layout.addWidget(self.combined_button)
  562. self.plot_buttons_layout.addWidget(self.compare_button)
  563. self.plot_buttons_layout.addWidget(self.follow_button)
  564. self.adc_checkbox_layout = QtGui.QHBoxLayout()
  565. self.adc_checkbox_layout.addWidget(self.adc1)
  566. self.adc_checkbox_layout.addWidget(self.adc2)
  567. self.adc_checkbox_layout.addWidget(self.adc3)
  568. self.adc_checkbox_layout.addWidget(self.adc4)
  569. self.groupWidget = QtGui.QWidget()
  570. self.groupWidget.setLayout(self.adc_checkbox_layout)
  571. self.group = QtGui.QButtonGroup()
  572. self.group.addButton(self.adc1, 1)
  573. self.group.addButton(self.adc2, 2)
  574. self.group.addButton(self.adc3, 3)
  575. self.group.addButton(self.adc4, 4)
  576. self.group.setExclusive(True)
  577. self.compare_heading_left = self.createLabel("ADC 1")
  578. self.compare_heading_left.setAlignment(QtCore.Qt.AlignCenter)
  579. self.compare_heading_right = self.createLabel("ADC 2")
  580. self.compare_heading_right.setAlignment(QtCore.Qt.AlignCenter)
  581. self.compare_heading = QtGui.QWidget()
  582. self.compare_heading_layout = QtGui.QHBoxLayout()
  583. self.compare_heading_layout.addWidget(self.compare_heading_left)
  584. self.compare_heading_layout.addWidget(self.compare_heading_right)
  585. self.compare_heading.setLayout(self.compare_heading_layout)
  586. self.compare_heading.hide()
  587. self.adc1Compare = self.createCheckbox(text="ADC 1", connect=lambda: self.change_adc_compare(who=1))
  588. self.adc2Compare = self.createCheckbox(text="ADC 2", connect=lambda: self.change_adc_compare(who=2))
  589. self.adc3Compare = self.createCheckbox(text="ADC 3", connect=lambda: self.change_adc_compare(who=3))
  590. self.adc4Compare = self.createCheckbox(text="ADC 4", connect=lambda: self.change_adc_compare(who=4))
  591. self.adc1Compare.setChecked(True)
  592. self.adc2Compare.setChecked(True)
  593. self.adc_checkbox_compare_layout = QtGui.QHBoxLayout()
  594. self.adc_checkbox_compare_layout.addWidget(self.adc1Compare)
  595. self.adc_checkbox_compare_layout.addWidget(self.adc2Compare)
  596. self.adc_checkbox_compare_layout.addWidget(self.adc3Compare)
  597. self.adc_checkbox_compare_layout.addWidget(self.adc4Compare)
  598. self.groupWidgetCompare = QtGui.QWidget()
  599. self.groupWidgetCompare.setLayout(self.adc_checkbox_compare_layout)
  600. self.groupCompare = QtGui.QButtonGroup()
  601. self.groupCompare.setExclusive(False)
  602. self.groupCompare.addButton(self.adc1Compare, 1)
  603. self.groupCompare.addButton(self.adc2Compare, 2)
  604. self.groupCompare.addButton(self.adc3Compare, 3)
  605. self.groupCompare.addButton(self.adc4Compare, 4)
  606. self.groupWidgetCompare.hide()
  607. self.from_to_layout = QtGui.QHBoxLayout()
  608. self.bucket_part = QtGui.QWidget()
  609. self.bucket_layout = QtGui.QHBoxLayout()
  610. self.bucket_layout.addWidget(self.createLabel(tr("Label", "Bucket:")))
  611. self.bucket_spinbox = self.createSpinbox(0, config.bunches_per_turn-1, interval=1, connect=lambda: self.plot(self.theType))
  612. self.bucket_layout.addWidget(self.bucket_spinbox)
  613. self.bucket_part.setLayout(self.bucket_layout)
  614. self.from_spinbox = self.createSpinbox(0, 100000000, interval=100, connect=lambda: self.plot(self.theType))
  615. self.to_spinbox = self.createSpinbox(0, 100000000, start_value=1000, interval=100, connect=lambda: self.plot(self.theType))
  616. self.from_to_layout.addStretch()
  617. self.from_to_layout.addWidget(self.bucket_part)
  618. self.from_to_layout.addWidget(self.createLabel(tr("Label", "From:")))
  619. self.from_to_layout.addWidget(self.from_spinbox)
  620. self.from_to_layout.addWidget(self.createLabel(tr("Label", "To:")))
  621. self.from_to_layout.addWidget(self.to_spinbox)
  622. self.layout.addLayout(self.plot_buttons_layout)
  623. self.layout.addWidget(self.compare_heading)
  624. self.layout.addWidget(self.plot_widget)
  625. self.layout.addWidget(self.groupWidget)
  626. self.layout.addWidget(self.groupWidgetCompare)
  627. self.layout.addLayout(self.from_to_layout)
  628. self.setLayout(self.layout)
  629. def change_adc(self):
  630. """
  631. Change the adc for which data is plotted
  632. :return: -
  633. """
  634. self.adc = self.group.checkedId()
  635. self.change_identifier_text()
  636. self.plot(self.theType)
  637. def change_adc_compare(self, who):
  638. """
  639. Change the adcs displayed in a compare plot
  640. :return:
  641. """
  642. if not self._single_adc_checked:
  643. self.adc1Compare.setChecked(False)
  644. self.adc2Compare.setChecked(False)
  645. self.adc3Compare.setChecked(False)
  646. self.adc4Compare.setChecked(False)
  647. getattr(self, 'adc'+str(who)+'Compare').setChecked(True)
  648. self._single_adc_checked = True
  649. else:
  650. checked = [i.isChecked() for i in self.groupCompare.buttons()]
  651. self._single_adc_checked = False
  652. self.adc = np.where(np.array(checked) == True)[0][0] + 1 # +1 because adcs are 1 based and indices 0
  653. self.secadc = np.where(np.array(checked) == True)[0][1] + 1 # +1 because adcs are 1 based and indices 0
  654. self.compare_heading_left.setText("ADC "+str(self.adc))
  655. self.compare_heading_right.setText("ADC "+str(self.secadc))
  656. self.change_identifier_text()
  657. if self.theType in [ERROR, NO_DATA]:
  658. self.plot(self._old_type)
  659. else:
  660. self.plot(self.theType)
  661. def disable_buttons(self, b_bool):
  662. """
  663. Disable the buttons on this widget
  664. This is not used at the moment
  665. :param b_bool: (bool) disable(False) or enable(True)
  666. :return: -
  667. """
  668. self.heatmap_button.setDisabled(b_bool)
  669. self.fft_button.setDisabled(b_bool)
  670. self.trains_button.setDisabled(b_bool)
  671. self.combined_button.setDisabled(b_bool)
  672. def changePlotType(self, type):
  673. """
  674. Change the plot type to the given type.
  675. :param type: the new type
  676. """
  677. if type not in [ERROR, NO_DATA]:
  678. self._old_type = type
  679. self.theType = type
  680. self.plot_widget.changeType(type)
  681. for btype, button in self.type_buttons.iteritems():
  682. style = ""
  683. if btype == type:
  684. style += "QPushButton {background-color:lightgreen;}"
  685. else:
  686. style += self.defaultButtonStyleSheet
  687. style += "QPushButton:focus{background-color: lightgrey!important; border-color: lightblue;}" # Note: this does not work
  688. button.setStyleSheet(style)
  689. if self.theType == PlotType.Follow:
  690. self.bucket_part.show()
  691. else:
  692. self.bucket_part.hide()
  693. def plot(self, type=None):
  694. """
  695. Wrapper function to call the correct plot function depending on type
  696. :param type: (int) the plot type
  697. :return: -
  698. """
  699. try:
  700. if type == None:
  701. if self.theType in [ERROR, NO_DATA]:
  702. type = self._old_type
  703. else:
  704. type = self.theType
  705. self.do_plot(type)
  706. except Exception:
  707. if self.data is None or len(self.data.array) == 0:
  708. self.changePlotType(NO_DATA)
  709. else:
  710. self.changePlotType(ERROR)
  711. self.plot_widget.plot(None)
  712. def plot_live(self, type=None, data=None):
  713. """
  714. Function to call when livedata is to be plotted
  715. :param type: (int) plot type
  716. :param data: (dataset.DataSet) data to plot
  717. :return: -
  718. """
  719. self.data = data
  720. try:
  721. if type == None:
  722. if self.theType in [ERROR, NO_DATA]:
  723. type = self._old_type
  724. else:
  725. type = self.theType
  726. if type == PlotType.Trains or type == PlotType.Combined or type == PlotType.Follow:
  727. self.do_plot(type, autorange=True)
  728. else:
  729. self.do_plot(type, autorange=False)
  730. except Exception:
  731. if len(self.data.array) == 0:
  732. self.changePlotType(NO_DATA)
  733. else:
  734. self.changePlotType(ERROR)
  735. self.plot_widget.plot(None)
  736. def do_plot(self, type, autorange=True):
  737. """
  738. Actually perform a plot (this calls SubPlotWidget.plot)
  739. :param type: (int) plot type
  740. :param autorange: (bool) whether to perform a autorange upon plot
  741. :return: -
  742. """
  743. if self.theType != type:
  744. if type == PlotType.Combined:
  745. self.groupWidget.hide()
  746. elif self.theType == PlotType.Combined:
  747. self.groupWidget.show()
  748. if type == PlotType.Compare:
  749. self.groupWidget.hide()
  750. self.groupWidgetCompare.show()
  751. self.compare_heading.show()
  752. else:
  753. self.groupWidget.show()
  754. self.groupWidgetCompare.hide()
  755. self.compare_heading.hide()
  756. self.changePlotType(type)
  757. self.setWindowTitle(self.theName + " - " + plotList[type] + " - " + str(self.thePrefix))
  758. self.change_identifier_text()
  759. if self.theType == PlotType.Compare:
  760. self.change_identifier_text()
  761. if self.theType == PlotType.Compare:
  762. self.groupWidget.hide()
  763. self.groupWidgetCompare.show()
  764. self.compare_heading.show()
  765. else:
  766. self.groupWidgetCompare.hide()
  767. self.compare_heading.hide()
  768. f = self.from_spinbox.value()
  769. t = self.to_spinbox.value() if self.to_spinbox.value() > f else f+1
  770. b = self.bucket_spinbox.value()
  771. if type == PlotType.FFT:
  772. self.plot_widget.plot(np.abs(self.data.fft(adc=self.adc, frm=f, to=t+1, drop_first_bin=True)).transpose(),
  773. autorange=autorange,
  774. xvalueborders=[self.data.fft_freq_dist(), self.data.fft_max_freq()])
  775. if type == PlotType.Heatmap:
  776. self.plot_widget.plot(self.data.heatmap(adc=self.adc, frm=f, to=t).transpose(), autorange=autorange)
  777. if type == PlotType.Compare:
  778. self.plot_widget.plot([self.data.heatmap(adc=self.adc, frm=f, to=t).transpose(),self.data.heatmap(adc=self.secadc, frm=f, to=t).transpose()], autorange=autorange)
  779. if type == PlotType.Trains:
  780. self.plot_widget.plot(self.data.train(adc=self.adc, frm=f, to=t), autorange=autorange)
  781. if type == PlotType.Follow:
  782. self.plot_widget.plot(self.data.follow(adc=self.adc, frm=f, to=t, bunch=b), autorange=autorange)
  783. if type == PlotType.Combined:
  784. self.plot_widget.plot(self.data.combined(frm=f, to=t), autorange=autorange)
  785. def change_identifier_text(self):
  786. """
  787. Change the text that identifies the plot in the left bar
  788. :return:
  789. """
  790. if self.theType is PlotType.Compare:
  791. the_text = str(self.adc)+"+"+str(self.secadc)
  792. else:
  793. the_text = str(self.adc)
  794. if self.theType in [ERROR, NO_DATA]:
  795. type = self._old_type
  796. else:
  797. type = self.theType
  798. if self.theDataType is LIVE:
  799. self.change_type_signal.emit(self.theId, type,
  800. the_text+" B: "+available_boards.get_board_name_from_id(self.board_id))
  801. else:
  802. self.change_type_signal.emit(self.theId, type, the_text)
  803. def closeEvent(self, event):
  804. """
  805. Event Handler to handle the event of closing this window
  806. :param event: QEvent
  807. :return: -
  808. """
  809. if not self.close_silent:
  810. if not self.parent.remove_plot(self.theId, silent=self.close_silent):
  811. event.ignore()
  812. return
  813. del self.data
  814. del self.plot_widget
  815. super(PlotWidget, self).closeEvent(event)
  816. if self.theDataType == LIVE:
  817. live_plot_windows.removeWindow(self.board_id, self)
  818. self.close_signal.emit()
  819. del self