From 63f9d2fac9ccb44a14c5b6cb522c0d26de59d4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Fri, 30 Dec 2022 21:59:50 +0100 Subject: [PATCH 01/20] Change sparkline to use cyclic buffer Since we specify the maximum ammount of data points, we can use cyclic buffer underneath, therefore avoiding memory fragmentation. This should also help for problems decribed in https://github.com/adafruit/Adafruit_CircuitPython_Display_Shapes/issues/25 --- adafruit_display_shapes/sparkline.py | 78 +++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/adafruit_display_shapes/sparkline.py b/adafruit_display_shapes/sparkline.py index 0ca9661..f1e2ee8 100644 --- a/adafruit_display_shapes/sparkline.py +++ b/adafruit_display_shapes/sparkline.py @@ -25,7 +25,7 @@ Various common shapes for use with displayio - Sparkline! -* Author(s): Kevin Matocha +* Author(s): Kevin Matocha, Maciej Sokołowski Implementation Notes -------------------- @@ -47,6 +47,56 @@ from adafruit_display_shapes.line import Line +class _CyclicBuffer(): + def __init__(self, size: int) -> None: + self._buffer = [None] * size + self._start = 0 # between 0 and size-1 + self._end = 0 # between 0 and 2*size-1 + + def push(self, value: float) -> None: + if self.len() == len(self._buffer): + raise RuntimeError("Trying to push to full buffer") + self._buffer[self._end % len(self._buffer)] = value + self._end += 1 + + def pop(self) -> float: + if self.len() == 0: + raise RuntimeError("Trying to pop from empty buffer") + result = self.first() + self._start += 1 + if self._start == len(self._buffer): + self._start -= len(self._buffer) + self._end -= len(self._buffer) + return result + + def first(self) -> float: + if self.len() == 0: + return None + return self._buffer[self._start] + + def last(self) -> float: + if self.len() == 0: + return None + return self._buffer[(self._end - 1) % len(self._buffer)] + + def len(self) -> int: + return self._end - self._start + + def clear(self) -> None: + self._start = 0 + self._end = 0 + + def values(self) -> List[float]: + if self.len() == 0: + return [] + start = self._start + end = self._end % len(self._buffer) + if start < end: + return self._buffer[start:end] + else: + return self._buffer[start:] + self._buffer[:end] + + class Sparkline(displayio.Group): # pylint: disable=too-many-arguments """A sparkline graph. @@ -85,7 +135,7 @@ def __init__( self.height = height # in pixels self.color = color # self._max_items = max_items # maximum number of items in the list - self._spark_list = [] # list containing the values + self._buffer = _CyclicBuffer(self._max_items) self.dyn_xpitch = dyn_xpitch if not dyn_xpitch: self._xpitch = (width - 1) / (self._max_items - 1) @@ -103,11 +153,11 @@ def __init__( super().__init__(x=x, y=y) # self is a group of lines def clear_values(self) -> None: - """Removes all values from the _spark_list list and removes all lines in the group""" + """Clears _buffer and removes all lines in the group""" for _ in range(len(self)): # remove all items from the current group self.pop() - self._spark_list = [] # empty the list + self._buffer.clear() self._redraw = True def add_value(self, value: float, update: bool = True) -> None: @@ -123,16 +173,16 @@ def add_value(self, value: float, update: bool = True) -> None: if value is not None: if ( - len(self._spark_list) >= self._max_items + self._buffer.len() >= self._max_items ): # if list is full, remove the first item - first = self._spark_list.pop(0) + first = self._buffer.pop() # check if boundaries have to be updated if self.y_min is None and first == self.y_bottom: - self.y_bottom = min(self._spark_list) + self.y_bottom = min(self._buffer.values()) if self.y_max is None and first == self.y_top: - self.y_top = max(self._spark_list) + self.y_top = max(self._buffer.values()) self._redraw = True - self._spark_list.append(value) + self._buffer.push(value) if self.y_min is None: self._redraw = self._redraw or value < self.y_bottom @@ -194,9 +244,9 @@ def update(self) -> None: """Update the drawing of the sparkline.""" # bail out early if we only have a single point - n_points = len(self._spark_list) + n_points = self._buffer.len() if n_points < 2: - self._last = [0, self._spark_list[0]] + self._last = [0, self._buffer.first()] return if self.dyn_xpitch: @@ -213,7 +263,7 @@ def update(self) -> None: y_m1 = self._last[1] # end of new line (new point, read as "x(0)") x_0 = int(x_m1 + xpitch) - y_0 = self._spark_list[-1] + y_0 = self._buffer.last() self._plotline(x_m1, y_m1, x_0, y_0) return @@ -221,7 +271,7 @@ def update(self) -> None: for _ in range(len(self)): # remove all items from the current group self.pop() - for count, value in enumerate(self._spark_list): + for count, value in enumerate(self._buffer.values()): if count == 0: pass # don't draw anything for a first point else: @@ -284,4 +334,4 @@ def update(self) -> None: def values(self) -> List[float]: """Returns the values displayed on the sparkline.""" - return self._spark_list + return self._buffer.values() From 2f709248b952a145ec09c8e4c0090e3503c0e17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Fri, 30 Dec 2022 23:09:45 +0100 Subject: [PATCH 02/20] Add option to draw polylines (not closed polygons) --- adafruit_display_shapes/polygon.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index 757ec6e..472dc87 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -9,7 +9,7 @@ Various common shapes for use with displayio - Polygon shape! -* Author(s): Melissa LeBlanc-Williams +* Author(s): Melissa LeBlanc-Williams, Maciej Sokołowski Implementation Notes -------------------- @@ -39,6 +39,7 @@ class Polygon(displayio.TileGrid): :param list points: A list of (x, y) tuples of the points :param int|None outline: The outline of the polygon. Can be a hex value for a color or ``None`` for no outline. + :param bool close: Wether to connect first and last point. """ def __init__( @@ -46,7 +47,11 @@ def __init__( points: List[Tuple[int, int]], *, outline: Optional[int] = None, + close: bool = True, ) -> None: + if close: + points.append(points[0]) + xs = [] ys = [] @@ -68,12 +73,9 @@ def __init__( if outline is not None: # print("outline") self.outline = outline - for index, _ in enumerate(points): + for index, _ in enumerate(points[:-1]): point_a = points[index] - if index == len(points) - 1: - point_b = points[0] - else: - point_b = points[index + 1] + point_b = points[index + 1] self._line( point_a[0] - x_offset, point_a[1] - y_offset, From c5774ef7eaef69294a7e2d6dcd52fa3bc113c854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Fri, 30 Dec 2022 23:11:31 +0100 Subject: [PATCH 03/20] Remove myself from authors Apparently contributors are not listed there --- adafruit_display_shapes/polygon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index 472dc87..8d25549 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -9,7 +9,7 @@ Various common shapes for use with displayio - Polygon shape! -* Author(s): Melissa LeBlanc-Williams, Maciej Sokołowski +* Author(s): Melissa LeBlanc-Williams Implementation Notes -------------------- From c701027fd2bc2860fa49bdb32e93497a3b36ae35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Sat, 31 Dec 2022 00:23:55 +0100 Subject: [PATCH 04/20] Update polygon.py Update docs --- adafruit_display_shapes/polygon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index 8d25549..423c13d 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -39,7 +39,7 @@ class Polygon(displayio.TileGrid): :param list points: A list of (x, y) tuples of the points :param int|None outline: The outline of the polygon. Can be a hex value for a color or ``None`` for no outline. - :param bool close: Wether to connect first and last point. + :param bool close: (Optional) Wether to connect first and last point. (True) """ def __init__( @@ -47,7 +47,7 @@ def __init__( points: List[Tuple[int, int]], *, outline: Optional[int] = None, - close: bool = True, + close: Optional[bool] = True, ) -> None: if close: points.append(points[0]) From b99718b3c1f338063e63a44a3bd41301fd354cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Sat, 31 Dec 2022 00:27:10 +0100 Subject: [PATCH 05/20] Change Sparkline to use polygon Previous implementation used multiple Line elements, causing unnecessary memory overhead --- adafruit_display_shapes/sparkline.py | 110 +++++++++------------------ 1 file changed, 34 insertions(+), 76 deletions(-) diff --git a/adafruit_display_shapes/sparkline.py b/adafruit_display_shapes/sparkline.py index f1e2ee8..3a7a37a 100644 --- a/adafruit_display_shapes/sparkline.py +++ b/adafruit_display_shapes/sparkline.py @@ -25,7 +25,7 @@ Various common shapes for use with displayio - Sparkline! -* Author(s): Kevin Matocha, Maciej Sokołowski +* Author(s): Kevin Matocha Implementation Notes -------------------- @@ -44,7 +44,7 @@ except ImportError: pass import displayio -from adafruit_display_shapes.line import Line +from adafruit_display_shapes.polygon import Polygon class _CyclicBuffer(): @@ -62,23 +62,13 @@ def push(self, value: float) -> None: def pop(self) -> float: if self.len() == 0: raise RuntimeError("Trying to pop from empty buffer") - result = self.first() + result = self._buffer[self._start] self._start += 1 if self._start == len(self._buffer): self._start -= len(self._buffer) self._end -= len(self._buffer) return result - def first(self) -> float: - if self.len() == 0: - return None - return self._buffer[self._start] - - def last(self) -> float: - if self.len() == 0: - return None - return self._buffer[(self._end - 1) % len(self._buffer)] - def len(self) -> int: return self._end - self._start @@ -147,10 +137,10 @@ def __init__( self.y_top = y_max # y_top: The actual minimum value of the vertical scale, will be # updated if autorange - self._redraw = True # _redraw: redraw primitives - self._last = [] # _last: last point of sparkline + self._points = [] # _points: all points of sparkline - super().__init__(x=x, y=y) # self is a group of lines + super().__init__(x=x, y=y) # self is a group of single Polygon + # (TODO: it has one element, maybe group is no longer needed?) def clear_values(self) -> None: """Clears _buffer and removes all lines in the group""" @@ -158,7 +148,6 @@ def clear_values(self) -> None: for _ in range(len(self)): # remove all items from the current group self.pop() self._buffer.clear() - self._redraw = True def add_value(self, value: float, update: bool = True) -> None: """Add a value to the sparkline. @@ -181,16 +170,13 @@ def add_value(self, value: float, update: bool = True) -> None: self.y_bottom = min(self._buffer.values()) if self.y_max is None and first == self.y_top: self.y_top = max(self._buffer.values()) - self._redraw = True self._buffer.push(value) if self.y_min is None: - self._redraw = self._redraw or value < self.y_bottom self.y_bottom = ( value if not self.y_bottom else min(value, self.y_bottom) ) if self.y_max is None: - self._redraw = self._redraw or value > self.y_top self.y_top = value if not self.y_top else max(value, self.y_top) if update: @@ -218,25 +204,23 @@ def _xintercept( ) / slope # calculate the x-intercept at position y=horizontalY return int(xint) - def _plotline( + def _add_point( self, - x_1: int, - last_value: float, - x_2: int, + x: int, value: float, ) -> None: # Guard for y_top and y_bottom being the same if self.y_top == self.y_bottom: - y_2 = int(0.5 * self.height) - y_1 = int(0.5 * self.height) + y = int(0.5 * self.height) else: - y_2 = int(self.height * (self.y_top - value) / (self.y_top - self.y_bottom)) - y_1 = int( - self.height * (self.y_top - last_value) / (self.y_top - self.y_bottom) - ) - self.append(Line(x_1, y_1, x_2, y_2, self.color)) # plot the line - self._last = [x_2, value] + y = int(self.height * (self.y_top - value) / (self.y_top - self.y_bottom)) + self._points.append((x, y)) + + def _draw(self) -> None: + while(len(self)): + self.pop() + self.append(Polygon(self._points, outline=self.color, close=False)) # plot the polyline # pylint: disable= too-many-branches, too-many-nested-blocks, too-many-locals, too-many-statements @@ -246,42 +230,27 @@ def update(self) -> None: # bail out early if we only have a single point n_points = self._buffer.len() if n_points < 2: - self._last = [0, self._buffer.first()] return if self.dyn_xpitch: # this is a float, only make int when plotting the line xpitch = (self.width - 1) / (n_points - 1) - self._redraw = True else: xpitch = self._xpitch - # only add new segment if redrawing is not necessary - if not self._redraw: - # end of last line (last point, read as "x(-1)") - x_m1 = self._last[0] - y_m1 = self._last[1] - # end of new line (new point, read as "x(0)") - x_0 = int(x_m1 + xpitch) - y_0 = self._buffer.last() - self._plotline(x_m1, y_m1, x_0, y_0) - return - - self._redraw = False # reset, since we now redraw everything - for _ in range(len(self)): # remove all items from the current group - self.pop() + self._points = [] # remove all points for count, value in enumerate(self._buffer.values()): if count == 0: - pass # don't draw anything for a first point + self._add_point(0, value) else: - x_2 = int(xpitch * count) - x_1 = int(xpitch * (count - 1)) + x = int(xpitch * count) + last_x = int(xpitch * (count - 1)) if (self.y_bottom <= last_value <= self.y_top) and ( self.y_bottom <= value <= self.y_top ): # both points are in range, plot the line - self._plotline(x_1, last_value, x_2, value) + self._add_point(x, value) else: # at least one point is out of range, clip one or both ends the line if ((last_value > self.y_top) and (value > self.y_top)) or ( @@ -291,10 +260,10 @@ def update(self) -> None: pass else: xint_bottom = self._xintercept( - x_1, last_value, x_2, value, self.y_bottom + last_x, last_value, x, value, self.y_bottom ) # get possible new x intercept points xint_top = self._xintercept( - x_1, last_value, x_2, value, self.y_top + last_x, last_value, x, value, self.y_top ) # on the top and bottom of range if (xint_bottom is None) or ( xint_top is None @@ -302,34 +271,23 @@ def update(self) -> None: pass else: # Initialize the adjusted values as the baseline - adj_x_1 = x_1 - adj_last_value = last_value - adj_x_2 = x_2 + adj_x = x adj_value = value if value > last_value: # slope is positive - if xint_bottom >= x_1: # bottom is clipped - adj_x_1 = xint_bottom - adj_last_value = self.y_bottom # y_1 - if xint_top <= x_2: # top is clipped - adj_x_2 = xint_top - adj_value = self.y_top # y_2 + if xint_top <= x: # top is clipped + adj_x = xint_top + adj_value = self.y_top # y else: # slope is negative - if xint_top >= x_1: # top is clipped - adj_x_1 = xint_top - adj_last_value = self.y_top # y_1 - if xint_bottom <= x_2: # bottom is clipped - adj_x_2 = xint_bottom - adj_value = self.y_bottom # y_2 - - self._plotline( - adj_x_1, - adj_last_value, - adj_x_2, - adj_value, - ) + if xint_bottom <= x: # bottom is clipped + adj_x = xint_bottom + adj_value = self.y_bottom # y + + self._add_point(adj_x, adj_value) last_value = value # store value for the next iteration + + self._draw() def values(self) -> List[float]: """Returns the values displayed on the sparkline.""" From 81c1ceeeea55b10e77c02428090edca05fc2e917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Sat, 31 Dec 2022 00:47:43 +0100 Subject: [PATCH 06/20] Add docstrings to new methods --- adafruit_display_shapes/sparkline.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/adafruit_display_shapes/sparkline.py b/adafruit_display_shapes/sparkline.py index 3a7a37a..5560d2e 100644 --- a/adafruit_display_shapes/sparkline.py +++ b/adafruit_display_shapes/sparkline.py @@ -54,12 +54,20 @@ def __init__(self, size: int) -> None: self._end = 0 # between 0 and 2*size-1 def push(self, value: float) -> None: + """Pushes value at the end of the buffer. + + :param float value: value to be pushed + + """ + if self.len() == len(self._buffer): raise RuntimeError("Trying to push to full buffer") self._buffer[self._end % len(self._buffer)] = value self._end += 1 def pop(self) -> float: + """Pop value from the start of the buffer and returns it.""" + if self.len() == 0: raise RuntimeError("Trying to pop from empty buffer") result = self._buffer[self._start] @@ -70,21 +78,26 @@ def pop(self) -> float: return result def len(self) -> int: + """Returns count of valid data in the buffer.""" + return self._end - self._start def clear(self) -> None: + """Marks all data as invalid.""" + self._start = 0 self._end = 0 def values(self) -> List[float]: + """Returns valid data from the buffer.""" + if self.len() == 0: return [] start = self._start end = self._end % len(self._buffer) if start < end: return self._buffer[start:end] - else: - return self._buffer[start:] + self._buffer[:end] + return self._buffer[start:] + self._buffer[:end] class Sparkline(displayio.Group): From f7742d2cb96e34507115ad1dbadee06555b32cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Sat, 31 Dec 2022 00:56:20 +0100 Subject: [PATCH 07/20] Update sparkline.py Formatting fixes --- adafruit_display_shapes/sparkline.py | 38 +++++++++++++++------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/adafruit_display_shapes/sparkline.py b/adafruit_display_shapes/sparkline.py index 5560d2e..87a98c2 100644 --- a/adafruit_display_shapes/sparkline.py +++ b/adafruit_display_shapes/sparkline.py @@ -47,27 +47,27 @@ from adafruit_display_shapes.polygon import Polygon -class _CyclicBuffer(): +class _CyclicBuffer: def __init__(self, size: int) -> None: self._buffer = [None] * size - self._start = 0 # between 0 and size-1 - self._end = 0 # between 0 and 2*size-1 - + self._start = 0 # between 0 and size-1 + self._end = 0 # between 0 and 2*size-1 + def push(self, value: float) -> None: """Pushes value at the end of the buffer. :param float value: value to be pushed """ - + if self.len() == len(self._buffer): raise RuntimeError("Trying to push to full buffer") self._buffer[self._end % len(self._buffer)] = value self._end += 1 - + def pop(self) -> float: """Pop value from the start of the buffer and returns it.""" - + if self.len() == 0: raise RuntimeError("Trying to pop from empty buffer") result = self._buffer[self._start] @@ -76,21 +76,21 @@ def pop(self) -> float: self._start -= len(self._buffer) self._end -= len(self._buffer) return result - + def len(self) -> int: """Returns count of valid data in the buffer.""" - + return self._end - self._start - + def clear(self) -> None: """Marks all data as invalid.""" - + self._start = 0 self._end = 0 - + def values(self) -> List[float]: """Returns valid data from the buffer.""" - + if self.len() == 0: return [] start = self._start @@ -153,7 +153,7 @@ def __init__( self._points = [] # _points: all points of sparkline super().__init__(x=x, y=y) # self is a group of single Polygon - # (TODO: it has one element, maybe group is no longer needed?) + # (TODO: it has one element, maybe group is no longer needed?) def clear_values(self) -> None: """Clears _buffer and removes all lines in the group""" @@ -229,11 +229,13 @@ def _add_point( else: y = int(self.height * (self.y_top - value) / (self.y_top - self.y_bottom)) self._points.append((x, y)) - + def _draw(self) -> None: - while(len(self)): + while len(self): self.pop() - self.append(Polygon(self._points, outline=self.color, close=False)) # plot the polyline + self.append( + Polygon(self._points, outline=self.color, close=False) + ) # plot the polyline # pylint: disable= too-many-branches, too-many-nested-blocks, too-many-locals, too-many-statements @@ -299,7 +301,7 @@ def update(self) -> None: self._add_point(adj_x, adj_value) last_value = value # store value for the next iteration - + self._draw() def values(self) -> List[float]: From 3bee68031435ab6130688ecb6526f2ac2f04df7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Mon, 2 Jan 2023 17:41:30 +0100 Subject: [PATCH 08/20] Remove unused color from palette --- adafruit_display_shapes/polygon.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index 423c13d..79d42a4 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -31,6 +31,7 @@ __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Shapes.git" +_OUTLINE_COLOR_INDEX = 1 class Polygon(displayio.TileGrid): # pylint: disable=too-many-arguments,invalid-name @@ -66,9 +67,9 @@ def __init__( width = max(xs) - min(xs) + 1 height = max(ys) - min(ys) + 1 - self._palette = displayio.Palette(3) + self._palette = displayio.Palette(2) self._palette.make_transparent(0) - self._bitmap = displayio.Bitmap(width, height, 3) + self._bitmap = displayio.Bitmap(width, height, 2) if outline is not None: # print("outline") @@ -81,7 +82,6 @@ def __init__( point_a[1] - y_offset, point_b[0] - x_offset, point_b[1] - y_offset, - 1, ) super().__init__( @@ -95,18 +95,17 @@ def _line( y0: int, x1: int, y1: int, - color: int, ) -> None: if x0 == x1: if y0 > y1: y0, y1 = y1, y0 for _h in range(y0, y1 + 1): - self._bitmap[x0, _h] = color + self._bitmap[x0, _h] = _OUTLINE_COLOR_INDEX elif y0 == y1: if x0 > x1: x0, x1 = x1, x0 for _w in range(x0, x1 + 1): - self._bitmap[_w, y0] = color + self._bitmap[_w, y0] = _OUTLINE_COLOR_INDEX else: steep = abs(y1 - y0) > abs(x1 - x0) if steep: @@ -129,9 +128,9 @@ def _line( for x in range(x0, x1 + 1): if steep: - self._bitmap[y0, x] = color + self._bitmap[y0, x] = _OUTLINE_COLOR_INDEX else: - self._bitmap[x, y0] = color + self._bitmap[x, y0] = _OUTLINE_COLOR_INDEX err -= dy if err < 0: y0 += ystep @@ -143,13 +142,13 @@ def _line( def outline(self) -> Optional[int]: """The outline of the polygon. Can be a hex value for a color or ``None`` for no outline.""" - return self._palette[1] + return self._palette[_OUTLINE_COLOR_INDEX] @outline.setter def outline(self, color: Optional[int]) -> None: if color is None: - self._palette[1] = 0 - self._palette.make_transparent(1) + self._palette[_OUTLINE_COLOR_INDEX] = 0 + self._palette.make_transparent(_OUTLINE_COLOR_INDEX) else: - self._palette[1] = color - self._palette.make_opaque(1) + self._palette[_OUTLINE_COLOR_INDEX] = color + self._palette.make_opaque(_OUTLINE_COLOR_INDEX) From 8393bcd34e4cf8a28c43f01a92e708054df005e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Mon, 2 Jan 2023 18:13:44 +0100 Subject: [PATCH 09/20] Fix CI errors Triangle is a subclass of Polygon, so we need to provide support for more than one color in palette --- adafruit_display_shapes/polygon.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index 79d42a4..2c862b3 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -31,7 +31,6 @@ __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Shapes.git" -_OUTLINE_COLOR_INDEX = 1 class Polygon(displayio.TileGrid): # pylint: disable=too-many-arguments,invalid-name @@ -41,14 +40,21 @@ class Polygon(displayio.TileGrid): :param int|None outline: The outline of the polygon. Can be a hex value for a color or ``None`` for no outline. :param bool close: (Optional) Wether to connect first and last point. (True) + :param int colors: (Optional) Number of colors to use. Most polygons would use two, one for + outline and one for fill. If you're not filling your polygon, set this to 1 + for smaller memory footprint. (2) """ + _OUTLINE = 1 + _FILL = 2 + def __init__( self, points: List[Tuple[int, int]], *, outline: Optional[int] = None, close: Optional[bool] = True, + colors: Optional[int] = 2, ) -> None: if close: points.append(points[0]) @@ -67,7 +73,7 @@ def __init__( width = max(xs) - min(xs) + 1 height = max(ys) - min(ys) + 1 - self._palette = displayio.Palette(2) + self._palette = displayio.Palette(colors + 1) self._palette.make_transparent(0) self._bitmap = displayio.Bitmap(width, height, 2) @@ -82,6 +88,7 @@ def __init__( point_a[1] - y_offset, point_b[0] - x_offset, point_b[1] - y_offset, + self._OUTLINE, ) super().__init__( @@ -95,17 +102,18 @@ def _line( y0: int, x1: int, y1: int, + color: int, ) -> None: if x0 == x1: if y0 > y1: y0, y1 = y1, y0 for _h in range(y0, y1 + 1): - self._bitmap[x0, _h] = _OUTLINE_COLOR_INDEX + self._bitmap[x0, _h] = color elif y0 == y1: if x0 > x1: x0, x1 = x1, x0 for _w in range(x0, x1 + 1): - self._bitmap[_w, y0] = _OUTLINE_COLOR_INDEX + self._bitmap[_w, y0] = color else: steep = abs(y1 - y0) > abs(x1 - x0) if steep: @@ -128,9 +136,9 @@ def _line( for x in range(x0, x1 + 1): if steep: - self._bitmap[y0, x] = _OUTLINE_COLOR_INDEX + self._bitmap[y0, x] = color else: - self._bitmap[x, y0] = _OUTLINE_COLOR_INDEX + self._bitmap[x, y0] = color err -= dy if err < 0: y0 += ystep @@ -142,13 +150,13 @@ def _line( def outline(self) -> Optional[int]: """The outline of the polygon. Can be a hex value for a color or ``None`` for no outline.""" - return self._palette[_OUTLINE_COLOR_INDEX] + return self._palette[self._OUTLINE] @outline.setter def outline(self, color: Optional[int]) -> None: if color is None: - self._palette[_OUTLINE_COLOR_INDEX] = 0 - self._palette.make_transparent(_OUTLINE_COLOR_INDEX) + self._palette[self._OUTLINE] = 0 + self._palette.make_transparent(self._OUTLINE) else: - self._palette[_OUTLINE_COLOR_INDEX] = color - self._palette.make_opaque(_OUTLINE_COLOR_INDEX) + self._palette[self._OUTLINE] = color + self._palette.make_opaque(self._OUTLINE) From 3be187b59e37f298a569506d334767c6a21c7d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Mon, 2 Jan 2023 18:16:12 +0100 Subject: [PATCH 10/20] Fix bug spotted in self-review --- adafruit_display_shapes/polygon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index 2c862b3..70e1b70 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -75,7 +75,7 @@ def __init__( self._palette = displayio.Palette(colors + 1) self._palette.make_transparent(0) - self._bitmap = displayio.Bitmap(width, height, 2) + self._bitmap = displayio.Bitmap(width, height, colors + 1) if outline is not None: # print("outline") From a1a80d8af51c71c91a42daa6839155cc47c3da92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Mon, 2 Jan 2023 18:19:35 +0100 Subject: [PATCH 11/20] Use color index constants in triangle --- adafruit_display_shapes/triangle.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/adafruit_display_shapes/triangle.py b/adafruit_display_shapes/triangle.py index 6d88464..27579a8 100644 --- a/adafruit_display_shapes/triangle.py +++ b/adafruit_display_shapes/triangle.py @@ -101,7 +101,7 @@ def __init__( point_a[1] - y0, point_b[0] - min(xs), point_b[1] - y0, - 1, + self._OUTLINE, ) # pylint: disable=invalid-name, too-many-branches @@ -126,7 +126,7 @@ def _draw_filled( a = x2 elif x2 > b: b = x2 - self._line(a, y0, b, y0, 2) + self._line(a, y0, b, y0, self._FILL) return if y1 == y2: @@ -140,7 +140,7 @@ def _draw_filled( b = round(x0 + (x2 - x0) * (y - y0) / (y2 - y0)) if a > b: a, b = b, a - self._line(a, y, b, y, 2) + self._line(a, y, b, y, self._FILL) # Lower Triangle for y in range(last + 1, y2 + 1): a = round(x1 + (x2 - x1) * (y - y1) / (y2 - y1)) @@ -148,7 +148,7 @@ def _draw_filled( if a > b: a, b = b, a - self._line(a, y, b, y, 2) + self._line(a, y, b, y, self._FILL) # pylint: enable=invalid-name, too-many-locals, too-many-branches @@ -156,13 +156,13 @@ def _draw_filled( def fill(self) -> Optional[int]: """The fill of the triangle. Can be a hex value for a color or ``None`` for transparent.""" - return self._palette[2] + return self._palette[self._FILL] @fill.setter def fill(self, color: Optional[int]) -> None: if color is None: - self._palette[2] = 0 - self._palette.make_transparent(2) + self._palette[self._FILL] = 0 + self._palette.make_transparent(self._FILL) else: - self._palette[2] = color - self._palette.make_opaque(2) + self._palette[self._FILL] = color + self._palette.make_opaque(self._FILL) From 2e287506ce3a529759f77ecf3cda183aa42fcbcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Mon, 2 Jan 2023 21:03:18 +0100 Subject: [PATCH 12/20] Refactor polygon.py Decouple polygon drawing from creating bitmap, so the code can be reused for drawing on external bitmaps --- adafruit_display_shapes/polygon.py | 68 ++++++++++++++++++------------ 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index 70e1b70..b9d748d 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -33,7 +33,6 @@ class Polygon(displayio.TileGrid): - # pylint: disable=too-many-arguments,invalid-name """A polygon. :param list points: A list of (x, y) tuples of the points @@ -56,15 +55,7 @@ def __init__( close: Optional[bool] = True, colors: Optional[int] = 2, ) -> None: - if close: - points.append(points[0]) - - xs = [] - ys = [] - - for point in points: - xs.append(point[0]) - ys.append(point[1]) + (xs, ys) = zip(*points) x_offset = min(xs) y_offset = min(ys) @@ -77,25 +68,37 @@ def __init__( self._palette.make_transparent(0) self._bitmap = displayio.Bitmap(width, height, colors + 1) + shifted = [(x - x_offset, y - y_offset) for (x, y) in points] + if outline is not None: - # print("outline") self.outline = outline - for index, _ in enumerate(points[:-1]): - point_a = points[index] - point_b = points[index + 1] - self._line( - point_a[0] - x_offset, - point_a[1] - y_offset, - point_b[0] - x_offset, - point_b[1] - y_offset, - self._OUTLINE, - ) + self.draw(self._bitmap, shifted, self._OUTLINE, close) super().__init__( self._bitmap, pixel_shader=self._palette, x=x_offset, y=y_offset ) - # pylint: disable=invalid-name, too-many-locals, too-many-branches + @staticmethod + def draw( + bitmap: displayio.Bitmap, + points: List[Tuple[int, int]], + color_id: int, + close: Optional[bool] = True, + ) -> None: + """Draw a polygon conecting points on provided bitmap with provided color_id + + :param displayio.Bitmap bitmap: bitmap to draw on + :param list points: A list of (x, y) tuples of the points + :param int color_id: Color to draw with + :param bool close: (Optional) Wether to connect first and last point. (True) + """ + + if close: + points.append(points[0]) + + for index in range(len(points) - 1): + Polygon._line_on(bitmap, points[index], points[index + 1], color_id) + def _line( self, x0: int, @@ -104,16 +107,27 @@ def _line( y1: int, color: int, ) -> None: + self._line_on(self._bitmap, (x0, y0), (x1, y1), color) + + @staticmethod + def _line_on( + bitmap: displayio.Bitmap, + p0: Tuple[int, int], + p1: Tuple[int, int], + color: int, + ) -> None: + (x0, y0) = p0 + (x1, y1) = p1 if x0 == x1: if y0 > y1: y0, y1 = y1, y0 for _h in range(y0, y1 + 1): - self._bitmap[x0, _h] = color + bitmap[x0, _h] = color elif y0 == y1: if x0 > x1: x0, x1 = x1, x0 for _w in range(x0, x1 + 1): - self._bitmap[_w, y0] = color + bitmap[_w, y0] = color else: steep = abs(y1 - y0) > abs(x1 - x0) if steep: @@ -136,16 +150,14 @@ def _line( for x in range(x0, x1 + 1): if steep: - self._bitmap[y0, x] = color + bitmap[y0, x] = color else: - self._bitmap[x, y0] = color + bitmap[x, y0] = color err -= dy if err < 0: y0 += ystep err += dx - # pylint: enable=invalid-name, too-many-locals, too-many-branches - @property def outline(self) -> Optional[int]: """The outline of the polygon. Can be a hex value for a color or From 8fda975768d78214b6bf24d1c89ea63ea8fcb14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Mon, 2 Jan 2023 21:25:54 +0100 Subject: [PATCH 13/20] Pylint fixes Fix some of the old warnings, reenable some of overrides --- adafruit_display_shapes/polygon.py | 86 ++++++++++++++++-------------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index b9d748d..6f76faa 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -55,14 +55,14 @@ def __init__( close: Optional[bool] = True, colors: Optional[int] = 2, ) -> None: - (xs, ys) = zip(*points) + (x_s, y_s) = zip(*points) - x_offset = min(xs) - y_offset = min(ys) + x_offset = min(x_s) + y_offset = min(y_s) # Find the largest and smallest X values to figure out width for bitmap - width = max(xs) - min(xs) + 1 - height = max(ys) - min(ys) + 1 + width = max(x_s) - min(x_s) + 1 + height = max(y_s) - min(y_s) + 1 self._palette = displayio.Palette(colors + 1) self._palette.make_transparent(0) @@ -99,64 +99,70 @@ def draw( for index in range(len(points) - 1): Polygon._line_on(bitmap, points[index], points[index + 1], color_id) + # pylint: disable=too-many-arguments def _line( self, - x0: int, - y0: int, - x1: int, - y1: int, + x_0: int, + y_0: int, + x_1: int, + y_1: int, color: int, ) -> None: - self._line_on(self._bitmap, (x0, y0), (x1, y1), color) + self._line_on(self._bitmap, (x_0, y_0), (x_1, y_1), color) + # pylint: enable=too-many-arguments + + # pylint: disable=too-many-branches, too-many-locals @staticmethod def _line_on( bitmap: displayio.Bitmap, - p0: Tuple[int, int], - p1: Tuple[int, int], + p_0: Tuple[int, int], + p_1: Tuple[int, int], color: int, ) -> None: - (x0, y0) = p0 - (x1, y1) = p1 - if x0 == x1: - if y0 > y1: - y0, y1 = y1, y0 - for _h in range(y0, y1 + 1): - bitmap[x0, _h] = color - elif y0 == y1: - if x0 > x1: - x0, x1 = x1, x0 - for _w in range(x0, x1 + 1): - bitmap[_w, y0] = color + (x_0, y_0) = p_0 + (x_1, y_1) = p_1 + if x_0 == x_1: + if y_0 > y_1: + y_0, y_1 = y_1, y_0 + for _h in range(y_0, y_1 + 1): + bitmap[x_0, _h] = color + elif y_0 == y_1: + if x_0 > x_1: + x_0, x_1 = x_1, x_0 + for _w in range(x_0, x_1 + 1): + bitmap[_w, y_0] = color else: - steep = abs(y1 - y0) > abs(x1 - x0) + steep = abs(y_1 - y_0) > abs(x_1 - x_0) if steep: - x0, y0 = y0, x0 - x1, y1 = y1, x1 + x_0, y_0 = y_0, x_0 + x_1, y_1 = y_1, x_1 - if x0 > x1: - x0, x1 = x1, x0 - y0, y1 = y1, y0 + if x_0 > x_1: + x_0, x_1 = x_1, x_0 + y_0, y_1 = y_1, y_0 - dx = x1 - x0 - dy = abs(y1 - y0) + d_x = x_1 - x_0 + d_y = abs(y_1 - y_0) - err = dx / 2 + err = d_x / 2 - if y0 < y1: + if y_0 < y_1: ystep = 1 else: ystep = -1 - for x in range(x0, x1 + 1): + for x in range(x_0, x_1 + 1): if steep: - bitmap[y0, x] = color + bitmap[y_0, x] = color else: - bitmap[x, y0] = color - err -= dy + bitmap[x, y_0] = color + err -= d_y if err < 0: - y0 += ystep - err += dx + y_0 += ystep + err += d_x + + # pylint: enable=too-many-branches, too-many-locals @property def outline(self) -> Optional[int]: From d1e96bfe79240ceb63d4482106c63d2367422699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Mon, 2 Jan 2023 22:54:01 +0100 Subject: [PATCH 14/20] Reuse the same bitmap in sparkline --- adafruit_display_shapes/sparkline.py | 46 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/adafruit_display_shapes/sparkline.py b/adafruit_display_shapes/sparkline.py index 87a98c2..d2e529d 100644 --- a/adafruit_display_shapes/sparkline.py +++ b/adafruit_display_shapes/sparkline.py @@ -100,7 +100,7 @@ def values(self) -> List[float]: return self._buffer[start:] + self._buffer[:end] -class Sparkline(displayio.Group): +class Sparkline(displayio.TileGrid): # pylint: disable=too-many-arguments """A sparkline graph. @@ -120,6 +120,8 @@ class Sparkline(displayio.Group): will scroll to the left. """ + _LINE_COLOR = 1 + def __init__( self, width: int, @@ -132,11 +134,7 @@ def __init__( y: int = 0, color: int = 0xFFFFFF, # line color, default is WHITE ) -> None: - # define class instance variables - self.width = width # in pixels - self.height = height # in pixels - self.color = color # self._max_items = max_items # maximum number of items in the list self._buffer = _CyclicBuffer(self._max_items) self.dyn_xpitch = dyn_xpitch @@ -151,15 +149,17 @@ def __init__( # y_top: The actual minimum value of the vertical scale, will be # updated if autorange self._points = [] # _points: all points of sparkline + colors = 2 + self._palette = displayio.Palette(colors + 1) + self._palette.make_transparent(0) + self._palette[self._LINE_COLOR] = color + self._bitmap = displayio.Bitmap(width, height, colors + 1) - super().__init__(x=x, y=y) # self is a group of single Polygon - # (TODO: it has one element, maybe group is no longer needed?) + super().__init__(self._bitmap, pixel_shader=self._palette, x=x, y=y) def clear_values(self) -> None: """Clears _buffer and removes all lines in the group""" - - for _ in range(len(self)): # remove all items from the current group - self.pop() + self._bitmap.fill(0) self._buffer.clear() def add_value(self, value: float, update: bool = True) -> None: @@ -222,20 +222,18 @@ def _add_point( x: int, value: float, ) -> None: - # Guard for y_top and y_bottom being the same if self.y_top == self.y_bottom: y = int(0.5 * self.height) else: - y = int(self.height * (self.y_top - value) / (self.y_top - self.y_bottom)) + y = int( + (self.height - 1) * (self.y_top - value) / (self.y_top - self.y_bottom) + ) self._points.append((x, y)) def _draw(self) -> None: - while len(self): - self.pop() - self.append( - Polygon(self._points, outline=self.color, close=False) - ) # plot the polyline + self._bitmap.fill(0) + Polygon.draw(self._bitmap, self._points, self._LINE_COLOR, close=False) # pylint: disable= too-many-branches, too-many-nested-blocks, too-many-locals, too-many-statements @@ -308,3 +306,17 @@ def values(self) -> List[float]: """Returns the values displayed on the sparkline.""" return self._buffer.values() + + @property + def width(self) -> int: + """ + :return: the width of the graph in pixels + """ + return self._bitmap.width + + @property + def height(self) -> int: + """ + :return: the height of the graph in pixels + """ + return self._bitmap.height From 064e1710ad623d0638c3aee1c25c7d8248b5eeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Tue, 3 Jan 2023 18:02:52 +0100 Subject: [PATCH 15/20] Add MultiSparkline And rewrite Sparkline to be a subclass of MultiSparkline --- adafruit_display_shapes/multisparkline.py | 332 ++++++++++++++++++++++ adafruit_display_shapes/sparkline.py | 235 +-------------- 2 files changed, 340 insertions(+), 227 deletions(-) create mode 100644 adafruit_display_shapes/multisparkline.py diff --git a/adafruit_display_shapes/multisparkline.py b/adafruit_display_shapes/multisparkline.py new file mode 100644 index 0000000..6739cf0 --- /dev/null +++ b/adafruit_display_shapes/multisparkline.py @@ -0,0 +1,332 @@ +# SPDX-FileCopyrightText: 2020 Kevin Matocha +# +# SPDX-License-Identifier: MIT + +""" +`multisparkline` +================================================================================ + +Various common shapes for use with displayio - Multiple Sparklines on one chart! + + +* Author(s): Kevin Matocha, Maciej Sokolowski + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +try: + from typing import Optional, List +except ImportError: + pass +import displayio +from adafruit_display_shapes.polygon import Polygon + + +class _CyclicBuffer: + def __init__(self, size: int) -> None: + self._buffer = [0.0] * size + self._start = 0 # between 0 and size-1 + self._end = 0 # between 0 and 2*size-1 + + def push(self, value: int | float) -> None: + """Pushes value at the end of the buffer. + + :param int|float value: value to be pushed + + """ + + if self.len() == len(self._buffer): + raise RuntimeError("Trying to push to full buffer") + self._buffer[self._end % len(self._buffer)] = value + self._end += 1 + + def pop(self) -> int | float: + """Pop value from the start of the buffer and returns it.""" + + if self.len() == 0: + raise RuntimeError("Trying to pop from empty buffer") + result = self._buffer[self._start] + self._start += 1 + if self._start == len(self._buffer): + self._start -= len(self._buffer) + self._end -= len(self._buffer) + return result + + def len(self) -> int: + """Returns count of valid data in the buffer.""" + + return self._end - self._start + + def clear(self) -> None: + """Marks all data as invalid.""" + + self._start = 0 + self._end = 0 + + def values(self) -> List[int | float]: + """Returns valid data from the buffer.""" + + if self.len() == 0: + return [] + start = self._start + end = self._end % len(self._buffer) + if start < end: + return self._buffer[start:end] + return self._buffer[start:] + self._buffer[:end] + + +class MultiSparkline(displayio.TileGrid): + """A multiple sparkline graph. + + :param int width: Width of the multisparkline graph in pixels + :param int height: Height of the multisparkline graph in pixels + :param int max_items: Maximum number of values housed in each sparkline + :param bool dyn_xpitch: (Optional) Dynamically change xpitch (True) + :param list y_mins: Lower range for the y-axis per line. + Set each to None for autorange of respective line. + Set to None for autorange of all lines. + :param list y_maxs: Upper range for the y-axis per line. + Set each to None for autorange of respective line. + Set to None for autorange of all lines. + :param int x: X-position on the screen, in pixels + :param int y: Y-position on the screen, in pixels + :param list colors: Each line color. Number of items in this list determines maximum + number of sparklines + + Note: If dyn_xpitch is True (default), each sparkline will allways span + the complete width. Otherwise, each sparkline will grow when you + add values. Once the line has reached the full width, each sparkline + will scroll to the left. + """ + + def __init__( + self, + width: int, + height: int, + max_items: int, + colors: List[int], # each line color + dyn_xpitch: Optional[bool] = True, # True = dynamic pitch size + y_mins: Optional[List[Optional[int]]] = None, # None = autoscaling + y_maxs: Optional[List[Optional[int]]] = None, # None = autoscaling + x: int = 0, + y: int = 0, + ) -> None: + # define class instance variables + self._max_items = max_items # maximum number of items in the list + self._lines = len(colors) + self._buffers = [ + _CyclicBuffer(self._max_items) for i in range(self._lines) + ] # values per sparkline + self._points = [ + _CyclicBuffer(self._max_items) for i in range(self._lines) + ] # _points: all points of sparkline + self.dyn_xpitch = dyn_xpitch + if not dyn_xpitch: + self._xpitch = (width - 1) / (self._max_items - 1) + self.y_mins = ( + [None] * self._lines if y_mins is None else y_mins + ) # minimum of each y-axis (None: autoscale) + self.y_maxs = ( + [None] * self._lines if y_maxs is None else y_maxs + ) # maximum of each y-axis (None: autoscale) + self.y_bottoms = self.y_mins.copy() + # y_bottom: The actual minimum value of the vertical scale, will be + # updated if autorange + self.y_tops = self.y_maxs.copy() + # y_top: The actual minimum value of the vertical scale, will be + # updated if autorange + self._palette = displayio.Palette(self._lines + 1) + self._palette.make_transparent(0) + for (i, color) in enumerate(colors): + self._palette[i + 1] = color + self._bitmap = displayio.Bitmap(width, height, self._lines + 1) + + super().__init__(self._bitmap, pixel_shader=self._palette, x=x, y=y) + + def clear_values(self) -> None: + """Clears _buffer and removes all lines in the group""" + self._bitmap.fill(0) + for buffer in self._buffers: + buffer.clear() + + def add_values(self, values: List[float], update: bool = True) -> None: + """Add a value to each sparkline. + + :param list values: The values to be added, one per sparkline + :param bool update: trigger recreation of primitives + + Note: when adding multiple values per sparkline it is more efficient to call + this method with parameter 'update=False' and then to manually + call the update()-method + """ + + for (i, value) in enumerate(values): + if value is not None: + top = self.y_tops[i] + bottom = self.y_bottoms[i] + if ( + self._buffers[i].len() >= self._max_items + ): # if list is full, remove the first item + first = self._buffers[i].pop() + # check if boundaries have to be updated + if self.y_mins[i] is None and first == bottom: + bottom = min(self._buffers[i].values()) + if self.y_maxs[i] is None and first == self.y_tops[i]: + top = max(self._buffers[i].values()) + self._buffers[i].push(value) + + if self.y_mins[i] is None: + bottom = value if not bottom else min(value, bottom) + if self.y_maxs[i] is None: + top = value if not top else max(value, top) + + self.y_tops[i] = top + self.y_bottoms[i] = bottom + + if update: + self.update(i) + + @staticmethod + def _xintercept( + x_1: float, + y_1: float, + x_2: float, + y_2: float, + horizontal_y: float, + ) -> Optional[ + int + ]: # finds intercept of the line and a horizontal line at horizontalY + slope = (y_2 - y_1) / (x_2 - x_1) + b = y_1 - slope * x_1 + + if slope == 0 and y_1 != horizontal_y: # does not intercept horizontalY + return None + else: + xint = ( + horizontal_y - b + ) / slope # calculate the x-intercept at position y=horizontalY + return int(xint) + + def _add_point( + self, + line: int, + x: int, + value: float, + ) -> None: + # Guard for y_top and y_bottom being the same + top = self.y_tops[line] + bottom = self.y_bottoms[line] + if top == bottom: + y = int(0.5 * self.height) + else: + y = int((self.height - 1) * (top - value) / (top - bottom)) + self._points[line].push((x, y)) + + def _draw(self) -> None: + self._bitmap.fill(0) + for i in range(self._lines): + Polygon.draw(self._bitmap, self._points[i].values(), i + 1, close=False) + + def update_line(self, line: int = None) -> None: + """Update the drawing of the sparkline. + param int|None line: Line to update. Set to None for updating all (default). + """ + + if line is None: + lines = range(self._lines) + else: + lines = [line] + + redraw = False + for l in lines: + # bail out early if we only have a single point + n_points = self._buffers[l].len() + if n_points < 2: + continue + + redraw = True + if self.dyn_xpitch: + # this is a float, only make int when plotting the line + xpitch = (self.width - 1) / (n_points - 1) + else: + xpitch = self._xpitch + + self._points[l].clear() # remove all points + + for count, value in enumerate(self._buffers[l].values()): + if count == 0: + self._add_point(l, 0, value) + else: + x = int(xpitch * count) + last_x = int(xpitch * (count - 1)) + top = self.y_tops[l] + bottom = self.y_bottoms[l] + + if (bottom <= last_value <= top) and ( + bottom <= value <= top + ): # both points are in range, plot the line + self._add_point(l, x, value) + + else: # at least one point is out of range, clip one or both ends the line + if ((last_value > top) and (value > top)) or ( + (last_value < bottom) and (value < bottom) + ): + # both points are on the same side out of range: don't draw anything + pass + else: + xint_bottom = self._xintercept( + last_x, last_value, x, value, bottom + ) # get possible new x intercept points + xint_top = self._xintercept( + last_x, last_value, x, value, top + ) # on the top and bottom of range + if (xint_bottom is None) or ( + xint_top is None + ): # out of range doublecheck + pass + else: + # Initialize the adjusted values as the baseline + adj_x = x + adj_value = value + + if value > last_value: # slope is positive + if xint_top <= x: # top is clipped + adj_x = xint_top + adj_value = top # y + else: # slope is negative + if xint_bottom <= x: # bottom is clipped + adj_x = xint_bottom + adj_value = bottom # y + + self._add_point(l, adj_x, adj_value) + + last_value = value # store value for the next iteration + + if redraw: + self._draw() + + def values(self, line: int) -> List[float]: + """Returns the values displayed on the sparkline at given index.""" + + return self._buffers[line].values() + + @property + def width(self) -> int: + """ + :return: the width of the graph in pixels + """ + return self._bitmap.width + + @property + def height(self) -> int: + """ + :return: the height of the graph in pixels + """ + return self._bitmap.height diff --git a/adafruit_display_shapes/sparkline.py b/adafruit_display_shapes/sparkline.py index d2e529d..d797f74 100644 --- a/adafruit_display_shapes/sparkline.py +++ b/adafruit_display_shapes/sparkline.py @@ -37,71 +37,14 @@ """ -# pylint: disable=too-many-instance-attributes - try: from typing import Optional, List except ImportError: pass -import displayio -from adafruit_display_shapes.polygon import Polygon - - -class _CyclicBuffer: - def __init__(self, size: int) -> None: - self._buffer = [None] * size - self._start = 0 # between 0 and size-1 - self._end = 0 # between 0 and 2*size-1 - - def push(self, value: float) -> None: - """Pushes value at the end of the buffer. - - :param float value: value to be pushed - - """ - - if self.len() == len(self._buffer): - raise RuntimeError("Trying to push to full buffer") - self._buffer[self._end % len(self._buffer)] = value - self._end += 1 - - def pop(self) -> float: - """Pop value from the start of the buffer and returns it.""" - - if self.len() == 0: - raise RuntimeError("Trying to pop from empty buffer") - result = self._buffer[self._start] - self._start += 1 - if self._start == len(self._buffer): - self._start -= len(self._buffer) - self._end -= len(self._buffer) - return result - - def len(self) -> int: - """Returns count of valid data in the buffer.""" +from adafruit_display_shapes.multisparkline import MultiSparkline - return self._end - self._start - def clear(self) -> None: - """Marks all data as invalid.""" - - self._start = 0 - self._end = 0 - - def values(self) -> List[float]: - """Returns valid data from the buffer.""" - - if self.len() == 0: - return [] - start = self._start - end = self._end % len(self._buffer) - if start < end: - return self._buffer[start:end] - return self._buffer[start:] + self._buffer[:end] - - -class Sparkline(displayio.TileGrid): - # pylint: disable=too-many-arguments +class Sparkline(MultiSparkline): """A sparkline graph. :param int width: Width of the sparkline graph in pixels @@ -120,8 +63,6 @@ class Sparkline(displayio.TileGrid): will scroll to the left. """ - _LINE_COLOR = 1 - def __init__( self, width: int, @@ -134,33 +75,9 @@ def __init__( y: int = 0, color: int = 0xFFFFFF, # line color, default is WHITE ) -> None: - # define class instance variables - self._max_items = max_items # maximum number of items in the list - self._buffer = _CyclicBuffer(self._max_items) - self.dyn_xpitch = dyn_xpitch - if not dyn_xpitch: - self._xpitch = (width - 1) / (self._max_items - 1) - self.y_min = y_min # minimum of y-axis (None: autoscale) - self.y_max = y_max # maximum of y-axis (None: autoscale) - self.y_bottom = y_min - # y_bottom: The actual minimum value of the vertical scale, will be - # updated if autorange - self.y_top = y_max - # y_top: The actual minimum value of the vertical scale, will be - # updated if autorange - self._points = [] # _points: all points of sparkline - colors = 2 - self._palette = displayio.Palette(colors + 1) - self._palette.make_transparent(0) - self._palette[self._LINE_COLOR] = color - self._bitmap = displayio.Bitmap(width, height, colors + 1) - - super().__init__(self._bitmap, pixel_shader=self._palette, x=x, y=y) - - def clear_values(self) -> None: - """Clears _buffer and removes all lines in the group""" - self._bitmap.fill(0) - self._buffer.clear() + super().__init__( + width, height, max_items, [color], dyn_xpitch, [y_min], [y_max], x, y + ) def add_value(self, value: float, update: bool = True) -> None: """Add a value to the sparkline. @@ -173,150 +90,14 @@ def add_value(self, value: float, update: bool = True) -> None: call the update()-method """ - if value is not None: - if ( - self._buffer.len() >= self._max_items - ): # if list is full, remove the first item - first = self._buffer.pop() - # check if boundaries have to be updated - if self.y_min is None and first == self.y_bottom: - self.y_bottom = min(self._buffer.values()) - if self.y_max is None and first == self.y_top: - self.y_top = max(self._buffer.values()) - self._buffer.push(value) - - if self.y_min is None: - self.y_bottom = ( - value if not self.y_bottom else min(value, self.y_bottom) - ) - if self.y_max is None: - self.y_top = value if not self.y_top else max(value, self.y_top) - - if update: - self.update() - - # pylint: disable=no-else-return - @staticmethod - def _xintercept( - x_1: float, - y_1: float, - x_2: float, - y_2: float, - horizontal_y: float, - ) -> Optional[ - int - ]: # finds intercept of the line and a horizontal line at horizontalY - slope = (y_2 - y_1) / (x_2 - x_1) - b = y_1 - slope * x_1 - - if slope == 0 and y_1 != horizontal_y: # does not intercept horizontalY - return None - else: - xint = ( - horizontal_y - b - ) / slope # calculate the x-intercept at position y=horizontalY - return int(xint) - - def _add_point( - self, - x: int, - value: float, - ) -> None: - # Guard for y_top and y_bottom being the same - if self.y_top == self.y_bottom: - y = int(0.5 * self.height) - else: - y = int( - (self.height - 1) * (self.y_top - value) / (self.y_top - self.y_bottom) - ) - self._points.append((x, y)) - - def _draw(self) -> None: - self._bitmap.fill(0) - Polygon.draw(self._bitmap, self._points, self._LINE_COLOR, close=False) - - # pylint: disable= too-many-branches, too-many-nested-blocks, too-many-locals, too-many-statements + self.add_values([value], update) def update(self) -> None: """Update the drawing of the sparkline.""" - # bail out early if we only have a single point - n_points = self._buffer.len() - if n_points < 2: - return - - if self.dyn_xpitch: - # this is a float, only make int when plotting the line - xpitch = (self.width - 1) / (n_points - 1) - else: - xpitch = self._xpitch - - self._points = [] # remove all points - - for count, value in enumerate(self._buffer.values()): - if count == 0: - self._add_point(0, value) - else: - x = int(xpitch * count) - last_x = int(xpitch * (count - 1)) - - if (self.y_bottom <= last_value <= self.y_top) and ( - self.y_bottom <= value <= self.y_top - ): # both points are in range, plot the line - self._add_point(x, value) - - else: # at least one point is out of range, clip one or both ends the line - if ((last_value > self.y_top) and (value > self.y_top)) or ( - (last_value < self.y_bottom) and (value < self.y_bottom) - ): - # both points are on the same side out of range: don't draw anything - pass - else: - xint_bottom = self._xintercept( - last_x, last_value, x, value, self.y_bottom - ) # get possible new x intercept points - xint_top = self._xintercept( - last_x, last_value, x, value, self.y_top - ) # on the top and bottom of range - if (xint_bottom is None) or ( - xint_top is None - ): # out of range doublecheck - pass - else: - # Initialize the adjusted values as the baseline - adj_x = x - adj_value = value - - if value > last_value: # slope is positive - if xint_top <= x: # top is clipped - adj_x = xint_top - adj_value = self.y_top # y - else: # slope is negative - if xint_bottom <= x: # bottom is clipped - adj_x = xint_bottom - adj_value = self.y_bottom # y - - self._add_point(adj_x, adj_value) - - last_value = value # store value for the next iteration - - self._draw() + self.update_line(0) def values(self) -> List[float]: """Returns the values displayed on the sparkline.""" - return self._buffer.values() - - @property - def width(self) -> int: - """ - :return: the width of the graph in pixels - """ - return self._bitmap.width - - @property - def height(self) -> int: - """ - :return: the height of the graph in pixels - """ - return self._bitmap.height + return self._buffers[0].values() From 34e866240affc8a58d18f9a00cb2392cbe96f843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Tue, 3 Jan 2023 18:28:23 +0100 Subject: [PATCH 16/20] Fix/override pylint remarks --- adafruit_display_shapes/multisparkline.py | 33 +++++++++++++---------- adafruit_display_shapes/sparkline.py | 5 +++- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/adafruit_display_shapes/multisparkline.py b/adafruit_display_shapes/multisparkline.py index 6739cf0..c17952c 100644 --- a/adafruit_display_shapes/multisparkline.py +++ b/adafruit_display_shapes/multisparkline.py @@ -106,6 +106,7 @@ class MultiSparkline(displayio.TileGrid): will scroll to the left. """ + # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__( self, width: int, @@ -150,6 +151,8 @@ def __init__( super().__init__(self._bitmap, pixel_shader=self._palette, x=x, y=y) + # pylint: enable=too-many-arguments + def clear_values(self) -> None: """Clears _buffer and removes all lines in the group""" self._bitmap.fill(0) @@ -208,11 +211,10 @@ def _xintercept( if slope == 0 and y_1 != horizontal_y: # does not intercept horizontalY return None - else: - xint = ( - horizontal_y - b - ) / slope # calculate the x-intercept at position y=horizontalY - return int(xint) + xint = ( + horizontal_y - b + ) / slope # calculate the x-intercept at position y=horizontalY + return int(xint) def _add_point( self, @@ -234,6 +236,7 @@ def _draw(self) -> None: for i in range(self._lines): Polygon.draw(self._bitmap, self._points[i].values(), i + 1, close=False) + # pylint: disable=too-many-locals, too-many-nested-blocks, too-many-branches def update_line(self, line: int = None) -> None: """Update the drawing of the sparkline. param int|None line: Line to update. Set to None for updating all (default). @@ -245,9 +248,9 @@ def update_line(self, line: int = None) -> None: lines = [line] redraw = False - for l in lines: + for a_line in lines: # bail out early if we only have a single point - n_points = self._buffers[l].len() + n_points = self._buffers[a_line].len() if n_points < 2: continue @@ -258,21 +261,21 @@ def update_line(self, line: int = None) -> None: else: xpitch = self._xpitch - self._points[l].clear() # remove all points + self._points[a_line].clear() # remove all points - for count, value in enumerate(self._buffers[l].values()): + for count, value in enumerate(self._buffers[a_line].values()): if count == 0: - self._add_point(l, 0, value) + self._add_point(a_line, 0, value) else: x = int(xpitch * count) last_x = int(xpitch * (count - 1)) - top = self.y_tops[l] - bottom = self.y_bottoms[l] + top = self.y_tops[a_line] + bottom = self.y_bottoms[a_line] if (bottom <= last_value <= top) and ( bottom <= value <= top ): # both points are in range, plot the line - self._add_point(l, x, value) + self._add_point(a_line, x, value) else: # at least one point is out of range, clip one or both ends the line if ((last_value > top) and (value > top)) or ( @@ -312,7 +315,9 @@ def update_line(self, line: int = None) -> None: if redraw: self._draw() - def values(self, line: int) -> List[float]: + # pylint: enable=too-many-locals, too-many-nested-blocks, too-many-branches + + def values_of(self, line: int) -> List[float]: """Returns the values displayed on the sparkline at given index.""" return self._buffers[line].values() diff --git a/adafruit_display_shapes/sparkline.py b/adafruit_display_shapes/sparkline.py index d797f74..c4240bd 100644 --- a/adafruit_display_shapes/sparkline.py +++ b/adafruit_display_shapes/sparkline.py @@ -63,6 +63,7 @@ class Sparkline(MultiSparkline): will scroll to the left. """ + # pylint: disable=too-many-arguments def __init__( self, width: int, @@ -79,6 +80,8 @@ def __init__( width, height, max_items, [color], dyn_xpitch, [y_min], [y_max], x, y ) + # pylint: enable=too-many-arguments + def add_value(self, value: float, update: bool = True) -> None: """Add a value to the sparkline. @@ -100,4 +103,4 @@ def update(self) -> None: def values(self) -> List[float]: """Returns the values displayed on the sparkline.""" - return self._buffers[0].values() + return self.values_of(0) From fc7c4bdcac58e5c7b7761d24c53b0c41975363e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Tue, 3 Jan 2023 18:32:44 +0100 Subject: [PATCH 17/20] One more pylint remark --- adafruit_display_shapes/multisparkline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_display_shapes/multisparkline.py b/adafruit_display_shapes/multisparkline.py index c17952c..11fae0c 100644 --- a/adafruit_display_shapes/multisparkline.py +++ b/adafruit_display_shapes/multisparkline.py @@ -308,7 +308,7 @@ def update_line(self, line: int = None) -> None: adj_x = xint_bottom adj_value = bottom # y - self._add_point(l, adj_x, adj_value) + self._add_point(a_line, adj_x, adj_value) last_value = value # store value for the next iteration From b2aec7ffc9fb81501a3c7db9e15c57b1c178d482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Sat, 7 Jan 2023 22:13:12 +0100 Subject: [PATCH 18/20] Address review remarks --- adafruit_display_shapes/multisparkline.py | 4 ++-- adafruit_display_shapes/sparkline.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/adafruit_display_shapes/multisparkline.py b/adafruit_display_shapes/multisparkline.py index 11fae0c..fb251ec 100644 --- a/adafruit_display_shapes/multisparkline.py +++ b/adafruit_display_shapes/multisparkline.py @@ -141,7 +141,7 @@ def __init__( # y_bottom: The actual minimum value of the vertical scale, will be # updated if autorange self.y_tops = self.y_maxs.copy() - # y_top: The actual minimum value of the vertical scale, will be + # y_top: The actual maximum value of the vertical scale, will be # updated if autorange self._palette = displayio.Palette(self._lines + 1) self._palette.make_transparent(0) @@ -194,7 +194,7 @@ def add_values(self, values: List[float], update: bool = True) -> None: self.y_bottoms[i] = bottom if update: - self.update(i) + self.update_line(i) @staticmethod def _xintercept( diff --git a/adafruit_display_shapes/sparkline.py b/adafruit_display_shapes/sparkline.py index c4240bd..1efbc29 100644 --- a/adafruit_display_shapes/sparkline.py +++ b/adafruit_display_shapes/sparkline.py @@ -104,3 +104,17 @@ def values(self) -> List[float]: """Returns the values displayed on the sparkline.""" return self.values_of(0) + + @property + def y_top(self) -> float: + """ + :return: The actual maximum value of the vertical scale, will be updated if autorange + """ + return self.y_tops[0] + + @property + def y_bottom(self) -> float: + """ + :return: The actual minimum value of the vertical scale, will be updated if autorange + """ + return self.y_bottoms[0] From fe8779f9c34ca7983a6c975f588e4ec87e01f6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Sat, 7 Jan 2023 23:53:18 +0100 Subject: [PATCH 19/20] Move checking if line fits on bitmap to polygon class --- adafruit_display_shapes/multisparkline.py | 91 +++-------------------- adafruit_display_shapes/polygon.py | 22 +++++- 2 files changed, 30 insertions(+), 83 deletions(-) diff --git a/adafruit_display_shapes/multisparkline.py b/adafruit_display_shapes/multisparkline.py index fb251ec..ae07965 100644 --- a/adafruit_display_shapes/multisparkline.py +++ b/adafruit_display_shapes/multisparkline.py @@ -22,7 +22,9 @@ """ try: - from typing import Optional, List + from typing import Optional, List, TypeVar + + T = TypeVar("T") except ImportError: pass import displayio @@ -30,15 +32,15 @@ class _CyclicBuffer: - def __init__(self, size: int) -> None: - self._buffer = [0.0] * size + def __init__(self, size: int, init_value: T) -> None: + self._buffer = [init_value] * size self._start = 0 # between 0 and size-1 self._end = 0 # between 0 and 2*size-1 - def push(self, value: int | float) -> None: + def push(self, value: T) -> None: """Pushes value at the end of the buffer. - :param int|float value: value to be pushed + :param T value: value to be pushed """ @@ -47,7 +49,7 @@ def push(self, value: int | float) -> None: self._buffer[self._end % len(self._buffer)] = value self._end += 1 - def pop(self) -> int | float: + def pop(self) -> T: """Pop value from the start of the buffer and returns it.""" if self.len() == 0: @@ -70,7 +72,7 @@ def clear(self) -> None: self._start = 0 self._end = 0 - def values(self) -> List[int | float]: + def values(self) -> List[T]: """Returns valid data from the buffer.""" if self.len() == 0: @@ -123,10 +125,10 @@ def __init__( self._max_items = max_items # maximum number of items in the list self._lines = len(colors) self._buffers = [ - _CyclicBuffer(self._max_items) for i in range(self._lines) + _CyclicBuffer(self._max_items, 0.0) for i in range(self._lines) ] # values per sparkline self._points = [ - _CyclicBuffer(self._max_items) for i in range(self._lines) + _CyclicBuffer(self._max_items, (0, 0)) for i in range(self._lines) ] # _points: all points of sparkline self.dyn_xpitch = dyn_xpitch if not dyn_xpitch: @@ -196,26 +198,6 @@ def add_values(self, values: List[float], update: bool = True) -> None: if update: self.update_line(i) - @staticmethod - def _xintercept( - x_1: float, - y_1: float, - x_2: float, - y_2: float, - horizontal_y: float, - ) -> Optional[ - int - ]: # finds intercept of the line and a horizontal line at horizontalY - slope = (y_2 - y_1) / (x_2 - x_1) - b = y_1 - slope * x_1 - - if slope == 0 and y_1 != horizontal_y: # does not intercept horizontalY - return None - xint = ( - horizontal_y - b - ) / slope # calculate the x-intercept at position y=horizontalY - return int(xint) - def _add_point( self, line: int, @@ -236,7 +218,6 @@ def _draw(self) -> None: for i in range(self._lines): Polygon.draw(self._bitmap, self._points[i].values(), i + 1, close=False) - # pylint: disable=too-many-locals, too-many-nested-blocks, too-many-branches def update_line(self, line: int = None) -> None: """Update the drawing of the sparkline. param int|None line: Line to update. Set to None for updating all (default). @@ -264,59 +245,11 @@ def update_line(self, line: int = None) -> None: self._points[a_line].clear() # remove all points for count, value in enumerate(self._buffers[a_line].values()): - if count == 0: - self._add_point(a_line, 0, value) - else: - x = int(xpitch * count) - last_x = int(xpitch * (count - 1)) - top = self.y_tops[a_line] - bottom = self.y_bottoms[a_line] - - if (bottom <= last_value <= top) and ( - bottom <= value <= top - ): # both points are in range, plot the line - self._add_point(a_line, x, value) - - else: # at least one point is out of range, clip one or both ends the line - if ((last_value > top) and (value > top)) or ( - (last_value < bottom) and (value < bottom) - ): - # both points are on the same side out of range: don't draw anything - pass - else: - xint_bottom = self._xintercept( - last_x, last_value, x, value, bottom - ) # get possible new x intercept points - xint_top = self._xintercept( - last_x, last_value, x, value, top - ) # on the top and bottom of range - if (xint_bottom is None) or ( - xint_top is None - ): # out of range doublecheck - pass - else: - # Initialize the adjusted values as the baseline - adj_x = x - adj_value = value - - if value > last_value: # slope is positive - if xint_top <= x: # top is clipped - adj_x = xint_top - adj_value = top # y - else: # slope is negative - if xint_bottom <= x: # bottom is clipped - adj_x = xint_bottom - adj_value = bottom # y - - self._add_point(a_line, adj_x, adj_value) - - last_value = value # store value for the next iteration + self._add_point(a_line, int(xpitch * count), value) if redraw: self._draw() - # pylint: enable=too-many-locals, too-many-nested-blocks, too-many-branches - def values_of(self, line: int) -> List[float]: """Returns the values displayed on the sparkline at given index.""" diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index 6f76faa..89295b3 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -112,6 +112,16 @@ def _line( # pylint: enable=too-many-arguments + @staticmethod + def _safe_draw( + bitmap: displayio.Bitmap, + p: Tuple[int, int], + color: int, + ) -> None: + (x, y) = p + if 0 <= x < bitmap.width and 0 <= y < bitmap.height: + bitmap[x, y] = color + # pylint: disable=too-many-branches, too-many-locals @staticmethod def _line_on( @@ -122,16 +132,20 @@ def _line_on( ) -> None: (x_0, y_0) = p_0 (x_1, y_1) = p_1 + + def pt_on(x, y): + Polygon._safe_draw(bitmap, (x, y), color) + if x_0 == x_1: if y_0 > y_1: y_0, y_1 = y_1, y_0 for _h in range(y_0, y_1 + 1): - bitmap[x_0, _h] = color + pt_on(x_0, _h) elif y_0 == y_1: if x_0 > x_1: x_0, x_1 = x_1, x_0 for _w in range(x_0, x_1 + 1): - bitmap[_w, y_0] = color + pt_on(_w, y_0) else: steep = abs(y_1 - y_0) > abs(x_1 - x_0) if steep: @@ -154,9 +168,9 @@ def _line_on( for x in range(x_0, x_1 + 1): if steep: - bitmap[y_0, x] = color + pt_on(y_0, x) else: - bitmap[x, y_0] = color + pt_on(x, y_0) err -= d_y if err < 0: y_0 += ystep From 72ebdc4ee86b20dbfc096113067ff972aefc6cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Soko=C5=82owski?= Date: Sat, 7 Jan 2023 23:58:05 +0100 Subject: [PATCH 20/20] Pylint compliance --- adafruit_display_shapes/polygon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index 89295b3..f9ece68 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -115,10 +115,10 @@ def _line( @staticmethod def _safe_draw( bitmap: displayio.Bitmap, - p: Tuple[int, int], + point: Tuple[int, int], color: int, ) -> None: - (x, y) = p + (x, y) = point if 0 <= x < bitmap.width and 0 <= y < bitmap.height: bitmap[x, y] = color