coderfrombiz
coderfrombiz

Reputation: 137

How to implement charts in Python without libraries such as Matplotlib or PyQtChart?

I'm starting a programming project as a part of my studies. My project will be a data visualization program that must be able to at least visualize or "draw" a line chart and possibly other charts such as a bar or a pie chart. However I'm not allowed to use Python libraries that make this easy (such as Matplotlib, QtDesigner, NumPy or PyQtChart). The visualization should be done with PyQt.

The project seems really hard to me as I'm quite new with programming. I was wondering if anyone has experience with similar programming projects or tips to help me approach the project. Thanks in advance!

Also, if this is not a relevant question for StackOverflow, let me know and I'll take it down.

Upvotes: 0

Views: 2600

Answers (2)

John
John

Reputation: 222

Thanks musicamante!

This saved me reimplementing my Gtk plotter to Qt. But I had trouble running this with complains that:

QRectF/QRect(int, int, int, int): argument has unexpected type 'float'

After changing the QRectF/QRect lines to:

bar = QtCore.QRect(*[int(x), int(columnHeight - height), int(columnWidth), int(height)])

labelRect = QtCore.QRect(int(x), int(columnHeight), int(columnSpace), int(labelHeight))

The code worked perfectly. In fact I run the code just to see all the pretty colours and random rectangles! :-)

Upvotes: 0

musicamante
musicamante

Reputation: 48260

Considering the constraints, the only remaining solutions are using custom widget painting through QPainter, or using the Graphics View Framework, which is more powerful but also much more complex.

In the following two simple examples you can see some differences between the two approaches, which will show the same data.

QWidget subclassing is normally much more simpler since you can directly draw basic shapes (including ellipses, arcs and pies). You just have to implement the paintEvent (remember, QPainter on a widget can only happen in a paint event! You cannot call paintEvent() on your own) construct a QPainter on the widget and then begin drawing.

This simplicity has the drawback that if you need higher modularity and better control it will probably result more cumbersome to modify.
The Graphics View (which is exactly what QtCharts uses) allows higher extensibility as it provides an object oriented framework (using QGraphicsItem subclasses) and, as such, allows manipulation of existing data, so you can add/remove bars/pies/etc more easily; unfortunately, it is as powerful as hard to get along with, and it requires some long studying and experimenting in order to begin to really grasp it.

from PyQt5 import QtCore, QtGui, QtWidgets
from random import random, randrange

class ChartWidget(QtWidgets.QWidget):
    def __init__(self, data):
        super().__init__()
        self.data = data
        self.setMinimumSize(480, 320)

    def paintEvent(self, event):
        if not self.data:
            return
        columnSpace = self.width() / len(self.data)
        columnWidth = columnSpace * .6
        qp = QtGui.QPainter(self)
        labelHeight = self.fontMetrics().height()
        columnHeight = self.height() - labelHeight
        for i, (value, color) in enumerate(self.data):
            height = columnHeight * value
            x = i * columnSpace
            bar = QtCore.QRect(x, columnHeight - height, columnWidth, height)
            qp.setBrush(color)
            qp.drawRect(bar)
            labelRect = QtCore.QRect(x, columnHeight, columnSpace, labelHeight)
            qp.drawText(labelRect, QtCore.Qt.AlignLeft, 'Value {}'.format(i + 1))


class ChartView(QtWidgets.QGraphicsView):
    def __init__(self, data):
        super().__init__()
        scene = QtWidgets.QGraphicsScene()
        self.setScene(scene)
        self.setRenderHint(QtGui.QPainter.Antialiasing)

        pen = QtGui.QPen(QtCore.Qt.black, 1)
        pen.setCosmetic(True)
        for i, (value, color) in enumerate(data):
            # for simplicity, I created rectangles from the *bottom*
            bar = scene.addRect(QtCore.QRectF(0, 0, 600, -value * 1000))
            bar.setX(i * 1000)
            bar.setPen(pen)
            bar.setBrush(color)
            label = scene.addSimpleText('Value {}'.format(i + 1))
            label.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresTransformations)
            label.setX(i * 1000)

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.fitInView(self.sceneRect().adjusted(-10, -10, 10, 10))


import sys
app = QtWidgets.QApplication(sys.argv)

data = []
for i in range(randrange(5, 10)):
    data.append((
        random(), 
        QtGui.QColor(randrange(255), randrange(255), randrange(255))))

test = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(test)
layout.addWidget(ChartWidget(data))
layout.addWidget(ChartView(data))

test.show()
sys.exit(app.exec_())

Some considerations:

  • widgets can have children; you can create a basic widget class for a single "bar", then add them to another widget with a layout;
  • pie graphs are a bit more trickier with the above implementation; also note that QPainter uses units of 1/16th of a degree for angles;
  • similarly to creating a QWidget class for bars, you can create a QGraphicsRectItem subclass for graphics view instead of using the convenience functions addRect() as above;
  • remember that all graphics items initially have 0x0 coordinates, this also includes creating a rectangle at (100, 100), which is a rectangle item positioned at 0x0, but that shows the rectangle at 100x100 relative to the position;

Upvotes: 3

Related Questions