Skip to content

Commit

Permalink
Merge pull request #15 from bonsai-rx/feature-dev
Browse files Browse the repository at this point in the history
Add support for composing bar and rolling graphs in a single panel
  • Loading branch information
glopesdev authored Mar 25, 2024
2 parents fde60fc + 6a988b6 commit 0c1da09
Show file tree
Hide file tree
Showing 34 changed files with 4,706 additions and 0 deletions.
82 changes: 82 additions & 0 deletions src/Bonsai.Gui.Visualizers/BarGraph.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Drawing;
using ZedGraph;

namespace Bonsai.Gui.Visualizers
{
class BarGraph : RollingGraph
{
public BarBase BaseAxis
{
get { return GraphPane.BarSettings.Base; }
set { GraphPane.BarSettings.Base = value; }
}

public BarType BarType
{
get { return GraphPane.BarSettings.Type; }
set { GraphPane.BarSettings.Type = value; }
}

internal override CurveItem CreateSeries(string label, IPointListEdit points, Color color)
{
var curve = new BarItem(label, points, color);
curve.Label.IsVisible = !string.IsNullOrEmpty(label);
curve.Bar.Fill.Type = FillType.Solid;
curve.Bar.Border.IsVisible = false;
return curve;
}

static int FindIndex(IPointListEdit series, string tag)
{
if (!string.IsNullOrEmpty(tag))
{
for (int i = 0; i < series.Count; i++)
{
if (EqualityComparer<string>.Default.Equals(tag, (string)series[i].Tag))
{
return i;
}
}
}

return -1;
}

public new void AddValues(double index, string label, double[] values)
{
if (values.Length > 0)
{
var updateIndex = FindIndex(Series[0], label);
if (updateIndex >= 0 && BaseAxis <= BarBase.X2) UpdateLastBaseX();
else if (updateIndex >= 0) UpdateLastBaseY();
else if (BaseAxis <= BarBase.X2) AddBaseX();
else AddBaseY();

void UpdateLastBaseX()
{
for (int i = 0; i < Series.Length; i++)
Series[i][updateIndex].Y = values[i];
}

void UpdateLastBaseY()
{
for (int i = 0; i < Series.Length; i++)
Series[i][updateIndex].X = values[i];
}

void AddBaseX()
{
for (int i = 0; i < Series.Length; i++)
Series[i].Add(index, values[i], label);
}

void AddBaseY()
{
for (int i = 0; i < Series.Length; i++)
Series[i].Add(values[i], index, label);
}
}
}
}
}
161 changes: 161 additions & 0 deletions src/Bonsai.Gui.Visualizers/BarGraphBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using Bonsai.Expressions;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using ZedGraph;

namespace Bonsai.Gui.Visualizers
{
/// <summary>
/// Represents an operator that configures a visualizer to plot each element
/// of the sequence as a bar graph.
/// </summary>
[DefaultProperty(nameof(ValueSelector))]
[TypeVisualizer(typeof(BarGraphVisualizer))]
[Description("A visualizer that plots each element of the sequence as a bar graph.")]
public class BarGraphBuilder : SingleArgumentExpressionBuilder
{
/// <summary>
/// Gets or sets the name of the property that will be used as index for the graph.
/// </summary>
[Editor("Bonsai.Design.MemberSelectorEditor, Bonsai.Design", DesignTypes.UITypeEditor)]
[Description("The name of the property that will be used as index for the graph.")]
public string IndexSelector { get; set; }

/// <summary>
/// Gets or sets the names of the properties that will be displayed in the graph.
/// </summary>
[Editor("Bonsai.Design.MultiMemberSelectorEditor, Bonsai.Design", DesignTypes.UITypeEditor)]
[Description("The names of the properties that will be displayed in the graph.")]
public string ValueSelector { get; set; }

/// <summary>
/// Gets or sets a value specifying the axis on which the bars in the graph will be displayed.
/// </summary>
[TypeConverter(typeof(BaseAxisConverter))]
[Category(nameof(CategoryAttribute.Appearance))]
[Description("Specifies the axis on which the bars in the graph will be displayed.")]
public BarBase BaseAxis { get; set; }

/// <summary>
/// Gets or sets a value specifying how the different bars in the graph will be visually arranged.
/// </summary>
[Category(nameof(CategoryAttribute.Appearance))]
[Description("Specifies how the different bars in the graph will be visually arranged.")]
public BarType BarType { get; set; }

/// <summary>
/// Gets the optional settings for each bar added to the graph.
/// </summary>
[Category(nameof(CategoryAttribute.Appearance))]
[Description("Specifies optional settings for each bar added to the graph.")]
public Collection<CurveConfiguration> CurveSettings { get; } = new();

/// <summary>
/// Gets or sets the optional capacity used for rolling bar graphs. If no capacity is specified,
/// all data points will be displayed.
/// </summary>
[Category("Range")]
[Description("The optional capacity used for rolling bar graphs. If no capacity is specified, all data points will be displayed.")]
public int? Capacity { get; set; }

/// <summary>
/// Gets or sets a value specifying a fixed lower limit for the bar range axis.
/// If no fixed range is specified, the graph limits can be edited online.
/// </summary>
[Category("Range")]
[Description("Specifies the optional fixed lower limit for the bar range axis.")]
public double? Min { get; set; }

/// <summary>
/// Gets or sets a value specifying a fixed upper limit for the bar range axis.
/// If no fixed range is specified, the graph limits can be edited online.
/// </summary>
[Category("Range")]
[Description("Specifies the optional fixed upper limit for the bar range axis.")]
public double? Max { get; set; }

internal VisualizerController Controller { get; set; }

internal class VisualizerController
{
internal int? Capacity;
internal double? Min;
internal double? Max;
internal Type IndexType;
internal string IndexLabel;
internal string[] ValueLabels;
internal CurveConfiguration[] CurveSettings;
internal Action<object, IBarGraphVisualizer> AddValues;
internal BarBase BaseAxis;
internal BarType BarType;
}

/// <summary>
/// Builds the expression tree for configuring and calling the
/// bar graph visualizer on the specified input argument.
/// </summary>
/// <inheritdoc/>
public override Expression Build(IEnumerable<Expression> arguments)
{
var source = arguments.First();
var parameterType = source.Type.GetGenericArguments()[0];
var valueParameter = Expression.Parameter(typeof(object));
var viewParameter = Expression.Parameter(typeof(IBarGraphVisualizer));
var elementVariable = Expression.Variable(parameterType);
Controller = new VisualizerController
{
Capacity = Capacity,
Min = Min,
Max = Max,
BaseAxis = BaseAxis,
BarType = BarType,
CurveSettings = CurveSettings.ToArray()
};

var selectedIndex = GraphHelper.SelectIndexMember(elementVariable, IndexSelector, out Controller.IndexLabel);
Controller.IndexType = selectedIndex.Type;
if (selectedIndex.Type != typeof(double) && selectedIndex.Type != typeof(string))
{
selectedIndex = Expression.Convert(selectedIndex, typeof(double));
}

var selectedValues = GraphHelper.SelectDataValues(elementVariable, ValueSelector, out Controller.ValueLabels);
var addValuesBody = Expression.Block(new[] { elementVariable },
Expression.Assign(elementVariable, Expression.Convert(valueParameter, parameterType)),
Expression.Call(viewParameter, nameof(IBarGraphVisualizer.AddValues), null, selectedIndex, selectedValues));
Controller.AddValues = Expression.Lambda<Action<object, IBarGraphVisualizer>>(addValuesBody, valueParameter, viewParameter).Compile();
return Expression.Call(typeof(BarGraphBuilder), nameof(Process), new[] { parameterType }, source);
}

static IObservable<TSource> Process<TSource>(IObservable<TSource> source)
{
return source;
}
}

class BaseAxisConverter : EnumConverter
{
public BaseAxisConverter(Type type)
: base(type)
{
}

public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
return new StandardValuesCollection(new[] { BarBase.X, BarBase.Y });
}
}

interface IBarGraphVisualizer
{
void AddValues(string index, params double[] values);

void AddValues(double index, params double[] values);

void AddValues(double index, string tag, params double[] values);
}
}
153 changes: 153 additions & 0 deletions src/Bonsai.Gui.Visualizers/BarGraphOverlay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Reactive;
using Bonsai;
using Bonsai.Design;
using Bonsai.Gui.Visualizers;
using Bonsai.Expressions;
using ZedGraph;

[assembly: TypeVisualizer(typeof(BarGraphOverlay), Target = typeof(MashupSource<GraphPanelVisualizer, BarGraphVisualizer>))]


namespace Bonsai.Gui.Visualizers
{
/// <summary>
/// Provides a type visualizer used to overlay a sequence of values as a bar graph.
/// </summary>
public class BarGraphOverlay : BufferedVisualizer, IBarGraphVisualizer
{
GraphPanelVisualizer visualizer;
BarGraphBuilder.VisualizerController controller;
BoundedPointPairList[] series;

void IBarGraphVisualizer.AddValues(string index, params double[] values) => AddValues(0, index, values);

void IBarGraphVisualizer.AddValues(double index, params double[] values) => AddValues(index, null, values);

void IBarGraphVisualizer.AddValues(double index, string tag, params double[] values) => AddValues(index, tag, values);

static int FindIndex(IPointListEdit series, string tag)
{
if (!string.IsNullOrEmpty(tag))
{
for (int i = 0; i < series.Count; i++)
{
if (EqualityComparer<string>.Default.Equals(tag, (string)series[i].Tag))
{
return i;
}
}
}

return -1;
}

internal void AddValues(double index, string tag, params double[] values)
{
if (values.Length > 0)
{
var updateIndex = FindIndex(series[0], tag);
if (updateIndex >= 0 && controller.BaseAxis <= BarBase.X2) UpdateLastBaseX();
else if (updateIndex >= 0) UpdateLastBaseY();
else if (controller.BaseAxis <= BarBase.X2) AddBaseX();
else AddBaseY();

void UpdateLastBaseX()
{
for (int i = 0; i < series.Length; i++)
series[i][updateIndex].Y = values[i];
}

void UpdateLastBaseY()
{
for (int i = 0; i < series.Length; i++)
series[i][updateIndex].X = values[i];
}

void AddBaseX()
{
for (int i = 0; i < series.Length; i++)
series[i].Add(index, values[i], index, tag);
}

void AddBaseY()
{
for (int i = 0; i < series.Length; i++)
series[i].Add(values[i], index, index, tag);
}
}
}

/// <inheritdoc/>
public override void Load(IServiceProvider provider)
{
visualizer = (GraphPanelVisualizer)provider.GetService(typeof(MashupVisualizer));
var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext));
var barGraphBuilder = (BarGraphBuilder)ExpressionBuilder.GetVisualizerElement(context.Source).Builder;
controller = barGraphBuilder.Controller;
visualizer.EnsureBarSettings(new BarSettings(visualizer.Control.GraphPane)
{
Base = controller.BaseAxis,
Type = controller.BarType
});
visualizer.EnsureIndex(controller.IndexType);

var hasLabels = controller.ValueLabels != null;
if (hasLabels)
{
series = new BoundedPointPairList[controller.ValueLabels.Length];
for (int i = 0; i < series.Length; i++)
{
series[i] = new BoundedPointPairList();
var curveSettings = controller.CurveSettings.Length > 0
? controller.CurveSettings[i % controller.CurveSettings.Length]
: null;
var color = curveSettings?.Color.IsEmpty == false
? curveSettings.Color
: visualizer.Control.GetNextColor();
var curve = CreateSeries(curveSettings?.Label ?? controller.ValueLabels[i], series[i], color);
visualizer.Control.GraphPane.CurveList.Add(curve);
}
}
}

private CurveItem CreateSeries(string label, IPointListEdit points, Color color)
{
var curve = new BarItem(label, points, color);
curve.Label.IsVisible = !string.IsNullOrEmpty(label);
curve.Bar.Fill.Type = FillType.Solid;
curve.Bar.Border.IsVisible = false;
return curve;
}

/// <inheritdoc/>
protected override void ShowBuffer(IList<Timestamped<object>> values)
{
base.ShowBuffer(values);
if (values.Count > 0)
{
visualizer.Control.Invalidate();
}
}

/// <inheritdoc/>
public override void Show(object value)
{
Show(DateTime.Now, value);
}

/// <inheritdoc/>
protected override void Show(DateTime time, object value)
{
controller.AddValues(value, this);
}

/// <inheritdoc/>
public override void Unload()
{
visualizer = null;
}
}
}
Loading

0 comments on commit 0c1da09

Please sign in to comment.