frame.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. """
  2. @package frame
  3. @brief Temporal Plot Tool
  4. Classes:
  5. - frame::DataCursor
  6. - frame::TplotFrame
  7. - frame::LookUp
  8. (C) 2012-2014 by the GRASS Development Team
  9. This program is free software under the GNU General Public License
  10. (>=v2). Read the file COPYING that comes with GRASS for details.
  11. @author Luca Delucchi
  12. """
  13. from itertools import cycle
  14. import numpy as np
  15. import wx
  16. try:
  17. import matplotlib
  18. # The recommended way to use wx with mpl is with the WXAgg
  19. # backend.
  20. matplotlib.use('WXAgg')
  21. from matplotlib.figure import Figure
  22. from matplotlib.backends.backend_wxagg import \
  23. FigureCanvasWxAgg as FigCanvas, \
  24. NavigationToolbar2WxAgg as NavigationToolbar
  25. import matplotlib.dates as mdates
  26. from matplotlib import cbook
  27. except ImportError:
  28. raise ImportError(_('The Temporal Plot Tool needs the "matplotlib" '
  29. '(python-matplotlib) package to be installed.'))
  30. from core.utils import _
  31. import grass.temporal as tgis
  32. from core.gcmd import GError, GException, RunCommand
  33. from gui_core import gselect
  34. from core import globalvar
  35. from grass.pygrass.vector.geometry import Point
  36. from grass.pygrass.raster import RasterRow
  37. from collections import OrderedDict
  38. ALPHA = 0.5
  39. COLORS = ['b', 'g', 'r', 'c', 'm', 'y', 'k']
  40. def check_version(*version):
  41. """Checks if given version or newer is installed"""
  42. versionInstalled = []
  43. for i in matplotlib.__version__.split('.'):
  44. try:
  45. v = int(i)
  46. versionInstalled.append(v)
  47. except ValueError:
  48. versionInstalled.append(0)
  49. if versionInstalled < list(version):
  50. return False
  51. else:
  52. return True
  53. class TplotFrame(wx.Frame):
  54. """The main frame of the application"""
  55. def __init__(self, parent):
  56. wx.Frame.__init__(self, parent, id=wx.ID_ANY,
  57. title=_("GRASS GIS Temporal Plot Tool"))
  58. tgis.init(True)
  59. self.datasets = []
  60. self.timeData = {}
  61. self._layout()
  62. self.temporalType = None
  63. self.unit = None
  64. # We create a database interface here to speedup the GUI
  65. self.dbif = tgis.SQLDatabaseInterfaceConnection()
  66. self.dbif.connect()
  67. def __del__(self):
  68. """Close the database interface and stop the messenger and C-interface
  69. subprocesses.
  70. """
  71. if self.dbif.connected is True:
  72. self.dbif.close()
  73. tgis.stop_subprocesses()
  74. def _layout(self):
  75. """Creates the main panel with all the controls on it:
  76. * mpl canvas
  77. * mpl navigation toolbar
  78. * Control panel for interaction
  79. """
  80. self.panel = wx.Panel(self)
  81. # Create the mpl Figure and FigCanvas objects.
  82. # 5x4 inches, 100 dots-per-inch
  83. #
  84. # color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND)
  85. self.fig = Figure((5.0, 4.0), facecolor=(1, 1, 1))
  86. self.canvas = FigCanvas(self.panel, wx.ID_ANY, self.fig)
  87. # axes are initialized later
  88. self.axes2d = None
  89. self.axes3d = None
  90. # Create the navigation toolbar, tied to the canvas
  91. #
  92. self.toolbar = NavigationToolbar(self.canvas)
  93. #
  94. # Layout
  95. #
  96. self.vbox = wx.BoxSizer(wx.VERTICAL)
  97. self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.EXPAND)
  98. self.vbox.Add(self.toolbar, 0, wx.EXPAND)
  99. self.vbox.AddSpacer(10)
  100. gridSizer = wx.GridBagSizer(hgap=5, vgap=5)
  101. self.datasetSelect = gselect.Select(parent=self.panel, id=wx.ID_ANY,
  102. # size=globalvar.DIALOG_GSELECT_SIZE,
  103. type='stds', size=(150, -1))
  104. self.drawButton = wx.Button(self.panel, id=wx.ID_ANY, label=_("Draw"))
  105. self.drawButton.Bind(wx.EVT_BUTTON, self.OnRedraw)
  106. self.helpButton = wx.Button(self.panel, id=wx.ID_ANY, label=_("Help"))
  107. self.helpButton.Bind(wx.EVT_BUTTON, self.OnHelp)
  108. # self.view3dCheck = wx.CheckBox(self.panel, id=wx.ID_ANY,
  109. # label=_("3D plot of queried data"))
  110. # self.view3dCheck.Bind(wx.EVT_CHECKBOX, self.OnRedraw)
  111. # if not check_version(1, 0, 0):
  112. # self.view3dCheck.SetLabel(_("3D plot of queried data "
  113. # "(matplotlib >= 1.0.0)"))
  114. # self.view3dCheck.Disable()
  115. self.xcoor = wx.StaticText(parent=self.panel, id=wx.ID_ANY,
  116. label=_('Insert longitude (x) coordinate'))
  117. self.xcoorval = wx.TextCtrl(parent=self.panel, id=wx.ID_ANY,
  118. size=(150, -1))
  119. self.ycoor = wx.StaticText(parent=self.panel, id=wx.ID_ANY,
  120. label=_('Insert latitude (y) coordinate'))
  121. self.ycoorval = wx.TextCtrl(parent=self.panel, id=wx.ID_ANY,
  122. size=(150, -1))
  123. gridSizer.Add(wx.StaticText(self.panel, id=wx.ID_ANY,
  124. label=_("Select space time dataset(s):")),
  125. pos=(0, 0), flag=wx.EXPAND | wx.ALIGN_CENTER_VERTICAL)
  126. gridSizer.Add(self.datasetSelect, pos=(1, 0), flag=wx.EXPAND)
  127. gridSizer.Add(self.xcoor, pos=(2, 0), flag=wx.EXPAND)
  128. gridSizer.Add(self.ycoor, pos=(2, 1), flag=wx.EXPAND)
  129. gridSizer.Add(self.xcoorval, pos=(3, 0), flag=wx.EXPAND)
  130. gridSizer.Add(self.ycoorval, pos=(3, 1), flag=wx.EXPAND)
  131. gridSizer.Add(self.drawButton, pos=(3, 2), flag=wx.EXPAND)
  132. gridSizer.Add(self.helpButton, pos=(3, 3), flag=wx.EXPAND)
  133. # gridSizer.Add(self.view3dCheck, pos=(4, 0), flag=wx.EXPAND | wx.ALIGN_CENTER_VERTICAL)
  134. self.vbox.Add(gridSizer, proportion=0, flag=wx.EXPAND | wx.ALL,
  135. border=10)
  136. self.panel.SetSizer(self.vbox)
  137. self.vbox.Fit(self)
  138. def _getData(self, timeseries):
  139. """Load data and read properties
  140. :param list timeseries: a list of timeseries
  141. """
  142. self.timeData = OrderedDict()
  143. mode = None
  144. unit = None
  145. columns = ','.join(['name', 'start_time', 'end_time'])
  146. for series in timeseries:
  147. name = series[0]
  148. fullname = name + '@' + series[1]
  149. etype = series[2]
  150. sp = tgis.dataset_factory(etype, fullname)
  151. sp.select(dbif=self.dbif)
  152. self.timeData[name] = OrderedDict()
  153. if not sp.is_in_db(dbif=self.dbif):
  154. GError(self, message=_("Dataset <%s> not found in temporal "
  155. "database") % (fullname))
  156. return
  157. self.timeData[name]['temporalDataType'] = etype
  158. self.timeData[name]['temporalType'] = sp.get_temporal_type()
  159. self.timeData[name]['granularity'] = sp.get_granularity()
  160. if mode is None:
  161. mode = self.timeData[name]['temporalType']
  162. elif self.timeData[name]['temporalType'] != mode:
  163. GError(parent=self, message=_("Datasets have different temporal"
  164. " type (absolute x relative), "
  165. "which is not allowed."))
  166. return
  167. # check topology
  168. maps = sp.get_registered_maps_as_objects(dbif=self.dbif)
  169. self.timeData[name]['validTopology'] = sp.check_temporal_topology(maps=maps, dbif=self.dbif)
  170. self.timeData[name]['unit'] = None # only with relative
  171. if self.timeData[name]['temporalType'] == 'relative':
  172. start, end, self.timeData[name]['unit'] = sp.get_relative_time()
  173. if unit is None:
  174. unit = self.timeData[name]['unit']
  175. elif self.timeData[name]['unit'] != unit:
  176. GError(self, _("Datasets have different time unit which "
  177. "is not allowed."))
  178. return
  179. rows = sp.get_registered_maps(columns=columns, where=None,
  180. order='start_time', dbif=self.dbif)
  181. for row in rows:
  182. self.timeData[name][row[0]] = {}
  183. self.timeData[name][row[0]]['start_datetime'] = row[1]
  184. self.timeData[name][row[0]]['end_datetime'] = row[2]
  185. r = RasterRow(row[0])
  186. r.open()
  187. val = r.get_value(self.poi)
  188. r.close()
  189. self.timeData[name][row[0]]['value'] = val
  190. self.unit = unit
  191. self.temporalType = mode
  192. return
  193. def _draw3dFigure(self):
  194. """Draws 3d view (spatio-temporal extents).
  195. Only for matplotlib versions >= 1.0.0.
  196. Earlier versions cannot draw time ticks and alpha
  197. and it has a slightly different API.
  198. """
  199. pass
  200. # self.axes3d.clear()
  201. # self.axes3d.grid(False)
  202. # # self.axes3d.grid(True)
  203. # if self.temporalType == 'absolute':
  204. # if check_version(1, 1, 0):
  205. # self.axes3d.zaxis_date()
  206. # convert = mdates.date2num
  207. # else:
  208. # convert = lambda x: x
  209. #
  210. # colors = cycle(COLORS)
  211. # plots = []
  212. # for name in self.datasets:
  213. # name = name[0] + '@' + name[1]
  214. # startZ = convert(self.timeData[name]['start_datetime'])
  215. # mapType = self.timeData[name]['temporalMapType']
  216. # if mapType == 'interval':
  217. # dZ = convert(self.timeData[name]['end_datetime']) - startZ
  218. #
  219. # else:
  220. # dZ = [0] * len(startZ)
  221. #
  222. # startX = self.timeData[name]['west']
  223. # dX = self.timeData[name]['east'] - np.array(startX)
  224. # startY = self.timeData[name]['south']
  225. # dY = self.timeData[name]['north'] - np.array(startY)
  226. #
  227. # color = colors.next()
  228. # plots.append(self.axes3d.bar3d(startX, startY, startZ, dX, dY, dZ,
  229. # color=color, alpha=ALPHA))
  230. # params = grass.read_command('g.proj', flags='g')
  231. # params = grass.parse_key_val(params)
  232. # if 'unit' in params:
  233. # self.axes3d.set_xlabel(_("X [%s]") % params['unit'])
  234. # self.axes3d.set_ylabel(_("Y [%s]") % params['unit'])
  235. # else:
  236. # self.axes3d.set_xlabel(_("X"))
  237. # self.axes3d.set_ylabel(_("Y"))
  238. #
  239. # self.axes3d.set_zlabel(_('Time'))
  240. # self.axes3d.mouse_init()
  241. # self.canvas.draw()
  242. def _draw2dFigure(self):
  243. """Draws or print 2D plot (temporal extents)"""
  244. self.axes2d.clear()
  245. self.axes2d.grid(False)
  246. if self.temporalType == 'absolute':
  247. self.axes2d.xaxis_date()
  248. self.fig.autofmt_xdate()
  249. self.convert = mdates.date2num
  250. self.invconvert = mdates.num2date
  251. else:
  252. self.convert = lambda x: x
  253. self.invconvert = self.convert
  254. colors = cycle(COLORS)
  255. yticksNames = []
  256. yticksPos = []
  257. plots = []
  258. lookUp = LookUp(self.timeData, self.invconvert)
  259. for i, name in enumerate(self.datasets):
  260. fullname = name[0] + '@' + name[1]
  261. name = name[0]
  262. yticksNames.append(name) # just name; with mapset it would be long
  263. yticksPos.append(i)
  264. xdata = []
  265. ydata = []
  266. for keys, values in self.timeData[name].iteritems():
  267. if keys in ['temporalType', 'granularity', 'validTopology',
  268. 'unit', 'temporalDataType']:
  269. continue
  270. xdata.append(self.convert(values['start_datetime']))
  271. ydata.append(values['value'])
  272. lookUp.AddDataset(yranges=ydata, xranges=xdata, datasetName=name)
  273. color = colors.next()
  274. plots.append(self.axes2d.plot(xdata, ydata, marker='o',
  275. color=color, label=name)[0])
  276. if self.temporalType == 'absolute':
  277. self.axes2d.set_xlabel(_("Temporal resolution: %s" % self.timeData[name]['granularity']))
  278. else:
  279. self.axes2d.set_xlabel(_("Time [%s]") % self.unit)
  280. self.axes2d.set_ylabel(', '.join(yticksNames))
  281. #legend
  282. handles, labels = self.axes2d.get_legend_handles_labels()
  283. self.axes2d.legend()
  284. if self.output:
  285. self.canvas.print_figure(filename=self.output, dpi=self.dpi)
  286. else:
  287. self.canvas.draw()
  288. DataCursor(plots, lookUp, InfoFormat, self.convert)
  289. def OnRedraw(self, event):
  290. """Required redrawing."""
  291. datasets = self.datasetSelect.GetValue().strip()
  292. if not datasets:
  293. return
  294. datasets = datasets.split(',')
  295. try:
  296. datasets = self._checkDatasets(datasets)
  297. if not datasets:
  298. return
  299. except GException:
  300. GError(parent=self, message=_("Invalid input data"))
  301. return
  302. self.datasets = datasets
  303. coors = [self.xcoorval.GetValue().strip(),
  304. self.ycoorval.GetValue().strip()]
  305. if coors:
  306. try:
  307. self.poi = Point(float(coors[0]), float(coors[1]))
  308. except GException:
  309. GError(parent=self, message=_("Invalid input coordinates"))
  310. return
  311. self._redraw()
  312. def _redraw(self):
  313. """Readraw data.
  314. Decides if to draw also 3D and adjusts layout if needed.
  315. """
  316. self._getData(self.datasets)
  317. # axes3d are physically removed
  318. if not self.axes2d:
  319. self.axes2d = self.fig.add_subplot(1, 1, 1)
  320. self._draw2dFigure()
  321. # if check_version(1, 0, 0):
  322. # if self.view3dCheck.IsChecked():
  323. # self.axes2d.change_geometry(2, 1, 1)
  324. # if not self.axes3d:
  325. # # do not remove this import - unused but it is required for 3D
  326. # from mpl_toolkits.mplot3d import Axes3D # pylint: disable=W0611
  327. # self.axes3d = self.fig.add_subplot(2, 1, 2, projection='3d')
  328. #
  329. # self.axes3d.set_visible(True)
  330. # self._draw3dFigure()
  331. # else:
  332. # if self.axes3d:
  333. # self.fig.delaxes(self.axes3d)
  334. # self.axes3d = None
  335. # self.axes2d.change_geometry(1, 1, 1)
  336. # self.canvas.draw()
  337. def _checkDatasets(self, datasets):
  338. """Checks and validates datasets.
  339. Reports also type of dataset (e.g. 'strds').
  340. :param list datasets: list of temporal dataset's name
  341. :return: (mapName, mapset, type)
  342. """
  343. validated = []
  344. tDict = tgis.tlist_grouped('stds', group_type=True, dbif=self.dbif)
  345. # nested list with '(map, mapset, etype)' items
  346. allDatasets = [[[(map, mapset, etype) for map in maps]
  347. for etype, maps in etypesDict.iteritems()]
  348. for mapset, etypesDict in tDict.iteritems()]
  349. # flatten this list
  350. if allDatasets:
  351. allDatasets = reduce(lambda x, y: x + y, reduce(lambda x, y: x + y,
  352. allDatasets))
  353. mapsets = tgis.get_tgis_c_library_interface().available_mapsets()
  354. allDatasets = [i for i in sorted(allDatasets,
  355. key=lambda l: mapsets.index(l[1]))]
  356. for dataset in datasets:
  357. errorMsg = _("Space time dataset <%s> not found.") % dataset
  358. if dataset.find("@") >= 0:
  359. nameShort, mapset = dataset.split('@', 1)
  360. indices = [n for n, (mapName, mapsetName, etype) in enumerate(allDatasets)
  361. if nameShort == mapName and mapsetName == mapset]
  362. else:
  363. indices = [n for n, (mapName, mapset, etype) in enumerate(allDatasets)
  364. if dataset == mapName]
  365. if len(indices) == 0:
  366. raise GException(errorMsg)
  367. elif len(indices) >= 2:
  368. dlg = wx.SingleChoiceDialog(self,
  369. message=_("Please specify the "
  370. "space time dataset "
  371. "<%s>." % dataset),
  372. caption=_("Ambiguous dataset name"),
  373. choices=[("%(map)s@%(mapset)s:"
  374. " %(etype)s" % {'map': allDatasets[i][0],
  375. 'mapset': allDatasets[i][1],
  376. 'etype': allDatasets[i][2]})
  377. for i in indices],
  378. style=wx.CHOICEDLG_STYLE | wx.OK)
  379. if dlg.ShowModal() == wx.ID_OK:
  380. index = dlg.GetSelection()
  381. validated.append(allDatasets[indices[index]])
  382. else:
  383. continue
  384. else:
  385. validated.append(allDatasets[indices[0]])
  386. return validated
  387. def OnHelp(self, event):
  388. """Function to show help"""
  389. RunCommand('g.manual', quiet=True, entry='g.gui.tplot')
  390. def SetDatasets(self, datasets, coors, output, dpi):
  391. """Set the data
  392. :param list datasets: a list of temporal dataset's name
  393. :param list coors: a list with x/y coordinates
  394. :param str output: the name of output png file
  395. :param int dpi: the dpi value for png file
  396. """
  397. if not datasets or not coors:
  398. return
  399. try:
  400. datasets = self._checkDatasets(datasets)
  401. if not datasets:
  402. return
  403. except GException:
  404. GError(parent=self, message=_("Invalid input temporal dataset"))
  405. return
  406. try:
  407. self.poi = Point(float(coors[0]), float(coors[1]))
  408. except GException:
  409. GError(parent=self, message=_("Invalid input coordinates"))
  410. return
  411. self.datasets = datasets
  412. self.output = output
  413. self.dpi = dpi
  414. self.datasetSelect.SetValue(','.join(map(lambda x: x[0] + '@' + x[1],
  415. datasets)))
  416. self.xcoorval.SetValue(str(coors[0]))
  417. self.ycoorval.SetValue(str(coors[1]))
  418. self._redraw()
  419. # def Show3D(self, show):
  420. # """Show also 3D if possible"""
  421. # if check_version(1, 0, 0):
  422. # self.view3dCheck.SetValue(show)
  423. class LookUp:
  424. """Helper class for searching info by coordinates"""
  425. def __init__(self, timeData, convert):
  426. self.data = {}
  427. self.timeData = timeData
  428. self.convert = convert
  429. def AddDataset(self, yranges, xranges, datasetName):
  430. if len(yranges) != len(xranges):
  431. GError(parent=self, message=_("Datasets have different number of"
  432. "values"))
  433. self.data[datasetName] = {}
  434. for i in range(len(xranges)):
  435. self.data[datasetName][xranges[i]] = yranges[i]
  436. def GetInformation(self, x):
  437. values = {}
  438. for key, value in self.data.iteritems():
  439. if value[x]:
  440. values[key] = [self.convert(x), value[x]]
  441. if len(values) == 0:
  442. return None
  443. return self.timeData, values
  444. def InfoFormat(timeData, values):
  445. """Formats information about dataset"""
  446. text = []
  447. for key, val in values.iteritems():
  448. etype = timeData[key]['temporalDataType']
  449. if etype == 'strds':
  450. text.append(_("Space time raster dataset: %s") % key)
  451. elif etype == 'stvds':
  452. text.append(_("Space time vector dataset: %s") % key)
  453. elif etype == 'str3ds':
  454. text.append(_("Space time 3D raster dataset: %s") % key)
  455. text.append(_("Value for {date} is {val}".format(date=val[0], val=val[1])))
  456. text.append('\n')
  457. text.append(_("Press Del to dismiss."))
  458. return '\n'.join(text)
  459. class DataCursor(object):
  460. """A simple data cursor widget that displays the x,y location of a
  461. matplotlib artist when it is selected.
  462. Source: http://stackoverflow.com/questions/4652439/
  463. is-there-a-matplotlib-equivalent-of-matlabs-datacursormode/4674445
  464. """
  465. def __init__(self, artists, lookUp, formatFunction, convert,
  466. tolerance=5, offsets=(-30, 20), display_all=False):
  467. """Create the data cursor and connect it to the relevant figure.
  468. "artists" is the matplotlib artist or sequence of artists that will be
  469. selected.
  470. "tolerance" is the radius (in points) that the mouse click must be
  471. within to select the artist.
  472. "offsets" is a tuple of (x,y) offsets in points from the selected
  473. point to the displayed annotation box
  474. "display_all" controls whether more than one annotation box will
  475. be shown if there are multiple axes. Only one will be shown
  476. per-axis, regardless.
  477. """
  478. self.lookUp = lookUp
  479. self.formatFunction = formatFunction
  480. self.offsets = offsets
  481. self.display_all = display_all
  482. if not cbook.iterable(artists):
  483. artists = [artists]
  484. self.artists = artists
  485. self.convert = convert
  486. self.axes = tuple(set(art.axes for art in self.artists))
  487. self.figures = tuple(set(ax.figure for ax in self.axes))
  488. self.annotations = {}
  489. for ax in self.axes:
  490. self.annotations[ax] = self.annotate(ax)
  491. for artist in self.artists:
  492. artist.set_picker(tolerance)
  493. for fig in self.figures:
  494. fig.canvas.mpl_connect('pick_event', self)
  495. fig.canvas.mpl_connect('key_press_event', self.keyPressed)
  496. def keyPressed(self, event):
  497. """Key pressed - hide annotation if Delete was pressed"""
  498. if event.key != 'delete':
  499. return
  500. for ax in self.axes:
  501. self.annotations[ax].set_visible(False)
  502. event.canvas.draw()
  503. def annotate(self, ax):
  504. """Draws and hides the annotation box for the given axis "ax"."""
  505. annotation = ax.annotate(self.formatFunction, xy=(0, 0), ha='center',
  506. xytext=self.offsets, va='bottom',
  507. textcoords='offset points',
  508. bbox=dict(boxstyle='round,pad=0.5',
  509. fc='yellow', alpha=0.7),
  510. arrowprops=dict(arrowstyle='->',
  511. connectionstyle='arc3,rad=0'),
  512. annotation_clip=False, multialignment='left')
  513. annotation.set_visible(False)
  514. return annotation
  515. def __call__(self, event):
  516. """Intended to be called through "mpl_connect"."""
  517. # Rather than trying to interpolate, just display the clicked coords
  518. # This will only be called if it's within "tolerance", anyway.
  519. x, y = event.mouseevent.xdata, event.mouseevent.ydata
  520. annotation = self.annotations[event.artist.axes]
  521. if x is not None:
  522. if not self.display_all:
  523. # Hide any other annotation boxes...
  524. for ann in self.annotations.values():
  525. ann.set_visible(False)
  526. if 'Line2D' in str(type(event.artist)):
  527. xData = []
  528. for a in event.artist.get_xdata():
  529. try:
  530. d = self.convert(a)
  531. except:
  532. d = a
  533. xData.append(d)
  534. x = xData[np.argmin(abs(xData - x))]
  535. info = self.lookUp.GetInformation(x)
  536. ys = zip(*info[1].values())[1]
  537. if not info:
  538. return
  539. # Update the annotation in the current axis..
  540. annotation.xy = x, max(ys)
  541. text = self.formatFunction(*info)
  542. annotation.set_text(text)
  543. annotation.set_visible(True)
  544. event.canvas.draw()
  545. def run(parent=None, datasets=None):
  546. frame = TplotFrame(parent)
  547. if datasets:
  548. frame.SetDatasets(datasets)
  549. frame.Show()
  550. if __name__ == '__main__':
  551. run()