diff --git a/Framework/PythonInterface/core/inc/MantidPythonInterface/core/NDArray.h b/Framework/PythonInterface/core/inc/MantidPythonInterface/core/NDArray.h
index 9939e5d5f57ee5aa6643629513a5dbde1246c11b..56b4bd7b603925bae834530a18f426fc07638a33 100644
--- a/Framework/PythonInterface/core/inc/MantidPythonInterface/core/NDArray.h
+++ b/Framework/PythonInterface/core/inc/MantidPythonInterface/core/NDArray.h
@@ -33,6 +33,7 @@ public:
   Py_intptr_t const *get_shape() const;
   int get_nd() const;
   void *get_data() const;
+  char get_typecode() const;
 
   NDArray astype(char dtype, bool copy = true) const;
 };
diff --git a/Framework/PythonInterface/core/src/NDArray.cpp b/Framework/PythonInterface/core/src/NDArray.cpp
index c60d4b9f2503529080282b63db170a136d8f3e0b..a6b8b1294eca44cbe080b8bfba67337d3300780b 100644
--- a/Framework/PythonInterface/core/src/NDArray.cpp
+++ b/Framework/PythonInterface/core/src/NDArray.cpp
@@ -76,6 +76,14 @@ void *NDArray::get_data() const {
   return PyArray_DATA(reinterpret_cast<PyArrayObject *>(this->ptr()));
 }
 
+/**
+ * See https://docs.scipy.org/doc/numpy/reference/arrays.dtypes.html
+ * @return The character code for the dtype of the array
+ */
+char NDArray::get_typecode() const {
+  return PyArray_DESCR(reinterpret_cast<PyArrayObject *>(this->ptr()))->type;
+}
+
 /**
  * Casts (and copies if necessary) the array to the given data type
  * @param dtype Character code for the numpy data types
diff --git a/buildconfig/CMake/FindCxxTest.cmake b/buildconfig/CMake/FindCxxTest.cmake
index 27a63e368dcf78f342d72d897425637361471215..98b03bd69c03ae18d5b6539ed536654a7119ac6b 100644
--- a/buildconfig/CMake/FindCxxTest.cmake
+++ b/buildconfig/CMake/FindCxxTest.cmake
@@ -106,7 +106,7 @@ include ( PrecompiledHeaderCommands )
 # CXXTEST_ADD_TEST (public macro to add unit tests)
 #=============================================================
 macro(CXXTEST_ADD_TEST _cxxtest_testname)
-    # output directory
+  # output directory
     set (_cxxtest_output_dir ${CMAKE_CURRENT_BINARY_DIR})
     if (CXXTEST_OUTPUT_DIR)
       set (_cxxtest_output_dir ${CXXTEST_OUTPUT_DIR})
diff --git a/buildconfig/CMake/QtTargetFunctions.cmake b/buildconfig/CMake/QtTargetFunctions.cmake
index 200f6c907315bcce15e2e73d622773daa8019f0a..056ad2ce5073e6bf65d0d733f34ff3bbe6b8bdfd 100644
--- a/buildconfig/CMake/QtTargetFunctions.cmake
+++ b/buildconfig/CMake/QtTargetFunctions.cmake
@@ -62,6 +62,7 @@ endfunction()
 # option: NO_SUFFIX If included, no suffix is added to the target name
 # option: EXCLUDE_FROM_ALL If included, the target is excluded from target ALL
 # keyword: TARGET_NAME The name of the target. The target will have -Qt{QT_VERSION} appended to it.
+# keyword: OUTPUT_NAME An optional filename for the library
 # keyword: QT_VERSION The major version of Qt to build against
 # keyword: SRC .cpp files to include in the target build
 # keyword: QT4_SRC .cpp files to include in a Qt4 build
@@ -92,7 +93,7 @@ endfunction()
 function (mtd_add_qt_target)
   set (options LIBRARY EXECUTABLE NO_SUFFIX EXCLUDE_FROM_ALL)
   set (oneValueArgs
-    TARGET_NAME QT_VERSION OUTPUT_DIR_BASE OUTPUT_SUBDIR
+    TARGET_NAME OUTPUT_NAME QT_VERSION OUTPUT_DIR_BASE OUTPUT_SUBDIR
     INSTALL_DIR INSTALL_DIR_BASE PRECOMPILED)
   set (multiValueArgs SRC UI MOC
     NOMOC RES DEFS QT4_DEFS QT5_DEFS INCLUDE_DIRS SYSTEM_INCLUDE_DIRS LINK_LIBS
@@ -137,9 +138,12 @@ function (mtd_add_qt_target)
 
   if (PARSED_NO_SUFFIX)
     set (_target ${PARSED_TARGET_NAME})
+    set (_output_name ${PARSED_OUTPUT_NAME})
   else()
     _append_qt_suffix (VERSION ${PARSED_QT_VERSION} OUTPUT_VARIABLE _target
                        ${PARSED_TARGET_NAME})
+    _append_qt_suffix (VERSION ${PARSED_QT_VERSION} OUTPUT_VARIABLE _output_name
+                       ${PARSED_OUTPUT_NAME})
   endif()
   _append_qt_suffix (VERSION ${PARSED_QT_VERSION} OUTPUT_VARIABLE _mtd_qt_libs
                      ${PARSED_MTD_QT_LINK_LIBS})
@@ -164,6 +168,9 @@ function (mtd_add_qt_target)
   endif()
 
   # Target properties
+  if ( _output_name )
+    set_target_properties ( ${_target} PROPERTIES OUTPUT_NAME ${_output_name} )
+  endif ()
   if (PARSED_OUTPUT_DIR_BASE)
     set ( _output_dir ${PARSED_OUTPUT_DIR_BASE}/qt${PARSED_QT_VERSION} )
     if (PARSED_OUTPUT_SUBDIR)
diff --git a/qt/widgets/CMakeLists.txt b/qt/widgets/CMakeLists.txt
index 3ded62f824ed03e175b2db855544b352950624b8..b633ff6390255a6cf71dca5304ee2a9a529410f5 100644
--- a/qt/widgets/CMakeLists.txt
+++ b/qt/widgets/CMakeLists.txt
@@ -3,6 +3,7 @@
 ###########################################################################
 add_subdirectory ( common )
 add_subdirectory ( legacyqwt )
+add_subdirectory ( mplcpp )
 add_subdirectory ( instrumentview )
 add_subdirectory ( sliceviewer )
 add_subdirectory ( spectrumviewer )
diff --git a/qt/widgets/mplcpp/CMakeLists.txt b/qt/widgets/mplcpp/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..97284a2caf175a33c70abf64933f7a1a75b88291
--- /dev/null
+++ b/qt/widgets/mplcpp/CMakeLists.txt
@@ -0,0 +1,58 @@
+# Rules for matplotlib cpp library
+set ( LIB_SRCS
+  src/Artist.cpp
+  src/Axes.cpp
+  src/Colors.cpp
+  src/Colormap.cpp
+  src/Figure.cpp
+  src/FigureCanvasQt.cpp
+  src/Line2D.cpp
+  src/ScalarMappable.cpp
+)
+
+set ( MOC_HEADERS
+  inc/MantidQtWidgets/MplCpp/FigureCanvasQt.h
+)
+
+set (NOMOC_HEADERS
+  inc/MantidQtWidgets/MplCpp/Artist.h
+  inc/MantidQtWidgets/MplCpp/Axes.h
+  inc/MantidQtWidgets/MplCpp/Colors.h
+  inc/MantidQtWidgets/MplCpp/Colormap.h
+  inc/MantidQtWidgets/MplCpp/Figure.h
+  inc/MantidQtWidgets/MplCpp/Line2D.h
+  inc/MantidQtWidgets/MplCpp/ScalarMappable.h
+)
+
+find_package ( BoostPython REQUIRED )
+
+# Target
+mtd_add_qt_library (TARGET_NAME MantidQtWidgetsMplCpp
+  QT_VERSION 5
+  SRC ${LIB_SRCS}
+  MOC ${MOC_HEADERS}
+  NOMOC ${NOMOC_HEADERS}
+  DEFS
+    IN_MANTIDQT_MPLCPP
+  INCLUDE_DIRS
+      inc
+      ../../../Framework/PythonInterface/core/inc
+      ${Boost_INCLUDE_DIRS}
+      ${PYTHON_INCLUDE_PATH}
+      ${PYTHON_NUMPY_INCLUDE_DIR}
+  LINK_LIBS
+    ${TCMALLOC_LIBRARIES_LINKTIME}
+    ${Boost_LIBRARIES}
+    ${PYTHON_LIBRARIES}
+    PythonInterfaceCore
+  INSTALL_DIR
+    ${LIB_DIR}
+  OSX_INSTALL_RPATH
+    @loader_path/../MacOS
+    @loader_path/../Libraries
+  LINUX_INSTALL_RPATH
+    "\$ORIGIN/../${LIB_DIR}"
+)
+
+# Testing
+add_subdirectory ( test )
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Artist.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Artist.h
new file mode 100644
index 0000000000000000000000000000000000000000..2451d2dcdb6f42d45ef1cbe2b664c0a6eda196c8
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Artist.h
@@ -0,0 +1,42 @@
+#ifndef MPLCPP_ARTIST_H
+#define MPLCPP_ARTIST_H
+/*
+ Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+ National Laboratory & European Spallation Source
+
+ This file is part of Mantid.
+
+ Mantid is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ Mantid is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+*/
+#include "MantidQtWidgets/MplCpp/DllConfig.h"
+#include "MantidQtWidgets/MplCpp/Python/Object.h"
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/**
+ * Wraps a matplotlib.artist object with a C++ interface
+ */
+class MANTID_MPLCPP_DLL Artist : public Python::InstanceHolder {
+public:
+  // Holds a reference to the matplotlib artist object
+  explicit Artist(Python::Object obj);
+
+  // Remove the artist from the canvas
+  void remove();
+};
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
+
+#endif // MPLCPP_ARTIST_H
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Axes.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Axes.h
new file mode 100644
index 0000000000000000000000000000000000000000..3ebb13b06d230a8114acf8ee124a0fa7129b1931
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Axes.h
@@ -0,0 +1,48 @@
+#ifndef MPLCPP_AXES_H
+#define MPLCPP_AXES_H
+/*
+ Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+ National Laboratory & European Spallation Source
+
+ This file is part of Mantid.
+
+ Mantid is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ Mantid is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+*/
+#include "MantidQtWidgets/MplCpp/DllConfig.h"
+#include "MantidQtWidgets/MplCpp/Line2D.h"
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+class MANTID_MPLCPP_DLL Axes : public Python::InstanceHolder {
+public:
+  explicit Axes(Python::Object obj);
+
+  /// @name Formatting
+  /// @{
+  void setXLabel(const char *label);
+  void setYLabel(const char *label);
+  void setTitle(const char *label);
+  /// @}
+
+  /// @name Plotting
+  /// @{
+  Line2D plot(std::vector<double> xdata, std::vector<double> ydata,
+              const char *format = "b-");
+  /// @}
+};
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
+
+#endif // MPLCPP_AXES_H
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/BackendQt.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/BackendQt.h
new file mode 100644
index 0000000000000000000000000000000000000000..9997a483b179c0c989cf8682c063b5f2fb4c811a
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/BackendQt.h
@@ -0,0 +1,57 @@
+#ifndef MPLCPP_BACKENDQT_H
+#define MPLCPP_BACKENDQT_H
+/*
+ Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+ National Laboratory & European Spallation Source
+
+ This file is part of Mantid.
+
+ Mantid is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ Mantid is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+*/
+#include "MantidQtWidgets/MplCpp/Python/Object.h"
+#include <QtGlobal>
+
+/*
+ * Defines constants relating to the matplotlib backend
+ */
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
+#error "Qt >= 5 required"
+#elif QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) &&                               \
+    QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
+
+/// Define PyQt version that matches the matplotlib backend
+const char *PYQT_MODULE = "PyQt5";
+
+/// Define matplotlib backend that will be used to draw the canvas
+const char *MPL_QT_BACKEND = "matplotlib.backends.backend_qt5agg";
+
+#else
+#error "Unknown Qt version. Cannot determine matplotlib backend."
+#endif
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/// Import and return the backend module for this version of Qt
+Python::Object backendModule() {
+  // Importing PyQt first allows matplotlib to select the correct
+  // backend
+  Python::NewRef(PyImport_ImportModule(PYQT_MODULE));
+  return Python::NewRef(PyImport_ImportModule(MPL_QT_BACKEND));
+}
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
+
+#endif // MPLCPP_BACKENDQT_H
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colormap.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colormap.h
new file mode 100644
index 0000000000000000000000000000000000000000..520d426e8a811e3eb01b41f9c5c7dab1e1e83be3
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colormap.h
@@ -0,0 +1,45 @@
+#ifndef MPLCPP_COLORMAP_H
+#define MPLCPP_COLORMAP_H
+/*
+  Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+  National Laboratory & European Spallation Source
+
+  This file is part of Mantid.
+
+  Mantid is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 3 of the License, or
+  (at your option) any later version.
+
+  Mantid is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+*/
+#include "MantidQtWidgets/MplCpp/DllConfig.h"
+#include "MantidQtWidgets/MplCpp/Python/Object.h"
+#include <QString>
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/**
+ * @brief Defines a wrapper
+ */
+class MANTID_MPLCPP_DLL Colormap : public Python::InstanceHolder {
+public:
+  Colormap(Python::Object obj);
+};
+
+/// Return the matplotlib.cm module
+MANTID_MPLCPP_DLL Python::Object cmModule();
+
+/// Return the named colormap if it exists
+MANTID_MPLCPP_DLL Colormap getCMap(const QString &name);
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
+
+#endif // MPLCPP_COLORMAP_H
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colors.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colors.h
new file mode 100644
index 0000000000000000000000000000000000000000..9c1e592fd0a258c100e37d640610342c5bc2e4e6
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colors.h
@@ -0,0 +1,76 @@
+#ifndef MPLCPP_COLORS_H
+#define MPLCPP_COLORS_H
+/*
+  Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+  National Laboratory & European Spallation Source
+
+  This file is part of Mantid.
+
+  Mantid is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 3 of the License, or
+  (at your option) any later version.
+
+  Mantid is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+*/
+#include "MantidQtWidgets/MplCpp/DllConfig.h"
+#include "MantidQtWidgets/MplCpp/Python/Object.h"
+
+/**
+ * @file Contains definitions of wrappers for types in
+ * matplotlib.colors. These types provide the ability to
+ * normalize data according to different scale types.
+ * See https://matplotlib.org/2.2.3/api/colors_api.html
+ */
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/**
+ * @brief C++ base class for Normalize types to allow a common interface
+ * to distinguish from a general Python::InstanceHolder.
+ */
+class MANTID_MPLCPP_DLL NormalizeBase : public Python::InstanceHolder {
+public:
+  NormalizeBase(Python::Object obj);
+};
+
+/**
+ * @brief The Normalize class provides a simple mapping of data in
+ * the internal [vmin, vmax] to the interval [0, 1].
+ * See
+ * https://matplotlib.org/2.2.3/api/_as_gen/matplotlib.colors.Normalize.html#matplotlib.colors.Normalize
+ */
+class MANTID_MPLCPP_DLL Normalize : public NormalizeBase {
+public:
+  Normalize(double vmin, double vmax);
+};
+
+/**
+ * @brief Map data values [vmin, vmax] onto a symmetrical log scale.
+ * See
+ * https://matplotlib.org/2.2.3/api/_as_gen/matplotlib.colors.SymLogNorm.html#matplotlib.colors.SymLogNorm
+ */
+class MANTID_MPLCPP_DLL SymLogNorm : public NormalizeBase {
+public:
+  SymLogNorm(double linthresh, double linscale, double vmin, double vmax);
+};
+
+/**
+ * @brief Map data values [vmin, vmax] onto a power law scale.
+ * See
+ * https://matplotlib.org/2.2.3/api/_as_gen/matplotlib.colors.PowerNorm.html#matplotlib.colors.PowerNorm
+ */
+class MANTID_MPLCPP_DLL PowerNorm : public NormalizeBase {
+public:
+  PowerNorm(double gamma, double vmin, double vmax);
+};
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
+#endif // MPLCPP_COLORS_H
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/DllConfig.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/DllConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..b46991b2360e4d1a5cb04ebd3ad77a5e6772111f
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/DllConfig.h
@@ -0,0 +1,12 @@
+#ifndef MANTIDQT_MPLCPP_DLLCONFIG_H_
+#define MANTIDQT_MPLCPP_DLLCONFIG_H_
+
+#include "MantidKernel/System.h"
+
+#ifdef IN_MANTIDQT_MPLCPP
+#define MANTID_MPLCPP_DLL DLLExport
+#else
+#define MANTID_MPLCPP_DLL DLLImport
+#endif /* IN_MANTIDQT_MPLCPP */
+
+#endif // MANTIDQT_MPLCPP_DLLCONFIG_H_
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Figure.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Figure.h
new file mode 100644
index 0000000000000000000000000000000000000000..83fbda2dbd7749c6b8d7960703fc133bf6764e12
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Figure.h
@@ -0,0 +1,50 @@
+#ifndef MPLCPP_FIGURE_H
+#define MPLCPP_FIGURE_H
+/*
+ Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+ National Laboratory & European Spallation Source
+
+ This file is part of Mantid.
+
+ Mantid is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ Mantid is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+*/
+#include "MantidQtWidgets/MplCpp/Axes.h"
+#include "MantidQtWidgets/MplCpp/DllConfig.h"
+#include "MantidQtWidgets/MplCpp/Python/Object.h"
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/**
+ * A thin C++ wrapper to create a new matplotlib figure
+ */
+class MANTID_MPLCPP_DLL Figure : public Python::InstanceHolder {
+public:
+  Figure(bool tightLayout = true);
+
+  /**
+   * @param index The index of the axes to return
+   * @return The axes instance
+   */
+  inline Axes axes(size_t index) const {
+    return Axes{pyobj().attr("axes")[index]};
+  }
+
+  Axes addAxes(double left, double bottom, double width, double height);
+  Axes addSubPlot(int subplotspec);
+};
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
+
+#endif // MPLCPP_FIGURE_H
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/FigureCanvasQt.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/FigureCanvasQt.h
new file mode 100644
index 0000000000000000000000000000000000000000..1cc2b81c8241772042eb95061394d68b1179bb49
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/FigureCanvasQt.h
@@ -0,0 +1,51 @@
+#ifndef MPLCPP_FIGURECANVASQT_H
+#define MPLCPP_FIGURECANVASQT_H
+/*
+  Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+  National Laboratory & European Spallation Source
+
+  This file is part of Mantid.
+
+  Mantid is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 3 of the License, or
+  (at your option) any later version.
+
+  Mantid is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+*/
+#include "MantidQtWidgets/MplCpp/DllConfig.h"
+#include "MantidQtWidgets/MplCpp/Figure.h"
+
+#include <QWidget>
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/**
+ * @brief FigureCanvasQt defines a QWidget that can be embedded within
+ * another widget to display a matplotlib figure. It roughly follows
+ * the matplotlib example on embedding a matplotlib canvas:
+ * https://matplotlib.org/examples/user_interfaces/embedding_in_qt5.html
+ */
+class MANTID_MPLCPP_DLL FigureCanvasQt : public QWidget,
+                                         public Python::InstanceHolder {
+  Q_OBJECT
+public:
+  FigureCanvasQt(int subplotspec, QWidget *parent = nullptr);
+  FigureCanvasQt(Figure fig, QWidget *parent = nullptr);
+
+  /// Non-const access to the current active axes instance.
+  inline Axes &gca() { return m_axes; }
+
+private: // members
+  Axes m_axes;
+};
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
+#endif // MPLCPP_FIGURECANVASQT_H
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Line2D.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Line2D.h
new file mode 100644
index 0000000000000000000000000000000000000000..24dd93158b065c4074efd28c0e0c8279bf0b0a61
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Line2D.h
@@ -0,0 +1,54 @@
+#ifndef MPLCPP_LINE2D_H
+#define MPLCPP_LINE2D_H
+/*
+ Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+ National Laboratory & European Spallation Source
+
+ This file is part of Mantid.
+
+ Mantid is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ Mantid is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+*/
+#include "MantidQtWidgets/MplCpp/Artist.h"
+#include "MantidQtWidgets/MplCpp/DllConfig.h"
+#include <vector>
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/**
+ * @brief Line2D holds an instance of a matplotlib Line2D type.
+ * This type is designed to hold an existing Line2D instance that contains
+ * data in numpy arrays that do not own their data but have a view on to an
+ * existing vector of data. This object keeps the data alive.
+ */
+class MANTID_MPLCPP_DLL Line2D : public Artist {
+public:
+  Line2D(Python::Object obj, std::vector<double> xdataOwner,
+         std::vector<double> ydataOwner);
+  ~Line2D();
+  // not copyable
+  Line2D(const Line2D &) = delete;
+  Line2D &operator=(const Line2D &) = delete;
+  // movable
+  Line2D(Line2D &&) = default;
+  Line2D &operator=(Line2D &&) = default;
+
+private:
+  // Containers that own the data making up the line
+  std::vector<double> m_xOwner, m_yOwner;
+};
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
+
+#endif // MPLCPP_LINE2D_H
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Object.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Object.h
new file mode 100644
index 0000000000000000000000000000000000000000..da0f9353bc9eb6e67953de9e8c5686c8d3bca85f
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Object.h
@@ -0,0 +1,82 @@
+#ifndef MPLCPP_PYTHON_OBJECT_H
+#define MPLCPP_PYTHON_OBJECT_H
+/*
+ Copyright &copy; 2017 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+ National Laboratory & European Spallation Source
+
+ This file is part of Mantid.
+
+ Mantid is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ Mantid is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+*/
+#include <boost/python/borrowed.hpp>
+#include <boost/python/object.hpp>
+#include <stdexcept>
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+namespace Python {
+
+// Alias for boost python object wrapper
+using Object = boost::python::object;
+
+// Alias for handle wrapping a raw PyObject*
+template <typename T = PyObject> using Handle = boost::python::handle<T>;
+
+// Alias to borrowed function that increments the reference count
+template <typename T> using BorrowedRef = boost::python::detail::borrowed<T>;
+
+// Helper to create an Object from a new reference to a raw PyObject*
+inline Python::Object NewRef(PyObject *obj) {
+  return Python::Object(Python::Handle<>(obj));
+}
+
+// Alias for exception indicating Python error handler is set
+using ErrorAlreadySet = boost::python::error_already_set;
+
+/**
+ * @brief Holds a Python instance of an object with a method to access it
+ */
+class InstanceHolder {
+public:
+  /**
+   * Construct an InstanceHolder with an existing Python object.
+   * @param obj An existing Python instance
+   */
+  explicit InstanceHolder(Object obj) : m_instance(std::move(obj)) {}
+
+  /**
+   * Construct an InstanceHolder with an existing Python object. The provided
+   * object is checked to ensure it has the named attr
+   * @param obj An existing Python instance
+   * @param attr The name of an attribute that must exist on the wrapped
+   * object
+   */
+  InstanceHolder(Object obj, const char *attr) : m_instance(std::move(obj)) {
+    if (PyObject_HasAttrString(pyobj().ptr(), attr) == 0) {
+      throw std::invalid_argument(std::string("object has no attribute ") +
+                                  attr);
+    }
+  }
+
+  /// Return the held instance object
+  inline const Object &pyobj() const { return m_instance; }
+
+private:
+  Object m_instance;
+};
+
+} // namespace Python
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
+
+#endif // MPLCPP_PYTHON_OBJECT_H
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Sip.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Sip.h
new file mode 100644
index 0000000000000000000000000000000000000000..5d495c36c8519f4c5140e6385fd3e5d51662ba2b
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Sip.h
@@ -0,0 +1,92 @@
+#ifndef MPLCPP_SIPUTILS_H
+#define MPLCPP_SIPUTILS_H
+/*
+  Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+  National Laboratory & European Spallation Source
+
+  This file is part of Mantid.
+
+  Mantid is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 3 of the License, or
+  (at your option) any later version.
+
+  Mantid is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+*/
+#include "MantidQtWidgets/MplCpp/Python/Object.h"
+#include <sip.h>
+#include <stdexcept>
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+namespace Python {
+
+namespace detail {
+/**
+ * @return A pointer to the C++ sip api object
+ */
+const sipAPIDef *sipAPI() {
+  static const sipAPIDef *sip_API = nullptr;
+  if (sip_API)
+    return sip_API;
+#if defined(SIP_USE_PYCAPSULE)
+  sip_API = (const sipAPIDef *)PyCapsule_Import("sip._C_API", 0);
+#else
+  /* Import the SIP module. */
+  PyObject *sip_module = PyImport_ImportModule("sip");
+  if (sip_module == NULL)
+    throw std::runtime_error("sip_api() - Error importing sip module");
+
+  /* Get the module's dictionary. */
+  PyObject *sip_module_dict = PyModule_GetDict(sip_module);
+
+  /* Get the "_C_API" attribute. */
+  PyObject *c_api = PyDict_GetItemString(sip_module_dict, "_C_API");
+  if (c_api == NULL)
+    throw std::runtime_error(
+        "sip_api() - Unable to find _C_API attribute in sip dictionary");
+
+  /* Sanity check that it is the right type. */
+  if (!PyCObject_Check(c_api))
+    throw std::runtime_error("sip_api() - _C_API type is not a CObject");
+
+  /* Get the actual pointer from the object. */
+  sip_API = (const sipAPIDef *)PyCObject_AsVoidPtr(c_api);
+#endif
+  return sip_API;
+}
+} // namespace detail
+
+/**
+ * Extract a C++ object of type T from the Python object
+ * @param obj A sip-wrapped Python object
+ */
+template <typename T> T *extract(const Object &obj) {
+  const auto sipapi = detail::sipAPI();
+  if (!PyObject_TypeCheck(obj.ptr(), sipapi->api_wrapper_type)) {
+    throw std::runtime_error("extract() - Object is not a sip-wrapped type.");
+  }
+  // transfer ownership from python to C++
+  sipapi->api_transfer_to(obj.ptr(), 0);
+  // reinterpret to sipWrapper
+  auto wrapper = reinterpret_cast<sipSimpleWrapper *>(obj.ptr());
+#if (SIP_API_MAJOR_NR == 8 && SIP_API_MINOR_NR >= 1) || SIP_API_MAJOR_NR > 8
+  return static_cast<T *>(sipapi->api_get_address(wrapper));
+#elif SIP_API_MAJOR_NR == 8
+  return static_cast<T *>(wrapper->data);
+#else
+  return static_cast<T *>(wrapper->u.cppPtr);
+#endif
+}
+
+} // namespace Python
+} // namespace MplCpp
+} // namespace Widgets
+
+} // namespace MantidQt
+
+#endif // MPLCPP_SIPUTILS_H
diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/ScalarMappable.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/ScalarMappable.h
new file mode 100644
index 0000000000000000000000000000000000000000..1f356277f99b19a8ab85c1ce3dc865b15d5ab700
--- /dev/null
+++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/ScalarMappable.h
@@ -0,0 +1,45 @@
+#ifndef MPLCPP_SCALARMAPPABLE_H
+#define MPLCPP_SCALARMAPPABLE_H
+/*
+  Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge
+  National Laboratory & European Spallation Source
+
+  This file is part of Mantid.
+
+  Mantid is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 3 of the License, or
+  (at your option) any later version.
+
+  Mantid is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+*/
+#include "MantidQtWidgets/MplCpp/Colormap.h"
+#include "MantidQtWidgets/MplCpp/Colors.h"
+#include "MantidQtWidgets/MplCpp/DllConfig.h"
+
+#include <QRgb>
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/**
+ * @brief A C++ wrapper around the matplotlib.cm.ScalarMappable
+ * type to provide the capability to map an arbitrary data
+ * value to an RGBA value within a given colormap
+ */
+class MANTID_MPLCPP_DLL ScalarMappable : public Python::InstanceHolder {
+public:
+  ScalarMappable(const NormalizeBase &norm, const Colormap &cmap);
+
+  QRgb toRGBA(double x, double alpha = 1.0) const;
+};
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
+
+#endif // MPLCPP_SCALARMAPPABLE_H
diff --git a/qt/widgets/mplcpp/src/Artist.cpp b/qt/widgets/mplcpp/src/Artist.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..64bd3509d03a15d9af5fcc9cf48d46459397f3b7
--- /dev/null
+++ b/qt/widgets/mplcpp/src/Artist.cpp
@@ -0,0 +1,21 @@
+#include "MantidQtWidgets/MplCpp/Artist.h"
+#include <cassert>
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/**
+ * @brief Create an Artist instance around an existing matplotlib Artist
+ * @param obj A Python object pointing to a matplotlib artist
+ */
+Artist::Artist(Python::Object obj) : InstanceHolder(std::move(obj), "draw") {}
+
+/**
+ * Call .remove on the underlying artist
+ */
+void Artist::remove() { pyobj().attr("remove")(); }
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
diff --git a/qt/widgets/mplcpp/src/Axes.cpp b/qt/widgets/mplcpp/src/Axes.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3107c43d828ee545eb79b7ed20aa908142e08c96
--- /dev/null
+++ b/qt/widgets/mplcpp/src/Axes.cpp
@@ -0,0 +1,77 @@
+#include "MantidQtWidgets/MplCpp/Axes.h"
+#include "MantidPythonInterface/core/Converters/VectorToNDArray.h"
+#include "MantidPythonInterface/core/Converters/WrapWithNDArray.h"
+#include "MantidPythonInterface/core/ErrorHandling.h"
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+using Mantid::PythonInterface::Converters::VectorToNDArray;
+using Mantid::PythonInterface::Converters::WrapReadOnly;
+using Mantid::PythonInterface::PythonRuntimeError;
+
+/**
+ * Construct an Axes wrapper around an existing Axes instance
+ * @param obj A matplotlib.axes.Axes instance
+ */
+Axes::Axes(Python::Object obj) : InstanceHolder(std::move(obj), "plot") {}
+
+/**
+ * @brief Set the X-axis label
+ * @param label String for the axis label
+ */
+void Axes::setXLabel(const char *label) { pyobj().attr("set_xlabel")(label); }
+
+/**
+ * @brief Set the Y-axis label
+ * @param label String for the axis label
+ */
+void Axes::setYLabel(const char *label) { pyobj().attr("set_ylabel")(label); }
+
+/**
+ * @brief Set the title
+ * @param label String for the title label
+ */
+void Axes::setTitle(const char *label) { pyobj().attr("set_title")(label); }
+
+/**
+ * @brief Take the data and draw a single Line2D on the axes
+ * @param xdata A vector containing the X data
+ * @param ydata A vector containing the Y data
+ * @param format A format string accepted by matplotlib.axes.Axes.plot. The
+ * default is 'b-'
+ * @return A new Line2D object that owns the xdata, ydata vectors. If the
+ * return value is not captured the line will be automatically removed from
+ * the canvas as the vector data will be destroyed.
+ */
+Line2D Axes::plot(std::vector<double> xdata, std::vector<double> ydata,
+                  const char *format) {
+  auto throwIfEmpty = [](const std::vector<double> &data, char vecId) {
+    if (data.empty()) {
+      throw std::invalid_argument(
+          std::string("Cannot plot line. Empty vector=") + vecId);
+    }
+  };
+  throwIfEmpty(xdata, 'X');
+  throwIfEmpty(ydata, 'Y');
+
+  // Wrap the vector data in a numpy facade to avoid a copy.
+  // The vector still owns the data so it needs to be kept alive too
+  // It is transferred to the Line2D for this purpose.
+  VectorToNDArray<double, WrapReadOnly> wrapNDArray;
+  Python::Object xarray{Python::NewRef(wrapNDArray(xdata))},
+      yarray{Python::NewRef(wrapNDArray(ydata))};
+
+  try {
+    return Line2D{pyobj().attr("plot")(xarray, yarray, format)[0],
+                  std::move(xdata), std::move(ydata)};
+
+  } catch (Python::ErrorAlreadySet &) {
+    throw PythonRuntimeError();
+  }
+}
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
diff --git a/qt/widgets/mplcpp/src/Colormap.cpp b/qt/widgets/mplcpp/src/Colormap.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e1da71ebf63ca17a021c065a3ca26c94422e1d7b
--- /dev/null
+++ b/qt/widgets/mplcpp/src/Colormap.cpp
@@ -0,0 +1,42 @@
+#include "MantidQtWidgets/MplCpp/Colormap.h"
+#include "MantidQtWidgets/MplCpp/Colors.h"
+
+#include "MantidPythonInterface/core/ErrorHandling.h"
+
+using Mantid::PythonInterface::PythonRuntimeError;
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/**
+ * @brief Construct a Colormap object given a name
+ */
+Colormap::Colormap(Python::Object obj)
+    : Python::InstanceHolder(obj, "is_gray") {}
+
+/**
+ * @return A reference to the matplotlib.cm module
+ */
+Python::Object cmModule() {
+  Python::Object cmModule{
+      Python::NewRef(PyImport_ImportModule("matplotlib.cm"))};
+  return cmModule;
+}
+
+/**
+ * @param name The name of an existing colormap.
+ * @return A new Colormap instance for the named map
+ * @throws std::invalid_argument if the name is unknown
+ */
+Colormap getCMap(const QString &name) {
+  try {
+    return cmModule().attr("get_cmap")(name.toLatin1().constData());
+  } catch (Python::ErrorAlreadySet &) {
+    throw PythonRuntimeError();
+  }
+}
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
diff --git a/qt/widgets/mplcpp/src/Colors.cpp b/qt/widgets/mplcpp/src/Colors.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9122de96d55428b084e26fd8bc1803fa312e49ff
--- /dev/null
+++ b/qt/widgets/mplcpp/src/Colors.cpp
@@ -0,0 +1,72 @@
+#include "MantidQtWidgets/MplCpp/Colors.h"
+#include "MantidPythonInterface/core/ErrorHandling.h"
+
+using Mantid::PythonInterface::PythonRuntimeError;
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+namespace {
+/**
+ * @return A reference to the matplotlib.colors module
+ */
+Python::Object colorsModule() {
+  Python::Object colorsModule{
+      Python::NewRef(PyImport_ImportModule("matplotlib.colors"))};
+  return colorsModule;
+}
+} // namespace
+
+// ------------------------ NormalizeBase---------------------------------------
+/**
+ * @brief NormalizeBase::NormalizeBase
+ * @param obj An existing Normalize instance or subtype
+ */
+NormalizeBase::NormalizeBase(Python::Object obj)
+    : Python::InstanceHolder(std::move(obj), "autoscale") {}
+
+// ------------------------ Normalize ------------------------------------------
+
+/**
+ * @brief Construct a Normalize object mapping data from [vmin, vmax]
+ * to [0, 1]
+ * @param vmin Minimum value of the data interval
+ * @param vmax Maximum value of the data interval
+ */
+Normalize::Normalize(double vmin, double vmax)
+    : NormalizeBase(colorsModule().attr("Normalize")(vmin, vmax)) {}
+
+// ------------------------ SymLogNorm -----------------------------------------
+
+/**
+ * @brief Construct a SymLogNorm object mapping data from [vmin, vmax]
+ * to a symmetric logarithm scale
+ * @param linthresh The range within which the plot is linear
+ * @param linscale This allows the linear range (-linthresh to linthresh) to be
+ * stretched relative to the logarithmic range.
+ * See
+ * https://matplotlib.org/2.2.3/api/_as_gen/matplotlib.colors.SymLogNorm.html#matplotlib.colors.SymLogNorm
+ * @param vmin Minimum value of the data interval
+ * @param vmax Maximum value of the data interval
+ */
+SymLogNorm::SymLogNorm(double linthresh, double linscale, double vmin,
+                       double vmax)
+    : NormalizeBase(
+          colorsModule().attr("SymLogNorm")(linthresh, linscale, vmin, vmax)) {}
+
+// ------------------------ PowerNorm ------------------------------------------
+
+/**
+ * @brief Construct a PowerNorm object to map data from [vmin,vmax] to
+ * [0,1] pn a power-law scale
+ * @param gamma The exponent for the power-law
+ * @param vmin Minimum value of the data interval
+ * @param vmax Maximum value of the data interval
+ */
+PowerNorm::PowerNorm(double gamma, double vmin, double vmax)
+    : NormalizeBase(colorsModule().attr("PowerNorm")(gamma, vmin, vmax)) {}
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
diff --git a/qt/widgets/mplcpp/src/Figure.cpp b/qt/widgets/mplcpp/src/Figure.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5e1fb6f3a5de4e9e0371f051ea6047902140ae91
--- /dev/null
+++ b/qt/widgets/mplcpp/src/Figure.cpp
@@ -0,0 +1,52 @@
+#include "MantidQtWidgets/MplCpp/Figure.h"
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+namespace {
+Python::Object newFigure(bool tightLayout = true) {
+  Python::Object figureModule{
+      Python::NewRef(PyImport_ImportModule("matplotlib.figure"))};
+  auto fig = figureModule.attr("Figure")();
+  if (tightLayout) {
+    auto kwargs = Python::NewRef(Py_BuildValue("{s:f}", "pad", 0.5));
+    fig.attr("set_tight_layout")(kwargs);
+  }
+  return fig;
+}
+} // namespace
+
+/**
+ * Construct a new default figure.
+ * @param tightLayout If true set a tight layout on the matplotlib figure
+ */
+Figure::Figure(bool tightLayout)
+    : Python::InstanceHolder(newFigure(tightLayout)) {}
+
+/**
+ * Add an Axes of the given dimensions to the current figure
+ * All quantities are in fractions of figure width and height
+ * @param left The X coordinate of the lower-left corner
+ * @param bottom The Y coordinate of the lower-left corner
+ * @param width The width of the Axes
+ * @param height The heigh of the Axes
+ * @return A new Axes instance
+ */
+Axes Figure::addAxes(double left, double bottom, double width, double height) {
+  return Axes{pyobj().attr("add_axes")(
+      Python::NewRef(Py_BuildValue("(ffff)", left, bottom, width, height)))};
+}
+
+/**
+ * Add a subplot Axes to the figure
+ * @param subplotspec
+ * @return
+ */
+Axes Figure::addSubPlot(int subplotspec) {
+  return Axes{pyobj().attr("add_subplot")(subplotspec)};
+}
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
diff --git a/qt/widgets/mplcpp/src/FigureCanvasQt.cpp b/qt/widgets/mplcpp/src/FigureCanvasQt.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d6530dbc2e558b2f62144bcef8f9ce8bbcfe5689
--- /dev/null
+++ b/qt/widgets/mplcpp/src/FigureCanvasQt.cpp
@@ -0,0 +1,75 @@
+#include "MantidQtWidgets/MplCpp/FigureCanvasQt.h"
+#include "MantidQtWidgets/MplCpp/BackendQt.h"
+#include "MantidQtWidgets/MplCpp/Figure.h"
+#include "MantidQtWidgets/MplCpp/Python/Sip.h"
+
+#include <QVBoxLayout>
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+namespace {
+/**
+ * @param fig An existing matplotlib Figure instance
+ * @return A new FigureCanvasQT object
+ */
+Python::Object createPyCanvasFromFigure(Figure fig) {
+  return backendModule().attr("FigureCanvasQTAgg")(fig.pyobj());
+}
+
+/**
+ * @param subplotspec A matplotlib subplot spec defined as a 3-digit
+ * integer.
+ * @return A new FigureCanvasQT object
+ */
+Python::Object createPyCanvas(int subplotspec) {
+  Figure fig{true};
+  if (subplotspec > 0)
+    fig.addSubPlot(subplotspec);
+  return createPyCanvasFromFigure(std::move(fig));
+}
+} // namespace
+
+/**
+ * @brief Common constructor code for FigureCanvasQt
+ * @param cppCanvas A pointer to the FigureCanvasQt object
+ */
+void initLayout(FigureCanvasQt *cppCanvas) {
+  cppCanvas->setLayout(new QVBoxLayout());
+  QWidget *pyCanvas = Python::extract<QWidget>(cppCanvas->pyobj());
+  cppCanvas->layout()->addWidget(pyCanvas);
+  pyCanvas->setMouseTracking(false);
+  pyCanvas->installEventFilter(cppCanvas);
+}
+
+/**
+ * @brief Constructor specifying an axes subplot spec and optional parent.
+ * An Axes with the given subplot specification is created on construction
+ * See
+ * https://matplotlib.org/2.2.3/api/_as_gen/matplotlib.figure.Figure.html?highlight=add_subplot#matplotlib.figure.Figure.add_subplot
+ * @param subplotspec A matplotlib subplot spec defined as a 3-digit integer
+ * @param parent The owning parent widget
+ */
+FigureCanvasQt::FigureCanvasQt(int subplotspec, QWidget *parent)
+    : QWidget(parent), InstanceHolder(createPyCanvas(subplotspec), "draw"),
+      m_axes(pyobj().attr("figure").attr("axes")[0]) {
+  // Cannot use delegating constructor here as InstanceHolder needs to be
+  // initialized before the axes can be created
+  initLayout(this);
+}
+
+/**
+ * @brief Constructor specifying an existing axes object and optional
+ * parent.
+ * @param axes An existing axes instance
+ * @param parent The owning parent widget
+ */
+FigureCanvasQt::FigureCanvasQt(Figure fig, QWidget *parent)
+    : QWidget(parent), InstanceHolder(createPyCanvasFromFigure(fig), "draw"),
+      m_axes(fig.axes(0)) {
+  initLayout(this);
+}
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
diff --git a/qt/widgets/mplcpp/src/Line2D.cpp b/qt/widgets/mplcpp/src/Line2D.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2ae28b6755452a9e3b5d627ff772c9312aabb7c5
--- /dev/null
+++ b/qt/widgets/mplcpp/src/Line2D.cpp
@@ -0,0 +1,35 @@
+#include "MantidQtWidgets/MplCpp/Line2D.h"
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+/**
+ * @brief Contruct a wrapper around an existing matplotlib.lines.Line2D instance
+ * This object owns the data that is part of the Line2D instance. It assumes the
+ * Line2D contains numpy arrays that are simple a view on to the existing data
+ * that is part of the given Y and X arrays.
+ * @param obj An existing Line2D instance
+ * @param xdataOwner The source data for X. It is moved into this object
+ * @param ydataOwner The source data for Y. It is moved into this object
+ */
+Line2D::Line2D(Python::Object obj, std::vector<double> xdataOwner,
+               std::vector<double> ydataOwner)
+    : Artist(obj), m_xOwner(std::move(xdataOwner)),
+      m_yOwner(std::move(ydataOwner)) {}
+
+/**
+ * The data is being deleted so the the line is removed from the axes
+ * if it is present
+ */
+Line2D::~Line2D() {
+  try {
+    this->remove();
+  } catch (Python::ErrorAlreadySet &) {
+    // line is not attached to an axes
+  }
+}
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
diff --git a/qt/widgets/mplcpp/src/ScalarMappable.cpp b/qt/widgets/mplcpp/src/ScalarMappable.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f5567194dc83a6857859b27b1514e2a2ba4256f6
--- /dev/null
+++ b/qt/widgets/mplcpp/src/ScalarMappable.cpp
@@ -0,0 +1,68 @@
+#include "MantidQtWidgets/MplCpp/ScalarMappable.h"
+#include "MantidQtWidgets/MplCpp/Colormap.h"
+
+#include "MantidPythonInterface/core/NDArray.h"
+
+using Mantid::PythonInterface::NDArray;
+
+namespace MantidQt {
+namespace Widgets {
+namespace MplCpp {
+
+namespace {
+
+Python::Object createScalarMappable(const NormalizeBase &norm,
+                                    const Colormap &cmap) {
+  return cmModule().attr("ScalarMappable")(norm.pyobj(), cmap.pyobj());
+}
+} // namespace
+
+/**
+ * @brief Construct a ScalarMappable instance with the given normalization
+ * type and colormap
+ * @param norm Instance use to define the mapping from data to [0,1]
+ * @param cmap A Colormap defning the RGBA values to use for drawing
+ */
+ScalarMappable::ScalarMappable(const NormalizeBase &norm, const Colormap &cmap)
+    : Python::InstanceHolder(createScalarMappable(norm, cmap), "to_rgba") {
+  // The internal array needs to be set to some iterable but apparently
+  // it is not used:
+  // https://stackoverflow.com/questions/28801803/matplotlib-scalarmappable-why-need-to-set-array-if-norm-set
+  pyobj().attr("set_array")(Python::NewRef(Py_BuildValue("()")));
+}
+
+/**
+ * @brief Convert a data value to an RGBA value
+ * @param x The data value within the
+ * @param alpha The alpha value (default = 1)
+ * @return A QRgb value corresponding to this data point
+ */
+QRgb ScalarMappable::toRGBA(double x, double alpha) const {
+  // Sending the first argument as an iterable gives a numpy array back.
+  // The final argument (bytes=true) forces the return value to be 0->255
+  NDArray rgba{pyobj().attr("to_rgba")(Python::NewRef(Py_BuildValue("(f)", x)),
+                                       alpha, true)};
+  // sanity check
+  auto shape = rgba.get_shape();
+  if (rgba.get_typecode() == 'B' && rgba.get_nd() == 2 && shape[0] == 1 &&
+      shape[1] == 4) {
+    auto bytes = reinterpret_cast<std::uint8_t *>(rgba.get_data());
+    return qRgba(bytes[0], bytes[1], bytes[2], bytes[3]);
+  } else {
+    std::string msg = "Unexpected return type from "
+                      "ScalarMappable.to_rgba. Expected "
+                      "np.array(dtype=B) with shape (1,4) but found "
+                      "np.array(dtype=";
+    msg.append(std::to_string(rgba.get_typecode()))
+        .append(") with shape (")
+        .append(std::to_string(shape[0]))
+        .append(",")
+        .append(std::to_string(shape[1]))
+        .append(")");
+    throw std::runtime_error(std::move(msg));
+  }
+}
+
+} // namespace MplCpp
+} // namespace Widgets
+} // namespace MantidQt
diff --git a/qt/widgets/mplcpp/test/ArtistTest.h b/qt/widgets/mplcpp/test/ArtistTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..f2a6f4a60ca7078242d14f5f2f75c12efd05fa74
--- /dev/null
+++ b/qt/widgets/mplcpp/test/ArtistTest.h
@@ -0,0 +1,37 @@
+#ifndef MPLCPP_ARTISTTEST_H
+#define MPLCPP_ARTISTTEST_H
+
+#include "MantidQtWidgets/MplCpp/Artist.h"
+
+#include <cxxtest/TestSuite.h>
+
+using namespace MantidQt::Widgets::MplCpp;
+
+class ArtistTest : public CxxTest::TestSuite {
+public:
+  static ArtistTest *createSuite() { return new ArtistTest; }
+  static void destroySuite(ArtistTest *suite) { delete suite; }
+
+public:
+  // ----------------- success tests ---------------------
+  void testConstructWithArtistIsSuccessful() {
+    auto artistModule(
+        Python::NewRef(PyImport_ImportModule("matplotlib.artist")));
+    Python::Object pyartist = artistModule.attr("Artist")();
+    TS_ASSERT_THROWS_NOTHING(Artist drawer(pyartist));
+  }
+
+  void testArtistCallsRemoveOnPyObject() {
+    auto textModule(Python::NewRef(PyImport_ImportModule("matplotlib.text")));
+    Artist label(textModule.attr("Text")());
+    TS_ASSERT_THROWS(label.remove(), Python::ErrorAlreadySet);
+  }
+  // ----------------- failure tests ---------------------
+
+  void testConstructWithNonArtistThrowsInvalidArgument() {
+    Python::Object none;
+    TS_ASSERT_THROWS(Artist artist(none), std::invalid_argument);
+  }
+};
+
+#endif // ARTISTTEST_H
diff --git a/qt/widgets/mplcpp/test/AxesTest.h b/qt/widgets/mplcpp/test/AxesTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..a1dff83573a9cc87012aa85ea2c58c41243a90bb
--- /dev/null
+++ b/qt/widgets/mplcpp/test/AxesTest.h
@@ -0,0 +1,88 @@
+#ifndef MPLCPP_AXESTEST_H
+#define MPLCPP_AXESTEST_H
+
+#include "MantidQtWidgets/MplCpp/Axes.h"
+#include <cxxtest/TestSuite.h>
+
+using namespace MantidQt::Widgets::MplCpp;
+
+class AxesTest : public CxxTest::TestSuite {
+public:
+  static AxesTest *createSuite() { return new AxesTest; }
+  static void destroySuite(AxesTest *suite) { delete suite; }
+
+public:
+  // ----------------- success tests ---------------------
+  void testConstructWithPyObjectAxes() {
+    TS_ASSERT_THROWS_NOTHING(Axes axes(pyAxes()));
+  }
+
+  void testSetXLabel() {
+    Axes axes(pyAxes());
+    axes.setXLabel("X");
+    TS_ASSERT_EQUALS("X", axes.pyobj().attr("get_xlabel")());
+  }
+
+  void testSetYLabel() {
+    Axes axes(pyAxes());
+    axes.setYLabel("Y");
+    TS_ASSERT_EQUALS("Y", axes.pyobj().attr("get_ylabel")());
+  }
+
+  void testSetTitle() {
+    Axes axes(pyAxes());
+    axes.setTitle("Title");
+    TS_ASSERT_EQUALS("Title", axes.pyobj().attr("get_title")());
+  }
+
+  void testPlotGivesLineWithExpectedData() {
+    Axes axes(pyAxes());
+    std::vector<double> xsrc{1, 2, 3}, ysrc{1, 2, 3};
+    auto line = axes.plot(xsrc, ysrc);
+    auto linex = line.pyobj().attr("get_xdata")(true);
+    auto liney = line.pyobj().attr("get_ydata")(true);
+    for (size_t i = 0; i < xsrc.size(); ++i) {
+      TSM_ASSERT_EQUALS("Mismatch in X data", linex[i], xsrc[i]);
+      TSM_ASSERT_EQUALS("Mismatch in Y data", liney[i], ysrc[i]);
+    }
+  }
+
+  void testPlotWithNoFormatUsesDefault() {
+    Axes axes(pyAxes());
+    auto line = axes.plot({1, 2, 3}, {1, 2, 3});
+    TS_ASSERT_EQUALS('b', line.pyobj().attr("get_color")());
+    TS_ASSERT_EQUALS('-', line.pyobj().attr("get_linestyle")());
+  }
+
+  void testPlotUsesFormatStringIfProvided() {
+    Axes axes(pyAxes());
+    const std::string format{"ro"};
+    auto line = axes.plot({1, 2, 3}, {1, 2, 3}, format.c_str());
+    TS_ASSERT_EQUALS(format[0], line.pyobj().attr("get_color")());
+    TS_ASSERT_EQUALS(format[1], line.pyobj().attr("get_marker")());
+  }
+
+  // ----------------- failure tests ---------------------
+  void testPlotThrowsWithEmptyData() {
+    Axes axes(pyAxes());
+    TS_ASSERT_THROWS(axes.plot({}, {}), std::invalid_argument);
+    TS_ASSERT_THROWS(axes.plot({1}, {}), std::invalid_argument);
+    TS_ASSERT_THROWS(axes.plot({}, {1}), std::invalid_argument);
+  }
+
+private:
+  Python::Object pyAxes() {
+    // An Axes requires a figure and rectangle definition
+    // to be constructible
+    const Python::Object figureModule{
+        Python::NewRef(PyImport_ImportModule("matplotlib.figure"))};
+    const Python::Object figure{figureModule.attr("Figure")()};
+    const Python::Object rect{
+        Python::NewRef(Py_BuildValue("(iiii)", 0, 0, 1, 1))};
+    const Python::Object axesModule{
+        Python::NewRef(PyImport_ImportModule("matplotlib.axes"))};
+    return axesModule.attr("Axes")(figure, rect);
+  }
+};
+
+#endif // MPLCPP_AXESTEST_H
diff --git a/qt/widgets/mplcpp/test/CMakeLists.txt b/qt/widgets/mplcpp/test/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7cfe4e760a8a81e14331f61916d167943a33b217
--- /dev/null
+++ b/qt/widgets/mplcpp/test/CMakeLists.txt
@@ -0,0 +1,32 @@
+#
+# Unit tests for MplCpp library
+#
+set ( TEST_FILES
+  ArtistTest.h
+  AxesTest.h
+  ColorsTest.h
+  ColormapTest.h
+  FigureTest.h
+  FigureCanvasQtTest.h
+  Line2DTest.h
+  ScalarMappableTest.h
+  SipTest.h
+)
+
+set (CXXTEST_EXTRA_HEADER_INCLUDE ${CMAKE_CURRENT_LIST_DIR}/MplCppTestInitialization.h)
+
+mtd_add_qt_tests (TARGET_NAME MantidQtWidgetsMplCppTest
+  QT_VERSION 5
+  INCLUDE_DIRS
+    ${CMAKE_CURRENT_LIST_DIR}/../inc
+  SRC
+    ${TEST_FILES}
+  LINK_LIBS
+    PythonInterfaceCore
+    ${Boost_LIBRARIES}
+    ${PYTHON_LIBRARIES}
+  MTD_QT_LINK_LIBS
+    MantidQtWidgetsMplCpp
+  PARENT_DEPENDENCIES
+    GUITests
+)
diff --git a/qt/widgets/mplcpp/test/ColormapTest.h b/qt/widgets/mplcpp/test/ColormapTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..043c550d863abf25bf307b5ca19dca2e94ccc6b1
--- /dev/null
+++ b/qt/widgets/mplcpp/test/ColormapTest.h
@@ -0,0 +1,40 @@
+#ifndef MPLCPP_COLORMAPTEST_H
+#define MPLCPP_COLORMAPTEST_H
+
+#include "MantidPythonInterface/core/ErrorHandling.h"
+#include "MantidQtWidgets/MplCpp/Colormap.h"
+
+#include <cxxtest/TestSuite.h>
+
+using Mantid::PythonInterface::PythonRuntimeError;
+using MantidQt::Widgets::MplCpp::Colormap;
+using MantidQt::Widgets::MplCpp::Python::Object;
+using MantidQt::Widgets::MplCpp::getCMap;
+
+class ColormapTest : public CxxTest::TestSuite {
+public:
+  static ColormapTest *createSuite() { return new ColormapTest; }
+  static void destroySuite(ColormapTest *suite) { delete suite; }
+
+public:
+  // ----------------------- Success tests ------------------------
+  void testgetCMapKnownCMapIsSuccesful() {
+    TS_ASSERT_THROWS_NOTHING(getCMap("jet"));
+  }
+
+  void testConstructionColorMapInstanceIsSuccessful() {
+    auto jet = getCMap("jet");
+    TS_ASSERT_EQUALS("jet", jet.pyobj().attr("name"));
+  }
+
+  // ----------------------- Failure tests ------------------------
+  void testgetCMapWithUnknownCMapThrowsException() {
+    TS_ASSERT_THROWS(getCMap("AnUnknownName"), PythonRuntimeError);
+  }
+
+  void testConstructionWithNonColorMapObjectThrows() {
+    TS_ASSERT_THROWS(Colormap cmap(Object{}), std::invalid_argument);
+  }
+};
+
+#endif // MPLCPP_COLORMAPTEST_H
diff --git a/qt/widgets/mplcpp/test/ColorsTest.h b/qt/widgets/mplcpp/test/ColorsTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..4d266b9457bf07c933da83745afb3d6cc6c445c1
--- /dev/null
+++ b/qt/widgets/mplcpp/test/ColorsTest.h
@@ -0,0 +1,40 @@
+#ifndef MPLCPP_COLORSTEST_H
+#define MPLCPP_COLORSTEST_H
+
+#include <cxxtest/TestSuite.h>
+
+#include "MantidQtWidgets/MplCpp/Colors.h"
+
+using MantidQt::Widgets::MplCpp::Normalize;
+using MantidQt::Widgets::MplCpp::PowerNorm;
+using MantidQt::Widgets::MplCpp::SymLogNorm;
+
+class ColorsTest : public CxxTest::TestSuite {
+public:
+  static ColorsTest *createSuite() { return new ColorsTest; }
+  static void destroySuite(ColorsTest *suite) { delete suite; }
+
+public:
+  void testNormalize() {
+    Normalize norm(-1, 1);
+    TS_ASSERT_EQUALS(-1, norm.pyobj().attr("vmin"));
+    TS_ASSERT_EQUALS(1, norm.pyobj().attr("vmax"));
+  }
+
+  void testSymLogNorm() {
+    SymLogNorm norm(0.001, 2, -1, 1);
+    // No public api method for access linscale
+    TS_ASSERT_EQUALS(0.001, norm.pyobj().attr("linthresh"));
+    TS_ASSERT_EQUALS(-1, norm.pyobj().attr("vmin"));
+    TS_ASSERT_EQUALS(1, norm.pyobj().attr("vmax"));
+  }
+
+  void testPowerNorm() {
+    PowerNorm norm(2, -1, 1);
+    TS_ASSERT_EQUALS(2, norm.pyobj().attr("gamma"));
+    TS_ASSERT_EQUALS(-1, norm.pyobj().attr("vmin"));
+    TS_ASSERT_EQUALS(1, norm.pyobj().attr("vmax"));
+  }
+};
+
+#endif // MPLCPP_COLORSTEST_H
diff --git a/qt/widgets/mplcpp/test/FigureCanvasQtTest.h b/qt/widgets/mplcpp/test/FigureCanvasQtTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..1dcc3796308a844726219d0ea53b42bae1d51021
--- /dev/null
+++ b/qt/widgets/mplcpp/test/FigureCanvasQtTest.h
@@ -0,0 +1,38 @@
+#ifndef MPLCPP_FIGURECANVASQTTEST_H
+#define MPLCPP_FIGURECANVASQTTEST_H
+
+#include <cxxtest/TestSuite.h>
+
+#include "MantidQtWidgets/MplCpp/Figure.h"
+#include "MantidQtWidgets/MplCpp/FigureCanvasQt.h"
+
+using MantidQt::Widgets::MplCpp::Figure;
+using MantidQt::Widgets::MplCpp::FigureCanvasQt;
+
+class FigureCanvasQtTest : public CxxTest::TestSuite {
+public:
+  static FigureCanvasQtTest *createSuite() { return new FigureCanvasQtTest; }
+  static void destroySuite(FigureCanvasQtTest *suite) { delete suite; }
+
+public:
+  void testConstructionYieldsExpectedSubplot() {
+    FigureCanvasQt canvas{111};
+    auto geometry = canvas.gca().pyobj().attr("get_geometry")();
+    TS_ASSERT_EQUALS(1, geometry[0]);
+    TS_ASSERT_EQUALS(1, geometry[1]);
+    TS_ASSERT_EQUALS(1, geometry[2]);
+  }
+
+  void testConstructionCapturesGivenAxesObject() {
+    using namespace MantidQt::Widgets::MplCpp;
+    Figure fig;
+    fig.addSubPlot(221);
+    FigureCanvasQt canvas{std::move(fig)};
+    auto geometry = canvas.gca().pyobj().attr("get_geometry")();
+    TS_ASSERT_EQUALS(2, geometry[0]);
+    TS_ASSERT_EQUALS(2, geometry[1]);
+    TS_ASSERT_EQUALS(1, geometry[2]);
+  }
+};
+
+#endif // MPLCPP_FIGURECANVASQTTEST_H
diff --git a/qt/widgets/mplcpp/test/FigureTest.h b/qt/widgets/mplcpp/test/FigureTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..636289799ea6cf8462980c0029f553c441e10f1e
--- /dev/null
+++ b/qt/widgets/mplcpp/test/FigureTest.h
@@ -0,0 +1,37 @@
+#ifndef MPLCPP_FIGURETEST_H
+#define MPLCPP_FIGURETEST_H
+
+#include "MantidQtWidgets/MplCpp/Figure.h"
+
+#include <cxxtest/TestSuite.h>
+
+using MantidQt::Widgets::MplCpp::Figure;
+
+class FigureTest : public CxxTest::TestSuite {
+public:
+  static FigureTest *createSuite() { return new FigureTest; }
+  static void destroySuite(FigureTest *suite) { delete suite; }
+
+public:
+  void testDefaultFigureHasTightLayout() {
+    Figure fig;
+    TS_ASSERT_EQUALS(true, fig.pyobj().attr("get_tight_layout")());
+  }
+
+  void testConstructFigureWithNoTightLayout() {
+    Figure fig{false};
+    TS_ASSERT_EQUALS(false, fig.pyobj().attr("get_tight_layout")());
+  }
+
+  void testAddAxes() {
+    Figure fig{false};
+    TS_ASSERT_THROWS_NOTHING(fig.addAxes(0.1, 0.1, 0.9, 0.9));
+  }
+
+  void testSubPlot() {
+    Figure fig{false};
+    TS_ASSERT_THROWS_NOTHING(fig.addSubPlot(111));
+  }
+};
+
+#endif // FigureTEST_H
diff --git a/qt/widgets/mplcpp/test/Line2DTest.h b/qt/widgets/mplcpp/test/Line2DTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..3d44efeff3b53a2e3c6c0675afee1475a061226e
--- /dev/null
+++ b/qt/widgets/mplcpp/test/Line2DTest.h
@@ -0,0 +1,35 @@
+#ifndef MPLCPP_LINE2DTEST_H
+#define MPLCPP_LINE2DTEST_H
+
+#include "MantidQtWidgets/MplCpp/Line2D.h"
+#include <cxxtest/TestSuite.h>
+
+using namespace MantidQt::Widgets::MplCpp;
+
+class Line2DTest : public CxxTest::TestSuite {
+public:
+  static Line2DTest *createSuite() { return new Line2DTest; }
+  static void destroySuite(Line2DTest *suite) { delete suite; }
+
+  // ---------------------- success tests --------------------
+  void testConstructionRequiresMplLine2DObject() {
+    TS_ASSERT_THROWS_NOTHING(Line2D line(pyLine2D(), {}, {}));
+  }
+
+  // ---------------------- failure tests --------------------
+  void testConstructionWithNonLine2DObjectThrowsInvalidArgument() {
+    Python::Object obj{Python::NewRef(Py_BuildValue("(i)", 1))};
+    TS_ASSERT_THROWS(Line2D line(obj, {}, {}), std::invalid_argument);
+  }
+
+private:
+  Python::Object pyLine2D() {
+    // A Line2D requires x and y data sequences
+    const Python::Object data{Python::NewRef(Py_BuildValue("(f, f)", 0., 1.))};
+    const Python::Object linesModule{
+        Python::NewRef(PyImport_ImportModule("matplotlib.lines"))};
+    return linesModule.attr("Line2D")(data, data);
+  }
+};
+
+#endif // MPLCPP_LINE2DTEST_H
diff --git a/qt/widgets/mplcpp/test/MplCppTestInitialization.h b/qt/widgets/mplcpp/test/MplCppTestInitialization.h
new file mode 100644
index 0000000000000000000000000000000000000000..98dd2fd8fe4a87760387c3bc16cb995337a1918e
--- /dev/null
+++ b/qt/widgets/mplcpp/test/MplCppTestInitialization.h
@@ -0,0 +1,71 @@
+#ifndef MPLCPPTESTGLOBALINITIALIZATION_H
+#define MPLCPPTESTGLOBALINITIALIZATION_H
+
+#include "cxxtest/GlobalFixture.h"
+
+#include "MantidPythonInterface/core/NDArray.h"
+#include "MantidPythonInterface/core/VersionCompat.h"
+#include <QApplication>
+
+/**
+ * PythonInterpreter
+ *
+ * Uses setUpWorld/tearDownWorld to initialize & finalize
+ * Python
+ */
+class PythonInterpreter : CxxTest::GlobalFixture {
+public:
+  bool setUpWorld() override {
+    Py_Initialize();
+    Mantid::PythonInterface::importNumpy();
+    return Py_IsInitialized();
+  }
+
+  bool tearDown() override {
+    // Some test methods may leave the Python error handler with an error
+    // set that confuse other tests when the executable is run as a whole
+    // Clear the errors after each suite method is run
+    PyErr_Clear();
+    return CxxTest::GlobalFixture::tearDown();
+  }
+
+  bool tearDownWorld() override {
+    Py_Finalize();
+    return true;
+  }
+};
+
+/**
+ * QApplication
+ *
+ * Uses setUpWorld/tearDownWorld to initialize & finalize
+ * QApplication object
+ */
+class QApplicationHolder : CxxTest::GlobalFixture {
+public:
+  bool setUpWorld() override {
+    int argc(0);
+    char **argv = {};
+    m_app = new QApplication(argc, argv);
+    return true;
+  }
+
+  bool tearDownWorld() override {
+    delete m_app;
+    return true;
+  }
+
+private:
+  QApplication *m_app;
+};
+
+//------------------------------------------------------------------------------
+// Static definitions
+//
+// We rely on cxxtest only including this file once so that the following
+// statements do not cause multiple-definition errors.
+//------------------------------------------------------------------------------
+static PythonInterpreter PYTHON_INTERPRETER;
+static QApplicationHolder MAIN_QAPPLICATION;
+
+#endif // MPLCPPTESTGLOBALINITIALIZATION_H
diff --git a/qt/widgets/mplcpp/test/ScalarMappableTest.h b/qt/widgets/mplcpp/test/ScalarMappableTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..8aa8bb59bf16f043e5b0d57ab5749b4344f719fe
--- /dev/null
+++ b/qt/widgets/mplcpp/test/ScalarMappableTest.h
@@ -0,0 +1,47 @@
+#ifndef MPLCPP_SCALARMAPPABLETEST_H
+#define MPLCPP_SCALARMAPPABLETEST_H
+
+#include "MantidQtWidgets/MplCpp/Colormap.h"
+#include "MantidQtWidgets/MplCpp/Colors.h"
+#include "MantidQtWidgets/MplCpp/ScalarMappable.h"
+
+#include <QRgb>
+
+#include <cxxtest/TestSuite.h>
+
+using MantidQt::Widgets::MplCpp::Normalize;
+using MantidQt::Widgets::MplCpp::ScalarMappable;
+using MantidQt::Widgets::MplCpp::getCMap;
+
+class ScalarMappableTest : public CxxTest::TestSuite {
+public:
+  static ScalarMappableTest *createSuite() { return new ScalarMappableTest; }
+  static void destroySuite(ScalarMappableTest *suite) { delete suite; }
+
+public:
+  // ----------------------- Success tests ------------------------
+  void testConstructionWithValidCMapAndNormalize() {
+    TS_ASSERT_THROWS_NOTHING(
+        ScalarMappable mappable(Normalize(-1, 1), getCMap("jet")));
+  }
+
+  void testtoRGBAWithNoAlphaGivesDefault() {
+    ScalarMappable mappable(Normalize(-1, 1), getCMap("jet"));
+    auto rgba = mappable.toRGBA(0.0);
+    TS_ASSERT_EQUALS(124, qRed(rgba));
+    TS_ASSERT_EQUALS(255, qGreen(rgba));
+    TS_ASSERT_EQUALS(121, qBlue(rgba));
+    TS_ASSERT_EQUALS(255, qAlpha(rgba));
+  }
+
+  void testtoRGBAWithAlpha() {
+    ScalarMappable mappable(Normalize(-1, 1), getCMap("jet"));
+    auto rgba = mappable.toRGBA(0.0, 0.5);
+    TS_ASSERT_EQUALS(124, qRed(rgba));
+    TS_ASSERT_EQUALS(255, qGreen(rgba));
+    TS_ASSERT_EQUALS(121, qBlue(rgba));
+    TS_ASSERT_EQUALS(127, qAlpha(rgba));
+  }
+};
+
+#endif // MPLCPP_SCALARMAPPABLETEST_H
diff --git a/qt/widgets/mplcpp/test/SipTest.h b/qt/widgets/mplcpp/test/SipTest.h
new file mode 100644
index 0000000000000000000000000000000000000000..7e72b106d6860d5a533cd050c43dc9be3cc1166f
--- /dev/null
+++ b/qt/widgets/mplcpp/test/SipTest.h
@@ -0,0 +1,42 @@
+#ifndef MPLCPP_SIPTEST_H
+#define MPLCPP_SIPTEST_H
+
+#include <cxxtest/TestSuite.h>
+
+#include "MantidQtWidgets/MplCpp/BackendQt.h"
+#include "MantidQtWidgets/MplCpp/Python/Sip.h"
+
+#include <QWidget>
+
+using MantidQt::Widgets::MplCpp::backendModule;
+namespace Python = MantidQt::Widgets::MplCpp::Python;
+
+class SipTest : public CxxTest::TestSuite {
+public:
+  static SipTest *createSuite() { return new SipTest; }
+  static void destroySuite(SipTest *suite) { delete suite; }
+
+public:
+  // ----------------- success tests ---------------------
+  void testExtractWithSipWrappedTypeSucceeds() {
+    Python::Object mplBackend{backendModule()};
+    Python::Object fig{
+        Python::NewRef(PyImport_ImportModule("matplotlib.figure"))
+            .attr("Figure")()};
+    Python::Object pyCanvas{mplBackend.attr("FigureCanvasQT")(fig)};
+    QWidget *w{nullptr};
+    TS_ASSERT_THROWS_NOTHING(w = Python::extract<QWidget>(pyCanvas));
+    TS_ASSERT(w);
+  }
+
+  // ----------------- failure tests ---------------------
+
+  void testExtractWithNonSipTypeThrowsException() {
+    const Python::Object nonSipType{
+        Python::NewRef(Py_BuildValue("(ii)", 1, 2))};
+    struct Foo;
+    TS_ASSERT_THROWS(Python::extract<Foo>(nonSipType), std::runtime_error);
+  }
+};
+
+#endif // MPLCPP_SIPTEST_H