ChartContainer is broken?

36 views (last 30 days)
Jan Kappen
Jan Kappen on 9 Feb 2021
Commented: Jan Kappen on 3 Feb 2025 at 10:04
Hi all,
Since R2020a, class properties, provided by the class constructor are assigned after the setup method runs, see https://www.mathworks.com/help/matlab/ref/matlab.graphics.chartcontainer.chartcontainer.setup.html#mw_892e1871-5986-41cf-b037-396fa9a9adbf
That means, I can't create plots that rely somehow on external information, provided by the user via constructor. An easy example would be this:
The user provides a timetable. Each column should be plotted to its own subplot:
classdef BrokenChartClass < matlab.graphics.chartcontainer.ChartContainer
properties
Data timetable
end
properties (Access = private)
NumSubplots = 1
DataAxes
DataLines
end
methods
function obj = BrokenChartClass(T)
arguments
T timetable
end
obj@matlab.graphics.chartcontainer.ChartContainer();
obj.NumSubplots = length(T.Properties.VariableNames); % create as many subplots as columns in data table
obj.Data = T;
end
end
methods (Access = protected)
function setup(obj)
tcl = getLayout(obj);
obj.DataAxes = arrayfun(@(n)nexttile(tcl, n), 1:obj.NumSubplots);
obj.DataLines = arrayfun(@(hax)plot(hax, NaT, NaN), obj.DataAxes);
end
function update(obj)
% Extract the time data from the table.
tbl = obj.Data;
t = tbl.Properties.RowTimes;
for k = 1:length(obj.DataLines)
set(obj.DataLines(k), 'XData', t, 'YData', tbl{:,k})
end
end
end
end
%% test via
T = timetable((datetime():seconds(1):datetime+hours(1))',randn(3601,1),10*randn(3601,1)); % 3601 rows, 2 data cols
head(T)
BrokenChartClass(T(:,1)); % -> one subplot, makes sense
BrokenChartClass(T(:,1:2)); % -> one subplot (uses default value of NumSubplots), not as expected but documented
Could you please help me understanding this concept? Calling setup before assigning the properties makes it impossible to use it. I always have to create another mySetup function to be called in update which requires some flag like bool IsInitialized or so.
I can't believe that's the purpose of this class. What am I missing?
Thanks!
  3 Comments
Jörn Froböse
Jörn Froböse on 16 Oct 2022
Same thing for me. Did you find any better solution than this workaround with mySetup?
Jan Kappen
Jan Kappen on 31 Jan 2025 at 9:46
@Jörn Froböse check out the answers from Benjamin below, they helped me a lot to understand the concept.

Sign in to comment.

Accepted Answer

Benjamin Kraus
Benjamin Kraus on 29 Jan 2025 at 20:20
Edited: Benjamin Kraus on 29 Jan 2025 at 20:25
@Jan Kappen: This is a question that frequently comes up. Let me try to explain our reasoning for making this change.
One of our goals with matlab.graphics.chartcontainer.ChartContainer was to make it easy for users to write charts that behave like built-in charts and work in the graphics ecosystem like other charts.
One aspect of our built-in charts is that (for improved performace and to support saving to FIG-files), you can change the data in your property after the object/chart has been created. You can also query the data back from the chart (most often to modify it, such as appending new data).
In the class you defined above, the Data property is a user-settable property, but the chart, as written above, won't work if the user changes the timetable stored in Data to have a different number of variables after creating the chart:
t = datetime()+hours(1:10)';
t1 = array2timetable(rand(10),'RowTimes', t);
t2 = array2timetable(rand(10,5),'RowTimes', t);
b = BrokenChartClass(t1);
b.Data = t2;
With the code above, you would set NumSubPlots to 10 within the constructor then you would set both DataAxes and DataLines within setup. This is only happening once when the chart is first created, and doesn't update when the Data changes. That means that within update you would get an index-out-of-bounds error because your table is narrower than 10. In addition, the number of axes and lines created will be incorrect. If you were to set Data to a table with 20 variables, your code as written would ignore all but the first 10 variables.
If a property is designed to be set by a user, it should be changable by the user at any time, including after the chart was first created.
There are two ways to allow the user to change the Data property without breaking the chart:
Option 1 - Not recommended (because it is more complicated): You can add a set.Data method. That method will be called any time the Data property is set. Within that method, you can update the values of NumSubPlots and DataAxes and DataLines based on the new value. This is not the recommended pattern for two reasons:
  • It causes complicated order-dependency issues between properties, which makes the logic in the code harder to follow and also often breaks saving and loading the chart. If you want to use this approach, you can resolve most of the save issues by making the NumSubPlots and DataAxes and DataLines transient properties.
  • It prevents the graphics system from automatically optimizing the performance of your chart. set.Data will run any time the user sets the Data property, but update will only run when MATLAB goes idle or you call drawnow. Imagine a user who is iteratively setting values in the Data property (see the code below). In that example, set.Data will run 10 times, but update will only run once. For that reason, we recommend using update instead (option 2).
t = datetime()+hours(1:10)';
t1 = array2timetable(rand(10),'RowTimes', t);
b = BrokenChartClass(t1);
for ii = 1:10
b.Data{1,ii} = b.Data{1,ii}+1;
end
Option 2 - Recommended: With update, check whether the number of variables in the table matches the current value of NumSubPlots. If they are different, update NumSubPlots and DataAxes and DataLines accordingly. This is the recommended pattern.
Note that neither of those approaches relied on an IsInitialized state on the chart, because that just recreates the same problem as-if you were to have run the code within setup.
What we found when people were writing charts is that in their first draft of the chart they would do a bunch of setup operations within setup, leveraging the user provided name/value pairs. Then they would discover that the chart doesn't support changing the data after the chart is created (and that saving and loading the chart is broken) and add duplicate code to the update method to do the same operation (or typically a subset of the operation) again. In reality, the code never should have been in setup in the first place.
The general rule of thumb I apply is that:
  • setup should only be used to do setup that will never change in the chart. Because users of the chart can change property values, setup should never rely on user-settable property values (or property values that could be changed indirectly by users, such as NumSubplots).
  • update should be used for anything that could change, such as if the user sets a property value.
By setting the name/value pairs after calling setup, we are encouraging people who are writing charts to follow that rule of thumb by not allowing access to the name/value pairs until after setup has finished.
We actually include an example chart in our documentation that shows the recommended pattern for adjusting the number of lines to update based on the data: Optimized Chart Class for Displaying Variable Number of Lines. I recommend taking a look at that example, as it can be easily adapted to create one axes per line as well.
Our goal with matlab.graphics.chartcontainer.ChartContainer was to allow all MATLAB users to easily create their own charts that behave like first-class citizens, but it is worth mentioning for power users that:
  • update is a "special" method. The graphics system will automatically call update once per graphics update and it will only call update if something has indicated to the system that your chart is out-of-date (such as a property value was set or the figure changed sizes).
  • setup is much less "special". It is called automatically from the ChartContainer constructor. If your chart does not have the ConstructOnLoad attribute, it is also called once when your chart is loaded from a FIG-file. Otherwise, there is nothing stopping a power-user from leaving setup empty and putting all the setup code within their own custom constructor, in which case they would have access to anything the user specified.
  6 Comments
Benjamin Kraus
Benjamin Kraus on 31 Jan 2025 at 14:52
@Jan Kappen: I'm glad you found these answers informative and useful. I'll talk to the documentation writer on my team to see if we can get some of this information into the documentation to help other users.
Regarding using set vs. setting properties one at a time:
Using set to set multiple properties "at once" should be nearly identical in performance (and in almost every other way) compared to setting each property individually. When you use set, the properties are set one at a time, in the order specified when you call set. In fact, you may get a (very, very slight) penalty using set because set supports ambiguous property matching (in other words, set(gca, 'pOsi', [0 0 1 1]) works equivalent to set(gca, 'Position', [0 0 1 1])) but using obj.Position requires the exact, case-sensitive, property name. The set method checks for exact matches first, so any difference in performance will likely be very small (if you could even measure it).
As for "does it prevent calling the drawnow function in between": It is true that calling set to set multiple properties "at once" will help you avoid triggering an update traversal in-between setting properties, but the same is true if you run a block of property sets all at once.
You can experiement with this by putting disp('running update') within your update method.
Using your example (with an added call to disp).
obj.DataLines(k).XData = t;
obj.DataLines(k).YData = tbl{:,k}
disp('done with code')
  • If you copy all three of those those lines of code together, and paste them together into the command window, and press "enter" once to run all three lines of code, you will trigger just one call to update. That update will occur when MATLAB has gone idle. You will see "done with code" before you see "running update". This is basically equivalent to using set. It does not matter whether the statements are all on a single line or if they are on separate lines.
  • If you have both those lines of code in a script or function or class method, and you run that script/function/method, it will queue a call to update that will run when MATLAB goes idle (or you call drawnow). So, once again, you will see "done with code", and then some time later you will see "running update". Note: depending on the circumstances, it may be a while before you see "running update", because MATLAB will wait until all the code has finished executing before running an update (unless you force an update by calling drawnow).
  • If you run those lines of code one at a time, you will see "running update" after each property set. Because MATLAB went idle after each property set, and setting the property dirtied the chart, update will run each time. This can happen if you copy/paste each line, and press "enter" once per line. You won't see "running update" after "done with code" because your chart wasn't marked dirty by the disp statement, so update wasn't run again.
  • And, of course, if you called either pause or drawnow after each line, you would see "running update" after each property set.
Another thing that often trips people up: Let's say you are debugging your code and you put a breakpoint at the beginning of your script, and you are running your code one line at a time in the debugger. This is equvialent to running the code one line at a time in the command window, and you will get one update following every property set. This means that debugging your code can change the order in which the code runs. This is perhaps the most confusing part about debugging anything related to graphics objects.
Regarding the Code Analyzer warning suppressed by %#ok<MCSUP>
I've had a few discussions about that warning with the team that works on the MATLAB object system, because it turns out that many of the code patterns that my team uses in graphics objects cause us to frequently encounter that particular warning.
The short answer is: When you see that warning, you should carefully consider what happens if you save your object, then load your object, and are you writing code that is dependent upon the order in which properties are being loaded. If you decide it will work, then it is OK to suppress the warning.
If you look at some of the text of the warning, you will get a better understanding of the intention:
"When MATLAB loads an object, the order in which the properties are set is not guaranteed. If a property is not set when your code expects it, a reference to that property can fail. For techniques to overcome this type of dependency, see Avoiding Property Initialization Order Dependency."
In the specific example from my earlier code (comments removed):
function set.Data(obj, T)
obj.Data = T;
if width(T) ~= numel(obj.DataLines) %#ok<MCSUP>
obj.NumVarsChanged = true; %#ok<MCSUP>
end
end
During the load process, you should not depend upon the order in which properties (in this case Data, DataLines, and NumVarsChanged) are loaded.
If you were to save an instance of BrokenChartClass, you don't want to rely on the order in which properties are loaded. For example:
  • If Data was restored before DataLines: set.Data will run when DataLines is empty (the default value). If that happens, then width(T) ~= numel(obj.DataLines) will return false (unless the table is empty), which means that NumVarsChanged will be set to true. This means that the code that creates axes and lines will run during the first update of the chart, which is what we want to happen, so we are OK. If the table was empty, NumVarsChanged will be set to false. This means that when update runs for the first time, the code that should be creating the axes and line objects won't run. But, that is OK, because the table is empty and that means we wouldn't create any axes anyway. I think we are OK in this case. However, this all assumes that DataLines is a transient property. If DataLines were not transient, and the table happened to be empty, then DataLines should also be empty, and in that case I think we are still OK.
  • If DataLines were not transient, and it was loaded before Data, then I think we are OK, because the code within set.Data is using the loaded value of DataLines to decide whether the chart is out-of-date. However, because DataLines is transient, we know the value will be empty when set.Data runs during the load process, and that should work the way we want it to.
  • If NumVarsChanged was not transient, and the value of the Data property was restored before NumVarsChanged, it is theoretically possible that set.Data will set a value for NumVarsChanged, that will be immediately replaced by the loaded value of NumVarsChanged. This could be a problem, but I mitigated this issue by making sure that NumVarsChanged was transient.
Based on the assessment above, I decided it was save to suppress those warnings. In a lot of those cases, if the properties you are accessing within set.Data are Transient, that mitigates any issues, but it is still worth thinking through all the possible scenarios.
Let's say that after working with your chart for a while, you realized that occasionally the timetable was empty, and this meant that no axes were created and your chart was invisible. You decide to add code to your update method to create a single empty axes when the timetable is empty. This will work great when your chart is first created, but if you save and load your chart:
  • NumVarsChanged and DataLines are both transient, so they won't be restored.
  • When set.Data runs, DataLines will be empty (which matches the width of the table), so NumVarsChanged will be false, and depending on how you wrote your code, update may not create the single axes that you expected to be created.
That warning is attempting to alert you to that kind of issue that can occur when your property setter accesses the values of other properties.
Regading how I came across this thread:
A teammate of mind told me about this thread. I'm not 100% sure how they found it, but it was related to a request we got from a customer via technical support.
Jan Kappen
Jan Kappen on 3 Feb 2025 at 10:04
Big thanks again. For now all my questions are answered very satisfactorily :)
Looking very much forward to any improvements of the auto-completions, even further improvements to the documentation (which is already top-notch imo), and of course to new features like the notification which property or object has changed and triggered update.
And I'm looking for more of your awesome answers :D Thanks.

Sign in to comment.

More Answers (0)

Categories

Find more on Developing Chart Classes in Help Center and File Exchange

Products

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!