Search code examples
pythonpython-3.xpyside2qtchartsqchart

PySide2 How to place a detached legend to another widget?


Recently, I've found that my QChart legend can be detached and placed into another, independent widget. Qt documentation says that a QLegend class method QLegend::detachFromChart() may separate legend from the chart.

Unfortunately, when I try to add legend to another widget layout, I have the following error:

Traceback (most recent call last):
  File "/home/artem/.local/lib/python3.6/site-packages/shiboken2/files.dir/shibokensupport/signature/loader.py", line 111, in seterror_argument
    return errorhandler.seterror_argument(args, func_name)
  File "/home/artem/.local/lib/python3.6/site-packages/shiboken2/files.dir/shibokensupport/signature/errorhandler.py", line 97, in seterror_argument
    update_mapping()
  File "/home/artem/.local/lib/python3.6/site-packages/shiboken2/files.dir/shibokensupport/signature/mapping.py", line 240, in update
    top = __import__(mod_name)
  File "/home/artem/.local/lib/python3.6/site-packages/numpy/__init__.py", line 142, in <module>
    from . import core
  File "/home/artem/.local/lib/python3.6/site-packages/numpy/core/__init__.py", line 67, in <module>
    raise ImportError(msg.format(path))
ImportError: Something is wrong with the numpy installation. While importing we detected an older version of numpy in ['/home/artem/.local/lib/python3.6/site-packages/numpy']. One method of fixing this is to repeatedly uninstall numpy until none is found, then reinstall this version.
Fatal Python error: seterror_argument did not receive a result

Current thread 0x00007efc67a96740 (most recent call first):
  File "/home/artem/\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b/Projects/QtChartsExamples/test/main.py", line 20 in <module>

Here's a quick example:

from PySide2 import QtGui, QtWidgets, QtCore
from PySide2.QtCharts import QtCharts
from psutil import cpu_percent, cpu_count
import sys
import random


class cpu_chart(QtCharts.QChart):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.legend().setAlignment(QtCore.Qt.AlignLeft)
        self.legend().setContentsMargins(0.0, 0.0, 5.0, 0.0)
        self.legend().setMarkerShape(QtCharts.QLegend.MarkerShapeCircle)
        self.legend().detachFromChart()

        self.axisX = QtCharts.QValueAxis()
        self.axisY = QtCharts.QValueAxis()

        self.axisX.setVisible(False)

        self.x = 0
        self.y = 0

        self.percent = cpu_percent(percpu=True)

        for i in range(cpu_count()):
            core_series = QtCharts.QSplineSeries()
            core_series.setName(f"CPU {i+1}: {self.percent[i]: .1f} %")

            colour = [random.randrange(0, 255),
                      random.randrange(0, 255),
                      random.randrange(0, 255)]

            pen = QtGui.QPen(QtGui.QColor(colour[0],
                                          colour[1],
                                          colour[2])
                             )
            pen.setWidth(1)

            core_series.setPen(pen)
            core_series.append(self.x, self.y)

            self.addSeries(core_series)

        self.addAxis(self.axisX, QtCore.Qt.AlignBottom)
        self.addAxis(self.axisY, QtCore.Qt.AlignLeft)

        for i in self.series():
            i.attachAxis(self.axisX)
            i.attachAxis(self.axisY)

        self.axisX.setRange(0, 100)
        self.axisY.setTickCount(5)
        self.axisY.setRange(0, 100)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = QtWidgets.QMainWindow()

    chart = cpu_chart()

    chart.setAnimationOptions(QtCharts.QChart.SeriesAnimations)

    chart_view = QtCharts.QChartView(chart)
    chart_view.setRenderHint(QtGui.QPainter.Antialiasing)

    container = QtWidgets.QWidget()
    hbox = QtWidgets.QHBoxLayout()
    hbox.addWidget(chart.legend())
    hbox.addWidget(chart_view)
    container.setLayout(hbox)

    window.setCentralWidget(container)
    window.resize(400, 300)
    window.show()

    sys.exit(app.exec_())

It would be interesting to figure out what's wrong with this. Could I really do it this way?


Solution

  • I do not get the message you indicate but the following:

    Traceback (most recent call last):
      File "main.py", line 70, in <module>
        hbox.addWidget(chart.legend())
    TypeError: 'PySide2.QtWidgets.QBoxLayout.addWidget' called with wrong argument types:
      PySide2.QtWidgets.QBoxLayout.addWidget(QLegend)
    Supported signatures:
      PySide2.QtWidgets.QBoxLayout.addWidget(PySide2.QtWidgets.QWidget, int = 0, PySide2.QtCore.Qt.Alignment = Default(Qt.Alignment))
      PySide2.QtWidgets.QBoxLayout.addWidget(PySide2.QtWidgets.QWidget)
    

    which makes more sense since QLegend is a QGraphicsWidget that is not a QWidget so you cannot place it in layout. So one possible solution is to use a QGraphicsView:

    import sys
    import random
    
    from PySide2 import QtGui, QtWidgets, QtCore
    from PySide2.QtCharts import QtCharts
    from psutil import cpu_percent, cpu_count
    
    
    class cpu_chart(QtCharts.QChart):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.legend().setAlignment(QtCore.Qt.AlignLeft)
            self.legend().setContentsMargins(0.0, 0.0, 5.0, 0.0)
            self.legend().setMarkerShape(QtCharts.QLegend.MarkerShapeCircle)
            self.legend().detachFromChart()
    
            self.axisX = QtCharts.QValueAxis()
            self.axisY = QtCharts.QValueAxis()
    
            self.axisX.setVisible(False)
    
            self.addAxis(self.axisX, QtCore.Qt.AlignBottom)
            self.addAxis(self.axisY, QtCore.Qt.AlignLeft)
            self.axisX.setRange(0, 100)
            self.axisY.setTickCount(5)
            self.axisY.setRange(0, 100)
    
            self.percent = cpu_percent(percpu=True)
    
            for i in range(cpu_count()):
                core_series = QtCharts.QSplineSeries()
                core_series.setName(f"CPU {i+1}: {self.percent[i]: .1f} %")
    
                colour = random.sample(range(255), 3)
    
                pen = QtGui.QPen(QtGui.QColor(*colour))
                pen.setWidth(1)
    
                core_series.setPen(pen)
                core_series.attachAxis(self.axisX)
                core_series.attachAxis(self.axisY)
                for i in range(100):
                    core_series.append(i, random.uniform(10, 90))
    
                self.addSeries(core_series)
    
    
    class LegendWidget(QtWidgets.QGraphicsView):
        def __init__(self, legend, parent=None):
            super().__init__(parent)
            self.m_legend = legend
    
            scene = QtWidgets.QGraphicsScene(self)
            self.setScene(scene)
            scene.addItem(self.m_legend)
    
        def resizeEvent(self, event):
            if isinstance(self.m_legend, QtCharts.QLegend):
                self.m_legend.setMinimumSize(self.size())
            super().resizeEvent(event)
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        window = QtWidgets.QMainWindow()
    
        chart = cpu_chart()
    
        chart.setAnimationOptions(QtCharts.QChart.SeriesAnimations)
    
        chart_view = QtCharts.QChartView(chart)
        chart_view.setRenderHint(QtGui.QPainter.Antialiasing)
    
        container = QtWidgets.QWidget()
        hbox = QtWidgets.QHBoxLayout()
    
        legend_widget = LegendWidget(chart.legend())
    
        hbox.addWidget(legend_widget)
        hbox.addWidget(chart_view)
        container.setLayout(hbox)
    
        window.setCentralWidget(container)
        window.resize(400, 300)
        window.show()
    
        sys.exit(app.exec_())
    

    enter image description here

    As you can see, it also doesn't work well, so instead of wanting to move the QLegend you can use a QListWidget with a QIcon:

    import sys
    import random
    from functools import partial
    
    from PySide2 import QtGui, QtWidgets, QtCore
    from PySide2.QtCharts import QtCharts
    from psutil import cpu_percent, cpu_count
    
    
    class cpu_chart(QtCharts.QChart):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.legend().setAlignment(QtCore.Qt.AlignLeft)
            self.legend().setContentsMargins(0.0, 0.0, 5.0, 0.0)
            self.legend().setMarkerShape(QtCharts.QLegend.MarkerShapeCircle)
            self.legend().detachFromChart()
            self.legend().hide()
    
            self.axisX = QtCharts.QValueAxis()
            self.axisY = QtCharts.QValueAxis()
    
            self.axisX.setVisible(False)
    
            self.addAxis(self.axisX, QtCore.Qt.AlignBottom)
            self.addAxis(self.axisY, QtCore.Qt.AlignLeft)
            self.axisX.setRange(0, 100)
            self.axisY.setTickCount(5)
            self.axisY.setRange(0, 100)
    
            self.x = 0
    
            # percent = cpu_percent(percpu=True)
    
            for i in range(cpu_count()):
                core_series = QtCharts.QSplineSeries()
                colour = random.sample(range(255), 3)
                pen = QtGui.QPen(QtGui.QColor(*colour))
                pen.setWidth(1)
                core_series.setPen(pen)
                self.addSeries(core_series)
                core_series.attachAxis(self.axisX)
                core_series.attachAxis(self.axisY)
    
            timer = QtCore.QTimer(self, timeout=self.onTimeout, interval=100)
            timer.start()
    
        @QtCore.Slot()
        def onTimeout(self):
            percent = cpu_percent(percpu=True)
            for i, (serie, value) in enumerate(zip(self.series(), percent)):
                serie.append(self.x, value)
                serie.setName(f"CPU {i+1}: {value: .1f} %")
    
            self.axisX.setRange(max(0, self.x - 100), max(100, self.x))
            self.x += 1
    
    
    def create_icon(pen):
        pixmap = QtGui.QPixmap(512, 512)
        pixmap.fill(QtCore.Qt.transparent)
        painter = QtGui.QPainter(pixmap)
        painter.setBrush(pen.brush())
        painter.drawEllipse(pixmap.rect().adjusted(50, 50, -50, -50))
        painter.end()
        icon = QtGui.QIcon(pixmap)
        return icon
    
    
    class LegendWidget(QtWidgets.QListWidget):
        def __init__(self, series, parent=None):
            super().__init__(parent)
            self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
            self.setSeries(series)
            self.horizontalScrollBar().hide()
    
        def setSeries(self, series):
            self.clear()
            for i, serie in enumerate(series):
                it = QtWidgets.QListWidgetItem()
                it.setIcon(create_icon(serie.pen()))
                self.addItem(it)
                wrapper = partial(self.onNameChanged, serie, i)
                serie.nameChanged.connect(wrapper)
                wrapper()
    
        def onNameChanged(self, serie, i):
            it = self.item(i)
            it.setText(serie.name())
            self.setFixedWidth(self.sizeHintForColumn(0))
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        window = QtWidgets.QMainWindow()
    
        chart = cpu_chart()
    
        chart.setAnimationOptions(QtCharts.QChart.SeriesAnimations)
    
        chart_view = QtCharts.QChartView(chart)
        chart_view.setRenderHint(QtGui.QPainter.Antialiasing)
    
        container = QtWidgets.QWidget()
        hbox = QtWidgets.QHBoxLayout()
    
        legend_widget = LegendWidget(chart.series())
    
        hbox.addWidget(legend_widget)
        hbox.addWidget(chart_view)
        container.setLayout(hbox)
    
        window.setCentralWidget(container)
        window.resize(1280, 480)
        window.show()
    
        sys.exit(app.exec_())
    

    enter image description here