========================
Adding a new RailPlotter
========================

All of the various plotting classes 
are implemented as subclasses of the :py:class:`rail.plotting.plotter.RailPlotter` class.
A ``RailPlotter`` is intended to take a particular set of inputs and configuration parameters, 
run a single bit of analysis, and produce one or more plots.  The inputs, outputs
and configuration parameters are all defined in particular ways to allow ``RailPlotter``
objects to be integrated into larger data analysis pipelines.


New RailPlotter Example
-----------------------

The following example has all of the required pieces of a ``RailPlotter`` and almost nothing else.

.. code-block:: python

    class PZPlotterPointEstimateVsTrueHist2D(RailPlotter):
        """Class to make a 2D histogram of p(z) point estimates
        versus true redshift
        """

        config_options: dict[str, StageParameter] = RailPlotter.config_options.copy()
        config_options.update(
            z_min=StageParameter(float, 0.0, fmt="%0.2f", msg="Minimum Redshift"),
            z_max=StageParameter(float, 3.0, fmt="%0.2f", msg="Maximum Redshift"),
            n_zbins=StageParameter(int, 150, fmt="%i", msg="Number of z bins"),
        )

        input_type = RailPZPointEstimateDataset

        def _make_2d_hist_plot(
            self,
            prefix: str,
            truth: np.ndarray,
            pointEstimate: np.ndarray,
            dataset_holder: RailDatasetHolder | None = None,
        ) -> RailPlotHolder:
            figure, axes = plt.subplots()
            bin_edges = np.linspace(
                self.config.z_min, self.config.z_max, self.config.n_zbins + 1
            )
            axes.hist2d(
                truth,
                pointEstimate,
                bins=(bin_edges, bin_edges),
            )
            plt.xlabel("True Redshift")
            plt.ylabel("Estimated Redshift")
            plot_name = self._make_full_plot_name(prefix, "")
            return RailPlotHolder(
                name=plot_name, figure=figure, plotter=self, dataset_holder=dataset_holder
            )

        def _make_plots(self, prefix: str, **kwargs: Any) -> dict[str, RailPlotHolder]:
            find_only = kwargs.get("find_only", False)
            figtype = kwargs.get("figtype", "png")
            dataset_holder = kwargs.get("dataset_holder")
            out_dict: dict[str, RailPlotHolder] = {}
            truth: np.ndarray = kwargs["truth"]
            pointEstimate: np.ndarray = kwargs["pointEstimate"]
            if find_only:
                plot_name = self._make_full_plot_name(prefix, "")
                assert dataset_holder
                plot = RailPlotHolder(
                    name=plot_name,
                    path=os.path.join(dataset_holder.config.name, f"{plot_name}.{figtype}"),
                    plotter=self,
                    dataset_holder=dataset_holder,
                )
            else:
                plot = self._make_2d_hist_plot(
                    prefix=prefix,
                    truth=truth,
                    pointEstimate=pointEstimate,
                    dataset_holder=dataset_holder,
                )
            out_dict[plot.name] = plot
            return out_dict

      
The required pieces, in the order that they appear are:

#. The ``PZPlotterPointEstimateVsTrueHist2D(RailPlotter):`` defines a class called ``PZPlotterPointEstimateVsTrueHist2D`` and specifies that it inherits from ``RailPlotter``.

#. The ``config_options`` lines define the configuration parameters for this class, as well as their default values.  Note that here we are copying the configuration parameters from the ``RailPlotter`` as well as defining some new ones.

#. The ``input_type = RailPZPointEstimateDataset`` specifies that this
   plotter expects a :py:class:`rail.plotting.pz_plotters.RailPZPointEstimateDataset` type dataset, which in
   this case is an dict with one item (called ``truth``) that is a
   numpy array, and a second item (called ``pointEstimate``) that is a
   also a numpy array.

#. The ``__init__`` method does any class-specific initialization.  In this case there isn't any and the method is superfluous.

#. The ``_make_2d_hist_plot(...)`` method does the actual work, note it takes some of the same arguements are define in ``inputs`` and that it uses ``self.config`` to access the configuration parameters.

#. The ``_make_plots(self, prefix: str, **kwargs: Any)`` method provides an interface to format the data for _make_2d_hist_plot(), the arguments to this function are specified in the ``RailPlotter`` class
