Creating Specialized Charts with MATLAB Object-Oriented Programming

By Ken Deeley and David Sampson, MathWorks

Developing advanced MATLAB® visualizations often involves managing multiple low-level graphics objects. This is especially the case for applications containing graphics that update dynamically. Such applications may require time-consuming programming.

A chart object provides a high-level application programming interface (API) for creating custom visualizations. A chart not only provides a convenient visualization API for your end users; it also removes the need for the user to implement low-level graphics programming.

Using a scatter plot containing the best-fit line as the main example, this article provides a step-by-step guide to creating and implementing a custom chart using MATLAB object-oriented programming. Topics include:

  • Writing a standard chart template
  • Writing a constructor method
  • Encapsulating chart data and graphics using private properties
  • Creating a high-level visualization API using Dependent properties
  • Managing the chart life cycle
  • Using inheritance to simplify the development of additional charts

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).

Figure 1. The heatmap and geobubble charts.

In addition, we've created several application-specific charts (Figure 2). You can download these charts, together with the MATLAB code used in this article, from File Exchange.

Figure 2. Custom charts available for download on File Exchange.

Creating a 2D Scatter Plot: Function or Chart?

Let's say we want to create a 2D scatter plot containing the corresponding line of best fit (Figure 3). We could use the scatter function to visualize the discrete (x,y) data points and the fitlm function from Statistics and Machine Learning Toolbox™ to compute the best-fit line.

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)

Figure 3. Best-fit line and the underlying scattered data.

The code above is sufficient for static visualizations. However, if the application requires the data to be dynamically modifiable, then we encounter several challenges:

  • If we replace the XData or YData with a new array of the same length as the current XData, the best-fit line is not dynamically updated (Figure 4).
    s.XData = s.XData+4;
    

Figure 4. The best-fit line is not updated after changing the XData of the scatter plot.

  • The Scatter object s issues a warning, and performs no graphics update, if either of its data properties (XData or YData) is set to an array that is longer or shorter than the current array.
    s.XData = s.XData(1:500);
    

We can resolve these challenges by designing a chart, ScatterFit.

Structuring the Chart Code: Function or Class?

A function encapsulates the code as a reusable unit and lets you create multiple charts without duplicating the code.

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

Note that this function requires the two data inputs (x and y). You can specify the graphics parent f (for example, a figure) as the first input argument.

  • scatterfit(x,y) specifies the two data inputs.
  • scatterfit(f,x,y) specifies the graphics parent and the data.

In the first case, the function exhibits autoparenting behavior—that is, a figure for the chart will be created automatically.

Using a function to create a chart has some drawbacks:

  • You cannot modify the data after the chart has been created.
  • To change the chart data, you need to call the function again to recreate the chart, specifying different data inputs.
  • It will be difficult for the end user to locate configurable chart parameters (such as annotations and scatter/line properties).

Implementing the chart as a class has all the benefits of code encapsulation and reusability that the function provides while also letting you modify the chart.

Defining a Chart

We'll implement the chart as a handle class, for consistency with MATLAB graphics objects and so that the chart can be modified in place. We support both the dot notation and the get/set syntax for chart properties. To achieve this, we derive the ScatterFit chart from the predefined matlab.mixin.SetGet class, which is itself a handle class.

classdef ScatterFit < matlab.mixin.SetGet

As a result, for any property, the syntax shown in Table 1 is automatically supported.

Syntax Type

Dot notation

get/set

Access

x = SF.XData;

x = get(SF,'XData');

Modification

SF.XData = x;

set(SF,'XData',x)

Table 1. Access and modification syntax for chart properties.

Writing the Chart’s Constructor Method

The constructor method is the function within the class definition where the chart object is created. A good place to start is to copy the code from the scatterfit function to our chart constructor. We then make the following modifications to support the required chart behavior:

  • Input arguments. We support the use of name-value pairs for all chart input arguments using varargin. This means that no input arguments need to be specified, and all inputs are optional.
    function obj = ScatterFit(varargin)
    
  • Parent graphics. Unlike the functional approach, if no Parent input on chart construction has been specified, then we do not automatically create one for the chart. Instead, we create an axes object with an empty Parent.
    obj.Axes = axes('Parent',[]);
    

    Note that this behavior is different from that of convenience functions such as plot and scatter, which exhibit autoparenting. If the user has specified the Parent as an input argument, then this will be set later in the constructor.

  • Chart graphics. We create and store any graphics objects required by the chart. Most charts will require an axes object together with some axes contents such as line or patch objects. In the ScatterFit chart, we need a Scatter object and a Line object.
    obj.ScatterSeries = scatter(obj.Axes,NaN,NaN);
    obj.BestFitLine = line(obj.Axes,NaN,NaN);
    
  • Graphics configuration. We configure the chart graphics by setting any required properties. For example, we may create annotations such as labels or titles, set a specific view of an axes, add a grid, or adjust the color, style, or width of a line.
  • User-specified inputs. We set any name-value pair arguments supplied by the end user. Since the chart is derived from matlab.mixin.SetGet, this is particularly easy to do:
    if ~isempty(varargin)
        set(obj,varargin{:});
    end
    

    This is where the data properties (XData and YData) are set, provided that the user has supplied these as name-value pair input arguments. We also note that this coding practice ensures that any errors caused by the user when specifying the name-value pairs will be caught and handled by the chart’s property set methods (discussed later).

Whenever practical, we use primitive objects to create the chart graphics, because high-level convenience functions reset many existing axes properties when called (Table 2). However, there are exceptions to this principle: within ScatterFit, we use the scatter function to create the Scatter graphics object because it supports subsequent changes to individual marker sizes and colors.

Primitive Graphics Function

line
surface
patch

High-Level Graphics Function

plot
surf
fill

Table 2. Examples of primitive and high-level graphics functions.

Encapsulating Chart Data and Graphics

In most charts, the underlying graphics comprise at least one axes object together with its contents (for example, line or surface objects) or axes peer objects (for example, legends or colorbars). The chart also retains the internal data properties to ensure that the public properties are consistent with each other. We store the underlying graphics and internal data as private chart properties. For example, the ScatterFit chart maintains the following private properties:

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

We use the naming convention XData_ to indicate that this is the private, internal version of the chart data. The corresponding public data property visible to the user will be named XData.

Using private properties serves three main purposes:

  • Visibility of the low-level graphics is restricted, hiding implementation details and reducing visual clutter in the API.
  • Access to the low-level graphics is restricted, reducing the chance of bypassing the API.
  • Chart data can be easily synchronized (for example, we require the XData and YData properties of ScatterFit to be related).

Providing a Visualization API

One of the main reasons for designing a chart is to provide a convenient and intuitive API. We equip the ScatterFit chart with easily recognizable properties, using names consistent with existing graphics object properties (Figure 5).

Figure 5. ScatterFit chart API.

Users can access or modify these properties using the syntax shown in Table 1. The associated chart graphics update dynamically in response to property modifications. For example, changing the LineWidth property of the chart updates the LineWidth of the best-fit line.

We implement the chart API using Dependent class properties. A Dependent property is one whose value is not stored explicitly but is derived from other properties in the class. In a chart, the Dependent properties depend on private properties such as the low-level graphics or internal data properties.

To define a Dependent property, we first declare its name in a properties block with the attribute Dependent. This indicates that the property’s value depends on other properties within the class.

properties (Dependent)
    XData
    YData
end

We also need to specify how the property depends on the other class properties by writing the corresponding get method. This method returns a single output argument—namely, the value of the Dependent property. In the ScatterFit chart, the XData property (part of the chart’s public interface) is simply the underlying XData_ property, which is stored internally as a private property of the chart.

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

We write a set method for each configurable chart property. This method assigns the user-specified value to the correct internal chart property, triggering graphics updates if necessary.

For the ScatterFit chart, we support dynamic modifications (including length changes) to the data properties (XData and YData). When the user sets the (public) XData of the chart, we either pad or truncate the opposite (private) data property YData_, depending on whether the new data vector is longer or shorter than the existing data. Recall that this set method will be invoked by the constructor if the user has specified XData when creating the chart.

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

We refresh the chart graphics by calling a separate update method. This method contains the code necessary to set the new data in the Scatter object, recompute the best-fit line, and set the new data in the corresponding Line object.

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

We implement the set method for YData in the same way, switching the roles of the X/YData properties. The update method is also called from the set method for YData.

To create a rich API appropriate for end users, we implement a broad set of Dependent properties. As a minimum, for each chart we recommend including the properties shown in Table 3.

Property

Parent

Purpose

Move the chart between figures, panels, or other container graphics


Position
Units
OuterPosition
ActivePositionProperty

Resize the chart


Visible

Toggle onscreen chart visibility

Table 3. Recommended Dependent properties.

Note that in most cases, these properties map directly to the underlying chart axes. For example, the get and set methods for the Parent property map the chart object’s Parent to the axes’ Parent.

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

We enable control of visual settings by defining additional public interface properties, with each property mapped to a specific low-level graphics object maintained by the chart. In this category, the ScatterFit chart supports various line-related properties such as LineStyle, LineWidth, and LineColor relating to the best-fit line. For example, the chart object’s LineColor property is mapped to the line object’s Color property.

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

Typical chart properties in this category include:

  • View-related properties—for example, the axes’ View, XLim, and YLim
  • Annotations—for example, the axes’ XLabel, YLabel, and Title
  • Cosmetic properties—for example, colors, line widths, and style; grids; transparencies; and lighting

Managing the Chart Life Cycle

The ScatterFit chart is closely associated with its underlying axes object, which is stored as one of the chart’s private properties. To manage the chart’s life cycle correctly, we need to guarantee two behaviors:

  • Deleting the axes (for example, by closing the main figure window) deletes the chart. If we do not guarantee this, then modifying the chart’s data properties will cause MATLAB to attempt an update on a deleted graphics object, leading to an error.
  • Deleting the chart (for example, when it goes out of scope or when its handle is deleted explicitly) deletes the axes. If we do not guarantee this, then on chart deletion we are left with its static graphical remnants.

Every graphics object in MATLAB has a property named DeleteFcn, a callback function invoked automatically when the graphics object goes out of scope. Therefore, we can guarantee the first requirement by setting the axes’ DeleteFcn in the chart constructor.

obj.Axes.DeleteFcn = @obj.onAxesDeleted;

Here, onAxesDeleted is a private class method and is simply a wrapper around the chart destructor method. We recall that every handle class comes equipped with a customizable destructor method. The destructor method is invoked when the object goes out of scope.

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

We ensure the second requirement by writing a custom chart destructor. On chart destruction, we delete the chart’s axes.

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

Implementing both requirements equips the chart object with the same life cycle as its underlying axes (Figure 6).

Figure 6. Managing the chart and axes life cycle.

Simplifying the Development of Additional Charts

Once we have written several charts, it is easy to identify similarities and duplicated sections of code. We can speed up the process of writing additional charts by centralizing common code in a superclass. Each new chart can be derived from this superclass, enabling us to focus on the implementation details of that particular chart and reducing the need for repetitive coding.

Our superclass (named Chart) is structured as follows:

  • Chart is derived from matlab.mixin.SetGet.
  • Chart implements the six core Dependent properties Parent, Position, Units, OuterPosition, ActivePositionProperty, and Visible.
  • Chart has a protected property Axes (the underlying graphics peer).
  • The Chart constructor creates the peer axes object and sets the axes’ DeleteFcn as the protected method onAxesDeleted. In turn, this method deletes the chart object.
  • The Chart destructor deletes the axes object.

Note that using the superclass Chart may not be appropriate for all charts. For example, charts maintaining multiple axes require some changes to the architecture described above. We could implement such charts using a uipanel instead of an axes object as the chart’s underlying graphics peer, and creating the multiple axes inside the panel.

Summary

In this article, we described a design pattern for implementing 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.

Published 2018


View Articles for Related Capabilities