diff --git a/src/Bonsai.ML.Visualizers/KinematicComponentVisualizer.cs b/src/Bonsai.ML.Visualizers/KinematicComponentVisualizer.cs deleted file mode 100644 index 31ba029d..00000000 --- a/src/Bonsai.ML.Visualizers/KinematicComponentVisualizer.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using System.Windows.Forms; -using System.Collections.Generic; -using System.Reflection; -using Bonsai; -using Bonsai.Design; -using Bonsai.ML.Visualizers; -using Bonsai.ML.LinearDynamicalSystems; -using Bonsai.ML.LinearDynamicalSystems.Kinematics; -using System.Drawing; -using System.Reactive; - -[assembly: TypeVisualizer(typeof(KinematicComponentVisualizer), Target = typeof(KinematicComponent))] - -namespace Bonsai.ML.Visualizers -{ - /// - /// Provides a type visualizer to display the state components of a Kalman Filter kinematics model. - /// - public class KinematicComponentVisualizer : BufferedVisualizer - { - - private PropertyInfo stateComponentProperty; - - private int selectedIndex = 0; - - private DateTime? _startTime; - - private TimeSeriesOxyPlotBase Plot; - - /// - /// The selected index of the state component to be visualized - /// - public int SelectedIndex { get => selectedIndex; set => selectedIndex = value; } - - /// - /// Size of the window when loaded - /// - public Size Size { get; set; } = new Size(320, 240); - - /// - /// Capacity or length of time shown along the x axis of the plot during automatic updating - /// - public int Capacity { get; set; } = 10; - - /// - public override void Load(IServiceProvider provider) - { - var stateComponents = GetStateComponents(); - stateComponentProperty = typeof(KinematicComponent).GetProperty(stateComponents[selectedIndex]); - - Plot = new TimeSeriesOxyPlotBase( - lineSeriesName: "Mean", - areaSeriesName: "Variance", - dataSource: stateComponents, - selectedIndex: selectedIndex - ) - { - Size = Size, - Capacity = Capacity, - Dock = DockStyle.Fill, - StartTime = DateTime.Now - }; - - Plot.ResetSeries(); - - Plot.ComboBoxValueChanged += ComponentChanged; - - var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService)); - if (visualizerService != null) - { - visualizerService.AddControl(Plot); - } - } - - /// - public override void Show(object value) - { - } - - /// - protected override void Show(DateTime time, object value) - { - if (!_startTime.HasValue) - { - _startTime = time; - Plot.StartTime = _startTime.Value; - Plot.ResetSeries(); - } - - KinematicComponent kinematicComponent = (KinematicComponent)value; - StateComponent stateComponent = (StateComponent)stateComponentProperty.GetValue(kinematicComponent); - double mean = stateComponent.Mean; - double variance = stateComponent.Variance; - - Plot.AddToLineSeries( - time: time, - mean: mean - ); - - Plot.AddToAreaSeries( - time: time, - mean: mean, - variance: variance - ); - - Plot.SetAxes(minTime: time.AddSeconds(-Capacity), maxTime: time); - - } - - /// - protected override void ShowBuffer(IList> values) - { - base.ShowBuffer(values); - if (values.Count > 0) - { - Plot.UpdatePlot(); - } - } - - /// - /// Gets the names of the state components defined in the kinematic component class - /// - private List GetStateComponents() - { - List stateComponents = new List(); - - foreach (PropertyInfo property in typeof(KinematicComponent).GetProperties()) - { - if (property.PropertyType == typeof(StateComponent)) - { - stateComponents.Add(property.Name); - } - } - - return stateComponents; - } - - /// - public override void Unload() - { - _startTime = null; - if (!Plot.IsDisposed) - { - Plot.Dispose(); - } - } - - /// - /// Callback function to update the visualizer when the selected component has changed - /// - private void ComponentChanged(object sender, EventArgs e) - { - var comboBox = Plot.ComboBox; - selectedIndex = comboBox.SelectedIndex; - var selectedName = comboBox.SelectedItem.ToString(); - stateComponentProperty = typeof(KinematicComponent).GetProperty(selectedName); - _startTime = null; - - Plot.ResetSeries(); - } - } -} diff --git a/src/Bonsai.ML.Visualizers/KinematicStateVisualizer.cs b/src/Bonsai.ML.Visualizers/KinematicStateVisualizer.cs new file mode 100644 index 00000000..8409eab1 --- /dev/null +++ b/src/Bonsai.ML.Visualizers/KinematicStateVisualizer.cs @@ -0,0 +1,180 @@ +using Bonsai.Design; +using Bonsai; +using Bonsai.ML.LinearDynamicalSystems.Kinematics; +using Bonsai.ML.Visualizers; +using System.Collections.Generic; +using System; +using System.Windows.Forms; +using System.Reactive.Linq; +using System.Linq; +using System.Reactive; + +[assembly: TypeVisualizer(typeof(KinematicStateVisualizer), Target = typeof(KinematicState))] + +namespace Bonsai.ML.Visualizers +{ + + /// + /// Provides a type visualizer to display the state components of a Kalman Filter Kinematics model. + /// + public class KinematicStateVisualizer : MashupVisualizer + { + private TableLayoutPanel container; + private int updateFrequency = 1000 / 50; + private bool resetAxes = true; + private int rowCount = 3; + private int columnCount = 2; + + internal List ComponentVisualizers { get; private set; } = new(); + + internal string[] Labels = new string[] { + "Position X", + "Position Y", + "Velocity X", + "Velocity Y", + "Acceleration X", + "Acceleration Y" + }; + + /// + public override void Load(IServiceProvider provider) + { + container = new TableLayoutPanel + { + ColumnCount = columnCount, + RowCount = rowCount, + Dock = DockStyle.Fill + }; + + for (int i = 0; i < container.RowCount; i++) + { + container.RowStyles.Add(new RowStyle(SizeType.Percent, 100f / rowCount)); + } + + for (int i = 0; i < container.ColumnCount; i++) + { + container.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100f / columnCount)); + } + + for (int i = 0 ; i < rowCount; i++) + { + for (int j = 0; j < columnCount; j++) + { + var StateComponentVisualizer = new StateComponentVisualizer() { + Label = Labels[i * columnCount + j] + }; + StateComponentVisualizer.Load(provider); + container.Controls.Add(StateComponentVisualizer.Plot, j, i); + ComponentVisualizers.Add(StateComponentVisualizer); + } + } + + var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService)); + + if (visualizerService != null) + { + visualizerService.AddControl(container); + } + + base.Load(provider); + } + + + /// + public override void Show(object value) + { + } + + /// + public void ShowBuffer(IList> values) + { + List> positionX = new(); + List> positionY = new(); + List> velocityX = new(); + List> velocityY = new(); + List> accelerationX = new(); + List> accelerationY = new(); + + foreach (var value in values) + { + positionX.Add(new Timestamped(((KinematicState)value.Value).Position.X, value.Timestamp)); + positionY.Add(new Timestamped(((KinematicState)value.Value).Position.Y, value.Timestamp)); + velocityX.Add(new Timestamped(((KinematicState)value.Value).Velocity.X, value.Timestamp)); + velocityY.Add(new Timestamped(((KinematicState)value.Value).Velocity.Y, value.Timestamp)); + accelerationX.Add(new Timestamped(((KinematicState)value.Value).Acceleration.X, value.Timestamp)); + accelerationY.Add(new Timestamped(((KinematicState)value.Value).Acceleration.Y, value.Timestamp)); + } + + ComponentVisualizers[0].ShowDataBuffer(positionX, resetAxes); + ComponentVisualizers[1].ShowDataBuffer(positionY, resetAxes); + ComponentVisualizers[2].ShowDataBuffer(velocityX, resetAxes); + ComponentVisualizers[3].ShowDataBuffer(velocityY, resetAxes); + ComponentVisualizers[4].ShowDataBuffer(accelerationX, resetAxes); + ComponentVisualizers[5].ShowDataBuffer(accelerationY, resetAxes); + } + + /// + public override IObservable Visualize(IObservable> source, IServiceProvider provider) + { + if (provider.GetService(typeof(IDialogTypeVisualizerService)) is not Control visualizerControl) + { + return source; + } + + var visualizerSource = VisualizeSource(source, visualizerControl); + + if (MashupSources.Count == 0) + { + resetAxes = true; + return visualizerSource; + } + + resetAxes = false; + + var mashupSourceStreams = MashupSources.Select(mashupSource => + mashupSource.Visualizer.Visualize(mashupSource.Source.Output, provider) + .Do(value => mashupSource.Visualizer.Show(value))); + + var mergedMashupSources = Observable.Merge(mashupSourceStreams); + + return Observable.Merge(mergedMashupSources, visualizerSource); + } + + private IObservable VisualizeSource(IObservable> source, Control visualizerControl) + { + return Observable.Using( + () => new Timer(), + timer => + { + timer.Interval = updateFrequency; + var timerTick = Observable.FromEventPattern( + handler => timer.Tick += handler, + handler => timer.Tick -= handler); + timer.Start(); + var mergedSource = source.SelectMany(xs => xs.Do( + _ => { }, + () => visualizerControl.BeginInvoke((Action)SequenceCompleted))); + return mergedSource + .Timestamp(HighResolutionScheduler.Default) + .Buffer(() => timerTick) + .Do(ShowBuffer) + .Finally(timer.Stop); + }); + } + + /// + public override void Unload() + { + foreach (var componentVisualizer in ComponentVisualizers) + { + componentVisualizer.Unload(); + } + if (ComponentVisualizers.Count > 0) + { + ComponentVisualizers.Clear(); + ComponentVisualizers = new(); + } + if (!container.IsDisposed) container.Dispose(); + } + } +} diff --git a/src/Bonsai.ML.Visualizers/StateComponentVisualizer.cs b/src/Bonsai.ML.Visualizers/StateComponentVisualizer.cs index fbdaf333..f75e5a1c 100644 --- a/src/Bonsai.ML.Visualizers/StateComponentVisualizer.cs +++ b/src/Bonsai.ML.Visualizers/StateComponentVisualizer.cs @@ -5,8 +5,10 @@ using Bonsai.Design; using Bonsai.ML.Visualizers; using Bonsai.ML.LinearDynamicalSystems; -using System.Drawing; +using OxyPlot; using System.Reactive; +using OxyPlot.Series; +using System.Linq; [assembly: TypeVisualizer(typeof(StateComponentVisualizer), Target = typeof(StateComponent))] @@ -17,36 +19,61 @@ namespace Bonsai.ML.Visualizers /// public class StateComponentVisualizer : BufferedVisualizer { + internal TimeSeriesOxyPlotBase Plot; - private DateTime? _startTime; + internal LineSeries LineSeries { get; private set; } - private TimeSeriesOxyPlotBase Plot; + internal AreaSeries AreaSeries { get; private set; } + + private bool resetAxes = true; + + private DateTime? startTime; /// - /// Size of the window when loaded + /// Gets or sets the amount of time in seconds that should be shown along the x axis. /// - public Size Size { get; set; } = new Size(320, 240); + public int Capacity { get; set; } = 10; /// - /// Capacity or length of time shown along the x axis of the plot during automatic updating + /// Gets or sets a boolean value that determines whether to buffer the data beyond the capacity. /// - public int Capacity { get; set; } = 10; + public bool BufferData { get; set; } = true; + + /// + /// Gets or sets the optional label to prepend to the line and area series names. + /// + public string Label { get; set; } + + /// + /// Gets or sets the color to use for the line series. + /// + public OxyColor? LineSeriesColor { get; set; } = null; + + /// + /// Gets or sets the color to use for the area series. + /// + public OxyColor? AreaSeriesColor { get; set; } = null; /// public override void Load(IServiceProvider provider) { - Plot = new TimeSeriesOxyPlotBase( - lineSeriesName: "Mean", - areaSeriesName: "Variance" - ) + Plot = new TimeSeriesOxyPlotBase() { - Size = Size, Capacity = Capacity, Dock = DockStyle.Fill, - StartTime = DateTime.Now + StartTime = DateTime.Now, + BufferData = BufferData }; - Plot.ResetSeries(); + var lineSeriesName = string.IsNullOrEmpty(Label) ? "Mean" : $"{Label} Mean"; + LineSeries = Plot.AddNewLineSeries(lineSeriesName, color: LineSeriesColor); + + var areaSeriesName = string.IsNullOrEmpty(Label) ? "Variance" : $"{Label} Variance"; + AreaSeries = Plot.AddNewAreaSeries(areaSeriesName, color: AreaSeriesColor); + + Plot.ResetLineSeries(LineSeries); + Plot.ResetAreaSeries(AreaSeries); + Plot.ResetAxes(); var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService)); if (visualizerService != null) @@ -63,11 +90,11 @@ public override void Show(object value) /// protected override void Show(DateTime time, object value) { - if (!_startTime.HasValue) + if (!startTime.HasValue) { - _startTime = time; - Plot.StartTime = _startTime.Value; - Plot.ResetSeries(); + startTime = time; + Plot.StartTime = startTime.Value; + Plot.ResetAxes(); } StateComponent stateComponent = (StateComponent)value; @@ -75,18 +102,17 @@ protected override void Show(DateTime time, object value) double variance = stateComponent.Variance; Plot.AddToLineSeries( + lineSeries: LineSeries, time: time, - mean: mean + value: mean ); Plot.AddToAreaSeries( + areaSeries: AreaSeries, time: time, - mean: mean, - variance: variance + value1: mean + variance, + value2: mean - variance ); - - Plot.SetAxes(minTime: time.AddSeconds(-Capacity), maxTime: time); - } /// @@ -95,14 +121,25 @@ protected override void ShowBuffer(IList> values) base.ShowBuffer(values); if (values.Count > 0) { + if (resetAxes) + { + var time = values.LastOrDefault().Timestamp.DateTime; + Plot.SetAxes(minTime: time.AddSeconds(-Capacity), maxTime: time); + } Plot.UpdatePlot(); } } + internal void ShowDataBuffer(IList> values, bool resetAxes = true) + { + this.resetAxes = resetAxes; + ShowBuffer(values); + } + /// public override void Unload() { - _startTime = null; + startTime = null; if (!Plot.IsDisposed) { Plot.Dispose(); diff --git a/src/Bonsai.ML.Visualizers/TimeSeriesOxyPlotBase.cs b/src/Bonsai.ML.Visualizers/TimeSeriesOxyPlotBase.cs index 2f28fd17..b321b05a 100644 --- a/src/Bonsai.ML.Visualizers/TimeSeriesOxyPlotBase.cs +++ b/src/Bonsai.ML.Visualizers/TimeSeriesOxyPlotBase.cs @@ -13,52 +13,39 @@ internal class TimeSeriesOxyPlotBase : UserControl { private PlotView view; private PlotModel model; - private ToolStripComboBox comboBox; - private ToolStripLabel label; + private OxyColor defaultLineSeriesColor = OxyColors.Blue; + private OxyColor defaultAreaSeriesColor = OxyColors.LightBlue; - private string _lineSeriesName; - private string _areaSeriesName; - private int _selectedIndex; - private IEnumerable _dataSource; - - private LineSeries lineSeries; - private AreaSeries areaSeries; private Axis xAxis; private Axis yAxis; private StatusStrip statusStrip; /// - /// Event handler which can be used to hook into events generated when the combobox values have changed. - /// - public event EventHandler ComboBoxValueChanged; - - /// - /// DateTime value that determines the starting time of the data values. + /// Gets or sets the datetime value that determines the starting time of the data values. /// public DateTime StartTime { get; set; } /// - /// Integer value that determines how many data points should be shown along the x axis. + /// Gets or sets the integer value that determines how many data points should be shown along the x axis. /// public int Capacity { get; set; } - public ToolStripComboBox ComboBox => comboBox; - + /// + /// Gets the status strip control. + /// public StatusStrip StatusStrip => statusStrip; /// - /// Constructor of the TimeSeriesOxyPlotBase class. - /// Requires a line series name and an area series name. - /// Data source is optional, since pasing it to the constructor will populate the combobox and leave it empty otherwise. - /// The selected index is only needed when the data source is provided. + /// Gets or sets a boolean value that determines whether to buffer the data beyond the capacity. + /// + public bool BufferData { get; set; } + + /// + /// Initializes a new instance of the class /// - public TimeSeriesOxyPlotBase(string lineSeriesName, string areaSeriesName, IEnumerable dataSource = null, int selectedIndex = 0) + public TimeSeriesOxyPlotBase() { - _lineSeriesName = lineSeriesName; - _areaSeriesName = areaSeriesName; - _dataSource = dataSource; - _selectedIndex = selectedIndex; Initialize(); } @@ -72,17 +59,6 @@ private void Initialize() model = new PlotModel(); - lineSeries = new LineSeries { - Title = _lineSeriesName, - Color = OxyColors.Blue - }; - - areaSeries = new AreaSeries { - Title = _areaSeriesName, - Color = OxyColors.LightBlue, - Fill = OxyColor.FromArgb(100, 173, 216, 230) - }; - xAxis = new DateTimeAxis { Position = AxisPosition.Bottom, Title = "Time", @@ -102,9 +78,6 @@ private void Initialize() model.Axes.Add(xAxis); model.Axes.Add(yAxis); - model.Series.Add(lineSeries); - model.Series.Add(areaSeries); - view.Model = model; Controls.Add(view); @@ -113,17 +86,8 @@ private void Initialize() Visible = false }; - if (_dataSource != null) - { - InitializeComboBox(_dataSource); - - statusStrip.Items.AddRange(new ToolStripItem[] { - label, - comboBox, - }); - - view.MouseClick += new MouseEventHandler(onMouseClick); - } + view.MouseClick += new MouseEventHandler(onMouseClick); + Controls.Add(statusStrip); Controls.Add(statusStrip); @@ -138,66 +102,144 @@ private void onMouseClick(object sender, MouseEventArgs e) } } - private void InitializeComboBox(IEnumerable dataSource) + /// + /// Method to add a combobox with a label to the status strip. + /// Requires a string label, an enumerable data source, a selected index, and a callback method for the selected index changed event. + /// + public void AddComboBoxWithLabel(string label, IEnumerable dataSource, int selectedIndex, EventHandler onComboBoxSelectionChanged) { - label = new ToolStripLabel - { - Text = "State component:", - AutoSize = true, - }; - - comboBox = new ToolStripComboBox() - { - Name = "stateComponent", - AutoSize = true, - }; + ToolStripLabel toolStripLabel = new ToolStripLabel(label); + ToolStripComboBox toolStripComboBox = new ToolStripComboBox(); foreach (var value in dataSource) { - comboBox.Items.Add(value); + toolStripComboBox.Items.Add(value); } - - comboBox.SelectedIndexChanged += ComboBoxSelectedIndexChanged; - comboBox.SelectedIndex = _selectedIndex; + + toolStripComboBox.SelectedIndexChanged += onComboBoxSelectionChanged; + toolStripComboBox.SelectedIndex = selectedIndex; + + statusStrip.Items.AddRange(new ToolStripItem[] { + toolStripLabel, + toolStripComboBox + }); + } - private void ComboBoxSelectedIndexChanged(object sender, EventArgs e) + /// + /// Method to add a new line series to the data plot. + /// Requires a string for the name of the line series + /// Color of the line series is optional. + /// + public LineSeries AddNewLineSeries(string lineSeriesName, OxyColor? color = null) { - if (comboBox.SelectedIndex != _selectedIndex) - { - _selectedIndex = comboBox.SelectedIndex; - ComboBoxValueChanged?.Invoke(this, e); - } + OxyColor _color = color.HasValue ? color.Value : defaultLineSeriesColor; + LineSeries lineSeries = new LineSeries { + Title = lineSeriesName, + Color = _color + }; + model.Series.Add(lineSeries); + return lineSeries; } - public void AddToLineSeries(DateTime time, double mean) + /// + /// Method to add a new area series to the data plot. + /// Requires a string for the name of the area series + /// Optional parameters are color of the lines, fill color, and opacity. + /// Returns the new line series. + /// + public AreaSeries AddNewAreaSeries(string areaSeriesName, OxyColor? color = null, OxyColor? fill = null, byte opacity = 100) + { + OxyColor _color = color.HasValue ? color.Value : defaultAreaSeriesColor; + OxyColor _fill = fill.HasValue? fill.Value : OxyColor.FromArgb(opacity, _color.R, _color.G, _color.B); + AreaSeries areaSeries = new AreaSeries { + Title = areaSeriesName, + Color = _color, + Fill = _fill + }; + model.Series.Add(areaSeries); + return areaSeries; + } + + /// + /// Method to add data to a line series. + /// Requires the line series, time, and value. + /// + public void AddToLineSeries(LineSeries lineSeries, DateTime time, double value) { - lineSeries.Points.Add(new DataPoint(DateTimeAxis.ToDouble(time), mean)); + if (!BufferData) + { + var timeCap = DateTimeAxis.ToDouble(time.AddSeconds(-Capacity)); + lineSeries.Points.RemoveAll(dataPoint => dataPoint.X < timeCap); + } + lineSeries.Points.Add(new DataPoint(DateTimeAxis.ToDouble(time), value)); } - public void AddToAreaSeries(DateTime time, double mean, double variance) + /// + /// Method to add data to an area series. + /// Requires the area series, time, value1, and value2. + /// + public void AddToAreaSeries(AreaSeries areaSeries, DateTime time, double value1, double value2) { - areaSeries.Points.Add(new DataPoint(DateTimeAxis.ToDouble(time), mean + variance)); - areaSeries.Points2.Add(new DataPoint(DateTimeAxis.ToDouble(time), mean - variance)); + if (!BufferData) + { + var timeCap = DateTimeAxis.ToDouble(time.AddSeconds(-Capacity)); + areaSeries.Points.RemoveAll(dataPoint => dataPoint.X < timeCap); + areaSeries.Points2.RemoveAll(dataPoint => dataPoint.X < timeCap); + } + var curTime = DateTimeAxis.ToDouble(time); + areaSeries.Points.Add(new DataPoint(curTime, value1)); + areaSeries.Points2.Add(new DataPoint(curTime, value2)); } + /// + /// Set the minimum and maximum values to show along the x axis. + /// Requires the minTime and maxTime. + /// public void SetAxes(DateTime minTime, DateTime maxTime) { xAxis.Minimum = DateTimeAxis.ToDouble(minTime); xAxis.Maximum = DateTimeAxis.ToDouble(maxTime); } + /// + /// Method to update the plot. + /// public void UpdatePlot() { model.InvalidatePlot(true); } - public void ResetSeries() + /// + /// Method to reset the line series. + /// + public void ResetLineSeries(LineSeries lineSeries) { lineSeries.Points.Clear(); + } + + /// + /// Method to reset the area series. + /// + public void ResetAreaSeries(AreaSeries areaSeries) + { areaSeries.Points.Clear(); areaSeries.Points2.Clear(); + } + /// + /// Method to reset all series in the current PlotModel. + /// + public void ResetModelSeries() + { + model.Series.Clear(); + } + + /// + /// Method to reset the x and y axes to their default. + /// + public void ResetAxes() + { xAxis.Reset(); yAxis.Reset();