CircuitPython_scales/scales.py
jposada202020 21e118626a refactor
2023-05-21 13:28:33 -04:00

463 lines
13 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2021, 2023 Jose David M.
#
# SPDX-License-Identifier: MIT
"""
`scales`
================================================================================
Allows display data in a graduated level
* Author(s): Jose David M.
Implementation Notes
--------------------
Scales version in CircuitPython
"""
################################
# A scales library for CircuitPython, using `displayio`` and `vectorio``
#
# Features:
# - Vertical and Horizontal direction
# - Animation to use with different sensor
try:
from typing import Union, Tuple, Optional
except ImportError:
pass
import displayio
import terminalio
from adafruit_display_text.bitmap_label import Label
from vectorio import Polygon, Rectangle
import ulab.numpy as np
try:
from typing import Tuple
except ImportError:
pass
__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/jposada202020/CircuitPython_scales.git"
# pylint: disable=too-many-arguments, too-few-public-methods,too-many-instance-attributes
# pylint: disable=invalid-unary-operand-type
class Axes(displayio.Group):
"""
:param int x: pixel position. Defaults to :const:`0`
:param int y: pixel position. Defaults to :const:`0`
:param int,int limits: tuple of value range for the scale. Defaults to (0, 100)
:param list ticks: list to ticks to display. If this is not enter a equally spaced scale
will be created between the given limits.
:param str direction: direction of the scale either :attr:`horizontal` or :attr:`vertical`
defaults to :attr:`horizontal`
:param int stroke: width in pixels of the scale axes. Defaults to :const:`3`
:param int length: scale length in pixels. Defaults to :const:`100`
:param int color: 24-bit hex value axes line color, Defaults to Purple :const:`0x990099`
"""
def __init__(
self,
x: int = 0,
y: int = 0,
limits: Tuple[int, int] = (0, 100),
ticks: Optional[Union[np.array, list]] = None,
direction: str = "horizontal",
stroke: int = 3,
length: int = 100,
color: int = 0x990099,
):
super().__init__()
if direction == "horizontal":
self.direction = True
else:
self.direction = False
self.x = x
self.y = y
self.limits = limits
self._valuemin = limits[0]
self._valuemax = limits[1]
self._newvalmin = length
self._newvalmax = 0
self.stroke = stroke
self.length = length
if ticks:
self.ticks = np.array(ticks)
else:
self.ticks = np.array(list(range(self._valuemin, self._valuemax, 10)))
self.ticksynorm = np.array(
transform(
self._valuemin,
self._valuemax,
self._newvalmax,
self._newvalmin,
self.ticks,
),
dtype=np.int16,
)
self._palette = displayio.Palette(2)
self._palette.make_transparent(0)
self._palette[1] = color
self._tick_length = None
self._tick_stroke = None
self.text_ticks = []
def _draw_line(self) -> None:
"""Private function to draw the Axe.
:return: None
"""
if self.direction:
self.append(rectangle_draw(0, 0, self.stroke, self.length, self._palette))
else:
self.append(
rectangle_draw(0, -self.length, self.length, self.stroke, self._palette)
)
def _draw_ticks(self, tick_length: int = 10, tick_stroke: int = 4) -> None:
"""Private function to draw the ticks
:param int tick_length: tick length in pixels
:param int tick_stroke: tick thickness in pixels
:return: None
"""
self._tick_length = tick_length
self._tick_stroke = tick_stroke
if self.direction:
for val in self.ticksynorm[:-1]:
self.append(
rectangle_draw(
int(val) - 1,
-self._tick_length,
self._tick_length,
3,
self._palette,
)
)
else:
for val in self.ticksynorm[:-1]:
self.append(
rectangle_draw(0, -int(val), 3, self._tick_length, self._palette)
)
def _draw_text(self) -> None:
"""Private function to draw the text, uses values found in ``_conversion``
:return: None
"""
index = 0
separation = 20
font_width = 12
if self.direction:
print("aca")
for tick_text in self.ticks[:-1]:
dist_x = self.ticksynorm[index] - font_width // 2
dist_y = separation // 2
text = str(int(tick_text))
tick_label = Label(terminalio.FONT, text=text, x=dist_x, y=dist_y)
self.append(tick_label)
index = index + 1
else:
for tick_text in self.ticks[:-1]:
dist_x = -separation
dist_y = -self.ticksynorm[index]
text = str(int(tick_text))
tick_label = Label(terminalio.FONT, text=text, x=dist_x, y=dist_y)
self.append(tick_label)
index = index + 1
class Scale(Axes):
"""
:param int x: pixel position. Defaults to :const:`0`
:param int y: pixel position. Defaults to :const:`0`
:param str direction: direction of the scale either :attr:`horizontal` or :attr:`vertical`
defaults to :attr:`horizontal`
:param int stroke: width in pixels of the axes line. Defaults to :const:`3` pixels
:param int length: scale length in pixels. Defaults to :const:`100` pixels
:param int color: 24-bit hex value axes line color, Defaults to Purple :const:`0x990099`
:param int width: scale width in pixels. Defaults to :const:`50` pixels
:param limits: tuple of value range for the scale. Defaults to :const:`(0, 100)`
:param list ticks: list to ticks to display. If this is not enter a equally spaced scale
will be created between the given limits.
:param int back_color: 24-bit hex value axes line color.
Defaults to Light Blue :const:`0x9FFFFF`
:param int tick_length: Scale tick length in pixels. Defaults to :const:`10`
:param int tick_stroke: Scale tick width in pixels. Defaults to :const:`4`
:param int pointer_length: length in pixels for the point. Defaults to :const:`20` pixels
:param int pointer_stroke: pointer thickness in pixels. Defaults to :const:`6` pixels
**Quickstart: Importing and using Scales**
Here is one way of importing the `Scale` class, so you can use it as
the name ``my_scale``:
.. code-block::
from scale import Scale
Now you can create a vertical Scale at pixel position x=50, y=180 and a range
of 0 to 80 using:
.. code-block::
my_scale = Scale(x=50, y=180, direction="vertical", limits=(0, 80))
Once you setup your display, you can now add ``my_scale`` to your display using:
.. code-block::
display.show(my_scale)
If you want to have multiple display elements, you can create a group and then
append the scale and the other elements to the group. Then, you can add the full
group to the display as in this example:
.. code-block:: python
my_scale= Scale(x=20, y=30)
my_group = displayio.Group() # make a group
my_group.append(my_scale) # Add my_slider to the group
#
# Append other display elements to the group
#
display.show(my_group) # add the group to the display
**Summary: Slider Features and input variables**
The `Scale` class has some options for controlling its position, visible appearance,
and value through a collection of input variables:
- **position**: :attr:`x``, :attr:`y`
- **size**: :attr:`length` and :attr:`width`
- **color**: :attr:`color`, :attr:`back_color`
- **linewidths**: :attr:`stroke` and :attr:`tick_stroke`
- **range**: :attr:`limits`
.. figure:: scales.png
:scale: 100 %
:align: center
:alt: Diagram of scales
Diagram showing a simple scale.
"""
def __init__(
self,
x: int = 0,
y: int = 0,
direction: str = "horizontal",
stroke: int = 3,
length: int = 100,
color: int = 0x990099,
width: int = 50,
limits: Tuple[int, int] = (0, 100),
back_color: int = 0x9FFFFF,
ticks: Optional[Union[np.array, list]] = None,
tick_length: int = 5,
tick_stroke: int = 4,
pointer_length: int = 20,
pointer_stroke: int = 6,
):
super().__init__(
x=x,
y=y,
direction=direction,
stroke=stroke,
length=length,
limits=limits,
ticks=ticks,
color=color,
)
self._width = width
self._back_color = back_color
self._draw_background()
self._draw_line()
self._draw_ticks()
self.value = 0
self._tick_length = tick_length
self._tick_stroke = tick_stroke
self._pointer_length = pointer_length
self._pointer_stroke = pointer_stroke
# Pointer Definitions
self._x0 = 0
self._y0 = 0
if self.direction:
self.center = width // 2
self._x1 = pointer_stroke
self._y1 = pointer_length
self._posx = 0
self._posy = -self.center - self._pointer_length // 2
else:
self.center = width // 2
self._x1 = pointer_length
self._y1 = pointer_stroke
self._posx = self.center - self._pointer_length // 2
self._posy = -10
self.pointer = None
self._draw_text()
self._draw_pointer()
def _draw_background(self):
"""Private function to draw the background for the scale
:return: None
"""
back_palette = displayio.Palette(2)
back_palette.make_transparent(0)
back_palette[1] = self._back_color
if self.direction:
self.append(
rectangle_draw(0, -self._width, self._width, self.length, back_palette)
)
else:
self.append(
rectangle_draw(0, -self.length, self.length, self._width, back_palette)
)
def _draw_pointer(
self,
color: int = 0xFF0000,
):
"""Private function to initial draw the pointer.
:param int color: 24-bit hex value axes line color. Defaults to red :const:`0xFF0000`
:param int val_ini: initial value to draw the pointer
:return: None
"""
pointer_palette = displayio.Palette(2)
pointer_palette.make_transparent(0)
pointer_palette[1] = color
points = [
(self._x0, self._y0),
(self._x1, self._y0),
(self._x1, self._y1),
(self._x0, self._y1),
]
self.pointer = Polygon(
pixel_shader=pointer_palette,
points=points,
x=self._posx,
y=self._posy,
color_index=1,
)
self.append(self.pointer)
def animate_pointer(self, new_value):
"""Public function to animate the pointer
:param new_value: value to draw the pointer
:return: None
"""
value = int(
transform(
self._valuemin,
self._valuemax,
self._newvalmax,
self._newvalmin,
new_value,
)
)
if self.direction:
self.pointer.x = value - self._pointer_stroke // 2
else:
self.pointer.y = -value - self._pointer_stroke // 2
# pylint: disable=invalid-name
def rectangle_draw(x0: int, y0: int, height: int, width: int, palette):
"""rectangle_draw function
Draws a rectangle using or `vectorio.Rectangle`
:param int x0: rectangle lower corner x position
:param int y0: rectangle lower corner y position
:param int width: rectangle upper corner x position
:param int height: rectangle upper corner y position
:param `~displayio.Palette` palette: palette object to be used to draw the rectangle
"""
return Rectangle(
pixel_shader=palette, width=width, height=height, x=x0, y=y0, color_index=1
)
def transform(
oldrangemin: Union[float, int],
oldrangemax: Union[float, int],
newrangemin: Union[float, int],
newrangemax: Union[float, int],
value: Union[float, int],
) -> Union[float, int]:
"""
This function converts the original value into a new defined value in the new range
:param int|float oldrangemin: minimum of the original range
:param int|float oldrangemax: maximum of the original range
:param int|float newrangemin: minimum of the new range
:param int|float newrangemax: maximum of the new range
:param int|float value: value to be converted
:return int|float: converted value
"""
return (
((value - oldrangemin) * (newrangemax - newrangemin))
/ (oldrangemax - oldrangemin)
) + newrangemin