mirror of
https://github.com/AxioDL/optick.git
synced 2026-03-30 11:49:24 -07:00
e3825692bb
Replacing LiveCharts with much more performant InteractiveDataDisplay library. New chart library is about 20x times faster.
950 lines
34 KiB
C#
950 lines
34 KiB
C#
// Copyright © Microsoft Corporation. All Rights Reserved.
|
|
// Licensed under the MIT License.
|
|
|
|
using System;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Media;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.ComponentModel;
|
|
using System.Reactive.Subjects;
|
|
|
|
namespace InteractiveDataDisplay.WPF
|
|
{
|
|
/// <summary>Represents a base for all elements that perform graphic plotting. Handles coordinate
|
|
/// transformations and manages composition of plots</summary>
|
|
public abstract class PlotBase : Panel
|
|
{
|
|
#region Fields
|
|
|
|
private double scaleXField;
|
|
private double scaleYField;
|
|
private double offsetXField;
|
|
private double offsetYField;
|
|
|
|
private bool isInternalChange = false;
|
|
|
|
private DataRect actualPlotRect = new DataRect(0, 0, 1, 1);
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
/// <summary>Gets or sets padding - distance in screen units from each side of border to edges of plot bounding rectangle.
|
|
/// Effective padding for composition of plots is computed as maximum of all paddings.</summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public Thickness Padding
|
|
{
|
|
get { return (Thickness)GetValue(PaddingProperty); }
|
|
set { SetValue(PaddingProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Desired ratio of horizontal scale to vertical scale. Values less than or equal to zero
|
|
/// represent unspecified aspect ratio.
|
|
/// </summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public double AspectRatio
|
|
{
|
|
get { return (double)GetValue(AspectRatioProperty); }
|
|
set { SetValue(AspectRatioProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets auto fit mode. <see cref="IsAutoFitEnabled"/> property of all
|
|
/// plots in compositions are updated instantly to have same value.
|
|
/// </summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public bool IsAutoFitEnabled
|
|
{
|
|
get { return (bool)GetValue(IsAutoFitEnabledProperty); }
|
|
set { SetValue(IsAutoFitEnabledProperty, value); }
|
|
}
|
|
|
|
/// <summary>Gets or sets desired plot width in plot coordinates. <see cref="PlotWidth"/> property of all
|
|
/// plots in compositions are updated instantly to have same value. Settings this property
|
|
/// turns off auto fit mode.</summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public double PlotWidth
|
|
{
|
|
get { return (double)GetValue(PlotWidthProperty); }
|
|
set { SetValue(PlotWidthProperty, value); }
|
|
}
|
|
|
|
/// <summary>Gets or sets desired plot height in plot coordinates. <see cref="PlotHeight"/> property of all
|
|
/// plots in compositions are updated instantly to have same value. Settings this property
|
|
/// turns off auto fit mode.</summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public double PlotHeight
|
|
{
|
|
get { return (double)GetValue(PlotHeightProperty); }
|
|
set { SetValue(PlotHeightProperty, value); }
|
|
}
|
|
|
|
/// <summary>Gets or sets desired minimal visible horizontal coordinate (in plot coordinate system).
|
|
/// <see cref="PlotOriginX"/> property of all
|
|
/// plots in compositions are updated instantly to have same value. Settings this property
|
|
/// turns off auto fit mode.</summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public double PlotOriginX
|
|
{
|
|
get { return (double)GetValue(PlotOriginXProperty); }
|
|
set { SetValue(PlotOriginXProperty, value); }
|
|
}
|
|
|
|
/// <summary>Gets or sets desired minimal visible vertical coordinate (in plot coordinate system).
|
|
/// <see cref="PlotOriginY"/> property of all
|
|
/// plots in compositions are updated instantly to have same value. Settings this property
|
|
/// turns off auto fit mode.</summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public double PlotOriginY
|
|
{
|
|
get { return (double)GetValue(PlotOriginYProperty); }
|
|
set { SetValue(PlotOriginYProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref="IsXAxisReversed"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty IsXAxisReversedProperty =
|
|
DependencyProperty.Register("IsXAxisReversed", typeof(bool), typeof(PlotBase), new PropertyMetadata(false));
|
|
|
|
/// <summary>
|
|
/// Gets or sets a flag indicating whether the x-axis is reversed or not.
|
|
/// </summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public bool IsXAxisReversed
|
|
{
|
|
get { return (bool)GetValue(IsXAxisReversedProperty); }
|
|
set { SetValue(IsXAxisReversedProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref="IsYAxisReversed"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty IsYAxisReversedProperty =
|
|
DependencyProperty.Register("IsYAxisReversed", typeof(bool), typeof(PlotBase), new PropertyMetadata(false));
|
|
|
|
/// <summary>
|
|
/// Gets or sets a flag indicating whether the y-axis is reversed or not.
|
|
/// </summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public bool IsYAxisReversed
|
|
{
|
|
get { return (bool)GetValue(IsYAxisReversedProperty); }
|
|
set { SetValue(IsYAxisReversedProperty, value); }
|
|
}
|
|
|
|
/// <summary>Gets or sets transform from user data to horizontal plot coordinate.
|
|
/// By default transform is <see cref="IdentityDataTransform"/>
|
|
/// </summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public DataTransform XDataTransform
|
|
{
|
|
get { return (DataTransform)GetValue(XDataTransformProperty); }
|
|
set { SetValue(XDataTransformProperty, value); }
|
|
}
|
|
|
|
/// <summary>Gets or sets transform from user data to vertical plot coordinate.
|
|
/// By default transform is <see cref="IdentityDataTransform"/>
|
|
/// </summary>
|
|
[Category("InteractiveDataDisplay")]
|
|
public DataTransform YDataTransform
|
|
{
|
|
get { return (DataTransform)GetValue(YDataTransformProperty); }
|
|
set { SetValue(YDataTransformProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or Sets horizontal scale parameter for plot transform
|
|
/// </summary>
|
|
public double ScaleX
|
|
{
|
|
get
|
|
{
|
|
return IsMaster ? scaleXField : masterField.ScaleX;
|
|
}
|
|
protected set
|
|
{
|
|
scaleXField = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets Horizontal offset for plot transform
|
|
/// </summary>
|
|
public double OffsetX
|
|
{
|
|
get
|
|
{
|
|
return IsMaster ? offsetXField : masterField.OffsetX;
|
|
}
|
|
protected set
|
|
{
|
|
offsetXField = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or Sets horizontal scale parameter for plot transform
|
|
/// </summary>
|
|
public double ScaleY
|
|
{
|
|
get
|
|
{
|
|
return IsMaster ? scaleYField : masterField.ScaleY;
|
|
}
|
|
protected set
|
|
{
|
|
scaleYField = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets Horizontal offset for plot transform
|
|
/// </summary>
|
|
public double OffsetY
|
|
{
|
|
get
|
|
{
|
|
return IsMaster ? offsetYField : masterField.OffsetY;
|
|
}
|
|
protected set
|
|
{
|
|
offsetYField = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets actual plot rectangle which is exactly corresponds to visible part of plot</summary>
|
|
public DataRect ActualPlotRect
|
|
{
|
|
get { return IsMaster ? actualPlotRect : masterField.ActualPlotRect; }
|
|
private set { actualPlotRect = value; }
|
|
}
|
|
|
|
/// <summary>Gets desired plot rectangle. Actual plot rectangle may be larger because of aspect ratio
|
|
/// constraint</summary>
|
|
public DataRect PlotRect
|
|
{
|
|
get { return new DataRect(PlotOriginX, PlotOriginY, PlotOriginX + PlotWidth, PlotOriginY + PlotHeight); }
|
|
}
|
|
|
|
private bool IsInternalChange
|
|
{
|
|
get { return IsMaster ? isInternalChange : masterField.IsInternalChange; }
|
|
set
|
|
{
|
|
if (IsMaster)
|
|
isInternalChange = value;
|
|
else
|
|
masterField.IsInternalChange = value;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Initializes new instance of <see cref="PlotBase"/> class
|
|
/// </summary>
|
|
protected PlotBase()
|
|
{
|
|
XDataTransform = new IdentityDataTransform();
|
|
YDataTransform = new IdentityDataTransform();
|
|
masterField = this;
|
|
Loaded += PlotBaseLoaded;
|
|
Unloaded += PlotBaseUnloaded;
|
|
}
|
|
|
|
|
|
#region dependants tree
|
|
|
|
private List<PlotBase> dependantsField = new List<PlotBase>();
|
|
/// <summary>
|
|
/// Master PlotBase of current PlotBase
|
|
/// </summary>
|
|
protected PlotBase masterField;
|
|
|
|
private void AddDependant(PlotBase dependant)
|
|
{
|
|
if (!dependantsField.Contains(dependant))
|
|
{
|
|
dependantsField.Add(dependant);
|
|
EnumAll(plot => plot.NotifyCompositionChange());
|
|
}
|
|
}
|
|
|
|
private void RemoveDependant(PlotBase dependant)
|
|
{
|
|
if (dependantsField.Contains(dependant))
|
|
{
|
|
dependantsField.Remove(dependant);
|
|
EnumAll(plot => plot.NotifyCompositionChange());
|
|
}
|
|
}
|
|
|
|
private void connectMaster()
|
|
{
|
|
if (!forceMasterField)
|
|
{
|
|
var element = VisualTreeHelper.GetParent(this);
|
|
while (element != null && !(element is PlotBase))
|
|
element = VisualTreeHelper.GetParent(element);
|
|
if (element != null)
|
|
{
|
|
var newMaster = (PlotBase)element;
|
|
|
|
// Take plot-related properties from new master plot
|
|
IsInternalChange = true;
|
|
PlotOriginX = newMaster.PlotOriginX;
|
|
PlotOriginY = newMaster.PlotOriginY;
|
|
PlotWidth = newMaster.PlotWidth;
|
|
PlotHeight = newMaster.PlotHeight;
|
|
IsAutoFitEnabled = newMaster.IsAutoFitEnabled;
|
|
IsInternalChange = false;
|
|
|
|
masterField = newMaster;
|
|
masterField.AddDependant(this);
|
|
InvalidateBounds();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void disconnectMaster()
|
|
{
|
|
if (masterField != this)
|
|
{
|
|
masterField.RemoveDependant(this);
|
|
InvalidateBounds();
|
|
masterField = this;
|
|
}
|
|
}
|
|
|
|
private void EnumLeaves(Action<PlotBase> action)
|
|
{
|
|
action(this);
|
|
foreach (var item in dependantsField)
|
|
{
|
|
item.EnumLeaves(action);
|
|
}
|
|
}
|
|
|
|
private void EnumAll(Action<PlotBase> action)
|
|
{
|
|
if (masterField != this)
|
|
{
|
|
if(masterField != null)
|
|
masterField.EnumAll(action);
|
|
}
|
|
else
|
|
EnumLeaves(action);
|
|
}
|
|
|
|
bool forceMasterField = false;
|
|
|
|
/// <summary>
|
|
/// Disables or enables the connection of the panel to its parent context.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The default value is false which allows automatically connecting to a parent master and switching to a dependant mode.
|
|
/// If set to true the connection is disabled and the pannel is always in a master mode.
|
|
/// </remarks>
|
|
public bool ForceMaster
|
|
{
|
|
get { return forceMasterField; }
|
|
set
|
|
{
|
|
forceMasterField = value;
|
|
if (forceMasterField)
|
|
disconnectMaster();
|
|
else
|
|
connectMaster();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the panel is a master mode or in a dependant mode.
|
|
/// </summary>
|
|
public bool IsMaster { get { return masterField == this; } }
|
|
|
|
private IEnumerable<PlotBase> SelfAndAllDependantPlots
|
|
{
|
|
get
|
|
{
|
|
IEnumerable<PlotBase> result = new PlotBase[] { this };
|
|
foreach (var item in dependantsField)
|
|
{
|
|
result = result.Concat(item.SelfAndAllDependantPlots);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets collections of all related plots of current in composition
|
|
/// </summary>
|
|
public IEnumerable<PlotBase> RelatedPlots
|
|
{
|
|
get
|
|
{
|
|
if (masterField != this)
|
|
return masterField.RelatedPlots;
|
|
else
|
|
{
|
|
IEnumerable<PlotBase> result = new PlotBase[] { this };
|
|
foreach (var item in dependantsField)
|
|
{
|
|
result = result.Concat(item.SelfAndAllDependantPlots);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
private Subject<PlotCompositionChange> compositionChange = new Subject<PlotCompositionChange>();
|
|
|
|
/// <summary>
|
|
/// Raises plots composition changed event
|
|
/// </summary>
|
|
protected void NotifyCompositionChange()
|
|
{
|
|
compositionChange.OnNext(new PlotCompositionChange());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets event which occures when plots composition is changed
|
|
/// </summary>
|
|
public IObservable<PlotCompositionChange> CompositionChange
|
|
{
|
|
get { return compositionChange; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Dependency Properties
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref=" XDataTransform"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty XDataTransformProperty =
|
|
DependencyProperty.Register("XDataTransform", typeof(DataTransform), typeof(PlotBase), new PropertyMetadata(null,
|
|
(o, e) =>
|
|
{
|
|
Plot plot = o as Plot;
|
|
if (plot != null)
|
|
{
|
|
plot.OnXDataTransformChanged(e);
|
|
}
|
|
}));
|
|
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref=" YDataTransform"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty YDataTransformProperty =
|
|
DependencyProperty.Register("YDataTransform", typeof(DataTransform), typeof(PlotBase), new PropertyMetadata(null,
|
|
(o, e) =>
|
|
{
|
|
Plot plot = o as Plot;
|
|
if (plot != null)
|
|
{
|
|
plot.OnYDataTransformChanged(e);
|
|
}
|
|
}));
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref="Padding"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty PaddingProperty =
|
|
DependencyProperty.Register("Padding", typeof(Thickness), typeof(PlotBase), new PropertyMetadata(new Thickness(0.0),
|
|
(o, e) =>
|
|
{
|
|
PlotBase plotBase = (PlotBase)o;
|
|
if (plotBase != null)
|
|
{
|
|
if (!plotBase.IsMaster)
|
|
{
|
|
plotBase.masterField.InvalidateMeasure();
|
|
}
|
|
else
|
|
plotBase.InvalidateMeasure();
|
|
}
|
|
}));
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref="AspectRatio"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty AspectRatioProperty =
|
|
DependencyProperty.Register("AspectRatio", typeof(double), typeof(PlotBase), new PropertyMetadata(0.0,
|
|
(o, e) =>
|
|
{
|
|
PlotBase plotBase = (PlotBase)o;
|
|
if (plotBase != null)
|
|
{
|
|
plotBase.InvalidateMeasure();
|
|
if (!plotBase.IsMaster)
|
|
{
|
|
plotBase.masterField.InvalidateMeasure();
|
|
}
|
|
}
|
|
}));
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref="IsAutoFitEnabled"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty IsAutoFitEnabledProperty =
|
|
DependencyProperty.Register("IsAutoFitEnabled", typeof(bool), typeof(PlotBase), new PropertyMetadata(true,
|
|
(o, e) =>
|
|
{
|
|
PlotBase plotBase = (PlotBase)o;
|
|
if (plotBase != null)
|
|
{
|
|
plotBase.OnIsAutoFitEnabledChanged(e);
|
|
}
|
|
}));
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref="PlotWidth"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty PlotWidthProperty =
|
|
DependencyProperty.Register("PlotWidth", typeof(double), typeof(PlotBase), new PropertyMetadata(1.0,
|
|
(o, e) =>
|
|
{
|
|
PlotBase plotBase = (PlotBase)o;
|
|
if (plotBase != null)
|
|
{
|
|
plotBase.OnPlotWidthChanged(e);
|
|
}
|
|
}));
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref="PlotHeight"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty PlotHeightProperty =
|
|
DependencyProperty.Register("PlotHeight", typeof(double), typeof(PlotBase), new PropertyMetadata(1.0,
|
|
(o, e) =>
|
|
{
|
|
PlotBase plotBase = (PlotBase)o;
|
|
if (plotBase != null)
|
|
{
|
|
plotBase.OnPlotHeightChanged(e);
|
|
}
|
|
}));
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref="PlotOriginX"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty PlotOriginXProperty =
|
|
DependencyProperty.Register("PlotOriginX", typeof(double), typeof(PlotBase), new PropertyMetadata(0.0,
|
|
(o, e) =>
|
|
{
|
|
PlotBase plotBase = (PlotBase)o;
|
|
if (plotBase != null)
|
|
{
|
|
plotBase.OnPlotOriginXChanged(e);
|
|
}
|
|
}));
|
|
|
|
/// <summary>
|
|
/// Identifies <see cref="PlotOriginY"/> dependency property
|
|
/// </summary>
|
|
public static readonly DependencyProperty PlotOriginYProperty =
|
|
DependencyProperty.Register("PlotOriginY", typeof(double), typeof(PlotBase), new PropertyMetadata(0.0,
|
|
(o, e) =>
|
|
{
|
|
PlotBase plotBase = (PlotBase)o;
|
|
if (plotBase != null)
|
|
{
|
|
plotBase.OnPlotOriginYChanged(e);
|
|
}
|
|
}));
|
|
|
|
/// <summary>Enables or disables clipping of graphic elements that are outside plotting area</summary>
|
|
public bool ClipToBounds
|
|
{
|
|
get { return (bool)GetValue(ClipToBoundsProperty); }
|
|
set { SetValue(ClipToBoundsProperty, value); }
|
|
}
|
|
|
|
/// <summary>Identifies <see cref="ClipToBounds"/> dependency property</summary>
|
|
public static readonly DependencyProperty ClipToBoundsProperty =
|
|
DependencyProperty.Register("ClipToBounds", typeof(bool), typeof(PlotBase), new PropertyMetadata(true,
|
|
(s, a) => ((PlotBase)s).OnClipToBoundsChanged(a)));
|
|
|
|
/// <summary>
|
|
/// Occurs when <see cref="ClipToBounds"/> property changed
|
|
/// </summary>
|
|
/// <param name="args">PropertyChanged parameters</param>
|
|
protected virtual void OnClipToBoundsChanged(DependencyPropertyChangedEventArgs args)
|
|
{
|
|
InvalidateMeasure();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// Handler for <see cref="PlotOriginX"/> property changes
|
|
/// </summary>
|
|
protected virtual void OnPlotOriginXChanged(DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (!IsInternalChange)
|
|
{
|
|
IsInternalChange = true;
|
|
EnumAll(p =>
|
|
{
|
|
p.PlotOriginX = (double)e.NewValue;
|
|
p.IsAutoFitEnabled = false;
|
|
p.InvalidateMeasure();
|
|
});
|
|
IsInternalChange = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for <see cref="PlotOriginY"/> property changes
|
|
/// </summary>
|
|
protected virtual void OnPlotOriginYChanged(DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (!IsInternalChange)
|
|
{
|
|
IsInternalChange = true;
|
|
EnumAll(p =>
|
|
{
|
|
p.PlotOriginY = (double)e.NewValue;
|
|
p.IsAutoFitEnabled = false;
|
|
p.InvalidateMeasure();
|
|
});
|
|
IsInternalChange = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for <see cref="PlotWidth"/> property changes
|
|
/// </summary>
|
|
protected virtual void OnPlotWidthChanged(DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (!IsInternalChange)
|
|
{
|
|
IsInternalChange = true;
|
|
EnumAll(p =>
|
|
{
|
|
p.PlotWidth = (double)e.NewValue;
|
|
p.IsAutoFitEnabled = false;
|
|
p.InvalidateMeasure();
|
|
});
|
|
IsInternalChange = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for <see cref="PlotHeight"/> property changes
|
|
/// </summary>
|
|
protected virtual void OnPlotHeightChanged(DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (!IsInternalChange)
|
|
{
|
|
IsInternalChange = true;
|
|
EnumAll(p =>
|
|
{
|
|
p.PlotHeight = (double)e.NewValue;
|
|
p.IsAutoFitEnabled = false;
|
|
p.InvalidateMeasure();
|
|
});
|
|
IsInternalChange = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for <see cref="XDataTransform"/> property changes
|
|
/// </summary>
|
|
protected virtual void OnXDataTransformChanged(DependencyPropertyChangedEventArgs e)
|
|
{
|
|
InvalidateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for <see cref="YDataTransform"/> property changes
|
|
/// </summary>
|
|
protected virtual void OnYDataTransformChanged(DependencyPropertyChangedEventArgs e)
|
|
{
|
|
InvalidateBounds();
|
|
}
|
|
|
|
private void OnIsAutoFitEnabledChanged(DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (!IsInternalChange)
|
|
{
|
|
IsInternalChange = true;
|
|
EnumAll(p =>
|
|
{
|
|
p.IsAutoFitEnabled = (bool)e.NewValue;
|
|
if (p.IsAutoFitEnabled)
|
|
p.InvalidateMeasure();
|
|
});
|
|
IsInternalChange = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the range of {x,y} plot coordinates that correspond to all elements produced by the plot.
|
|
/// </summary>
|
|
/// <returns>The <see cref="DataRect"/> structure that holds the ranges of {x} and {y} plot coordinates or the <see cref="DataRect.Empty"/> value.</returns>
|
|
protected virtual DataRect ComputeBounds()
|
|
{
|
|
return DataRect.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalidates effective plot coordinate ranges. This usually schedules recalculation of plot layout.
|
|
/// </summary>
|
|
public virtual void InvalidateBounds()
|
|
{
|
|
EnumAll(p => p.InvalidateMeasure());
|
|
}
|
|
|
|
/// <summary>
|
|
/// A common padding used by all graphing elements in the dependant tree.
|
|
/// </summary>
|
|
/// <remarks>This is computed by traversing the master-dependant tree and computing the maximum mapping.</remarks>
|
|
public Thickness ActualPadding { get { return IsMaster ? AggregatePadding() : masterField.ActualPadding; } }
|
|
|
|
private double GetEffectiveAspectRatio()
|
|
{
|
|
double result = AspectRatio;
|
|
for (int i = 0; i < dependantsField.Count && result <= 0; i++)
|
|
result = dependantsField[i].GetEffectiveAspectRatio();
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes padding of current <see cref="PlotBase"/> instance
|
|
/// </summary>
|
|
/// <returns>Padding of current <see cref="PlotBase"/> instance</returns>
|
|
protected virtual Thickness ComputePadding()
|
|
{
|
|
return Padding;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes padding with maximum values for each side from padding of current <see cref="PlotBase"/> instance and of all child <see cref="PlotBase"/> instances
|
|
/// </summary>
|
|
/// <returns>Padding with maximum values for each side from padding of current <see cref="PlotBase"/> instance and of all child <see cref="PlotBase"/> instances</returns>
|
|
protected virtual Thickness AggregatePadding()
|
|
{
|
|
Thickness result = ComputePadding();
|
|
foreach (var item in dependantsField)
|
|
{
|
|
Thickness their = item.AggregatePadding();
|
|
if (their.Left > result.Left) result.Left = their.Left;
|
|
if (their.Right > result.Right) result.Right = their.Right;
|
|
if (their.Top > result.Top) result.Top = their.Top;
|
|
if (their.Bottom > result.Bottom) result.Bottom = their.Bottom;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes minimal plot rectangle which contains plot rectangles of current <see cref="PlotBase"/> instance and of all child <see cref="PlotBase"/> instances
|
|
/// </summary>
|
|
/// <returns>Minimal plot rectangle which contains plot rectangles of current <see cref="PlotBase"/> instance and of all child <see cref="PlotBase"/> instances</returns>
|
|
protected DataRect AggregateBounds()
|
|
{
|
|
DataRect result = ComputeBounds();
|
|
foreach (var item in dependantsField)
|
|
result.Surround(item.AggregateBounds());
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>This event is fired when transform from plot to screen coordinates changes.</summary>
|
|
public event EventHandler PlotTransformChanged;
|
|
|
|
/// <summary>
|
|
/// For a given range of {x,y} plot coordinates and a given screen size taking into account effective aspect ratio and effective padding computes navigation transform.
|
|
/// </summary>
|
|
/// <param name="plotBounds">The range of {x,y} plot coordinates that must fit into the screen.</param>
|
|
/// <param name="screenSize">The width and height of the screen that must fit plotBounds.</param>
|
|
internal void Fit(DataRect plotBounds, Size screenSize)
|
|
{
|
|
var padding = AggregatePadding();
|
|
var screenWidth = Math.Max(1.0, screenSize.Width - padding.Left - padding.Right);
|
|
var screenHeight = Math.Max(1.0, screenSize.Height - padding.Top - padding.Bottom);
|
|
var plotWidth = plotBounds.X.IsEmpty ? 0.0 : plotBounds.X.Max - plotBounds.X.Min;
|
|
if (plotWidth <= 0) plotWidth = 1.0;
|
|
var plotHeight = plotBounds.Y.IsEmpty ? 0.0 : plotBounds.Y.Max - plotBounds.Y.Min;
|
|
if (plotHeight <= 0) plotHeight = 1.0;
|
|
|
|
ScaleX = screenWidth / plotWidth;
|
|
ScaleY = screenHeight / plotHeight;
|
|
|
|
var aspect = GetEffectiveAspectRatio();
|
|
if (aspect > 0)
|
|
{
|
|
if (aspect * ScaleY < ScaleX)
|
|
ScaleX = aspect * ScaleY;
|
|
else
|
|
ScaleY = ScaleX / aspect;
|
|
}
|
|
|
|
OffsetX = plotBounds.X.IsEmpty ? 0.0 : ((plotBounds.X.Min + plotBounds.X.Max) * ScaleX - screenWidth) * 0.5 - padding.Left;
|
|
OffsetY = plotBounds.Y.IsEmpty ? screenSize.Height : ((plotBounds.Y.Min + plotBounds.Y.Max) * ScaleY + screenHeight) * 0.5 + padding.Top;
|
|
|
|
ActualPlotRect = new DataRect(
|
|
XFromLeft(screenSize.Width > padding.Left + padding.Right ? 0 : screenSize.Width * 0.5 - 0.5),
|
|
YFromTop(screenSize.Height > padding.Top + padding.Bottom ? screenSize.Height : screenSize.Height * 0.5 + 0.5),
|
|
XFromLeft(screenSize.Width > padding.Left + padding.Right ? screenSize.Width : screenSize.Width * 0.5 + 0.5),
|
|
YFromTop(screenSize.Height > padding.Top + padding.Bottom ? 0 : screenSize.Height * 0.5 - 0.5));
|
|
|
|
EnumAll(p =>
|
|
{
|
|
if (p.PlotTransformChanged != null)
|
|
p.PlotTransformChanged(this, EventArgs.Empty);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets plot rectangle for current instance of <see cref="PlotBase"/>
|
|
/// </summary>
|
|
/// <param name="plotRect">plot rectangle value that would be set for current instance of <see cref="PlotBase"/></param>
|
|
/// <param name="fromAutoFit">Identifies that it is internal call</param>
|
|
protected void SetPlotRect(DataRect plotRect, bool fromAutoFit)
|
|
{
|
|
IsInternalChange = true;
|
|
|
|
EnumAll(p =>
|
|
{
|
|
p.PlotOriginX = plotRect.XMin;
|
|
p.PlotOriginY = plotRect.YMin;
|
|
p.PlotWidth = plotRect.Width;
|
|
p.PlotHeight = plotRect.Height;
|
|
if (!fromAutoFit)
|
|
{
|
|
p.IsAutoFitEnabled = false;
|
|
p.InvalidateMeasure();
|
|
}
|
|
});
|
|
|
|
IsInternalChange = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets plot rectangle for current instance of <see cref="PlotBase"/>
|
|
/// </summary>
|
|
/// <param name="plotRect">plot rectangle value that would be set for current instance of <see cref="PlotBase"/></param>
|
|
public void SetPlotRect(DataRect plotRect)
|
|
{
|
|
SetPlotRect(plotRect, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs horizontal transform from plot coordinates to screen coordinates
|
|
/// </summary>
|
|
/// <param name="x">Value in plot coordinates</param>
|
|
/// <returns>Value in screen coordinates</returns>
|
|
public double LeftFromX(double x)
|
|
{
|
|
return ScaleX > 0 ? x * ScaleX - OffsetX : x;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs vertical transform from plot coordinates to screen coordinates
|
|
/// </summary>
|
|
/// <param name="y">Value in plot coordinates</param>
|
|
/// <returns>Value in screen coordinates</returns>
|
|
public double TopFromY(double y)
|
|
{
|
|
return ScaleY > 0 ? OffsetY - y * ScaleY : OffsetY - y;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Horizontal transform from screen coordinates to plot coordinates
|
|
/// </summary>
|
|
/// <param name="left">Value in screen coordinates</param>
|
|
/// <returns>Value in plot coordinates</returns>
|
|
public double XFromLeft(double left)
|
|
{
|
|
return ScaleX > 0 ? (left + OffsetX) / ScaleX : left;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Vertical transform from screen coordinates to plot coordinates
|
|
/// </summary>
|
|
/// <param name="top">Value in screen coordinates</param>
|
|
/// <returns>Value in plot coordinates</returns>
|
|
public double YFromTop(double top)
|
|
{
|
|
return ScaleY > 0 ? (OffsetY - top) / ScaleY : OffsetY - top;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds master plot for specifyied element
|
|
/// </summary>
|
|
/// <param name="element">Element, which mater plot should be found</param>
|
|
/// <returns>Master Plot for specified element</returns>
|
|
public static PlotBase FindMaster(DependencyObject element)
|
|
{
|
|
var result = VisualTreeHelper.GetParent(element);
|
|
|
|
while (result != null && !(result is PlotBase))
|
|
result = VisualTreeHelper.GetParent(result);
|
|
|
|
return result as PlotBase;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when PlotBase is loaded
|
|
/// </summary>
|
|
protected virtual void PlotBaseLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
connectMaster();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when PlotBase is unloaded
|
|
/// </summary>
|
|
protected virtual void PlotBaseUnloaded(object sender, RoutedEventArgs e)
|
|
{
|
|
disconnectMaster();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs measure algorithm if current instance of <see cref="PlotBase"/> is master
|
|
/// </summary>
|
|
/// <param name="availableSize">Availible size for measure</param>
|
|
/// <returns>Desired size for current plot</returns>
|
|
protected Size PerformAsMaster(Size availableSize)
|
|
{
|
|
if (double.IsNaN(availableSize.Width)
|
|
|| double.IsNaN(availableSize.Height)
|
|
|| double.IsInfinity(availableSize.Width)
|
|
|| double.IsInfinity(availableSize.Height))
|
|
availableSize = new Size(100, 100);
|
|
if (IsMaster)
|
|
{
|
|
DataRect desiredRect;
|
|
if (IsAutoFitEnabled)
|
|
{
|
|
desiredRect = AggregateBounds();
|
|
if (desiredRect.IsEmpty)
|
|
desiredRect = new DataRect(0, 0, 1, 1);
|
|
|
|
SetPlotRect(desiredRect, true);
|
|
}
|
|
else // resize
|
|
desiredRect = PlotRect;
|
|
Fit(desiredRect, availableSize);
|
|
}
|
|
return availableSize;
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
}
|
|
}
|
|
|