diff --git a/src/Bonsai.Gui.Visualizers/BarGraphOverlay.cs b/src/Bonsai.Gui.Visualizers/BarGraphOverlay.cs index ce580c8..e7fd9fc 100644 --- a/src/Bonsai.Gui.Visualizers/BarGraphOverlay.cs +++ b/src/Bonsai.Gui.Visualizers/BarGraphOverlay.cs @@ -8,7 +8,7 @@ using Bonsai.Expressions; using ZedGraph; -[assembly: TypeVisualizer(typeof(BarGraphOverlay), Target = typeof(MashupSource))] +[assembly: TypeVisualizer(typeof(BarGraphOverlay), Target = typeof(MashupSource))] namespace Bonsai.Gui.Visualizers @@ -18,7 +18,7 @@ namespace Bonsai.Gui.Visualizers /// public class BarGraphOverlay : BufferedVisualizer, IBarGraphVisualizer { - GraphPanelVisualizer visualizer; + RollingGraphPanelVisualizer visualizer; BarGraphBuilder.VisualizerController controller; BoundedPointPairList[] series; @@ -83,7 +83,7 @@ void AddBaseY() /// public override void Load(IServiceProvider provider) { - visualizer = (GraphPanelVisualizer)provider.GetService(typeof(MashupVisualizer)); + visualizer = (RollingGraphPanelVisualizer)provider.GetService(typeof(MashupVisualizer)); var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext)); var barGraphBuilder = (BarGraphBuilder)ExpressionBuilder.GetVisualizerElement(context.Source).Builder; controller = barGraphBuilder.Controller; diff --git a/src/Bonsai.Gui.Visualizers/Bonsai.Gui.Visualizers.csproj.user b/src/Bonsai.Gui.Visualizers/Bonsai.Gui.Visualizers.csproj.user index 12f9947..b948eb6 100644 --- a/src/Bonsai.Gui.Visualizers/Bonsai.Gui.Visualizers.csproj.user +++ b/src/Bonsai.Gui.Visualizers/Bonsai.Gui.Visualizers.csproj.user @@ -14,16 +14,16 @@ UserControl - + UserControl - + UserControl - + UserControl - + UserControl diff --git a/src/Bonsai.Gui.Visualizers/GraphPanel.cs b/src/Bonsai.Gui.Visualizers/GraphPanel.cs index a27e91b..875f4ba 100644 --- a/src/Bonsai.Gui.Visualizers/GraphPanel.cs +++ b/src/Bonsai.Gui.Visualizers/GraphPanel.cs @@ -4,56 +4,87 @@ namespace Bonsai.Gui.Visualizers { internal class GraphPanel : BoundedGraphPanel { - bool autoScale; + bool autoScaleX; + bool autoScaleY; public GraphPanel() { - autoScale = true; + autoScaleX = true; + autoScaleY = true; } - public Axis ScaleAxis => GraphPane.BarSettings.Base switch + public double XMin { - BarBase.Y => GraphPane.XAxis, - BarBase.Y2 => GraphPane.X2Axis, - BarBase.X2 => GraphPane.Y2Axis, - _ => GraphPane.YAxis - }; + get { return GraphPane.XAxis.Scale.Min; } + set + { + GraphPane.XAxis.Scale.Min = value; + GraphPane.AxisChange(); + Invalidate(); + } + } - public double Min + public double XMax { - get { return ScaleAxis.Scale.Min; } + get { return GraphPane.XAxis.Scale.Max; } set { - ScaleAxis.Scale.Min = value; + GraphPane.XAxis.Scale.Max = value; GraphPane.AxisChange(); Invalidate(); } } - public double Max + public double YMin { - get { return ScaleAxis.Scale.Max; } + get { return GraphPane.YAxis.Scale.Min; } set { - ScaleAxis.Scale.Max = value; + GraphPane.YAxis.Scale.Min = value; GraphPane.AxisChange(); Invalidate(); } } - public bool AutoScale + public double YMax + { + get { return GraphPane.YAxis.Scale.Max; } + set + { + GraphPane.YAxis.Scale.Max = value; + GraphPane.AxisChange(); + Invalidate(); + } + } + + public bool AutoScaleX + { + get { return autoScaleX; } + set + { + var changed = autoScaleX != value; + autoScaleX = value; + if (changed) + { + GraphPane.XAxis.Scale.MaxAuto = autoScaleX; + GraphPane.XAxis.Scale.MinAuto = autoScaleX; + if (autoScaleX) Invalidate(); + } + } + } + + public bool AutoScaleY { - get { return autoScale; } + get { return autoScaleY; } set { - var changed = autoScale != value; - autoScale = value; + var changed = autoScaleY != value; + autoScaleY = value; if (changed) { - var baseAxis = ScaleAxis; - baseAxis.Scale.MaxAuto = autoScale; - baseAxis.Scale.MinAuto = autoScale; - if (autoScale) Invalidate(); + GraphPane.YAxis.Scale.MaxAuto = autoScaleY; + GraphPane.YAxis.Scale.MinAuto = autoScaleY; + if (autoScaleY) Invalidate(); } } } diff --git a/src/Bonsai.Gui.Visualizers/GraphPanelBuilder.cs b/src/Bonsai.Gui.Visualizers/GraphPanelBuilder.cs index ec3589e..63a1543 100644 --- a/src/Bonsai.Gui.Visualizers/GraphPanelBuilder.cs +++ b/src/Bonsai.Gui.Visualizers/GraphPanelBuilder.cs @@ -3,7 +3,6 @@ using System.Linq.Expressions; using System.Reactive.Linq; using System.Reactive; -using ZedGraph; namespace Bonsai.Gui.Visualizers { @@ -16,46 +15,47 @@ namespace Bonsai.Gui.Visualizers public class GraphPanelBuilder : GraphPanelBuilderBase { /// - /// Gets or sets a value specifying the axis on which the bars in the graph will be displayed. + /// Gets or sets a value specifying a fixed lower limit for the X-axis range. + /// If no fixed range is specified, the graph limits can be edited online. /// - [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; } + [Category("Range")] + [Description("Specifies the optional fixed lower limit of the X-axis range.")] + public double? XMin { get; set; } /// - /// Gets or sets a value specifying how the different bars in the graph will be visually arranged. + /// Gets or sets a value specifying a fixed upper limit for the X-axis range. + /// If no fixed range is specified, the graph limits can be edited online. /// - [Category(nameof(CategoryAttribute.Appearance))] - [Description("Specifies how the different bars in the graph will be visually arranged.")] - public BarType BarType { get; set; } + [Category("Range")] + [Description("Specifies the optional fixed upper limit of the X-axis range.")] + public double? XMax { get; set; } /// - /// Gets or sets a value specifying a fixed lower limit for the axis range. + /// Gets or sets a value specifying a fixed lower limit for the Y-axis range. /// If no fixed range is specified, the graph limits can be edited online. /// [Category("Range")] - [Description("Specifies the optional fixed lower limit of the axis range.")] - public double? Min { get; set; } + [Description("Specifies the optional fixed lower limit of the Y-axis range.")] + public double? YMin { get; set; } /// - /// Gets or sets a value specifying a fixed upper limit for the axis range. + /// Gets or sets a value specifying a fixed upper limit for the Y-axis range. /// If no fixed range is specified, the graph limits can be edited online. /// [Category("Range")] - [Description("Specifies the optional fixed upper limit of the axis range.")] - public double? Max { get; set; } + [Description("Specifies the optional fixed upper limit of the Y-axis range.")] + public double? YMax { get; set; } internal VisualizerController Controller { get; set; } internal class VisualizerController { - internal BarBase BaseAxis; - internal BarType BarType; internal double? Span; internal int? Capacity; - internal double? Min; - internal double? Max; + internal double? XMin; + internal double? XMax; + internal double? YMin; + internal double? YMax; internal bool ReverseX; internal bool ReverseY; } @@ -69,12 +69,12 @@ public override Expression Build(IEnumerable arguments) { Controller = new VisualizerController { - BaseAxis = BaseAxis, - BarType = BarType, Span = Span, Capacity = Capacity, - Min = Min, - Max = Max, + XMin = XMin, + XMax = XMax, + YMin = YMin, + YMax = YMax, ReverseX = ReverseX, ReverseY = ReverseY }; diff --git a/src/Bonsai.Gui.Visualizers/GraphPanelView.Designer.cs b/src/Bonsai.Gui.Visualizers/GraphPanelView.Designer.cs index 030a70a..26100c9 100644 --- a/src/Bonsai.Gui.Visualizers/GraphPanelView.Designer.cs +++ b/src/Bonsai.Gui.Visualizers/GraphPanelView.Designer.cs @@ -35,10 +35,14 @@ private void InitializeComponent() this.spanValueLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.capacityStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.capacityValueLabel = new System.Windows.Forms.ToolStripStatusLabel(); - this.scaleStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); - this.minStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); - this.maxStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); - this.autoScaleButton = new System.Windows.Forms.ToolStripButton(); + this.scaleStatusLabelX = new System.Windows.Forms.ToolStripStatusLabel(); + this.minStatusLabelX = new System.Windows.Forms.ToolStripStatusLabel(); + this.maxStatusLabelX = new System.Windows.Forms.ToolStripStatusLabel(); + this.scaleStatusLabelY = new System.Windows.Forms.ToolStripStatusLabel(); + this.minStatusLabelY = new System.Windows.Forms.ToolStripStatusLabel(); + this.maxStatusLabelY = new System.Windows.Forms.ToolStripStatusLabel(); + this.autoScaleButtonX = new System.Windows.Forms.ToolStripButton(); + this.autoScaleButtonY = new System.Windows.Forms.ToolStripButton(); this.statusStrip.SuspendLayout(); this.SuspendLayout(); // @@ -50,10 +54,14 @@ private void InitializeComponent() this.spanValueLabel, this.capacityStatusLabel, this.capacityValueLabel, - this.scaleStatusLabel, - this.minStatusLabel, - this.maxStatusLabel, - this.autoScaleButton}); + this.scaleStatusLabelX, + this.minStatusLabelX, + this.maxStatusLabelX, + this.autoScaleButtonX, + this.scaleStatusLabelY, + this.minStatusLabelY, + this.maxStatusLabelY, + this.autoScaleButtonY}); this.statusStrip.Location = new System.Drawing.Point(0, 218); this.statusStrip.Name = "statusStrip"; this.statusStrip.Size = new System.Drawing.Size(400, 22); @@ -91,44 +99,76 @@ private void InitializeComponent() this.capacityValueLabel.Size = new System.Drawing.Size(12, 17); this.capacityValueLabel.Text = "count"; // - // scaleStatusLabel + // scaleStatusLabelX // - this.scaleStatusLabel.Name = "scaleStatusLabel"; - this.scaleStatusLabel.Size = new System.Drawing.Size(47, 17); - this.scaleStatusLabel.Text = "Scale:"; + this.scaleStatusLabelX.Name = "statusLabelX"; + this.scaleStatusLabelX.Size = new System.Drawing.Size(47, 17); + this.scaleStatusLabelX.Text = "X:"; // - // minStatusLabel + // minStatusLabelX // - this.minStatusLabel.Name = "minStatusLabel"; - this.minStatusLabel.Size = new System.Drawing.Size(13, 17); - this.minStatusLabel.Text = "min"; - this.minStatusLabel.Visible = false; + this.minStatusLabelX.Name = "minStatusLabelX"; + this.minStatusLabelX.Size = new System.Drawing.Size(13, 17); + this.minStatusLabelX.Text = "min"; + this.minStatusLabelX.Visible = false; // - // maxStatusLabel + // maxStatusLabelX // - this.maxStatusLabel.Name = "maxStatusLabel"; - this.maxStatusLabel.Size = new System.Drawing.Size(14, 17); - this.maxStatusLabel.Text = "Max"; - this.maxStatusLabel.Visible = false; + this.maxStatusLabelX.Name = "maxStatusLabelX"; + this.maxStatusLabelX.Size = new System.Drawing.Size(14, 17); + this.maxStatusLabelX.Text = "Max"; + this.maxStatusLabelX.Visible = false; // - // autoScaleButton + // autoScaleButtonX // - this.autoScaleButton.Checked = true; - this.autoScaleButton.CheckOnClick = true; - this.autoScaleButton.CheckState = System.Windows.Forms.CheckState.Checked; - this.autoScaleButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; - this.autoScaleButton.ImageTransparentColor = System.Drawing.Color.Magenta; - this.autoScaleButton.Name = "autoScaleButton"; - this.autoScaleButton.Size = new System.Drawing.Size(35, 20); - this.autoScaleButton.Text = "auto"; - this.autoScaleButton.CheckedChanged += new System.EventHandler(this.autoScaleButton_CheckedChanged); + this.autoScaleButtonX.Checked = true; + this.autoScaleButtonX.CheckOnClick = true; + this.autoScaleButtonX.CheckState = System.Windows.Forms.CheckState.Checked; + this.autoScaleButtonX.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; + this.autoScaleButtonX.ImageTransparentColor = System.Drawing.Color.Magenta; + this.autoScaleButtonX.Name = "autoScaleButtonX"; + this.autoScaleButtonX.Size = new System.Drawing.Size(35, 20); + this.autoScaleButtonX.Text = "auto"; + this.autoScaleButtonX.CheckedChanged += new System.EventHandler(this.autoScaleButtonX_CheckedChanged); // - // GraphPanelView + // scaleStatusLabelY + // + this.scaleStatusLabelY.Name = "statusLabelY"; + this.scaleStatusLabelY.Size = new System.Drawing.Size(47, 17); + this.scaleStatusLabelY.Text = "Y:"; + // + // minStatusLabelY + // + this.minStatusLabelY.Name = "minStatusLabelY"; + this.minStatusLabelY.Size = new System.Drawing.Size(13, 17); + this.minStatusLabelY.Text = "min"; + this.minStatusLabelY.Visible = false; + // + // maxStatusLabelY + // + this.maxStatusLabelY.Name = "maxStatusLabelY"; + this.maxStatusLabelY.Size = new System.Drawing.Size(14, 17); + this.maxStatusLabelY.Text = "Max"; + this.maxStatusLabelY.Visible = false; + // + // autoScaleButtonY + // + this.autoScaleButtonY.Checked = true; + this.autoScaleButtonY.CheckOnClick = true; + this.autoScaleButtonY.CheckState = System.Windows.Forms.CheckState.Checked; + this.autoScaleButtonY.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; + this.autoScaleButtonY.ImageTransparentColor = System.Drawing.Color.Magenta; + this.autoScaleButtonY.Name = "autoScaleButtonY"; + this.autoScaleButtonY.Size = new System.Drawing.Size(35, 20); + this.autoScaleButtonY.Text = "auto"; + this.autoScaleButtonY.CheckedChanged += new System.EventHandler(this.autoScaleButtonY_CheckedChanged); + // + // GraphPanelView2D // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.statusStrip); - this.Name = "GraphPanelView"; + this.Name = "GraphPanelView2D"; this.Size = new System.Drawing.Size(400, 240); this.statusStrip.ResumeLayout(false); this.statusStrip.PerformLayout(); @@ -140,11 +180,15 @@ private void InitializeComponent() #endregion private System.Windows.Forms.StatusStrip statusStrip; - private System.Windows.Forms.ToolStripButton autoScaleButton; + private System.Windows.Forms.ToolStripButton autoScaleButtonX; + private System.Windows.Forms.ToolStripButton autoScaleButtonY; private System.Windows.Forms.ToolStripStatusLabel cursorStatusLabel; - private System.Windows.Forms.ToolStripStatusLabel scaleStatusLabel; - private System.Windows.Forms.ToolStripStatusLabel minStatusLabel; - private System.Windows.Forms.ToolStripStatusLabel maxStatusLabel; + private System.Windows.Forms.ToolStripStatusLabel scaleStatusLabelX; + private System.Windows.Forms.ToolStripStatusLabel minStatusLabelX; + private System.Windows.Forms.ToolStripStatusLabel maxStatusLabelX; + private System.Windows.Forms.ToolStripStatusLabel scaleStatusLabelY; + private System.Windows.Forms.ToolStripStatusLabel minStatusLabelY; + private System.Windows.Forms.ToolStripStatusLabel maxStatusLabelY; private System.Windows.Forms.ToolStripStatusLabel capacityStatusLabel; private System.Windows.Forms.ToolStripStatusLabel capacityValueLabel; private System.Windows.Forms.ToolStripStatusLabel spanStatusLabel; diff --git a/src/Bonsai.Gui.Visualizers/GraphPanelView.cs b/src/Bonsai.Gui.Visualizers/GraphPanelView.cs index 2284566..f0c21aa 100644 --- a/src/Bonsai.Gui.Visualizers/GraphPanelView.cs +++ b/src/Bonsai.Gui.Visualizers/GraphPanelView.cs @@ -7,26 +7,33 @@ namespace Bonsai.Gui.Visualizers { partial class GraphPanelView : GraphPanel { - readonly ToolStripEditableLabel minEditableLabel; - readonly ToolStripEditableLabel maxEditableLabel; + readonly ToolStripEditableLabel minEditableLabelX; + readonly ToolStripEditableLabel maxEditableLabelX; + readonly ToolStripEditableLabel minEditableLabelY; + readonly ToolStripEditableLabel maxEditableLabelY; readonly ToolStripEditableLabel capacityEditableLabel; readonly ToolStripEditableLabel spanEditableLabel; public GraphPanelView() { InitializeComponent(); - autoScaleButton.Checked = true; + autoScaleButtonX.Checked = true; + autoScaleButtonY.Checked = true; spanEditableLabel = new ToolStripEditableLabel(spanValueLabel, OnSpanEdit); capacityEditableLabel = new ToolStripEditableLabel(capacityValueLabel, OnCapacityEdit); - minEditableLabel = new ToolStripEditableLabel(minStatusLabel, OnMinEdit); - maxEditableLabel = new ToolStripEditableLabel(maxStatusLabel, OnMaxEdit); + minEditableLabelX = new ToolStripEditableLabel(minStatusLabelX, OnXMinEdit); + maxEditableLabelX = new ToolStripEditableLabel(maxStatusLabelX, OnXMaxEdit); + minEditableLabelY = new ToolStripEditableLabel(minStatusLabelY, OnYMinEdit); + maxEditableLabelY = new ToolStripEditableLabel(maxStatusLabelY, OnYMaxEdit); GraphPane.AxisChangeEvent += GraphPane_AxisChangeEvent; MouseMoveEvent += GraphPanelView_MouseMoveEvent; MouseClick += GraphPanelView_MouseClick; components.Add(spanEditableLabel); components.Add(capacityEditableLabel); - components.Add(minEditableLabel); - components.Add(maxEditableLabel); + components.Add(minEditableLabelX); + components.Add(maxEditableLabelX); + components.Add(minEditableLabelY); + components.Add(maxEditableLabelY); } protected StatusStrip StatusStrip @@ -46,30 +53,40 @@ public bool CanEditCapacity set { capacityEditableLabel.Enabled = value; } } - public bool AutoScaleVisible + public bool AutoScaleXVisible { - get { return autoScaleButton.Visible; } + get { return autoScaleButtonX.Visible; } set { - autoScaleButton.Visible = value; - minEditableLabel.Enabled = value; - maxEditableLabel.Enabled = value; + autoScaleButtonX.Visible = value; + minEditableLabelX.Enabled = value; + maxEditableLabelX.Enabled = value; } } - private bool IsTimeSpan + public bool AutoScaleYVisible { - get + get { return autoScaleButtonY.Visible; } + set { - var baseAxis = GraphPane.BarSettings.BarBaseAxis(); - return baseAxis.Type == AxisType.Date || baseAxis.Type == AxisType.DateAsOrdinal; + autoScaleButtonY.Visible = value; + minEditableLabelY.Enabled = value; + maxEditableLabelY.Enabled = value; } } - public event EventHandler AutoScaleChanged + public bool IsTimeSpan { get; set; } + + public event EventHandler AutoScaleXChanged + { + add { autoScaleButtonX.CheckedChanged += value; } + remove { autoScaleButtonX.CheckedChanged -= value; } + } + + public event EventHandler AutoScaleYChanged { - add { autoScaleButton.CheckedChanged += value; } - remove { autoScaleButton.CheckedChanged -= value; } + add { autoScaleButtonY.CheckedChanged += value; } + remove { autoScaleButtonY.CheckedChanged -= value; } } public event EventHandler AxisChanged; @@ -104,22 +121,33 @@ private void GraphPane_AxisChangeEvent(GraphPane pane) { var span = Span; var capacity = Capacity; - var scale = ScaleAxis.Scale; - autoScaleButton.Checked = scale.MaxAuto; + var scaleX = pane.XAxis.Scale; + var scaleY = pane.YAxis.Scale; + autoScaleButtonX.Checked = pane.XAxis.Scale.MaxAuto; + autoScaleButtonY.Checked = pane.YAxis.Scale.MaxAuto; spanValueLabel.Text = IsTimeSpan ? TimeSpan.FromDays(span).ToString() : span.ToString("G5", CultureInfo.InvariantCulture); capacityValueLabel.Text = capacity.ToString(CultureInfo.InvariantCulture); - minStatusLabel.Text = scale.Min.ToString("G5", CultureInfo.InvariantCulture); - maxStatusLabel.Text = scale.Max.ToString("G5", CultureInfo.InvariantCulture); + minStatusLabelX.Text = scaleX.Min.ToString("G5", CultureInfo.InvariantCulture); + maxStatusLabelX.Text = scaleX.Max.ToString("G5", CultureInfo.InvariantCulture); + minStatusLabelY.Text = scaleY.Min.ToString("G5", CultureInfo.InvariantCulture); + maxStatusLabelY.Text = scaleY.Max.ToString("G5", CultureInfo.InvariantCulture); OnAxisChanged(EventArgs.Empty); } - private void autoScaleButton_CheckedChanged(object sender, EventArgs e) + private void autoScaleButtonX_CheckedChanged(object sender, EventArgs e) { - AutoScale = autoScaleButton.Checked; - minStatusLabel.Visible = !autoScaleButton.Checked; - maxStatusLabel.Visible = !autoScaleButton.Checked; + AutoScaleX = autoScaleButtonX.Checked; + minStatusLabelX.Visible = !autoScaleButtonX.Checked; + maxStatusLabelX.Visible = !autoScaleButtonX.Checked; + } + + private void autoScaleButtonY_CheckedChanged(object sender, EventArgs e) + { + AutoScaleY = autoScaleButtonY.Checked; + minStatusLabelY.Visible = !autoScaleButtonY.Checked; + maxStatusLabelY.Visible = !autoScaleButtonY.Checked; } private void OnSpanEdit(string text) @@ -145,19 +173,35 @@ private void OnCapacityEdit(string text) } } - private void OnMinEdit(string text) + private void OnXMinEdit(string text) + { + if (double.TryParse(text, out double min)) + { + XMin = min; + } + } + + private void OnXMaxEdit(string text) + { + if (double.TryParse(text, out double max)) + { + XMax = max; + } + } + + private void OnYMinEdit(string text) { if (double.TryParse(text, out double min)) { - Min = min; + YMin = min; } } - private void OnMaxEdit(string text) + private void OnYMaxEdit(string text) { if (double.TryParse(text, out double max)) { - Max = max; + YMax = max; } } } diff --git a/src/Bonsai.Gui.Visualizers/GraphPanelVisualizer.cs b/src/Bonsai.Gui.Visualizers/GraphPanelVisualizer.cs index 62ba2f6..7256af2 100644 --- a/src/Bonsai.Gui.Visualizers/GraphPanelVisualizer.cs +++ b/src/Bonsai.Gui.Visualizers/GraphPanelVisualizer.cs @@ -13,8 +13,6 @@ namespace Bonsai.Gui.Visualizers public class GraphPanelVisualizer : MashupControlVisualizerBase { Type indexType; - BarSettings barSettings; - GraphPanelBuilder graphBuilder; /// /// Gets or sets the maximum span of data displayed at any one moment in the graph. @@ -27,61 +25,48 @@ public class GraphPanelVisualizer : MashupControlVisualizerBase - /// Gets or sets the lower limit of the axis range when using a fixed scale. + /// Gets or sets the lower limit of the X-axis range when using a fixed scale. /// - public double Min { get; set; } + public double XMin { get; set; } /// - /// Gets or sets the upper limit of the axis range when using a fixed scale. + /// Gets or sets the upper limit of the X-axis range when using a fixed scale. /// - public double Max { get; set; } = 1; + public double XMax { get; set; } = 1; + + /// + /// Gets or sets the lower limit of the Y-axis range when using a fixed scale. + /// + public double YMin { get; set; } + + /// + /// Gets or sets the upper limit of the Y-axis range when using a fixed scale. + /// + public double YMax { get; set; } = 1; /// /// Gets or sets a value indicating whether the axis range should be recalculated /// automatically as the graph updates. /// - public bool AutoScale { get; set; } = true; - - internal BarSettings BarSettings => barSettings; + public bool AutoScaleX { get; set; } = true; - private Axis BarBaseAxis() - { - return graphBuilder.BaseAxis switch - { - BarBase.Y => Control.GraphPane.YAxis, - BarBase.Y2 => Control.GraphPane.Y2Axis, - BarBase.X2 => Control.GraphPane.X2Axis, - _ => Control.GraphPane.XAxis - }; - } + /// + /// Gets or sets a value indicating whether the axis range should be recalculated + /// automatically as the graph updates. + /// + public bool AutoScaleY { get; set; } = true; internal void EnsureIndex(Type type) { if (indexType == null) { indexType = type; - var baseAxis = BarBaseAxis(); - if (type == typeof(string)) - { - GraphHelper.FormatOrdinalAxis(baseAxis, indexType); - } - if (type == typeof(XDate)) + if (type == typeof(XDate) && Control is GraphPanelView view) { - GraphHelper.FormatLinearDateAxis(baseAxis); + view.IsTimeSpan = true; } } - else ThrowHelper.ThrowIfNotEquals(indexType, type, "Only overlays with identical axis are allowed."); - } - - internal void EnsureBarSettings(BarSettings settings) - { - const string ErrorMessage = "All bar graph overlays must have bar settings compatible with the graph panel."; - ThrowHelper.ThrowIfNotEquals(barSettings.Base, settings.Base, ErrorMessage); - ThrowHelper.ThrowIfNotEquals(barSettings.ClusterScaleWidth, settings.ClusterScaleWidth, ErrorMessage); - ThrowHelper.ThrowIfNotEquals(barSettings.ClusterScaleWidthAuto, settings.ClusterScaleWidthAuto, ErrorMessage); - ThrowHelper.ThrowIfNotEquals(barSettings.MinBarGap, settings.MinBarGap, ErrorMessage); - ThrowHelper.ThrowIfNotEquals(barSettings.MinClusterGap, settings.MinClusterGap, ErrorMessage); - ThrowHelper.ThrowIfNotEquals(barSettings.Type, settings.Type, ErrorMessage); + else ThrowHelper.ThrowIfNotEquals(indexType, type, "Only overlays with identical indices are allowed."); } /// @@ -93,24 +78,38 @@ protected override GraphControl CreateControl(IServiceProvider provider, GraphPa var controller = graphPanelBuilder.Controller; view.GraphPane.XAxis.Scale.IsReverse = controller.ReverseX; view.GraphPane.YAxis.Scale.IsReverse = controller.ReverseY; - barSettings = view.GraphPane.BarSettings; - barSettings.Base = controller.BaseAxis; - barSettings.Type = controller.BarType; - if (controller.Min.HasValue || controller.Max.HasValue) + if (controller.XMin.HasValue || controller.XMax.HasValue) + { + view.AutoScaleX = false; + view.AutoScaleXVisible = false; + view.XMin = controller.XMin.GetValueOrDefault(); + view.XMax = controller.XMax.GetValueOrDefault(); + } + else + { + view.AutoScaleX = AutoScaleX; + if (!AutoScaleX) + { + view.XMin = XMin; + view.XMax = XMax; + } + } + + if (controller.YMin.HasValue || controller.YMax.HasValue) { - view.AutoScale = false; - view.AutoScaleVisible = false; - view.Min = controller.Min.GetValueOrDefault(); - view.Max = controller.Max.GetValueOrDefault(); + view.AutoScaleY = false; + view.AutoScaleYVisible = false; + view.YMin = controller.YMin.GetValueOrDefault(); + view.YMax = controller.YMax.GetValueOrDefault(); } else { - view.AutoScale = AutoScale; - if (!AutoScale) + view.AutoScaleY = AutoScaleY; + if (!AutoScaleY) { - view.Min = Min; - view.Max = Max; + view.YMin = YMin; + view.YMax = YMax; } } @@ -139,13 +138,14 @@ protected override GraphControl CreateControl(IServiceProvider provider, GraphPa view.Dock = DockStyle.Fill; view.HandleDestroyed += delegate { - Min = view.Min; - Max = view.Max; - AutoScale = view.AutoScale; + XMin = view.XMin; + XMax = view.XMax; + YMin = view.YMin; + YMax = view.YMax; + AutoScaleX = view.AutoScaleX; Capacity = view.Capacity; Span = view.Span; }; - graphBuilder = builder; return view; } @@ -160,9 +160,6 @@ protected override void LoadMashupSource(int index, MashupSource mashupSource, I public override void Unload() { base.Unload(); - indexType = null; - barSettings = null; - graphBuilder = null; } } } diff --git a/src/Bonsai.Gui.Visualizers/LineGraphOverlay.cs b/src/Bonsai.Gui.Visualizers/LineGraphOverlay.cs new file mode 100644 index 0000000..7000bbc --- /dev/null +++ b/src/Bonsai.Gui.Visualizers/LineGraphOverlay.cs @@ -0,0 +1,100 @@ +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(LineGraphOverlay), Target = typeof(MashupSource))] + + +namespace Bonsai.Gui.Visualizers +{ + /// + /// Provides a type visualizer used to overlay a sequence of points as a line graph. + /// + public class LineGraphOverlay : BufferedVisualizer, ILineGraphVisualizer + { + GraphPanelVisualizer visualizer; + LineGraphBuilder.VisualizerController controller; + BoundedPointPairList[] series; + + void ILineGraphVisualizer.AddValues(double index, params PointPair[] values) + { + for (int i = 0; i < series.Length; i++) + { + series[i].Add(values[i].X, values[i].Y, index, values[i].Tag); + } + } + + /// + public override void Load(IServiceProvider provider) + { + visualizer = (GraphPanelVisualizer)provider.GetService(typeof(MashupVisualizer)); + var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext)); + var lineGraphBuilder = (LineGraphBuilder)ExpressionBuilder.GetVisualizerElement(context.Source).Builder; + controller = lineGraphBuilder.Controller; + 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 LineItem(label, points, color, controller.SymbolType, controller.LineWidth); + curve.Line.IsAntiAlias = true; + curve.Line.IsOptimizedDraw = true; + curve.Label.IsVisible = !string.IsNullOrEmpty(label); + curve.Symbol.Fill.Type = FillType.Solid; + curve.Symbol.IsAntiAlias = true; + return curve; + } + + /// + protected override void ShowBuffer(IList> values) + { + base.ShowBuffer(values); + if (values.Count > 0) + { + visualizer.Control.Invalidate(); + } + } + + /// + public override void Show(object value) + { + Show(DateTime.Now, value); + } + + /// + protected override void Show(DateTime time, object value) + { + controller.AddValues(time, value, this); + } + + /// + public override void Unload() + { + visualizer = null; + } + } +} diff --git a/src/Bonsai.Gui.Visualizers/RollingGraphOverlay.cs b/src/Bonsai.Gui.Visualizers/RollingGraphOverlay.cs index 35be8b8..95c9d4c 100644 --- a/src/Bonsai.Gui.Visualizers/RollingGraphOverlay.cs +++ b/src/Bonsai.Gui.Visualizers/RollingGraphOverlay.cs @@ -8,7 +8,7 @@ using Bonsai.Expressions; using ZedGraph; -[assembly: TypeVisualizer(typeof(RollingGraphOverlay), Target = typeof(MashupSource))] +[assembly: TypeVisualizer(typeof(RollingGraphOverlay), Target = typeof(MashupSource))] namespace Bonsai.Gui.Visualizers @@ -18,7 +18,7 @@ namespace Bonsai.Gui.Visualizers /// public class RollingGraphOverlay : BufferedVisualizer, IRollingGraphVisualizer { - GraphPanelVisualizer visualizer; + RollingGraphPanelVisualizer visualizer; RollingGraphBuilder.VisualizerController controller; BoundedPointPairList[] series; @@ -49,7 +49,7 @@ internal void AddValues(double index, string tag, params double[] values) /// public override void Load(IServiceProvider provider) { - visualizer = (GraphPanelVisualizer)provider.GetService(typeof(MashupVisualizer)); + visualizer = (RollingGraphPanelVisualizer)provider.GetService(typeof(MashupVisualizer)); var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext)); var rollingGraphBuilder = (RollingGraphBuilder)ExpressionBuilder.GetVisualizerElement(context.Source).Builder; controller = rollingGraphBuilder.Controller; diff --git a/src/Bonsai.Gui.Visualizers/RollingGraphPanel.cs b/src/Bonsai.Gui.Visualizers/RollingGraphPanel.cs new file mode 100644 index 0000000..acfff06 --- /dev/null +++ b/src/Bonsai.Gui.Visualizers/RollingGraphPanel.cs @@ -0,0 +1,61 @@ +using ZedGraph; + +namespace Bonsai.Gui.Visualizers +{ + internal class RollingGraphPanel : BoundedGraphPanel + { + bool autoScale; + + public RollingGraphPanel() + { + autoScale = true; + } + + public Axis ScaleAxis => GraphPane.BarSettings.Base switch + { + BarBase.Y => GraphPane.XAxis, + BarBase.Y2 => GraphPane.X2Axis, + BarBase.X2 => GraphPane.Y2Axis, + _ => GraphPane.YAxis + }; + + public double Min + { + get { return ScaleAxis.Scale.Min; } + set + { + ScaleAxis.Scale.Min = value; + GraphPane.AxisChange(); + Invalidate(); + } + } + + public double Max + { + get { return ScaleAxis.Scale.Max; } + set + { + ScaleAxis.Scale.Max = value; + GraphPane.AxisChange(); + Invalidate(); + } + } + + public bool AutoScale + { + get { return autoScale; } + set + { + var changed = autoScale != value; + autoScale = value; + if (changed) + { + var baseAxis = ScaleAxis; + baseAxis.Scale.MaxAuto = autoScale; + baseAxis.Scale.MinAuto = autoScale; + if (autoScale) Invalidate(); + } + } + } + } +} diff --git a/src/Bonsai.Gui.Visualizers/RollingGraphPanelBuilder.cs b/src/Bonsai.Gui.Visualizers/RollingGraphPanelBuilder.cs new file mode 100644 index 0000000..f9e049b --- /dev/null +++ b/src/Bonsai.Gui.Visualizers/RollingGraphPanelBuilder.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reactive.Linq; +using System.Reactive; +using ZedGraph; + +namespace Bonsai.Gui.Visualizers +{ + /// + /// Represents an operator that specifies a mashup graph panel that can be used + /// to combine multiple graphs displaying data over a shared axis. + /// + [TypeVisualizer(typeof(RollingGraphPanelVisualizer))] + [Description("Specifies a mashup graph panel that can be used to combine multiple graphs displaying data over a shared axis.")] + public class RollingGraphPanelBuilder : GraphPanelBuilderBase + { + /// + /// Gets or sets a value specifying the axis on which the bars in the graph will be displayed. + /// + [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; } + + /// + /// Gets or sets a value specifying how the different bars in the graph will be visually arranged. + /// + [Category(nameof(CategoryAttribute.Appearance))] + [Description("Specifies how the different bars in the graph will be visually arranged.")] + public BarType BarType { get; set; } + + /// + /// Gets or sets a value specifying a fixed lower limit for the axis range. + /// If no fixed range is specified, the graph limits can be edited online. + /// + [Category("Range")] + [Description("Specifies the optional fixed lower limit of the axis range.")] + public double? Min { get; set; } + + /// + /// Gets or sets a value specifying a fixed upper limit for the axis range. + /// If no fixed range is specified, the graph limits can be edited online. + /// + [Category("Range")] + [Description("Specifies the optional fixed upper limit of the axis range.")] + public double? Max { get; set; } + + internal VisualizerController Controller { get; set; } + + internal class VisualizerController + { + internal BarBase BaseAxis; + internal BarType BarType; + internal double? Span; + internal int? Capacity; + internal double? Min; + internal double? Max; + internal bool ReverseX; + internal bool ReverseY; + } + + /// + /// Builds the expression tree for configuring and calling the + /// graph panel visualizer. + /// + /// + public override Expression Build(IEnumerable arguments) + { + Controller = new VisualizerController + { + BaseAxis = BaseAxis, + BarType = BarType, + Span = Span, + Capacity = Capacity, + Min = Min, + Max = Max, + ReverseX = ReverseX, + ReverseY = ReverseY + }; + return Expression.Call(typeof(Observable), nameof(Observable.Never), new[] { typeof(Unit) }); + } + } +} diff --git a/src/Bonsai.Gui.Visualizers/RollingGraphPanelView.Designer.cs b/src/Bonsai.Gui.Visualizers/RollingGraphPanelView.Designer.cs new file mode 100644 index 0000000..c1d7881 --- /dev/null +++ b/src/Bonsai.Gui.Visualizers/RollingGraphPanelView.Designer.cs @@ -0,0 +1,153 @@ +namespace Bonsai.Gui.Visualizers +{ + partial class RollingGraphPanelView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.cursorStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.spanStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.spanValueLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.capacityStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.capacityValueLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.scaleStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.minStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.maxStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.autoScaleButton = new System.Windows.Forms.ToolStripButton(); + this.statusStrip.SuspendLayout(); + this.SuspendLayout(); + // + // statusStrip + // + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.cursorStatusLabel, + this.spanStatusLabel, + this.spanValueLabel, + this.capacityStatusLabel, + this.capacityValueLabel, + this.scaleStatusLabel, + this.minStatusLabel, + this.maxStatusLabel, + this.autoScaleButton}); + this.statusStrip.Location = new System.Drawing.Point(0, 218); + this.statusStrip.Name = "statusStrip"; + this.statusStrip.Size = new System.Drawing.Size(400, 22); + this.statusStrip.TabIndex = 1; + this.statusStrip.Text = "statusStrip1"; + this.statusStrip.Visible = false; + // + // cursorStatusLabel + // + this.cursorStatusLabel.Name = "cursorStatusLabel"; + this.cursorStatusLabel.Size = new System.Drawing.Size(45, 17); + this.cursorStatusLabel.Text = "Cursor:"; + // + // spanStatusLabel + // + this.spanStatusLabel.Name = "spanStatusLabel"; + this.spanStatusLabel.Size = new System.Drawing.Size(47, 17); + this.spanStatusLabel.Text = "Span:"; + // + // spanValueLabel + // + this.spanValueLabel.Name = "spanValueLabel"; + this.spanValueLabel.Size = new System.Drawing.Size(12, 17); + this.spanValueLabel.Text = "value"; + // + // capacityStatusLabel + // + this.capacityStatusLabel.Name = "capacityStatusLabel"; + this.capacityStatusLabel.Size = new System.Drawing.Size(47, 17); + this.capacityStatusLabel.Text = "Capacity:"; + // + // capacityValueLabel + // + this.capacityValueLabel.Name = "capacityValueLabel"; + this.capacityValueLabel.Size = new System.Drawing.Size(12, 17); + this.capacityValueLabel.Text = "count"; + // + // scaleStatusLabel + // + this.scaleStatusLabel.Name = "scaleStatusLabel"; + this.scaleStatusLabel.Size = new System.Drawing.Size(47, 17); + this.scaleStatusLabel.Text = "Scale:"; + // + // minStatusLabel + // + this.minStatusLabel.Name = "minStatusLabel"; + this.minStatusLabel.Size = new System.Drawing.Size(13, 17); + this.minStatusLabel.Text = "min"; + this.minStatusLabel.Visible = false; + // + // maxStatusLabel + // + this.maxStatusLabel.Name = "maxStatusLabel"; + this.maxStatusLabel.Size = new System.Drawing.Size(14, 17); + this.maxStatusLabel.Text = "Max"; + this.maxStatusLabel.Visible = false; + // + // autoScaleButton + // + this.autoScaleButton.Checked = true; + this.autoScaleButton.CheckOnClick = true; + this.autoScaleButton.CheckState = System.Windows.Forms.CheckState.Checked; + this.autoScaleButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; + this.autoScaleButton.ImageTransparentColor = System.Drawing.Color.Magenta; + this.autoScaleButton.Name = "autoScaleButton"; + this.autoScaleButton.Size = new System.Drawing.Size(35, 20); + this.autoScaleButton.Text = "auto"; + this.autoScaleButton.CheckedChanged += new System.EventHandler(this.autoScaleButton_CheckedChanged); + // + // GraphPanelView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.statusStrip); + this.Name = "GraphPanelView"; + this.Size = new System.Drawing.Size(400, 240); + this.statusStrip.ResumeLayout(false); + this.statusStrip.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.StatusStrip statusStrip; + private System.Windows.Forms.ToolStripButton autoScaleButton; + private System.Windows.Forms.ToolStripStatusLabel cursorStatusLabel; + private System.Windows.Forms.ToolStripStatusLabel scaleStatusLabel; + private System.Windows.Forms.ToolStripStatusLabel minStatusLabel; + private System.Windows.Forms.ToolStripStatusLabel maxStatusLabel; + private System.Windows.Forms.ToolStripStatusLabel capacityStatusLabel; + private System.Windows.Forms.ToolStripStatusLabel capacityValueLabel; + private System.Windows.Forms.ToolStripStatusLabel spanStatusLabel; + private System.Windows.Forms.ToolStripStatusLabel spanValueLabel; + } +} diff --git a/src/Bonsai.Gui.Visualizers/RollingGraphPanelView.cs b/src/Bonsai.Gui.Visualizers/RollingGraphPanelView.cs new file mode 100644 index 0000000..a94e2be --- /dev/null +++ b/src/Bonsai.Gui.Visualizers/RollingGraphPanelView.cs @@ -0,0 +1,164 @@ +using System; +using System.Windows.Forms; +using ZedGraph; +using System.Globalization; + +namespace Bonsai.Gui.Visualizers +{ + partial class RollingGraphPanelView : RollingGraphPanel + { + readonly ToolStripEditableLabel minEditableLabel; + readonly ToolStripEditableLabel maxEditableLabel; + readonly ToolStripEditableLabel capacityEditableLabel; + readonly ToolStripEditableLabel spanEditableLabel; + + public RollingGraphPanelView() + { + InitializeComponent(); + autoScaleButton.Checked = true; + spanEditableLabel = new ToolStripEditableLabel(spanValueLabel, OnSpanEdit); + capacityEditableLabel = new ToolStripEditableLabel(capacityValueLabel, OnCapacityEdit); + minEditableLabel = new ToolStripEditableLabel(minStatusLabel, OnMinEdit); + maxEditableLabel = new ToolStripEditableLabel(maxStatusLabel, OnMaxEdit); + GraphPane.AxisChangeEvent += GraphPane_AxisChangeEvent; + MouseMoveEvent += GraphPanelView_MouseMoveEvent; + MouseClick += GraphPanelView_MouseClick; + components.Add(spanEditableLabel); + components.Add(capacityEditableLabel); + components.Add(minEditableLabel); + components.Add(maxEditableLabel); + } + + protected StatusStrip StatusStrip + { + get { return statusStrip; } + } + + public bool CanEditSpan + { + get { return spanEditableLabel.Enabled; } + set { spanEditableLabel.Enabled = value; } + } + + public bool CanEditCapacity + { + get { return capacityEditableLabel.Enabled; } + set { capacityEditableLabel.Enabled = value; } + } + + public bool AutoScaleVisible + { + get { return autoScaleButton.Visible; } + set + { + autoScaleButton.Visible = value; + minEditableLabel.Enabled = value; + maxEditableLabel.Enabled = value; + } + } + + private bool IsTimeSpan + { + get + { + var baseAxis = GraphPane.BarSettings.BarBaseAxis(); + return baseAxis.Type == AxisType.Date || baseAxis.Type == AxisType.DateAsOrdinal; + } + } + + public event EventHandler AutoScaleChanged + { + add { autoScaleButton.CheckedChanged += value; } + remove { autoScaleButton.CheckedChanged -= value; } + } + + public event EventHandler AxisChanged; + + protected virtual void OnAxisChanged(EventArgs e) + { + AxisChanged?.Invoke(this, e); + } + + private bool GraphPanelView_MouseMoveEvent(ZedGraphControl sender, MouseEventArgs e) + { + var pane = MasterPane.FindChartRect(e.Location); + if (pane != null) + { + pane.ReverseTransform(e.Location, out double x, out double y); + cursorStatusLabel.Text = string.Format("Cursor: ({0:G5}, {1:G5})", x, y); + } + return false; + } + + private void GraphPanelView_MouseClick(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Right) + { + statusStrip.Visible = !statusStrip.Visible; + ((ViewPane)MasterPane).StatusGap = statusStrip.Visible ? statusStrip.Height : 0; + ZedGraphControl_ReSize(this, EventArgs.Empty); + } + } + + private void GraphPane_AxisChangeEvent(GraphPane pane) + { + var span = Span; + var capacity = Capacity; + var scale = ScaleAxis.Scale; + autoScaleButton.Checked = scale.MaxAuto; + spanValueLabel.Text = IsTimeSpan + ? TimeSpan.FromDays(span).ToString() + : span.ToString("G5", CultureInfo.InvariantCulture); + capacityValueLabel.Text = capacity.ToString(CultureInfo.InvariantCulture); + minStatusLabel.Text = scale.Min.ToString("G5", CultureInfo.InvariantCulture); + maxStatusLabel.Text = scale.Max.ToString("G5", CultureInfo.InvariantCulture); + OnAxisChanged(EventArgs.Empty); + } + + private void autoScaleButton_CheckedChanged(object sender, EventArgs e) + { + AutoScale = autoScaleButton.Checked; + minStatusLabel.Visible = !autoScaleButton.Checked; + maxStatusLabel.Visible = !autoScaleButton.Checked; + } + + private void OnSpanEdit(string text) + { + if (IsTimeSpan) + { + if (TimeSpan.TryParse(text, out TimeSpan timeSpan)) + { + Span = timeSpan.TotalDays; + } + } + else if (double.TryParse(text, out double span)) + { + Span = span; + } + } + + private void OnCapacityEdit(string text) + { + if (int.TryParse(text, out int capacity)) + { + Capacity = capacity; + } + } + + private void OnMinEdit(string text) + { + if (double.TryParse(text, out double min)) + { + Min = min; + } + } + + private void OnMaxEdit(string text) + { + if (double.TryParse(text, out double max)) + { + Max = max; + } + } + } +} diff --git a/src/Bonsai.Gui.Visualizers/RollingGraphPanelView.resx b/src/Bonsai.Gui.Visualizers/RollingGraphPanelView.resx new file mode 100644 index 0000000..422cca5 --- /dev/null +++ b/src/Bonsai.Gui.Visualizers/RollingGraphPanelView.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/src/Bonsai.Gui.Visualizers/RollingGraphPanelVisualizer.cs b/src/Bonsai.Gui.Visualizers/RollingGraphPanelVisualizer.cs new file mode 100644 index 0000000..bf63f8c --- /dev/null +++ b/src/Bonsai.Gui.Visualizers/RollingGraphPanelVisualizer.cs @@ -0,0 +1,168 @@ +using System; +using System.Windows.Forms; +using Bonsai.Design; +using Bonsai.Expressions; +using ZedGraph; + +namespace Bonsai.Gui.Visualizers +{ + /// + /// Provides a type visualizer that can be used to overlay multiple plots sharing + /// the same index axis in a single graph panel. + /// + public class RollingGraphPanelVisualizer : MashupControlVisualizerBase + { + Type indexType; + BarSettings barSettings; + RollingGraphPanelBuilder graphBuilder; + + /// + /// Gets or sets the maximum span of data displayed at any one moment in the graph. + /// + public double Span { get; set; } + + /// + /// Gets or sets the maximum number of points displayed at any one moment in the graph. + /// + public int Capacity { get; set; } + + /// + /// Gets or sets the lower limit of the axis range when using a fixed scale. + /// + public double Min { get; set; } + + /// + /// Gets or sets the upper limit of the axis range when using a fixed scale. + /// + public double Max { get; set; } = 1; + + /// + /// Gets or sets a value indicating whether the axis range should be recalculated + /// automatically as the graph updates. + /// + public bool AutoScale { get; set; } = true; + + internal BarSettings BarSettings => barSettings; + + private Axis BarBaseAxis() + { + return graphBuilder.BaseAxis switch + { + BarBase.Y => Control.GraphPane.YAxis, + BarBase.Y2 => Control.GraphPane.Y2Axis, + BarBase.X2 => Control.GraphPane.X2Axis, + _ => Control.GraphPane.XAxis + }; + } + + internal void EnsureIndex(Type type) + { + if (indexType == null) + { + indexType = type; + var baseAxis = BarBaseAxis(); + if (type == typeof(string)) + { + GraphHelper.FormatOrdinalAxis(baseAxis, indexType); + } + if (type == typeof(XDate)) + { + GraphHelper.FormatLinearDateAxis(baseAxis); + } + } + else ThrowHelper.ThrowIfNotEquals(indexType, type, "Only overlays with identical axis are allowed."); + } + + internal void EnsureBarSettings(BarSettings settings) + { + const string ErrorMessage = "All bar graph overlays must have bar settings compatible with the graph panel."; + ThrowHelper.ThrowIfNotEquals(barSettings.Base, settings.Base, ErrorMessage); + ThrowHelper.ThrowIfNotEquals(barSettings.ClusterScaleWidth, settings.ClusterScaleWidth, ErrorMessage); + ThrowHelper.ThrowIfNotEquals(barSettings.ClusterScaleWidthAuto, settings.ClusterScaleWidthAuto, ErrorMessage); + ThrowHelper.ThrowIfNotEquals(barSettings.MinBarGap, settings.MinBarGap, ErrorMessage); + ThrowHelper.ThrowIfNotEquals(barSettings.MinClusterGap, settings.MinClusterGap, ErrorMessage); + ThrowHelper.ThrowIfNotEquals(barSettings.Type, settings.Type, ErrorMessage); + } + + /// + protected override GraphControl CreateControl(IServiceProvider provider, RollingGraphPanelBuilder builder) + { + var view = new RollingGraphPanelView(); + var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext)); + var graphPanelBuilder = (RollingGraphPanelBuilder)ExpressionBuilder.GetVisualizerElement(context.Source).Builder; + var controller = graphPanelBuilder.Controller; + view.GraphPane.XAxis.Scale.IsReverse = controller.ReverseX; + view.GraphPane.YAxis.Scale.IsReverse = controller.ReverseY; + barSettings = view.GraphPane.BarSettings; + barSettings.Base = controller.BaseAxis; + barSettings.Type = controller.BarType; + + if (controller.Min.HasValue || controller.Max.HasValue) + { + view.AutoScale = false; + view.AutoScaleVisible = false; + view.Min = controller.Min.GetValueOrDefault(); + view.Max = controller.Max.GetValueOrDefault(); + } + else + { + view.AutoScale = AutoScale; + if (!AutoScale) + { + view.Min = Min; + view.Max = Max; + } + } + + if (controller.Capacity.HasValue) + { + view.Capacity = controller.Capacity.Value; + view.CanEditCapacity = false; + } + else + { + view.Capacity = Capacity; + view.CanEditCapacity = true; + } + + if (controller.Span.HasValue) + { + view.Span = controller.Span.Value; + view.CanEditSpan = false; + } + else + { + view.Span = Span; + view.CanEditSpan = true; + } + + view.Dock = DockStyle.Fill; + view.HandleDestroyed += delegate + { + Min = view.Min; + Max = view.Max; + AutoScale = view.AutoScale; + Capacity = view.Capacity; + Span = view.Span; + }; + graphBuilder = builder; + return view; + } + + /// + protected override void LoadMashupSource(int index, MashupSource mashupSource, IServiceProvider provider) + { + Control.ResetColorCycle(); + base.LoadMashupSource(index, mashupSource, provider); + } + + /// + public override void Unload() + { + base.Unload(); + indexType = null; + barSettings = null; + graphBuilder = null; + } + } +}