/ uboxplot.py
uboxplot.py
  1  # SPDX-FileCopyrightText: Copyright (c) 2023 Jose D. Montoya
  2  #
  3  # SPDX-License-Identifier: MIT
  4  """
  5  `uboxplot`
  6  ================================================================================
  7  
  8  Calculates boxplot and creates its graphical representation
  9  
 10  
 11  * Author: Jose D. Montoya
 12  
 13  Implementation Notes
 14  --------------------
 15  
 16  
 17  **Hardware:**
 18  Boards with CircuitPython >8.0.0 RC2
 19  
 20  **Software and Dependencies:**
 21  
 22  * Adafruit CircuitPython firmware for the supported boards:
 23    https://circuitpython.org/downloads
 24  
 25  
 26  """
 27  
 28  try:
 29      from typing import Union, Tuple
 30  except ImportError:
 31      pass
 32  
 33  from ulab import numpy as np
 34  from bitmaptools import draw_line
 35  import displayio
 36  
 37  __version__ = "0.0.0+auto.0"
 38  __repo__ = "https://github.com/jposada202020/CircuitPython_boxplot.git"
 39  
 40  
 41  # pylint: disable=too-many-instance-attributes, too-many-arguments, invalid-name
 42  class Boxplot(displayio.TileGrid):
 43      """A BoxPlot TileGrid. The origin is set using ``x`` and ``y``.
 44  
 45      :param (list, tuple) data: source data to calculate the boxplot
 46      :param int x: x position of the boxplot origin
 47      :param int y: y position of the boxplot origin
 48  
 49      :param int width: requested width, in pixels. Defaults to 20 pixels.
 50      :param int height: requested height, in pixels.
 51  
 52      :param int background_color: background color to use defaults to black (0x000000)
 53      :param int fill_color: background color to use defaults to black (0x000000)
 54      :param int line_color: background color to use defaults to white (0xFFFFFF)
 55  
 56      **Quickstart: Importing and using UBoxplot**
 57  
 58      Here is one way of importing the `Boxplot` class so you can use it as
 59      the name ``Boxplot``:
 60  
 61      .. code-block:: python
 62  
 63          from uboxplot import Boxplot
 64          import displayio
 65  
 66      Now you can create a boxplot at pixel position x=20, y=30 using:
 67  
 68      .. code-block:: python
 69  
 70          a=[1, 1, 4, 5, 6, 7, 7, 7, 8, 9, 10, 15, 16, 17, 24, 56, 76, 87, 87]
 71          my_boxplot=Boxplot(a, x=50, y=50) # instance the boxplot at x=50, y=50
 72          my_group = displayio.Group()
 73  
 74      Once you set up your display, you can now add ``my_boxplot`` to your display.Group() using:
 75  
 76      .. code-block:: python
 77  
 78          my_group.append(my_boxplot)
 79          display.show(my_group) # add the group to the display
 80  
 81  
 82      **Summary: Boxplot Features and input variables**
 83  
 84      The `uboxplot` TileGrid has some options for controlling its position, visible appearance,
 85      through a collection of input variables:
 86  
 87          - **position**: ``x``, ``y``
 88  
 89          - **size**: ``width`` and ``height``
 90  
 91          - **color**: ``background_color``, ``fill_color``, ``line_color``
 92  
 93          - **range**: ``xrange`` and ``yrange`` This is the range in absolute units.
 94            For example, when using (20-90), the X axis will start at 20 finishing at 90.
 95            However, the height of the graph is given by the height parameter. The scale
 96            is handled internally to provide a 1:1 experience when you update the graph.
 97  
 98  
 99      .. figure:: boxplot.jpg
100         :scale: 100 %
101         :figwidth: 50%
102         :align: center
103         :alt: Diagram of the boxplot TileGrid with the pointer in motion.
104  
105         This is a diagram of a boxplot
106  
107  
108      """
109  
110      def __init__(
111          self,
112          data: Union[list, Tuple],
113          x: int,
114          y: int,
115          height: int,
116          width: int = 20,
117          background_color: int = 0x000000,
118          fill_color: int = 0xFFFFFF,
119          line_color: int = 0xFFFFFF,
120      ) -> None:
121          self.data = np.array(data)
122          self._width = width
123          self.ynorm = np.array(
124              self.normalize(np.min(self.data), np.max(self.data), 0, height, self.data),
125              dtype=np.uint16,
126          )
127          self._whisker_width = width / 4
128          self._color_palette = displayio.Palette(4)
129          self._color_palette[0] = background_color
130          self._color_palette[1] = fill_color
131          self._color_palette[2] = line_color
132          self._color_palette[3] = 0x0000FF
133          bq3, bq2, bq1, minimum, maximum = self.find_points(self.ynorm)
134          self._bitmap = displayio.Bitmap(width + 1, height + 1, 3)
135          self._new_max = int(self.normalize(minimum, maximum, maximum, minimum, maximum))
136          self._new_q3 = int(self.normalize(minimum, maximum, maximum, minimum, bq3))
137          self._new_q2 = int(self.normalize(minimum, maximum, maximum, minimum, bq2))
138          self._new_q1 = int(self.normalize(minimum, maximum, maximum, minimum, bq1))
139          self._new_min = int(self.normalize(minimum, maximum, maximum, minimum, minimum))
140  
141          self._whiskerxs = int(self._width / 2 - self._whisker_width / 2)
142          self._whiskerse = int(self._width / 2 + self._whisker_width / 2)
143          self._middle = int(self._width / 2)
144  
145          super().__init__(self._bitmap, pixel_shader=self._color_palette, x=x, y=y)
146  
147      @staticmethod
148      def find_points(data):
149          """
150          this function finds the quartiles, minimum and maximum for the box plot
151  
152          :param data: data to be processed
153          :return: tuple with the values
154  
155          """
156          q2 = np.median(data)
157  
158          for i, element in enumerate(data):
159              if element >= q2:
160                  pos = i
161                  break
162  
163          q1 = np.median(data[0:pos])
164          q3 = np.median(data[pos:])
165          lower_whisker = np.min(data[0:pos])
166          upper_whisker = np.max(data[pos:])
167  
168          return q3, q2, q1, lower_whisker, upper_whisker
169  
170      @staticmethod
171      def normalize(oldrangemin, oldrangemax, newrangemin, newrangemax, value):
172          """
173          This function converts the original value into a new defined value in the new range
174  
175          :param oldrangemin: minimum of the original range
176          :param oldrangemax: maximum of the original range
177          :param newrangemin: minimum of the new range
178          :param newrangemax: maximum of the new range
179          :param value: value to be converted
180          :return: converted value
181  
182          """
183          return (
184              ((value - oldrangemin) * (newrangemax - newrangemin))
185              / (oldrangemax - oldrangemin)
186          ) + newrangemin
187  
188      def print_data(self) -> None:
189          """
190          This function prints the quartiles data
191  
192          :return: None
193          """
194          q3, q2, q1, lower_whisker, upper_whisker = self.find_points(self.data)
195  
196          print("q1: ", q1)
197          print("q2: ", q2)
198          print("q3: ", q3)
199          print("IQR: ", q3 - q1)
200          print("Minium: ", lower_whisker)
201          print("Maximum: ", upper_whisker)
202  
203      def draw(self) -> None:
204          """
205          This function draws the boxplot
206  
207          :return: None
208          """
209          draw_line(self._bitmap, 0, self._new_q3, self._width, self._new_q3, 2)
210          draw_line(self._bitmap, 0, self._new_q3, 0, self._new_q1, 2)
211          draw_line(self._bitmap, 0, self._new_q1, self._width, self._new_q1, 2)
212          draw_line(self._bitmap, self._width, self._new_q3, self._width, self._new_q1, 2)
213          draw_line(self._bitmap, 0, self._new_q2, self._width, self._new_q2, 2)
214          draw_line(
215              self._bitmap, self._middle, self._new_max, self._middle, self._new_q3, 2
216          )
217          draw_line(
218              self._bitmap, self._middle, self._new_min, self._middle, self._new_q1, 2
219          )
220          draw_line(
221              self._bitmap,
222              self._whiskerxs,
223              self._new_max,
224              self._whiskerse,
225              self._new_max,
226              2,
227          )
228          draw_line(
229              self._bitmap,
230              self._whiskerxs,
231              self._new_min,
232              self._whiskerse,
233              self._new_min,
234              2,
235          )