diff --git a/Wrap/Python/fit_monitor.py b/Wrap/Python/fit_monitor.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed4758193a550adcbfd20fe3b0986755c2ae777e
--- /dev/null
+++ b/Wrap/Python/fit_monitor.py
@@ -0,0 +1,250 @@
+#  **************************************************************************  #
+"""
+#   BornAgain: simulate and fit scattering at grazing incidence
+#
+#   @file      Wrap/Python/fit_monitor.py
+#   @brief     Plotter classes for monitoring fit progress.
+#
+#   @homepage  http://apps.jcns.fz-juelich.de/BornAgain
+#   @license   GNU General Public License v3 or higher (see COPYING)
+#   @copyright Forschungszentrum Juelich GmbH 2019
+#   @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
+"""
+#  **************************************************************************  #
+
+import plot_utils
+
+class Plotter:
+    """
+    Draws fit progress. Base class for simulation-specific classes (PlotterGISAS etc).
+    """
+    def __init__(self,
+                 zmin=None,
+                 zmax=None,
+                 xlabel=None,
+                 ylabel=None,
+                 units=ba.Axes.DEFAULT,
+                 aspect=None):
+
+        self._fig = plt.figure(figsize=(10.25, 7.69))
+        self._fig.canvas.draw()
+        self._zmin = zmin
+        self._zmax = zmax
+        self._xlabel = xlabel
+        self._ylabel = ylabel
+        self._units = units
+        self._aspect = aspect
+
+    def reset(self):
+        self._fig.clf()
+
+    def plot(self):
+        self._fig.tight_layout()
+        plt.pause(0.03)
+
+
+class PlotterGISAS(Plotter):
+    """
+    Draws fit progress, for GISAS simulation.
+    """
+    def __init__(self,
+                 zmin=None,
+                 zmax=None,
+                 xlabel=None,
+                 ylabel=None,
+                 units=ba.Axes.DEFAULT,
+                 aspect=None):
+        Plotter.__init__(self, zmin, zmax, xlabel, ylabel, units, aspect)
+
+    @staticmethod
+    def make_subplot(nplot):
+        plt.subplot(2, 2, nplot)
+        plt.subplots_adjust(wspace=0.2, hspace=0.2)
+
+    def plot(self, fit_objective):
+        Plotter.reset(self)
+
+        real_data = fit_objective.experimentalData()
+        sim_data = fit_objective.simulationResult()
+        diff = fit_objective.absoluteDifference()
+
+        self.make_subplot(1)
+
+        # same limits for both plots
+        arr = real_data.array()
+        zmax = np.amax(arr) if self._zmax is None else self._zmax
+        zmin = zmax*1e-6 if self._zmin is None else self._zmin
+
+        ba.plot_colormap(real_data,
+                         title="Experimental data",
+                         zmin=zmin,
+                         zmax=zmax,
+                         units=self._units,
+                         xlabel=self._xlabel,
+                         ylabel=self._ylabel,
+                         zlabel='',
+                         aspect=self._aspect)
+
+        self.make_subplot(2)
+        ba.plot_colormap(sim_data,
+                         title="Simulated data",
+                         zmin=zmin,
+                         zmax=zmax,
+                         units=self._units,
+                         xlabel=self._xlabel,
+                         ylabel=self._ylabel,
+                         zlabel='',
+                         aspect=self._aspect)
+
+        self.make_subplot(3)
+        ba.plot_colormap(diff,
+                         title="Difference",
+                         zmin=zmin,
+                         zmax=zmax,
+                         units=self._units,
+                         xlabel=self._xlabel,
+                         ylabel=self._ylabel,
+                         zlabel='',
+                         aspect=self._aspect)
+
+        self.make_subplot(4)
+        plt.title('Parameters')
+        plt.axis('off')
+
+        iteration_info = fit_objective.iterationInfo()
+
+        plt.text(
+            0.01, 0.85,
+            "Iterations  " + '{:d}'.format(iteration_info.iterationCount()))
+        plt.text(0.01, 0.75,
+                 "Chi2       " + '{:8.4f}'.format(iteration_info.chi2()))
+        index = 0
+        params = iteration_info.parameterMap()
+        for key in params:
+            plt.text(0.01, 0.55 - index*0.1,
+                     '{:30.30s}: {:6.3f}'.format(key, params[key]))
+            index = index + 1
+
+        Plotter.plot(self)
+
+
+class PlotterSpecular(Plotter):
+    """
+    Draws fit progress, for specular simulation.
+    """
+    def __init__(self, units=ba.Axes.DEFAULT):
+        Plotter.__init__(self)
+        self.gs = gridspec.GridSpec(1, 2, width_ratios=[2.5, 1], wspace=0)
+        self.units = units
+
+    def __call__(self, fit_objective):
+        self.plot(fit_objective)
+
+    @staticmethod
+    def as_si(val, ndp):
+        """
+        Fancy print of scientific-formatted values
+        :param val: numeric value
+        :param ndp: number of decimal digits to print
+        :return: a string corresponding to the _val_
+        """
+        s = '{x:0.{ndp:d}e}'.format(x=val, ndp=ndp)
+        m, e = s.split('e')
+        return r'{m:s}\times 10^{{{e:d}}}'.format(m=m, e=int(e))
+
+    @staticmethod
+    def trunc_str(token, length):
+        """
+        Truncates token if it is longer than length.
+
+        Example:
+            trunc_str("123456789", 8) returns "123456.."
+
+            trunc_str("123456789", 9) returns "123456789"
+
+        :param token: input string
+        :param length: max non-truncated length
+        :return:
+        """
+        return (token[:length - 2] + '..') if len(token) > length else token
+
+    def plot_table(self, fit_objective):
+
+        iteration_info = fit_objective.iterationInfo()
+
+        trunc_length = 9  # max string field width in the table
+        n_digits = 1  # number of decimal digits to print
+
+        n_iterations = iteration_info.iterationCount(
+        )  # current number of iterations passed
+        rel_dif = fit_objective.relativeDifference().array().max(
+        )  # maximum relative difference
+        fitted_parameters = iteration_info.parameterMap()
+
+        # creating table content
+        labels = ("Parameter", "Value")
+        table_data = [["Iteration", '${:d}$'.format(n_iterations)],
+                      [
+                          "$d_{r, max}$",
+                          '${:s}$'.format(self.as_si(rel_dif, n_digits))
+                      ]]
+        for key, value in fitted_parameters.iteritems():
+            table_data.append([
+                '{:s}'.format(self.trunc_str(key, trunc_length)),
+                '${:s}$'.format(self.as_si(value, n_digits))
+            ])
+
+        # creating table
+        axs = plt.subplot(self.gs[1])
+        axs.axis('tight')
+        axs.axis('off')
+        table = plt.table(cellText=table_data,
+                          colLabels=labels,
+                          cellLoc='center',
+                          loc='bottom left',
+                          bbox=[0.0, 0.0, 1.0, 1.0])
+
+    def plot_graph(self, fit_objective):
+        # retrieving data from fit suite
+        real_data = fit_objective.experimentalData()
+        sim_data = fit_objective.simulationResult()
+        unc_data = fit_objective.uncertaintyData()
+
+        # data values
+        sim_values = sim_data.array(self.units)
+        real_values = real_data.array(self.units)
+        unc_values = None if unc_data is None else unc_data.array(self.units)
+
+        # default font properties dictionary to use
+        font = {'family': 'serif', 'weight': 'normal', 'size': label_fontsize}
+
+        plt.subplot(self.gs[0])
+        plt.semilogy(sim_data.axis(), sim_values, 'b', real_data.axis(),
+                     real_values, 'k--')
+        if unc_values is not None:
+            plt.semilogy(real_data.axis(),
+                         real_values - unc_values,
+                         'xkcd:grey',
+                         alpha=0.6)
+            plt.semilogy(real_data.axis(),
+                         real_values + unc_values,
+                         'xkcd:grey',
+                         alpha=0.6)
+        plt.ylim((0.5*np.min(real_values), 5*np.max(real_values)))
+
+        xlabel = get_axes_labels(real_data, self.units)[0]
+        legend = ['BornAgain', 'Data']
+        if unc_values is not None:
+            legend = ['BornAgain', 'Data', r'Data $\pm \sigma$']
+        plt.legend(legend, loc='upper right', prop=font)
+        plt.xlabel(xlabel, fontdict=font)
+        plt.ylabel("Intensity", fontdict=font)
+        plt.title("Specular data fitting", fontdict=font)
+
+    def plot(self, fit_objective):
+        Plotter.reset(self)
+
+        self.plot_graph(fit_objective)
+        self.plot_table(fit_objective)
+
+        Plotter.plot(self)
diff --git a/Wrap/Python/plot_utils.py b/Wrap/Python/plot_utils.py
index a19327dd31162e29775f5b03ccab21356e61d642..b1d0eb674727d0447293500f1e0c98cb8252ce5e 100644
--- a/Wrap/Python/plot_utils.py
+++ b/Wrap/Python/plot_utils.py
@@ -2,14 +2,13 @@
 """
 #   BornAgain: simulate and fit scattering at grazing incidence
 #
-#   @file      Wrap/Python.plot_utils
+#   @file      Wrap/Python/plot_utils.py
 #   @brief     Python extensions of the SWIG-generated Python module bornagain.
 #
 #   @homepage  http://apps.jcns.fz-juelich.de/BornAgain
 #   @license   GNU General Public License v3 or higher (see COPYING)
 #   @copyright Forschungszentrum Juelich GmbH 2016
-#   @authors   Scientific Computing Group at MLZ Garching
-#   @authors   J. Fisher, M. Ganeva, G. Pospelov, W. Van Herck, J. Wuttke
+#   @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
 """
 #  **************************************************************************  #
 
@@ -48,19 +47,19 @@ def translate_axis_label(label):
     """
     label_dict = {
         'X [nbins]': r'$X \; $(bins)',
+        'X [mm]': r'$X \; $(mm)',
+        'Y [nbins]': r'$Y \; $(bins)',
+        'Y [mm]': r'$Y \; $(mm)',
         'phi_f [rad]': r'$\varphi_f \; $(rad)',
         'phi_f [deg]': r'$\varphi_f \; $(deg)',
         'alpha_i [rad]': r'$\alpha_i \; $(rad)',
         'alpha_i [deg]': r'$\alpha_i \; $(deg)',
-        'X [mm]': r'$X \; $(mm)',
-        'Qx [1/nm]': r'$Q_x \; $(nm$^{-1}$)',
-        'Qy [1/nm]': r'$Q_y \; $(nm$^{-1}$)',
-        'Q [1/nm]': r'$Q \; $(nm$^{-1}$)',
-        'Y [nbins]': r'$Y \; $(bins)',
         'alpha_f [rad]': r'$\alpha_f \; $(rad)',
         'alpha_f [deg]': r'$\alpha_f \; $(deg)',
-        'Y [mm]': r'$Y \; $(mm)',
+        'Qx [1/nm]': r'$Q_x \; $(nm$^{-1}$)',
+        'Qy [1/nm]': r'$Q_y \; $(nm$^{-1}$)',
         'Qz [1/nm]': r'$Q_z \; $(nm$^{-1}$)',
+        'Q [1/nm]': r'$Q \; $(nm$^{-1}$)',
         'Position [nm]': r'$Position \; $(nm)'
     }
     if label in label_dict.keys():
@@ -265,234 +264,3 @@ def plot_simulation_result(result,
     plt.tight_layout()
     if not postpone_show:
         plt.show()
-
-
-class Plotter:
-    def __init__(self,
-                 zmin=None,
-                 zmax=None,
-                 xlabel=None,
-                 ylabel=None,
-                 units=ba.Axes.DEFAULT,
-                 aspect=None):
-
-        self._fig = plt.figure(figsize=(10.25, 7.69))
-        self._fig.canvas.draw()
-        self._zmin = zmin
-        self._zmax = zmax
-        self._xlabel = xlabel
-        self._ylabel = ylabel
-        self._units = units
-        self._aspect = aspect
-
-    def reset(self):
-        self._fig.clf()
-
-    def plot(self):
-        self._fig.tight_layout()
-        plt.pause(0.03)
-
-
-class PlotterGISAS(Plotter):
-    def __init__(self,
-                 zmin=None,
-                 zmax=None,
-                 xlabel=None,
-                 ylabel=None,
-                 units=ba.Axes.DEFAULT,
-                 aspect=None):
-        Plotter.__init__(self, zmin, zmax, xlabel, ylabel, units, aspect)
-
-    @staticmethod
-    def make_subplot(nplot):
-        plt.subplot(2, 2, nplot)
-        plt.subplots_adjust(wspace=0.2, hspace=0.2)
-
-    def plot(self, fit_objective):
-        Plotter.reset(self)
-
-        real_data = fit_objective.experimentalData()
-        sim_data = fit_objective.simulationResult()
-        diff = fit_objective.absoluteDifference()
-
-        self.make_subplot(1)
-
-        # same limits for both plots
-        arr = real_data.array()
-        zmax = np.amax(arr) if self._zmax is None else self._zmax
-        zmin = zmax*1e-6 if self._zmin is None else self._zmin
-
-        ba.plot_colormap(real_data,
-                         title="Experimental data",
-                         zmin=zmin,
-                         zmax=zmax,
-                         units=self._units,
-                         xlabel=self._xlabel,
-                         ylabel=self._ylabel,
-                         zlabel='',
-                         aspect=self._aspect)
-
-        self.make_subplot(2)
-        ba.plot_colormap(sim_data,
-                         title="Simulated data",
-                         zmin=zmin,
-                         zmax=zmax,
-                         units=self._units,
-                         xlabel=self._xlabel,
-                         ylabel=self._ylabel,
-                         zlabel='',
-                         aspect=self._aspect)
-
-        self.make_subplot(3)
-        ba.plot_colormap(diff,
-                         title="Difference",
-                         zmin=zmin,
-                         zmax=zmax,
-                         units=self._units,
-                         xlabel=self._xlabel,
-                         ylabel=self._ylabel,
-                         zlabel='',
-                         aspect=self._aspect)
-
-        self.make_subplot(4)
-        plt.title('Parameters')
-        plt.axis('off')
-
-        iteration_info = fit_objective.iterationInfo()
-
-        plt.text(
-            0.01, 0.85,
-            "Iterations  " + '{:d}'.format(iteration_info.iterationCount()))
-        plt.text(0.01, 0.75,
-                 "Chi2       " + '{:8.4f}'.format(iteration_info.chi2()))
-        index = 0
-        params = iteration_info.parameterMap()
-        for key in params:
-            plt.text(0.01, 0.55 - index*0.1,
-                     '{:30.30s}: {:6.3f}'.format(key, params[key]))
-            index = index + 1
-
-        Plotter.plot(self)
-
-
-class PlotterSpecular(Plotter):
-    """
-    Draws fit progress. Intended specifically for observing
-    specular data fit.
-    """
-    def __init__(self, units=ba.Axes.DEFAULT):
-        Plotter.__init__(self)
-        self.gs = gridspec.GridSpec(1, 2, width_ratios=[2.5, 1], wspace=0)
-        self.units = units
-
-    def __call__(self, fit_objective):
-        self.plot(fit_objective)
-
-    @staticmethod
-    def as_si(val, ndp):
-        """
-        Fancy print of scientific-formatted values
-        :param val: numeric value
-        :param ndp: number of decimal digits to print
-        :return: a string corresponding to the _val_
-        """
-        s = '{x:0.{ndp:d}e}'.format(x=val, ndp=ndp)
-        m, e = s.split('e')
-        return r'{m:s}\times 10^{{{e:d}}}'.format(m=m, e=int(e))
-
-    @staticmethod
-    def trunc_str(token, length):
-        """
-        Truncates token if it is longer than length.
-
-        Example:
-            trunc_str("123456789", 8) returns "123456.."
-
-            trunc_str("123456789", 9) returns "123456789"
-
-        :param token: input string
-        :param length: max non-truncated length
-        :return:
-        """
-        return (token[:length - 2] + '..') if len(token) > length else token
-
-    def plot_table(self, fit_objective):
-
-        iteration_info = fit_objective.iterationInfo()
-
-        trunc_length = 9  # max string field width in the table
-        n_digits = 1  # number of decimal digits to print
-
-        n_iterations = iteration_info.iterationCount(
-        )  # current number of iterations passed
-        rel_dif = fit_objective.relativeDifference().array().max(
-        )  # maximum relative difference
-        fitted_parameters = iteration_info.parameterMap()
-
-        # creating table content
-        labels = ("Parameter", "Value")
-        table_data = [["Iteration", '${:d}$'.format(n_iterations)],
-                      [
-                          "$d_{r, max}$",
-                          '${:s}$'.format(self.as_si(rel_dif, n_digits))
-                      ]]
-        for key, value in fitted_parameters.iteritems():
-            table_data.append([
-                '{:s}'.format(self.trunc_str(key, trunc_length)),
-                '${:s}$'.format(self.as_si(value, n_digits))
-            ])
-
-        # creating table
-        axs = plt.subplot(self.gs[1])
-        axs.axis('tight')
-        axs.axis('off')
-        table = plt.table(cellText=table_data,
-                          colLabels=labels,
-                          cellLoc='center',
-                          loc='bottom left',
-                          bbox=[0.0, 0.0, 1.0, 1.0])
-
-    def plot_graph(self, fit_objective):
-        # retrieving data from fit suite
-        real_data = fit_objective.experimentalData()
-        sim_data = fit_objective.simulationResult()
-        unc_data = fit_objective.uncertaintyData()
-
-        # data values
-        sim_values = sim_data.array(self.units)
-        real_values = real_data.array(self.units)
-        unc_values = None if unc_data is None else unc_data.array(self.units)
-
-        # default font properties dictionary to use
-        font = {'family': 'serif', 'weight': 'normal', 'size': label_fontsize}
-
-        plt.subplot(self.gs[0])
-        plt.semilogy(sim_data.axis(), sim_values, 'b', real_data.axis(),
-                     real_values, 'k--')
-        if unc_values is not None:
-            plt.semilogy(real_data.axis(),
-                         real_values - unc_values,
-                         'xkcd:grey',
-                         alpha=0.6)
-            plt.semilogy(real_data.axis(),
-                         real_values + unc_values,
-                         'xkcd:grey',
-                         alpha=0.6)
-        plt.ylim((0.5*np.min(real_values), 5*np.max(real_values)))
-
-        xlabel = get_axes_labels(real_data, self.units)[0]
-        legend = ['BornAgain', 'Data']
-        if unc_values is not None:
-            legend = ['BornAgain', 'Data', r'Data $\pm \sigma$']
-        plt.legend(legend, loc='upper right', prop=font)
-        plt.xlabel(xlabel, fontdict=font)
-        plt.ylabel("Intensity", fontdict=font)
-        plt.title("Specular data fitting", fontdict=font)
-
-    def plot(self, fit_objective):
-        Plotter.reset(self)
-
-        self.plot_graph(fit_objective)
-        self.plot_table(fit_objective)
-
-        Plotter.plot(self)