frame.py 23 KB

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