-
Notifications
You must be signed in to change notification settings - Fork 0
/
rotateXLabels.m
431 lines (387 loc) · 17.7 KB
/
rotateXLabels.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
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
function hh = rotateXLabels( ax, angle, varargin )
%rotateXLabels: rotate any xticklabels
%
% hh = rotateXLabels(ax,angle) rotates all XLabels on axes AX by an angle
% ANGLE (in degrees). Handles to the resulting text objects are returned
% in HH.
%
% hh = rotateXLabels(ax,angle,param,value,...) also allows one or more
% optional parameters to be specified. Possible parameters are:
% 'MaxStringLength' The maximum length of label to show (default inf)
%
% Examples:
% >> bar( hsv(5)+0.05 )
% >> days = {'Monday','Tuesday','Wednesday','Thursday','Friday'};
% >> set( gca(), 'XTickLabel', days )
% >> rotateXLabels( gca(), 45 )
%
% See also: GCA, BAR
% Copyright 2006-2013 The MathWorks Ltd.
error( nargchk( 2, inf, nargin ) );
if ~isnumeric( angle ) || ~isscalar( angle )
error( 'RotateXLabels:BadAngle', 'Parameter ANGLE must be a scalar angle in degrees' )
end
angle = mod( angle, 360 );
% From R2014b, rotating labels is built-in using 'XTickLabelRotation'
if ~verLessThan('matlab','8.4.0')
set(ax, 'XTickLabelRotation', angle)
if nargout > 1
hh = [];
end
return;
end
[maxStringLength] = parseInputs( varargin{:} );
% Get the existing label texts and clear them
[vals, labels] = findAndClearExistingLabels( ax, maxStringLength );
% Create the new label texts
h = createNewLabels( ax, vals, labels, angle );
% Reposition the axes itself to leave space for the new labels
repositionAxes( ax );
% If an X-label is present, move it too
repositionXLabel( ax );
% Store angle
setappdata( ax, 'RotateXLabelsAngle', angle );
% Only send outputs if requested
if nargout
hh = h;
end
%-------------------------------------------------------------------------%
function [maxStringLength] = parseInputs( varargin )
% Parse optional inputs
maxStringLength = inf;
if nargin > 0
params = varargin(1:2:end);
values = varargin(2:2:end);
if numel( params ) ~= numel( values )
error( 'RotateXLabels:BadSyntax', 'Optional arguments must be specified as parameter-value pairs.' );
end
if any( ~cellfun( 'isclass', params, 'char' ) )
error( 'RotateXLabels:BadSyntax', 'Optional argument names must be specified as strings.' );
end
for pp=1:numel( params )
switch upper( params{pp} )
case 'MAXSTRINGLENGTH'
maxStringLength = values{pp};
otherwise
error( 'RotateXLabels:BadParam', 'Optional parameter ''%s'' not recognised.', params{pp} );
end
end
end
end % parseInputs
%-------------------------------------------------------------------------%
function [vals,labels] = findAndClearExistingLabels( ax, maxStringLength )
% Get the current tick positions so that we can place our new labels
vals = get( ax, 'XTick' );
% Now determine the labels. We look first at for previously rotated labels
% since if there are some the actual labels will be empty.
ex = findall( ax, 'Tag', 'RotatedXTickLabel' );
if isempty( ex )
% Store the positions and labels
labels = get( ax, 'XTickLabel' );
if isempty( labels )
% No labels!
return
else
if ~iscell(labels)
labels = cellstr(labels);
end
end
% Clear existing labels so that xlabel is in the right position
set( ax, 'XTickLabel', {}, 'XTickMode', 'Manual' );
setappdata( ax, 'OriginalXTickLabels', labels );
else
% Labels have already been rotated, so capture them
labels = getappdata( ax, 'OriginalXTickLabels' );
set(ex, 'DeleteFcn', []);
delete(ex);
end
% Limit the length, if requested
if isfinite( maxStringLength )
for ll=1:numel( labels )
if length( labels{ll} ) > maxStringLength
labels{ll} = labels{ll}(1:maxStringLength);
end
end
end
end % findAndClearExistingLabels
%-------------------------------------------------------------------------%
function restoreDefaultLabels( ax )
% Restore the default axis behavior
removeListeners( ax );
% Try to restore the tick marks and labels
set( ax, 'XTickMode', 'auto', 'XTickLabelMode', 'auto' );
rmappdata( ax, 'OriginalXTickLabels' );
% Try to restore the axes position
if isappdata( ax, 'OriginalAxesPosition' )
set( ax, 'Position', getappdata( ax, 'OriginalAxesPosition' ) );
rmappdata( ax, 'OriginalAxesPosition' );
end
end
%-------------------------------------------------------------------------%
function textLabels = createNewLabels( ax, vals, labels, angle )
% Work out the ticklabel positions
zlim = get(ax,'ZLim');
z = zlim(1);
% We want to work in normalised coords, but this doesn't print
% correctly. Instead we have to work in data units even though it
% makes positioning hard.
y = getYPositionToUse( ax );
% Now create new text objects in similar positions.
textLabels = -1*ones( numel( vals ), 1 );
for ll=1:numel(vals)
textLabels(ll) = text( ...
'Units', 'Data', ...
'Position', [vals(ll), y, z], ...
'String', labels{ll}, ...
'Parent', ax, ...
'Clipping', 'off', ...
'Rotation', angle, ...
'Tag', 'RotatedXTickLabel', ...
'UserData', vals(ll));
end
% So that we can respond to CLA and CLOSE, attach a delete
% callback. We only attach it to one label to save massive numbers
% of callbacks during axes shut-down.
set(textLabels(end), 'DeleteFcn', @onTextLabelDeleted);
% Now copy font properties into the texts
updateFont();
% Update the alignment of the text
updateAlignment();
end % createNewLabels
%-------------------------------------------------------------------------%
function repositionAxes( ax )
% Reposition the axes so that there's room for the labels
% Note that we only do this if the OuterPosition is the thing being
% controlled
if ~strcmpi( get( ax, 'ActivePositionProperty' ), 'OuterPosition' )
return;
end
% Work out the maximum height required for the labels
labelHeight = getLabelHeight(ax);
% Remove listeners while we mess around with things, otherwise we'll
% trigger redraws recursively
removeListeners( ax );
% Change to normalized units for the position calculation
oldUnits = get( ax, 'Units' );
set( ax, 'Units', 'Normalized' );
% Not sure why, but the extent seems to be proportional to the height of the axes.
% Correct that now.
set( ax, 'ActivePositionProperty', 'Position' );
pos = get( ax, 'Position' );
axesHeight = pos(4);
% Make sure we don't adjust away the axes entirely!
heightAdjust = min( (axesHeight*0.9), labelHeight*axesHeight );
% Move the axes
if isappdata( ax, 'OriginalAxesPosition' )
pos = getappdata( ax, 'OriginalAxesPosition' );
else
pos = get(ax,'Position');
setappdata( ax, 'OriginalAxesPosition', pos );
end
if strcmpi( get( ax, 'XAxisLocation' ), 'Bottom' )
% Move it up and reduce the height
set( ax, 'Position', pos+[0 heightAdjust 0 -heightAdjust] )
else
% Just reduce the height
set( ax, 'Position', pos+[0 0 0 -heightAdjust] )
end
set( ax, 'Units', oldUnits );
set( ax, 'ActivePositionProperty', 'OuterPosition' );
% Make sure we find out if axes properties are changed
addListeners( ax );
end % repositionAxes
%-------------------------------------------------------------------------%
function repositionXLabel( ax )
% Try to work out where to put the xlabel
removeListeners( ax );
labelHeight = getLabelHeight(ax);
% Use the new max extent to move the xlabel. We may also need to
% move the title
xlab = get(ax,'XLabel');
titleh = get( ax, 'Title' );
set( [xlab,titleh], 'Units', 'Normalized' );
if strcmpi( get( ax, 'XAxisLocation' ), 'Top' )
titleExtent = get( xlab, 'Extent' );
set( xlab, 'Position', [0.5 1+labelHeight-titleExtent(4) 0] );
set( titleh, 'Position', [0.5 1+labelHeight 0] );
else
set( xlab, 'Position', [0.5 -labelHeight 0] );
set( titleh, 'Position', [0.5 1 0] );
end
addListeners( ax );
end % repositionXLabel
%-------------------------------------------------------------------------%
function height = getLabelHeight(ax)
height = 0;
textLabels = findall( ax, 'Tag', 'RotatedXTickLabel' );
if isempty(textLabels)
return;
end
oldUnits = get( textLabels(1), 'Units' );
set( textLabels, 'Units', 'Normalized' );
for ll=1:numel(vals)
ext = get( textLabels(ll), 'Extent' );
if ext(4) > height
height = ext(4);
end
end
set( textLabels, 'Units', oldUnits );
end % getLabelExtent
%-------------------------------------------------------------------------%
function updateFont()
% Update the rotated text fonts when the axes font changes
properties = {
'FontName'
'FontSize'
'FontAngle'
'FontWeight'
'FontUnits'
};
propertyValues = get( ax, properties );
textLabels = findall( ax, 'Tag', 'RotatedXTickLabel' );
set( textLabels, properties, propertyValues );
end % updateFont
function updateAlignment()
textLabels = findall( ax, 'Tag', 'RotatedXTickLabel' );
angle = get( textLabels(1), 'Rotation' );
% Depending on the angle, we may need to change the alignment. We change
% alignments within 5 degrees of each 90 degree orientation.
if strcmpi( get( ax, 'XAxisLocation' ), 'Top' )
if 0 <= angle && angle < 5
set( textLabels, 'HorizontalAlignment', 'Center', 'VerticalAlignment', 'Bottom' );
elseif 5 <= angle && angle < 85
set( textLabels, 'HorizontalAlignment', 'Left', 'VerticalAlignment', 'Bottom' );
elseif 85 <= angle && angle < 95
set( textLabels, 'HorizontalAlignment', 'Left', 'VerticalAlignment', 'Middle' );
elseif 95 <= angle && angle < 175
set( textLabels, 'HorizontalAlignment', 'Left', 'VerticalAlignment', 'Top' );
elseif 175 <= angle && angle < 185
set( textLabels, 'HorizontalAlignment', 'Center', 'VerticalAlignment', 'Top' );
elseif 185 <= angle && angle < 265
set( textLabels, 'HorizontalAlignment', 'Right', 'VerticalAlignment', 'Top' );
elseif 265 <= angle && angle < 275
set( textLabels, 'HorizontalAlignment', 'Right', 'VerticalAlignment', 'Middle' );
elseif 275 <= angle && angle < 355
set( textLabels, 'HorizontalAlignment', 'Right', 'VerticalAlignment', 'Bottom' );
else % 355-360
set( textLabels, 'HorizontalAlignment', 'Center', 'VerticalAlignment', 'Bottom' );
end
else
if 0 <= angle && angle < 5
set( textLabels, 'HorizontalAlignment', 'Center', 'VerticalAlignment', 'Top' );
elseif 5 <= angle && angle < 85
set( textLabels, 'HorizontalAlignment', 'Right', 'VerticalAlignment', 'Top' );
elseif 85 <= angle && angle < 95
set( textLabels, 'HorizontalAlignment', 'Right', 'VerticalAlignment', 'Middle' );
elseif 95 <= angle && angle < 175
set( textLabels, 'HorizontalAlignment', 'Right', 'VerticalAlignment', 'Bottom' );
elseif 175 <= angle && angle < 185
set( textLabels, 'HorizontalAlignment', 'Center', 'VerticalAlignment', 'Bottom' );
elseif 185 <= angle && angle < 265
set( textLabels, 'HorizontalAlignment', 'Left', 'VerticalAlignment', 'Bottom' );
elseif 265 <= angle && angle < 275
set( textLabels, 'HorizontalAlignment', 'Left', 'VerticalAlignment', 'Middle' );
elseif 275 <= angle && angle < 355
set( textLabels, 'HorizontalAlignment', 'Left', 'VerticalAlignment', 'Top' );
else % 355-360
set( textLabels, 'HorizontalAlignment', 'Center', 'VerticalAlignment', 'Top' );
end
end
end
%-------------------------------------------------------------------------%
function onAxesFontChanged( ~, ~ )
updateFont();
repositionAxes( ax );
repositionXLabel( ax );
end % onAxesFontChanged
%-------------------------------------------------------------------------%
function onAxesPositionChanged( ~, ~ )
% We need to accept the new position, so remove the appdata before
% redrawing
if isappdata( ax, 'OriginalAxesPosition' )
rmappdata( ax, 'OriginalAxesPosition' );
end
if isappdata( ax, 'OriginalXLabelPosition' )
rmappdata( ax, 'OriginalXLabelPosition' );
end
repositionAxes( ax );
repositionXLabel( ax );
end % onAxesPositionChanged
%-------------------------------------------------------------------------%
function onXAxisLocationChanged( ~, ~ )
updateAlignment();
repositionAxes( ax );
repositionXLabel( ax );
end % onXAxisLocationChanged
%-------------------------------------------------------------------------%
function onAxesLimitsChanged( ~, ~ )
% The limits have moved, so make sure the labels are still ok
textLabels = findall( ax, 'Tag', 'RotatedXTickLabel' );
xlim = get( ax, 'XLim' );
pos = [0 getYPositionToUse( ax )];
for tt=1:numel( textLabels )
xval = get( textLabels(tt), 'UserData' );
pos(1) = xval;
% If the tick is off the edge, make it invisible
if xval<xlim(1) || xval>xlim(2)
set( textLabels(tt), 'Visible', 'off', 'Position', pos )
elseif ~strcmpi( get( textLabels(tt), 'Visible' ), 'on' )
set( textLabels(tt), 'Visible', 'on', 'Position', pos )
else
% Just set the position
set( textLabels(tt), 'Position', pos );
end
end
repositionXLabel( ax );
end % onAxesPositionChanged
%-------------------------------------------------------------------------%
function onTextLabelDeleted( ~, ~ )
% The final text label has been deleted. This is likely from a
% "cla" or "close" call, so we should remove all of our dirty
% hacks.
restoreDefaultLabels(ax);
end
%-------------------------------------------------------------------------%
function addListeners( ax )
% Create listeners. We store the array of listeners in the axes to make
% sure that they have the same life-span as the axes they are listening to.
axh = handle( ax );
listeners = [
handle.listener( axh, findprop( axh, 'FontName' ), 'PropertyPostSet', @onAxesFontChanged )
handle.listener( axh, findprop( axh, 'FontSize' ), 'PropertyPostSet', @onAxesFontChanged )
handle.listener( axh, findprop( axh, 'FontWeight' ), 'PropertyPostSet', @onAxesFontChanged )
handle.listener( axh, findprop( axh, 'FontAngle' ), 'PropertyPostSet', @onAxesFontChanged )
handle.listener( axh, findprop( axh, 'FontUnits' ), 'PropertyPostSet', @onAxesFontChanged )
handle.listener( axh, findprop( axh, 'OuterPosition' ), 'PropertyPostSet', @onAxesPositionChanged )
handle.listener( axh, findprop( axh, 'XLim' ), 'PropertyPostSet', @onAxesLimitsChanged )
handle.listener( axh, findprop( axh, 'YLim' ), 'PropertyPostSet', @onAxesLimitsChanged )
handle.listener( axh, findprop( axh, 'XAxisLocation' ), 'PropertyPostSet', @onXAxisLocationChanged )
];
setappdata( ax, 'RotateXLabelsListeners', listeners );
end % addListeners
%-------------------------------------------------------------------------%
function removeListeners( ax )
% Rempove any property listeners whilst we are fiddling with the axes
if isappdata( ax, 'RotateXLabelsListeners' )
delete( getappdata( ax, 'RotateXLabelsListeners' ) );
rmappdata( ax, 'RotateXLabelsListeners' );
end
end % removeListeners
%-------------------------------------------------------------------------%
function y = getYPositionToUse( ax )
% Use the direction and XAxisLocation properties to work out which
% Y-value to draw the labels at.
whichYLim = 1;
% If YDir is reversed, switch where we position
if strcmpi( get( ax, 'YDir' ), 'Reverse' )
whichYLim = 3-whichYLim;
end
% If set to "top", then switch (again?)
if strcmpi( get( ax, 'XAxisLocation' ), 'Top' )
whichYLim = 3-whichYLim;
end
% Now get the value
ylim = get( ax, 'YLim' );
y = ylim(whichYLim);
end % getYPositionToUse
end % EOF