plotWidget.py 37 KB

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