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