|
25 | 25 | import time |
26 | 26 | from abc import ABC, abstractmethod |
27 | 27 | from enum import IntEnum |
28 | | -from typing import Tuple |
| 28 | +from typing import Tuple, List |
29 | 29 |
|
30 | 30 | import serial |
31 | 31 | from PIL import Image, ImageDraw, ImageFont |
@@ -321,6 +321,105 @@ def DisplayProgressBar(self, x: int, y: int, width: int, height: int, min_value: |
321 | 321 |
|
322 | 322 | self.DisplayPILImage(bar_image, x, y) |
323 | 323 |
|
| 324 | + def DisplayLineGraph(self, x: int, y: int, width: int, height: int, |
| 325 | + values: List[float], |
| 326 | + min_value: int = 0, |
| 327 | + max_value: int = 100, |
| 328 | + autoscale: bool = False, |
| 329 | + line_color: Tuple[int, int, int] = (0, 0, 0), |
| 330 | + graph_axis: bool = True, |
| 331 | + axis_color: Tuple[int, int, int] = (0, 0, 0), |
| 332 | + background_color: Tuple[int, int, int] = (255, 255, 255), |
| 333 | + background_image: str = None): |
| 334 | + # Generate a plot graph and display it |
| 335 | + # Provide the background image path to display plot graph with transparent background |
| 336 | + |
| 337 | + if isinstance(line_color, str): |
| 338 | + line_color = tuple(map(int, line_color.split(', '))) |
| 339 | + |
| 340 | + if isinstance(axis_color, str): |
| 341 | + axis_color = tuple(map(int, axis_color.split(', '))) |
| 342 | + |
| 343 | + if isinstance(background_color, str): |
| 344 | + background_color = tuple(map(int, background_color.split(', '))) |
| 345 | + |
| 346 | + assert x <= self.get_width(), 'Progress bar X coordinate must be <= display width' |
| 347 | + assert y <= self.get_height(), 'Progress bar Y coordinate must be <= display height' |
| 348 | + assert x + width <= self.get_width(), 'Progress bar width exceeds display width' |
| 349 | + assert y + height <= self.get_height(), 'Progress bar height exceeds display height' |
| 350 | + |
| 351 | + if background_image is None: |
| 352 | + # A bitmap is created with solid background |
| 353 | + graph_image = Image.new('RGB', (width, height), background_color) |
| 354 | + else: |
| 355 | + # A bitmap is created from provided background image |
| 356 | + graph_image = self.open_image(background_image) |
| 357 | + |
| 358 | + # Crop bitmap to keep only the plot graph background |
| 359 | + graph_image = graph_image.crop(box=(x, y, x + width, y + height)) |
| 360 | + |
| 361 | + # if autoscale is enabled, define new min/max value to "zoom" the graph |
| 362 | + if autoscale: |
| 363 | + trueMin = max_value |
| 364 | + trueMax = min_value |
| 365 | + for value in values: |
| 366 | + if not math.isnan(value): |
| 367 | + if trueMin > value: |
| 368 | + trueMin = value |
| 369 | + if trueMax < value: |
| 370 | + trueMax = value |
| 371 | + |
| 372 | + if trueMin != max_value and trueMax != min_value: |
| 373 | + min_value = max(trueMin - 5, min_value) |
| 374 | + max_value = min(trueMax + 5, max_value) |
| 375 | + |
| 376 | + step = width / len(values) |
| 377 | + # pre compute yScale multiplier value |
| 378 | + yScale = height / (max_value - min_value) |
| 379 | + |
| 380 | + plotsX = [] |
| 381 | + plotsY = [] |
| 382 | + count = 0 |
| 383 | + for value in values: |
| 384 | + if not math.isnan(value): |
| 385 | + # Don't let the set value exceed our min or max value, this is bad :) |
| 386 | + if value < min_value: |
| 387 | + value = min_value |
| 388 | + elif max_value < value: |
| 389 | + value = max_value |
| 390 | + |
| 391 | + assert min_value <= value <= max_value, 'Plot point value shall be between min and max' |
| 392 | + |
| 393 | + plotsX.append(count * step) |
| 394 | + plotsY.append(height - (value - min_value) * yScale) |
| 395 | + |
| 396 | + count += 1 |
| 397 | + |
| 398 | + # Draw plot graph |
| 399 | + draw = ImageDraw.Draw(graph_image) |
| 400 | + draw.line(list(zip(plotsX, plotsY)), fill=line_color, width=2) |
| 401 | + |
| 402 | + if graph_axis: |
| 403 | + # Draw axis |
| 404 | + draw.line([0, height - 1, width - 1, height - 1], fill=axis_color) |
| 405 | + draw.line([0, 0, 0, height - 1], fill=axis_color) |
| 406 | + |
| 407 | + # Draw Legend |
| 408 | + draw.line([0, 0, 1, 0], fill=axis_color) |
| 409 | + text = f"{int(max_value)}" |
| 410 | + font = ImageFont.truetype("./res/fonts/" + "roboto/Roboto-Black.ttf", 10) |
| 411 | + left, top, right, bottom = font.getbbox(text) |
| 412 | + draw.text((2, 0 - top), text, |
| 413 | + font=font, fill=axis_color) |
| 414 | + |
| 415 | + text = f"{int(min_value)}" |
| 416 | + font = ImageFont.truetype("./res/fonts/" + "roboto/Roboto-Black.ttf", 10) |
| 417 | + left, top, right, bottom = font.getbbox(text) |
| 418 | + draw.text((width - 1 - right, height - 2 - bottom), text, |
| 419 | + font=font, fill=axis_color) |
| 420 | + |
| 421 | + self.DisplayPILImage(graph_image, x, y) |
| 422 | + |
324 | 423 | def DisplayRadialProgressBar(self, xc: int, yc: int, radius: int, bar_width: int, |
325 | 424 | min_value: int = 0, |
326 | 425 | max_value: int = 100, |
|
0 commit comments