Repository: pyqtgraph/pyqtgraph

Issue:
key error when setAxisItems in PlotItem
<!-- In the following, please describe your issue in detail! -->
When I call the 
```python
plotItem = pg.PlotItem(viewBox=vb, name=name, axisItems={'bottom': self.axisTime})
```
I got this error:
```python
  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:
```python
        # 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)
```
<!-- If some of the sections do not apply, just remove them. -->

### Short description
<!-- This should summarize the issue. -->

### Code to reproduce
<!-- Please provide a minimal working example that reproduces the issue in the code block below.
     Ideally, this should be a full example someone else could run without additional setup. -->
```python
import pyqtgraph as pg
import numpy as np
```

### Expected behavior
<!-- What should happen? -->

### Real behavior
<!-- What happens? -->

```
An error occurred?
Post the full traceback inside these 'code fences'!
```

### Tested environment(s)

 * PyQtGraph version: <!-- output of pyqtgraph.__version__ -->
 * Qt Python binding: <!-- output of pyqtgraph.Qt.VERSION_INFO -->
 * Python version:  python3.8
 * NumPy version: <!-- output of numpy.__version__ -->
 * Operating system:  ubuntu
 * Installation method: <!-- e.g. pip, conda, system packages, ... -->

### Additional context



Discussion:
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?
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_())
```
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? 
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 `PlotItem`s that are not part of the same scene [[ref]](https://github.com/pyqtgraph/pyqtgraph/pull/1154#issuecomment-616234808). 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.
That's fine, thank you!

Resolving patch:
diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py
--- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py
+++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py
@@ -307,7 +307,7 @@ def setAxisItems(self, axisItems=None):
         # 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(axisItems.keys()) # Note that it does not matter that this adds
+        visibleAxes.extend(axisItems.keys()) # 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))):
@@ -325,8 +325,11 @@ def setAxisItems(self, axisItems=None):
             if k in axisItems:
                 axis = axisItems[k]
                 if axis.scene() is not None:
-                    if axis != self.axes[k]["item"]:
-                        raise RuntimeError("Can't add an axis to multiple plots.")
+                    if k not in self.axes or axis != self.axes[k]["item"]:
+                        raise RuntimeError(
+                            "Can't add an axis to multiple plots. Shared axes"
+                            " can be achieved with multiple AxisItem instances"
+                            " and set[X/Y]Link.")
             else:
                 axis = AxisItem(orientation=k, parent=self)
             


Test patch:
diff --git a/pyqtgraph/graphicsItems/PlotItem/tests/__init__.py b/pyqtgraph/graphicsItems/PlotItem/tests/__init__.py
new file mode 100644
diff --git a/pyqtgraph/graphicsItems/PlotItem/tests/test_PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/tests/test_PlotItem.py
new file mode 100644
--- /dev/null
+++ b/pyqtgraph/graphicsItems/PlotItem/tests/test_PlotItem.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+import pytest
+import pyqtgraph as pg
+
+app = pg.mkQApp()
+
+
+@pytest.mark.parametrize('orientation', ['left', 'right', 'top', 'bottom'])
+def test_PlotItem_shared_axis_items(orientation):
+    """Adding an AxisItem to multiple plots raises RuntimeError"""
+    ax1 = pg.AxisItem(orientation)
+    ax2 = pg.AxisItem(orientation)
+
+    layout = pg.GraphicsLayoutWidget()
+
+    pi1 = layout.addPlot(axisItems={orientation: ax1})
+
+    pi2 = layout.addPlot()
+    # left or bottom replaces, right or top adds new
+    pi2.setAxisItems({orientation: ax2})
+
+    with pytest.raises(RuntimeError):
+        pi2.setAxisItems({orientation: ax1})