## Copyright (C) 2022 Andreas Bertsatos <abertsatos@biol.uoa.gr>
## Based on previous work by Paul Kienzle <pkienzle@users.sf.net> originally
## granted to the public domain.
##
## This file is part of the statistics package for GNU Octave.
##
## This program 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.
##
## This program 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.
##
## You should have received a copy of the GNU General Public License along with
## this program; if not, see <http://www.gnu.org/licenses/>.

## -*- texinfo -*-
## @deftypefn  {Function File} {} normplot (@var{x})
## @deftypefnx {Function File} {} normplot (@var{ax}, @var{x})
## @deftypefnx {Function File} {@var{h} =} normplot (@dots{})
##
## Produce normal probability plot of the data in @var{x}.  If @var{x} is a
## matrix, @code{normplot} plots the data for each column.  NaN values are
## ignored.
##
## @code{@var{h} = normplot (@var{ax}, @var{x})} takes a handle @var{ax} in
## addition to the data in @var{x} and it uses that axes for plotting.  You may
## get this handle of an existing plot with @code{gca}.
##
## The line joining the 1st and 3rd quantile is drawn solid whereas its
## extensions to both ends are dotted. If the underlying distribution is normal,
## the points will cluster around the solid part of the line. Other distribution
## types will introduce curvature in the plot.
##
## @seealso{cdfplot, wblplot}
## @end deftypefn

function h = normplot (varargin)

  ## Check for valid input arguments
  narginchk (1, 2);
  ## Parse input arguments
  if (nargin == 1)
    ax = [];
    x = varargin{1};
  else
    ax = varargin{1};
    ## Check that ax is a valid axis handle
    try
      isstruct (get (ax));
    catch
      error ("normplot: invalid handle %f.", ax);
    end_try_catch
    x = varargin{2};
  endif
  ## Check that x is a vector or a 2-D matrix
  if (isscalar (x) || ndims (x) > 2)
    error ("normplot: x must be a vector or a 2-D matrix handle.");
  endif
  ## If x is a vector, make it a column vector
  if (rows (x) == 1)
    x = x(:);
  endif
  ## If ax is empty, create a new axes
  if (isempty (ax))
    ax = newplot ();
  endif
  ## Get number of column vectors in x
  col = size (x, 2);
  ## Process each column and plot data and fit lines
  color = {'blue', 'black', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow'};
  hold on;
  for i = 1:col
    xc = x(:,i);
    ## Remove NaNs, get min, max, and range
    xc(isnan (xc)) = [];
    if (isempty (xc))
      break;
    endif
    ## Transform data
    row_xc = rows (xc);
    yc = norminv (([1:row_xc]' - 0.5) / row_xc);
    xc = sort (xc);
    ## Find quartiles
    q1x = prctile (xc, 25);
    q3x = prctile (xc, 75);
    q1y = prctile (yc, 25);
    q3y = prctile (yc, 75);
    qx = [q1x; q3x];
    qy = [q1y; q3y];
    ## Calculate coordinates and limits for fitting lines
    dx = q3x - q1x;
    dy = q3y - q1y;
    slope = dy ./ dx;
    centerx = (q1x + q3x)/2;
    centery = (q1y + q3y)/2;
    maxx = max (xc);
    minx = min (xc);
    maxy = centery + slope.*(maxx - centerx);
    miny = centery - slope.*(centerx - minx);
    yinter = centery - slope.*(centerx);
    mx = [minx; maxx];
    my = [miny; maxy];
    ## Plot data and corresponding reference lines in the same color,
    ## following the default color order.  Plot reference line first,
    ## followed by the data, so that data will be on top of reference line.
    h_end(i) = line (ax, mx, my, 'LineStyle', '-.', 'Marker', 'none', ...
                                 'color', color{mod(i,8)});
    h_mid(i) = line (ax, qx, qy, 'LineStyle', '-', 'Marker', 'none', ...
                                 'color', color{mod(i,8)});
    h_dat(i) = line (ax, xc, yc, 'LineStyle', 'none', 'Marker', '+', ...
                                 'color', color{mod(i,8)});
  endfor
  hold off;
  ## Change colors for single column vector
  if (i == 1)
    set (h_dat, 'Color', 'b');
    set (h_mid, 'Color', 'r');
    set (h_end, 'Color', 'r');
  endif
  ## Bundle handles together if output requested
  if (nargout > 0)
    h = [h_dat, h_mid, h_end]';
  endif
  ## Plot labels
  title 'Normal Probability Plot'
  ylabel 'Probability'
  xlabel 'Data'
  ## Plot grid
  p = [0.001, 0.003, 0.01, 0.02, 0.05, 0.10, 0.25, 0.5, ...
       0.75, 0.90, 0.95, 0.98, 0.99, 0.997, 0.999];
  label = {'0.001', '0.003', '0.01', '0.02', '0.05', '0.10', '0.25', '0.50', ...
           '0.75', '0.90', '0.95', '0.98', '0.99', '0.997', '0.999'};
  tick = norminv (p, 0, 1);
  set (ax, 'ytick', tick, 'yticklabel', label);
  ## Set view range with a bit of space around data
  range = nanmax (x(:)) - nanmin (x(:));
  if (range > 0)
    minxaxis = nanmin (x(:)) - 0.025 * range;
    maxxaxis = nanmax (x(:)) + 0.025 * range;
  else
    minxaxis = nanmin (x(:)) - 1;
    maxxaxis = nanmax (x(:)) + 1;
  endif
  minyaxis = norminv (0.25 ./ row_xc, 0, 1);
  maxyaxis = norminv ((row_xc - 0.25) ./ row_xc, 0, 1);
  set (ax, 'ylim', [minyaxis, maxyaxis], 'xlim', [minxaxis, maxxaxis]);
  grid (ax, 'on');
  box (ax, 'off');
endfunction

%!demo
%! h = normplot ([1:20]);
%!demo
%! h = normplot ([1:20;5:2:44]');
%!demo
%! ax = newplot ();
%! h = normplot (ax, [1:20]);
%! ax = gca;
%! h = normplot (ax, [-10:10]);
%! set (ax, 'xlim', [-11, 21]);

## Test input validation
%!error normplot ();
%!error normplot (23);
%!error normplot (23, [1:20]);
%!error normplot (ones (3,4,5));

## Test plotting
%!test
%! hf = figure ('visible', 'off');
%! unwind_protect
%!   ax = newplot (hf);
%!   h = normplot (ax, [1:20]);
%!   ax = gca;
%!   h = normplot (ax, [-10:10]);
%!   set (ax, 'xlim', [-11, 21]);
%! unwind_protect_cleanup
%!   close (hf);
%! end_unwind_protect
%!test
%! hf = figure ('visible', 'off');
%! unwind_protect
%!   h = normplot ([1:20;5:2:44]');
%! unwind_protect_cleanup
%!   close (hf);
%! end_unwind_protect
