plotWidget.py 37 KB

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