forked from SeismicData/asdf_sextant
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDateAxisItem.py
229 lines (199 loc) · 8.52 KB
/
DateAxisItem.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
"""
From https://github.com/pyqtgraph/pyqtgraph/pull/74
Modified a bit.
"""
import numpy as np
import time
from datetime import datetime
from pyqtgraph import AxisItem
__all__ = ['DateAxisItem', 'ZoomLevel']
MS_SPACING = 1/1000.0
SECOND_SPACING = 1
MINUTE_SPACING = 60
HOUR_SPACING = 3600
DAY_SPACING = 24 * HOUR_SPACING
WEEK_SPACING = 7 * DAY_SPACING
MONTH_SPACING = 30 * DAY_SPACING
YEAR_SPACING = 365 * DAY_SPACING
def makeMSStepper(stepSize):
def stepper(val, n):
val *= 1000
f = stepSize * 1000
return (val // (n*f) + 1) * (n*f) / 1000.0
return stepper
def makeSStepper(stepSize):
def stepper(val, n):
return (val // (n*stepSize) + 1) * (n*stepSize)
return stepper
def makeMStepper(stepSize):
def stepper(val, n):
d = datetime.utcfromtimestamp(val)
base0m = (d.month + n*stepSize - 1)
d = datetime(d.year + base0m // 12, base0m % 12 + 1, 1)
return (d - datetime(1970, 1, 1)).total_seconds()
return stepper
def makeYStepper(stepSize):
def stepper(val, n):
d = datetime.utcfromtimestamp(val)
next_date = datetime((d.year // (n*stepSize) + 1) * (n*stepSize), 1, 1)
return (next_date - datetime(1970, 1, 1)).total_seconds()
return stepper
class TickSpec:
""" Specifies the properties for a set of date ticks and computes ticks
within a given utc timestamp range """
def __init__(self, spacing, stepper, format, autoSkip=None):
"""
============= =========================================================
Arguments
spacing approximate (average) tick spacing
stepper a stepper function that takes a utc time stamp and a step
steps number n to compute the start of the next unit. You
can use the make_X_stepper functions to create common
steppers.
format a strftime compatible format string which will be used to
convert tick locations to date/time strings
autoSkip list of step size multipliers to be applied when the tick
density becomes too high. The tick spec automatically
applies additional powers of 10 (10, 100, ...) to the
list if necessary. Set to None to switch autoSkip off
============= =========================================================
"""
self.spacing = spacing
self.step = stepper
self.format = format
self.autoSkip = autoSkip
def makeTicks(self, minVal, maxVal, minSpc):
ticks = []
n = self.skipFactor(minSpc)
x = self.step(minVal, n)
while x <= maxVal:
ticks.append(x)
x = self.step(x, n)
return (np.array(ticks), n)
def skipFactor(self, minSpc):
if self.autoSkip is None or minSpc < self.spacing:
return 1
factors = np.array(self.autoSkip)
while True:
for f in factors:
spc = self.spacing * f
if spc > minSpc:
return f
factors *= 10
class ZoomLevel:
""" Generates the ticks which appear in a specific zoom level """
def __init__(self, tickSpecs):
"""
============= =========================================================
tickSpecs a list of one or more TickSpec objects with decreasing
coarseness
============= =========================================================
"""
self.tickSpecs = tickSpecs
self.utcOffset = 0
def tickValues(self, minVal, maxVal, minSpc):
# return tick values for this format in the range minVal, maxVal
# the return value is a list of tuples (<avg spacing>,
# [tick positions]) minSpc indicates the minimum spacing (in seconds)
# between two ticks to fullfill the maxTicksPerPt constraint of the
# DateAxisItem at the current zoom level. This is used for auto
# skipping ticks.
allTicks = []
valueSpecs = []
# back-project (minVal maxVal) to UTC, compute ticks then offset to
# back to local time again
utcMin = minVal - self.utcOffset
utcMax = maxVal - self.utcOffset
for spec in self.tickSpecs:
ticks, skipFactor = spec.makeTicks(utcMin, utcMax, minSpc)
# reposition tick labels to local time coordinates
ticks += self.utcOffset
# remove any ticks that were present in higher levels
tick_list = [x for x in ticks.tolist() if x not in allTicks]
allTicks.extend(tick_list)
valueSpecs.append((spec.spacing, tick_list))
# if we're skipping ticks on the current level there's no point in
# producing lower level ticks
if skipFactor > 1:
break
return valueSpecs
YEAR_MONTH_ZOOM_LEVEL = ZoomLevel([
TickSpec(YEAR_SPACING, makeYStepper(1), '%Y', autoSkip=[1, 5, 10, 25]),
TickSpec(MONTH_SPACING, makeMStepper(1), '%b')
])
MONTH_DAY_ZOOM_LEVEL = ZoomLevel([
TickSpec(MONTH_SPACING, makeMStepper(1), '%b'),
TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%d', autoSkip=[1, 5])
])
DAY_HOUR_ZOOM_LEVEL = ZoomLevel([
TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%a %d'),
TickSpec(HOUR_SPACING, makeSStepper(HOUR_SPACING), '%H:%M',
autoSkip=[1, 6])
])
HOUR_MINUTE_ZOOM_LEVEL = ZoomLevel([
TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%a %d'),
TickSpec(MINUTE_SPACING, makeSStepper(MINUTE_SPACING), '%H:%M',
autoSkip=[1, 5, 15])
])
HMS_ZOOM_LEVEL = ZoomLevel([
TickSpec(SECOND_SPACING, makeSStepper(SECOND_SPACING), '%H:%M:%S',
autoSkip=[1, 5, 15, 30])
])
MS_ZOOM_LEVEL = ZoomLevel([
TickSpec(MINUTE_SPACING, makeSStepper(MINUTE_SPACING), '%H:%M:%S'),
TickSpec(MS_SPACING, makeMSStepper(MS_SPACING), '%S.%f',
autoSkip=[1, 5, 10, 25])
])
class DateAxisItem(AxisItem):
""" An AxisItem that displays dates from unix timestamps
The display format is adjusted automatically depending on the current time
density (seconds/point) on the axis.
You can customize the behaviour by specifying a different set of zoom
levels than the default one. The zoomLevels variable is a dictionary with
the maximum number of seconds/point which are allowed for each ZoomLevel
before the axis switches to the next coarser level.
"""
def __init__(self, orientation, utcOffset=None, **kvargs):
super(DateAxisItem, self).__init__(orientation, **kvargs)
# Set the zoom level to use depending on the time density on the axis
if utcOffset is None:
self.utcOffset = time.timezone
else:
self.utcOffset = utcOffset
self.zoomLevel = YEAR_MONTH_ZOOM_LEVEL
# we need about 60pt for our largest label
self.maxTicksPerPt = 1/60.0
self.zoomLevels = {
self.maxTicksPerPt: MS_ZOOM_LEVEL,
30 * self.maxTicksPerPt: HMS_ZOOM_LEVEL,
15 * 60 * self.maxTicksPerPt: HOUR_MINUTE_ZOOM_LEVEL,
6 * 3600 * self.maxTicksPerPt: DAY_HOUR_ZOOM_LEVEL,
5 * 3600*24 * self.maxTicksPerPt: MONTH_DAY_ZOOM_LEVEL,
3600*24*30 * self.maxTicksPerPt: YEAR_MONTH_ZOOM_LEVEL
}
def tickStrings(self, values, scale, spacing):
tickSpecs = self.zoomLevel.tickSpecs
tickSpec = next((s for s in tickSpecs if s.spacing == spacing), None)
dates = [datetime.utcfromtimestamp(v - self.utcOffset) for v in values]
formatStrings = []
for x in dates:
try:
if '%f' in tickSpec.format:
# we only support ms precision
formatStrings.append(x.strftime(tickSpec.format)[:-3])
else:
formatStrings.append(x.strftime(tickSpec.format))
except ValueError: # Windows can't handle dates before 1970
formatStrings.append('')
return formatStrings
def tickValues(self, minVal, maxVal, size):
density = (maxVal - minVal) / size
self.setZoomLevelForDensity(density)
minSpacing = density / self.maxTicksPerPt
values = self.zoomLevel.tickValues(minVal, maxVal, minSpc=minSpacing)
return values
def setZoomLevelForDensity(self, density):
keys = sorted(self.zoomLevels.keys())
key = next((k for k in keys if density < k), keys[-1])
self.zoomLevel = self.zoomLevels[key]
self.zoomLevel.utcOffset = self.utcOffset