Technical Articles and Newsletters

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

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.

## 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)
```

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;
```
• 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).

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

## 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