From d6c8d039a2f828e5ea009d7181c8145d23932d69 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Adjemian=20=28Ry=C3=BBk=29?=
 <stepan@adjemian.eu>
Date: Thu, 11 May 2023 14:35:51 +0200
Subject: [PATCH] Add routine to plot expressions from dseries objects.

Example:

>> ds1 = dseries(randn(100,3), dates('2000Q1'), {'x','y','z'});
>> ds2 = dseries(randn(100,3), dates('2000Q1'), {'x','y','z'});
>> dplot --expression 2*cumsum(x/y(-1)-1) --dseries toto --dseries noddy --range 2001Q1:2024Q1
---
 src/initialize_dseries_class.m |   5 +-
 src/utilities/dplot/dplot.m    | 238 +++++++++++++++++++++++++++++++++
 2 files changed, 241 insertions(+), 2 deletions(-)
 create mode 100644 src/utilities/dplot/dplot.m

diff --git a/src/initialize_dseries_class.m b/src/initialize_dseries_class.m
index 6a8ac93..082a7eb 100644
--- a/src/initialize_dseries_class.m
+++ b/src/initialize_dseries_class.m
@@ -1,6 +1,6 @@
 function initialize_dseries_class()
 
-% Copyright © 2015-2021 Dynare Team
+% Copyright © 2015-2023 Dynare Team
 %
 % This code is free software: you can redistribute it and/or modify
 % it under the terms of the GNU General Public License as published by
@@ -38,7 +38,8 @@ p = {'mdbnomics2dseries'; ...
      'utilities/cumulate'; ...
      'utilities/struct'; ...
      'utilities/misc'; ...
-     'utilities/x13'};
+     'utilities/x13'; ...
+     'utilities/dplot'};
 
 % Add missing routines if dynare is not in the path
 if ~exist('isint','file')
diff --git a/src/utilities/dplot/dplot.m b/src/utilities/dplot/dplot.m
new file mode 100644
index 0000000..d8f9f27
--- /dev/null
+++ b/src/utilities/dplot/dplot.m
@@ -0,0 +1,238 @@
+function dplot(varargin)
+
+% Plot expressions extracting data from different dseries objects.
+%
+% EXAMPLE
+%
+% >> ds1 = dseries(randn(100,3), dates('2000Q1'), {'x','y','z'});
+% >> ds2 = dseries(randn(100,3), dates('2000Q1'), {'x','y','z'});
+% >> dplot --expression 2*cumsum(x/y(-1)-1) --dseries toto --dseries noddy --range 2001Q1:2024Q1
+%
+% Will produce plots of 2*cumsum(x/y(-1)-1), where x and y are variables in objects toto and noddy,
+% in the same figure.
+%
+% REMARKS
+% [1] More than one --expression argument is allowed
+% [2] --expression arguments must come first
+% [3] For each dseries object we plot all the expressions. We use two nested loops, the outer loop is over
+%     the dseries objects and the inner loop over the expressions. This determines the ordering of the plotted
+%     lines.
+% [4] All dseries objects must be defined in the calling workspace, if a dseries object is missing the routine
+%     throws a warning (we only build the plots for the available dseries objects), if all dseries objects are
+%     missing the routine throws an error.
+% [5] An expression involves variables (defined in dseries objects belonging to the caller workspace), parameters
+%     (defined in the caller workspace) or numbers. dseries methods returning dseries objects can also be used
+%     in an expression.
+% [6] If the --range argument is missing the range is defined by the dates property of the first dseries object. In
+%     in this case, lags/leads or diff on the variables should not be used.
+
+% Copyright © 2023 Dynare Team
+%
+% This file is part of Dynare.
+%
+% Dynare 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.
+%
+% Dynare 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 Dynare.  If not, see <https://www.gnu.org/licenses/>.
+
+    % Get expressions to be plotted
+    expressions = getexpressions(varargin);
+
+    % Get dseriesnames
+    names = getdseriesnames(varargin);
+
+    % Put all dseries objects in a cell array
+    data = cell(size(names));
+    Names = names;
+    for i=1:length(Names)
+        try
+            data{i} = evalin('caller', Names{i});
+        catch
+            warning('dplot:: dseries object %s is unknown.', Names{i})
+            names(i) = [];
+        end
+    end
+    assert(length(names)>0, 'dplot:: None of the dseries objects declared is available.')
+    data = data(~cellfun(@isempty, data));
+    % Check that all elements in data are actually dseries objects
+    for i=1:length(data)
+        if ~isdseries(data{i})
+            error('%s is not a dseries object.', names{i})
+        end
+    end
+
+    range = getrange(varargin);
+    if isempty(range)
+        range = data{1}.dates;
+    end
+
+    ts = cell(1, length(data)*length(expressions)); l = 0;
+    Expressions = cell(size(expressions));
+
+    for i=1:length(data)
+        for j=1:length(expressions)
+            % Check that brackets are balanced
+            openbrackets = regexpi(expressions{j}, '(','match');
+            closebrackets = regexpi(expressions{j}, ')','match');
+            assert(length(openbrackets)==length(closebrackets), 'dplot:: Brackets are not balanced.')
+            % Get all tokens
+            tokens = unique(regexpi(expressions{j}, '\w*','match'));
+            % Filter out dseries methods
+            tokens = setdiff(tokens, allowedmethods());
+            % Filter out numbers
+            tokens = tokens(cellfun(@(x)isnan(str2double(x)), tokens));
+            listofvariables = tokens(cellfun(@(x)ismember(x,data{i}.name), tokens));
+            listofparameters = setdiff(tokens, listofvariables);
+            % Test if parameters are defined scalars in the caller workspace
+            for k=1:length(listofparameters)
+                if ~evalin('caller', sprintf('exist(''%s'', ''var'')', listofparameters{k}))
+                    error('dplot:: %s is not a known object.', listofparameters{k})
+                end
+                if ~evalin('caller', sprintf('isscalar(%s) && isnumeric(%s)', listofparameters{k}, listofparameters{k}))
+                    error('dplot:: Parameter %s has to be a numeric scalar.', listofparameters{k})
+                end
+                eval(sprintf('%s = evalin(''caller'', sprintf(''%%s'', listofparameters{k}));', listofparameters{k}))
+            end
+            Expressions{j} = rewrite(expressions{j}, listofvariables, i);
+            l = l+1;
+            ts{l} = eval(Expressions{j});
+        end
+    end
+    %
+    % Build plots
+    %
+    plot(1:length(range), ts{1}(range).data)
+    if length(ts)>1
+        hold on
+        for i=2:length(ts)
+            plot(1:length(range), ts{i}(range).data)
+        end
+        hold off
+    end
+    axis tight
+    %
+    % Reset x-axis labels (with dates)
+    %
+    ax = gca;
+    ax.XTick = unique(round(ax.XTick)); % Only keep integer labels
+    id = ax.XTick;
+    dd = strings(range(id));
+    ax.XTickLabel = dd;
+end
+
+function expr = getexpressions(cellarray)
+
+% Return expressions to be plotted.
+%
+% INPUTS
+% - cellarray     [char]      1×n cell array of row char arrays.
+%
+% OUTPUTS
+% - expr          [char]      1×p cell array of row char arrays.
+
+    [epos, dpos, rpos, zpos] = positions(cellarray);
+    expr = cell(1, length(rpos));
+    Epos = [epos, zpos];
+    for i=1:length(epos)
+        tmp = cellarray(Epos(i)+1:Epos(i+1)-1);
+        expr{i} = strcat(tmp{:});
+    end
+end
+
+function names = getdseriesnames(cellarray)
+
+% Return expressions to be plotted.
+%
+% INPUTS
+% - cellarray     [char]      1×n cell array of row char arrays.
+%
+% OUTPUTS
+% - names         [char]      1×p cell array of row char arrays.
+
+    [~, dpos] = positions(cellarray);
+
+    names = cellarray(dpos+1);
+end
+
+function range = getrange(cellarray)
+
+% Return period range for the plots.
+%
+% INPUTS
+% - cellarray     [char]      1×n cell array of row char arrays.
+%
+% OUTPUTS
+% - range         [dates]
+
+    [~, ~, rpos] = positions(cellarray);
+
+    if length(rpos)==0
+        range = dates();
+        return
+    end
+
+    str = cellarray{rpos+1};
+    sepid = strfind(str, ':');
+    assert(~isempty(sepid), 'dplot:: --range argument is wrong.')
+    assert(length(sepid)==1, 'dplot:: Only one semicolon is allowed in --range argument.')
+    assert(isdate(str(1:sepid-1)), 'dplot:: --range argument is wrong (first declared period cannot be interpreted as a dates object).')
+    assert(isdate(str(sepid+1:end)), 'dplot:: --range argument is wrong (second declared period cannot be interpreted as a dates object).')
+    range = dates(str(1:sepid-1)):dates(str(sepid+1:end));
+
+end
+
+function [epos, dpos, rpos, zpos] = positions(cellarray)
+
+% Return  positions of the arguments.
+%
+% INPUTS
+% - cellarray     [char]      1×n cell array of row char arrays.
+%
+% OUTPUTS
+% - epos          [integer]   1×pₑ vector, indices for the --expression arguments.
+% - dpos          [integer]   1×pₛ vector, indices for the --dseries arguments.
+% - rpos          [integer]   scalar, index of the --range argument.
+% - zpos          [integer]   first index of non --expression argument.
+
+    % Indices for --expression arguments.
+    epos = find(strcmp('--expression', cellarray));
+    if isempty(epos)
+        error('dplot::positions: --expression argument is mandatory.')
+    end
+    % Indices for the --dseries arguments.
+    dpos = find(strcmp('--dseries', cellarray));
+    if isempty(dpos)
+        error('dplot::positions: --dseries argument is mandatory.')
+    end
+    % Index for --range argument.
+    rpos = find(strcmp('--range', cellarray));
+    assert(length(rpos)==1 || length(rpos)==0, 'dplot::positions: Only one range is allowed.')
+    % Define index for the first argument name different from --expression
+    if isempty(rpos)
+        zpos = dpos(1);
+    else
+        zpos = min(dpos(1), rpos);
+    end
+    % Check that expressions are coming before other arguments
+    assert(max(epos)<zpos, 'dplot::positions: --expression must come befor the other arguments.')
+end
+
+function m = allowedmethods()
+    m = {'abs', 'center', 'cumprod', 'cumsum', 'detrend', 'dgrowth', 'diff', 'exp', 'log', 'hdiff', 'hgrowth', 'baxter_king_filter', ...
+         'hpcycle', 'hptrend', 'onesidedhpcycle', 'onesidedhptrend', 'lag', 'lead', 'mdiff', 'mgrowth', 'qdiff', 'qgrowth', 'mean', 'std', ...
+         'ydiff', 'ygrowth'};
+end
+
+function expr = rewrite(expr, list, index)
+    for i=1:length(list)
+        expr = regexprep(expr, sprintf('\\<%s\\>', list{i}), sprintf('data{%u}.%s', index, list{i}));
+    end
+end
-- 
GitLab