ChartContainer is broken?
現在この質問をフォロー中です
- フォローしているコンテンツ フィードに更新が表示されます。
- コミュニケーション基本設定に応じて電子メールを受け取ることができます。
エラーが発生しました
ページに変更が加えられたため、アクションを完了できません。ページを再度読み込み、更新された状態を確認してください。
古いコメントを表示
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 件のコメント
Jan Kappen
2021 年 2 月 16 日
Addendum, suppose you want to embed the chart class into a panel or uifigure, you can specify the Parent property. But since it's not assigned in setup(), a default figure is opened when I call getLayout(). I'd need to remember the old parent and overwrite it in a second call to setup().
This renders the automatic call of setup completely useless. Did I get something wrong?!
Jörn Froböse
2022 年 10 月 16 日
Same thing for me. Did you find any better solution than this workaround with mySetup?
Jan Kappen
2025 年 1 月 31 日
@Jörn Froböse check out the answers from Benjamin below, they helped me a lot to understand the concept.
採用された回答
Benjamin Kraus
2025 年 1 月 29 日
編集済み: Benjamin Kraus
2025 年 1 月 29 日
@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.
8 件のコメント
Jan Kappen
2025 年 1 月 30 日
編集済み: Jan Kappen
2025 年 1 月 30 日
first of all: thanks a lot for this elaborated answer. I'll go through your items.
I understand the rationale to ensure that the chart can adapt to structural changes in the properties, and I also understand that you encourage (well, actually enforce ;)) users to avoid code duplication and make sure that ChartContainer can react to data changes.
- 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.
In contrary - I think setup is much more special, because I'm not aware of any other pattern in MATLAB that calls a user-implemented function automatically in the constructor before assigning the properties. It might enforce the chart pattern, but it feels very counter-intuitive seen from the normal class-based/OOP approach.
- 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).
For me it's way easier to accept that there's a property listener (or something similar), that calls the update method automatically whenever something has changed as long as that's documented - but might be my personal taste.
When I first read about the ChartContainer I was hyped, because there's a pattern that helps the user, but after the change it felt not that helpful anymore. Most of the time the setup method is (almost) empty, and the heavy lifting needs to be done in the update function. I'd prefer separation of concerns (a very important pattern), which uses setup to - well - setup the initial state, create the graphic objects, create the graphic handle properties, assigns the parent, etc. If there's already data, use that data as an initial "guess" how the plots look like. update only updates/extends/shrinks existing graphic objects, it should not be responsible to create all objects from scratch (except the case of a structural change in the data). update became the all-in-one super method. To avoid code duplication there's the nice concepts of functions or methods ;)
If that's the recommended pattern I'll try to get used to it, but I'd appreciate to get some more information about the update method calls.
Is there a clear overview which events will trigger the update function? E.g. is it called when setting properties from inside the class? Is it triggered when private properties are set? What about set methods?
Is there a way to understand which property has caused the update, i.e., which data has changed? I really don't like the idea to redraw everything all the time in the update function. In your particular example the structure of the data changed, indeed there's a larger redraw required and that's fine.
But what about just setting a label, or axes limits? I would assume that the recommended way is to set a property, e.g. XLabel which then triggers the update method. How do I avoid to redraw all the data when only the XLabel has changed?
What about adding parameters like update(obj, src, changeEvent) to figure out what exactly needs to be changed? Similar to any other callbacks in the MATLAB world?
People don't have to use it if they don't want - but they could to make their code more efficient.
Or am I thinking to complicated here? Are those kinds of optimizations already done by the graphics system? E.g., does plot()or the underlying functions check if the Data properties are the same and at set time and simply trigger a no-op? Or does the system redraw everything all the time?
I'm handling bigger amounts of data in an user interface and I need to make it as snappy as possible.
Thanks again!
Jan
edit: thanks for the example in Optimized Chart Class for Displaying Variable Number of Lines, I have not seen that one yet - I think it's a good starting point.
PS: I'd love to get rid of any custom constructor - but the most important item is missing: Auto-completion of properties. I've not shown that above, but really the killer feature is that:
classdef ScatterVsTargetChart < matlab.graphics.chartcontainer.ChartContainer
properties
ScatterOrientation (1,1) string {mustBeMember(ScatterOrientation, ["rowmajor", "colmajor"])} = "rowmajor"
ChosenResponseVariable (1,1) string = ""
XData table
YData table
end
events
HighlightedData
end
methods
function obj = ScatterVsTargetChart(scatterData, targetData, opts)
arguments
scatterData table
targetData table
opts.?ScatterVsTargetChart
end
opts = namedargs2cell(opts);
args = {"XData", scatterData, "YData", targetData};
allArgs = [args opts];
obj@matlab.graphics.chartcontainer.ChartContainer(allArgs{:});
end
...
Now I can type ScatterVsTargetChart(xdat, ydat, <tab> and all other properties are shown. If MathWorks would put that functionality into the default ctor, that would be awesome.
Benjamin Kraus
2025 年 1 月 30 日
編集済み: Benjamin Kraus
2025 年 1 月 30 日
I'll start by addressing your PS: I 100% agree with you regarding auto-completion of the properties. We are aware of this limitation and we haven't been able to get that working due to some technical limitations. I have been negotiating with the team that is responsible for auto-completion to try to get this working. Thank you for mentioning this limitation, I'll capture this discussion and add it to my notes regarding that feature.
As for ChartContainer in general:
Prior to ChartContainer, a user could write their own MATLAB classdef and try to mimic a built-in graphics object, and they could get pretty close, but there are three things that ChartContainer offers that you couldn't have done using a regular MATLAB object:
- ChartContainer can be part of the actual graphics tree. In other words: ChartContainer can be in the Children list of a Figure or Panel. This brings with it an easy way to make your chart be included when you save a FIG-file (without resorting to hacks like UserData or setappdata). This also provides some abstraction (users of a chart don't see the implementation details of the chart).
- ChartContainer hooks into the "Current Axes" mechanism. This allows you to do things like call title('My Data') or call xlim(1:10) without needing to specify your chart as an input argument.
- ChartContainer hooks into the built-in "update" mechanism of the MATLAB graphics system.
I think that last bullet is under-appreciated, but I don't think we have done a great job of explaining why it is special.
At a very high level (the details are much more complicated), every graphics object has an internal bit that tracks whether it is "clean" or "dirty". The graphics object always starts dirty when it is first created, and an object is marked "dirty" when (a) a property is set on the object or (b) something in the environment changes that the object needs to know about.
- For properties: This applies to all the properties defined on the object itself (as well as some properties, like OuterPosition, defined on base classes) regardless of whether they are private or public and regardless of whether they were set by the object or by something else. This also applies regardless of how you set the property (using obj.Property, using the set function, or if you have a set.<Property> method). We have considered a feature that lets the author of a chart differentiate which properties should mark the object dirty and we may release a feature like this in the future, but for now this applies to all properties defined by the ChartContainer subclass(es).
- Examples of things in the environment that mark an object dirty actually depend on the particular object. Axes, for example, are marked dirty any time the figure size changes. For ChartContainer the list is currently kind of small, it includes: the Parent changed, the Parent of one of the chart ancestors changed, or the theme changed.
The update method is not called directly when your object is marked dirty, instead the call to update is (effectively) queued until the next "update traversal". There are several ways to trigger an update traversal.
- Most promiment and direct is to call drawnow.
- Calling 'pause' will also trigger an update traversal.
- In addition, simply letting MATLAB go idle after running some code will trigger an update traversal (if there is work to be done).
- There are other ways to trigger an update traversal, including querying a property (such as the XLim property on Axes), that we can only answer after we have collected information about all the objects in the axes.
Once you have triggered an update traversal, MATLAB will start at groot and walk down the graphics tree asking each object "are you dirty". If they are dirty, the update method is called on that object. After the update method finishes, the object is marked "clean". This is important to note, because if your chart sets properties on itself during the update method, the object will still be marked "clean" after update is finished (to prevent your chart infinitely updating).
So, when you asked "is [update] called when setting properties from inside the class?". update isn't called directly when you set a property, but setting a property will mark the object dirty, which means that update will eventually get called (either the next time someone calls drawnow or the next time MATLAB goes idle). We guarantee it will eventually get called.
As I mentioned in my previous comment, this is a bit different than a simple property listener. For example: If you had a chart with XLimits and YLimits properties that had the SetObservable attribute, and you had a 4x4 grid of charts:
tiledlayout(4,4);
c = gobjects(4,4);
for i = 1:4;
for j = 1:4;
nexttile
c(i,j) = myChart;
end
end
for i = 1:4
set(c(:,i), 'XLimits', i+[0 1]);
set(c(i,:), 'YLimits', i+[0 1]+10);
end
A simple PostSet listener would fire twice for every object in that second loop, where as update will only run once for each chart after the loop has finished. You could imagine that if you were also setting the title, and other attributes of each chart, the number of times a PostSet listener would fire would dramatically increase, where as update will still only run once per chart, when the script finishes executing. This reduces the performance impact of update, because update isn't going to run once per property set, but rather once after a collection of property sets have finished. This is what I meant when I said that update was special: there is really no way to replicate the behavior of the update method outside of using ChartContainer.
You suggested update(obj, src, changeEvent): We would like to offer a feature like this that tells the chart/update method what changed, rather than just something changed. In the meantime, there is a way to implement this today using existing MATLAB features. To show you how you could do this, considering your original chart. I've tweaked the code a bit and added a new property called NumVarsChanged and a set.Data method.
classdef BrokenChartClass < matlab.graphics.chartcontainer.ChartContainer
properties
Data timetable
end
properties (Access = protected, Transient)
NumVarsChanged (1,1) logical = true
DataLines
end
methods
function obj = BrokenChartClass(T)
arguments
T timetable
end
obj@matlab.graphics.chartcontainer.ChartContainer();
obj.Data = T;
end
function set.Data(obj, T)
obj.Data = T;
% Set NumVarsChanged after setting Data, in case setting Data
% throws an error.
if width(T) ~= numel(obj.DataLines) %#ok<MCSUP>
obj.NumVarsChanged = true; %#ok<MCSUP>
end
end
end
methods (Access = protected)
function setup(~)
end
function update(obj)
tbl = obj.Data;
if obj.NumVarsChanged
% Update the number of axes/lines.
tcl = obj.getLayout();
numVars = width(tbl);
% This could be more efficient by reusing axes/lines.
delete(tcl.Children);
h = gobjects(1,numVars);
for n = 1:numVars
ax = nexttile(tcl, n);
h(n) = plot(ax, NaT, NaN);
end
obj.DataLines = h;
obj.NumVarsChanged = false;
end
% Update the data in the lines.
t = tbl.Properties.RowTimes;
for k = 1:length(obj.DataLines)
set(obj.DataLines(k), 'XData', t, 'YData', tbl{:,k})
end
end
end
end
In this modified code, I've added my own property (NumVarsChanged) to track whether I need to do the expensive work of creating new axes and lines. This is just an example. In this case the separate property (NumVarsChanged) isn't necessary: rather than needing set.Data and a separate property, the chart can just check whether the width of the table matches the number of lines within the update method. However, this is meant as an example to show how you could optimize your update method to run more efficiently (if necessary). Effectively, you've added your own method to track "which property (or group of properties) changed".
My recommendation to authors of charts is: Don't worry about those optimizations in the first version of the chart. Get the chart working, then do some performance measurements. If you see that sections of your update method are slow you can add code to avoid that expensive work when it isn't necessary. That may mean checking "have the number of variables in my table changed" within update, or it may mean adding a property to the chart to track "has something changed" and the corresponding set.<property> methods. That decision depends on how expensive it is to check what has changed within the update method itself.
Coming back to the setup method: I sometimes wonder if we should not have implemented a setup method. For some types of charts (for example: if the number of axes never changes), the setup method is quite helpful, but for many tpes of charts (such as the example above), the setup method frequently ends up empty. The reason we included the setup method was to allow users to benefit from the built-in constructor and not have to write their own, but as you observed, it is unfortunate that the built-in constructor doesn't have full auto-completion.
I hope this helps clarify how update works (and why setup behaves the way it does), but regardless: Please keep feedback like this coming. It really helps us to see how our customers are using ChartContainer and it will definitely be considered when we are working on future enhancements.
Benjamin Kraus
2025 年 1 月 30 日
編集済み: Benjamin Kraus
2025 年 1 月 30 日
I should also add: While the built-in constructor doesn't have auto-completion for all properties, you can implement a file called functionSignatures.json that adds tab-completion for name/value pairs.
You can read details here: Customize Code Suggestions and Completions
For example, create a folder called resources, and within that folder a file called functionSignatures.json that looks like this:
{
"BrokenChartClass": {
"inputs": [
{"name":"data", "kind":"required", "type":"timetable"},
{"name":"options", "kind":"properties", "type":"BrokenChartClass"}
]
}
}
It isn't ideal that this requires a separate file, but it is another option to consider.
Jan Kappen
2025 年 1 月 31 日
編集済み: Jan Kappen
2025 年 1 月 31 日
this is an insane answer, can't thank you enough for that - it clarifies things, and - more important - it shows that there are similar thoughts going on in MathWorks.
I think for the users it's okay when things are not exactly working as they expect, as long as it's known that there are smart people thinking about the issue or concept which might get improved (or not, but then providing a proper rationale really helps) in the future.
First: I really like to see that auto-completion is on MathWorks list and identified as a limitation. I think the progress in usability made in the last years is remarkable. Property argument validation and things like mustBeMember are awesome and really help to create self-documenting code, which is highly important.
Second: Thanks for that detailed explanation of update and setup. I also think that the main issue might have been the documentation. It would be worth highlighting the "specialness" of update and that it's more than just a property listener or so. It's very important to understand that groups of properties can be set and update just runs once.
Also good call on premature optimization, you're of course right there. Experience just showed that redrawing axes elements, or even creating the whole plot by calling the main plotting function again is way slower than just updating the graphic element's properties. And then, by introducing the setup function, it felt like I should do all the heavy stuff there. Just by the name. Perhaps setup should've been called differently or made optional - but I understand it's there now. It would be good to highlight in the docs, that setup is like a onStartup calback, when there's no custom ctor. Perhaps that would help people to understand its purpose - it would have helped me at least :)
Regarding update(obj, src, changeEvent), I absolutely meant what you described. Just knowing that something has changed is not valuable - we would not be in the update method otherwise. So either an (optional) parameter in the function as shown, pointing to the one or multiple objects that were dirty and thus needed to be redrawn, or a property or method in the ChartContainer base class à la whatChangedSinceLastUpdate(obj) or something like that would be nice for optimization.
I understand the proposed code pattern and I likely can live with that - if it turns out that redrawing often is slow. Just a few questions:
- Is set(obj.DataLines(k), 'XData', t, 'YData', tbl{:,k}) faster than calling obj.DataLines(k).XData = t; obj.DataLines(k).YData = tbl{:,k} or does it prevent calling the drawnow function in between? I remember I made some trials with that but could not draw a clear conclusion. Or is it just personal taste?
- Setting other variables in the setters... This is something that comes across my road every now and then - is it save to supress the warnings using %#ok<MCSUP> in patterns you're showing?
I'm also aware of functionSignatures.json, but the whole beauty of the newer auto-completion mechanisms is that it's very close to the code and automatic. No need to take track of an external, potentially (or rather likely) outdated json file ;) But thanks for bringing that up, for now I'll stick to the ctor approach.
Last question: I'm curios, how did you find this thread? Your answers are awesome, and you're asking for feedback like this to come in - but the initial thread is from 2021. What "caused" you to check-out this thread now and what would be your recommendation to provide that kind of feedback?
Again, those answers were awesome, can't thank you enough.
Jan
Benjamin Kraus
2025 年 1 月 31 日
@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
2025 年 2 月 3 日
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.
Jan Kappen
2025 年 3 月 1 日
I continued playing around a bit and investigated other methods to mark the data dirty to avoid unnecessary re-plotting data attempts.
Of course your set method approach above works, but it's a bit of boilerplate, especially when there are more than just XData and YData or Data, but multiple public data properties (which happened to be in a confidence interval class I've recently written).
So, what about AbortSet? My current understanding is that it's never a bad idea for public properties that are expense to update to be used? Or are there side-conditions (other than the compare overhead for sure)?
Also, what about SetObservable? Does it interfer with the automatic update process you described above? I'm thinking of something like
% public properties with customized update behavior, because updates
% might be expensive
properties (SetObservable, AbortSet)
XData (:,1) {mustBeA(XData, ["duration", "datetime", "numeric"])}
YData (:,:) double
YReferenceData (:,:)
ConfidenceMode (1,1) string {mustBeMember(ConfidenceMode, ["property", "minmax", "std"])} = "minmax"
ConfidenceMargin (1,2)
CenterLineMode (1,1) string {mustBeMember(CenterLineMode, ["mean", "median"])} = "mean"
end
.
.
.
% add listeners to all props that are expensive to update
% just search for observable properties of this subclass, not
% the superclass(es)
mc = metaclass(obj);
metaprops = [mc.PropertyList(...
[mc.PropertyList.SetObservable] ...
& [mc.PropertyList.DefiningClass] == mc)];
addlistener(obj, metaprops, "PreSet", @obj.markDataDirty);
.
.
.
function markDataDirty(obj, src, evnt)
% potentially more checks
obj.NeedsDataUpdate = true;
end
This would potentially reduce the number of set methods a bit. Are there any downsides?
Thanks!
Benjamin Kraus
2025 年 3 月 3 日
@Jan Kappen: You are right, the set method approach definitely leads to a lot of boilerplate code (I feel that pain regularly). We have discussed internally a few features we could implement to reduce the amount of boilerplate code, but we haven't settled on a good solution yet.
AbortSet should work to avoid unneccesary updates. If the property has the AbortSet attribute and the new property value equals the old property value, the object won't be marked dirty and update won't run (unless something else has marked the object dirty). Note that "equals" is isequal in this case, which can mean some strange behaviors in some cases. For example, if the value of your property is double(1:10) and you set the property to int8(1:10), that is considered "equal" as far as AbortSet is concerned. It may also be useful to remember how isequal works with handle objects. This is the note from the isequal doc page on the topic: "When comparing two handle objects, use == to test whether objects have the same handle. Use isequal to determine if two objects with different handles have equal property values."
One other thing to note for AbortSet: The penalty isn't just the time it takes to compare the values, but it includes the time it takes to query the value, so if you have a get method for the property, keep in mind that the get method will run every time you set the property value, and sometimes that can be more expensive than the comparison.
As for SetObservable: I think your approach will work and I've used a pattern like that before, specifically when the property is defined in a base class (so I can't overload set). SetObservable won't impact the automatic update process and in your implementation NeedsDataUpdate will be set to true whenever the data does change. The SetObservable callback will execute before update runs. Your approach looks like a reasonable approach to avoid needing separate set methods for every property. I also think you can use PreSet or PostSet and both would work. I usually use PostSet, and I think that is what I would recommend unless you have a good reason for needing PreSet. The main reason is that if there are any unexpected side-effects of your PostSet callback, they will see the new property value instead of the old property value.
I think AbortSet is optional (and orthogonal) in your approach above, so if you didn't like the isequal behavior, your could drop AbortSet entirely. It would mean that NeedsDataUpdate would be set to true even if the data has not changed, but it would still allow you know that someone did set a value to that property (whether it was different or not).
As for AbortSet plus SetObservable: They work fine together, but note that AbortSet means the PreSet and PostSet events won't run unless the data changes.
その他の回答 (0 件)
カテゴリ
ヘルプ センター および File Exchange で Entering Commands についてさらに検索
製品
参考
Community Treasure Hunt
Find the treasures in MATLAB Central and discover how the community can help you!
Start Hunting!Web サイトの選択
Web サイトを選択すると、翻訳されたコンテンツにアクセスし、地域のイベントやサービスを確認できます。現在の位置情報に基づき、次のサイトの選択を推奨します:
また、以下のリストから Web サイトを選択することもできます。
最適なサイトパフォーマンスの取得方法
中国のサイト (中国語または英語) を選択することで、最適なサイトパフォーマンスが得られます。その他の国の MathWorks のサイトは、お客様の地域からのアクセスが最適化されていません。
南北アメリカ
- América Latina (Español)
- Canada (English)
- United States (English)
ヨーロッパ
- Belgium (English)
- Denmark (English)
- Deutschland (Deutsch)
- España (Español)
- Finland (English)
- France (Français)
- Ireland (English)
- Italia (Italiano)
- Luxembourg (English)
- Netherlands (English)
- Norway (English)
- Österreich (Deutsch)
- Portugal (English)
- Sweden (English)
- Switzerland
- United Kingdom (English)
