Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signal handles and multi-axis support #9

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
7 changes: 7 additions & 0 deletions python/lognplot/chart/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def __init__(self, db):
self.x_axis = Axis()
self.y_axis = Axis()
self.curves = []
self.activeCurve = None
self.cursor = None
self.db = db

Expand All @@ -31,9 +32,15 @@ def add_curve(self, name, color):
if not self.has_curve(name):
curve = Curve(self.db, name, color)
self.curves.append(curve)
self.change_active_curve(curve)

def clear_curves(self):
self.curves.clear()
self.y_axis = Axis()

def change_active_curve(self, curve):
self.activeCurve = curve
self.y_axis = self.activeCurve.axis

def info(self):
print(f"Chart with {len(self.curves)} series")
Expand Down
8 changes: 7 additions & 1 deletion python/lognplot/chart/curve.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ..tsdb.aggregation import Aggregation

from .axis import Axis

class Curve:
""" A curve is a view onto a signal in the database.
Expand All @@ -14,6 +14,12 @@ def __init__(self, db, name, color):
self._db = db
self.name = name
self.color = color
# Average of the visual part of the curve
self.average = 0
# Corresponding handle (polygon area)
self.handle = []
# Each curve has its own vertical axis
self.axis = Axis()

def __repr__(self):
return "Database proxy-curve"
Expand Down
8 changes: 4 additions & 4 deletions python/lognplot/qt/render/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def calc_y_ticks(self, axis):
y_ticks = axis.get_ticks(amount_y_ticks)
return y_ticks

def draw_grid(self, x_ticks, y_ticks):
def draw_grid(self, y_axis, x_ticks, y_ticks):
""" Render a grid on the given x and y tick markers. """
pen = QtGui.QPen(Qt.gray)
pen.setWidth(1)
Expand All @@ -46,7 +46,7 @@ def draw_grid(self, x_ticks, y_ticks):
self.painter.drawLine(x, self.layout.chart_top, x, self.layout.chart_bottom)

for value, _ in y_ticks:
y = self.to_y_pixel(value)
y = self.to_y_pixel(y_axis, value)
self.painter.drawLine(self.layout.chart_left, y, self.layout.chart_right, y)

def draw_x_axis(self, x_ticks):
Expand All @@ -69,15 +69,15 @@ def draw_x_axis(self, x_ticks):
text_y = y + 10 - text_rect.y()
self.painter.drawText(text_x, text_y, label)

def draw_y_axis(self, y_ticks):
def draw_y_axis(self, y_axis, y_ticks):
""" Draw the Y-axis. """
pen = QtGui.QPen(Qt.black)
pen.setWidth(2)
self.painter.setPen(pen)
x = self.layout.chart_right + 5
self.painter.drawLine(x, self.layout.chart_top, x, self.layout.chart_bottom)
for value, label in y_ticks:
y = self.to_y_pixel(value)
y = self.to_y_pixel(y_axis, value)

# Tick handle:
self.painter.drawLine(x, y, x + 5, y)
Expand Down
68 changes: 51 additions & 17 deletions python/lognplot/qt/render/chart.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ..qtapi import QtGui, QtCore, Qt
from ...chart import Chart
from ...chart import Axis, Chart
from ...utils import bench_it
from ...tsdb import Aggregation
from .layout import ChartLayout
Expand All @@ -25,16 +25,21 @@ def render(self):
y_ticks = self.calc_y_ticks(self.chart.y_axis)

if self.options.show_grid:
self.draw_grid(x_ticks, y_ticks)
self.draw_grid(self.chart.y_axis, x_ticks, y_ticks)

self.draw_bouding_rect()

if self.options.show_axis:
self.draw_x_axis(x_ticks)
self.draw_y_axis(y_ticks)
self.draw_y_axis(self.chart.y_axis, y_ticks)

if self.options.show_handles:
self._draw_handles()

self._draw_curves()
self._draw_legend()

if self.options.show_legend:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This option certainly makes sense.

self._draw_legend()
self._draw_cursor()

def shade_region(self, region):
Expand Down Expand Up @@ -77,17 +82,17 @@ def _draw_curve(self, curve):

if data:
if isinstance(data[0], Aggregation):
self._draw_aggregations_as_shape(data, curve_color)
curve.average = self._draw_aggregations_as_shape(curve.axis, data, curve_color)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could query the average from by querying the summary of the data in view to the time series database (tsdb). This will give you a quicker result than re-calculating the average in the draw function.

Also: having the draw function doing two things (draw the plot and calculate the average) is probably not so clean, it would be better to seperate the two functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, still have to get familiar with the codebase.

else:
self._draw_samples_as_lines(data, curve_color)
curve.average = self._draw_samples_as_lines(curve.axis, data, curve_color)

def _draw_samples_as_lines(self, samples, curve_color: QtGui.QColor):
def _draw_samples_as_lines(self, y_axis: Axis, samples, curve_color: QtGui.QColor):
""" Draw raw samples as lines! """
pen = QtGui.QPen(curve_color)
pen.setWidth(2)
self.painter.setPen(pen)
points = [
QtCore.QPoint(self.to_x_pixel(x), self.to_y_pixel(y)) for (x, y) in samples
QtCore.QPoint(self.to_x_pixel(x), self.to_y_pixel(y_axis, y)) for (x, y) in samples
]
line = QtGui.QPolygon(points)
self.painter.drawPolyline(line)
Expand All @@ -97,8 +102,10 @@ def _draw_samples_as_lines(self, samples, curve_color: QtGui.QColor):
rect = QtCore.QRect(point.x() - 3, point.y() - 3, 6, 6)
self.painter.drawEllipse(rect)

return sum(p.y() for p in points) / len(points)

def _draw_aggregations_as_shape(
self, aggregations: Aggregation, curve_color: QtGui.QColor
self, y_axis: Axis, aggregations: Aggregation, curve_color: QtGui.QColor
):
""" Draw aggregates as polygon shapes.

Expand All @@ -119,30 +126,30 @@ def _draw_aggregations_as_shape(
# x2 = self.to_x_pixel(metric.x2)

# max line:
y_max = self.to_y_pixel(aggregation.metrics.maximum)
y_max = self.to_y_pixel(y_axis, aggregation.metrics.maximum)
max_points.append(QtCore.QPoint(x1, y_max))
# max_points.append(QtCore.QPoint(x2, y_max))

# min line:
y_min = self.to_y_pixel(aggregation.metrics.minimum)
y_min = self.to_y_pixel(y_axis, aggregation.metrics.minimum)
min_points.append(QtCore.QPoint(x1, y_min))
# min_points.append(QtCore.QPoint(x2, y_min))

mean = aggregation.metrics.mean
stddev = aggregation.metrics.stddev

# Mean line:
y_mean = self.to_y_pixel(mean)
y_mean = self.to_y_pixel(y_axis, mean)
mean_points.append(QtCore.QPoint(x1, y_mean))
# mean_points.append(QtCore.QPoint(x2, y_mean))

# stddev up line:
y_stddev_up = self.to_y_pixel(mean + stddev)
y_stddev_up = self.to_y_pixel(y_axis, mean + stddev)
stddev_up_points.append(QtCore.QPoint(x1, y_stddev_up))
# stddev_up_points.append(QtCore.QPoint(x2, y_stddev_up))

# stddev down line:
y_stddev_down = self.to_y_pixel(mean - stddev)
y_stddev_down = self.to_y_pixel(y_axis, mean - stddev)
stddev_down_points.append(QtCore.QPoint(x1, y_stddev_down))
# stddev_down_points.append(QtCore.QPoint(x2, y_stddev_down))

Expand Down Expand Up @@ -188,6 +195,8 @@ def _draw_aggregations_as_shape(
min_line = QtGui.QPolygon(min_points)
self.painter.drawPolyline(min_line)

return (sum(p.y() for p in mean_points) / len(mean_points))

def _draw_legend(self):
""" Draw names / color of the curve next to eachother.
"""
Expand Down Expand Up @@ -240,7 +249,7 @@ def _draw_cursor(self):
pen.setWidth(2)
self.painter.setPen(pen)
marker_x = self.to_x_pixel(curve_point_timestamp)
marker_y = self.to_y_pixel(curve_point_value)
marker_y = self.to_y_pixel(curve.axis, curve_point_value)
marker_size = 10
indicator_rect = QtCore.QRect(
marker_x - marker_size // 2,
Expand All @@ -267,11 +276,36 @@ def _draw_cursor(self):
color,
)

def _draw_handles(self):
x = self.layout.handles.left()

for _, curve in enumerate(self.chart.curves):
handle_y = curve.average
x_full = self.options.handle_width
x_half = x_full / 2
y_half = self.options.handle_height / 2

curve.handle = [
QtCore.QPointF(x, handle_y - y_half),
QtCore.QPointF(x, handle_y - y_half),
QtCore.QPointF(x + x_half, handle_y - y_half),
QtCore.QPointF(x + x_full, handle_y),
QtCore.QPointF(x + x_half, handle_y + y_half),
QtCore.QPointF(x, handle_y + y_half)
]

polygon = QtGui.QPainterPath(curve.handle[0])
for p in curve.handle[1:]:
polygon.lineTo(p)

color = QtGui.QColor(curve.color)
self.painter.fillPath(polygon, QtGui.QBrush(color))

def to_x_pixel(self, value):
return transform.to_x_pixel(value, self.chart.x_axis, self.layout)

def to_y_pixel(self, value):
return transform.to_y_pixel(value, self.chart.y_axis, self.layout)
def to_y_pixel(self, y_axis, value):
return transform.to_y_pixel(value, y_axis, self.layout)

def x_pixel_to_domain(self, pixel):
axis = self.chart.x_axis
Expand Down
12 changes: 11 additions & 1 deletion python/lognplot/qt/render/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ def __init__(self, rect: QtCore.QRect, options):
# print(rect, type(rect))
self.rect = rect

self.handles = QtCore.QRect(self.rect.left() + self.options.padding,
self.rect.top(),
self.options.handle_width,
self.rect.height())

# Endless sea of variables :)
self.do_layout()

def do_layout(self):
# self.right = self.rect.right()
# self.bottom = self.rect.bottom()
self.chart_top = self.rect.top() + self.options.padding
self.chart_left = self.rect.left() + self.options.padding

if self.options.show_handles:
self.chart_left = self.handles.right() + 3
else:
self.chart_left = self.rect.left() + self.options.padding

if self.options.show_axis:
axis_height = self.axis_height
axis_width = self.axis_width
Expand Down
5 changes: 5 additions & 0 deletions python/lognplot/qt/render/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ class ChartOptions:
def __init__(self):
self.show_axis = True
self.show_grid = True
self.show_legend = False
self.show_handles = True
self.autoscale_y_axis = False
self.padding = 10
self.handle_width = 20
self.handle_height = 15
12 changes: 10 additions & 2 deletions python/lognplot/qt/widgets/basewidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def mousePressEvent(self, event):
super().mousePressEvent(event)
self.disable_tailing()
self._mouse_drag_source = event.x(), event.y()
self.mousePress(event.x(), event.y())
self.update()

def mouseMoveEvent(self, event):
Expand All @@ -36,19 +37,26 @@ def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
self._update_mouse_pan(event.x(), event.y())
self._mouse_drag_source = None
self.mouseRelease(event.x(), event.y())

def _update_mouse_pan(self, x, y):
if self._mouse_drag_source:
x0, y0 = self._mouse_drag_source
if x != x0 or y != y0:
dy = y - y0
dx = x - x0
self.mouseDrag(x, y, dx, dy)
self.pan(dx, dy)
self._mouse_drag_source = (x, y)
self.update()

def mouse_move(self, x, y):
""" Intended for override. """
def mousePress(self, x, y):
pass

def mouseRelease(self, x, y):
pass

def mouseDrag(self, x, y, dx, dy):
pass

def pan(self, dx, dy):
Expand Down
32 changes: 30 additions & 2 deletions python/lognplot/qt/widgets/chartwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from ..qtapi import QtCore, QtWidgets, QtGui, Qt, pyqtSignal
from ...utils import bench_it
from ...chart import Chart
from ...chart import Chart, Curve
from ..render import render_chart_on_qpainter, ChartLayout, ChartOptions
from ..render import transform
from . import mime
Expand Down Expand Up @@ -78,11 +78,34 @@ def mouse_move(self, x, y):
self.chart.set_cursor(value)
self.update()

def curveHandleAtPoint(self, x, y) -> Curve:
for curve in self.chart.curves:
topleft = curve.handle[0]
middleright = curve.handle[3]
bottomleft = curve.handle[-1]
if (x >= topleft.x() and
x <= middleright.x() and
y >= topleft.y() and
y <= bottomleft.y()
):
return curve
return None

# Mouse interactions:
def mousePress(self, x, y):
curve = self.curveHandleAtPoint(x,y)
if curve is not None:
self._drag_handle = curve
self.chart.change_active_curve(curve)

def pan(self, dx, dy):
# print("pan", dx, dy)
shift = transform.x_pixels_to_domain(dx, self.chart.x_axis, self.chart_layout)
self.chart.horizontal_pan_absolute(-shift)
self.chart.autoscale_y()
if self.chart_options.autoscale_y_axis:
self.chart.autoscale_y()
else:
self._drag_handle.axis.pan_relative(dy / self.rect().height())
self.update()

def add_curve(self, name, color=None):
Expand All @@ -109,16 +132,19 @@ def horizontal_zoom(self, amount, around):
self.chart.horizontal_zoom(amount, around)
# Autoscale Y for a nice effect?
self.chart.autoscale_y()
self.repaint()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this function do? update also triggers a paint action eventually. Does this result in snappier look / feel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handles on the left hand side otherwise lagged behind in the repainting. Not sure about the reason though. Maybe that 'update' only invalidated the chart area itself?

self.update()

def vertical_zoom(self, amount):
self.chart.vertical_zoom(amount)
self.repaint()
self.update()

def horizontal_pan(self, amount):
self.chart.horizontal_pan_relative(amount)
# Autoscale Y for a nice effect?
self.chart.autoscale_y()
self.repaint()
self.update()

def vertical_pan(self, amount):
Expand All @@ -128,12 +154,14 @@ def vertical_pan(self, amount):
def zoom_fit(self):
""" Autoscale all in fit! """
self.chart.zoom_fit()
self.repaint()
self.update()

def zoom_to_last(self, span):
""" Zoom to fit the last x time in view.
"""
self.chart.zoom_to_last(span)
self.repaint()
self.update()

def enable_tailing(self, timespan):
Expand Down