Repository: fitbenchmarking/fitbenchmarking

Issue:
Add error bars
We need to add error bars to the fitting report.

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/67254785/badge)](https://coveralls.io/builds/67254785)

coverage: 90.82% (-0.01%) from 90.832%
when pulling **b1a7dabddae3df01c937185e22f0d82c9bc8e269 on RabiyaF:1245-error-bars**
into **76eb8d7dd3c0c9c108768946cb2e82e4d7dfab8d on fitbenchmarking:master**.


Patch:
diff --git a/fitbenchmarking/results_processing/plots.py b/fitbenchmarking/results_processing/plots.py
index a2350fa8..da108f4a 100644
--- a/fitbenchmarking/results_processing/plots.py
+++ b/fitbenchmarking/results_processing/plots.py
@@ -18,32 +18,36 @@ class Plot:
     """
     Class providing plotting functionality.
     """
-    data_marker = {"symbol": "x",
-                   "color": "black"}
-    best_fit_line = {"dash": "dot",
-                     "color": '#6699ff'}
-    legend_options = {"yanchor": "top",
-                      "y": 0.99,
-                      "xanchor": "left",
-                      "x": 0.01,
-                      "bgcolor": 'rgba(0,0,0,0.1)'}
-    summary_best_plot_line = {"width": 2}
-    summary_plot_line = {"width": 1}
+    _data_marker = {"symbol": "x",
+                    "color": "black"}
+    _best_fit_line = {"dash": "dot",
+                      "color": '#6699ff'}
+    _legend_options = {"yanchor": "top",
+                       "y": 0.99,
+                       "xanchor": "left",
+                       "x": 0.01,
+                       "bgcolor": 'rgba(0,0,0,0.1)'}
+    _summary_best_plot_line = {"width": 2}
+    _summary_plot_line = {"width": 1}
+    _error_dict = dict(type='data',
+                       array=None,
+                       thickness=1,
+                       width=4)
 
     def __init__(self, best_result, options, figures_dir):
         self.result = best_result
+        self.plots_failed = False
 
-        self.plots_failed = True
         if self.result.multivariate:
+            self.plots_failed = True
             raise PlottingError(
                 'Plots cannot be generated for multivariate problems')
         if self.result.problem_format == 'horace':
+            self.plots_failed = True
             raise PlottingError(
                 'Plots cannot be generated for Horace problems')
-        self.plots_failed = False
 
         self.options = options
-
         self.figures_dir = figures_dir
 
     @staticmethod
@@ -85,14 +89,16 @@ class Plot:
                       color="minimizer",
                       title=self.result.name,
                       markers=True)
+        self._error_dict['array'] = df["e"][df["minimizer"] == 'Data']
         # add the raw data as a scatter plot
         fig.add_trace(go.Scatter(
             x=df["x"][df["minimizer"] == 'Data'],
             y=df["y"][df["minimizer"] == 'Data'],
+            error_y=self._error_dict,
             mode='markers',
             name='Data',
-            marker=self.data_marker))
-        fig.update_layout(legend=self.legend_options)
+            marker=self._data_marker))
+        fig.update_layout(legend=self._legend_options)
 
         if self.result.plot_scale in ["loglog", "logx"]:
             fig.update_xaxes(type="log")
@@ -139,6 +145,7 @@ class Plot:
         y_best = df["y"][df["best"]]
         x_data = df["x"][df["minimizer"] == 'Data']
         y_data = df["y"][df["minimizer"] == 'Data']
+        self._error_dict['array'] = df["e"][df["minimizer"] == 'Data']
 
         for minimizer in df['minimizer'].unique():
             if minimizer not in ["Data", "Starting Guess"]:
@@ -157,16 +164,17 @@ class Plot:
                                    y=y_best,
                                    mode='lines',
                                    name=name,
-                                   line=self.best_fit_line))
+                                   line=self._best_fit_line))
                 # add the raw data as a scatter plot
                 fig.add_trace(
                     go.Scatter(
                         x=x_data,
                         y=y_data,
+                        error_y=self._error_dict,
                         mode='markers',
                         name='Data',
-                        marker=self.data_marker))
-                fig.update_layout(legend=self.legend_options)
+                        marker=self._data_marker))
+                fig.update_layout(legend=self._legend_options)
 
                 if self.result.plot_scale in ["loglog", "logx"]:
                     fig.update_xaxes(type="log")
@@ -266,22 +274,22 @@ class Plot:
                                        error_y=error_y,
                                        mode='markers',
                                        name='Data',
-                                       marker=cls.data_marker))
+                                       marker=cls._data_marker))
 
         for (key, results), colour in zip(categories.items(), plotly_colours):
             # Plot category
             for result in results:
                 if result.params is not None:
-                    line = cls.summary_best_plot_line \
-                        if result.is_best_fit else cls.summary_plot_line
+                    line = cls._summary_best_plot_line \
+                        if result.is_best_fit else cls._summary_plot_line
 
                     line["color"] = colour
                     label = key if result.is_best_fit else ''
                     if result.is_best_fit:
-                        line = cls.summary_best_plot_line
+                        line = cls._summary_best_plot_line
                         line["color"] = colour
                     else:
-                        line = cls.summary_plot_line
+                        line = cls._summary_plot_line
                         transparency = 0.5
                         line["color"] = 'rgba' + colour[3:-1] + ', ' + \
                             str(transparency) + ')'


Test patch:
diff --git a/fitbenchmarking/results_processing/tests/test_plots.py b/fitbenchmarking/results_processing/tests/test_plots.py
index acfcf69c..4b129d7b 100644
--- a/fitbenchmarking/results_processing/tests/test_plots.py
+++ b/fitbenchmarking/results_processing/tests/test_plots.py
@@ -45,6 +45,15 @@ def load_mock_result():
     return results
 
 
+def find_error_bar_count(path):
+    """
+    Reads an html file and counts the error_y count
+    """
+    with open(path, 'r', encoding='utf-8') as file:
+        html_content = file.read()
+    return html_content.count("error_y")
+
+
 class PlotTests(unittest.TestCase):
     """
     Test the plot object is correct.
@@ -108,7 +117,8 @@ class PlotTests(unittest.TestCase):
 
     def test_plot_initial_guess_create_files(self):
         """
-        Test that initial plot creates a file.
+        Test that initial plot creates a file and errorbars are
+        added to the plot.
         """
         file_name = self.plot.plot_initial_guess(
             self.df[('Fake_Test_Data', 'prob_1')])
@@ -116,6 +126,7 @@ class PlotTests(unittest.TestCase):
         self.assertEqual(file_name, 'start_for_prob_1.html')
         path = os.path.join(self.figures_dir, file_name)
         self.assertTrue(os.path.exists(path))
+        self.assertEqual(find_error_bar_count(path), 2)
 
     def test_best_filename_return(self):
         """
@@ -127,15 +138,24 @@ class PlotTests(unittest.TestCase):
 
     def test_plotly_fit_create_files(self):
         """
-        Test that plotly_fit creates a file.
+        Test that plotly_fit creates a file and errorbars are
+        added to the plot.
         """
         file_names = self.plot.plotly_fit(
             self.df[('Fake_Test_Data', 'prob_1')])
 
-        self.assertEqual(file_names['m10_[s1]_jj0'],
-                         'm10_[s1]_jj0_fit_for_cf1_prob_1.html')
-        path = os.path.join(self.figures_dir, file_names['m10_[s1]_jj0'])
-        self.assertTrue(os.path.exists(path))
+        for m, s, j in zip(['m10', 'm11', 'm01', 'm00',
+                            'm10', 'm11', 'm01', 'm00'],
+                           ['s1', 's1', 's0', 's0',
+                            's1', 's1', 's0', 's0'],
+                           ['jj0', 'jj0', 'jj0', 'jj0',
+                            'jj1', 'jj1', 'jj1', 'jj1']):
+            file_name_prefix = f'{m}_[{s}]_{j}'
+            self.assertEqual(file_names[file_name_prefix],
+                             file_name_prefix+'_fit_for_cf1_prob_1.html')
+            path = os.path.join(self.figures_dir, file_names[file_name_prefix])
+            self.assertTrue(os.path.exists(path))
+            self.assertEqual(find_error_bar_count(path), 2)
 
     def test_multivariate_plot(self):
         """