plotWidget.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901
  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. from .. import config
  16. tr = kcgw.tr
  17. LIVE = 1
  18. FILE = 2
  19. ERROR = 99
  20. NO_DATA = 98
  21. class Enum():
  22. """
  23. Simple Enum Class (as this is not supported by clean python)
  24. """
  25. def __init__(self, *args):
  26. self.idx = 0
  27. for i in args:
  28. setattr(self, i, self.idx)
  29. self.idx += 1
  30. plotList = [tr("Label", "Heatmap"), tr("Label", "FFT"), tr("Label", "Trains"), tr("Label", "Combined"), tr("Label", "Compare"), tr("Label", "Follow")]
  31. PlotType = Enum(*[str(i) for i in plotList])
  32. # gradient = {'mode': 'rgb',
  33. # 'ticks': [(0.0, (0.37281, 0.11883, 3.53583, 255)),
  34. # (0.0390625, (4.9401150000000005, 3.858915, 22.635585, 255)),
  35. # (0.078125, (15.6417, 9.330449999999999, 45.29871, 255)),
  36. # (0.1171875, (29.74728, 12.13137, 69.44185499999999, 255)),
  37. # (0.15625, (46.774395000000005, 10.283895, 90.51760499999999, 255)),
  38. # (0.1953125, (64.1631, 9.614775, 102.86139, 255)),
  39. # (0.234375, (80.65191, 13.63995, 108.40458, 255)),
  40. # (0.2734375, (96.64525499999999, 19.444515, 110.343345, 255)),
  41. # (0.3125, (112.507785, 25.33119, 110.05646999999999, 255)),
  42. # (0.3515625, (128.390715, 31.001625, 107.95578, 255)),
  43. # (0.390625, (144.29277, 36.609585, 104.10579, 255)),
  44. # (0.4296875, (160.100985, 42.476625, 98.50038, 255)),
  45. # (0.46875, (175.606515, 49.020945, 91.188765, 255)),
  46. # (0.5078125, (190.517385, 56.70639, 82.32827999999999, 255)),
  47. # (0.546875, (204.477105, 65.96187, 72.190245, 255)),
  48. # (0.5859375, (217.10292, 77.07629999999999, 61.10718, 255)),
  49. # (0.625, (228.047775, 90.11674500000001, 49.36392, 255)),
  50. # (0.6640625, (237.05922, 104.927145, 37.068585, 255)),
  51. # (0.703125, (243.99726, 121.21578, 24.147225, 255)),
  52. # (0.7421875, (248.797635, 138.66849, 11.122589999999999, 255)),
  53. # (0.78125, (251.41776000000002, 157.01625, 6.52596, 255)),
  54. # (0.8203125, (251.80332, 176.04333, 20.397450000000003, 255)),
  55. # (0.859375, (249.90816, 195.543435, 42.420015, 255)),
  56. # (0.8984375, (245.92047, 215.18124, 69.714705, 255)),
  57. # (0.9375, (241.63647, 233.936745, 104.719575, 255)),
  58. # (0.9765625, (245.26206, 248.86062, 145.840875, 255)),
  59. # (0.99609375, (252.03231, 254.58282, 164.45562, 255))]}
  60. gradient = {'mode': 'rgb',
  61. 'ticks': [(0.0, (0.37281, 0.11883, 3.53583, 255)),
  62. (0.1171875, (29.74728, 12.13137, 69.44185499999999, 255)),
  63. (0.234375, (80.65191, 13.63995, 108.40458, 255)),
  64. (0.3515625, (128.390715, 31.001625, 107.95578, 255)),
  65. (0.46875, (175.606515, 49.020945, 91.188765, 255)),
  66. (0.5859375, (217.10292, 77.07629999999999, 61.10718, 255)),
  67. (0.703125, (243.99726, 121.21578, 24.147225, 255)),
  68. (0.8203125, (251.80332, 176.04333, 20.397450000000003, 255)),
  69. (0.9375, (241.63647, 233.936745, 104.719575, 255)),
  70. (0.99609375, (252.03231, 254.58282, 164.45562, 255))]}
  71. class CustomGradientEditorItem(pg.GradientEditorItem):
  72. """
  73. A Gradient Editor Item to insert a perception linear gradient
  74. """
  75. def __init__(self, **kwargs):
  76. pg.GradientEditorItem.__init__(self, **kwargs)
  77. self.customGradients = {}
  78. def addGrad(self, name, grad_dic):
  79. """
  80. Add a gradient to the list of gradients in the gui
  81. :param name: the name of the gradient
  82. :param grad_dic: the dictionary containing the gradient data
  83. """
  84. if name not in self.customGradients:
  85. self.customGradients[name] = grad_dic
  86. px = QtGui.QPixmap(100, 15)
  87. p = QtGui.QPainter(px)
  88. self.restoreState(grad_dic)
  89. length_backup = self.length
  90. self.length = 100
  91. grad = self.getGradient()
  92. self.length = length_backup
  93. brush = QtGui.QBrush(grad)
  94. p.fillRect(QtCore.QRect(0, 0, 100, 15), brush)
  95. p.end()
  96. label = QtGui.QLabel()
  97. label.setPixmap(px)
  98. label.setContentsMargins(1, 1, 1, 1)
  99. act = QtGui.QWidgetAction(self)
  100. act.setDefaultWidget(label)
  101. act.triggered.connect(self.contextMenuClicked)
  102. act.name = name
  103. if len(self.customGradients) > 1:
  104. self.menu.insertAction(self.menu.actions()[0], act)
  105. else:
  106. sep = self.menu.insertSeparator(self.menu.actions()[0])
  107. self.menu.insertAction(sep, act)
  108. self.restoreState(grad_dic)
  109. def restoreState(self, state):
  110. """
  111. Reimplemented of pyqtgraph.GradientEditorItem.restoreState to work with our custom perception linear gradient.
  112. :param state: the state to restore to
  113. """
  114. self.setColorMode(state['mode'])
  115. for t in list(self.ticks.keys()):
  116. self.removeTick(t, finish=False)
  117. self.bottomTick = [None, 10]
  118. self.topmostTick = [None, -1]
  119. for t in state['ticks']:
  120. c = QtGui.QColor(*t[1])
  121. tick = self.addTick(t[0], c, finish=False)
  122. if t[0] < self.bottomTick[1]:
  123. self.bottomTick = [tick, t[0]]
  124. if t[0] > self.topmostTick[1]:
  125. self.topmostTick = [tick, t[0]]
  126. self.tickPosInPercent = self.ticks.copy()
  127. for t in self.ticks:
  128. if t is self.topmostTick[0] or t is self.bottomTick[0]:
  129. continue
  130. t.hide()
  131. t.movable = False
  132. # self.bottomTick[1] *= self.length
  133. # self.topmostTick[1] *= self.length
  134. self.updateGradient()
  135. self.sigGradientChangeFinished.emit(self)
  136. def loadPreset(self, name):
  137. """
  138. Reimplemented of pyqtgraph.GradientEditorItem.loadPreset to work with our custom perception linear gradient.
  139. :param name: the name of the preset to load
  140. """
  141. if name in self.customGradients:
  142. self.restoreState(self.customGradients[name])
  143. else:
  144. super(CustomGradientEditorItem, self).loadPreset(name)
  145. def tickMoved(self, tick, pos):
  146. """
  147. Reimplemented of pyqtgraph.GradientEditorItem.tickMoved to work with our custom perception linear gradient,
  148. which has a lot of steps which would create a lot of ticks in the gradient legend. This removes all but 2
  149. ticks and aligns all the internal ticks accordingly.
  150. :param tick: the tick to move
  151. :param pos: the position to move to
  152. """
  153. if tick is self.bottomTick[0]:
  154. # self.bottomTick[1] = pos.x()
  155. pg.TickSliderItem.tickMoved(self, tick, pos)
  156. elif tick is self.topmostTick[0]:
  157. # self.topmostTick[1] = pos.x()
  158. pg.TickSliderItem.tickMoved(self, tick, pos)
  159. if tick in [self.bottomTick[0], self.topmostTick[0]]: # if bottom or topmost tick was moved
  160. if pos.x() < self.length and pos.x() > 0: # if tick is not at top or bottom
  161. newUnit = (self.topmostTick[0].pos().x() - self.bottomTick[0].pos().x()) # create new virtual unit length
  162. for t in self.ticks: # for every tick
  163. if t not in [self.bottomTick[0], self.topmostTick[0]]: # if tick is not bottom or topmost tick
  164. pos = t.pos()
  165. new_x = self.bottomTick[0].pos().x() + self.tickPosInPercent[t] * newUnit
  166. pos.setX(new_x)
  167. t.setPos(pos)
  168. pg.TickSliderItem.tickMoved(self, t, pos)
  169. self.updateGradient()
  170. class SpectrogramColorLegendItem(pg.GraphicsWidget):
  171. """
  172. The Item used as Legend for Heatmap and FFT Plot
  173. """
  174. gradientChanged = QtCore.pyqtSignal()
  175. def __init__(self, img=None):
  176. """
  177. Initialise the Legend
  178. :param img: (ImageItem) the image item this is the legend for
  179. :return: -
  180. """
  181. super(SpectrogramColorLegendItem, self).__init__()
  182. self.layout = QtGui.QGraphicsGridLayout()
  183. self.setLayout(self.layout)
  184. self.img = img
  185. # self.gei = pg.GradientEditorItem(orientation='right')
  186. self.gei = CustomGradientEditorItem(orientation='right', allowAdd=False)
  187. self.legend_axis = pg.AxisItem(orientation='left')
  188. # self.addItem(self.legend_axis)
  189. self.layout.addItem(self.legend_axis, 0, 0, alignment=QtCore.Qt.AlignVCenter)
  190. # self.addItem(self.gei)
  191. self.layout.addItem(self.gei, 0, 1)
  192. self.gei.sigGradientChanged.connect(self.gradient_changed)
  193. # self.gei.loadPreset('spectrum')
  194. self.gei.addGrad('inferno', gradient)
  195. # self.setFixedWidth(80)
  196. self.image_changed()
  197. def gradient_changed(self):
  198. """
  199. Proxy function for the gradient_cnahged event of the GradientEditorItem
  200. :return:
  201. """
  202. self.gradientChanged.emit()
  203. def set_image(self, img):
  204. """
  205. Set the Image this is the legend for (only needed if not already done upon initialisation)
  206. :param img: (ImageItem) The image item to use
  207. :return: -
  208. """
  209. self.img = img
  210. self.image_changed(True)
  211. def update_axis(self):
  212. """
  213. Update the axis of this legend
  214. :return: -
  215. """
  216. if self.img.getLevels():
  217. # min, max = self.img.getLevels()
  218. min, max = (np.min(self.img.image), np.max(self.img.image))
  219. self.legend_axis.setRange(min, max)
  220. def reset_gradient(self):
  221. """
  222. Reset the gradient to the preset "spectrum"
  223. :return: -
  224. """
  225. # self.gei.loadPreset('spectrum')
  226. self.gei.loadPreset('inferno')
  227. # self.gei.restoreState(gradient)
  228. def image_changed(self, reset_gradient=True):
  229. """
  230. Call this to adjust the legend when the image has changed
  231. :param reset_gradient: (bool) whether to reset the gradient or not
  232. :return:
  233. """
  234. if self.img:
  235. self.update_axis()
  236. if reset_gradient:
  237. self.reset_gradient()
  238. def getLookupTable(self, nPts, alpha=None):
  239. """
  240. Get the look up table of the imageitem
  241. :param nPts: (int) number of points the lookup table is defined on
  242. :param alpha: ??
  243. :return: -
  244. """
  245. return self.gei.getLookupTable(nPts, alpha=alpha)
  246. def resizeEvent(self, event):
  247. """
  248. Handle resizing of the window
  249. """
  250. self.legend_axis.setHeight(self.gei.length)
  251. class SubPlotWidget(pg.GraphicsLayoutWidget):
  252. """
  253. The Widget actually containing the plots and images
  254. """
  255. def __init__(self, dType=FILE):
  256. super(SubPlotWidget, self).__init__()
  257. self.dType = dType
  258. self._type_changed = False
  259. # self.setStyleSheet("border: 5px solid black; margin: 10px;")
  260. self.plotType = 1
  261. self.plotItem = pg.PlotItem()
  262. self.img = pg.ImageItem()
  263. self.plotItem2 = pg.PlotItem()
  264. self.img2 = pg.ImageItem()
  265. self.plotItem2.addItem(self.img2)
  266. self.setFrameStyle(self.StyledPanel | self.Sunken)
  267. self.gradient_legend = SpectrogramColorLegendItem(self.img)
  268. def changelut():
  269. """
  270. Handle changing of the lookup table
  271. """
  272. self.img.setLookupTable(self.gradient_legend.getLookupTable(512))
  273. self.img2.setLookupTable(self.gradient_legend.getLookupTable(512))
  274. self.gradient_legend.gradientChanged.connect(changelut)
  275. self.addItem(self.plotItem)
  276. self.plotItem.addItem(self.img)
  277. self.plotItemPlotScatter = pg.PlotDataItem(pen=None, symbolPen=pg.mkPen(50, 50, 255), symbolSize=3, pxMode=True, symbolBrush=pg.mkBrush(50, 50, 255), downsample=200)
  278. self.plotItemPlotScatter1 = pg.PlotDataItem(pen=None, symbolPen=pg.mkPen(255, 50, 50), symbolSize=3, pxMode=True, symbolBrush=pg.mkBrush(255, 50, 50), downsample=200)
  279. # self.plotItemPlotScatter2 = pg.PlotDataItem(pen=None, symbolPen=pg.mkPen(50, 255, 50), symbolSize=3, pxMode=True, symbolBrush=pg.mkBrush(50, 255, 50), downsample=200)
  280. self.plotItemPlot = pg.PlotDataItem() # pen="#000000")
  281. self.plotItem.addItem(self.plotItemPlot)
  282. self.plotItem.addItem(self.plotItemPlotScatter)
  283. self.plotItem.addItem(self.plotItemPlotScatter1)
  284. # self.plotItem.addItem(self.plotItemPlotScatter2)
  285. self.plotItem.vb.origAutoRange = self.plotItem.vb.autoRange
  286. self.error_label = pg.LabelItem("Error during plot.", color=(200, 50, 50))
  287. self.error_label.hide()
  288. self.error_label.scale(1, -0.6)
  289. self.error_label.translate(0, -30)
  290. self.plotItem.addItem(self.error_label)
  291. self.no_data_label = pg.LabelItem("No data to plot.", color=(200, 50, 50))
  292. self.no_data_label.hide()
  293. self.no_data_label.scale(1, -0.6)
  294. self.no_data_label.translate(0, -30)
  295. self.plotItem.addItem(self.no_data_label)
  296. def _enableCustomAutoRange(self, data):
  297. """
  298. Enable custom auto range in this plot
  299. :param data: the data to autorange
  300. :return:
  301. """
  302. def newAutoRange(*args, **kwargs):
  303. ''' function to handle the new autorange '''
  304. if len(data) == 0:
  305. return
  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 or self.plotType == PlotType.Follow:
  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 or self.plotType == PlotType.Follow:
  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.follow_button = self.createButton(text=tr("Button", "Follow"), connect=lambda: self.plot(type=PlotType.Follow))
  546. self.type_buttons = {PlotType.Heatmap:self.heatmap_button, PlotType.FFT:self.fft_button,
  547. PlotType.Trains:self.trains_button, PlotType.Combined:self.combined_button,
  548. PlotType.Compare:self.compare_button, PlotType.Follow:self.follow_button}
  549. self.defaultButtonStyleSheet = self.heatmap_button.styleSheet()
  550. self.adc1 = self.createCheckbox(text="ADC 1", connect=self.change_adc)
  551. self.adc1.setChecked(True)
  552. self.adc2 = self.createCheckbox(text="ADC 2", connect=self.change_adc)
  553. self.adc3 = self.createCheckbox(text="ADC 3", connect=self.change_adc)
  554. self.adc4 = self.createCheckbox(text="ADC 4", connect=self.change_adc)
  555. self.plot_buttons_layout.addWidget(self.heatmap_button)
  556. self.plot_buttons_layout.addWidget(self.fft_button)
  557. self.plot_buttons_layout.addWidget(self.trains_button)
  558. self.plot_buttons_layout.addWidget(self.combined_button)
  559. self.plot_buttons_layout.addWidget(self.compare_button)
  560. self.plot_buttons_layout.addWidget(self.follow_button)
  561. self.adc_checkbox_layout = QtGui.QHBoxLayout()
  562. self.adc_checkbox_layout.addWidget(self.adc1)
  563. self.adc_checkbox_layout.addWidget(self.adc2)
  564. self.adc_checkbox_layout.addWidget(self.adc3)
  565. self.adc_checkbox_layout.addWidget(self.adc4)
  566. self.groupWidget = QtGui.QWidget()
  567. self.groupWidget.setLayout(self.adc_checkbox_layout)
  568. self.group = QtGui.QButtonGroup()
  569. self.group.addButton(self.adc1, 1)
  570. self.group.addButton(self.adc2, 2)
  571. self.group.addButton(self.adc3, 3)
  572. self.group.addButton(self.adc4, 4)
  573. self.group.setExclusive(True)
  574. self.compare_heading_left = self.createLabel("ADC 1")
  575. self.compare_heading_left.setAlignment(QtCore.Qt.AlignCenter)
  576. self.compare_heading_right = self.createLabel("ADC 2")
  577. self.compare_heading_right.setAlignment(QtCore.Qt.AlignCenter)
  578. self.compare_heading = QtGui.QWidget()
  579. self.compare_heading_layout = QtGui.QHBoxLayout()
  580. self.compare_heading_layout.addWidget(self.compare_heading_left)
  581. self.compare_heading_layout.addWidget(self.compare_heading_right)
  582. self.compare_heading.setLayout(self.compare_heading_layout)
  583. self.compare_heading.hide()
  584. self.adc1Compare = self.createCheckbox(text="ADC 1", connect=lambda: self.change_adc_compare(who=1))
  585. self.adc2Compare = self.createCheckbox(text="ADC 2", connect=lambda: self.change_adc_compare(who=2))
  586. self.adc3Compare = self.createCheckbox(text="ADC 3", connect=lambda: self.change_adc_compare(who=3))
  587. self.adc4Compare = self.createCheckbox(text="ADC 4", connect=lambda: self.change_adc_compare(who=4))
  588. self.adc1Compare.setChecked(True)
  589. self.adc2Compare.setChecked(True)
  590. self.adc_checkbox_compare_layout = QtGui.QHBoxLayout()
  591. self.adc_checkbox_compare_layout.addWidget(self.adc1Compare)
  592. self.adc_checkbox_compare_layout.addWidget(self.adc2Compare)
  593. self.adc_checkbox_compare_layout.addWidget(self.adc3Compare)
  594. self.adc_checkbox_compare_layout.addWidget(self.adc4Compare)
  595. self.groupWidgetCompare = QtGui.QWidget()
  596. self.groupWidgetCompare.setLayout(self.adc_checkbox_compare_layout)
  597. self.groupCompare = QtGui.QButtonGroup()
  598. self.groupCompare.setExclusive(False)
  599. self.groupCompare.addButton(self.adc1Compare, 1)
  600. self.groupCompare.addButton(self.adc2Compare, 2)
  601. self.groupCompare.addButton(self.adc3Compare, 3)
  602. self.groupCompare.addButton(self.adc4Compare, 4)
  603. self.groupWidgetCompare.hide()
  604. self.from_to_layout = QtGui.QHBoxLayout()
  605. self.bucket_part = QtGui.QWidget()
  606. self.bucket_layout = QtGui.QHBoxLayout()
  607. self.bucket_layout.addWidget(self.createLabel(tr("Label", "Bucket:")))
  608. self.bucket_spinbox = self.createSpinbox(0, config.bunches_per_turn-1, interval=1, connect=lambda: self.plot(self.theType))
  609. self.bucket_layout.addWidget(self.bucket_spinbox)
  610. self.bucket_part.setLayout(self.bucket_layout)
  611. self.from_spinbox = self.createSpinbox(0, 100000000, interval=100, connect=lambda: self.plot(self.theType))
  612. self.to_spinbox = self.createSpinbox(0, 100000000, start_value=1000, interval=100, connect=lambda: self.plot(self.theType))
  613. self.from_to_layout.addStretch()
  614. self.from_to_layout.addWidget(self.bucket_part)
  615. self.from_to_layout.addWidget(self.createLabel(tr("Label", "From:")))
  616. self.from_to_layout.addWidget(self.from_spinbox)
  617. self.from_to_layout.addWidget(self.createLabel(tr("Label", "To:")))
  618. self.from_to_layout.addWidget(self.to_spinbox)
  619. self.layout.addLayout(self.plot_buttons_layout)
  620. self.layout.addWidget(self.compare_heading)
  621. self.layout.addWidget(self.plot_widget)
  622. self.layout.addWidget(self.groupWidget)
  623. self.layout.addWidget(self.groupWidgetCompare)
  624. self.layout.addLayout(self.from_to_layout)
  625. self.setLayout(self.layout)
  626. def change_adc(self):
  627. """
  628. Change the adc for which data is plotted
  629. :return: -
  630. """
  631. self.adc = self.group.checkedId()
  632. self.change_identifier_text()
  633. self.plot(self.theType)
  634. def change_adc_compare(self, who):
  635. """
  636. Change the adcs displayed in a compare plot
  637. :return:
  638. """
  639. if not self._single_adc_checked:
  640. self.adc1Compare.setChecked(False)
  641. self.adc2Compare.setChecked(False)
  642. self.adc3Compare.setChecked(False)
  643. self.adc4Compare.setChecked(False)
  644. getattr(self, 'adc'+str(who)+'Compare').setChecked(True)
  645. self._single_adc_checked = True
  646. else:
  647. checked = [i.isChecked() for i in self.groupCompare.buttons()]
  648. self._single_adc_checked = False
  649. self.adc = np.where(np.array(checked) == True)[0][0] + 1 # +1 because adcs are 1 based and indices 0
  650. self.secadc = np.where(np.array(checked) == True)[0][1] + 1 # +1 because adcs are 1 based and indices 0
  651. self.compare_heading_left.setText("ADC "+str(self.adc))
  652. self.compare_heading_right.setText("ADC "+str(self.secadc))
  653. self.change_identifier_text()
  654. if self.theType in [ERROR, NO_DATA]:
  655. self.plot(self._old_type)
  656. else:
  657. self.plot(self.theType)
  658. def disable_buttons(self, b_bool):
  659. """
  660. Disable the buttons on this widget
  661. This is not used at the moment
  662. :param b_bool: (bool) disable(False) or enable(True)
  663. :return: -
  664. """
  665. self.heatmap_button.setDisabled(b_bool)
  666. self.fft_button.setDisabled(b_bool)
  667. self.trains_button.setDisabled(b_bool)
  668. self.combined_button.setDisabled(b_bool)
  669. def changePlotType(self, type):
  670. """
  671. Change the plot type to the given type.
  672. :param type: the new type
  673. """
  674. if type not in [ERROR, NO_DATA]:
  675. self._old_type = type
  676. self.theType = type
  677. self.plot_widget.changeType(type)
  678. for btype, button in self.type_buttons.iteritems():
  679. style = ""
  680. if btype == type:
  681. style += "QPushButton {background-color:lightgreen;}"
  682. else:
  683. style += self.defaultButtonStyleSheet
  684. style += "QPushButton:focus{background-color: lightgrey!important; border-color: lightblue;}" # Note: this does not work
  685. button.setStyleSheet(style)
  686. if self.theType == PlotType.Follow:
  687. self.bucket_part.show()
  688. else:
  689. self.bucket_part.hide()
  690. def plot(self, type=None):
  691. """
  692. Wrapper function to call the correct plot function depending on type
  693. :param type: (int) the plot type
  694. :return: -
  695. """
  696. try:
  697. if type == None:
  698. if self.theType in [ERROR, NO_DATA]:
  699. type = self._old_type
  700. else:
  701. type = self.theType
  702. self.do_plot(type)
  703. except Exception:
  704. if self.data is None or len(self.data.array) == 0:
  705. self.changePlotType(NO_DATA)
  706. else:
  707. self.changePlotType(ERROR)
  708. self.plot_widget.plot(None)
  709. def plot_live(self, type=None, data=None):
  710. """
  711. Function to call when livedata is to be plotted
  712. :param type: (int) plot type
  713. :param data: (dataset.DataSet) data to plot
  714. :return: -
  715. """
  716. self.data = data
  717. try:
  718. if type == None:
  719. if self.theType in [ERROR, NO_DATA]:
  720. type = self._old_type
  721. else:
  722. type = self.theType
  723. if type == PlotType.Trains or type == PlotType.Combined or type == PlotType.Follow:
  724. self.do_plot(type, autorange=True)
  725. else:
  726. self.do_plot(type, autorange=False)
  727. except Exception:
  728. if len(self.data.array) == 0:
  729. self.changePlotType(NO_DATA)
  730. else:
  731. self.changePlotType(ERROR)
  732. self.plot_widget.plot(None)
  733. def do_plot(self, type, autorange=True):
  734. """
  735. Actually perform a plot (this calls SubPlotWidget.plot)
  736. :param type: (int) plot type
  737. :param autorange: (bool) whether to perform a autorange upon plot
  738. :return: -
  739. """
  740. if self.theType != type:
  741. if type == PlotType.Combined:
  742. self.groupWidget.hide()
  743. elif self.theType == PlotType.Combined:
  744. self.groupWidget.show()
  745. if type == PlotType.Compare:
  746. self.groupWidget.hide()
  747. self.groupWidgetCompare.show()
  748. self.compare_heading.show()
  749. else:
  750. self.groupWidget.show()
  751. self.groupWidgetCompare.hide()
  752. self.compare_heading.hide()
  753. self.changePlotType(type)
  754. self.setWindowTitle(self.theName + " - " + plotList[type] + " - " + str(self.thePrefix))
  755. self.change_identifier_text()
  756. if self.theType == PlotType.Compare:
  757. self.change_identifier_text()
  758. if self.theType == PlotType.Compare:
  759. self.groupWidget.hide()
  760. self.groupWidgetCompare.show()
  761. self.compare_heading.show()
  762. else:
  763. self.groupWidgetCompare.hide()
  764. self.compare_heading.hide()
  765. f = self.from_spinbox.value()
  766. t = self.to_spinbox.value() if self.to_spinbox.value() > f else f+1
  767. b = self.bucket_spinbox.value()
  768. if type == PlotType.FFT:
  769. self.plot_widget.plot(np.abs(self.data.fft(adc=self.adc, frm=f, to=t+1, drop_first_bin=True)).transpose(),
  770. autorange=autorange,
  771. xvalueborders=[self.data.fft_freq_dist(), self.data.fft_max_freq()])
  772. if type == PlotType.Heatmap:
  773. self.plot_widget.plot(self.data.heatmap(adc=self.adc, frm=f, to=t).transpose(), autorange=autorange)
  774. if type == PlotType.Compare:
  775. 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)
  776. if type == PlotType.Trains:
  777. self.plot_widget.plot(self.data.train(adc=self.adc, frm=f, to=t), autorange=autorange)
  778. if type == PlotType.Follow:
  779. self.plot_widget.plot(self.data.follow(adc=self.adc, frm=f, to=t, bunch=b), autorange=autorange)
  780. if type == PlotType.Combined:
  781. self.plot_widget.plot(self.data.combined(frm=f, to=t), autorange=autorange)
  782. def change_identifier_text(self):
  783. """
  784. Change the text that identifies the plot in the left bar
  785. :return:
  786. """
  787. if self.theType is PlotType.Compare:
  788. the_text = str(self.adc)+"+"+str(self.secadc)
  789. else:
  790. the_text = str(self.adc)
  791. if self.theType in [ERROR, NO_DATA]:
  792. type = self._old_type
  793. else:
  794. type = self.theType
  795. if self.theDataType is LIVE:
  796. self.change_type_signal.emit(self.theId, type,
  797. the_text+" B: "+available_boards.get_board_name_from_id(self.board_id))
  798. else:
  799. self.change_type_signal.emit(self.theId, type, the_text)
  800. def closeEvent(self, event):
  801. """
  802. Event Handler to handle the event of closing this window
  803. :param event: QEvent
  804. :return: -
  805. """
  806. if not self.close_silent:
  807. if not self.parent.remove_plot(self.theId, silent=self.close_silent):
  808. event.ignore()
  809. return
  810. del self.data
  811. del self.plot_widget
  812. super(PlotWidget, self).closeEvent(event)
  813. if self.theDataType == LIVE:
  814. live_plot_windows.removeWindow(self.board_id, self)
  815. self.close_signal.emit()
  816. del self