使用MATLAB面向对象编程创建专用图表

作者 Ken Deeley and David Sampson, MathWorks


开发高级MATLAB®可视化效果通常需要管理多个低层的图形对象,包含动态更新图形的应用程序更是如此。这种应用程序可能需要非常耗时的编程。

Chart对象可提供高级应用程序编程接口(API),实现可视化的自定义创建。图表可为最终用户提供方便的可视化API,无需用户执行低层图形编程。

本文以包含最佳拟合线的散点图为主要示例,通过分步指导展示如何使用MATLAB面向对象的编程创建和实现自定义图表。包含以下主题:

  • 编写标准图表模板
  • 编写构造方法
  • 使用private属性封装图表数据和图形
  • 使用Dependent属性创建高级可视化API
  • 管理图表生命周期
  • 使用继承简化附加图表的开发

图表示例

MATLAB提供几种图表,包括heatmap图和geobubble图,前者显示叠放在彩色网格上的矩阵值,后者可快速在地图上绘制离散数据点(图1)。

Chart Examples

Several charts are available in MATLAB, including the heatmap chart, which visualizes matrix values overlaid on colored grid squares, and the geobubble chart, which provides a quick way to plot discrete data points on a map (Figure 1).

图1. heatmap 图和 geobubble 图。

此外,我们还创建几种特定于应用程序的图表(图2)。您可以从 File Exchange中随本文使用的MATLAB代码一起,下载这些图表。

图2. 可在File Exchange中下载自定义图表。

创建二维散点图:函数或图表?

假设我们要创建包含对应最佳拟合线的二维散点图(图3)。我们可以使用scatter函数显示离散(x,y)数据点,并使用Statistics and Machine Learning Toolbox™中的fitlm函数计算最佳拟合线。

rng default
x = randn(1000,1);
y = 2*x+1+randn(size(x));
s = scatter(x,y,6,'filled','MarkerFaceAlpha',0.5);
m = fitlm(x,y);
hold on
plot(x,m.Fitted,'LineWidth',2)

图3. 最佳拟合线和底层的分散数据。

以上代码可满足静态可视化的需求。但是,如果应用程序要求对数据进行动态修改,我们会遇到几个难题:

  • 如果使用长度与当前XData相同的新数组替换XDataYdata,最佳拟合线不会动态更新(图4)。
    s.XData = s.XData+4;
    

图4. 最佳拟合线在更改散点图的XData后未更新。

  • 如果Scatter 对象s的任一数据属性(XDataYData) 设置为长度比当前数组长或短的数组,该对象会发出警告且不会执行图形更新。
    s.XData = s.XData(1:500);
    

我们可以通过设计一个图表ScatterFit来解决这些难题。

构建图表代码:函数或类?

函数将代码封装为可重用单元,用户无需重复代码即可创建多个图表。

function scatterfit(varargin)
 
% Ensure 2 or 3 inputs.
narginchk(2,3)
 
% We support the calling syntax scatterfit(x,y) or 
% scatterfit(f,x,y), where f is the parent graphics.
switch nargin
    case 2
        f = gcf;
        x = varargin{1};
        y = varargin{2};
    otherwise % case 3
        f = varargin{1};
        x = varargin{2};
        y = varargin{3};
end % switch/case
 
% Create the chart axes and scatter plot.
ax = axes('Parent',f);
scatter(ax,x,y,6,'filled')
% Compute and create the best-fit line.      
m = fitlm(x,y);
hold(ax,'on')
plot(ax,x,m.Fitted,'LineWidth',2)
hold(ax,'off')
 
end % scatterfit function

请注意,此函数需要输入两个数据(xy)。您可以指定图形父项f(例如,图形)作为第一个输入参数。

  • scatterfit(x,y)指定输入的两个数据。
  • scatterfit(f,x,y)指定图形父项和数据。

在第一种情况下,该函数展示自动生成的行为,即将自动创建图表的图形。

使用函数创建图表具有某些缺点:

  • 图表创建后数据无法修改。
  • 要更改图表数据,指定不同的数据输入后,需要再次调用函数重新创建图表。
  • 最终用户很难查找可配置的图表参数(如注释和分散/线属性)

用类来实现图表具有代码封装及方法可重用性等好处,同时支持对图表进行修改。

定义图表类

为了与MATLAB图形对象保持一致,我们将图表实现为handle类,以便在适当位置修改图表。我们支持在图表属性中使用点记法和get/set语法。要实现这一目标,我们从预定义matlab.mixin.SetGet类(本身是handle类)中派生出ScatterFit图表。

classdef ScatterFit < matlab.mixin.SetGet

因此,任何属性都将自动支持表1中显示的语法。

Syntax Type Access Modification
Dot notation x = SF.XData; SF.XData = x;
get/set x = get(SF, "XData"); set(SF, "XData", x)

表1. 图表属性的访问和修改语法。

编写图表类的构造方法

构造方法是类定义中的一个函数,用于构造图表对象。首先,将代码从scatterfit函数复制到我们的图表构造方法之中。随后,进行以下修改支持所需图表行为:

  • 输入参数。我们使用varargin支持在所有图表输入参数中使用名称值对。这意味着不需要指定输入参数,所有输入值均可选。
    function obj = ScatterFit(varargin)
    
  • 父图形。不同于函数,如果已指定图形构造中没有输入 Parent 则无法为图表自动创建该输入。我们使用空 Parent创建axes对象。
    obj.Axes = axes('Parent',[]);
    

    请注意,此行为与plotscatter等便捷函数的行为不同,后两者可以自动生成输入。如果用户将Parent指定为输入参数,则随后将在构造方法中对其进行设置。

  • 图表图形。我们可创建和存储图形所需的任何图形对象。大多数图表需要axes对象以及线或贴片对象等axes内容。在ScatterFit图表中,我们需要Scatter对象和Line对象。
    obj.ScatterSeries = scatter(obj.Axes,NaN,NaN);
    obj.BestFitLine = line(obj.Axes,NaN,NaN);
    
  • 图形配置。我们可通过设置任何所需属性配置图表图形。例如,我创建标签或标题等注释、设置axes的特定视图、添加网格,或者调整颜色、样式或线宽。
  • 用户指定的输入。我们可设置最终用户提供的任何名称值对参数。由于图表派生自matlab.mixin.SetGet,实现这一点非常容易:
    if ~isempty(varargin)
        set(obj,varargin{:});
    end
    

    如果用户已作为名称值对输入参数提供数据属性,在此处设置这些数据(XDataYData)。我们还注意到,此编码方法可确保用户在指定名称值对时出现的任何错误都将被图表的属性set方法发现和解决(稍后讨论)。

如可行,我们将使用基元对象创建图表图形,因为高级便捷函数在调用时将重置多个现有axes属性(图2)。但是,这条原则存在例外情况:在ScatterFit内部,我们将使用scatter函数创建Scatter图形对象,因为它支持对个别标记的尺寸和颜色进行后续更改。

Primitive Graphics Function High-Level Graphics Function
line plot
surface surf
patch fill

表2. 基元和高级图形函数的示例。

封装图表数据和图形

在大多数图表中,底层图形包含至少一个axes对象及其内容(例如线或面对象)或多个对等的axes对象(例如,图例或彩条)。这些图表还包含内部数据属性,以确保公共属性之间保持一致。我们存储底层图形和内部数据为专有图表属性。例如,ScatterFit图表会维护以下专有属性:

properties (Access = private)
    XData_
    YData_
    Axes
    ScatterSeries
    BestFitLine
    Legend
end

我们使用命名约定XData_指示该版本是图表数据的专有内部版本。用户可见的对应公共数据属性将命名为XData

使用private属性主要有三个目的:

  • 限制底层图形的可见性,隐藏实现细节,减少API中的视觉混乱。
  • 限制对底层图形的访问,减少绕过API的几率。
  • 轻松同步图表数据(例如,我们需要对ScatterFitXDataYData属性进行关联)。

提供可视化API

设计图表的一个主要原因是提供方便、直观的API。我们使用与现有图形对象属性一致的名称为ScatterFit图表提供容易识别的属性(图5)。

图5. ScatterFit图表API。

用户可使用表1中所示的语法访问或修改这些属性。关联的图表图形会动态更新,以响应属性的修改。例如,更改图表的LineWidth属性会更新最佳拟合线的LineWidth

我们使用Dependent类属性实现图表API。Dependent属性的值未进行显式存储,而是获取自类中的其他属性。在图表中,Dependent属性依赖于低级别图形等专有属性或内部数据属性。 要定义Dependent属性,我们首先使用属性Dependentproperties块中声明其名称。这表明该属性的值依赖于类中的其他属性。

properties (Dependent)
    XData
    YData
end

通过编写对应的get方法,我们还可以指定依赖于其他属性的属性。此方法会返回单个输出参数,即Dependent属性的值。在ScatterFit图表中,XData属性(图表公共接口的一部分)就是底层XData_属性,它作为图表的private属性存储在内部。

function x = get.XData(obj)
    x = obj.XData_;
end

我们为每个可配置的图表属性编写set方法。此方法将用户指定的值分配到正确的内部图表属性,以在必要时触发图形更新。

对于ScatterFit图表,我们支持对数据属性(XDataYData)进行动态修改(包括长度更改)。当用户设置图表的(公共)XData时,我们我们将根据比较新数据矢量与现有数据的长短,填充或截断相对的(专有)数据属性YData_。请记住,如果用户在创建图表时已指定XData,此set方法将被构造方法调用。

function set.XData( obj, x )
            
    % Perform basic validation.
    validateattributes(x,{'double'},...
    {'real', 'vector'},'ScatterFit/set.XData','the x-data')
            
    % Decide how to modify the chart data.
    nX = numel(x);
    nY = numel(obj.YData_);
           
    if nX < nY % If the new x-data is too short ...
        % ... then truncate the chart y-data.
        obj.YData_ = obj.YData_(1:nX);
    else
        % Otherwise, if nX >= nY, then pad the y-data.
        obj.YData_(end+1:nX) = NaN;
    end % if
            
    % Set the internal x-data.
    obj.XData_ = x(:);
            
    % Update the chart graphics.
    update(obj);
            
end % set.XData

我们通过调用不同的update方法刷新图表图形。此方法包含在Scatter对象中设置新数据、重新计算最佳拟合线,以及在对应的Line对象中设置新数据所需的代码。

function update(obj)
            
    % Update the scatter series with the new data.
    set(obj.ScatterSeries,'XData',obj.XData_,...
                          'YData',obj.YData_);
    % Obtain the new best-fit line. 
    m = fitlm(obj.XData_,obj.YData_);
    % Update the best-fit line graphics.
    [~, posMin] = min(obj.XData_);
    [~, posMax] = max(obj.XData_);
    set(obj.BestFitLine,'XData',obj.XData_([posMin, posMax]), ...
                        'YData',m.Fitted([posMin, posMax]));
        
end % update

我们以相同方式为YData实现set方法,以切换X/YData属性的角色。还会从 set 方法中为 YData 调用 update 方法。

要创建适用于最终用户的丰富API,我们会实现一组广泛的Dependent属性。建议在每个图表中至少包含表3中所示的属性。

表3. 建议的Dependent 属性。

请注意,在大多数情况下,这些属性将直接映射到底层图表axes。例如,Parent属性的get和set方法将图表对象的Parent映射到axesParent

function value = get.Parent(obj)
    value = obj.Axes.Parent;
end
 
function set.Parent(obj,value)
    obj.Axes.Parent = value;
end

我们通过定义额外公共接口属性启用对可视化设置的控制,其中每个属性映射到图表维护的特定低级别图形对象。在此类别中,ScatterFit图表支持各种线相关属性,如最佳拟合线相关LineStyle、LineWidth和LineColor。例如,图表对象的LineColor属性会映射到线对象的Color属性。

function value = get.LineColor(obj)
    value = obj.BestFitLine.Color;
end
 
function set.LineColor(obj,value)
    obj.BestFitLine.Color = value;
end

此类别中的典型图表属性包括:

  • 视图相关属性 — 例如,axes的ViewXLimYLim
  • 注释— 例如,axes的XLabel, YLabelTitle
  • 装饰性属性 —例如,颜色、线宽,样式、网格、透明效果和明暗度

管理图表的生命周期

ScatterFit图表与其底层axes对象密切关联,该对象作为图表的private属性之一存储。要正确管理图表的生命周期,我们需要确保两种行为:

  • 删除axes(例如,通过关闭主图形窗口)即删除图表。如果不能保证这一点,一旦修改图表的数据属性,将导致MATLAB尝试更新删除的图形对象,从而引发错误。
  • 删除图表(例如,在其超出范围时或当其句柄显式删除时)即删除axes。如果做不到这一点,则在删除图表后,仍会残留其静态图形。

MATLAB中的每个图形对象都具有DeleteFcn属性 — 一种在图形对象超出范围后被自动调用的回调函数。因此,在图表构造方法中设置axes的DeleteFcn满足第一个要求。

obj.Axes.DeleteFcn = @obj.onAxesDeleted;

此处,onAxesDeletedprivate类方法,仅充当图表析构方法周围的包装程序。如前所述,每个handle类在创建时都包含可自定义析构方法。当对象超过范围后,析构方法将被调用。

function onAxesDeleted(obj,~,~)
     delete(obj);
end

通过编写自定义图表类的析构方法,我们可满足第二个要求。在图表析构时,我们将删除图表的axes。

function delete(obj)
     delete(obj.Axes);
end

实现这两个要求后,图表对象与其底层axes将具有相同的生命周期(图6)。

图6. 管理图表和axes生命周期。

We initialize these controls in the chart setup method, using components for app building. Each control has a callback function, implemented as a private method. This method has three input arguments:

  • The chart object.
  • A reference to the source object (the object responsible for triggering the callback)—in this case, the source object is the corresponding user control.
  • Event data. This is an object automatically passed to the callback function by MATLAB when the user interacts with the control. The event data object contains additional information about the event.

For example, consider the callback function for the checkbox controlling the visibility of the best-fit line. This function toggles the visibility of the underlying line object based on the value of the checkbox.

function toggleLineVisibility(obj, s, ~)
%TOGGLELINEVISIBILITY Toggle the visibility of the best-fit line.

    obj.BestFitLine.Visible = s.Value;
            
end % toggleLineVisibility

The values of each control must be synchronized with the corresponding chart property. To achieve this, we equip the chart property with the Dependent attribute and then implement its get and set methods. Note that in addition to updating the internal graphics object, the set method must also update the value of the control object.

The code corresponding to the best-fit line visibility is shown below. To ensure compatibility between the property and checkbox values, we convert the property to the matlab.lang.OnOffSwitchState type. This type supports any compatible syntax for representing true and false values, such as 1 and 0, as well as "on" and "off".

properties (Dependent)
    % Visibility of the best-fit line.
    LineVisible(1, 1) matlab.lang.OnOffSwitchState
end % properties (Dependent)

function value = get.LineVisible(obj)
            
value = obj.BestFitLine.Visible;
            
end % get.LineVisible
        
function set.LineVisible(obj, value)
            
% Update the property.
obj.BestFitLine.Visible = value;
% Update the check box.
obj.BestFitLineCheckBox.Value = value;
            
end % set.LineVisible

Integrating Charts with App Designer

As of MATLAB R2021a, charts developed using the ComponentContainer superclass can be integrated with App Designer (Figure 7). With App Designer you can share charts with end users by creating metadata. The installed chart will then appear in the user’s App Designer Component Library, where it can be used interactively in the canvas like any other component.

Figure 7. Custom chart integrated with App Designer.

Summary

In this article, we described design patterns and best practices for the implementation of custom charts, using the ScatterFit chart as an example. Many common visualization tasks, especially those that require dynamic graphics, can be performed using an appropriate chart. Designing and creating a chart requires up-front development time and effort, but charts can substantially simplify many visualization workflows.

简化附加表格的开发

在编写几个图表后,我们能够轻松识别相似性和重复的代码段。通过在超类中集中放置通用代码,我们可以加速编写额外图表的进程。每个新图表可派生自此超类,这可使我们专注于实现该特定图表的细节,减少重复编码的需求。

我们的超类(即Chart)具有以下结构:

  • Chart 派生自matlab.mixin.SetGet.
  • Chart 将实现六个核心 Dependent 属性 Parent, PositionUnitsOuterPositionActivePositionPropertyVisible.
  • Chart 具有 protected 属性 Axes(底层对等图形)。
  • Chart 构造方法将创建对等的axes对象并将axes的 DeleteFcn 设置为 protected 方法 onAxesDeleted。此方法进而将删除图表对象。
  • Chart 析构方法将删除axes对象。

请注意,使用超类Chart可能并不适用于所有图表。例如,维护多个axes的图表需要对上述体系结构进行某些更改。我们可以实现此类图表,方法是使用uipanel替代axes对象作为图表的对等底层图形,并在面板内部创建多个axes。

总结

在本文中,我们以ScatterFit图表为例介绍实现自定义图表的设计模式。许多公共可视化任务,尤其是需要动态图形的任务,可使用适当的图表执行。设计和创建图表需要事先投入开发时间和精力,但图表可以大幅简化大量可视化工作流。

出版年份 2021

了解更多

查看文章,了解相关功能