frame.py 24 KB

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