Skip to content

Commit 5924fc8

Browse files
committed
Add QwtDateTimeScaleEngine for intelligent datetime scale divisions
1 parent 56ea562 commit 5924fc8

File tree

2 files changed

+177
-1
lines changed

2 files changed

+177
-1
lines changed

qwt/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@
4949
)
5050
from qwt.scale_div import QwtScaleDiv # noqa: F401
5151
from qwt.scale_draw import QwtAbstractScaleDraw, QwtScaleDraw # noqa: F401
52-
from qwt.scale_engine import QwtLinearScaleEngine, QwtLogScaleEngine # noqa: F401
52+
from qwt.scale_engine import ( # noqa: F401
53+
QwtDateTimeScaleEngine,
54+
QwtLinearScaleEngine,
55+
QwtLogScaleEngine,
56+
)
5357
from qwt.scale_map import QwtScaleMap # noqa: F401
5458
from qwt.symbol import QwtSymbol as QSbl # see deprecated section
5559
from qwt.text import QwtText # noqa: F401

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)