|
3 | 3 | import warnings |
4 | 4 | from xml.sax.saxutils import escape |
5 | 5 | from math import log10, floor, ceil |
| 6 | +from datetime import datetime, timezone |
| 7 | +from time import gmtime |
6 | 8 |
|
7 | 9 | import numpy as np |
8 | 10 | from AnyQt.QtCore import Qt, QRectF, QSize, QTimer, pyqtSignal as Signal, \ |
|
22 | 24 | ) |
23 | 25 | from pyqtgraph.graphicsItems.TextItem import TextItem |
24 | 26 |
|
| 27 | +from Orange.preprocess.discretize import _time_binnings |
25 | 28 | from Orange.widgets.utils import colorpalettes |
26 | 29 | from Orange.util import OrangeDeprecationWarning |
27 | 30 | from Orange.widgets import gui |
@@ -286,6 +289,77 @@ def _make_pen(color, width): |
286 | 289 | return p |
287 | 290 |
|
288 | 291 |
|
| 292 | +class AxisItem(pg.AxisItem): |
| 293 | + """ |
| 294 | + Axis that if needed displays ticks appropriate for time data. |
| 295 | + """ |
| 296 | + |
| 297 | + _label_width = 80 |
| 298 | + |
| 299 | + def __init__(self, *args, **kwargs): |
| 300 | + super().__init__(*args, **kwargs) |
| 301 | + self._use_time = False |
| 302 | + |
| 303 | + def use_time(self, enable): |
| 304 | + """Enables axes to display ticks for time data.""" |
| 305 | + self._use_time = enable |
| 306 | + self.enableAutoSIPrefix(not enable) |
| 307 | + |
| 308 | + def tickValues(self, minVal, maxVal, size): |
| 309 | + """Find appropriate tick locations.""" |
| 310 | + if not self._use_time: |
| 311 | + return super().tickValues(minVal, maxVal, size) |
| 312 | + |
| 313 | + # if timezone is not set, then local is used which cause exceptions |
| 314 | + minVal = max(minVal, |
| 315 | + datetime.min.replace(tzinfo=timezone.utc).timestamp() + 1) |
| 316 | + maxVal = min(maxVal, |
| 317 | + datetime.max.replace(tzinfo=timezone.utc).timestamp() - 1) |
| 318 | + mn, mx = gmtime(minVal), gmtime(maxVal) |
| 319 | + |
| 320 | + try: |
| 321 | + bins = _time_binnings(mn, mx, 6, 30)[-1] |
| 322 | + except (IndexError, ValueError): |
| 323 | + # cannot handle very large and very small time intervals |
| 324 | + return super().tickValues(minVal, maxVal, size) |
| 325 | + |
| 326 | + ticks = bins.thresholds |
| 327 | + |
| 328 | + max_steps = max(int(size / self._label_width), 1) |
| 329 | + if len(ticks) > max_steps: |
| 330 | + # remove some of ticks so that they don't overlap |
| 331 | + step = int(np.ceil(float(len(ticks)) / max_steps)) |
| 332 | + ticks = ticks[::step] |
| 333 | + |
| 334 | + spacing = min(b - a for a, b in zip(ticks[:-1], ticks[1:])) |
| 335 | + return [(spacing, ticks)] |
| 336 | + |
| 337 | + def tickStrings(self, values, scale, spacing): |
| 338 | + """Format tick values according to space between them.""" |
| 339 | + if not self._use_time: |
| 340 | + return super().tickStrings(values, scale, spacing) |
| 341 | + |
| 342 | + if spacing >= 3600 * 24 * 365: |
| 343 | + fmt = "%Y" |
| 344 | + elif spacing >= 3600 * 24 * 28: |
| 345 | + fmt = "%Y %b" |
| 346 | + elif spacing >= 3600 * 24: |
| 347 | + fmt = "%Y %b %d" |
| 348 | + elif spacing >= 3600: |
| 349 | + fmt = "%d %Hh" |
| 350 | + elif spacing >= 60: |
| 351 | + fmt = "%H:%M" |
| 352 | + elif spacing >= 1: |
| 353 | + fmt = "%H:%M:%S" |
| 354 | + else: |
| 355 | + fmt = '%S.%f' |
| 356 | + |
| 357 | + # if timezone is not set, then local timezone is used |
| 358 | + # which cause exceptions for edge cases |
| 359 | + return [datetime.fromtimestamp(x, tz=timezone.utc).strftime(fmt) |
| 360 | + for x in values] |
| 361 | + |
| 362 | + |
289 | 363 | class OWScatterPlotBase(gui.OWComponent, QObject): |
290 | 364 | """ |
291 | 365 | Provide a graph component for widgets that show any kind of point plot |
@@ -408,8 +482,9 @@ def __init__(self, scatter_widget, parent=None, view_box=ViewBox): |
408 | 482 | self.subset_is_shown = False |
409 | 483 |
|
410 | 484 | self.view_box = view_box(self) |
| 485 | + _axis = {"left": AxisItem("left"), "bottom": AxisItem("bottom")} |
411 | 486 | self.plot_widget = pg.PlotWidget(viewBox=self.view_box, parent=parent, |
412 | | - background="w") |
| 487 | + background="w", axisItems=_axis) |
413 | 488 | self.plot_widget.hideAxis("left") |
414 | 489 | self.plot_widget.hideAxis("bottom") |
415 | 490 | self.plot_widget.getPlotItem().buttonsHidden = True |
|
0 commit comments