Add dimension groups to TimeSeries for better plots#19
Conversation
| """ | ||
| TODO | ||
| This module contains the different log types: `BasicLog` (the default), `NullLog` (doesn't | ||
| log), and `HDF5Log` (import HDF5 for this one to work). |
There was a problem hiding this comment.
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.
|
|
||
| # 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. |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
By moving the other constructors out of the struct definition, this function is implemented by default, so I could just delete this line.
There was a problem hiding this comment.
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
groupsmetadata toTimeSeriesandVariableDescriptionto 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
groupsthrough log-createdTimeSeriesobjects (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
TimeSeriesis constructed withgroups = [], but thegroupsfield is typed asVector{Pair{String, Vector{String}}}. An untyped empty literal[]isVector{Any}and will cause a type error at construction. Use a typed empty vector (e.g.,Pair{String, Vector{String}}[]) or omitgroupsto 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.
| 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, | ||
| ) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
| 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, | ||
| ) |
There was a problem hiding this comment.
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.
| 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, | ||
| ) |
There was a problem hiding this comment.
Same keyword-argument syntax issue: TimeSeries(; var.title, ..., var.dimensions, ..., var.groups) will not parse. Use explicit keyword assignments for these values.
| 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 |
There was a problem hiding this comment.
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.
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
groupssection to the VariableDescription, like so:and then we get a plot that looks like this:
By default, plots look the same, except that this also changes the default plot style for discrete signals to use
scatter!rather thanstairs!, 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 fort = [0., 1., 2.],data = [5., 4., 3.], there's a line at 4 fromt = 0.tot = 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 thinkscatter!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:The code for this:
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:
This PR also reorganizes things so that most of what was in
plot_tsis now inplot_ts!, and allplot_tsdoes is make the figure and link the axes when all is done.