Skip to content

Add dimension groups to TimeSeries for better plots#19

Merged
tuckermcclure merged 10 commits into
mainfrom
tucker/add-dimension-groups
Apr 1, 2026
Merged

Add dimension groups to TimeSeries for better plots#19
tuckermcclure merged 10 commits into
mainfrom
tucker/add-dimension-groups

Conversation

@tuckermcclure
Copy link
Copy Markdown
Member

@tuckermcclure tuckermcclure commented Mar 27, 2026

Previously, the plot for the IMU measurement time series was useless because too many signals were stacked vertically. This is a draft of the "dimension groups" idea that lets us group dimensions into axes when plotting a time series.

We add a groups section to the VariableDescription, like so:

discrete_outputs = (;
            measurement = VariableDescription{IMUMeasurement}(
                missing;
                title = "IMU Measurement",
                dimensions = [
                    "t" => "s",
                    "delta_theta_x" => "rad",
                    "delta_theta_y" => "rad",
                    "delta_theta_z" => "rad",
                    "delta_v_x" => "m/s",
                    "delta_v_y" => "m/s",
                    "delta_v_z" => "m/s",
                    "delta_t" => "s",
                    "temperature" => "C",
                ],
                groups = [
                    "Delta Theta" => ["delta_theta_x", "delta_theta_y", "delta_theta_z"],
                    "Delta V" => ["delta_v_x", "delta_v_y", "delta_v_z"],
                    "Temperature" => ["temperature"],
                ],
            ),
            ...

and then we get a plot that looks like this:

image

By default, plots look the same, except that this also changes the default plot style for discrete signals to use scatter! rather than stairs!, so users can clearly see the sample times in a plot. (Also, stairs! very oddly "holds" the right sample between two samples rather than the left, so that for t = [0., 1., 2.], data = [5., 4., 3.], there's a line at 4 from t = 0. to t = 1. This was not expected. I even wonder if it's a bug. There's an option controlling which side of the interval is used for the line between the two, but I think scatter! is more useful anyway.)

This also adds a "matrix" implementation of plot_ts, allowing one to a arrange a bunch of plots in a figure. Here is an example:

image

The code for this:

# Let's make a little matrix of all of the plots we want.
plot_matrix = Matrix{Any}(undef, 2, 2)
plot_matrix[1, 1] = history["/"]["position"]
plot_matrix[1, 2] = history["/"]["thrust_command"]
plot_matrix[2, 2] = history["/"]["filtered_acceleration"]

# For velocity, we'll combine the truth and the measurement.
plot_matrix[2, 1] = [
    "truth" => history["/"]["velocity"],
    "measured" => history["/"]["velocity_measurement"],
]

# Now we can view that plot.
display(
    SystemsOfSystems.plot_ts(
        plot_matrix;
        figure_kwargs = (; size = (1200, 800)), # Passed along to the figure window.
    ),
)

That's actually a pretty complex plot, because it uses the "truth" vs "measured" behavior for the velocity (which breaks out of the dimension grouping and expands each dimension since the whole point is to show the differences in multiple time series for each dimension). If we didn't need to do that, it gets easier:

display(
    SystemsOfSystems.plot_ts(
        [
            history["/"]["position"]  history["/"]["thrust_command"];
            history["/"]["velocity"]  history["/"]["filtered_acceleration"];
        ];
        figure_kwargs = (; size = (1200, 800)), # Passed along to the figure window.
    ),
)
image

This PR also reorganizes things so that most of what was in plot_ts is now in plot_ts!, and all plot_ts does is make the figure and link the axes when all is done.

Comment thread src/Logs.jl
"""
TODO
This module contains the different log types: `BasicLog` (the default), `NullLog` (doesn't
log), and `HDF5Log` (import HDF5 for this one to work).
Copy link
Copy Markdown
Member Author

@tuckermcclure tuckermcclure Mar 28, 2026

Choose a reason for hiding this comment

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

Turns out I had these as local edits and didn't notice. Then, I had to edit this file (below). I think we should just keep them in this PR.

Comment thread src/Logs.jl

# TODO: Add a Base.show multiline method for a log.

# TODO: Consider making a getindex that breaks apart a single string into model and var.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I was thinking that we might support history["/aircraft/body:position"] as a shortcut for history["/aircraft/body"]["position"], but that seems really unnecessary. It means that this function sometimes returns a model history and sometimes a time series, and that just makes life complicated. The syntax is already perfectly short.

Comment thread src/SystemsOfSystems.jl
title::String
dimensions::Vector{Dimension}
# record::Bool # To let users decide on a per-model basis if they want this model to log anything at all. Or, let this be a set of symbols to record.
VariableDescription(value, title, dimensions) = new{typeof(value)}(value, title, dimensions)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

By moving the other constructors out of the struct definition, this function is implemented by default, so I could just delete this line.

@tuckermcclure tuckermcclure marked this pull request as ready for review March 28, 2026 04:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces “dimension groups” for TimeSeries so plots can group multiple dimensions onto shared axes (improving readability for high-dimensional signals like IMU measurements). It also expands the plotting API to support in-place plotting (plot_ts!) and composing multiple plots in vertical stacks or matrices.

Changes:

  • Add groups metadata to TimeSeries and VariableDescription to control how dimensions map to plot axes.
  • Update Makie plotting extension to render grouped axes, change discrete default to scatter!, and add stacked/matrix plot composition helpers.
  • Thread groups through log-created TimeSeries objects (BasicLog/HDF5Log) and update tests accordingly.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/TimeSeries.jl Adds groups to TimeSeries, keyword constructor, default dimension/group generation, and updated metadata-preserving slicing/resampling.
src/SystemsOfSystems.jl Extends VariableDescription with groups so log-produced TimeSeries can inherit plotting group metadata.
src/Logs.jl Passes groups from VariableDescription into TimeSeries created by BasicLog; adds a show method for logs.
ext/SystemsOfSystemsMakieExt/SystemsOfSystemsMakieExt.jl Implements grouped plotting, new plot_ts! plumbing, discrete scatter rendering, and vector/matrix composition support.
ext/SystemsOfSystemsHDF5Ext/SystemsOfSystemsHDF5Ext.jl Passes groups into TimeSeries created by HDF5Log (saving/loading groups still TODO).
test/runtests.jl Updates TimeSeries construction in tests to match the new API.
.gitignore Ignores .vscode/.
Comments suppressed due to low confidence (1)

test/runtests.jl:32

  • TimeSeries is constructed with groups = [], but the groups field is typed as Vector{Pair{String, Vector{String}}}. An untyped empty literal [] is Vector{Any} and will cause a type error at construction. Use a typed empty vector (e.g., Pair{String, Vector{String}}[]) or omit groups to use the default grouping behavior.
    ts = SystemsOfSystems.TimeSeries(;
        title = "Rotor Speed",
        time = collect(0.0:0.1:2.0),
        data = collect(100.0:120.0),
        time_dimension = SystemsOfSystems.Dimension("time", "s"),
        dimensions = [SystemsOfSystems.Dimension("angular speed", "rad/s"),],
        path = "/rotor/omega",
        discrete = false,
    )


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/runtests.jl
Comment thread src/TimeSeries.jl
Comment thread src/TimeSeries.jl
Comment thread src/TimeSeries.jl
Comment on lines +154 to 163
return TimeSeries(;
ts.title,
ts.time[i],
ts.data[i],
time = ts.time[i],
data = ts.data[i],
ts.time_dimension,
copy(ts.dimensions),
dimensions = copy(ts.dimensions),
ts.path,
ts.discrete,
ts.groups,
)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

This call uses keyword-only syntax (TimeSeries(; ...)) but passes bare expressions like ts.title, ts.time_dimension, ts.path, etc. Property accesses are not valid keyword shorthand and this will not parse/compile. Use explicit keywords (e.g., title = ts.title, time_dimension = ts.time_dimension, path = ts.path, groups = ts.groups).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You are incorrect about this. Property accesses are valid keyword shorthand. E.g., this creates a named tuple called y with a field called a that has the same value as x.a:

x = (; a = 1., )
y = (; x.a, )

There is no need to change this code.

Comment thread src/TimeSeries.jl
Comment on lines +226 to 235
return TimeSeries(;
ts.title,
collected_times,
[ts(t) for t in collected_times],
time = collected_times,
data = [ts(t) for t in collected_times],
ts.time_dimension,
copy(ts.dimensions),
dimensions = copy(ts.dimensions),
ts.path,
ts.discrete,
ts.groups,
)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Same issue here: TimeSeries(; ts.title, ..., ts.groups) is not valid keyword argument syntax and will fail to parse/compile. Switch these to explicit keyword assignments for all non-identifier values.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to 60
return TimeSeries(;
var.title,
create_hdf5_vector(group, "time", Float64),
create_hdf5_vector(group, "data", el_type),
time = create_hdf5_vector(group, "time", Float64),
data = create_hdf5_vector(group, "data", el_type),
time_dimension,
var.dimensions,
slug,
path = slug,
discrete,
var.groups,
)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Same keyword-argument syntax issue: TimeSeries(; var.title, ..., var.dimensions, ..., var.groups) will not parse. Use explicit keyword assignments for these values.

Copilot uses AI. Check for mistakes.
Comment thread ext/SystemsOfSystemsMakieExt/SystemsOfSystemsMakieExt.jl Outdated
Comment on lines +59 to +68
first_units = ts.dimensions[dim_nums[1]].units
units_are_consistent = all(
ts.dimensions[k].units == first_units
for k in dim_nums
)
ylabel = if units_are_consistent
"$group_label ($first_units)"
else
group_label
end
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

When units_are_consistent and first_units is the empty string, the ylabel becomes "<group> ()". Consider omitting the parentheses when units are empty to avoid awkward labels.

Copilot uses AI. Check for mistakes.
Comment thread ext/SystemsOfSystemsMakieExt/SystemsOfSystemsMakieExt.jl Outdated
Comment thread src/SystemsOfSystems.jl
@tuckermcclure tuckermcclure merged commit ce40e23 into main Apr 1, 2026
3 checks passed
@tuckermcclure tuckermcclure deleted the tucker/add-dimension-groups branch April 1, 2026 21:48
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