Implement log scale for y axis

This commit is contained in:
Peter J. Holzer 2022-12-31 16:58:38 +01:00 committed by Peter J. Holzer
parent d6bdaa4128
commit fedc4c66ac
1 changed files with 191 additions and 124 deletions

View File

@ -63,8 +63,10 @@ class Dashboard:
class Widget: class Widget:
def __init__(self, d): def __init__(self, d):
log.debug("")
self.type = d["type"] self.type = d["type"]
self.stops = d["stops"] self.stops = d["stops"]
self.yscale = d.get("yscale", "linear")
log.debug("data = %s", d["data"]) log.debug("data = %s", d["data"])
self.lts = LTS(id=d["data"][0]) # by default we handle only one data source self.lts = LTS(id=d["data"][0]) # by default we handle only one data source
pass pass
@ -140,6 +142,7 @@ class Widget:
class TimeSeries(Widget): class TimeSeries(Widget):
def __init__(self, d): def __init__(self, d):
log.debug("")
super().__init__(d) super().__init__(d)
pass pass
@ -149,113 +152,84 @@ class TimeSeries(Widget):
x = n - math.log((t_last - t) / (n*dt) + 1) * n / k x = n - math.log((t_last - t) / (n*dt) + 1) * n / k
return x return x
data = self.lts.data def v2y(v):
n = len(data) if self.yscale == "log":
t_last = data[-1][0] return (1 - math.log(v / min_value)
if len(data) < 5: / math.log(max_value / min_value)
return "(not enough data)" ) * 200
dt = (t_last - data[-5][0]) / 4 elif self.yscale == "linear":
k = math.log((t_last - data[0][0]) / dt / n + 1) return (1 - v/max_value) * 200,
else:
raise ValueError(f"Unknown yscale {self.yscale}")
max_value = max([d[1] for d in self.lts.data]) def set_x_tickmarks():
max_value = max(max_value, 0.001) # ensure positive log.debug("")
v_data = [] tickmarks = []
for i in range(n): t = v_data[-1]["t"]
t = data[i][0] x = v_data[-1]["x"]
x = t2x(t) d = datetime.datetime.fromtimestamp(t)
t_h = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t)) tickmarks.append({"t": t, "t_h": d.strftime("%Y-%m-%d %H:%M:%S"), "x": x})
#print(t, t_h, x)
v_data.append(
{
"t": t,
"v": data[i][1],
"x": x,
"y": (1 - data[i][1]/max_value) * 200,
"color": self.criticalcolor(data[i][1]),
})
tickmarks = [] min_step = 25
t = v_data[-1]["t"] steps = ("s", "m", "h", "D", "10D", "M", "Y")
x = v_data[-1]["x"] step_i = 0
d = datetime.datetime.fromtimestamp(t) while True:
tickmarks.append({"t": t, "t_h": d.strftime("%Y-%m-%d %H:%M:%S"), "x": x}) t0 = tickmarks[-1]["t"]
x0 = tickmarks[-1]["x"]
d0 = datetime.datetime.fromtimestamp(t0)
min_step = 25 if steps[step_i] == "s":
steps = ("s", "m", "h", "D", "10D", "M", "Y") d1 = datetime.datetime(d0.year, d0.month, d0.day, d0.hour, d0.minute, d0.second)
step_i = 0
while True:
t0 = tickmarks[-1]["t"]
x0 = tickmarks[-1]["x"]
d0 = datetime.datetime.fromtimestamp(t0)
if steps[step_i] == "s":
d1 = datetime.datetime(d0.year, d0.month, d0.day, d0.hour, d0.minute, d0.second)
t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
d1 -= datetime.timedelta(seconds=1)
t1 = d1.timestamp() t1 = d1.timestamp()
x1 = t2x(t1) x1 = t2x(t1)
if x0 - x1 < min_step: if x0 - x1 < min_step:
step_i += 1 d1 -= datetime.timedelta(seconds=1)
continue t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
step_i += 1
continue
if steps[step_i] == "m": if steps[step_i] == "m":
d1 = datetime.datetime(d0.year, d0.month, d0.day, d0.hour, d0.minute) d1 = datetime.datetime(d0.year, d0.month, d0.day, d0.hour, d0.minute)
t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
d1 -= datetime.timedelta(minutes=1)
t1 = d1.timestamp() t1 = d1.timestamp()
x1 = t2x(t1) x1 = t2x(t1)
if x0 - x1 < min_step: if x0 - x1 < min_step:
step_i += 1 d1 -= datetime.timedelta(minutes=1)
continue t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
step_i += 1
continue
if steps[step_i] == "h": if steps[step_i] == "h":
d1 = datetime.datetime(d0.year, d0.month, d0.day, d0.hour) d1 = datetime.datetime(d0.year, d0.month, d0.day, d0.hour)
t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
d1 -= datetime.timedelta(hours=1)
t1 = d1.timestamp() t1 = d1.timestamp()
x1 = t2x(t1) x1 = t2x(t1)
if x0 - x1 < min_step: if x0 - x1 < min_step:
step_i += 1 d1 -= datetime.timedelta(hours=1)
continue t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
step_i += 1
continue
if steps[step_i] == "D": if steps[step_i] == "D":
d1 = datetime.datetime(d0.year, d0.month, d0.day) d1 = datetime.datetime(d0.year, d0.month, d0.day)
t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
d1 -= datetime.timedelta(days=1)
t1 = d1.timestamp() t1 = d1.timestamp()
x1 = t2x(t1) x1 = t2x(t1)
if x0 - x1 < min_step: if x0 - x1 < min_step:
step_i += 1 d1 -= datetime.timedelta(days=1)
continue t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
step_i += 1
continue
if steps[step_i] == "10D": if steps[step_i] == "10D":
year = d0.year year = d0.year
month = d0.month month = d0.month
day = d0.day day = d0.day
if day > 21:
day = 21
elif day > 11:
day = 11
elif day > 1:
day = 1
else:
day = 21
month -= 1
if month < 1:
month += 12
year -= 1
d1 = datetime.datetime(year, month, day)
t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
if day > 21: if day > 21:
day = 21 day = 21
elif day > 11: elif day > 11:
@ -272,52 +246,143 @@ class TimeSeries(Widget):
t1 = d1.timestamp() t1 = d1.timestamp()
x1 = t2x(t1) x1 = t2x(t1)
if x0 - x1 < min_step: if x0 - x1 < min_step:
step_i += 1 if day > 21:
continue day = 21
elif day > 11:
day = 11
elif day > 1:
day = 1
else:
day = 21
month -= 1
if month < 1:
month += 12
year -= 1
d1 = datetime.datetime(year, month, day)
t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
step_i += 1
continue
if steps[step_i] == "M": if steps[step_i] == "M":
d1 = datetime.datetime(d0.year, d0.month, 1) d1 = datetime.datetime(d0.year, d0.month, 1)
t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
if d1.month > 1:
d1 = datetime.datetime(d1.year, d1.month-1, 1)
else:
d1 = datetime.datetime(d1.year-1, 12, 1)
t1 = d1.timestamp() t1 = d1.timestamp()
x1 = t2x(t1) x1 = t2x(t1)
if x0 - x1 < min_step: if x0 - x1 < min_step:
step_i += 1 if d1.month > 1:
continue d1 = datetime.datetime(d1.year, d1.month-1, 1)
else:
d1 = datetime.datetime(d1.year-1, 12, 1)
t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
step_i += 1
continue
if steps[step_i] == "Y": if steps[step_i] == "Y":
d1 = datetime.datetime(d0.year, 1, 1) d1 = datetime.datetime(d0.year, 1, 1)
t1 = d1.timestamp()
x1 = t2x(t1)
if x0 - x1 < min_step:
d1 = datetime.datetime(d1.year-1, 1, 1)
t1 = d1.timestamp() t1 = d1.timestamp()
x1 = t2x(t1) x1 = t2x(t1)
if x0 - x1 < min_step: if x0 - x1 < min_step:
step_i += 1 d1 = datetime.datetime(d1.year-1, 1, 1)
continue t1 = d1.timestamp()
if x1 < 0: x1 = t2x(t1)
break if x0 - x1 < min_step:
tickmarks.append({"t": t1, "t_h": d1.strftime("%Y-%m-%d %H:%M:%S"), "x": x1}) step_i += 1
if tickmarks[-1]["x"] > min_step: continue
t = v_data[0]["t"] if x1 < 0:
x = v_data[0]["x"] break
d = datetime.datetime.fromtimestamp(t) tickmarks.append({"t": t1, "t_h": d1.strftime("%Y-%m-%d %H:%M:%S"), "x": x1})
tickmarks.append({"t": t, "t_h": d.strftime("%Y-%m-%d %H:%M:%S"), "x": x}) if tickmarks[-1]["x"] > min_step:
self.x_tickmarks = tickmarks t = v_data[0]["t"]
x = v_data[0]["x"]
d = datetime.datetime.fromtimestamp(t)
tickmarks.append({"t": t, "t_h": d.strftime("%Y-%m-%d %H:%M:%S"), "x": x})
self.x_tickmarks = tickmarks
log.debug("")
self.y_tickmarks = [] def set_y_tickmarks():
step = 10 ** math.floor(math.log10(max_value)) log.debug("")
v = 0 self.y_tickmarks = []
while v < max_value: if self.yscale == "linear":
self.y_tickmarks.append({"y": (1 - v/max_value) * 200, "v_h": str(v)}) log.debug("")
v += step step = 10 ** math.floor(math.log10(max_value))
v = 0
while v < max_value:
self.y_tickmarks.append({"y": v2y(v), "v_h": str(v)})
v += step
log.debug("")
elif self.yscale == "log":
log.debug("")
v = 10 ** math.ceil(math.log10(min_value))
log.debug("v = %s", v)
if v > max_value:
# Implement that when it happens
log.warning("No tickmark between %s and %s", min_value, max_value)
return
while v <= max_value:
y = v2y(v)
log.debug("v = %s, y = %s", v, y)
self.y_tickmarks.append({"y": y, "v_h": str(v)})
v *= 10
log.debug("")
else:
log.debug("")
raise ValueError(f"Unknown yscale {self.yscale}")
log.debug("")
log.debug("in graph")
data = self.lts.data
n = len(data)
t_last = data[-1][0]
if len(data) < 5:
return "(not enough data)"
dt = (t_last - data[-5][0]) / 4
k = math.log((t_last - data[0][0]) / dt / n + 1)
max_value = max([d[1] for d in self.lts.data])
max_value = max(max_value, 0.001) # ensure positive
if self.yscale == "log":
try:
min_value = min(d[1] for d in self.lts.data if d[1] > 0)
except ValueError:
# no non-negative values
min_value = max_value / 2
if any(True for d in self.lts.data if d[1] < min_value):
# if there are values smaller than the minimum (i.e.
# 0 or negative unless we have a configured minimum)
# reduce the minimum again so that we have a distinct
# value to clamp to.
min_value /= 2
if min_value == max_value:
# Make sure min_value is less than max_value
min_value /= 2
log.debug("min_value = %s, max_value = %s", min_value, max_value)
log.debug("collecting data")
v_data = []
for i in range(n):
t = data[i][0]
v = data[i][1]
x = t2x(t)
t_h = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t))
y = v2y(v)
#print(t, t_h, x)
v_data.append(
{
"t": t,
"v": v,
"x": x,
"y": y,
"color": self.criticalcolor(v),
})
log.debug("setting tickmarks")
set_x_tickmarks()
set_y_tickmarks()
log.debug("assembling svg")
html = "" html = ""
html += "<svg width=1150 height=300>" html += "<svg width=1150 height=300>"
for tm in self.x_tickmarks: for tm in self.x_tickmarks:
@ -329,9 +394,11 @@ class TimeSeries(Widget):
for v in v_data: for v in v_data:
html += f"<circle cx={v['x']} cy={v['y']} r=3 fill='{v['color']}' />" html += f"<circle cx={v['x']} cy={v['y']} r=3 fill='{v['color']}' />"
html += "</svg>" html += "</svg>"
log.debug("len(html) = %s", len(html))
return Markup(html) return Markup(html)
def as_html(self): def as_html(self):
log.debug("in as_html")
return Markup(render_template("timeseries.html", widget=self)) return Markup(render_template("timeseries.html", widget=self))
class Gauge(Widget): class Gauge(Widget):