Repository: microsoft/Qcodes

Issue:
plot_by_id fails with a line plot and a heatmap
If a run contains a 1D plot and a heatmap, `plot_by_id` gets the colormaps confused if the dependent parameter of the heatmap precedes the line plot.

To be more specific, this example works:
```python
import numpy as np

from qcodes.tests.instrument_mocks import DummyInstrument
from qcodes.dataset.measurements import Measurement
from qcodes.dataset.plotting import plot_by_id

inst = DummyInstrument('dummy', gates=['s1', 'm1', 's2', 'm2'])

inst.m1.get = lambda: np.random.randn()
inst.m2.get = lambda: np.random.randint(0, 5)

meas = Measurement()
meas.register_parameter(inst.s1)
meas.register_parameter(inst.s2)
meas.register_parameter(inst.m1, setpoints=(inst.s1,))
meas.register_parameter(inst.m2, setpoints=(inst.s1, inst.s2) )

with meas.run() as datasaver:
    for outer in range(10): 
        datasaver.add_result((inst.s1, outer),
                             (inst.m1, inst.m1()))
        for inner in range(10):
            datasaver.add_result((inst.s1, outer),
                                 (inst.s2, inner),
                                 (inst.m2, inst.m2())) 
        
dataid = datasaver.run_id
plot_by_id(dataid)
```
whereas *this* example fails
```python
import numpy as np

from qcodes.tests.instrument_mocks import DummyInstrument
from qcodes.dataset.measurements import Measurement
from qcodes.dataset.plotting import plot_by_id

inst = DummyInstrument('dummy', gates=['s1', 'm1', 's2', 'm2'])

inst.m1.get = lambda: np.random.randn()
inst.m2.get = lambda: np.random.randint(0, 5)

meas = Measurement()
meas.register_parameter(inst.s1)
meas.register_parameter(inst.s2)
meas.register_parameter(inst.m2, setpoints=(inst.s1, inst.s2) )  # <--- DIFFERENCE HERE!
meas.register_parameter(inst.m1, setpoints=(inst.s1,))

with meas.run() as datasaver:
    for outer in range(10): 
        datasaver.add_result((inst.s1, outer),
                             (inst.m1, inst.m1()))
        for inner in range(10):
            datasaver.add_result((inst.s1, outer),
                                 (inst.s2, inner),
                                 (inst.m2, inst.m2())) 
        
dataid = datasaver.run_id
plot_by_id(dataid)
```
with `AttributeError: Unknown property cmap`



Discussion:

Resolving patch:
diff --git a/qcodes/dataset/data_export.py b/qcodes/dataset/data_export.py
--- a/qcodes/dataset/data_export.py
+++ b/qcodes/dataset/data_export.py
@@ -238,9 +238,9 @@ def get_1D_plottype(xpoints: np.ndarray, ypoints: np.ndarray) -> str:
     Determine plot type for a 1D plot by inspecting the data
 
     Possible plot types are:
-    * 'bar' - bar plot
-    * 'point' - scatter plot
-    * 'line' - line plot
+    * '1D_bar' - bar plot
+    * '1D_point' - scatter plot
+    * '1D_line' - line plot
 
     Args:
         xpoints: The x-axis values
@@ -252,11 +252,11 @@ def get_1D_plottype(xpoints: np.ndarray, ypoints: np.ndarray) -> str:
 
     if isinstance(xpoints[0], str) and not isinstance(ypoints[0], str):
         if len(xpoints) == len(np.unique(xpoints)):
-            return 'bar'
+            return '1D_bar'
         else:
-            return 'point'
+            return '1D_point'
     if isinstance(xpoints[0], str) or isinstance(ypoints[0], str):
-        return 'point'
+        return '1D_point'
     else:
         return datatype_from_setpoints_1d(xpoints)
 
@@ -267,8 +267,8 @@ def datatype_from_setpoints_1d(setpoints: np.ndarray) -> str:
     provided setpoints.
 
     The type is:
-        * 'point' (scatter plot) when all setpoints are identical
-        * 'line' otherwise
+        * '1D_point' (scatter plot) when all setpoints are identical
+        * '1D_line' otherwise
 
     Args:
         setpoints: The x-axis values
@@ -277,9 +277,9 @@ def datatype_from_setpoints_1d(setpoints: np.ndarray) -> str:
         A string representing the plot type as described above
     """
     if np.allclose(setpoints, setpoints[0]):
-        return 'point'
+        return '1D_point'
     else:
-        return 'line'
+        return '1D_line'
 
 
 def get_2D_plottype(xpoints: np.ndarray,
@@ -289,10 +289,10 @@ def get_2D_plottype(xpoints: np.ndarray,
     Determine plot type for a 2D plot by inspecting the data
 
     Plot types are:
-    * 'grid' - colormap plot for data that is on a grid
-    * 'equidistant' - colormap plot for data that is on equidistant grid
-    * 'scatter' - scatter plot
-    * 'unknown' - returned in case the data did not match any criteria of the
+    * '2D_grid' - colormap plot for data that is on a grid
+    * '2D_equidistant' - colormap plot for data that is on equidistant grid
+    * '2D_scatter' - scatter plot
+    * '2D_unknown' - returned in case the data did not match any criteria of the
     other plot types
 
     Args:
@@ -316,10 +316,11 @@ def datatype_from_setpoints_2d(xpoints: np.ndarray,
     to display the data.
 
     Plot types are:
-    * 'grid' - colormap plot for data that is on a grid
-    * 'equidistant' - colormap plot for data that is on equidistant grid
-    * 'scatter' - scatter plot
-    * 'unknown' - returned in case the data did not match any criteria of the
+    * '2D_point' - all setpoint are the same in each direction; one point
+    * '2D_grid' - colormap plot for data that is on a grid
+    * '2D_equidistant' - colormap plot for data that is on equidistant grid
+    * '2D_scatter' - scatter plot
+    * '2D_unknown' - returned in case the data did not match any criteria of the
     other plot types
 
     Args:
@@ -341,7 +342,7 @@ def datatype_from_setpoints_2d(xpoints: np.ndarray,
     y_all_the_same = np.allclose(ypoints, ypoints[0])
 
     if x_all_the_same or y_all_the_same:
-        return 'point'
+        return '2D_point'
 
     # Now check if this is a simple rectangular sweep,
     # possibly interrupted in the middle of one row
@@ -357,16 +358,16 @@ def datatype_from_setpoints_2d(xpoints: np.ndarray,
 
     # this is the check that we are on a "simple" grid
     if y_check and x_check:
-        return 'grid'
+        return '2D_grid'
 
     x_check = _all_steps_multiples_of_min_step(xrows)
     y_check = _all_steps_multiples_of_min_step(yrows)
 
     # this is the check that we are on an equidistant grid
     if y_check and x_check:
-        return 'equidistant'
+        return '2D_equidistant'
 
-    return 'unknown'
+    return '2D_unknown'
 
 
 def reshape_2D_data(x: np.ndarray, y: np.ndarray, z: np.ndarray
diff --git a/qcodes/dataset/plotting.py b/qcodes/dataset/plotting.py
--- a/qcodes/dataset/plotting.py
+++ b/qcodes/dataset/plotting.py
@@ -8,6 +8,7 @@
 import matplotlib
 import matplotlib.pyplot as plt
 from matplotlib.ticker import FuncFormatter
+from contextlib import contextmanager
 
 import qcodes as qc
 from qcodes.dataset.data_set import load_by_id
@@ -37,6 +38,46 @@
 SUBPLOTS_KWARGS = SUBPLOTS_OWN_KWARGS.union(FIGURE_KWARGS)
 
 
+@contextmanager
+def _appropriate_kwargs(plottype: str,
+                        colorbar_present: bool,
+                        **kwargs):
+    """
+    NB: Only to be used inside :meth"`plot_by_id`.
+
+    Context manager to temporarily mutate the plotting kwargs to be appropriate
+    for a specific plottype. This is helpful since :meth:`plot_by_id` may have
+    to generate different kinds of plots (e.g. heatmaps and line plots) and
+    the user may want to specify kwargs only relevant to some of them
+    (e.g. 'cmap', that line plots cannot consume). Those kwargs should then not
+    be passed to all plots, which is what this contextmanager handles.
+
+    Args:
+        plottype: The plot type for which the kwargs should be adjusted
+        colorbar_present: Is there a non-None colorbar in this plot iteration?
+    """
+
+    def linehandler(**kwargs):
+        kwargs.pop('cmap', None)
+        return kwargs
+
+    def heatmaphandler(**kwargs):
+        if not(colorbar_present) and 'cmap' not in kwargs:
+            kwargs['cmap'] = qc.config.plotting.default_color_map
+        return kwargs
+
+    plot_handler_mapping = {'1D_line': linehandler,
+                            '1D_point': linehandler,
+                            '1D_bar': linehandler,
+                            '2D_point': heatmaphandler,
+                            '2D_grid': heatmaphandler,
+                            '2D_scatter': heatmaphandler,
+                            '2D_equidistant': heatmaphandler,
+                            '2D_unknown': heatmaphandler}
+
+    yield plot_handler_mapping[plottype](**kwargs.copy())
+
+
 def plot_by_id(run_id: int,
                axes: Optional[Union[matplotlib.axes.Axes,
                               Sequence[matplotlib.axes.Axes]]]=None,
@@ -138,7 +179,7 @@ def plot_by_id(run_id: int,
     for data, ax, colorbar in zip(alldata, axes, colorbars):
 
         if len(data) == 2:  # 1D PLOTTING
-            log.debug('Plotting by id, doing a 1D plot')
+            log.debug(f'Doing a 1D plot with kwargs: {kwargs}')
 
             xpoints = data[0]['data']
             ypoints = data[1]['data']
@@ -146,17 +187,23 @@ def plot_by_id(run_id: int,
             plottype = get_1D_plottype(xpoints, ypoints)
             log.debug(f'Determined plottype: {plottype}')
 
-            if plottype == 'line':
+            if plottype == '1D_line':
                 # sort for plotting
                 order = xpoints.argsort()
                 xpoints = xpoints[order]
                 ypoints = ypoints[order]
 
-                ax.plot(xpoints, ypoints, **kwargs)
-            elif plottype == 'point':
-                ax.scatter(xpoints, ypoints, **kwargs)
-            elif plottype == 'bar':
-                ax.bar(xpoints, ypoints, **kwargs)
+                with _appropriate_kwargs(plottype,
+                                         colorbar is not None, **kwargs) as k:
+                    ax.plot(xpoints, ypoints, **k)
+            elif plottype == '1D_point':
+                with _appropriate_kwargs(plottype,
+                                         colorbar is not None, **kwargs) as k:
+                    ax.scatter(xpoints, ypoints, **k)
+            elif plottype == '1D_bar':
+                with _appropriate_kwargs(plottype,
+                                         colorbar is not None, **kwargs) as k:
+                    ax.bar(xpoints, ypoints, **k)
             else:
                 raise ValueError('Unknown plottype. Something is way wrong.')
 
@@ -170,7 +217,7 @@ def plot_by_id(run_id: int,
             ax.set_title(title)
 
         elif len(data) == 3:  # 2D PLOTTING
-            log.debug('Plotting by id, doing a 2D plot')
+            log.debug(f'Doing a 2D plot with kwargs: {kwargs}')
 
             # From the setpoints, figure out which 2D plotter to use
             # TODO: The "decision tree" for what gets plotted how and how
@@ -184,17 +231,17 @@ def plot_by_id(run_id: int,
 
             log.debug(f'Determined plottype: {plottype}')
 
-            how_to_plot = {'grid': plot_on_a_plain_grid,
-                           'equidistant': plot_on_a_plain_grid,
-                           'point': plot_2d_scatterplot,
-                           'unknown': plot_2d_scatterplot}
+            how_to_plot = {'2D_grid': plot_on_a_plain_grid,
+                           '2D_equidistant': plot_on_a_plain_grid,
+                           '2D_point': plot_2d_scatterplot,
+                           '2D_unknown': plot_2d_scatterplot}
             plot_func = how_to_plot[plottype]
 
-            if colorbar is None and 'cmap' not in kwargs:
-                kwargs['cmap'] = qc.config.plotting.default_color_map
-
-            ax, colorbar = plot_func(xpoints, ypoints, zpoints, ax, colorbar,
-                                     **kwargs)
+            with _appropriate_kwargs(plottype,
+                                     colorbar is not None, **kwargs) as k:
+                ax, colorbar = plot_func(xpoints, ypoints, zpoints,
+                                         ax, colorbar,
+                                         **k)
 
             _set_data_axes_labels(ax, data, colorbar)
 


Test patch:
diff --git a/qcodes/tests/dataset/test_plotting.py b/qcodes/tests/dataset/test_plotting.py
--- a/qcodes/tests/dataset/test_plotting.py
+++ b/qcodes/tests/dataset/test_plotting.py
@@ -3,9 +3,15 @@
 from hypothesis.strategies import text, sampled_from, floats, lists, data, \
     one_of, just
 
+import qcodes as qc
 from qcodes.dataset.plotting import _make_rescaled_ticks_and_units, \
     _ENGINEERING_PREFIXES, _UNITS_FOR_RESCALING
 
+from qcodes.dataset.plotting import plot_by_id, _appropriate_kwargs
+from qcodes.dataset.measurements import Measurement
+from qcodes.tests.instrument_mocks import DummyInstrument
+from qcodes.tests.dataset.temporary_databases import empty_temp_db, experiment
+
 
 @given(param_name=text(min_size=1, max_size=10),
        param_label=text(min_size=0, max_size=15),
@@ -83,3 +89,61 @@ def test_rescaled_ticks_and_units(scale, unit,
     # also test the fact that "{:g}" is used in ticks formatter function
     assert '2.12346' == ticks_formatter(2.123456789 / (10 ** (-scale)))
 
+
+def test_plot_by_id_line_and_heatmap(experiment):
+    """
+    Test that line plots and heatmaps can be plotted together
+    """
+    inst = DummyInstrument('dummy', gates=['s1', 'm1', 's2', 'm2'])
+
+    inst.m1.get = np.random.randn
+    inst.m2.get = lambda: np.random.randint(0, 5)
+
+    meas = Measurement()
+    meas.register_parameter(inst.s1)
+    meas.register_parameter(inst.s2)
+    meas.register_parameter(inst.m2, setpoints=(inst.s1, inst.s2) )
+    meas.register_parameter(inst.m1, setpoints=(inst.s1,))
+
+    with meas.run() as datasaver:
+        for outer in range(10):
+            datasaver.add_result((inst.s1, outer),
+                                (inst.m1, inst.m1()))
+            for inner in range(10):
+                datasaver.add_result((inst.s1, outer),
+                                    (inst.s2, inner),
+                                    (inst.m2, inst.m2()))
+
+    dataid = datasaver.run_id
+    plot_by_id(dataid)
+    plot_by_id(dataid, cmap='bone')
+
+
+def test_appropriate_kwargs():
+
+    kwargs = {'cmap': 'bone'}
+    check = kwargs.copy()
+
+    with _appropriate_kwargs('1D_line', False, **kwargs) as ap_kwargs:
+        assert ap_kwargs == {}
+
+    assert kwargs == check
+
+    with _appropriate_kwargs('1D_point', False, **kwargs) as ap_kwargs:
+        assert ap_kwargs == {}
+
+    assert kwargs == check
+
+    with _appropriate_kwargs('1D_bar', False, **kwargs) as ap_kwargs:
+        assert ap_kwargs == {}
+
+    assert kwargs == check
+
+    with _appropriate_kwargs('2D_grid', False, **kwargs) as ap_kwargs:
+        assert ap_kwargs == kwargs
+
+    assert kwargs == check
+
+    with _appropriate_kwargs('2D_point', False, **{}) as ap_kwargs:
+        assert len(ap_kwargs) == 1
+        assert ap_kwargs['cmap'] == qc.config.plotting.default_color_map