This repository has been archived by the owner on Apr 5, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathlegtools.m
375 lines (329 loc) · 16.7 KB
/
legtools.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
classdef (Abstract) legtools
% LEGTOOLS is a MATLAB class definition providing the user with a set of
% methods to modify existing Legend objects.
%
% This is an HG2 specific implementation and requires MATLAB R2014b or
% newer.
%
% legtools methods:
% append - Add one or more entries to the end of the legend
% permute - Rearrange the legend entries
% remove - Remove one or more legend entries
% adddummy - Add one or more entries to the legend for unsupported graphics objects
%
% NOTE:
% For MATLAB versions >= R2017a, the legend object's 'AutoUpdate'
% property must be set to 'off' before using this utility
%
% See also legend
methods (Static)
function append(lh, newStrings)
% APPEND appends strings, newStrings, to the specified Legend
% object, lh. newStrings can be a 1D character array or a 1D
% cell array of strings. Character arrays are treated as a
% single string. If multiple Legend objects are specified, only
% the first will be modified.
%
% The legend will only be updated with the new strings if the
% number of strings in the existing legend plus the number of
% strings in newStrings is the same as the number of plots on
% the associated axes object (e.g. if you have 2 lineseries and
% 2 legend entries already no changes will be made).
legtools.verchk()
lh = legtools.handlecheck('append', lh);
% Make sure newString exists & isn't empty
if ~exist('newStrings', 'var') || isempty(newStrings)
error('legtools:append:EmptyStringInput', ...
'No strings provided' ...
);
end
newStrings = legtools.strcheck('append', newStrings);
legtools.autoupdatecheck(lh)
% To make sure we target the right axes, pull the legend's
% PlotChildren and get their parent axes object
parentaxes = lh.PlotChildren(1).Parent;
% Get line object handles
plothandles = flipud(parentaxes.Children); % Flip so order matches
% Update legend with line object handles & new string array
newlegendstr = [lh.String newStrings]; % Need to generate this before adding new plot objects
% Use the union of the parent axes' Children and the legend
% handle's PlotChildren to properly order the legend strings.
% Union(A, B, 'Sorted') will return A concatenated with the
% values of B not in A, so we have the handles associated with
% the existing entries and then the remaining handles in the
% order they are plotted.
lh.PlotChildren = union(lh.PlotChildren, plothandles, 'stable');
if numel(newlegendstr) > numel(lh.PlotChildren)
% MATLAB automatically throws out the extra legend entries
% if the number of strings to be added is larger than the
% number of supported graphics objects that are children of
% the parent axes. legend throws a warning in this case and
% we should too
warning('legtools:append:IgnoringExtraEntries', ...
'Ignoring extra legend entries');
end
lh.String = newlegendstr;
if ~verLessThan('matlab', '9.2')
% The addition of 'AutoUpdate' to legend in R2017a breaks
% the functionality of append. With 'AutoUpdate' turned off
% we can restore the functionality of legtools, but turning
% it back on causes our appended legend entries to be
% deleted. Clearing out the undocumented
% 'PlotChildrenExcluded' legend property seems to prevent
% this from occuring
%
% NOTE: This is untested in versions < R2017a
lh.PlotChildrenExcluded = [];
end
end
function permute(lh, order)
% PERMUTE rearranges the entries of the specified Legend
% object, lh, so they are then the order specified by the
% vector order. order must be the same length as the number of
% legend entries in lh. All elements of order must be unique,
% real, positive, integer values.
legtools.verchk()
% Temporarily throw an error for MATLAB >= R2017a
if ~verLessThan('matlab', '9.2')
error('legtools:permute:NotImplementedError', ...
'legtools.permute is currently not functional in MATLAB >= R2017a', ...
)
end
if ~exist('order', 'var') || isempty(order)
error('legtools:permute:EmptyOrderInput', ...
'No permute order provided' ...
);
end
lh = legtools.handlecheck('permute', lh);
% Catch length & uniqueness issues with order, let MATLAB deal
% with the rest.
if numel(order) ~= numel(lh.String)
error('legtools:permute:TooManyIndices', ...
'Number of values in order must match the number of legend strings' ...
);
end
if numel(unique(order)) < numel(lh.String)
error('legtools:permute:NotEnoughUniqueIndices', ...
'order must contain enough unique indices to index all legend strings' ...
);
end
% Permute the legend data source(s) and string(s)
% MATLAB has a listener on the PlotChildren so when their order
% is modified the string order is changed with it
lh.PlotChildren = lh.PlotChildren(order);
end
function remove(lh, remidx)
% REMOVE removes the legend entries of the legend object, lh,
% at the locations specified by remidx. All elements of remidx
% must be real, positive, integer values.
%
% If remidx specifies all the legend entries, the legend
% object is deleted
legtools.verchk()
lh = legtools.handlecheck('remove', lh);
% Temporarily throw an error for MATLAB >= R2017a
if ~verLessThan('matlab', '9.2')
error('legtools:remove:NotImplementedError', ...
'legtools.remove is currently not functional in MATLAB >= R2017a', ...
)
end
% Catch length issues, let MATLAB deal with the rest
if numel(unique(remidx)) > numel(lh.String)
error('legtools:remove:TooManyIndices', ...
'Number of unique values in remidx exceeds number of legend entries' ...
);
end
% Check remidx for indices greater than the number of legend
% entries and throw them out.
nlegendentries = numel(lh.PlotChildren);
invalididxmask = remidx > nlegendentries; % Logical test
if any(invalididxmask)
% If we have any invalid entries, remove them and throw a
% warning
remidx(invalididxmask) = [];
warning('legtools:remove:InvalidIndex', ...
'Removal indices > %u have been ignored', nlegendentries ...
);
end
if numel(unique(remidx)) == numel(lh.String)
delete(lh);
warning('legtools:remove:LegendDeleted', ...
'All legend entries specified for removal, deleting Legend Object' ...
);
else
% Check legend entries to be removed for dummy lineseries
% objects and delete them
objtodelete = [];
count = 1;
for ii = remidx
% Our dummy lineseries contain a single NaN YData entry
if length(lh.PlotChildren(ii).YData) == 1 && isnan(lh.PlotChildren(ii).YData)
% Deleting the graphics object here also deletes it
% from the legend, which screws up the one-liner
% plot children removal. Instead store the objects
% to be deleted and delete them after the legend is
% properly modified
objtodelete(count) = lh.PlotChildren(ii);
count = count + 1;
end
end
lh.PlotChildren(remidx) = [];
delete(objtodelete);
end
end
function adddummy(lh, newStrings, plotParams)
% ADDDUMMY appends strings, newStrings, to the Legend Object,
% lh, for graphics objects that are not supported by legend.
%
% For a single dummy legend entry, plotParams is defined as a
% cell array of strings that follow MATLAB's PLOT syntax.
% Entries can be either a LineSpec or a series of Name/Value
% pairs. For multiple dummy legend entries, plotParams is
% defined as a cell array of cells where each top-level cell
% corresponds to a string in newStrings.
%
% ADDDUMMY adds a Chart Line Object to the parent axes of lh
% consisting of a single NaN value so nothing is rendered in
% the axes but it provides a valid object for legend to include
%
% LEGTOOLS.REMOVE will remove this Chart Line Object if its
% legend entry is removed.
legtools.verchk()
lh = legtools.handlecheck('addummy', lh);
% Make sure newStrings exists & isn't empty
if ~exist('newStrings', 'var') || isempty(newStrings)
error('legtools:adddummy:EmptyStringInput', ...
'No string provided' ...
);
end
newStrings = legtools.strcheck('adddummy', newStrings);
legtools.autoupdatecheck(lh)
% See if we have a character input for the single addition case
% and put it into a cell. Double nest the cells so behavior is
% consistent with a cell array of cells for multiple new dummy
% entries
if ischar(plotParams)
plotParams = {cellstr(plotParams)};
end
% For the single dummy entry case, make sure each cell of
% plotParams is a cell so behavior is sonsistent with a cell
% array of cells for multiple new dummy entries
if length(newStrings) == 1
if ~iscell([plotParams{:}])
plotParams = {plotParams};
end
end
parentaxes = lh.PlotChildren(1).Parent;
washeld = ishold(parentaxes); % Set a flag for previous hold state ofthe parent axes
hold(parentaxes, 'on');
for ii = 1:length(newStrings)
plot(parentaxes, NaN, plotParams{ii}{:}); % Leave input validation up to plot
end
if ~washeld
% If parentaxes wasn't previously held, turn hold back off
hold(parentaxes, 'off');
end
legtools.append(lh, newStrings); % Add legend entries
if ~verLessThan('matlab', '9.2')
% The addition of 'AutoUpdate' to legend in R2017a breaks
% the functionality of append. With 'AutoUpdate' turned off
% we can restore the functionality of legtools, but turning
% it back on causes our appended legend entries to be
% deleted. Clearing out the undocumented
% 'PlotChildrenExcluded' legend property seems to prevent
% this from occuring
%
% NOTE: This is untested in versions < R2017a
lh.PlotChildrenExcluded = [];
end
end
end
methods (Static, Access = private)
function verchk()
% Throw error if we're not using R2014b or newer
if verLessThan('matlab', '8.4')
error('legtools:UnsupportedMATLABver', ...
'MATLAB releases prior to R2014b are not supported' ...
);
end
end
function [lh] = handlecheck(src, lh)
% Make sure lh exists and is a legend object
if ~isa(lh, 'matlab.graphics.illustration.Legend')
msgID = sprintf('legtools:%s:InvalidLegendHandle', src);
error(msgID, 'Invalid legend handle provided');
end
% Pick first legend handle if more than one is passed
if numel(lh) > 1
msgID = sprintf('legtools:%s:TooManyLegends', src);
warning(msgID, ...
'%u Legend objects specified, modifying the first one only', ...
numel(lh) ...
);
lh = lh(1);
end
end
function [newString] = strcheck(src, newString)
% Validate the input strings
if ischar(newString)
% Input string is a character array, use cellstr to convert
% to a cell array. See the documentation for cellstr for
% its handling of 2D char arrays.
newString = cellstr(newString);
end
if isa(newString, 'string')
% MATLAB introduced the String data type in R2016b. To
% avoid having to write separate behavior everywhere to
% handle this, convert the String array to a Cell array
newString = cellstr(newString);
end
% Check to see if we now have a cell array
if ~iscell(newString)
msgID = sprintf('legtools:%s:InvalidLegendString', src);
if ~verLessThan('matlab', '9.1')
% String data type introduced in MATLAB R2016b so this
% trying to get its class in older versions will error
% out our error
error(msgID, ...
'Invalid Data Type Passed: %s\n\nData must be of type: ''%s'', ''%s'', or ''%s''', ...
class(newString), class(cell(1)), class(''), class("") ...
);
else
% Error message for MATLAB versions older than R2016b
error(msgID, ...
'Invalid Data Type Passed: %s\n\nData must be of type: ''%s'' or ''%s''', ...
class(newString), class(cell(1)), class('') ...
);
end
end
% Check shape of newStrings and make sure it's 1D
if size(newString, 1) > 1
newString = reshape(newString', 1, []);
end
% Check to make sure we're only passing strings
for ii = 1:length(newString)
% Check for characters, let MATLAB handle errors for data
% types not compatible with num2str
if ~ischar(newString{ii})
msgID = sprintf('legtools:%s:ConvertingInvalidLegendString', src);
warning(msgID, ...
'Input legend ''string'' is of type %s, converting to %s', ...
class(newString{ii}), class('') ...
);
newString{ii} = num2str(newString{ii});
end
end
end
function autoupdatecheck(lh)
% If we're using R2017a or newer, we need to make sure that
% 'AutoUpdate' is off
if ~verLessThan('matlab', '9.2')
if ~strcmp(lh.AutoUpdate, 'off')
lh.AutoUpdate = 'off';
warning('legtools:autoupdatecheck:AutoUpdateNotOff', ...
'Input legend object''s ''AutoUpdate'' property has been set to ''off''')
end
end
end
end
end