/ scales.py
scales.py
  1  # SPDX-FileCopyrightText: Copyright (c) 2021, 2023 Jose David M.
  2  #
  3  # SPDX-License-Identifier: MIT
  4  """
  5  
  6  `scales`
  7  ================================================================================
  8  
  9  Allows display data in a graduated level
 10  
 11  
 12  * Author(s): Jose David M.
 13  
 14  Implementation Notes
 15  --------------------
 16  
 17  Scales version in CircuitPython
 18  
 19  """
 20  
 21  ################################
 22  # A scales library for CircuitPython, using `displayio`` and `vectorio``
 23  #
 24  # Features:
 25  #  - Vertical and Horizontal direction
 26  #  - Animation to use with different sensor
 27  
 28  try:
 29      from typing import Union, Tuple, Optional
 30  except ImportError:
 31      pass
 32  
 33  import displayio
 34  import terminalio
 35  from adafruit_display_text.bitmap_label import Label
 36  from vectorio import Polygon, Rectangle
 37  import ulab.numpy as np
 38  
 39  try:
 40      from typing import Tuple
 41  except ImportError:
 42      pass
 43  
 44  __version__ = "0.0.0-auto.0"
 45  __repo__ = "https://github.com/jposada202020/CircuitPython_scales.git"
 46  
 47  # pylint: disable=too-many-arguments, too-few-public-methods,too-many-instance-attributes
 48  # pylint: disable=invalid-unary-operand-type
 49  class Axes(displayio.Group):
 50      """
 51      :param int x: pixel position. Defaults to :const:`0`
 52      :param int y: pixel position. Defaults to :const:`0`
 53  
 54      :param int,int limits: tuple of value range for the scale. Defaults to (0, 100)
 55      :param list ticks: list to ticks to display. If this is not enter a equally spaced scale
 56       will be created between the given limits.
 57  
 58      :param str direction: direction of the scale either :attr:`horizontal` or :attr:`vertical`
 59       defaults to :attr:`horizontal`
 60  
 61      :param int stroke: width in pixels of the scale axes. Defaults to :const:`3`
 62  
 63      :param int length: scale length in pixels. Defaults to :const:`100`
 64  
 65      :param int color: 24-bit hex value axes line color, Defaults to Purple :const:`0x990099`
 66  
 67      """
 68  
 69      def __init__(
 70          self,
 71          x: int = 0,
 72          y: int = 0,
 73          limits: Tuple[int, int] = (0, 100),
 74          ticks: Optional[Union[np.array, list]] = None,
 75          direction: str = "horizontal",
 76          stroke: int = 3,
 77          length: int = 100,
 78          color: int = 0x990099,
 79      ):
 80  
 81          super().__init__()
 82  
 83          if direction == "horizontal":
 84              self.direction = True
 85          else:
 86              self.direction = False
 87  
 88          self.x = x
 89          self.y = y
 90          self.limits = limits
 91  
 92          self._valuemin = limits[0]
 93          self._valuemax = limits[1]
 94          self._newvalmin = length
 95          self._newvalmax = 0
 96  
 97          self.stroke = stroke
 98          self.length = length
 99  
100          if ticks:
101              self.ticks = np.array(ticks)
102          else:
103              self.ticks = np.array(list(range(self._valuemin, self._valuemax, 10)))
104  
105          self.ticksynorm = np.array(
106              transform(
107                  self._valuemin,
108                  self._valuemax,
109                  self._newvalmax,
110                  self._newvalmin,
111                  self.ticks,
112              ),
113              dtype=np.int16,
114          )
115  
116          self._palette = displayio.Palette(2)
117          self._palette.make_transparent(0)
118          self._palette[1] = color
119  
120          self._tick_length = None
121          self._tick_stroke = None
122  
123          self.text_ticks = []
124  
125      def _draw_line(self) -> None:
126          """Private function to draw the Axe.
127          :return: None
128          """
129          if self.direction:
130              self.append(rectangle_draw(0, 0, self.stroke, self.length, self._palette))
131          else:
132              self.append(
133                  rectangle_draw(0, -self.length, self.length, self.stroke, self._palette)
134              )
135  
136      def _draw_ticks(self, tick_length: int = 10, tick_stroke: int = 4) -> None:
137          """Private function to draw the ticks
138          :param int tick_length: tick length in pixels
139          :param int tick_stroke: tick thickness in pixels
140          :return: None
141          """
142  
143          self._tick_length = tick_length
144          self._tick_stroke = tick_stroke
145  
146          if self.direction:
147              for val in self.ticksynorm[:-1]:
148                  self.append(
149                      rectangle_draw(
150                          int(val) - 1,
151                          -self._tick_length,
152                          self._tick_length,
153                          3,
154                          self._palette,
155                      )
156                  )
157          else:
158              for val in self.ticksynorm[:-1]:
159                  self.append(
160                      rectangle_draw(0, -int(val), 3, self._tick_length, self._palette)
161                  )
162  
163      def _draw_text(self) -> None:
164          """Private function to draw the text, uses values found in ``_conversion``
165          :return: None
166          """
167          index = 0
168          separation = 20
169          font_width = 12
170          if self.direction:
171              print("aca")
172              for tick_text in self.ticks[:-1]:
173                  dist_x = self.ticksynorm[index] - font_width // 2
174                  dist_y = separation // 2
175                  text = str(int(tick_text))
176                  tick_label = Label(terminalio.FONT, text=text, x=dist_x, y=dist_y)
177                  self.append(tick_label)
178                  index = index + 1
179          else:
180              for tick_text in self.ticks[:-1]:
181                  dist_x = -separation
182                  dist_y = -self.ticksynorm[index]
183                  text = str(int(tick_text))
184                  tick_label = Label(terminalio.FONT, text=text, x=dist_x, y=dist_y)
185                  self.append(tick_label)
186                  index = index + 1
187  
188  
189  class Scale(Axes):
190      """
191      :param int x: pixel position. Defaults to :const:`0`
192      :param int y: pixel position. Defaults to :const:`0`
193  
194      :param str direction: direction of the scale either :attr:`horizontal` or :attr:`vertical`
195       defaults to :attr:`horizontal`
196  
197      :param int stroke: width in pixels of the axes line. Defaults to :const:`3` pixels
198  
199      :param int length: scale length in pixels. Defaults to :const:`100` pixels
200  
201      :param int color: 24-bit hex value axes line color, Defaults to Purple :const:`0x990099`
202  
203      :param int width: scale width in pixels. Defaults to :const:`50` pixels
204  
205      :param limits: tuple of value range for the scale. Defaults to :const:`(0, 100)`
206      :param list ticks: list to ticks to display. If this is not enter a equally spaced scale
207       will be created between the given limits.
208  
209      :param int back_color: 24-bit hex value axes line color.
210       Defaults to Light Blue :const:`0x9FFFFF`
211  
212      :param int tick_length: Scale tick length in pixels. Defaults to :const:`10`
213      :param int tick_stroke: Scale tick width in pixels. Defaults to :const:`4`
214  
215      :param int pointer_length: length in pixels for the point. Defaults to :const:`20` pixels
216      :param int pointer_stroke: pointer thickness in pixels. Defaults to :const:`6` pixels
217  
218  
219      **Quickstart: Importing and using Scales**
220  
221      Here is one way of importing the `Scale` class, so you can use it as
222      the name ``my_scale``:
223  
224      .. code-block::
225  
226          from scale import Scale
227  
228      Now you can create a vertical Scale at pixel position x=50, y=180 and a range
229      of 0 to 80 using:
230  
231      .. code-block::
232  
233          my_scale = Scale(x=50, y=180, direction="vertical", limits=(0, 80))
234  
235      Once you setup your display, you can now add ``my_scale`` to your display using:
236  
237      .. code-block::
238  
239          display.show(my_scale)
240  
241      If you want to have multiple display elements, you can create a group and then
242      append the scale and the other elements to the group. Then, you can add the full
243      group to the display as in this example:
244  
245      .. code-block:: python
246  
247          my_scale= Scale(x=20, y=30)
248          my_group = displayio.Group() # make a group
249          my_group.append(my_scale) # Add my_slider to the group
250  
251          #
252          # Append other display elements to the group
253          #
254  
255          display.show(my_group) # add the group to the display
256  
257  
258      **Summary: Slider Features and input variables**
259  
260      The `Scale` class has some options for controlling its position, visible appearance,
261      and value through a collection of input variables:
262  
263          - **position**: :attr:`x``, :attr:`y`
264  
265          - **size**: :attr:`length` and :attr:`width`
266  
267          - **color**: :attr:`color`, :attr:`back_color`
268  
269          - **linewidths**: :attr:`stroke` and :attr:`tick_stroke`
270  
271          - **range**: :attr:`limits`
272  
273  
274      .. figure:: scales.png
275        :scale: 100 %
276        :align: center
277        :alt: Diagram of scales
278  
279        Diagram showing a simple scale.
280  
281  
282      """
283  
284      def __init__(
285          self,
286          x: int = 0,
287          y: int = 0,
288          direction: str = "horizontal",
289          stroke: int = 3,
290          length: int = 100,
291          color: int = 0x990099,
292          width: int = 50,
293          limits: Tuple[int, int] = (0, 100),
294          back_color: int = 0x9FFFFF,
295          ticks: Optional[Union[np.array, list]] = None,
296          tick_length: int = 5,
297          tick_stroke: int = 4,
298          pointer_length: int = 20,
299          pointer_stroke: int = 6,
300      ):
301  
302          super().__init__(
303              x=x,
304              y=y,
305              direction=direction,
306              stroke=stroke,
307              length=length,
308              limits=limits,
309              ticks=ticks,
310              color=color,
311          )
312  
313          self._width = width
314          self._back_color = back_color
315          self._draw_background()
316          self._draw_line()
317          self._draw_ticks()
318          self.value = 0
319  
320          self._tick_length = tick_length
321          self._tick_stroke = tick_stroke
322          self._pointer_length = pointer_length
323          self._pointer_stroke = pointer_stroke
324  
325          # Pointer Definitions
326          self._x0 = 0
327          self._y0 = 0
328  
329          if self.direction:
330              self.center = width // 2
331              self._x1 = pointer_stroke
332              self._y1 = pointer_length
333              self._posx = 0
334              self._posy = -self.center - self._pointer_length // 2
335          else:
336              self.center = width // 2
337              self._x1 = pointer_length
338              self._y1 = pointer_stroke
339              self._posx = self.center - self._pointer_length // 2
340              self._posy = -10
341          self.pointer = None
342  
343          self._draw_text()
344          self._draw_pointer()
345  
346      def _draw_background(self):
347          """Private function to draw the background for the scale
348          :return: None
349          """
350          back_palette = displayio.Palette(2)
351          back_palette.make_transparent(0)
352          back_palette[1] = self._back_color
353  
354          if self.direction:
355              self.append(
356                  rectangle_draw(0, -self._width, self._width, self.length, back_palette)
357              )
358          else:
359              self.append(
360                  rectangle_draw(0, -self.length, self.length, self._width, back_palette)
361              )
362  
363      def _draw_pointer(
364          self,
365          color: int = 0xFF0000,
366      ):
367          """Private function to initial draw the pointer.
368  
369          :param int color: 24-bit hex value axes line color. Defaults to red :const:`0xFF0000`
370          :param int val_ini: initial value to draw the pointer
371  
372  
373          :return: None
374  
375          """
376  
377          pointer_palette = displayio.Palette(2)
378          pointer_palette.make_transparent(0)
379          pointer_palette[1] = color
380  
381          points = [
382              (self._x0, self._y0),
383              (self._x1, self._y0),
384              (self._x1, self._y1),
385              (self._x0, self._y1),
386          ]
387          self.pointer = Polygon(
388              pixel_shader=pointer_palette,
389              points=points,
390              x=self._posx,
391              y=self._posy,
392              color_index=1,
393          )
394  
395          self.append(self.pointer)
396  
397      def animate_pointer(self, new_value):
398          """Public function to animate the pointer
399  
400          :param new_value: value to draw the pointer
401          :return: None
402  
403          """
404          value = int(
405              transform(
406                  self._valuemin,
407                  self._valuemax,
408                  self._newvalmax,
409                  self._newvalmin,
410                  new_value,
411              )
412          )
413          if self.direction:
414              self.pointer.x = value - self._pointer_stroke // 2
415          else:
416              self.pointer.y = -value - self._pointer_stroke // 2
417  
418  
419  # pylint: disable=invalid-name
420  def rectangle_draw(x0: int, y0: int, height: int, width: int, palette):
421      """rectangle_draw function
422  
423      Draws a rectangle using or `vectorio.Rectangle`
424  
425      :param int x0: rectangle lower corner x position
426      :param int y0: rectangle lower corner y position
427  
428      :param int width: rectangle upper corner x position
429      :param int height: rectangle upper corner y position
430  
431      :param `~displayio.Palette` palette: palette object to be used to draw the rectangle
432  
433      """
434  
435      return Rectangle(
436          pixel_shader=palette, width=width, height=height, x=x0, y=y0, color_index=1
437      )
438  
439  
440  def transform(
441      oldrangemin: Union[float, int],
442      oldrangemax: Union[float, int],
443      newrangemin: Union[float, int],
444      newrangemax: Union[float, int],
445      value: Union[float, int],
446  ) -> Union[float, int]:
447      """
448      This function converts the original value into a new defined value in the new range
449  
450      :param int|float oldrangemin: minimum of the original range
451      :param int|float oldrangemax: maximum of the original range
452      :param int|float newrangemin: minimum of the new range
453      :param int|float newrangemax: maximum of the new range
454      :param int|float value: value to be converted
455  
456      :return int|float: converted value
457  
458      """
459  
460      return (
461          ((value - oldrangemin) * (newrangemax - newrangemin))
462          / (oldrangemax - oldrangemin)
463      ) + newrangemin