I need to display more rows like this (in the direction of the red arrow), with more of those squares. Along with the squares I could also use other shapes of various colours, and the objects need to be placed in very specific coordinates. When I hover over any of those objects, it has to show a tooltip relevant to that object. The text shown, would be rendered as a graphical object, and not as text that can be selected with a mouse pointer. Basically, everything is fully graphically rendered.
As more data is generated, more rows will be added and it goes on infinitely. Obviously a scrollbar is needed.
So far, from libraries like Matplotlib, I've known only options of creating a graphical screen of fixed size. I considered PyQt widgets, but it doesn't seem to have the desired functionality.
I considered HTML (since adding new rows infinitely is easy) and JavaScript, but it's too cumbersome to export the data from Python and load and parse it in JavaScript.
Is there any way to do such a display in Python? Demonstrating one way of achieving this objective effectively would suffice.
Purpose: I'm creating this to visualize how certain foods and sleep loss lead to health issues. Displaying it like this allows me to see patterns across weeks or months. It's not just about plotting points and text, it's also about being able to dynamically update their color and size on clicking any of the graphical elements.
I came up with a basic example, based on musicamante's suggestion.
import sys
import random
import string
import datetime
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QLabel
from PyQt5.QtGui import QPainter, QColor
from PyQt5.QtCore import Qt, QTimer
class Constants:
MAIN_WINDOW_WIDTH = 900
MAIN_WINDOW_HEIGHT = 600
TOOLTIP_DURATION_MSEC = 3000
SHAPE_RECTANGLE = 'Rectangle'
SHAPE_ELLIPSE = 'Ellipse'
class Markers(QWidget):
colors = {} # Class-level variable to store colors for each instance
shapes = {}
def __init__(self):
super().__init__()
self.init_color()
self.color = Markers.colors[self]
self.shapes = Markers.shapes[self]
def init_color(self):
if self not in Markers.colors:
Markers.colors[self] = self.generate_random_color()
Markers.shapes[self] = self.generate_random_shape()
def paintEvent(self, event):
painter = QPainter(self)
self.draw_random_shapes(painter)
def draw_random_shapes(self, painter):
colors = [QColor(255, 0, 0), QColor(0, 255, 0), QColor(0, 0, 255)]
for _ in range(random.randint(1, 5)):
painter.setBrush(self.color)
x = 0; y = 0;
if self.shapes == Constants.SHAPE_RECTANGLE:
#width = random.randint(10, 40); height = random.randint(10, 40)
width = 5; height = width
painter.drawRect(x, y, width, height)
elif self.shapes == Constants.SHAPE_ELLIPSE:
radiusWidth = 5; radiusHeight = radiusWidth
painter.drawEllipse(x, y, radiusWidth, radiusHeight)
def generate_random_color(self):
return QColor(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
def generate_random_shape(self):
return random.choice([Constants.SHAPE_ELLIPSE, Constants.SHAPE_RECTANGLE])
class OneRowData(QWidget):
def __init__(self, date, parent=None):
super().__init__(parent)
self.date = date
self.initUI()
def initUI(self):
layout = QHBoxLayout(self)
dateLabel = QLabel(self.date.strftime("%Y-%m-%d %H:%M:%S"))
layout.addWidget(dateLabel, Qt.AlignLeft)
dots = Markers()
toolstring = ''.join(random.choices(string.ascii_lowercase, k=10))
dots.setToolTip(toolstring) #hovering over any of the squares or circles for a while will make the tooltip show
dots.setToolTipDuration(Constants.TOOLTIP_DURATION_MSEC)
layout.addWidget(dots, Qt.AlignLeft)
#https://doc.qt.io/qtforpython-5/PySide2/QtGui/QPainter.html
def generateRandomColor(self):
return '#{0:06x}'.format(random.randint(0, 0xFFFFFF)) # Random color in hexadecimal
class Header(QWidget):
def __init__(self, date, parent=None):
super().__init__(parent)
self.date = date
self.initUI()
def initUI(self):
layout = QHBoxLayout(self)
TIME_HEADER_OFFSET = " "
headerString = TIME_HEADER_OFFSET + self.getTimeHeaderString()
dateLabel = QLabel(headerString)
layout.addWidget(dateLabel, Qt.AlignLeft)
dots = Markers()
def getTimeHeaderString(self):
TIME_HEADER_GAPS = " "
timeHeader = ["00:00", "01:00", "02:00", "03:00", "04:00", "05:00", "06:00", "07:00", "08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "017:00", "18:00", "19:00", "20:00", "21:00", "22:00", "23:00", "00:00"]
return "".join(x + TIME_HEADER_GAPS for x in timeHeader) #concatenates all elements in the list
class InfiniteScroll(QScrollArea):
def __init__(self):
super().__init__()
self.setWidgetResizable(True)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.content = QWidget()
self.layout = QVBoxLayout(self.content)
self.layout.setAlignment(Qt.AlignTop)
self.layout.setSpacing(1)
self.setWidget(self.content)
self.timer = QTimer(self)
self.timer.timeout.connect(self.addItem)
self.timer.start(500) # Add an item at intervals
date = datetime.datetime.now()
item2 = Header(date)
self.layout.addWidget(item2)
def addItem(self):
date = datetime.datetime.now()
item = OneRowData(date)
self.layout.addWidget(item)
def main():
app = QApplication(sys.argv)
scroll = InfiniteScroll()
scroll.resize(Constants.MAIN_WINDOW_WIDTH, Constants.MAIN_WINDOW_HEIGHT) #set initial size of the main window
scroll.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()