/ 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 )