diff --git a/README.md b/README.md
index 6b5b8ab29ed9b84ff0a1b0637a6e9d588de82a70..2613e7230d8aa558129442f34347fb1daa266d41 100644
--- a/README.md
+++ b/README.md
@@ -7,5 +7,6 @@ This project consists of Jupyter Notebook definitions used by RWTH Aachen Univer
 - Create conda environment `conda env create -f environment.yml`
 - Activate environment `conda activate rwthlab`
 - Install Jupyterlab extensions with `jupyter labextension install @jupyter-widgets/jupyterlab-manager jupyter-matplotlib jupyterlab-rwth`
-
-For developers: Install `rwth_nb` with `python ./setup.sh develop`
+- Install `rwth_nb`:
+    - with `pip install git+https://git.rwth-aachen.de/jupyter/rwth-nb.git` or
+    - with `python ./setup.sh develop` (for developers)
diff --git a/rwth_nb/plots/mpl_decorations.py b/rwth_nb/plots/mpl_decorations.py
index 1fe4bfafe668051337b02d1fe55d3425594466bb..73ef80f62bccb8f9e2323174012c24b5678e4e3a 100644
--- a/rwth_nb/plots/mpl_decorations.py
+++ b/rwth_nb/plots/mpl_decorations.py
@@ -57,56 +57,42 @@ def axis(ax):
     ax.spines['right'].set_color('none')
     ax.spines['top'].set_color('none')
 
-    def on_xlims_change(axes=[]):
+    def on_xlims_change(ax):
         # update x spine position to be always at zero, left or right according to set limits
         # update y-label x-position and horizontal alignment
-        if ax.get_xscale() != 'log':  # most likely linear scale
-            is_lim_all_negative = (np.array(ax.get_xlim()) < 0).all()
-            is_lim_all_positive = (np.array(ax.get_xlim()) > 0).all()
-            if is_lim_all_negative:  # all limits negative
-                left_spine_pos = ('axes', 1)
-                ylabel_xpos = 1
-                ylabel_halignment = 'right'
-            elif is_lim_all_positive:  # all limits positive
-                left_spine_pos = ('axes', 0)  # spine at the left
-                ylabel_xpos = 0
-                ylabel_halignment = 'left'
-            else:  # zero is in plot
-                left_spine_pos = 'zero'  # spine at zero ([0, 0])
-                xmin = ax.get_xlim()[0]
-                xmax = ax.get_xlim()[1]
-                ylabel_xpos = np.abs(xmin) / (xmax + np.abs(xmin))
-                ylabel_halignment = 'left'
-        else:  # logarithmic scale
-            left_spine_pos = ('axes', 0)
+        if (np.array(ax.get_xlim()) < 0).all():  # all limits negative
+            left_spine_pos = ('axes', 1)
+            ylabel_xpos = 1
+            ylabel_halignment = 'right'
+        elif (np.array(ax.get_xlim()) > 0).all():  # all limits positive
+            left_spine_pos = ('axes', 0)  # spine at the left
             ylabel_xpos = 0
             ylabel_halignment = 'left'
+        else:  # zero is in plot
+            left_spine_pos = 'zero'  # spine at zero ([0, 0])
+            xmin = ax.get_xlim()[0]
+            xmax = ax.get_xlim()[1]
+            ylabel_xpos = np.abs(xmin) / (np.abs(xmax) + np.abs(xmin))
+            ylabel_halignment = 'left'
 
         ax.spines['left'].set_position(left_spine_pos)
         ax.yaxis.set_label_coords(ylabel_xpos, 1)
         ax.yaxis.label.set_horizontalalignment(ylabel_halignment)
 
-    def on_ylims_change(axes=[]):
+    def on_ylims_change(ax):
         # update y spine position to be always at zero, top or bottom according to set limits
         # update x-label y-position
-        if ax.get_yscale() != 'log':  # most likely linear scale
-            is_lim_all_negative = (np.array(ax.get_ylim()) < 0).all()
-            is_lim_all_positive = (np.array(ax.get_ylim()) > 0).all()
-            if is_lim_all_negative:  # all limits negative
-                bottom_spine_pos = ('axes', 1)  # spine at the top
-                xlabel_ypos = 1
-
-            elif is_lim_all_positive:  # all limits positive
-                bottom_spine_pos = ('axes', 0)  # spine at the bottom
-                xlabel_ypos = 0
-            else:  # zero is in plot
-                bottom_spine_pos = 'zero'  # spine at zero ([0, 0])
-                ymin = ax.get_ylim()[0]
-                ymax = ax.get_ylim()[1]
-                xlabel_ypos = np.abs(ymin) / (ymax + np.abs(ymin))
-        else:  # logarithmic scale
-            bottom_spine_pos = ('axes', 0)
+        if (np.array(ax.get_ylim()) < 0).all():  # all limits negative
+            bottom_spine_pos = ('axes', 1)  # spine at the top
+            xlabel_ypos = 1
+        elif (np.array(ax.get_ylim()) > 0).all():  # all limits positive
+            bottom_spine_pos = ('axes', 0)  # spine at the bottom
             xlabel_ypos = 0
+        else:  # zero is in plot
+            bottom_spine_pos = 'zero'  # spine at zero ([0, 0])
+            ymin = ax.get_ylim()[0]
+            ymax = ax.get_ylim()[1]
+            xlabel_ypos = np.abs(ymin) / (np.abs(ymax) + np.abs(ymin))
 
         ax.spines['bottom'].set_position(bottom_spine_pos)
         ax.xaxis.set_label_coords(1, xlabel_ypos)
@@ -114,12 +100,12 @@ def axis(ax):
     ax.callbacks.connect('xlim_changed', on_xlims_change)
     ax.callbacks.connect('ylim_changed', on_ylims_change)
 
-    on_xlims_change()
+    on_xlims_change(ax)
     ax.xaxis.label.set_verticalalignment('bottom')
     ax.xaxis.label.set_horizontalalignment('right')
 
     ax.yaxis.label.set_rotation(0)
-    on_ylims_change()
+    on_ylims_change(ax)
     ax.yaxis.label.set_verticalalignment('top')
     ax.yaxis.label.set_horizontalalignment('left')
 
@@ -127,6 +113,86 @@ def axis(ax):
     ax.yaxis.label.set_fontsize(12)
 
 
+def twinx(ax, visible_spine='left'):
+    """
+    Create a twin Axes sharing the x-axis.
+
+    Parameters
+    ----------
+    ax: matplotlib.axes.Axes
+        Existing Axes
+    visible_spine: {'left', 'right'}, str, optional
+        Position of the only visible axis spine
+
+    Returns
+    -------
+    ax_twin: matplotlib.axes.Axes
+        The newly created Axes instance
+
+    See also
+    --------
+    matplotlib.axes.Axes.twinx
+    twiny: Create a twin Axes sharing the y-axis.
+
+    """
+    if visible_spine in ['left', 'right']:
+        # remove visible spine from hidden spine list
+        hidden_spines = ['top', 'bottom', 'left', 'right']
+        hidden_spines.remove(visible_spine)
+
+        # create twiny and hide spines
+        ax_twin = ax.twiny()
+        for pos in hidden_spines:
+            ax_twin.spines[pos].set_color('none')
+
+        # set label position according to spine position (left/right, top)
+        ax_twin.yaxis.set_label_coords(visible_spine == 'right', 1)
+
+        return ax_twin
+    else:
+        # invalid keyword
+        raise ValueError('Twin x-axis location must be either "left" or "right"')
+
+
+def twiny(ax, visible_spine='top'):
+    """
+    Create a twin Axes sharing the y-axis.
+
+    Parameters
+    ----------
+    ax: matplotlib.axes.Axes
+        Existing Axes
+    visible_spine: {'top', 'bottom'}, str, optional
+        Position of the only visible axis spine
+
+    Returns
+    -------
+    ax_twin: matplotlib.axes.Axes
+        The newly created Axes instance
+
+    See also
+    --------
+    matplotlib.axes.Axes.twiny
+    twinx: Create a twin Axes sharing the x-axis.
+    """
+    if visible_spine in ['top', 'bottom']:
+        # remove visible spine from hidden spine list
+        hidden_spines = ['top', 'bottom', 'left', 'right']
+        hidden_spines.remove(visible_spine)
+
+        # create twiny and hide spines
+        ax_twin = ax.twiny()
+        for pos in hidden_spines:
+            ax_twin.spines[pos].set_color('none')
+
+        # set label position according to spine position (right, bottom/top)
+        ax_twin.xaxis.set_label_coords(1, visible_spine == 'top')
+
+        return ax_twin
+    else:
+        # invalid keyword
+        raise ValueError('Twin y-axis location must be either "top" or "bottom"')
+
 
 def annotate_xtick(ax, txt, x, y=0, col='black', fs=12):
     """