frame.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. """
  2. @package frame
  3. @brief Timeline Tool
  4. Classes:
  5. - frame::DataCursor
  6. - frame::TimelineFrame
  7. - frame::LookUp
  8. (C) 2012-2016 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 Anna Kratochvilova <kratochanna gmail.com>
  12. """
  13. import os
  14. import signal
  15. from math import ceil
  16. from itertools import cycle
  17. import numpy as np
  18. import wx
  19. try:
  20. import matplotlib
  21. # The recommended way to use wx with mpl is with the WXAgg
  22. # backend.
  23. matplotlib.use('WXAgg')
  24. from matplotlib.figure import Figure
  25. from matplotlib.backends.backend_wxagg import \
  26. FigureCanvasWxAgg as FigCanvas, \
  27. NavigationToolbar2WxAgg as NavigationToolbar
  28. import matplotlib.dates as mdates
  29. from matplotlib import cbook
  30. except ImportError as e:
  31. raise ImportError(_('The Timeline Tool needs the "matplotlib" '
  32. '(python-matplotlib) package to be installed. {}').format(e))
  33. import grass.script as grass
  34. from core.utils import _
  35. import grass.temporal as tgis
  36. from core.gcmd import GError, GException, RunCommand
  37. from gui_core import gselect
  38. from core import globalvar
  39. ALPHA = 0.5
  40. COLORS = ['b', 'g', 'r', 'c', 'm', 'y', 'k']
  41. def check_version(*version):
  42. """Checks if given version or newer is installed"""
  43. versionInstalled = []
  44. for i in matplotlib.__version__.split('.'):
  45. try:
  46. v = int(i)
  47. versionInstalled.append(v)
  48. except ValueError:
  49. versionInstalled.append(0)
  50. if versionInstalled < list(version):
  51. return False
  52. else:
  53. return True
  54. class TimelineFrame(wx.Frame):
  55. """The main frame of the application"""
  56. def __init__(self, parent):
  57. wx.Frame.__init__(self, parent, id=wx.ID_ANY, title=_("GRASS GIS Timeline 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', multiple=True)
  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 spatio-temporal extents"))
  110. self.view3dCheck.Bind(wx.EVT_CHECKBOX, self.OnRedraw)
  111. if not check_version(1, 0, 0):
  112. self.view3dCheck.SetLabel(_("3D plot of spatio-temporal extents "
  113. "(matplotlib >= 1.0.0)"))
  114. self.view3dCheck.Disable()
  115. gridSizer.Add(wx.StaticText(self.panel, id=wx.ID_ANY,
  116. label=_("Select space time dataset(s):")),
  117. pos=(0, 0), flag=wx.EXPAND | wx.ALIGN_CENTER_VERTICAL)
  118. gridSizer.Add(self.datasetSelect, pos=(1, 0), flag=wx.EXPAND)
  119. gridSizer.Add(self.drawButton, pos=(1, 1), flag=wx.EXPAND)
  120. gridSizer.Add(self.helpButton, pos=(1, 2), flag=wx.EXPAND)
  121. gridSizer.Add(self.view3dCheck, pos=(2, 0), flag=wx.EXPAND | wx.ALIGN_CENTER_VERTICAL)
  122. self.vbox.Add(gridSizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=10)
  123. self.panel.SetSizer(self.vbox)
  124. self.vbox.Fit(self)
  125. def _getData(self, timeseries):
  126. """Load data and read properties"""
  127. self.timeData = {}
  128. mode = None
  129. unit = None
  130. for series in timeseries:
  131. name = series[0] + '@' + series[1]
  132. etype = series[2]
  133. sp = tgis.dataset_factory(etype, name)
  134. if not sp.is_in_db(dbif=self.dbif):
  135. GError(self, message=_("Dataset <%s> not found in temporal database") % (name))
  136. return
  137. sp.select(dbif=self.dbif)
  138. self.timeData[name] = {}
  139. self.timeData[name]['elementType'] = series[2]
  140. self.timeData[name]['temporalType'] = sp.get_temporal_type() # abs/rel
  141. if mode is None:
  142. mode = self.timeData[name]['temporalType']
  143. elif self.timeData[name]['temporalType'] != mode:
  144. GError(parent=self, message=_("Datasets have different temporal type "
  145. "(absolute x relative), which is not allowed."))
  146. return
  147. # check topology
  148. maps = sp.get_registered_maps_as_objects(dbif=self.dbif)
  149. self.timeData[name]['validTopology'] = sp.check_temporal_topology(maps=maps, dbif=self.dbif)
  150. self.timeData[name]['temporalMapType'] = sp.get_map_time() # point/interval
  151. self.timeData[name]['unit'] = None # only with relative
  152. if self.timeData[name]['temporalType'] == 'relative':
  153. start, end, self.timeData[name]['unit'] = sp.get_relative_time()
  154. if unit is None:
  155. unit = self.timeData[name]['unit']
  156. elif self.timeData[name]['unit'] != unit:
  157. GError(self, _("Datasets have different time unit which is not allowed."))
  158. return
  159. self.timeData[name]['start_datetime'] = []
  160. # self.timeData[name]['start_plot'] = []
  161. self.timeData[name]['end_datetime'] = []
  162. # self.timeData[name]['end_plot'] = []
  163. self.timeData[name]['names'] = []
  164. self.timeData[name]['north'] = []
  165. self.timeData[name]['south'] = []
  166. self.timeData[name]['west'] = []
  167. self.timeData[name]['east'] = []
  168. columns = ','.join(['name', 'start_time', 'end_time',
  169. 'north', 'south', 'west', 'east'])
  170. rows = sp.get_registered_maps(columns=columns, where=None,
  171. order='start_time', dbif=self.dbif)
  172. if not rows:
  173. GError(parent=self, message=_("Dataset <{name}> is empty").format(name=series[0] + '@' + series[1]))
  174. return
  175. for row in rows:
  176. mapName, start, end, north, south, west, east = row
  177. self.timeData[name]['start_datetime'].append(start)
  178. self.timeData[name]['end_datetime'].append(end)
  179. self.timeData[name]['names'].append(mapName)
  180. self.timeData[name]['north'].append(north)
  181. self.timeData[name]['south'].append(south)
  182. self.timeData[name]['west'].append(west)
  183. self.timeData[name]['east'].append(east)
  184. self.temporalType = mode
  185. self.unit = unit
  186. def _draw3dFigure(self):
  187. """Draws 3d view (spatio-temporal extents).
  188. Only for matplotlib versions >= 1.0.0.
  189. Earlier versions cannot draw time ticks and alpha
  190. and it has a slightly different API.
  191. """
  192. self.axes3d.clear()
  193. self.axes3d.grid(False)
  194. # self.axes3d.grid(True)
  195. if self.temporalType == 'absolute':
  196. convert = mdates.date2num
  197. else:
  198. convert = lambda x: x
  199. colors = cycle(COLORS)
  200. plots = []
  201. for name in self.datasets:
  202. name = name[0] + '@' + name[1]
  203. startZ = convert(self.timeData[name]['start_datetime'])
  204. mapType = self.timeData[name]['temporalMapType']
  205. if mapType == 'interval':
  206. dZ = convert(self.timeData[name]['end_datetime']) - startZ
  207. else:
  208. dZ = [0] * len(startZ)
  209. startX = self.timeData[name]['west']
  210. dX = self.timeData[name]['east'] - np.array(startX)
  211. startY = self.timeData[name]['south']
  212. dY = self.timeData[name]['north'] - np.array(startY)
  213. color = colors.next()
  214. plots.append(self.axes3d.bar3d(startX, startY, startZ, dX, dY, dZ,
  215. color=color, alpha=ALPHA))
  216. params = grass.read_command('g.proj', flags='g')
  217. params = grass.parse_key_val(params)
  218. if 'unit' in params:
  219. self.axes3d.set_xlabel(_("X [%s]") % params['unit'])
  220. self.axes3d.set_ylabel(_("Y [%s]") % params['unit'])
  221. else:
  222. self.axes3d.set_xlabel(_("X"))
  223. self.axes3d.set_ylabel(_("Y"))
  224. if self.temporalType == 'absolute':
  225. if check_version(1, 1, 0):
  226. self.axes3d.zaxis_date()
  227. self.axes3d.set_zlabel(_('Time'))
  228. self.axes3d.mouse_init()
  229. self.canvas.draw()
  230. def _draw2dFigure(self):
  231. """Draws 2D plot (temporal extents)"""
  232. self.axes2d.clear()
  233. self.axes2d.grid(True)
  234. if self.temporalType == 'absolute':
  235. convert = mdates.date2num
  236. else:
  237. convert = lambda x: x
  238. colors = cycle(COLORS)
  239. yticksNames = []
  240. yticksPos = []
  241. plots = []
  242. lookUp = LookUp(self.timeData)
  243. for i, name in enumerate(self.datasets):
  244. yticksNames.append(name[0]) # just name; with mapset it would be long
  245. name = name[0] + '@' + name[1]
  246. yticksPos.append(i)
  247. barData = []
  248. pointData = []
  249. mapType = self.timeData[name]['temporalMapType']
  250. start = convert(self.timeData[name]['start_datetime'])
  251. # TODO: mixed
  252. if mapType == 'interval':
  253. end = convert(self.timeData[name]['end_datetime'])
  254. lookUpData = zip(start, end)
  255. duration = end - np.array(start)
  256. barData = zip(start, duration)
  257. lookUp.AddDataset(type_='bar', yrange=(i - 0.1, i + 0.1),
  258. xranges=lookUpData, datasetName=name)
  259. else:
  260. # self.timeData[name]['end_plot'] = None
  261. pointData = start
  262. lookUp.AddDataset(type_='point', yrange=i, xranges=pointData, datasetName=name)
  263. color = colors.next()
  264. if mapType == 'interval':
  265. plots.append(self.axes2d.broken_barh(xranges=barData, yrange=(i - 0.1, 0.2),
  266. facecolors=color, alpha=ALPHA))
  267. else:
  268. plots.append(self.axes2d.plot(pointData, [i] * len(pointData),
  269. marker='o', linestyle='None', color=color)[0])
  270. if self.temporalType == 'absolute':
  271. self.axes2d.xaxis_date()
  272. self.fig.autofmt_xdate()
  273. # self.axes2d.set_xlabel(_("Time"))
  274. else:
  275. self.axes2d.set_xlabel(_("Time [%s]") % self.unit)
  276. self.axes2d.set_yticks(yticksPos)
  277. self.axes2d.set_yticklabels(yticksNames)
  278. self.axes2d.set_ylim(min(yticksPos) - 1, max(yticksPos) + 1)
  279. # adjust xlim
  280. xlim = self.axes2d.get_xlim()
  281. padding = ceil((xlim[1] - xlim[0]) / 20.)
  282. self.axes2d.set_xlim(xlim[0] - padding, xlim[1] + padding)
  283. self.canvas.draw()
  284. DataCursor(plots, lookUp, InfoFormat)
  285. def OnRedraw(self, event):
  286. """Required redrawing."""
  287. datasets = self.datasetSelect.GetValue().strip()
  288. if not datasets:
  289. return
  290. datasets = datasets.split(',')
  291. try:
  292. datasets = self._checkDatasets(datasets)
  293. if not datasets:
  294. return
  295. except GException, e:
  296. GError(parent=self, message=unicode(e), showTraceback=False)
  297. return
  298. self.datasets = datasets
  299. self._redraw()
  300. def _redraw(self):
  301. """Readraw data.
  302. Decides if to draw also 3D and adjusts layout if needed.
  303. """
  304. self._getData(self.datasets)
  305. # axes3d are physically removed
  306. if not self.axes2d:
  307. self.axes2d = self.fig.add_subplot(1, 1, 1)
  308. self._draw2dFigure()
  309. if check_version(1, 0, 0):
  310. if self.view3dCheck.IsChecked():
  311. self.axes2d.change_geometry(2, 1, 1)
  312. if not self.axes3d:
  313. # do not remove this import - unused but it is required for 3D
  314. from mpl_toolkits.mplot3d import Axes3D # pylint: disable=W0611
  315. self.axes3d = self.fig.add_subplot(2, 1, 2, projection='3d')
  316. self.axes3d.set_visible(True)
  317. self._draw3dFigure()
  318. else:
  319. if self.axes3d:
  320. self.fig.delaxes(self.axes3d)
  321. self.axes3d = None
  322. self.axes2d.change_geometry(1, 1, 1)
  323. self.canvas.draw()
  324. def _checkDatasets(self, datasets):
  325. """Checks and validates datasets.
  326. Reports also type of dataset (e.g. 'strds').
  327. :return: (mapName, mapset, type)
  328. """
  329. validated = []
  330. tDict = tgis.tlist_grouped('stds', group_type=True, dbif=self.dbif)
  331. # nested list with '(map, mapset, etype)' items
  332. allDatasets = [[[(map, mapset, etype) for map in maps]
  333. for etype, maps in etypesDict.iteritems()]
  334. for mapset, etypesDict in tDict.iteritems()]
  335. # flatten this list
  336. if allDatasets:
  337. allDatasets = reduce(lambda x, y: x + y, reduce(lambda x, y: x + y, allDatasets))
  338. mapsets = tgis.get_tgis_c_library_interface().available_mapsets()
  339. allDatasets = [i for i in sorted(allDatasets, key=lambda l: mapsets.index(l[1]))]
  340. for dataset in datasets:
  341. errorMsg = _("Space time dataset <%s> not found.") % dataset
  342. if dataset.find("@") >= 0:
  343. nameShort, mapset = dataset.split('@', 1)
  344. indices = [n for n, (mapName, mapsetName, etype) in enumerate(allDatasets)
  345. if nameShort == mapName and mapsetName == mapset]
  346. else:
  347. indices = [n for n, (mapName, mapset, etype) in enumerate(allDatasets)
  348. if dataset == mapName]
  349. if len(indices) == 0:
  350. raise GException(errorMsg)
  351. elif len(indices) >= 2:
  352. dlg = wx.SingleChoiceDialog(self,
  353. message=_("Please specify the space time dataset <%s>." % dataset),
  354. caption=_("Ambiguous dataset name"),
  355. choices=[("%(map)s@%(mapset)s: %(etype)s" % {'map': allDatasets[i][0],
  356. 'mapset': allDatasets[i][1],
  357. 'etype': allDatasets[i][2]})
  358. for i in indices],
  359. style=wx.CHOICEDLG_STYLE | wx.OK)
  360. if dlg.ShowModal() == wx.ID_OK:
  361. index = dlg.GetSelection()
  362. validated.append(allDatasets[indices[index]])
  363. else:
  364. continue
  365. else:
  366. validated.append(allDatasets[indices[0]])
  367. return validated
  368. def OnHelp(self, event):
  369. RunCommand('g.manual', quiet=True, entry='g.gui.timeline')
  370. # interface
  371. def SetDatasets(self, datasets):
  372. """Set data"""
  373. if not datasets:
  374. return
  375. try:
  376. datasets = self._checkDatasets(datasets)
  377. if not datasets:
  378. return
  379. except GException, e:
  380. GError(parent=self, message=unicode(e), showTraceback=False)
  381. return
  382. self.datasets = datasets
  383. self.datasetSelect.SetValue(','.join(map(lambda x: x[0] + '@' + x[1], datasets)))
  384. self._redraw()
  385. def Show3D(self, show):
  386. """Show also 3D if possible"""
  387. if check_version(1, 0, 0):
  388. self.view3dCheck.SetValue(show)
  389. class LookUp:
  390. """Helper class for searching info by coordinates"""
  391. def __init__(self, timeData):
  392. self.data = {}
  393. self.timeData = timeData
  394. def AddDataset(self, type_, yrange, xranges, datasetName):
  395. if type_ == 'bar':
  396. self.data[yrange] = {'name': datasetName}
  397. for i, (start, end) in enumerate(xranges):
  398. self.data[yrange][(start, end)] = i
  399. elif type_ == 'point':
  400. self.data[(yrange, yrange)] = {'name': datasetName}
  401. for i, start in enumerate(xranges):
  402. self.data[(yrange, yrange)][(start, start)] = i
  403. def GetInformation(self, x, y):
  404. keys = None
  405. for keyY in self.data.keys():
  406. if keyY[0] <= y <= keyY[1]:
  407. for keyX in self.data[keyY].keys():
  408. if keyX != 'name' and keyX[0] <= x <= keyX[1]:
  409. keys = keyY, keyX
  410. break
  411. if keys:
  412. break
  413. if not keys:
  414. return None
  415. datasetName = self.data[keys[0]]['name']
  416. mapIndex = self.data[keys[0]][keys[1]]
  417. return self.timeData, datasetName, mapIndex
  418. def InfoFormat(timeData, datasetName, mapIndex):
  419. """Formats information about dataset"""
  420. text = []
  421. etype = timeData[datasetName]['elementType']
  422. name, mapset = datasetName.split('@')
  423. if etype == 'strds':
  424. text.append(_("Space time raster dataset: %s") % name)
  425. elif etype == 'stvds':
  426. text.append(_("Space time vector dataset: %s") % name)
  427. elif etype == 'str3ds':
  428. text.append(_("Space time 3D raster dataset: %s") % name)
  429. text.append(_("Mapset: %s") % mapset)
  430. text.append(_("Map name: %s") % timeData[datasetName]['names'][mapIndex])
  431. text.append(_("Start time: %s") % timeData[datasetName]['start_datetime'][mapIndex])
  432. text.append(_("End time: %s") % timeData[datasetName]['end_datetime'][mapIndex])
  433. if not timeData[datasetName]['validTopology']:
  434. text.append(_("WARNING: invalid topology"))
  435. text.append(_("\nPress Del to dismiss."))
  436. return '\n'.join(text)
  437. class DataCursor(object):
  438. """A simple data cursor widget that displays the x,y location of a
  439. matplotlib artist when it is selected.
  440. Source: http://stackoverflow.com/questions/4652439/
  441. is-there-a-matplotlib-equivalent-of-matlabs-datacursormode/4674445
  442. """
  443. def __init__(self, artists, lookUp, formatFunction, tolerance=5, offsets=(-30, 30),
  444. display_all=False):
  445. """Create the data cursor and connect it to the relevant figure.
  446. "artists" is the matplotlib artist or sequence of artists that will be
  447. selected.
  448. "tolerance" is the radius (in points) that the mouse click must be
  449. within to select the artist.
  450. "offsets" is a tuple of (x,y) offsets in points from the selected
  451. point to the displayed annotation box
  452. "display_all" controls whether more than one annotation box will
  453. be shown if there are multiple axes. Only one will be shown
  454. per-axis, regardless.
  455. """
  456. self.lookUp = lookUp
  457. self.formatFunction = formatFunction
  458. self.offsets = offsets
  459. self.display_all = display_all
  460. if not cbook.iterable(artists):
  461. artists = [artists]
  462. self.artists = artists
  463. self.axes = tuple(set(art.axes for art in self.artists))
  464. self.figures = tuple(set(ax.figure for ax in self.axes))
  465. self.annotations = {}
  466. for ax in self.axes:
  467. self.annotations[ax] = self.annotate(ax)
  468. for artist in self.artists:
  469. artist.set_picker(tolerance)
  470. for fig in self.figures:
  471. fig.canvas.mpl_connect('pick_event', self)
  472. fig.canvas.mpl_connect('key_press_event', self.keyPressed)
  473. def keyPressed(self, event):
  474. """Key pressed - hide annotation if Delete was pressed"""
  475. if event.key != 'delete':
  476. return
  477. for ax in self.axes:
  478. self.annotations[ax].set_visible(False)
  479. event.canvas.draw()
  480. def annotate(self, ax):
  481. """Draws and hides the annotation box for the given axis "ax"."""
  482. annotation = ax.annotate(self.formatFunction, xy=(0, 0), ha='center',
  483. xytext=self.offsets, textcoords='offset points', va='bottom',
  484. bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.7),
  485. arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'),
  486. annotation_clip=False, multialignment='left')
  487. annotation.set_visible(False)
  488. return annotation
  489. def __call__(self, event):
  490. """Intended to be called through "mpl_connect"."""
  491. # Rather than trying to interpolate, just display the clicked coords
  492. # This will only be called if it's within "tolerance", anyway.
  493. x, y = event.mouseevent.xdata, event.mouseevent.ydata
  494. annotation = self.annotations[event.artist.axes]
  495. if x is not None:
  496. if not self.display_all:
  497. # Hide any other annotation boxes...
  498. for ann in self.annotations.values():
  499. ann.set_visible(False)
  500. # Update the annotation in the current axis..
  501. annotation.xy = x, y
  502. if 'Line2D' in str(type(event.artist)):
  503. y = event.artist.get_ydata()[0]
  504. xData = event.artist.get_xdata()
  505. x = xData[np.argmin(abs(xData - x))]
  506. info = self.lookUp.GetInformation(x, y)
  507. if not info:
  508. return
  509. text = self.formatFunction(*info)
  510. annotation.set_text(text)
  511. annotation.set_visible(True)
  512. event.canvas.draw()
  513. def run(parent=None, datasets=None):
  514. frame = TimelineFrame(parent)
  515. if datasets:
  516. frame.SetDatasets(datasets)
  517. frame.Show()
  518. if __name__ == '__main__':
  519. run()