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 xvalxlim(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