Contenuto principale

Results for


Four (of many) new features for Apps in MATLAB R2021a

These four new features are the solutions to many inquiries in the Answers forum that continue to receive hundreds of views per month long after they were asked.

Table of Contents

  • Keyboard Shortcuts for UI Components
  • Programmatically scroll UITables
  • Figure always on top
  • UI hyperlink component
  • Demo app: Keyboard shortcut challenge

Keyboard Shortcuts for UI Components

Keyboard shortcuts can now change the focus and make contiguous and non-contiguous selections in ListBox, Table, DatePicker, and ColorPicker menus created in uifigures. The table below summarizes the shortcuts made available in R2021a ( release notes ).

Reminder: in MATLAB R2020b and later you can change the tab order of objects in an app by changing their stack order in AppDesigner using the Reorder tool in the drop-down menu or by right-clicking the component and selecting Reorder from the menu (see R2020b release notes and a screenshot in the Answers forum ).

Programmatically scroll UITables

The scroll function was added in R2016a but as of R2021a it can be used with uitables to programmatically scroll to the top, bottom, left, or right of a table or to a specific row, column or cell ( release notes ). Combined with a uistyle (R2019b) you can scroll to and highlight any part of the uitable.

Syntax examples:

Figure always on top

Figures created with uifigure now have an additional WindowStyle property: 'alwaysontop' ( release notes ). Figures with this setting will stay on top of other windows but unlike the modal option, other windows are still accessible.

See the WindowStyle property description for tips on setting and changing this property.

To toggle the AlwaysOnTop state of your app using a checkbox, state button, or another UI component, follow this callback function template,

function alwaysOnTopCheckBoxValueChanged(app, event)
    value = app.alwaysOnTopCheckBox.Value;
    if value
        app.UIFigure.WindowStyle = 'alwaysontop';
    else
        app.UIFigure.WindowStyle = 'normal';
    end
end

UI hyperlink component

Use the uihyperlink function or the UI hyperlink component in App Designer or a uifigure to add and configure a clickable link ( release notes ). The hyperlink can be assigned to a figure, panel object, tab object, ButtonGroup, or GridLayout object when created in a uifigure. In addition to setting the text and URL, there are lots of properties to control the text format including the VisitedColor property that controls the color of the text after the link is clicked and an optional user-defined HyperlinkClickedFcn function that is evoked when the link is clicked.

Demo app: Keyboard shortcut challenge

The attached zip file contains an app, keyboardShortcutsDemo_R2021a.mlapp , that demonstrates these 4 features. The app displays the extent of arctic sea ice from 1979 to 2016 during the months when extent typically maximizes and minimizes.

Mouseless challenge: After opening the app, without using your mouse, try the following.

  • Select a month (September or March) and any number of years from the list boxes
  • Navigate through the Date Picker and select a date within the range or your selected years (disabled when only 1 year is selected).
  • Navigate to the always-on-top checkbox to pin the app to the top of other windows.
  • Navigate to the text box and enter a year that appears in the uitable to go to that row in the uitable (disabled when only 1 year is selected).
  • Navigate to the URL and press Enter to open the website containing the raw data.

Download the attached zip file, FourNewAppFeatures_R2021a.zip, for a Live script copy of this thread and an app that demonstrates each feature.

New in R2021a, LimitsChangedFcn

LimitsChangedFcn is a callback function that responds to changes to axis limits ( release notes ). The function responds to axis interaction such as panning and zooming, programmatically setting the axis limits, or when axis limits are automatically adjusted by other processes.

LimitsChangedFcn is a property of ruler objects which are properties of axes and can be independently set for each axis. For example,

ax = gca(); 
ax.XAxis.LimitsChangedFcn = ... % Responds to changes to XLim
ax.YAxis.LimitsChangedFcn = ... % Responds to changes to YLim
ax.ZAxis.LimitsChangedFcn = ... % Responds to changes to ZLim

Previously, a listener could be assigned to respond to changes to axis limits. Here are some examples.

However, LimitsChangedFcn responds more reliably than a listener that responds to setting/getting axis limits. For example, after zooming or panning the axes in the demo below, the listener does not respond to the Restore View button in the axis toolbar but LimitsChangedFcn does! After restoring the view, try zooming out which does not result in changes to axis limits yet the listener will respond but the LimitsChangedFcn will not. Adding objects to axes after an axis-limit listener is set will not trigger the listener even if the added object expands the axis limits ( why not? ) but LimitsChangedFcn will!

ax = gca(); 
ax.UserData.Listener = addlistener(ax,'XLim','PostSet',@(~,~)disp('Listener')); 
ax.XAxis.LimitsChangedFcn = @(~,~)disp('LimitsChangedFcn')

How to use LimitsChangedFcn

The LimitsChangedFcn works like any other callback. For review,

The first input to the LimitsChangedFcn callback function is the handle to the axis ruler object that was changed.

The second input is a structure that contains the old and new limits. For example,

    LimitsChanged with properties:
      OldLimits: [0 1]
      NewLimits: [0.25 0.75]
         Source: [1×1 NumericRuler]
      EventName: 'LimitsChanged'

Importantly, since LimitsChangedFcn is a property of the axis rulers rather than the axis object, changes to the axes may clear the LimitsChangedFcn property if the axes aren't held using hold on. For example,

% Axes not held
ax = gca(); 
ax.XAxis.LimitsChangedFcn = @(ruler,~)title(ancestor(ruler,'axes'),'LimitsChangedFcn fired!'); 
plot(ax, 1:5, rand(1,5), 'o')
ax.XAxis.LimitsChangedFcn
ans =
    0×0 empty char array
% Axes held
ax = gca(); 
hold(ax,'on')
ax.XAxis.LimitsChangedFcn = @(ruler,~)title(ancestor(ruler,'axes'),'LimitsChangedFcn fired!'); 
plot(ax, 1:5, rand(1,5), 'o')
ax.XAxis.LimitsChangedFcn
ans =
  function_handle with value:
    @(ruler,~)title(ancestor(ruler,'axes'),'LimitsChangedFcn fired!')

Demo

In this simple app a LimitsChangedFcn callback function is assigned to the x and y axes. The function does two things:

  1. Text boxes showing the current axis limits are updated
  2. The prying eyes that are centered on the axes will move to the new axis center

This demo also uses Name=Value syntax and emoji text objects !

Create app

h.fig = uifigure(Name="LimitsChangedFcn Demo", ...
    Resize="off");
h.fig.Position(3:4) = [500,260];
movegui(h.fig)
h.ax = uiaxes(h.fig,...
    Units="pixels", ...
    Position=[200 26 250 208], ...
    Box="on");
grid(h.ax,"on")
title(h.ax,"I'm following you!")
h.eyeballs = text(h.ax, .5, .5, ...
    char([55357 56385 55357 56385]), ...
    HorizontalAlignment="center", ...
    FontSize=40);
h.label = uilabel(h.fig, ...
    Text="Axis limits", ...
    Position=[25 212 160 15], ...
    FontWeight="bold",...
    HorizontalAlignment="center");
h.xtxt = uitextarea(h.fig, ...
    position=[25 191 160 20], ...
    HorizontalAlignment="center", ...
    WordWrap="off", ...
    Editable="off",...
    FontName=get(groot, 'FixedWidthFontName'));
h.ytxt = uitextarea(h.fig, ...
    position=[25 165 160 20], ...
    HorizontalAlignment="center", ...
    WordWrap="off", ...
    Editable="off", ...
    FontName=get(groot, 'FixedWidthFontName'));
h.label = uilabel(h.fig, ...
    Text=['X',newline,newline,'Y'], ...
    Position=[10 170 15 38], ...
    FontWeight="bold");

Set LimitsChangedFcn of x and y axes

h.ax.XAxis.LimitsChangedFcn = @(hObj,data)limitsChangedCallbackFcn(hObj,data,h,'x');
h.ax.YAxis.LimitsChangedFcn = @(hObj,data)limitsChangedCallbackFcn(hObj,data,h,'y');

Update text fields

xlim(h.ax, [-100,100])
ylim(h.ax, [-100,100])

Define LimitsChangedFcn

function limitsChangedCallbackFcn(rulerHand, limChgData, handles, xy)
% limitsChangedCallbackFcn() responds to changes to x or y axis limits.
% - rulerHand: Ruler handle for x or y axis that was changed (not used in this demo)
% - limChgData: LimitsChanged data structure
% - handles: structure of App handles
% - xy: either 'x' or 'y' identifying rulerHand
switch lower(xy)
    case 'x'
        textHandle = handles.xtxt;
        positionIndex = 1; 
    case 'y'
        textHandle = handles.ytxt;
        positionIndex = 2; 
    otherwise
        error('xy is a character ''x'' or ''y''.')
end
% Update text boxes showing rounded axis limits
textHandle.Value = sprintf('[%.3f, %.3f]',limChgData.NewLimits);
% Move the eyes to the new center position
handles.eyeballs.Position(positionIndex) = limChgData.NewLimits(1)+range(limChgData.NewLimits)/2; % for linear scales only!
drawnow
end

See attached mlx file for a copy of this thread.

Highlight Icon image

Starting in MATLAB R2021a, name-value arguments have a new optional syntax!

A property name can be paired with its value by an equal sign and the property name is not enclosed in quotes.

Compare the comma-separated name,value syntax to the new equal-sign syntax, either of which can be used in >=r2021a:

  • plot(x, y, "b-", "LineWidth", 2)
  • plot(x, y, "b-", LineWidth=2)

It comes with some limitations:

  1. It's recommended to use only one syntax in a function call but if you're feeling rebellious and want to mix the syntaxes, all of the name=value arguments must appear after the comma-separated name,value arguments.
  2. Like the comma-separated name,value arguments, the name=value arguments must appear after positional arguments.
  3. Name=value pairs must be used directly in function calls and cannot be wrapped in cell arrays or additional parentheses.

Some other notes:

  1. The property names are not case-sensitive so color='r' and Color='r' are both supported.
  2. Partial name matches are also supported. plot(1:5, LineW=4)

The new syntax is helpful in distinguishing property names from property values in long lists of name-value arguments within the same line.

For example, compare the following 2 lines:

h = uicontrol(hfig, "Style", "checkbox", "String", "Long", "Units", "Normalize", "Tag", "chkBox1")
h = uicontrol(hfig,  Style="checkbox",    String="Long",    Units="Normalize",    Tag="chkBox1")

Here's another side-by-side comparison of the two syntaxes. See the attached mlx file for the full code and all content of this Community Highlight.

tiledlayout, introduced in MATLAB R2019b, offers a flexible way to add subplots, or tiles, to a figure.

Reviewing two changes to tiledlayout in MATLAB R2021a

  1. The new TileIndexing property
  2. Changes to TileSpacing and Padding properties

1) TileIndexing

By default, axes within a tiled layout are created from left to right, top to bottom, but sometimes it's better to organize plots column-wise from top to bottom and then left to right. Starting in r2021a, the TileIndexing property of tiledlayout specifies the direction of flow when adding new tiles.

tiledlayout(__,'TileIndexing','rowmajor') creates tiles by row (default).

tiledlayout(__,'TileIndexing','columnmajor') creates tiles by column.

.

2) TileSpacing & Padding changes

Some changes have been made to the spacing properties of tiles created by tiledlayout.

TileSpacing: sets the spacing between tiles.

  • "loose" is the new default and replaces "normal" which is no longer recommended but is still accepted.
  • "tight" replaces "none" and brings the tiles closer together still leaving space for axis ticks and labels.
  • "none" results in tile borders touching, following the true meaning of none.
  • "compact" is unchanged and has slightly more space between tiles than "tight".

Padding: sets the spacing of the figure margins.

  • "loose" is the new default and replaces "normal" which is no longer recommended but is still accepted.
  • "tight" replaces "none" and reduces the figure margins. "none" is no longer recommended but is still accepted.
  • "compact" is unchanged and adds slightly more marginal space than "tight".
  • Reducing the figure margins to a true none is still not an option.

The release notes show a comparison of these properties between r2020b and r2021a.

Here's what the new TileSpacing options (left column of figures below) and Padding options (right column) look like in R2021a. Spacing properties are written in the figure names.

.

And here's a grid of all 12 combinations of the 4 TileSpacing options and 3 Padding options in R2021a.

.

Code used to generate these figures

%% Animate the RowMajor and ColumnMajor indexing with colored tiles 
fig1 = figure('position',[200 200 560 420]); 
tlo1 = tiledlayout(fig1, 3, 3, 'TileIndexing','rowmajor');
title(tlo1, 'RowMajor indexing')
fig2 = figure('position',[760 200 560 420]); 
tlo2 = tiledlayout(fig2, 3, 3, 'TileIndexing','columnmajor');
title(tlo2, 'ColumnMajor indexing')
colors = jet(9);
drawnow()
for i = 1:9
    ax = nexttile(tlo1);
    ax.Color = colors(i,:);
    text(ax, .5, .5, num2str(i), 'Horiz','Cent','Vert','Mid','Fontsize',24)
      ax = nexttile(tlo2);
      ax.Color = colors(i,:);
      text(ax, .5, .5, num2str(i), 'Horiz','Cent','Vert','Mid','Fontsize',24)
      drawnow
      pause(.3)
  end
%% Show TileSpacing options
tileSpacing = ["loose","compact","tight","none"];
figHeight = 140;  % unit: pixels
figPosY = fliplr(50 : figHeight+32 : (figHeight+30)*numel(tileSpacing)); 
for i = 1:numel(tileSpacing)
    uif = uifigure('Units','Pixels','Position', [150 figPosY(i) 580 figHeight], ...
        'Name', ['TileSpacing: ', tileSpacing{i}]);
    tlo = tiledlayout(uif,1,3,'TileSpacing',tileSpacing(i)); 
    h = arrayfun(@(i)nexttile(tlo), 1:tlo.GridSize(2));
    box(h,'on')
    drawnow()
end
%% Show Padding options
padding = ["loose","compact","tight"];
for i = 1:numel(padding)
    uif = uifigure('Units','Pixels','Position', [732 figPosY(i) 580 figHeight], ...
        'Name', ['Padding: ', padding{i}]);
    tlo = tiledlayout(uif,1,3,'Padding',padding(i)); 
    h = arrayfun(@(i)nexttile(tlo), 1:tlo.GridSize(2));
    box(h,'on')
    drawnow()
end
%% Show all combinations of TileSpacing and Padding options
tileSpacing = ["loose","compact","tight","none"];
padding = ["loose","compact","tight"];
[tsIdx, padIdx] = meshgrid(1:numel(tileSpacing), 1:numel(padding));
figSize = [320 220]; % width, height (pixels)
figPosX = 150 + (figSize(1)+2)*(0:numel(tileSpacing)-1); 
figPosY = 50 + (figSize(2)+32)*(0:numel(padding)-1);
[figX, figY] = meshgrid(figPosX, fliplr(figPosY));
for i = 1:numel(padIdx)
    uif = uifigure('Units','pixels','Position',[figX(i), figY(i), figSize], ...
        'name', ['TS: ', tileSpacing{tsIdx(i)}, ', Pad: ', padding{padIdx(i)}]);
    tlo = tiledlayout(uif,2,2,'TileSpacing',tileSpacing(tsIdx(i)),'Padding',padding(padIdx(i))); 
    h = arrayfun(@(i)nexttile(tlo), 1:prod(tlo.GridSize));
    box(h,'on')
    drawnow()
end

Did you know you can use most emoticons in text objects?

Most emoticons are just unicode characters. Using them as characters in Matlab is as simple as finding their numeric representation and then converting the numeric value back to character. Not all emoticons are convertible in Matlab.

Here's a secret message with emoticons. Use char(x) to decode it.

x = [79 77 71 33 32 55357 56878 32 104 97 118 101 32 121 111 ...
    117 32 117 112 100 97 116 101 100 32 116 111 32 77 97 116 ...
    108 97 98 32 55358 56595 32 114 50 48 50 49 97 32 121 101 116 32 8265];

Happy St. Patrick's Day!

fig = figure('MenuBar','none','Color', [0 .62 .376]);  % Shamrock green
ax = axes(fig,'Units','Normalized','Position',[0 0 1 1]);
axis(ax,'off')
axis(ax,'equal')
hold(ax,'on')
xlim(ax,[-1,1]); ylim(ax,[-1,1])
text(ax, 0, 0, char(9752), 'VerticalAlignment','middle','HorizontalAlignment','center','FontSize', 200)
str = num2cell('Happy St Patrick''s day!');
th = linspace(-pi/2,pi/2,numel(str)); 
txtHandle = text(ax,sin(th)*.8, cos(th)*.8, str, 'VerticalAlignment','middle','HorizontalAlignment','center','FontSize', 25);
set(txtHandle,{'rotation'}, num2cell(rad2deg(-th')))
thr = 0.017;
rotateCCW = @(xyz)([cos(thr) -sin(thr) 0; sin(thr), cos(thr), 0; 0 0 1]*xyz.').';
while all(isvalid(txtHandle))
    newposition = rotateCCW(vertcat(txtHandle.Position)); 
    set(txtHandle,{'position'}, mat2cell(newposition,ones(numel(txtHandle),1),3), ...
        {'rotation'}, num2cell([txtHandle.Rotation].'+thr*180/pi))
    drawnow()
end

We've all been there. You've got some kind of output that displays perfectly in the command window and you just want to capture that display as a string so you can use it again somewhere else. Maybe it's a multidimensional array, a table, a structure, or a fit object that perfectly displays the information you need in a neat and tidy format but when you try to recreate the display in a string variable it's like reconstructing the Taj Mahal out of legos.

Enter Matlab r2021a > formattedDisplayText()

Use str=formattedDisplayText(var) the same way you use disp(var) except instead of displaying the output, it's stored as a string as it would appear in the command window.

Additional name-value pairs allow you to

  • Specify a numeric format
  • Specify loose|compact line spacing
  • Display true|false instead of 1|0 for logical values
  • Include or suppress markup formatting that may appear in the display such as the bold headers in tables.

Demo: Record the input table and results of a polynomial curve fit

load census
[fitobj, gof] = fit(cdate, pop, 'poly3', 'normalize', 'on')

Results printed to the command window:

fitobj = 
     Linear model Poly3:
     fitobj(x) = p1*x^3 + p2*x^2 + p3*x + p4
       where x is normalized by mean 1890 and std 62.05
     Coefficients (with 95% confidence bounds):
       p1 =       0.921  (-0.9743, 2.816)
       p2 =       25.18  (23.57, 26.79)
       p3 =       73.86  (70.33, 77.39)
       p4 =       61.74  (59.69, 63.8)
gof = 
  struct with fields:
             sse: 149.77
         rsquare: 0.99879
             dfe: 17
      adjrsquare: 0.99857
            rmse: 2.9682

Capture the input table, the printed fit object, and goodness-of-fit structure as strings:

rawDataStr = formattedDisplayText(table(cdate,pop),'SuppressMarkup',true)
fitStr = formattedDisplayText(fitobj)
gofStr = formattedDisplayText(gof)

Display the strings:

rawDataStr = 
    "    cdate     pop 
         _____    _____
         1790       3.9
         1800       5.3
         1810       7.2
         1820       9.6
         1830      12.9
         1840      17.1
         1850      23.1
         1860      31.4
         1870      38.6
         1880      50.2
         1890      62.9
         1900        76
         1910        92
         1920     105.7
         1930     122.8
         1940     131.7
         1950     150.7
         1960       179
         1970       205
         1980     226.5
         1990     248.7
     "
fitStr = 
    "     Linear model Poly3:
          ary(x) = p1*x^3 + p2*x^2 + p3*x + p4
            where x is normalized by mean 1890 and std 62.05
          Coefficients (with 95% confidence bounds):
            p1 =       0.921  (-0.9743, 2.816)
            p2 =       25.18  (23.57, 26.79)
            p3 =       73.86  (70.33, 77.39)
            p4 =       61.74  (59.69, 63.8)
     "
gofStr = 
    "           sse: 149.77
            rsquare: 0.99879
                dfe: 17
         adjrsquare: 0.99857
               rmse: 2.9682
     "

Combine the strings into a single string and write it to a text file in your temp directory:

txt =  strjoin([rawDataStr; fitStr; gofStr],[newline newline]);
file = fullfile(tempdir,'results.txt');
fid = fopen(file,'w+');
cleanup = onCleanup(@()fclose(fid)); 
fprintf(fid, '%s', txt);
clear cleanup

Open results.txt.

winopen(file) % for Windows platforms
  1. Use the new exportapp function to capture an image of your app|uifigure
  2. MATLAB's getframe now supports apps & uifigures
  3. Review: How to get the handle to an app figure

Use the new exportapp function to capture an image of your app|uifigure

Imagine these scenarios:

  • Your app contains several adjustable parameters that update an embedded plot and you'd like to remember the values of each app component so that you can recreate the plot with the same dataset
  • You're constructing a manual for your app and would like to include images of your app
  • You're app contains a process that automatically updates regularly and you'd like to store periodic snapshots of your app.

As of MATLABs R2020b release , we no longer must rely on 3rd party software to record an image of an app or uifigure.

exportapp(fig,filename) saves an image (JPEG | PNG | TIFF | PDF) of a uifigure ( fig) with the specified file name or full file path ( filename). MATLAB's documentation includes an example of how to add an [Export] button to an app that allows the user to select a path, filename, and extension for their exported image.

Here's another example that merely saves the image as a PDF to the app's main folder.

1. Add a button to the app and assign a ButtonPushed callback function to the button. This one also assigns an icon to the button in the form of an svg file.

2. Define the callback function to name the image after the app's name and include a datetime stamp. The image will be saved to the app's main folder.

% Button pushed function: SnapshotButton
function SnapshotButtonPushed(app, ~) 
    % create filename containing the app's figure name (spaces removed)
    % and a datetime stamp in format yymmdd_hhmmss
    filename = sprintf('%s_%s.pdf',regexprep(app.MyApp.Name,' +',''), datestr(now(),'yymmdd_HHMMSS'));
    % Get the app's path
    filepath = fileparts(which([mfilename,'.mlapp']));
    % Store snapshot
    exportapp(app.MyApp, fullfile(filepath,filename))
end

Matlab's getframe now supports apps & uifigures

getframe(h) captures images of axes or a uifigure as a structure containing the image data which defines a movie frame. This function has been around for a while but as of r2020b , it now supports uifigures. By capturing consecutive frames, you can create a movie that can be played back within a Matlab figure (using movie ) or as an AVI file (using VideoWriter ). This is useful when demonstrating the effects of changes to app components.

The general steps to recording a process within an app as a movie are,

1. Add a button or some other event to your app that can invoke the frame recording process.

2. Animation is typically controlled by a loop with n iterations. Preallocate the structure array that will store the outputs to getframe. The example below stores the outputs within the app so that they are available by other functions within the app. That will require you to define the variable as a property in the app.

% nFrames is the number of iterations that will be recorded.
% recordedFrames is defined as a private property within the app
app.recordedFrames(1:nFrames) = struct('cdata',[],'colormap',[]);

3. Call getframe from within the loop that controls the animation. If you're using VideoWriter to create an AVI file, you'll also do that here (not shown, but see an example in the documentation ).

% app.myAppUIFigure: the app's figure handle
% getframe() also accepts axis handles
for i = 1:nFrames
      ... % code that updates the app for the next frame
      app.recordedFrames(i) = getframe(app.myAppUIFigure);
  end

4. Now the frame data are stored in app.recordedFrames and can be accessed from anywhere within the app. To play them back as a movie,

movie(app.recordedFrames) 
% or 
movie(app.recordedFrames, n) % to play the movie n-times
movie(app.recordedFrames, n, fps) % to specify the number of frames per second

To demonstrate this, I adapted a copy of Matlab's built-in PulseGenerator.mlapp by adding

  • a record button
  • a record status lamp with frame counter
  • a playback button
  • a function that animates the effects of the Edge Knob

Recording process (The GIF is a lot faster than realtime and only shows part of the recording) (Open the image in a new window or see the attached Live Script for a clearer image).

Playback process (Open the image in a new window or see the attached Live Script for a clearer image.)

Review: How to get the handle to an app figure

To use either of these functions outside of app designer, you'll need to access the App's figure handle. By default, the HandleVisibility property of uifigures is set to off preventing the use of gcf to retrieve the figure handle. Here are 4 ways to access the app's figure handle from outside of the app.

1. Store the app's handle when opening the app.

app = myApp;
% Get the figure handle
figureHandle = app.myAppUIFigure;

2. Search for the figure handle using the app's name, tag, or any other unique property value

allfigs = findall(0, 'Type', 'figure'); % handle to all existing figures
figureHandle = findall(allfigs, 'Name', 'MyApp', 'Tag', 'MyUniqueTagName');

3. Change the HandleVisibility property to on or callback so that the figure handle is accessible by gcf anywhere or from within callback functions. This can be changed programmatically or from within the app designer component browser. Note, this is not recommended since any function that uses gcf such as axes(), clf(), etc can now access your app!.

4. If the app's figure handle is needed within a callback function external to the app, you could pass the app's figure handle in as an input variable or you could use gcbf() even if the HandleVisibility is off.

See a complete list of changes to the PulseGenerator app in the attached Live Script file to recreate the app.

Prior to r2020b the height (number of rows) and width (number of columns) of an array or table can be determined by the size function,

array = rand(102, 16);
% Method 1
[dimensions] = size(array);
h = dimensions(1);
w = dimensions(2);
% Method 2
[h, w] = size(array); %#ok<*ASGLU>
% or
[h, ~] = size(array);
[~, w] = size(array);
% Method 3
h = size(array,1);
w = size(array,2);

In r2013b, the height(T) and width(T) functions were introduced to return the size of single dimensions for tables and timetables.

Starting in r2020b, height() and width() can be applied to arrays as an alternative to the size() function.

Continuing from the section above,

h = height(array)
% h =  102
w = width(array)
% w =  16

height() and width() can also be applied to multidimensional arrays including cell and structure arrays

mdarray = rand(4,3,20);
h = height(mdarray)
% h =  4
w = width(mdarray)
% w =  3

The expanded support of the height() and width() functions means,

  1. when reading code, you can no longer assume the variable T in height(T) or width(T) refers to a table or timetable
  2. greater flexibility in expressions such as the these, below
% C is a 1x4 cell array containing 4 matrices with different dimensions
rng('default')
C = {rand(5,2), rand(2,3), rand(3,4), rand(1,1)};
celldisp(C)
% C{1} =
%       0.81472      0.09754
%       0.90579       0.2785
%       0.12699      0.54688
%       0.91338      0.95751
%       0.63236      0.96489
% C{2} =
%       0.15761      0.95717      0.80028
%       0.97059      0.48538      0.14189
% C{3} =
%       0.42176      0.95949      0.84913      0.75774
%       0.91574      0.65574      0.93399      0.74313
%       0.79221     0.035712      0.67874      0.39223
% C{4} =
%       0.65548

What's the max number of rows in C?

maxRows1 = max(cellfun(@height,C))         % using height()
% maxRows1 =  5;
maxRows2 = max(cellfun(@(x)size(x,1),C))   % using size()
% maxRows2 =  5; 

What's the total number of columns in C?

totCols1 = sum(cellfun(@width,C))          % using width()
%totCols1 =  10
totCols2 = sum(cellfun(@(x)size(x,2),C))   % using size(x,2)
% totCols2 =  10

Attached is a live script containing the content of this post.

'
The Matlab r2020b release introduces the new horizontal ('_') and vertical (' | ') line marker symbols that are centered around the coordinate similarly to the plus marker ('+').
plot(x,y,'_')
plot(x,y,'|')
See the attached Live Script to reproduce all plots in this post.
'
Use case example 1: Days in August 2020 that COVID-19 cases (vertical ticks) and number of tests (horizontal ticks) increased from the previous day in countries with populations greater than 100M (4 countries eliminated for incomplete data).
'
Use case example 2: (An alternative to stacked bar plots) Number of power outages in 2005 across regions of the USA, broken down by calendar quarters.

Add a subtitle

Multi-lined titles have been supported for a long time but starting in r2020b, you can add a subtitle with its own independent properties to a plot in two easy ways.

  1. Use the new subtitle function: s=subtitle('mySubtitle')
  2. Use the new second argument to the title function: [t,s]=title('myTitle','mySubtitle')
figure()
tiledlayout(2,2)
% Method 1
ax(1) = nexttile;
th(1) = title('Pupil size'); 
sh(1) = subtitle('Happy faces');
ax(2) = nexttile;
th(2) = title('Pupil size'); 
sh(2) = subtitle('Sad faces');
% Method 2
ax(3) = nexttile;
[th(3), sh(3)] = title('Fixation duration', 'Happy faces'); 
ax(4) = nexttile;
[th(4), sh(4)] = title('Fixation duration', 'Sad faces'); 
set(ax, 'xticklabel', [], 'yticklabel', [],'xlim',[0,1],'ylim',[0,1])
% Set all title colors to orange and subtitles colors to purple.
set(th, 'Color', [0.84314, 0.53333, 0.1451])
set(sh, 'Color', [0, 0.27843, 0.56078])

Control title/Label alignment

Title and axis label positions can be changed via their Position, VerticalAlignment and HorizontalAlignment properties but this is usually clumsy and leads to other problems when trying to align the title or labels with an axis edge. For example, when the position units are set to 'data' and the axis limits change, the corresponding axis label will change position relative to the axis edges. If units are normalized and the axis position or size changes, the corresponding label will no longer maintain its relative position to the axis, and that's assuming the normalized position was computed correctly in the first place.

Starting in r2020b, title and axis label alignment can be set to center|left|right, relative to the axis edges.

  • TitleHorizontalAlignment is a property of the axis: h.TitleHorizontalAlignment='left';
  • LabelHorizontalAlignment is a property of the ruler object that defines the x | y | z axis: h.XAxis.LabelHorizontalAlignment='left';
% Create data
x = randi(50,1,100)'; 
y = x.*[.2, -.2] + (rand(numel(x),2)-.5)*10; 
gray = [.65 .65 .65];
% Plot comparison between columns of y
figure()
tiledlayout(2,2,'TileSpacing','none')
ax(1) = nexttile(1);
plot(x, y(:,1), 'o', 'color', gray)
lsline
ylabel('Y1 (units)')
title('Regression','Y1 & Y2 separately')
ax(2) = nexttile(3);
plot(x, y(:,2), 'd', 'color', gray)
lsline
xlabel('X Label (units)')
ylabel('Y2 (units)')
grid(ax, 'on')
linkaxes(ax, 'x')
%  Move title and labels leftward
set(ax, 'TitleHorizontalAlignment', 'left')
set([ax.XAxis], 'LabelHorizontalAlignment', 'left')
set([ax.YAxis], 'LabelHorizontalAlignment', 'left')
% Combine the two comparisons into plot and flip the second 
% y-axis so trend are in the same direction
ax(3) = nexttile([2,1]);
yyaxis('left')
plot(x, y(:,1), 'o')
ylim([-6,16])
lsline
xlabel('X Label (units)')
ylabel('Y1 (units) \rightarrow')
yyaxis('right')
plot(x, y(:,2), 'd')
ylim([-16,6])
lsline
ylabel('\leftarrow Y2 (units)')
title('Direct comparison','(Y2 axis flipped)')
set(ax(3),  'YDir','Reverse')
% Align the ylabels with the minimum axis limit to emphasize the
% directions of each axis. Keep the title and xlabel centered
ax(3).YAxis(1).LabelHorizontalAlignment = 'left';
ax(3).YAxis(2).LabelHorizontalAlignment = 'right';
ax(3).TitleHorizontalAlignment = 'Center';       % not needed; default value.
ax(3).XAxis.LabelHorizontalAlignment = 'Center'; % not needed; default value.

Starting in r2020a , you can change the mouse pointer symbol in apps and uifigures.

The Pointer property of a figure defines the cursor’s default pointer symbol within the figure. You can also create your own pointer symbols (see part 3, below).

Part 1. How to define a default pointer symbol for a uifigure or app

For figures or uifigures, set the pointer property when you define the figure or change the pointer property using the figure handle.

% Set pointer when creating the figure
uifig = uifigure('Pointer', 'crosshair');
% Change pointer after creating the figure
uifig.Pointer = 'crosshair';

For apps made in AppDesigner, you can either set the pointer from the Design View or you can set the pointer property of the app’s UIFigure from the startup function using the second syntax shown above.

Part 2. How to change the pointer symbol dynamically

The pointer can be changed by setting specific conditions that trigger a change in the pointer symbol.

For example, the pointer can be temporarily changed to a busy-symbol when a button is pressed. This ButtonPushed callback function changes the pointer for 1 second.

function WaitasecondButtonPushed(app, event)
   % Change pointer for 1 second.
   set(app.UIFigure, 'Pointer','watch')
   pause(1)
   % Change back to default.
   set(app.UIFigure, 'Pointer','arrow')
   app.WaitasecondButton.Value = false;
end

The pointer can be changed every time it enters or leaves a uiaxes or any plotted object within the uiaxes. This is controlled by a set of pointer management functions that can be set in the app’s startup function.

iptSetPointerBehavior(obj,pointerBehavior) allows you to define what happens when the pointer enters, leaves, or moves within an object. Currently, only axes and axes objects seem to be supported for UIFigures.

iptPointerManager(hFigure,'enable') enables the figure’s pointer manager and updates it to recognize the newly added pointer behaviors.

The snippet below can be placed in the app’s startup function to change the pointer to crosshairs when the pointer enters the outerposition of a uiaxes and then change it back to the default arrow when it leaves the uiaxes.

% Define pointer behavior when pointer enter axes
pm.enterFcn = @(~,~) set(app.UIFigure, 'Pointer', 'crosshair');
pm.exitFcn  = @(~,~) set(app.UIFigure, 'Pointer', 'arrow');
pm.traverseFcn = [];
iptSetPointerBehavior(app.UIAxes, pm)
% Enable pointer manager for app
iptPointerManager(app.UIFigure,'enable'); 

Any function can be triggered when entering/exiting an axes object which makes the pointer management tools quite powerful. This snippet below defines a custom function cursorPositionFeedback() that responds to the pointer entering/exiting a patch object plotted within the uiaxes. When the pointer enters the patch, the patch color is changed to red, the pointer is changed to double arrows, and text appears in the app’s text area. When the pointer exits, the patch color changes back to blue, the pointer changes back to crosshairs, and the text area is cleared.

% Plot patch on uiaxes
hold(app.UIAxes, 'on')
region1 = patch(app.UIAxes,[1.5 3.5 3.5 1.5],[0 0 5 5],'b','FaceAlpha',0.07,...
    'LineWidth',2,'LineStyle','--','tag','region1');
% Define pointer behavior for patch
pm.enterFcn = @(~,~) cursorPositionFeedback(app, region1, 'in');
pm.exitFcn  = @(~,~) cursorPositionFeedback(app, region1, 'out');
pm.traverseFcn = [];
iptSetPointerBehavior(region1, pm)
% Enable pointer manager for app
iptPointerManager(app.UIFigure,'enable');
function cursorPositionFeedback(app, hobj, inout)
% When inout is 'in', change hobj facecolor to red and update textbox.
% When inout is 'out' change hobj facecolor to blue, and clear textbox.
% Check tag property of hobj to identify the object.
switch lower(inout)
    case 'in'
        facecolor = 'r';
        txt = 'Inside region 1';
        pointer = 'fleur';
    case 'out'
        facecolor = 'b';
        txt = '';
        pointer = 'crosshair';
end
hobj.FaceColor = facecolor;
app.TextArea.Value = txt;
set(app.UIFigure, 'Pointer', pointer)
end  

The app showing the demo below is attached.

Part 3. Create your own custom pointer symbol

  1. Set the figure’s pointer property to ‘custom’.
  2. Set the figure’s PointerShapeCData property to the custom pointer matrix. A custom pointer is defined by a 16x16 or 32x32 matrix where NaN values are transparent, 1=black, and 2=white.
  3. Set the figure’s PointerShapeHotSpot to [m,n] where m and n are the coordinates that define the tip or "hotspot" of the matrix.

This demo uses the attached mat file to create a black hand pointer symbol.

iconData = load('blackHandPointer.mat');
uifig = uifigure(); 
uifig.Pointer = 'custom'; 
uifig.PointerShapeCData = iconData.blackHandIcon; 
uifig.PointerShapeHotSpot = iconData.hotspot;

Also see Jiro's pointereditor() function on the file exchange which allows you to draw your own pointer.

Starting in r2020a, AppDesigner buttons and TreeNodes can display animated GIFs, SVG, and truecolor image arrays.

Every component in the App above is either a Button or a TreeNode!

Prior to r2020a the icon property of buttons and TreeNodes in AppDesigner supported JPEG, PNG, or GIF image files specified by a character vector or string array but did not support animation.

Here's how to display an animated GIF, SVG, or truecolor image in an App button or TreeNode starting in r2020a. And for the record, "GIF" is pronounced with a hard-g .

Display an animated GIF

Select the button or TreeNode from within AppDesigner > Design View and navigate to Component Browser > Inspector > Button dropdown list of properties (shown below). Select an animated GIF file and set the text and icon alignment properties.

To set the icon property programmatically,

app.Button.Icon = 'launch.gif';  % or "launch.gif"

The filename can be an image file on the Matlab path (see addpath ) or a full path to an image file.

Display SVG

Use “scalable vector graphics” files for high-resolution images that are scaled to different sizes while preserving their shape and retaining their clarity. A quick and easy way to remember which plotting function is assigned to each button in an app is to assign an image of the plot to the button.

After creating the figure, expand the axes by setting the position or outerposition property to [0 0 1 1] in normalized units and save the figure using File > Save as and select svg format. Save the image to the folder containing your app. Then follow the same procedure as animated GIFs.

Display truecolor image

A truecolor image comes in the form of an [m x n x 3] array where each m x n pixel color is specified by an RGB triplet (read more) . This feature allows you to dynamically create a digital image or to upload an image from a mat file rather than an image file.

In this example, a progress bar is created within the uibutton callback function and it’s updated within a loop. For a complete demo of this feature see this comment .

% Button pushed function: ProcessDataButton
function ProcessDataButtonPushed(app, event)
    % Change button name to "Processing"
    app.ProcessDataButton.Text = 'Processing...';
    % Put text on top of icon
    app.ProcessDataButton.IconAlignment = 'bottom';
    % Create waitbar with same color as button
    wbar = permute(repmat(app.ProcessDataButton.BackgroundColor,15,1,200),[1,3,2]);
    % Black frame around waitbar
    wbar([1,end],:,:) = 0;
    wbar(:,[1,end],:) = 0;
    % Load the empty waitbar to the button
    app.ProcessDataButton.Icon = wbar;
    % Loop through something and update waitbar
    n = 10;
    for i = 1:n
        % Update image data (royalblue)
        % if mod(i,10)==0 % update every 10 cycles; improves efficiency
         currentProg = min(round((size(wbar,2)-2)*(i/n)),size(wbar,2)-2);
         RGB = app.ProcessDataButton.Icon;
         RGB(2:end-1, 2:currentProg+1, 1) = 0.25391; % (royalblue)
         RGB(2:end-1, 2:currentProg+1, 2) = 0.41016;
         RGB(2:end-1, 2:currentProg+1, 3) = 0.87891;
           app.ProcessDataButton.Icon = RGB;
          % Pause to slow down animation
          pause(.3)
          % end
      end
      % remove waitbar
      app.ProcessDataButton.Icon = '';
      % Change button name
      app.ProcessDataButton.Text = 'Process Data';
  end

The for-loop above was improved on Feb-11-2022.

Credit for the black & teal GIF icons: lordicon.com

This week is National Volunteer Week in the USA and Canada and to celebrate, I’d like to pay tribute to the volunteers in the Matlab Central Answers forum who have given countless hours to help total strangers make progress in their education, careers, and hobbies.

As of April 20, 2020, there have been 375,869 [1,2] questions asked by 183,968 [3] contributors dating back to the earliest existing question on January 4, 2011.

41,890 volunteers have contributed at least one answer leading up to 68% of the questions answered.

There is no contribution too small for earning well-deserved recognition and appreciation. A single answer or comment may benefit countless individuals who finally find the ideal solution to a problem that kept them up at night.

A number of volunteers in the forum have contributed far and beyond the imaginable and have shared so much of their time and expertise that it’s difficult to fathom. The bar graph below shows the top 10 volunteers in the forum by the number of answers provided. It’s hard to believe that Walter Roberson , a single individual ( we think ), has contributed a portion of answers equal to more than 18% of answered questions in the forum [4]. The top two volunteers, adding Image Analyst , contributed enough answers to equal almost 30% of the answered questions. These folks along with many others not listed in the bar graph who can be found on the contributors page are the foundation of so many Matlab users’ success including my own from June 2014, when I asked my first question.

Whether you’ve come to the forum to look for an answer or to write an answer, you’re undoubtedly standing on the shoulders of giants.

Footnotes

  1. Based on the number of answered and unanswered questions listed in the ‘Status’ table in recently added questions .
  2. Questions and answers posted by the MathWorks Support Team are not included in the data presented here, though much appreciated.
  3. The number of people who provided an answer is based on sorting the contributors page by ‘answers given’ in descending order.
  4. Since a contributor can write more than one answer to a question, we can’t easily measure the number of questions answered by a contributor.

New to r2020a, leapseconds() creates a table showing all leap seconds recognized by the datetime data type.

>> leapseconds()
ans =
  27×2 timetable
       Date        Type    CumulativeAdjustment
    ___________    ____    ____________________
    30-Jun-1972     +              1 sec       
    31-Dec-1972     +              2 sec       
    31-Dec-1973     +              3 sec       
    31-Dec-1974     +              4 sec       
    31-Dec-1975     +              5 sec       
             << 21 rows removed >>
    31-Dec-2016     +             27 sec   

Leap seconds can covertly sneak into your data and cause errors that are difficult to resolve if the leap seconds go undetected. A leap second was responsible for crashing Reddit , Mozilla, Yelp, LinkedIn and other sites in 2012.

Detect leap seconds present in a vector of years

% Define a vector of years
t = 2005:2008; 
allLeapSeconds = leapseconds(); 
isYearWithLeapSecond = ismember(t,year(allLeapSeconds.Date));
% Show years that contain a leap second
t(isYearWithLeapSecond)
ans =
        2005        2008

Detect leap seconds present in a vector of months

% Define a vector of months 
t = datetime(1972, 1, 1) + calmonths(0:11);
t.Format = 'MMM-yyyy';
allLeapSeconds = leapseconds(); 
[tY,tM] = ymd(t); 
[leapSecY, leapSecM] = ymd(allLeapSeconds.Date); 
isMonthWithLeapSecond = ismember([tY(:),tM(:)], [leapSecY, leapSecM], 'rows');
% Show months that contain a leap second
t(isMonthWithLeapSecond)
ans = 
  1×2 datetime array
   Jun-1972   Dec-1972

List all leap seconds in your lifetime

% Enter your birthday in mm/dd/yyyy hh:mm:ss format
yourBirthday = '01/15/1988 14:30:00'; 
yourTimeRange = timerange(datetime(yourBirthday), datetime('now')); 
allLeapSeconds = leapseconds(); 
lifeLeapSeconds = allLeapSeconds(yourTimeRange,:);
lifeLeapSeconds.YourAge = lifeLeapSeconds.Date - datetime(yourBirthday);
lifeLeapSeconds.YourAge.Format = 'y';
% Show table
fprintf('\n Leap seconds in your lifetime:\n')
disp(lifeLeapSeconds)
Leap seconds in your lifetime:
       Date        Type    CumulativeAdjustment     YourAge  
    ___________    ____    ____________________    __________
    31-Dec-1989     +             15 sec           1.9587 yrs
    31-Dec-1990     +             16 sec            2.958 yrs
                        << 8 rows removed >>
    30-Jun-2012     +             25 sec           24.456 yrs
    30-Jun-2015     +             26 sec           27.454 yrs
    31-Dec-2016     +             27 sec            28.96 yrs

What is a leap second?

A second is defined as the time it takes a cesium-133 atom to oscillate 9,192,631,770 times under controlled conditions. The transition frequency is so precise that it takes 100 million years to gain 1 second of error [1]. If the earth’s rotation were perfectly synchronized to the atomic second, a day would be 86,400 seconds. But the earth’s rate of rotation is affected by climate, winds, atmospheric pressure, and the rate of rotation is gradually decreasing due to tidal friction [2,3]. Several months before the expected difference between the atomic clock-based time (UTC) and universal time (UT1) reaches +/- 0.9 seconds the IERS authorizes the addition (or subtraction) of 1 leap second (see plot below). Since the first leap second in 1972, all leap second adjustments have been made on the last day of June or December and all adjustments have been +1 second which explains the + signs in the type column of the leapseconds() table.

[ Image source ]

How to reference the leap second schedule in your code

Since leap second adjustments are not regularly timed, you can record the official IERS Bulletin C version used at the time of your analysis by accessing the 2nd output to leapseconds().

[T,vers] = leapseconds

What do leap seconds look like in datetime values?

A minute typically has 60 seconds spanning from 0:59. A minute containing a leap second has 61 seconds spanning from 0:60.

December 30, 2016 was a normal day. If we add the usual 86400 seconds to the start of that day, the result is the start of the next day.

d = datetime(2016, 12, 30, 'TimeZone','UTCLeapSeconds') + seconds(86400)
d = 
  datetime
   2016-12-31T00:00:00.000Z

The next day, December 31, 2016, had a leap second. If we add 86400 seconds to the start of that day, the result is not the start of the next day.

d = datetime(2016, 12, 31, 'TimeZone','UTCLeapSeconds') + seconds(86400)
d = 
  datetime
   2016-12-31T23:59:60.000Z

When will the next leap second be?

As of the current date (April 2020) the timing of the next leap second is unknown. Based on the data from the plot above, what's your guess?

References

  1. https://www.timeanddate.com/time/how-do-atomic-clocks-work.html
  2. https://www.nasa.gov/centers/goddard/news/topstory/2003/0210rotation.html
  3. https://en.wikipedia.org/wiki/%CE%94T#Universal_time

Do date ranges from two different timetables intersect?

Is a specific datetime value within the range of a timetable?

Is the range of row times in a timetable within the limits of a datetime array?

Three new functions in r2020a will help to answer these questions.

In these function inputs, TT is a timetable and input #2 is one of the following:

The tf output is a logical scalar indicating pass|fail and the whichRows output is a logical vector identifying the rows of TT that are within the specified time range.

How do these functions differ?

Let's test all 3 functions with different time ranges and a timetable of electric utility outages in the United States, provided by Matlab. The first few rows of outages.csv are shown below in a timetable. You can see that the row times are not sorted which won't affect the behavior of these functions.

8×5 timetable
     OutageTime          Region         Loss     Customers     RestorationTime            Cause       
  ________________    _____________    ______    __________    ________________    ___________________
  2002-02-01 12:18    {'SouthWest'}    458.98    1.8202e+06    2002-02-07 16:50    {'winter storm'   }
  2003-01-23 00:49    {'SouthEast'}    530.14    2.1204e+05                 NaT    {'winter storm'   }
  2003-02-07 21:15    {'SouthEast'}     289.4    1.4294e+05    2003-02-17 08:14    {'winter storm'   }
  2004-04-06 05:44    {'West'     }    434.81    3.4037e+05    2004-04-06 06:10    {'equipment fault'}
  2002-03-16 06:18    {'MidWest'  }    186.44    2.1275e+05    2002-03-18 23:23    {'severe storm'   }
  2003-06-18 02:49    {'West'     }         0             0    2003-06-18 10:54    {'attack'         }
  2004-06-20 14:39    {'West'     }    231.29           NaN    2004-06-20 19:16    {'equipment fault'}
  2002-06-06 19:28    {'West'     }    311.86           NaN    2002-06-07 00:51    {'equipment fault'}

The range of times in utility.csv is depicted by the gray timeline bar in the plot below labeled "Timetable row times". The timeline bars above it are various time ranges or scalar datetime values used to test the three new functions.

The three columns of checkboxes on the right of the plot show the results of the first output of each function, tf, for each time range.

The time ranges were created by the timerange function using the syntax timerange(startTime,endTime). The default intervalType in timerange() is 'openright' which means that datetimes are matched when they are equal to or greater than startTime and less than but not equal to endTime. The scalar datetime values were created with datetime().

The colorbar along with the colored points at the bottom of each timeline bar show the row numbers of timetable TT that were selected by the whichRows output of the three functions.

The containsrange() function returns true when all of the time range values are within the timetable including the timetable's minimum and maximum datetime.

The overlapsrange() function returns true when any of the time range values are with the timetable's range.

The withinrange() function returns true only when all of the timetable's datetime values are within the time range values. A careful observer may see that comparison number 1 is false even though that time range is exactly equal to the timetable's row time range. This is because the default value for intervalType in the timerange() function is 'openright' which does not match the end values if they are equal. If you change the intervalType to 'closed' the withinrange result for comparison 1 would be true.

The scalar datetime values for comparisons 8, 9 and 10 are all exact matches of datetimes within the timetable and result in a single match in the whichRows output. The datetime values for comparisons 7 and 11 do not match any of the timetable row times and the values in whichRows are all false even though comparison 7 is within the timetable range. For all three tests, the whichRows outputs are identical.

---------------------------------------------------------------

Here is the code used to generate this data, test the functions, and produce the plot.

% Read in the outage data as a table and convert it to a timetable TT.
T = readtable('outages.csv');
TT = table2timetable(T);
% Look at the first few rows.
head(TT)
% Show that row time vector is not sorted.
issorted(TT)
% Get the earliest and latest row time.
outageTimeLims = [min(TT.OutageTime), max(TT.OutageTime)];
% Define time ranges to test [start,end] or scalar times [datetime, NaT]
% The scalar times must be listed after time ranges.
dateRanges = [ % start, end
    outageTimeLims;                                 % original data
    outageTimeLims;                                 % equal time range
    datetime(2005,2,1), datetime(2011,2,1);         % all within
    datetime(1998,3,16), datetime(2018,4,11);       % starts before, ends after
    datetime(2000,1,1), datetime(2010,4,11);        % starts before, ends within
    datetime(2009,1,15), datetime(2019, 4,7);       % starts within, ends after
    datetime(2015,6,15), datetime(2019,12,31);      % all outside
    datetime(2008,6,6), NaT;                        % 1-value, inside, not a member
    [TT.OutageTime(find(year(TT.OutageTime)==2010,1)), NaT] % 1 value, inside, is a member
    outageTimeLims(1), NaT;                         % 1-value, on left edge
    outageTimeLims(2), NaT;                         % 1-value, on right edge
    datetime(2000,6,6), NaT;                        % 1-value, outside
    ];
nRanges = size(dateRanges,1);
dateRangeLims = [min(dateRanges,[],'all'), max(dateRanges,[],'all')];
% Set up the figure and axes
uifig = uifigure('Name', 'Timetable_intersection_demo', 'Resize', 'off');
uifig.Position(3:4) = [874,580];
movegui(uifig,'center')
uiax = uiaxes(uifig, 'Position', [0,0,uifig.Position(3:4)], 'box', 'on', 'YAxisLocation', 'right',...
    'ytick', -.5:1:nRanges, 'YTickLabel', [], 'ylim', [-3.5, nRanges], 'FontSize', 18);
hold(uiax, 'on')
grid(uiax, 'on')
uiax.Toolbar.Visible = 'off';
% Add axes labels & title
title(uiax, strrep(uifig.Name,'_',' '))
xlabel(uiax, 'Timeline')
ylab = ylabel(uiax, 'Comparison number');
set(ylab, 'Rotation', -90, 'VerticalAlignment', 'Bottom')
% Add the timetable frame
fill(uiax, outageTimeLims([1,2,2,1]), [-.7,-.7,nRanges-.3,nRanges-.3] , 0.85938*[1,1,1], ... %gainsboro
    'EdgeColor', 0.41016*[1,1,1], 'LineStyle', '--', 'LineWidth', 1.5, 'FaceAlpha', .25)     %dimgray
% Set xtick & xlim after x-axis is converted to datetime
range = @(x)max(x)-min(x); 
uiax.XLim = dateRangeLims + range(dateRangeLims).*[-.01, .40];
uiax.XTick = dateshift(dateRangeLims(1),'start','Year') : calyears(2) : dateshift(dateRangeLims(2),'start','Year','next');
xtickformat(uiax,'yyyy')
% Set up timeline plot
lineColors = [0.41016*[1,1,1]; lines(nRanges-1)]; %dimGray
uiax.Colormap = parula(size(TT,1));
tfUniCodes = {char(09745),char(09746)}; %{true, false} checkbox characters
barHeight = 0.8;
rightMargin = [max(dateRangeLims),max(uiax.XLim)];
tfCenters = linspace(rightMargin(1),rightMargin(2),5);
tfCenters([1,end]) = [];
intervalType = 'openright';  % open, closed, openleft openright(def);  see https://www.mathworks.com/help/matlab/ref/timerange.html#bvdk6vh-intervalType
% Loop through each row of dateRanges
for i = 0:nRanges-1
    % Plot timeline bar
    pObj = fill(uiax, dateRanges(i+1,[1,2,2,1]), i+[-barHeight,-barHeight,barHeight,barHeight]/2, lineColors(i+1,:), 'FaceAlpha', .4);
      % Evaluate date ranges and single values differently
      if any(isnat(dateRanges(i+1,:)))
          % Test single datetime
          tr = dateRanges(i+1,~isnat(dateRanges(i+1,:)));
          set(pObj, 'LineWidth', 3, 'EdgeAlpha', .6, 'EdgeColor', lineColors(i+1,:))
      else
          % Test date range
          tr = timerange(dateRanges(i+1,1), dateRanges(i+1,2),intervalType);
      end
      % Create timerange obj and test for intersections
      [tf(1), whichRows{1}] = containsrange(TT,tr);
      [tf(2), whichRows{2}] = overlapsrange(TT,tr);
      [tf(3), whichRows{3}] = withinrange(TT,tr);
      % Confirm that all 'whichRows' are equal
      assert(isequal(whichRows{1},whichRows{2},whichRows{3}), 'Unequal whichRows outputs.')
      if i>0
          % Add pass/fail checkboxes
          text(uiax, tfCenters(tf), repmat(i,1,sum(tf)), repmat(tfUniCodes(1),1,sum(tf)), ...
              'HorizontalAlignment', 'Center', 'Color', [0 .5 0], 'FontSize', 36, 'FontWeight', 'Bold')
          % Fail checkboxes
          text(uiax, tfCenters(~tf), repmat(i,1,sum(~tf)), repmat(tfUniCodes(2),1,sum(~tf)), ...
              'HorizontalAlignment', 'Center', 'Color', [1 0 0], 'FontSize', 36, 'FontWeight', 'Bold')
          % Plot the TT row number matches
          scatter(uiax, TT.OutageTime(whichRows{1}), repmat(i-barHeight/2+.1,1,sum(whichRows{1})), ...
              10, uiax.Colormap(whichRows{1},:), 'filled', 'MarkerFaceAlpha', 0.2)
      else
          % add stripes to reference bar
          xHatch = linspace(dateRanges(i+1,1)-days(2), dateRanges(i+1,2)+days(2), 20);
          yHatch = repmat(unique(pObj.YData), 1, 19);
          plot(uiax, [xHatch(1:end-1);xHatch(2:end)], yHatch, '-', 'Color', [1 1 1 .5], 'LineWidth', 4)
          pObj.FaceAlpha = .9;
          text(uiax, mean(outageTimeLims), 0, 'Timetable row times', 'FontSize', 20, ...
              'HorizontalAlignment', 'Center', 'VerticalAlignment', 'middle', 'FontWeight', 'Bold')
      end
  end
% Draw frame around checkboxs for duration comparisons and label intervalType
rectEdges = linspace(rightMargin(1),rightMargin(2),33);
nDurations = sum(~isnat(dateRanges(:,2)))-1;
fill(uiax, rectEdges([6,end-5,end-5,6]), [.4 .4 nDurations+[.4,.4]], 'w', ...
    'FaceAlpha', 0, 'LineWidth', 1.5, 'EdgeColor', 0.41016*[1,1,1]) % dimgray
text(uiax, rectEdges(6), nDurations/2+.5, sprintf('intervalType: %s', intervalType), 'FontSize', 16, ...
    'HorizontalAlignment', 'Center', 'VerticalAlignment', 'Bottom', 'Rotation', 90)
% Add text labels for checkboxes and comparison number
text(uiax, tfCenters, [.5 .5 .5], {'containsrange ', 'overlapsrange ', 'withinrange '}, 'Fontsize', 22, 'Rotation', 90, ...
    'HorizontalAlignment', 'right', 'FontWeight', 'b')
text(uiax, repmat(rightMargin(2)-range(xlim(uiax))*.02,1,nRanges-1), 1:nRanges-1, cellstr(num2str((1:nRanges-1)')), ...
    'HorizontalAlignment', 'Right', 'FontSize', 16)
% Add color bar; position it under the timetable bar (must be done after all other axes properties are set)
% requires coordinate tranformation from data units to fig position units.
cb = colorbar(uiax, 'Location', 'South', 'TickDirection', 'Out', 'YAxisLocation', 'Bottom', 'FontSize', 11);
caxis(uiax, [1,size(TT,1)])
cb.Position(3) = range(outageTimeLims)/range(xlim(uiax))  * (uiax.InnerPosition(3)/uifig.Position(3));
cb.Position(1) = ((outageTimeLims(1)-min(xlim(uiax)))/range(xlim(uiax)) * uiax.InnerPosition(3) + uiax.InnerPosition(1)) / uifig.Position(3); 
cb.Position(4) = 0.008;
cb.Position(2) = ((-barHeight-min(ylim(uiax))-.5)/range(ylim(uiax)) * uiax.InnerPosition(4) + uiax.InnerPosition(2)) / uifig.Position(4);
ylabel(cb, 'Timetable row number')