Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

key error when setAxisItems in PlotItem #1358

Closed
zhangr011 opened this issue Sep 8, 2020 · 5 comments · Fixed by #1376
Closed

key error when setAxisItems in PlotItem #1358

zhangr011 opened this issue Sep 8, 2020 · 5 comments · Fixed by #1376
Labels

Comments

@zhangr011
Copy link

When I call the

plotItem = pg.PlotItem(viewBox=vb, name=name, axisItems={'bottom': self.axisTime})

I got this error:

  File "/home/zhangr/anaconda3/envs/py38/lib/python3.8/site-packages/pyqtgraph/graphicsItems/PlotItem/PlotItem.py", line 159, in __init__
    self.setAxisItems(axisItems)
  File "/home/zhangr/anaconda3/envs/py38/lib/python3.8/site-packages/pyqtgraph/graphicsItems/PlotItem/PlotItem.py", line 329, in setAxisItems
    if axis != self.axes[k]["item"]:
KeyError: 'bottom'

There are maybe two bugs in this function, this is one solution:

        # Array containing visible axis items
        # Also containing potentially hidden axes, but they are not touched so it does not matter
        visibleAxes = ['left', 'bottom']
        # visibleAxes.append(key)  key is dict_key, cannot append into a list
        for key in axisItems.keys():
            visibleAxes.append(key) # Note that it does not matter that this adds
                                    # some values to visibleAxes a second time

        for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))):
            if k in self.axes:
                if k not in axisItems:
                    continue # Nothing to do here

                # Remove old axis
                oldAxis = self.axes[k]['item']
                self.layout.removeItem(oldAxis)
                oldAxis.scene().removeItem(oldAxis)
                oldAxis.unlinkFromView()

            # Create new axis
            if k in axisItems:
                axis = axisItems[k]
                if axis.scene() is not None:
                    # check self.axes's key at first
                    if self.axes.get(k) and axis != self.axes[k]["item"]:
                        raise RuntimeError("Can't add an axis to multiple plots.")
            else:
                axis = AxisItem(orientation=k, parent=self)

Short description

Code to reproduce

import pyqtgraph as pg
import numpy as np

Expected behavior

Real behavior

An error occurred?
Post the full traceback inside these 'code fences'!

Tested environment(s)

  • PyQtGraph version:
  • Qt Python binding:
  • Python version: python3.8
  • NumPy version:
  • Operating system: ubuntu
  • Installation method:

Additional context

@ixjlyons
Copy link
Member

Agreed, the visibleAxes.append should be visibleAxes.extend. I think the other change makes sense as well, but I'm not sure I see how exactly you're reaching the code path that throws the KeyError. My understanding is this would be if you've given the AxisItem a parent, which would probably raise that RuntimeError anyway (though a specific exception would be better than a key error). Would you mind describing a little more about how you're constructing things or giving a standalone example that throws this error?

@ixjlyons ixjlyons added the bug label Sep 17, 2020
@zhangr011
Copy link
Author

Here is the example. The first create_plot_item is just worked, but the second one will cause the key error.

# encoding: UTF-8

from collections import deque, OrderedDict
from qtpy import QtGui, QtCore, QtWidgets
import pyqtgraph as pg
import numpy as np
import sys


#----------------------------------------------------------------------
class CustomViewBox(pg.ViewBox):

    def __init__(self, *args, **kwds):
        pg.ViewBox.__init__(self, *args, **kwds)

    def mouseClickEvent(self, ev):
        if ev.button() == QtCore.Qt.RightButton:
            self.autoRange()


#----------------------------------------------------------------------
class MyStringAxis(pg.AxisItem):
    """"""

    def __init__(self, xdict, *args, **kwargs):
        pg.AxisItem.__init__(self, *args, **kwargs)
        self.minVal = 0
        self.maxVal = 0
        # sequence <=> time_sequence
        self.xdict = OrderedDict()
        self.xdict.update(xdict)
        # time_sequence <=> sequence
        self.tdict = OrderedDict([(v, k) for k, v in xdict.items()])
        self.x_values = np.asarray(xdict.keys())
        self.x_strings = list(xdict.values())
        self.setPen(color=(255, 255, 255, 255), width=0.8)
        self.setStyle(tickFont=QtGui.QFont("Roman times", 10, QtGui.QFont.Bold), autoExpandTextSpace=True)

    def update_xdict(self, xdict):
        """
        更新坐标映射表
        :param xdict:
        :return:
        """
        # 更新 x轴-时间映射
        self.xdict.update(xdict)
        # 更新 时间-x轴映射
        tdict = dict([(v, k) for k, v in xdict.items()])
        self.tdict.update(tdict)

        # 重新生成x轴队列和时间字符串显示队列
        self.x_values = np.asarray(self.xdict.keys())
        self.x_strings = list(self.xdict.values())

    def get_x_by_time(self, t_value):
        """
        通过 时间,找到匹配或最接近x轴
        :param t_value: datetime 类型时间
        :return:
        """
        last_time = None
        for t in self.x_strings:
            if t >= t_value:
                last_time = t
                break

        x = self.tdict.get(last_time, 0)
        return x

    def tickStrings(self, values, scale, spacing):
        """
        将原始横坐标转换为时间字符串,第一个坐标包含日期
        :param values:
        :param scale:
        :param spacing:
        :return:
        """
        strings = []
        for v in values:
            vs = v * scale
            if vs in self.x_values:
                vstr = self.x_strings[np.abs(self.x_values - vs).argmin()]
                vstr = vstr.strftime('%Y-%m-%d %H:%M:%S')
            else:
                vstr = ""
            strings.append(vstr)
        return strings


#----------------------------------------------------------------------
class KLineWidget(QtWidgets.QWidget):

    clsId = 0

    def __init__(self, parent=None, **kargs):
        """"""
        super(KLineWidget, self).__init__(parent)
        self.clsId += 1
        self.windowId = str(KLineWidget.clsId)
        self.initUi()

    #----------------------------------------------------------------------
    def initUi(self):
        """
        -----------------
        |     main      |
        |               |
        -----------------
        |    volume     |
        -----------------
        |     sub       |
        -----------------

        """
        self.pw = pg.PlotWidget()
        # the layout
        self.lay_KL = pg.GraphicsLayout(border=(100, 100, 100))
        self.lay_KL.setContentsMargins(5, 5, 5, 5)
        self.lay_KL.setSpacing(0)
        self.lay_KL.setBorder(color=(100, 100, 100, 250), width=0.4)
        self.lay_KL.setZValue(0)
        self.KLtitle = self.lay_KL.addLabel(u'')
        self.pw.setCentralItem(self.lay_KL)
        # the x axis
        xdict = {}
        self.axisTime = MyStringAxis(xdict, orientation='bottom')

        self.init_plot_main()
        self.init_plot_volume()
        self.init_plot_sub()

    def create_plot_item(self, name):
        """generate the PlotItem"""
        vb = CustomViewBox()
        plotItem = pg.PlotItem(viewBox=vb, name=name, axisItems={'bottom': self.axisTime})
        plotItem.setMenuEnabled(False)
        plotItem.setClipToView(True)
        plotItem.hideAxis('left')
        plotItem.showAxis('right')
        plotItem.setDownsampling(mode='peak')
        plotItem.setRange(xRange=(0, 1), yRange=(0, 1))
        plotItem.getAxis('right').setWidth(60)
        plotItem.getAxis('right').setStyle(tickFont=QtGui.QFont("Roman times", 10, QtGui.QFont.Bold))
        plotItem.getAxis('right').setPen(color=(255, 255, 255, 255), width=0.8)
        plotItem.showGrid(True, True)
        plotItem.hideButtons()
        return plotItem

    #----------------------------------------------------------------------
    def init_plot_main(self):
        """main widget"""
        self.pi_main = self.create_plot_item('_'.join([self.windowId, 'Plot_Main']))
        self.pi_main.setMinimumHeight(200)
        self.pi_main.setXLink('_'.join([self.windowId, 'Plot_Sub']))
        self.pi_main.hideAxis('bottom')
        self.lay_KL.nextRow()
        self.lay_KL.addItem(self.pi_main)

    #----------------------------------------------------------------------
    def init_plot_volume(self):
        """volumn widget"""
        self.pi_volume = self.create_plot_item('_'.join([self.windowId, 'Plot_Volume']))
        self.pi_volume.setMaximumHeight(150)
        self.pi_volume.setXLink('_'.join([self.windowId, 'Plot_Sub']))
        self.pi_volume.hideAxis('bottom')
        self.lay_KL.nextRow()
        self.lay_KL.addItem(self.pi_volume)

    #----------------------------------------------------------------------
    def init_plot_sub(self):
        """sub widget"""
        self.pi_sub = self.create_plot_item('_'.join([self.windowId, 'Plot_Sub']))
        self.lay_KL.nextRow()
        self.lay_KL.addItem(self.pi_sub)


if __name__ == '__main__':
    qapp = QtWidgets.QApplication([])
    window = KLineWidget()
    window.showMaximized()

    sys.exit(qapp.exec_())

@zhangr011
Copy link
Author

Perhaps I have catched the error. the 'self.pi_sub' is defined too late, which caused the setXLink failed.
But why was the first call init_plot_main passed?

@ixjlyons
Copy link
Member

I actually think your example should be possible for the most part. It was definitely possible in the past (before #1154) to achieve a shared axis between multiple plot items, so long as they belong to the same scene.

I found that the check throwing the exception was due to a possibility of this really failing if you try to add a single AxisItem to multiple PlotItems that are not part of the same scene [ref]. I spent a little time trying to figure out a way to handle all the cases, but it's proving somewhat difficult.

Your example already uses the alternative approach to a "shared axis", though, which is to setXLink and hide all but one. In this case, you don't need to use the same AxisItem instance in all three plots, just create a new AxisItem for each one. Your example should then work without fixing pyqtgraph itself for now.

So, I think the action here is to switch append to extend and make the check adding an axis item to multiple plots more robust. It might also be nice to add to the message that you can use separate axis items and use set[X/Y]Link.

@zhangr011
Copy link
Author

That's fine, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants