// 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; namespace InteractiveDataDisplay.WPF { /// Represents a base for all elements that perform graphic plotting. Handles coordinate /// transformations and manages composition of plots 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 /// 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. [Category("InteractiveDataDisplay")] public Thickness Padding { get { return (Thickness)GetValue(PaddingProperty); } set { SetValue(PaddingProperty, value); } } /// /// Desired ratio of horizontal scale to vertical scale. Values less than or equal to zero /// represent unspecified aspect ratio. /// [Category("InteractiveDataDisplay")] public double AspectRatio { get { return (double)GetValue(AspectRatioProperty); } set { SetValue(AspectRatioProperty, value); } } /// /// Gets or sets auto fit mode. property of all /// plots in compositions are updated instantly to have same value. /// [Category("InteractiveDataDisplay")] public bool IsAutoFitEnabled { get { return (bool)GetValue(IsAutoFitEnabledProperty); } set { SetValue(IsAutoFitEnabledProperty, value); } } /// Gets or sets desired plot width in plot coordinates. property of all /// plots in compositions are updated instantly to have same value. Settings this property /// turns off auto fit mode. [Category("InteractiveDataDisplay")] public double PlotWidth { get { return (double)GetValue(PlotWidthProperty); } set { SetValue(PlotWidthProperty, value); } } /// Gets or sets desired plot height in plot coordinates. property of all /// plots in compositions are updated instantly to have same value. Settings this property /// turns off auto fit mode. [Category("InteractiveDataDisplay")] public double PlotHeight { get { return (double)GetValue(PlotHeightProperty); } set { SetValue(PlotHeightProperty, value); } } /// Gets or sets desired minimal visible horizontal coordinate (in plot coordinate system). /// property of all /// plots in compositions are updated instantly to have same value. Settings this property /// turns off auto fit mode. [Category("InteractiveDataDisplay")] public double PlotOriginX { get { return (double)GetValue(PlotOriginXProperty); } set { SetValue(PlotOriginXProperty, value); } } /// Gets or sets desired minimal visible vertical coordinate (in plot coordinate system). /// property of all /// plots in compositions are updated instantly to have same value. Settings this property /// turns off auto fit mode. [Category("InteractiveDataDisplay")] public double PlotOriginY { get { return (double)GetValue(PlotOriginYProperty); } set { SetValue(PlotOriginYProperty, value); } } /// /// Identifies dependency property /// public static readonly DependencyProperty IsXAxisReversedProperty = DependencyProperty.Register("IsXAxisReversed", typeof(bool), typeof(PlotBase), new PropertyMetadata(false)); /// /// Gets or sets a flag indicating whether the x-axis is reversed or not. /// [Category("InteractiveDataDisplay")] public bool IsXAxisReversed { get { return (bool)GetValue(IsXAxisReversedProperty); } set { SetValue(IsXAxisReversedProperty, value); } } /// /// Identifies dependency property /// public static readonly DependencyProperty IsYAxisReversedProperty = DependencyProperty.Register("IsYAxisReversed", typeof(bool), typeof(PlotBase), new PropertyMetadata(false)); /// /// Gets or sets a flag indicating whether the y-axis is reversed or not. /// [Category("InteractiveDataDisplay")] public bool IsYAxisReversed { get { return (bool)GetValue(IsYAxisReversedProperty); } set { SetValue(IsYAxisReversedProperty, value); } } /// Gets or sets transform from user data to horizontal plot coordinate. /// By default transform is /// [Category("InteractiveDataDisplay")] public DataTransform XDataTransform { get { return (DataTransform)GetValue(XDataTransformProperty); } set { SetValue(XDataTransformProperty, value); } } /// Gets or sets transform from user data to vertical plot coordinate. /// By default transform is /// [Category("InteractiveDataDisplay")] public DataTransform YDataTransform { get { return (DataTransform)GetValue(YDataTransformProperty); } set { SetValue(YDataTransformProperty, value); } } /// /// Gets or Sets horizontal scale parameter for plot transform /// public double ScaleX { get { return IsMaster ? scaleXField : masterField.ScaleX; } protected set { scaleXField = value; } } /// /// Gets or sets Horizontal offset for plot transform /// public double OffsetX { get { return IsMaster ? offsetXField : masterField.OffsetX; } protected set { offsetXField = value; } } /// /// Gets or Sets horizontal scale parameter for plot transform /// public double ScaleY { get { return IsMaster ? scaleYField : masterField.ScaleY; } protected set { scaleYField = value; } } /// /// Gets or sets Horizontal offset for plot transform /// public double OffsetY { get { return IsMaster ? offsetYField : masterField.OffsetY; } protected set { offsetYField = value; } } /// Gets actual plot rectangle which is exactly corresponds to visible part of plot public DataRect ActualPlotRect { get { return IsMaster ? actualPlotRect : masterField.ActualPlotRect; } private set { actualPlotRect = value; } } /// Gets desired plot rectangle. Actual plot rectangle may be larger because of aspect ratio /// constraint 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 /// /// Initializes new instance of class /// protected PlotBase() { XDataTransform = new IdentityDataTransform(); YDataTransform = new IdentityDataTransform(); masterField = this; Loaded += PlotBaseLoaded; Unloaded += PlotBaseUnloaded; } #region dependants tree private List dependantsField = new List(); /// /// Master PlotBase of current PlotBase /// 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 action) { action(this); foreach (var item in dependantsField) { item.EnumLeaves(action); } } private void EnumAll(Action action) { if (masterField != this) { if(masterField != null) masterField.EnumAll(action); } else EnumLeaves(action); } bool forceMasterField = false; /// /// Disables or enables the connection of the panel to its parent context. /// /// /// 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. /// public bool ForceMaster { get { return forceMasterField; } set { forceMasterField = value; if (forceMasterField) disconnectMaster(); else connectMaster(); } } /// /// Gets a value indicating whether the panel is a master mode or in a dependant mode. /// public bool IsMaster { get { return masterField == this; } } private IEnumerable SelfAndAllDependantPlots { get { IEnumerable result = new PlotBase[] { this }; foreach (var item in dependantsField) { result = result.Concat(item.SelfAndAllDependantPlots); } return result; } } /// /// Gets collections of all related plots of current in composition /// public IEnumerable RelatedPlots { get { if (masterField != this) return masterField.RelatedPlots; else { IEnumerable result = new PlotBase[] { this }; foreach (var item in dependantsField) { result = result.Concat(item.SelfAndAllDependantPlots); } return result; } } } /// /// Raises plots composition changed event /// protected void NotifyCompositionChange() { //compositionChange.OnNext(new PlotCompositionChange()); } /// /// Gets event which occures when plots composition is changed /// public IObservable CompositionChange { get { return null; } } #endregion #region Dependency Properties /// /// Identifies dependency property /// 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); } })); /// /// Identifies dependency property /// 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); } })); /// /// Identifies dependency property /// 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(); } })); /// /// Identifies dependency property /// 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(); } } })); /// /// Identifies dependency property /// 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); } })); /// /// Identifies dependency property /// 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); } })); /// /// Identifies dependency property /// 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); } })); /// /// Identifies dependency property /// 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); } })); /// /// Identifies dependency property /// 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); } })); /// Enables or disables clipping of graphic elements that are outside plotting area public bool ClipToBounds { get { return (bool)GetValue(ClipToBoundsProperty); } set { SetValue(ClipToBoundsProperty, value); } } /// Identifies dependency property public static readonly DependencyProperty ClipToBoundsProperty = DependencyProperty.Register("ClipToBounds", typeof(bool), typeof(PlotBase), new PropertyMetadata(true, (s, a) => ((PlotBase)s).OnClipToBoundsChanged(a))); /// /// Occurs when property changed /// /// PropertyChanged parameters protected virtual void OnClipToBoundsChanged(DependencyPropertyChangedEventArgs args) { InvalidateMeasure(); } #endregion #region Methods /// /// Handler for property changes /// protected virtual void OnPlotOriginXChanged(DependencyPropertyChangedEventArgs e) { if (!IsInternalChange) { IsInternalChange = true; EnumAll(p => { p.PlotOriginX = (double)e.NewValue; p.IsAutoFitEnabled = false; p.InvalidateMeasure(); }); IsInternalChange = false; } } /// /// Handler for property changes /// protected virtual void OnPlotOriginYChanged(DependencyPropertyChangedEventArgs e) { if (!IsInternalChange) { IsInternalChange = true; EnumAll(p => { p.PlotOriginY = (double)e.NewValue; p.IsAutoFitEnabled = false; p.InvalidateMeasure(); }); IsInternalChange = false; } } /// /// Handler for property changes /// protected virtual void OnPlotWidthChanged(DependencyPropertyChangedEventArgs e) { if (!IsInternalChange) { IsInternalChange = true; EnumAll(p => { p.PlotWidth = (double)e.NewValue; p.IsAutoFitEnabled = false; p.InvalidateMeasure(); }); IsInternalChange = false; } } /// /// Handler for property changes /// protected virtual void OnPlotHeightChanged(DependencyPropertyChangedEventArgs e) { if (!IsInternalChange) { IsInternalChange = true; EnumAll(p => { p.PlotHeight = (double)e.NewValue; p.IsAutoFitEnabled = false; p.InvalidateMeasure(); }); IsInternalChange = false; } } /// /// Handler for property changes /// protected virtual void OnXDataTransformChanged(DependencyPropertyChangedEventArgs e) { InvalidateBounds(); } /// /// Handler for property changes /// 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; } } /// /// Gets the range of {x,y} plot coordinates that correspond to all elements produced by the plot. /// /// The structure that holds the ranges of {x} and {y} plot coordinates or the value. protected virtual DataRect ComputeBounds() { return DataRect.Empty; } /// /// Invalidates effective plot coordinate ranges. This usually schedules recalculation of plot layout. /// public virtual void InvalidateBounds() { EnumAll(p => p.InvalidateMeasure()); } /// /// A common padding used by all graphing elements in the dependant tree. /// /// This is computed by traversing the master-dependant tree and computing the maximum mapping. 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; } /// /// Computes padding of current instance /// /// Padding of current instance protected virtual Thickness ComputePadding() { return Padding; } /// /// Computes padding with maximum values for each side from padding of current instance and of all child instances /// /// Padding with maximum values for each side from padding of current instance and of all child instances 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; } /// /// Computes minimal plot rectangle which contains plot rectangles of current instance and of all child instances /// /// Minimal plot rectangle which contains plot rectangles of current instance and of all child instances protected DataRect AggregateBounds() { DataRect result = ComputeBounds(); foreach (var item in dependantsField) result.Surround(item.AggregateBounds()); return result; } /// This event is fired when transform from plot to screen coordinates changes. public event EventHandler PlotTransformChanged; /// /// 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. /// /// The range of {x,y} plot coordinates that must fit into the screen. /// The width and height of the screen that must fit plotBounds. 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); }); } /// /// Sets plot rectangle for current instance of /// /// plot rectangle value that would be set for current instance of /// Identifies that it is internal call 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; } /// /// Sets plot rectangle for current instance of /// /// plot rectangle value that would be set for current instance of public void SetPlotRect(DataRect plotRect) { SetPlotRect(plotRect, false); } /// /// Performs horizontal transform from plot coordinates to screen coordinates /// /// Value in plot coordinates /// Value in screen coordinates public double LeftFromX(double x) { return ScaleX > 0 ? x * ScaleX - OffsetX : x; } /// /// Performs vertical transform from plot coordinates to screen coordinates /// /// Value in plot coordinates /// Value in screen coordinates public double TopFromY(double y) { return ScaleY > 0 ? OffsetY - y * ScaleY : OffsetY - y; } /// /// Horizontal transform from screen coordinates to plot coordinates /// /// Value in screen coordinates /// Value in plot coordinates public double XFromLeft(double left) { return ScaleX > 0 ? (left + OffsetX) / ScaleX : left; } /// /// Vertical transform from screen coordinates to plot coordinates /// /// Value in screen coordinates /// Value in plot coordinates public double YFromTop(double top) { return ScaleY > 0 ? (OffsetY - top) / ScaleY : OffsetY - top; } /// /// Finds master plot for specifyied element /// /// Element, which mater plot should be found /// Master Plot for specified element 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; } /// /// Occurs when PlotBase is loaded /// protected virtual void PlotBaseLoaded(object sender, RoutedEventArgs e) { connectMaster(); } /// /// Occurs when PlotBase is unloaded /// protected virtual void PlotBaseUnloaded(object sender, RoutedEventArgs e) { disconnectMaster(); } /// /// Performs measure algorithm if current instance of is master /// /// Availible size for measure /// Desired size for current plot 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 } }