Skip to content

Commit bff8759

Browse files
committed
Add QwtDateTimeScaleDraw and QwtDateTimeScaleEngine for datetime axis support
1 parent dab3cac commit bff8759

File tree

4 files changed

+243
-2
lines changed

4 files changed

+243
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Version 0.14.7
44

5+
- Added support for `QwtDateTimeScaleDraw` and `QwtDateTimeScaleEngine` for datetime axis support (see `QwtDateTimeScaleDraw` and `QwtDateTimeScaleEngine` classes in the `qwt` module)
56
- Improved font rendering for rotated text in `QwtPlainTextEngine.draw` method: disabled font hinting to avoid character misalignment in rotated text
67

78
## Version 0.14.6

qwt/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,16 @@
4848
QwtSeriesStore,
4949
)
5050
from qwt.scale_div import QwtScaleDiv # noqa: F401
51-
from qwt.scale_draw import QwtAbstractScaleDraw, QwtScaleDraw # noqa: F401
52-
from qwt.scale_engine import QwtLinearScaleEngine, QwtLogScaleEngine # noqa: F401
51+
from qwt.scale_draw import ( # noqa: F401
52+
QwtAbstractScaleDraw,
53+
QwtDateTimeScaleDraw,
54+
QwtScaleDraw,
55+
)
56+
from qwt.scale_engine import ( # noqa: F401
57+
QwtDateTimeScaleEngine,
58+
QwtLinearScaleEngine,
59+
QwtLogScaleEngine,
60+
)
5361
from qwt.scale_map import QwtScaleMap # noqa: F401
5462
from qwt.symbol import QwtSymbol as QSbl # see deprecated section
5563
from qwt.text import QwtText # noqa: F401

qwt/scale_draw.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"""
2121

2222
import math
23+
from datetime import datetime
2324

2425
from qtpy.QtCore import (
2526
QLineF,
@@ -1218,3 +1219,62 @@ def updateMap(self):
12181219
sm.setPaintInterval(pos.y() + len_, pos.y())
12191220
else:
12201221
sm.setPaintInterval(pos.x(), pos.x() + len_)
1222+
1223+
1224+
class QwtDateTimeScaleDraw(QwtScaleDraw):
1225+
"""Scale draw for datetime axis
1226+
1227+
This class formats axis labels as date/time strings from Unix timestamps.
1228+
1229+
Args:
1230+
format: Format string for datetime display (default: "%Y-%m-%d %H:%M:%S").
1231+
Uses Python datetime.strftime() format codes.
1232+
spacing: Spacing between labels (default: 4)
1233+
1234+
Examples:
1235+
>>> # Create a datetime scale with default format
1236+
>>> scale = QwtDateTimeScaleDraw()
1237+
1238+
>>> # Create a datetime scale with custom format (time only)
1239+
>>> scale = QwtDateTimeScaleDraw(format="%H:%M:%S")
1240+
1241+
>>> # Create a datetime scale with date only
1242+
>>> scale = QwtDateTimeScaleDraw(format="%Y-%m-%d", spacing=4)
1243+
"""
1244+
1245+
def __init__(self, format: str = "%Y-%m-%d %H:%M:%S", spacing: int = 4) -> None:
1246+
super().__init__()
1247+
self._format = format
1248+
self.setSpacing(spacing)
1249+
1250+
def get_format(self) -> str:
1251+
"""Get the current datetime format string
1252+
1253+
Returns:
1254+
str: Format string
1255+
"""
1256+
return self._format
1257+
1258+
def set_format(self, format: str) -> None:
1259+
"""Set the datetime format string
1260+
1261+
Args:
1262+
format: Format string for datetime display
1263+
"""
1264+
self._format = format
1265+
1266+
def label(self, value: float) -> QwtText:
1267+
"""Convert a timestamp value to a formatted date/time label
1268+
1269+
Args:
1270+
value: Unix timestamp (seconds since epoch)
1271+
1272+
Returns:
1273+
QwtText: Formatted label
1274+
"""
1275+
try:
1276+
dt = datetime.fromtimestamp(value)
1277+
return QwtText(dt.strftime(self._format))
1278+
except (ValueError, OSError):
1279+
# Handle invalid timestamps
1280+
return QwtText("")

qwt/scale_engine.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,3 +906,175 @@ def align(self, interval, stepSize):
906906
x2 = interval.maxValue()
907907

908908
return qwtPowInterval(self.base(), QwtInterval(x1, x2))
909+
910+
911+
class QwtDateTimeScaleEngine(QwtLinearScaleEngine):
912+
"""
913+
A scale engine for datetime scales that creates intelligent time-based tick intervals.
914+
915+
This engine calculates tick intervals that correspond to meaningful time units
916+
(seconds, minutes, hours, days, weeks, months, years) rather than arbitrary
917+
numerical spacing.
918+
"""
919+
920+
# Time intervals in seconds
921+
TIME_INTERVALS = [
922+
1, # 1 second
923+
5, # 5 seconds
924+
10, # 10 seconds
925+
15, # 15 seconds
926+
30, # 30 seconds
927+
60, # 1 minute
928+
2 * 60, # 2 minutes
929+
5 * 60, # 5 minutes
930+
10 * 60, # 10 minutes
931+
15 * 60, # 15 minutes
932+
30 * 60, # 30 minutes
933+
60 * 60, # 1 hour
934+
2 * 60 * 60, # 2 hours
935+
3 * 60 * 60, # 3 hours
936+
6 * 60 * 60, # 6 hours
937+
12 * 60 * 60, # 12 hours
938+
24 * 60 * 60, # 1 day
939+
2 * 24 * 60 * 60, # 2 days
940+
7 * 24 * 60 * 60, # 1 week
941+
2 * 7 * 24 * 60 * 60, # 2 weeks
942+
30 * 24 * 60 * 60, # 1 month (approx)
943+
3 * 30 * 24 * 60 * 60, # 3 months (approx)
944+
6 * 30 * 24 * 60 * 60, # 6 months (approx)
945+
365 * 24 * 60 * 60, # 1 year (approx)
946+
]
947+
948+
def __init__(self, base=10):
949+
super(QwtDateTimeScaleEngine, self).__init__(base)
950+
951+
def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.0):
952+
"""
953+
Calculate a scale division for a datetime interval
954+
955+
:param float x1: First interval limit (Unix timestamp)
956+
:param float x2: Second interval limit (Unix timestamp)
957+
:param int maxMajorSteps: Maximum for the number of major steps
958+
:param int maxMinorSteps: Maximum number of minor steps
959+
:param float stepSize: Step size. If stepSize == 0.0, calculates intelligent datetime step
960+
:return: Calculated scale division
961+
"""
962+
interval = QwtInterval(x1, x2).normalized()
963+
if interval.width() <= 0:
964+
return QwtScaleDiv()
965+
966+
# If stepSize is provided and > 0, use parent implementation
967+
if stepSize > 0.0:
968+
return super(QwtDateTimeScaleEngine, self).divideScale(
969+
x1, x2, maxMajorSteps, maxMinorSteps, stepSize
970+
)
971+
972+
# Calculate intelligent datetime step size
973+
duration = interval.width() # Duration in seconds
974+
975+
# Find the best time interval for the given duration and max steps
976+
best_step = self._find_best_time_step(duration, maxMajorSteps)
977+
978+
# Use the calculated datetime step
979+
scaleDiv = QwtScaleDiv()
980+
if best_step > 0.0:
981+
ticks = self.buildTicks(interval, best_step, maxMinorSteps)
982+
scaleDiv = QwtScaleDiv(interval, ticks)
983+
984+
if x1 > x2:
985+
scaleDiv.invert()
986+
987+
return scaleDiv
988+
989+
def _find_best_time_step(self, duration, max_steps):
990+
"""
991+
Find the best time interval step for the given duration and maximum steps.
992+
993+
:param float duration: Total duration in seconds
994+
:param int max_steps: Maximum number of major ticks
995+
:return: Best step size in seconds
996+
"""
997+
if max_steps < 1:
998+
max_steps = 1
999+
1000+
# Calculate the target step size
1001+
target_step = duration / max_steps
1002+
1003+
# Find the time interval that is closest to our target
1004+
best_step = self.TIME_INTERVALS[0]
1005+
min_error = abs(target_step - best_step)
1006+
1007+
for interval in self.TIME_INTERVALS:
1008+
error = abs(target_step - interval)
1009+
if error < min_error:
1010+
min_error = error
1011+
best_step = interval
1012+
# If the interval is getting much larger than target, stop
1013+
elif interval > target_step * 2:
1014+
break
1015+
1016+
return float(best_step)
1017+
1018+
def buildMinorTicks(self, ticks, maxMinorSteps, stepSize):
1019+
"""
1020+
Calculate minor ticks for datetime intervals
1021+
1022+
:param list ticks: List of tick arrays
1023+
:param int maxMinorSteps: Maximum number of minor steps
1024+
:param float stepSize: Major tick step size
1025+
"""
1026+
if maxMinorSteps < 1:
1027+
return
1028+
1029+
# For datetime, create intelligent minor tick intervals
1030+
minor_step = self._get_minor_step(stepSize, maxMinorSteps)
1031+
1032+
if minor_step <= 0:
1033+
return
1034+
1035+
major_ticks = ticks[QwtScaleDiv.MajorTick]
1036+
if len(major_ticks) < 2:
1037+
return
1038+
1039+
minor_ticks = []
1040+
1041+
# Generate minor ticks between each pair of major ticks
1042+
for i in range(len(major_ticks) - 1):
1043+
start = major_ticks[i]
1044+
end = major_ticks[i + 1]
1045+
1046+
# Add minor ticks between start and end
1047+
current = start + minor_step
1048+
while current < end:
1049+
minor_ticks.append(current)
1050+
current += minor_step
1051+
1052+
ticks[QwtScaleDiv.MinorTick] = minor_ticks
1053+
1054+
def _get_minor_step(self, major_step, max_minor_steps):
1055+
"""
1056+
Calculate appropriate minor tick step size for datetime intervals
1057+
1058+
:param float major_step: Major tick step size in seconds
1059+
:param int max_minor_steps: Maximum number of minor steps
1060+
:return: Minor tick step size in seconds
1061+
"""
1062+
# Define sensible minor tick divisions for different time scales
1063+
if major_step >= 365 * 24 * 60 * 60: # 1 year or more
1064+
return 30 * 24 * 60 * 60 # 1 month
1065+
elif major_step >= 30 * 24 * 60 * 60: # 1 month or more
1066+
return 7 * 24 * 60 * 60 # 1 week
1067+
elif major_step >= 7 * 24 * 60 * 60: # 1 week or more
1068+
return 24 * 60 * 60 # 1 day
1069+
elif major_step >= 24 * 60 * 60: # 1 day or more
1070+
return 6 * 60 * 60 # 6 hours
1071+
elif major_step >= 60 * 60: # 1 hour or more
1072+
return 15 * 60 # 15 minutes
1073+
elif major_step >= 10 * 60: # 10 minutes or more
1074+
return 2 * 60 # 2 minutes
1075+
elif major_step >= 60: # 1 minute or more
1076+
return 15 # 15 seconds
1077+
elif major_step >= 10: # 10 seconds or more
1078+
return 2 # 2 seconds
1079+
else: # Less than 10 seconds
1080+
return major_step / max(max_minor_steps, 2)

0 commit comments

Comments
 (0)