|
| 1 | +function plot_timeseries(Data, Mdata, variables, depth, basename, varargin) |
| 2 | +% plot_timeseries This function is part of the |
| 3 | +% MATLAB toolbox for accessing BGC Argo float data. |
| 4 | +% |
| 5 | +% USAGE: |
| 6 | +% plot_timeseries(Data, Mdata, variables, depth, basename, varargin) |
| 7 | +% |
| 8 | +% DESCRIPTION: |
| 9 | +% This function plots time series of one or more specified float(s) for |
| 10 | +% the specified variable(s). |
| 11 | +% |
| 12 | +% PREREQUISITE: |
| 13 | +% Sprof file(s) for the specified float(s) must exist locally. |
| 14 | +% |
| 15 | +% INPUTS: |
| 16 | +% Data : struct that must contain the PRES field and the given |
| 17 | +% variables (_ADJUSTED fields are used if available) |
| 18 | +% Mdata : struct that must contain the WMO_ID field |
| 19 | +% variables : cell array with names of the measured fields (e.g., DOXY) |
| 20 | +% depth : array of depth levels to plot |
| 21 | +% basename : if not empty, create png files of all plots; |
| 22 | +% if per_float is used, the file names will be |
| 23 | +% <basename>_<WMOID>_<variable>.png, |
| 24 | +% if per_float is not used, the file names will be |
| 25 | +% <basename>_<variable>.png |
| 26 | +% |
| 27 | +% OPTIONAL INPUTS: |
| 28 | +% 'end',end_date : end date (in one of the following formats: |
| 29 | +% [YYYY MM DD HH MM SS] or [YYYY MM DD]) |
| 30 | +% 'legend',legend : legend (string) can be 'yes' to show legend along with |
| 31 | +% plot (default) or 'no' |
| 32 | +% 'per_float',per_float : show time series separately for each float (1) |
| 33 | +% or all in one plot (0); default: 1 |
| 34 | +% 'qc',flags : show only values with the given QC flags (array) |
| 35 | +% 0: no QC was performed; |
| 36 | +% 1: good data; |
| 37 | +% 2: probably good data; |
| 38 | +% 3: probably bad data that are potentially correctable; |
| 39 | +% 4: bad data; |
| 40 | +% 5: value changed; |
| 41 | +% 6,7: not used; |
| 42 | +% 8: estimated value; |
| 43 | +% 9: missing value |
| 44 | +% default setting: 0:9 (all flags) |
| 45 | +% See Table 7 in Bittig et al.: |
| 46 | +% https://www.frontiersin.org/files/Articles/460352/fmars-06-00502-HTML-r1/image_m/fmars-06-00502-t007.jpg |
| 47 | +% 'raw',raw : plot raw, i.e., unadjusted data if set to 'yes'; |
| 48 | +% default: 'no' (i.e., plot adjusted data if |
| 49 | +% available) |
| 50 | +% 'start',start_date : start date (in one of the following formats: |
| 51 | +% [YYYY MM DD HH MM SS] or [YYYY MM DD]) |
| 52 | +% 'time_label',label : use either years or months ('m'); default depends |
| 53 | +% on length of time shown ('m' for up to 18 months, |
| 54 | +% 'title',title : title for the plot (default: "Depth: .. db"); an |
| 55 | +% empty string ('') suppresses the title |
| 56 | +% 'var2',variable : if variable is not empty, profiles of this second |
| 57 | +% variable will be plotted; if it is the same type as the |
| 58 | +% first variable (e.g., DOXY2 compared to DOXY), it will |
| 59 | +% be plotted using the same axes; otherwise, the right |
| 60 | +% axis will be used for the second variable |
| 61 | +% |
| 62 | +% OUTPUT: None |
| 63 | +% |
| 64 | +% AUTHORS: |
| 65 | +% H. Frenzel, J. Sharp, A. Fassbender (NOAA-PMEL), N. Buzby (UW), |
| 66 | +% J. Plant, T. Maurer, Y. Takeshita (MBARI), D. Nicholson (WHOI), |
| 67 | +% and A. Gray (UW) |
| 68 | +% |
| 69 | +% CITATION: |
| 70 | +% H. Frenzel*, J. Sharp*, A. Fassbender, N. Buzby, J. Plant, T. Maurer, |
| 71 | +% Y. Takeshita, D. Nicholson, A. Gray, 2021. BGC-Argo-Mat: A MATLAB |
| 72 | +% toolbox for accessing and visualizing Biogeochemical Argo data. |
| 73 | +% Zenodo. https://doi.org/10.5281/zenodo.4971318. |
| 74 | +% (*These authors contributed equally to the code.) |
| 75 | +% |
| 76 | +% LICENSE: bgc_argo_mat_license.m |
| 77 | +% |
| 78 | +% DATE: DECEMBER 1, 2021 (Version 1.1) |
| 79 | + |
| 80 | +global Settings; |
| 81 | + |
| 82 | +if nargin < 5 |
| 83 | + warning(['Usage: plot_timeseries(Data, Mdata, variables, depth, ', ... |
| 84 | + 'basename [, varargin])']) |
| 85 | + return; |
| 86 | +end |
| 87 | + |
| 88 | +% set defaults |
| 89 | +per_float = 1; % show time series for each float in a separate plot |
| 90 | +raw = 'no'; % plot adjusted data by default |
| 91 | +title1 = []; % generate default titles |
| 92 | +qc_flags = 0:9; % use all data |
| 93 | +var2_orig = []; |
| 94 | +lgnd = 'yes'; |
| 95 | +start_date = []; |
| 96 | +end_date = []; |
| 97 | +time_label = []; % used as flag |
| 98 | + |
| 99 | +% parse optional arguments |
| 100 | +for i = 1:2:length(varargin)-1 |
| 101 | + if strcmpi(varargin{i}, 'per_float') |
| 102 | + per_float = varargin{i+1}; |
| 103 | + elseif strcmpi(varargin{i}, 'raw') |
| 104 | + raw = varargin{i+1}; |
| 105 | + elseif strcmpi(varargin{i}, 'title') |
| 106 | + title1 = varargin{i+1}; |
| 107 | + elseif strcmpi(varargin{i}, 'qc') |
| 108 | + qc_flags = varargin{i+1}; |
| 109 | + elseif strcmpi(varargin{i}, 'var2') |
| 110 | + var2_orig = varargin{i+1}; |
| 111 | + elseif strcmpi(varargin{i}, 'legend') |
| 112 | + lgnd = varargin{i+1}; |
| 113 | + elseif strcmpi(varargin{i}, 'time_label') |
| 114 | + time_label = varargin{i+1}; |
| 115 | + elseif strcmp(varargin{i}, 'start') |
| 116 | + if length(varargin{i+1}) == 3 || length(varargin{i+1}) == 6 |
| 117 | + start_date = datenum(varargin{i+1}); |
| 118 | + else |
| 119 | + warning(['dates should be in [YYYY MM DD HH MM SS] or ', ... |
| 120 | + '[YYYY MM DD] format']); |
| 121 | + end |
| 122 | + elseif strcmp(varargin{i}, 'end') |
| 123 | + if length(varargin{i+1}) == 3 || length(varargin{i+1}) == 6 |
| 124 | + end_date = datenum(varargin{i+1}); |
| 125 | + else |
| 126 | + warning(['dates should be in [YYYY MM DD HH MM SS] or ', ... |
| 127 | + '[YYYY MM DD] format']); |
| 128 | + end |
| 129 | + else |
| 130 | + warning('unknown option: %s', varargin{i}); |
| 131 | + end |
| 132 | +end |
| 133 | + |
| 134 | +if ~isempty(var2_orig) && ~per_float |
| 135 | + warning('var2 can only be used with per float time series plots') |
| 136 | + return |
| 137 | +end |
| 138 | + |
| 139 | +floats = fieldnames(Data); |
| 140 | +nfloats = length(floats); |
| 141 | +if ~nfloats |
| 142 | + warning('no floats found in Data structure') |
| 143 | + return |
| 144 | +end |
| 145 | + |
| 146 | +float_ids = fieldnames(Mdata); |
| 147 | +nvars = length(variables); |
| 148 | +ndepths = length(depth); |
| 149 | +if per_float |
| 150 | + nplots = nfloats * nvars * ndepths; |
| 151 | + warn_insert = 'floats, '; |
| 152 | +else |
| 153 | + nplots = nvars * ndepths; |
| 154 | + warn_insert = ''; |
| 155 | +end |
| 156 | +if nplots > Settings.max_plots |
| 157 | + warning(['too many plots requested (%d) - use fewer %sdepths and/or variables', ... |
| 158 | + newline, 'or increase Settings.max_plots (%d) if possible'], ... |
| 159 | + nplots, warn_insert, Settings.max_plots) |
| 160 | + return |
| 161 | +end |
| 162 | + |
| 163 | +if ~isempty(var2_orig) |
| 164 | + [var2{1:nvars}] = deal(var2_orig); % default; may change later |
| 165 | +end |
| 166 | +[title_added{1:nvars}] = deal(''); % default; may change later |
| 167 | +% unless 'raw' is specified, plot adjusted data |
| 168 | +if strncmpi(raw,'y',1) |
| 169 | + if ~ischar(title1) || ~isempty(title1) % not added if title1 == '' |
| 170 | + [title_added{1:nvars}] = deal(' [raw values]'); |
| 171 | + end |
| 172 | +else |
| 173 | + for v = 1:nvars |
| 174 | + % if all floats have adjusted values available for a variable, |
| 175 | + % they will be used instead of raw values |
| 176 | + has_adj = 0; |
| 177 | + for f = 1:nfloats |
| 178 | + % the "_ADJUSTED" variable usually exists, but it may not |
| 179 | + % be filled with actual values |
| 180 | + if isfield(Data.(floats{f}),[variables{v}, '_ADJUSTED']) && ... |
| 181 | + sum(isfinite(Data.(floats{f}).([variables{v}, '_ADJUSTED'])(:))) |
| 182 | + has_adj = has_adj + 1; |
| 183 | + else |
| 184 | + warning(['adjusted values for %s for float %s are not available,',... |
| 185 | + ' showing raw values for all floats instead'], ... |
| 186 | + variables{v}, floats{f}); |
| 187 | + if ~ischar(title1) || ~isempty(title1) |
| 188 | + title_added{v} = [title_added{v}, ' [raw values]']; |
| 189 | + end |
| 190 | + break |
| 191 | + end |
| 192 | + if ~isempty(var2_orig) |
| 193 | + if isfield(Data.(floats{f}),[var2_orig, '_ADJUSTED']) && ... |
| 194 | + sum(isfinite(Data.(floats{f}).([var2_orig, '_ADJUSTED'])(:))) |
| 195 | + has_adj = has_adj + 1; |
| 196 | + else |
| 197 | + warning(['adjusted values for %s for float %s ', ... |
| 198 | + 'are not available, showing raw values for ', ... |
| 199 | + 'all floats instead'], var2_orig, floats{f}); |
| 200 | + if ~ischar(title1) || ~isempty(title1) |
| 201 | + title_added{v} = ' [raw values]'; |
| 202 | + end |
| 203 | + break |
| 204 | + end |
| 205 | + end |
| 206 | + end |
| 207 | + if has_adj == nfloats * (1 + ~isempty(var2_orig)) |
| 208 | + variables{v} = [variables{v}, '_ADJUSTED']; |
| 209 | + if ~isempty(var2_orig) |
| 210 | + var2{v} = [var2_orig, '_ADJUSTED']; |
| 211 | + end |
| 212 | + end |
| 213 | + end |
| 214 | +end |
| 215 | + |
| 216 | +if ~isempty(basename) |
| 217 | + [var2_insert{1:nvars}] = deal(''); |
| 218 | + if ~isempty(var2_orig) |
| 219 | + for v = 1:nvars |
| 220 | + var2_insert{v} = sprintf('_%s', var2{v}); |
| 221 | + end |
| 222 | + end |
| 223 | +end |
| 224 | + |
| 225 | +% vertical interpolation to depths with regular intervals |
| 226 | +for f = 1:nfloats |
| 227 | + Datai.(floats{f}) = depth_interp(Data.(floats{f}), qc_flags, 'raw', raw); |
| 228 | +end |
| 229 | + |
| 230 | +% determine indices matching requested depths |
| 231 | +ndepths = length(depth); |
| 232 | +idx = cell(nfloats, ndepths); |
| 233 | +press = cell(nfloats, ndepths); % actual pressure levels used |
| 234 | +for d = 1:ndepths |
| 235 | + for f = 1:nfloats |
| 236 | + [mini, idx{f,d}] = min(abs(Datai.(floats{f}).PRES(:,1) - depth(d))); |
| 237 | + press{f,d} = Datai.(floats{f}).PRES(idx{f,d},1); |
| 238 | + if mini > Settings.depth_tol |
| 239 | + warning(['Closest depth level to requested %.1f db for ', ... |
| 240 | + 'float %s is %.1f db'], depth(d), floats{f}, press{f,d}); |
| 241 | + end |
| 242 | + end |
| 243 | +end |
| 244 | + |
| 245 | +for v = 1:nvars |
| 246 | + if ~isempty(var2_orig) |
| 247 | + same_var_type = strncmp(variables{v}, var2{v}, ... |
| 248 | + length(variables{v}) - ... |
| 249 | + 9 * endsWith(variables{v}, '_ADJUSTED')); |
| 250 | + end |
| 251 | + for d = 1:ndepths |
| 252 | + if ~per_float |
| 253 | + % one figure per variable for all floats; make wide plot |
| 254 | + f1 = figure('Position',[20 20 800 400]); |
| 255 | + % reset color order |
| 256 | + set(gca,'ColorOrderIndex',1); |
| 257 | + hold(gca, 'on') |
| 258 | + end |
| 259 | + for f = 1:nfloats |
| 260 | + if per_float |
| 261 | + % one figure per variable for each float; make wide plot |
| 262 | + f1 = figure('Position',[20 20 800 400]); |
| 263 | + set(gca,'ColorOrderIndex',1); |
| 264 | + hold(gca, 'on') |
| 265 | + end |
| 266 | + plot(Datai.(floats{f}).TIME(1,:), ... |
| 267 | + Datai.(floats{f}).(variables{v})(idx{f,d},:), 'LineWidth', 2); |
| 268 | + xl = xlim; |
| 269 | + if ~isempty(start_date) || ~isempty(end_date) |
| 270 | + if ~isempty(start_date) |
| 271 | + xl(1) = start_date; |
| 272 | + end |
| 273 | + if ~isempty(end_date) |
| 274 | + xl(2) = end_date; |
| 275 | + end |
| 276 | + xlim(xl); |
| 277 | + end |
| 278 | + % determine type of time label based on length of time series |
| 279 | + if isempty(time_label) |
| 280 | + if xl(2) - xl(1) > 548 % in days; ~1.5 years |
| 281 | + time_label = 'y'; |
| 282 | + else |
| 283 | + time_label = 'm'; |
| 284 | + end |
| 285 | + end |
| 286 | + if strncmpi(time_label, 'y', 1) |
| 287 | + set(gca,'XTick',datenum([(2000:2030)' ones(31,1) ones(31,1)])); |
| 288 | + datetick('x','yyyy','keeplimits','keepticks'); |
| 289 | + xlabel('Year','FontSize',14); |
| 290 | + else |
| 291 | + set(gca,'XTick',datenum([repelem((2000:2030)',12,1) ... |
| 292 | + repmat((1:12)',31, 1) ones(31*12,1)])); |
| 293 | + datetick('x','mm-yyyy','keeplimits','keepticks'); |
| 294 | + xlabel('Month','FontSize',14); |
| 295 | + end |
| 296 | + |
| 297 | + [long_name, units] = get_var_name_units(variables{v}); |
| 298 | + ylabel([long_name, ' ', units]) |
| 299 | + if ~isempty(var2_orig) |
| 300 | + if ~same_var_type |
| 301 | + yyaxis right; |
| 302 | + end |
| 303 | + plot(Datai.(floats{f}).TIME(1,:), ... |
| 304 | + Datai.(floats{f}).(var2{v})(idx{f,d},:), 'LineWidth', 2); |
| 305 | + if ~same_var_type |
| 306 | + [long_name, units] = get_var_name_units(var2_orig); |
| 307 | + ylabel([long_name, ' ', units]) |
| 308 | + end |
| 309 | + end |
| 310 | + if per_float |
| 311 | + hold off |
| 312 | + box on; |
| 313 | + if isempty(title1) && ~ischar(title1) % i.e., [] |
| 314 | + if strcmp(lgnd, 'no') || ~isempty(var2_orig) |
| 315 | + % add float number to title instead |
| 316 | + title(sprintf('Depth: %d db (%s)%s', press{f,d}, ... |
| 317 | + floats{f}, title_added{v})); |
| 318 | + else % legend shows float number |
| 319 | + title(sprintf('Depth: %d db%s', press{f,d}, ... |
| 320 | + title_added{v})); |
| 321 | + end |
| 322 | + else |
| 323 | + title([title1, title_added{v}]); |
| 324 | + end |
| 325 | + if strcmp(lgnd,'yes') |
| 326 | + if isempty(var2_orig) |
| 327 | + legend(float_ids{f},'location','eastoutside',... |
| 328 | + 'AutoUpdate','off'); |
| 329 | + else |
| 330 | + legend({strrep(variables{v},'_','\_'); ... |
| 331 | + strrep(var2{v},'_','\_')}, ... |
| 332 | + 'location', 'eastoutside', 'AutoUpdate', 'off'); |
| 333 | + end |
| 334 | + end |
| 335 | + if ~isempty(basename) |
| 336 | + fn_png = sprintf('%s_%d_%s%s_%ddb.png', basename, ... |
| 337 | + Mdata.(float_ids{f}).WMO_NUMBER, variables{v}, ... |
| 338 | + var2_insert{v}, press{f,d}); |
| 339 | + print(f1, '-dpng', fn_png); |
| 340 | + end |
| 341 | + end |
| 342 | + end |
| 343 | + if ~per_float |
| 344 | + hold off |
| 345 | + box on; |
| 346 | + if isempty(title1) && ~ischar(title1) % i.e., [] |
| 347 | + if strcmp(lgnd, 'yes') |
| 348 | + title(sprintf('Depth: %d db%s', press{f,d}, ... |
| 349 | + title_added{v})); |
| 350 | + else |
| 351 | + title(sprintf('Depth: %d db (%s)%s', press{f,d}, ... |
| 352 | + floats{f}, title_added{v})); |
| 353 | + end |
| 354 | + else |
| 355 | + title([title1, title_added{v}]); |
| 356 | + end |
| 357 | + if strcmp(lgnd,'yes') |
| 358 | + legend(float_ids,'location','eastoutside',... |
| 359 | + 'AutoUpdate','off'); |
| 360 | + end |
| 361 | + if ~isempty(basename) |
| 362 | + fn_png = sprintf('%s_%s%s_%ddb.png', basename, variables{v}, ... |
| 363 | + var2_insert{v}, press{f,d}); |
| 364 | + print(f1, '-dpng', fn_png); |
| 365 | + end |
| 366 | + end |
| 367 | + end |
| 368 | +end |
0 commit comments