Skip to content

visualizer: display several values of a statistic on one figure, and the IEEE 802.11 per-peer rate visualizer - #1125

Open
adamgeorge309 wants to merge 8 commits into
masterfrom
topic/gy/statistic-visualizer-bar-charts
Open

visualizer: display several values of a statistic on one figure, and the IEEE 802.11 per-peer rate visualizer#1125
adamgeorge309 wants to merge 8 commits into
masterfrom
topic/gy/statistic-visualizer-bar-charts

Conversation

@adamgeorge309

@adamgeorge309 adamgeorge309 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Lets a statistic visualizer display several values of one statistic at once, on a bar chart above the network node, and adds an 802.11 per-peer data rate visualizer configured from it.

Why. A statistic visualizer could show one number per signal source. For a quantity that exists per peer, per source or per flow, that forces a choice between an aggregate that hides the distribution and one visualizer instance per value with no visual relationship between them — while the recording side already handles this with demux(), visible only after the run.

Three names for three things. An earlier version of this branch called all of it a "series". That word was doing three jobs at once, and one parameter fused two independent decisions:

  • split — one signal source's values become several statistics
  • group — several statistics are displayed together on one figure
  • item — one of the several things a figure displays

splitBy determines whether the values a signal source emits are split into several statistics, and what identifies each:

  • "none" — the source emits the values of a single statistic
  • "details" — one statistic per distinct details object emitted with the value; the live counterpart of the demux() result filter
  • "flow" — one statistic per packet flow of the source (statisticExpression contains demuxFlow())

groupBy determines which statistics are displayed together as the items of a single figure:

  • "none" — each statistic gets a figure of its own
  • "source" — the statistics of one signal source, i.e. the ones splitBy split its values into
  • "networkNode" — the matching signal sources of one network node, one item per source, each labelled with its path relative to the node (wlan[0].mac), which is unique where the bare module name is not

Only the combinations the visualizations are keyed for are accepted, and the error names all three rules it enforces. Fully decoupling them — so splitBy = "details" with groupBy = "none" gives one text instrument per peer — is a follow-up that will not change this NED contract.

The figure is given as the attributes of an @figure property, either in a figure template along the module path (propertyName, which already existed) or in a new figure object parameter, which unlike the template can be set from an ini file and can refer to module parameters:

*.visualizer.*.statisticVisualizer.figure = {type: "barChart", maxValue: 1, valueFormat: "%.2f", barColor: "gold"}
*.visualizer.*.statisticVisualizer.figure = {type: "gauge", size: [60, 60], maxValue: 100, tickSize: 20}

Both funnel into cCanvas::createFigure() + cFigure::parse(), so there is one attribute vocabulary, one set of allowed keys, and no second dialect.

BarChartFigure (Register_Figure("barChart")) is the first indicator figure whose item count is not fixed. It follows PlotFigure: the interface carries the getter, the figure carries the setters — setNumItems(n) sizes the chart and setItemLabel(i, label) names each bar, as setNumSeries(3) and setLineColor(0…2) do for a plot. Bars are drawn in index order. The visualizer owns the label to index mapping: it holds the items in a std::map keyed by label, so an item's index is its position in label order, and the figure is only ever given a count, labels and values. A figure that cannot display labelled items is rejected with a message that says so.

IIndicatorFigure is renamed, series to item: getNumItems(), and setValue(int index, …). "Series" only fits PlotFigure, where an item really is a sequence of values over time. getNumSeries() stays for one release as a deprecated method that getNumItems() forwards to, so a figure implemented outside INET keeps working. Note that overriding a deprecated method is not a use of its name, so the compiler does not warn at the override — the deprecation is visible in the header and at call sites, and WHATSNEW says so rather than promising a warning that does not happen.

Ieee80211RateCanvasVisualizer shows the data rate a node is using towards each of its peers. It is a NED configuration of the generic visualizer, not new visualizer code, and being a StatisticCanvasVisualizer it needs no submodule of its own:

*.visualizer.*.statisticVisualizer.typename = "Ieee80211RateCanvasVisualizer"
*.visualizer.*.statisticVisualizer.displayRates = true

Also here: a bounds-check fix in FigureRecorder (a figure index equal to the item count was accepted and then wrote one past the end; a negative index passed too), and lifetime handling the per-frame refresh makes necessary — a result recorder is deleted with its source module, and a network node visualization is a figure group deleted with its node that takes the statistic figure with it, so an item whose source is gone and a visualization whose module is gone are both dropped before anything reads them, and the node visualization is looked up rather than remembered. That last part also repairs a pre-existing crash on this path.

Test

Builds at every one of the eight commits (release).

Four module testsinet_run_module_tests -m release -f StatisticVisualizerItems, all PASS:

test asserts
_1 three sink applications become three bar chart items; the bar count and labels in index order, read from the figure through a BarChartProbe
_2 a gauge fed several items is rejected, with the message
_3 deleting a signal source removes its bar; the survivors keep their own labels
_4 deleting a whole network node leaves nothing and does not crash

The visualizer is GUI-only (initialize() returns early on !hasGUI()), so all four run under Cmdenv's fake GUI, which makes hasGUI() true and drives refreshDisplay(). That matters for what the assertions are worth: the visualizer records nothing to a result file, so asserting on received packet counts would pass with the visualizer switched off — checked, it does. Test _1 therefore reads the figure, and _2's rejection is what proves the path executed at all. Test _4 was written for the lifetime handling and found a segfault at teardown in the first version of it.

Fingerprints. examples/visualizer/statisticbars (new) has rows in tests/fingerprint/store.json for Packets, Flow and GaugeFlow being the only splitBy = "flow" coverage and Gauge the only non-bar-chart figure. All three pass through inet_run_fingerprint_tests. The tyf ingredient runs under fake GUI and hashes canvas figure geometry, so these are genuine visualizer coverage: Gauge shares its tplx/~tNl/~tND values with Packets (identical traffic) but differs on tyf.

Runs. All seven example configs run clean under fake GUI with the visualizer live.

Deliberately not in this PR

The OSG visualizer accepts splitBy/groupBy and renders nothing, and still has the pre-existing form of the lifetime defect fixed here on the canvas side. @statistic(record=figure) cannot target a barChart, because nothing pre-sizes its bars from a property. No test covers splitBy = "details" or Ieee80211RateCanvasVisualizer itself. BarChartFigure relayouts once per setValue, and its autoscale moves only the maximum. The display-unit conversion differs between the unsplit path (walks the unit list) and the split path (uses the first unit only, and none when statisticUnit is unset).

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +344 to +349
double range = maxForScale - minValue;
double fraction = range > 0 ? (value - minValue) / range : 0;
if (fraction < 0) fraction = 0;
if (fraction > 1) fraction = 1;
double pos = fraction * (barColors.size() - 1);
int index = (int)std::floor(pos);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Charts with a not-yet-measured bar can read past the end of the color list and crash

A bar whose value is still unknown is fed into the color picker (getBarColor() at src/inet/visualizer/base/StatisticVisualizerBase.cc:344-349) without any check for the "no value yet" case, so the color index becomes garbage and the program can read outside the color list and crash.
Impact: Simulations using the bar chart display can crash or draw corrupt charts as soon as a bar exists before its first measurement.

NaN propagation into the gradient index and into figure geometry

In sources mode every newly registered bar is initialized with values[label] = NaN (src/inet/visualizer/base/StatisticVisualizerBase.cc:378), and refreshGroupedBarValues() keeps NaN until the recorder produces a value; refreshFlowBarValues() and processBarValue() can also store NaN.

In getBarColor(), with value = NaN: fraction = (NaN - minValue)/range = NaN; both clamps if (fraction < 0) and if (fraction > 1) are false for NaN, so pos = NaN, and int index = (int)std::floor(NaN) is undefined behaviour (typically INT_MIN). index >= (int)barColors.size() - 1 is then false, so barColors[index] / barColors[index + 1] index the vector far out of range.

The same NaN also reaches StatisticCanvasVisualizer::refreshChart() (src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc:242-248), producing h = NaN and a cRectangleFigure with NaN bounds.

Both places need an explicit NaN check (e.g. skip drawing the bar, or draw a zero-height bar in a neutral color), like the text mode does in DirectiveResolver::resolveDirective which renders "-" for NaN.

Suggested change
double range = maxForScale - minValue;
double fraction = range > 0 ? (value - minValue) / range : 0;
if (fraction < 0) fraction = 0;
if (fraction > 1) fraction = 1;
double pos = fraction * (barColors.size() - 1);
int index = (int)std::floor(pos);
double fraction = range > 0 ? (value - minValue) / range : 0;
if (std::isnan(fraction)) fraction = 0;
if (fraction < 0) fraction = 0;
if (fraction > 1) fraction = 1;
double pos = fraction * (barColors.size() - 1);
int index = (int)std::floor(pos);
if (index >= (int)barColors.size() - 1)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled by the rework this review predates: the gradient moved into BarChartFigure, whose getBarColor() returns the first color for NaN, and whose layout() maps a NaN value to zero height and hides the bar. No NaN reaches the color index or the figure bounds.

Comment on lines +366 to +374
// attach a result recorder (statisticExpression, e.g. count or throughput) whose value the bar will show
addResultRecorder(source, signal);
auto recorder = getResultRecorder(source, signal);
auto networkNode = getContainingNode(module);
auto barSetVisualization = getBarSetVisualization(networkNode->getId());
if (barSetVisualization == nullptr) {
barSetVisualization = createBarSetVisualization(networkNode);
if (barSetVisualization == nullptr)
return; // bar charts not supported by this concrete visualizer (e.g. osg)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Statistic collectors pile up without limit when a display cannot draw bar charts

A new hidden statistic collector is attached to the same module on every single signal (addResultRecorder() at src/inet/visualizer/base/StatisticVisualizerBase.cc:366) before it is known whether the chart can actually be created, so when charts are unsupported the collectors grow without bound.
Impact: With a display that does not support bar charts, memory use and per-signal processing cost grow continuously until the run slows to a crawl or runs out of memory.

Early return skips the registration bookkeeping

processGroupedBarSource() first checks groupedBarSourceIds and the source filter, then calls addResultRecorder(source, signal). If createBarSetVisualization() returns nullptr (the base-class default, i.e. any visualizer that does not implement bar charts, e.g. StatisticOsgVisualizer), it returns at src/inet/visualizer/base/StatisticVisualizerBase.cc:373 before groupedBarSourceIds.insert(module->getId()) at line 379. Consequently the guard at line 361 never fires, and each subsequent emission of the signal appends yet another recorder chain to the source module.

processFlowBarSource() has exactly the same ordering problem: addResultRecorder() at line 404, early return at line 409, groupedBarSourceIds.insert() at line 412.

A fix is to determine whether a bar set can be created (or mark the source as handled / disable bar mode) before attaching the recorder.

Prompt for agents
In StatisticVisualizerBase::processGroupedBarSource() and processFlowBarSource() (src/inet/visualizer/base/StatisticVisualizerBase.cc), addResultRecorder() is called before the code checks whether createBarSetVisualization() succeeds. When the concrete visualizer does not support bar charts (base implementation returns nullptr), the function returns early without inserting the module id into groupedBarSourceIds, so the duplicate-registration guard never trips and a new recorder chain is attached to the source module on every signal emission, growing memory and per-signal cost without bound. Reorder so the bar set is obtained/created (or the unsupported case detected and permanently remembered, e.g. by disabling bar handling or still inserting into groupedBarSourceIds) before any recorder is attached.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled by the rework this review predates: registerSeriesSource() now inserts into seriesSourceIds before it tries to create the visualization, and calls addResultRecorder() only after the nullptr check, so a visualizer that cannot display series registers each source once and attaches nothing.

Comment on lines +376 to +378
}
std::string label = module->getFullName();
barSetVisualization->recorders[label] = recorder;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Bars for same-named modules in one node overwrite each other

Each bar is labelled only with the short module name (module->getFullName() at src/inet/visualizer/base/StatisticVisualizerBase.cc:376), so two identically named modules inside the same node share one bar and only one of them is shown.
Impact: In nodes with several same-named submodules (for example multiple wireless interfaces), some sources silently disappear from the chart.

Label collision in the per-node bar map

In sources mode the bar set is keyed by the network node id, and each bar is keyed by module->getFullName(). For a source filter matching e.g. **.wlan[*].mac, both wlan[0].mac and wlan[1].mac have full name mac, so barSetVisualization->recorders[label] and values[label] (lines 377-378) overwrite the earlier entry; the first module's recorder is dropped from the map and its value never displayed, while the count of bars is lower than the number of matching sources.

A label that is unique within the node (e.g. the path relative to the network node) would avoid the collision.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real, fixed in 1afadc8. The label is now the path of the source relative to the network node, so pointing sourceFilter at *.switch.eth[*].mac in examples/visualizer/statisticbars gives eth[0].mac and eth[1].mac instead of a single mac bar. Direct submodules such as app[0] keep their label, so the existing configs render the same.

@levy

levy commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The statistic visualizer should be capable of visualizing the value using any instrument figure. Each kind of instrument figure comes with its own set of parameters. We can't let all those parameters go into the statistic visualizer module.

For example, we can have bar chart/line chart/histogram chart/gauge/text instruments, each have their own set parameters.

Options:

  • add a @figure template to the derived statistic visualizer module which can have its figure type fixed and have its own parameters. Cons: doesn't really combine well with using a different figure.
  • add an object parameter to the statistic visualizer to control the appearance of figures

@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch from 6f77762 to 1e5fc68 Compare August 6, 2026 15:35
@adamgeorge309 adamgeorge309 changed the title visualizer: add statistic bar charts and the IEEE 802.11 per-station rate visualizer visualizer: display several series of a statistic with any indicator figure, and the IEEE 802.11 per-station rate visualizer Aug 6, 2026
@adamgeorge309

Copy link
Copy Markdown
Contributor Author

Reworked along these lines — thanks, the objection was right, and it turned out the mechanism was already in the codebase and this PR was simply bypassing it.

StatisticCanvasVisualizer already had propertyName/propertyIndex: it looks up a figure template property along the module path, instantiates the class named by its type= key, and calls figure->parse(property), so every figure-specific parameter lives in the property. @statistic(record=figure; targetFigure=...) + @figure does the same thing for the canvas of a module. The 17 bar-specific parameters on the statistic visualizer were exactly what that mechanism exists to prevent, and displayMode was redundant with it — the figure type is the display mode.

So the two options are not alternatives: both are front ends for the same thing, and both now funnel into cCanvas::createFigure() + cFigure::parse(), i.e. one attribute vocabulary, one construction path, no second dialect to document.

  • The @figure template (option 1). The con named above is weaker than it sounds: the type is in the template's type=, propertyName/propertyIndex are ordinary string parameters, and the lookup walks the module path — so a derived visualizer only sets defaults and the user can put their own template on the network and select it from the ini. The real limit is different: NED property values are static strings, so a single attribute cannot be overridden from an ini file and cannot refer to a module parameter.
  • An object parameter (option 2) has neither limit, which matters immediately here: Ieee80211RateVisualizer forwards its maxRate parameter into the figure as {... maxValue: this.maxRate / 1Mbps ...}, which a static template cannot do.

Since the template is the only one of the two that can replace a figure a derived visualizer already defaults to, it takes precedence over the parameter.

What was actually missing was on the figure side, not the parameter side: IIndicatorFigure had a fixed series count and integer-indexed series, so a chart fed by a live demux had nowhere to go — which is why the bar chart ended up inside the visualizer. It now has getSeriesIndex(label, createIfMissing) (default implementation maps the empty label to series 0, so existing figures are unaffected), and BarChartFigure is a normal registered figure owning its own parameters. The visualizer keeps only the data-side question — seriesBy = "none"|"details"|"sources"|"flow" — which is a demultiplexing policy, not appearance.

The new examples/visualizer/statisticbars shows the same visualizer and the same series driving a bar chart or a gauge, by one ini line.

@adamgeorge309
adamgeorge309 force-pushed the topic/gy/ieee80211-per-station-rate-control branch 2 times, most recently from 05bb4f7 to 1be82e6 Compare August 10, 2026 09:44
@adamgeorge309

Copy link
Copy Markdown
Contributor Author

Pushed 1afadc8 for the third Devin finding, which was real: in seriesBy = "sources" a series was labelled with the name of its source module alone, so same-named sources in one node - the MACs of several network interfaces, say - collapsed into a single series and all but the last one silently vanished from the chart. The label is now the path of the source relative to the network node; direct submodules such as app[0] are unaffected, so the existing example configs render unchanged.

The other two findings were made against the pre-rework commit and no longer apply: the NaN handling now sits in BarChartFigure (getBarColor() and layout() both special-case it), and registerSeriesSource() registers the source before attaching any recorder, so nothing accumulates when series are unsupported.

@adamgeorge309
adamgeorge309 force-pushed the topic/gy/ieee80211-per-station-rate-control branch from 0248366 to a91a713 Compare August 12, 2026 14:14
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch 2 times, most recently from 07641e9 to e47d2b0 Compare August 12, 2026 15:59
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/ieee80211-per-station-rate-control branch 2 times, most recently from 87e64f2 to 353ab79 Compare August 14, 2026 14:38
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch from e47d2b0 to d39a093 Compare August 14, 2026 14:38
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/ieee80211-per-station-rate-control branch from 353ab79 to fa2bc31 Compare August 14, 2026 14:57
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch from d39a093 to 05d85ae Compare August 14, 2026 14:57
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/ieee80211-per-station-rate-control branch from fa2bc31 to 42d0174 Compare August 14, 2026 15:45
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch from 05d85ae to e865b4f Compare August 14, 2026 15:45
@levy

levy commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

The word series makes me so confused. Is it not grouping statistics instead? Groups have members, some figures are capable of displaying a group of statistics. Grouped by flow name for example. A series is just a sequence of data points, so I don't quite understand why is this word used.

@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch 2 times, most recently from 1b90cac to 0aa0d1e Compare August 28, 2026 14:38
@adamgeorge309 adamgeorge309 changed the title visualizer: display several series of a statistic with any indicator figure, and the IEEE 802.11 per-station rate visualizer visualizer: display several values of a statistic with any indicator figure, and the IEEE 802.11 per-station rate visualizer Aug 28, 2026
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch 2 times, most recently from 83adb44 to 2ab6568 Compare August 31, 2026 15:04
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/ieee80211-per-station-rate-control branch from 42d0174 to f55498c Compare August 31, 2026 15:40
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch from 2ab6568 to 01064c9 Compare August 31, 2026 15:53
levy added a commit that referenced this pull request Sep 1, 2026
The three reports were written against pull-request.md and never
committed. They are the worked examples of a PR-* audit.
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/ieee80211-per-station-rate-control branch from f55498c to a93ad8c Compare September 1, 2026 10:10
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch from 01064c9 to 1542b8b Compare September 1, 2026 10:13
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/ieee80211-per-station-rate-control branch from a93ad8c to 14725f1 Compare September 1, 2026 12:18
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch from 1542b8b to 0f90a30 Compare September 1, 2026 12:21
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/ieee80211-per-station-rate-control branch from 14725f1 to e5d28a4 Compare September 1, 2026 13:06
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch from 0f90a30 to 496fe51 Compare September 1, 2026 13:11
FigureRecorder accepted a figure index equal to the number of series the
figure has, and then called setValue() one past the end. The check was
'>' where the index is zero based, so only an index beyond that was
refused.

Reject it at initialization instead. A model that used the boundary index
by mistake now stops with an error naming the figure and the bound,
rather than writing into a series that does not exist.
~IIndicatorFigure called the values it displays series. That word only
fits ~PlotFigure, where a series really is a sequence of values over
time; for a gauge, a counter or a thermometer the indexed thing is a
single value that happens to have an identity of its own.

Rename it to item: getNumItems() instead of getNumSeries(), and the index
parameter of setValue() to index. Only ~PlotFigure and ~FigureRecorder
follow, because the name of a parameter is not part of a signature, so
the figures that display a single value are untouched.

getNumSeries() stays for one release as a deprecated method, and the
default getNumItems() calls it, so an indicator figure implemented
outside INET keeps working until its author renames the override. Note
that overriding a deprecated method is not a use of its name, so the
compiler does not warn at the override itself; the deprecation is visible
in the header and at any remaining call site. ~PlotFigure keeps its own
getNumSeries(), also deprecated, so that a caller of it does not silently
get the interface default of 1 instead of the series count.

No behavior change beyond the deprecation.
An indicator figure displayed either one value, or a fixed number of them
identified by index. A quantity that exists per peer, per source or per
flow fits neither: the set of them only becomes known while the
simulation is running, and they carry names rather than indices.

Add BarChartFigure, registered as the "barChart" figure type. Its items
are bars, and the number of them is not fixed: setNumItems() sets it and
setItemLabel() names each, the same way ~PlotFigure::setNumSeries() sets
the number of its series and setLineColor() configures each. Bars are
displayed in index order, so whoever sets them decides the order.

A bar height represents the value over the minValue..maxValue range, or
over the autoscaled range of the current values when maxValue is not
given. barColor accepts a list of colors interpolated over the same
range, so the color of a bar carries the value even where the chart is
too small to read.

Like the other instrument figures, everything about its appearance is a
figure attribute parsed from a property, so none of it has to appear as a
parameter of whatever displays the figure.
~StatisticCanvasVisualizer displayed a statistic with a text label, or
with an indicator figure taken from a figure template property along its
module path (the propertyName parameter). A template cannot be given in
an ini file and cannot refer to module parameters, so choosing a gauge
instead of a label, or merely changing its scale, meant editing a NED
file.

Add the figure parameter, which takes the attributes of the figure the
same way an @figure property gives them:

  *.visualizer.statisticVisualizer.figure = {type: "gauge", size: [60, 60], maxValue: 100}

The parameter, unlike the template, can be set from an ini file and can
refer to module parameters; the template, unlike the parameter, can
replace a figure a derived visualizer already defaults to, and therefore
takes precedence.

Along the way, the figure type is now also looked up among the types
registered with Register_Figure(), not only as an inet::<Type>Figure
class; an indicator figure is given the value in the display unit (what
the text label would display) rather than the raw value; and the size
reserved for the figure among the annotations of the network node is
updated when it changes, as it does in a counter gaining digits.
~StatisticVisualizerBase displayed the last value of a statistic per
signal source. A quantity that exists per peer or per flow forces a
choice between an aggregate that hides the distribution and one
visualizer instance per value with no visual relationship between them,
while the recording side already handles it with demux().

Add the splitBy parameter, which determines whether the values a signal
source emits are split into several statistics, and what identifies each
of them:

- "details": one statistic per distinct details object emitted with the
  value, the live counterpart of the demux() result filter
- "flow": one statistic per packet flow of the source, demultiplexing its
  signal by the flow tag (statisticExpression contains demuxFlow()), so
  an item can display a count or a throughput rather than the raw value

StatisticVisualization now holds the items it displays instead of a
single value. An unsplit statistic has one item whose label is empty, so
there is one visualization class and one code path rather than one of
each per case. The visualizer owns the label to index mapping -- items
are displayed in label order, so an item's index is its position among
them -- and the figure is only ever given a number of items, their
labels, and their values.

How the items are displayed is not for the visualizer to decide: that is
what the figure is for. A visualizer displaying several values without a
figure configured defaults to a bar chart.

The per frame refresh this adds is the first thing that touches a
visualization outside a signal receipt from a live source, so it is also
the first that can meet a deleted one. A visualization whose module is
gone is dropped before its figure is read, and the network node
visualization is looked up rather than remembered, because a node
visualization is a figure group that is deleted with its node and takes
the statistic figure with it.
Splitting puts the values of one signal source on that source's figure.
Which statistics share a figure is a separate decision, and the other
useful answer is per network node: a quantity that exists once per
source, displayed for every matching source of a node on a single figure,
e.g. the throughput of each of a host's applications.

Add the groupBy parameter, which determines which statistics are
displayed together as the items of a single figure:

- "none": each statistic is displayed on a figure of its own
- "source": the statistics of one signal source, i.e. the ones splitBy
  split its values into
- "networkNode": the matching signal sources of one network node, one
  item per source. An item is labelled with the path of its source
  relative to the node (e.g. wlan[0].mac), which is unique by
  construction where the bare module name is not -- the MACs of several
  network interfaces all share one name, and would merge into a single
  item. For a source that is a direct submodule of the node the path is
  just its name (e.g. app[0]).

The values then come from the result recorders built from
statisticExpression, so an item can display a count or a throughput
rather than the raw value of the signal. The error that rejects an
unsupported combination names all three rules the guard enforces.

This is the first mode whose items hold the result recorder of a module
other than the one the visualization is keyed on, and the first whose
items can go away. A recorder is deleted with its source module
(cResultListener::unsubscribedFrom), and a network node visualization is
a figure group that is deleted with its node, taking the statistic figure
with it. So an item whose source is gone is dropped before the values are
read, a visualization whose module is gone is dropped before its figure
is touched, and the network node visualization is looked up rather than
remembered, because the remembered pointer is exactly what goes stale.
Because the set of items is no longer only grown, the figure is told to
relabel on a version counter rather than on the item count, which a
removal and an addition between two refreshes would leave unchanged.

Four module tests cover it. The first reads the bar chart itself through
a probe -- how many bars, and their labels in index order -- because the
visualizer records nothing to a result file, so asserting on the traffic
would pass just as well with the visualizer switched off. The second
asserts that a figure which cannot display labelled items is rejected,
which is also what proves the visualizer ran at all. The third deletes a
signal source and the fourth deletes a whole network node, each asserting
what is left on the figure afterwards.
Three UDP streams from the server to the receiver, with a bar chart above
the receiver showing a per stream quantity: one bar per sink app
(groupBy = "networkNode", with count or throughput), or one bar per named
flow demultiplexed from a single signal (splitBy = "flow"). A further
config displays the throughput of one stream on a gauge instead, to show
that the figure is a configuration choice rather than a display mode.
Shows the data rate an access point is using towards each of its
associated stations as a bar chart above the node, so that rate diversity
across stations is visible while the simulation runs rather than only in
the results afterwards.

It is a configuration of the generic ~StatisticCanvasVisualizer rather
than new visualizer code: it subscribes to the rate control's
datarateChanged signal, which tags each value with the receiving
station, displays one bar per receiver, and configures a bar chart figure
with the scale, colors and label format that suit a data rate. Pointing
signalName at the coordination function's datarateSelected instead makes
it cover fixed and per-receiver configured rates too; the NED
documentation gives that configuration.

Being a ~StatisticCanvasVisualizer, it goes wherever one does: it is
selected as the type of the integrated visualizer's statistic visualizer,
and so needs no submodule of its own.
@adamgeorge309
adamgeorge309 force-pushed the topic/gy/statistic-visualizer-bar-charts branch from 496fe51 to 17f1b14 Compare September 1, 2026 15:43
@adamgeorge309 adamgeorge309 changed the title visualizer: display several values of a statistic with any indicator figure, and the IEEE 802.11 per-station rate visualizer visualizer: display several values of a statistic on one figure, and the IEEE 802.11 per-peer rate visualizer Sep 1, 2026
@adamgeorge309
adamgeorge309 changed the base branch from topic/gy/ieee80211-per-station-rate-control to master September 1, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants