//copyright(c) 2016 Alberto Rodriguez //Permission is hereby granted, free of charge, to any person obtaining a copy //of this software and associated documentation files (the "Software"), to deal //in the Software without restriction, including without limitation the rights //to use, copy, modify, merge, publish, distribute, sublicense, and/or sell //copies of the Software, and to permit persons to whom the Software is //furnished to do so, subject to the following conditions: //The above copyright notice and this permission notice shall be included in all //copies or substantial portions of the Software. //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Windows.Input; using Windows.Foundation; using Windows.UI; using Windows.UI.Text; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Shapes; using LiveCharts.Charts; using LiveCharts.Definitions.Charts; using LiveCharts.Dtos; using LiveCharts.Events; using LiveCharts.Uwp.Components; namespace LiveCharts.Uwp { /// /// An Axis of a chart /// public class Axis : FrameworkElement, IAxisView { #region Constructors /// /// Initializes a new instance of Axis class /// public Axis() { TitleBlock = BindATextBlock(); this.SetIfNotSet(SeparatorProperty, new Separator()); this.SetIfNotSet(SectionsProperty, new SectionsCollection()); TitleBlock.SetBinding(TextBlock.TextProperty, new Binding {Path = new PropertyPath("Title"), Source = this}); } #endregion #region Events /// /// Occurs when an axis range changes by an user action (zooming or panning) /// public event RangeChangedHandler RangeChanged; /// /// The range changed command property /// public static readonly DependencyProperty RangeChangedCommandProperty = DependencyProperty.Register( "RangeChangedCommand", typeof(ICommand), typeof(Axis), new PropertyMetadata(default(ICommand))); /// /// Gets or sets the command to execute when an axis range changes by an user action (zooming or panning) /// /// /// The range changed command. /// public ICommand RangeChangedCommand { get { return (ICommand)GetValue(RangeChangedCommandProperty); } set { SetValue(RangeChangedCommandProperty, value); } } /// /// Occurs before an axis range changes by an user action (zooming or panning) /// public event PreviewRangeChangedHandler PreviewRangeChanged; /// /// The preview range changed command property /// public static readonly DependencyProperty PreviewRangeChangedCommandProperty = DependencyProperty.Register( "PreviewRangeChangedCommand", typeof(ICommand), typeof(Axis), new PropertyMetadata(default(ICommand))); /// /// Gets or sets the command to execute before an axis range changes by an user action (zooming or panning) /// /// /// The preview range changed command. /// public ICommand PreviewRangeChangedCommand { get { return (ICommand)GetValue(PreviewRangeChangedCommandProperty); } set { SetValue(PreviewRangeChangedCommandProperty, value); } } #endregion #region properties private TextBlock TitleBlock { get; set; } /// /// Gets the Model of the axis, the model is used a DTO to communicate with the core of the library. /// public AxisCore Model { get; set; } /// /// Gets previous Max Value /// public double PreviousMaxValue { get; internal set; } /// /// Gets previous Min Value /// public double PreviousMinValue { get; internal set; } #endregion #region Dependency Properties /// /// The labels property /// public static readonly DependencyProperty LabelsProperty = DependencyProperty.Register( "Labels", typeof (IList), typeof (Axis), new PropertyMetadata(default(IList), UpdateChart())); /// /// Gets or sets axis labels, labels property stores the array to map for each index and value, for example if axis value is 0 then label will be labels[0], when value 1 then labels[1], value 2 then labels[2], ..., value n labels[n], use this property instead of a formatter when there is no conversion between value and label for example names, if you are plotting sales vs salesman name. /// //[TypeConverter(typeof(StringCollectionConverter))] public IList Labels { get { return (IList) GetValue(LabelsProperty); } set { SetValue(LabelsProperty, value); } } /// /// The sections property /// public static readonly DependencyProperty SectionsProperty = DependencyProperty.Register( "Sections", typeof (SectionsCollection), typeof (Axis), new PropertyMetadata(default(SectionsCollection))); /// /// Gets or sets the axis sectionsCollection, a section is useful to highlight ranges or values in a chart. /// public SectionsCollection Sections { get { return (SectionsCollection) GetValue(SectionsProperty); } set { SetValue(SectionsProperty, value); } } /// /// The label formatter property /// public static readonly DependencyProperty LabelFormatterProperty = DependencyProperty.Register( "LabelFormatter", typeof (Func), typeof (Axis), new PropertyMetadata(default(Func), UpdateChart())); /// /// Gets or sets the function to convert a value to label, for example when you need to display your chart as currency ($1.00) or as degrees (10°), if Labels property is not null then formatter is ignored, and label will be pulled from Labels prop. /// public Func LabelFormatter { get { return (Func) GetValue(LabelFormatterProperty); } set { SetValue(LabelFormatterProperty, value); } } /// /// The separator property /// public static readonly DependencyProperty SeparatorProperty = DependencyProperty.Register( "Separator", typeof (Separator), typeof (Axis), new PropertyMetadata(default(Separator), UpdateChart())); /// /// Get or sets configuration for parallel lines to axis. /// public Separator Separator { get { return (Separator) GetValue(SeparatorProperty); } set { SetValue(SeparatorProperty, value); } } /// /// The show labels property /// public static readonly DependencyProperty ShowLabelsProperty = DependencyProperty.Register( "ShowLabels", typeof (bool), typeof (Axis), new PropertyMetadata(true, LabelsVisibilityChanged)); /// /// Gets or sets if labels are shown in the axis. /// public bool ShowLabels { get { return (bool) GetValue(ShowLabelsProperty); } set { SetValue(ShowLabelsProperty, value); } } /// /// The maximum value property /// public static readonly DependencyProperty MaxValueProperty = DependencyProperty.Register( "MaxValue", typeof (double), typeof (Axis), new PropertyMetadata(double.NaN, UpdateChart())); /// /// Gets or sets axis max value, set it to double.NaN to make this property Auto, default value is double.NaN /// public double MaxValue { get { return (double) GetValue(MaxValueProperty); } set { SetValue(MaxValueProperty, value); } } /// /// The minimum value property /// public static readonly DependencyProperty MinValueProperty = DependencyProperty.Register( "MinValue", typeof (double), typeof (Axis), new PropertyMetadata(double.NaN, UpdateChart())); /// /// Gets or sets axis min value, set it to double.NaN to make this property Auto, default value is double.NaN /// public double MinValue { get { return (double)GetValue(MinValueProperty); } set { SetValue(MinValueProperty, value); } } /// /// Gets the actual minimum value. /// /// /// The actual minimum value. /// public double ActualMinValue => Model.BotLimit; /// /// Gets the actual maximum value. /// /// /// The actual maximum value. /// public double ActualMaxValue => Model.TopLimit; /// /// The minimum range property /// public static readonly DependencyProperty MinRangeProperty = DependencyProperty.Register( "MinRange", typeof(double), typeof(Axis), new PropertyMetadata(double.MinValue)); /// /// Gets or sets the min range this axis can display, useful to limit user zooming. /// public double MinRange { get { return (double) GetValue(MinRangeProperty); } set { SetValue(MinRangeProperty, value); } } /// /// The maximum range property /// public static readonly DependencyProperty MaxRangeProperty = DependencyProperty.Register( "MaxRange", typeof(double), typeof(Axis), new PropertyMetadata(double.MaxValue)); /// /// Gets or sets the max range this axis can display, useful to limit user zooming. /// public double MaxRange { get { return (double) GetValue(MaxRangeProperty); } set { SetValue(MaxRangeProperty, value); } } /// /// The title property /// public static readonly DependencyProperty TitleProperty = DependencyProperty.Register( "Title", typeof(string), typeof(Axis), new PropertyMetadata(null, UpdateChart())); /// /// Gets or sets axis title, the title will be displayed only if this property is not null, default is null. /// public string Title { get { return (string)GetValue(TitleProperty); } set { SetValue(TitleProperty, value); } } /// /// The position property /// public static readonly DependencyProperty PositionProperty = DependencyProperty.Register( "Position", typeof (AxisPosition), typeof (Axis), new PropertyMetadata(default(AxisPosition), UpdateChart())); /// /// Gets or sets the axis position, default is Axis.Position.LeftBottom, when the axis is at Y and Position is LeftBottom, then axis will be placed at left, RightTop position will place it at Right, when the axis is at X and position LeftBottom, the axis will be placed at bottom, if position is RightTop then it will be placed at top. /// public AxisPosition Position { get { return (AxisPosition) GetValue(PositionProperty); } set { SetValue(PositionProperty, value); } } /// /// The is merged property /// public static readonly DependencyProperty IsMergedProperty = DependencyProperty.Register( "IsMerged", typeof (bool), typeof (Axis), new PropertyMetadata(default(bool), UpdateChart())); /// /// Gets or sets if the axis labels should me placed inside the chart, this is useful to save some space. /// public bool IsMerged { get { return (bool) GetValue(IsMergedProperty); } set { SetValue(IsMergedProperty, value); } } /// /// The bar unit property /// public static readonly DependencyProperty BarUnitProperty = DependencyProperty.Register( "BarUnit", typeof(double), typeof(Axis), new PropertyMetadata(double.NaN)); /// /// Gets or sets the bar's series unit width (rows and columns), this property specifies the value in the chart that any bar should take as width. /// [Obsolete("This property was renamed, please use Unit property instead.")] public double BarUnit { get { return (double) GetValue(BarUnitProperty); } set { SetValue(BarUnitProperty, value); } } /// /// The unit property /// public static readonly DependencyProperty UnitProperty = DependencyProperty.Register( "Unit", typeof(double), typeof(Axis), new PropertyMetadata(double.NaN)); /// /// Gets or sets the axis unit, setting this property to your actual scale unit (seconds, minutes or any other scale) helps you to fix possible visual issues. /// /// /// The unit. /// public double Unit { get { return (double)GetValue(UnitProperty); } set { SetValue(UnitProperty, value); } } /// /// The disable animations property /// public static readonly DependencyProperty DisableAnimationsProperty = DependencyProperty.Register( "DisableAnimations", typeof (bool), typeof (Axis), new PropertyMetadata(default(bool), UpdateChart(true))); /// /// Gets or sets if the axis is animated. /// public bool DisableAnimations { get { return (bool) GetValue(DisableAnimationsProperty); } set { SetValue(DisableAnimationsProperty, value); } } /// /// The font family property /// public static readonly DependencyProperty FontFamilyProperty = DependencyProperty.Register("FontFamily", typeof(FontFamily), typeof(Axis), new PropertyMetadata(new FontFamily("Calibri"))); /// /// Gets or sets labels font family, font to use for any label in this axis /// public FontFamily FontFamily { get { return (FontFamily)GetValue(FontFamilyProperty); } set { SetValue(FontFamilyProperty, value); } } /// /// The font size property /// public static readonly DependencyProperty FontSizeProperty = DependencyProperty.Register("FontSize", typeof(double), typeof(Axis), new PropertyMetadata(11.0)); /// /// Gets or sets labels font size /// public double FontSize { get { return (double)GetValue(FontSizeProperty); } set { SetValue(FontSizeProperty, value); } } /// /// The font weight property /// public static readonly DependencyProperty FontWeightProperty = DependencyProperty.Register("FontWeight", typeof(FontWeight), typeof(Axis), new PropertyMetadata(FontWeights.Normal)); /// /// Gets or sets labels font weight /// public FontWeight FontWeight { get { return (FontWeight)GetValue(FontWeightProperty); } set { SetValue(FontWeightProperty, value); } } /// /// The font style property /// public static readonly DependencyProperty FontStyleProperty = DependencyProperty.Register("FontStyle", typeof(FontStyle), typeof(Axis), new PropertyMetadata(FontStyle.Normal)); /// /// Gets or sets labels font style /// public FontStyle FontStyle { get { return (FontStyle)GetValue(FontStyleProperty); } set { SetValue(FontStyleProperty, value); } } /// /// The font stretch property /// public static readonly DependencyProperty FontStretchProperty = DependencyProperty.Register("FontStretch", typeof(FontStretch), typeof(Axis), new PropertyMetadata(FontStretch.Normal)); /// /// Gets or sets labels font stretch /// public FontStretch FontStretch { get { return (FontStretch)GetValue(FontStretchProperty); } set { SetValue(FontStretchProperty, value); } } /// /// The foreground property /// public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register("Foreground", typeof(Brush), typeof(Axis), new PropertyMetadata(new SolidColorBrush(Color.FromArgb(255, 170, 170, 170)))); /// /// Gets or sets labels text color. /// public Brush Foreground { get { return (Brush)GetValue(ForegroundProperty); } set { SetValue(ForegroundProperty, value); } } /// /// The labels rotation property /// public static readonly DependencyProperty LabelsRotationProperty = DependencyProperty.Register( "LabelsRotation", typeof (double), typeof (Axis), new PropertyMetadata(default(double), UpdateChart())); /// /// Gets or sets the labels rotation in the axis, the angle starts as a horizontal line, you can use any angle in degrees, even negatives. /// public double LabelsRotation { get { return (double) GetValue(LabelsRotationProperty); } set { SetValue(LabelsRotationProperty, value); } } /// /// The is enabled property /// public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.Register( "IsEnabled", typeof(bool), typeof(Axis), new PropertyMetadata(default(bool))); /// /// Gets or sets a value indicating whether this instance is enabled. /// /// /// true if this instance is enabled; otherwise, false. /// public bool IsEnabled { get { return (bool) GetValue(IsEnabledProperty); } set { SetValue(IsEnabledProperty, value); } } /// /// The axis orientation property /// public static readonly DependencyProperty AxisOrientationProperty = DependencyProperty.Register( "AxisOrientation", typeof(AxisOrientation), typeof(Axis), new PropertyMetadata(default(AxisOrientation))); /// /// Gets or sets the element orientation ind the axis /// public AxisOrientation AxisOrientation { get { return (AxisOrientation)GetValue(AxisOrientationProperty); } internal set { SetValue(AxisOrientationProperty, value); } } #endregion #region Public Methods /// /// Cleans this instance. /// public void Clean() { if (Model == null) return; Model.ClearSeparators(); Model.Chart.View.RemoveFromView(TitleBlock); Sections.Clear(); TitleBlock = null; } /// /// Sets the range. /// /// The minimum. /// The maximum. public void SetRange(double min, double max) { var bMax = double.IsNaN(MaxValue) ? Model.TopLimit : MaxValue; var bMin = double.IsNaN(MinValue) ? Model.BotLimit : MinValue; var nMax = double.IsNaN(MaxValue) ? Model.TopLimit : MaxValue; var nMin = double.IsNaN(MinValue) ? Model.BotLimit : MinValue; var e = new RangeChangedEventArgs { Range = nMax - nMin, RightLimitChange = bMax - nMax, LeftLimitChange = bMin - nMin, Axis = this }; var pe = new PreviewRangeChangedEventArgs(e) { PreviewMaxValue = max, PreviewMinValue = min }; OnPreviewRangeChanged(pe); if (pe.Cancel) return; MaxValue = max; MinValue = min; Model.Chart.Updater.Run(); OnRangeChanged(e); } /// /// Renders the separator. /// /// The model. /// The chart. public void RenderSeparator(SeparatorElementCore model, ChartCore chart) { AxisSeparatorElement ase; if (model.View == null) { ase = new AxisSeparatorElement(model) { Line = BindALine(), TextBlock = BindATextBlock() }; model.View = ase; chart.View.AddToView(ase.Line); chart.View.AddToView(ase.TextBlock); Canvas.SetZIndex(ase.Line, -1); } else { ase = (AxisSeparatorElement) model.View; } ase.Line.Visibility = !Separator.IsEnabled ? Visibility.Collapsed : Visibility.Visible; ase.TextBlock.Visibility = !ShowLabels ? Visibility.Collapsed : Visibility.Visible; } /// /// Updates the title. /// /// The chart. /// The rotation angle. /// public CoreSize UpdateTitle(ChartCore chart, double rotationAngle = 0) { if (TitleBlock.Parent == null) { if (Math.Abs(rotationAngle) > 1) TitleBlock.RenderTransform = new RotateTransform {Angle = rotationAngle}; chart.View.AddToView(TitleBlock); } TitleBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); return string.IsNullOrWhiteSpace(Title) ? new CoreSize() : new CoreSize(TitleBlock.DesiredSize.Width, TitleBlock.DesiredSize.Height); } /// /// Sets the title top. /// /// The value. public void SetTitleTop(double value) { Canvas.SetTop(TitleBlock, value); } /// /// Sets the title left. /// /// The value. public void SetTitleLeft(double value) { Canvas.SetLeft(TitleBlock, value); } /// /// Gets the title left. /// /// public double GetTitleLeft() { return Canvas.GetLeft(TitleBlock); } /// /// Gets the tile top. /// /// public double GetTileTop() { return Canvas.GetTop(TitleBlock); } /// /// Gets the size of the label. /// /// public CoreSize GetLabelSize() { return new CoreSize(TitleBlock.DesiredSize.Width, TitleBlock.DesiredSize.Height); } /// /// Ases the core element. /// /// The chart. /// The source. /// public virtual AxisCore AsCoreElement(ChartCore chart, AxisOrientation source) { if (Model == null) Model = new AxisCore(this); Model.ShowLabels = ShowLabels; Model.Chart = chart; Model.IsMerged = IsMerged; Model.Labels = Labels; Model.LabelFormatter = LabelFormatter; Model.MaxValue = MaxValue; Model.MinValue = MinValue; Model.Title = Title; Model.Position = Position; Model.Separator = Separator.AsCoreElement(Model, source); Model.DisableAnimations = DisableAnimations; Model.Sections = Sections.Select(x => x.AsCoreElement(Model, source)).ToList(); return Model; } #endregion internal TextBlock BindATextBlock() { var tb = new TextBlock(); tb.SetBinding(TextBlock.FontFamilyProperty, new Binding { Path = new PropertyPath("FontFamily"), Source = this }); tb.SetBinding(TextBlock.FontSizeProperty, new Binding { Path = new PropertyPath("FontSize"), Source = this }); tb.SetBinding(TextBlock.FontStretchProperty, new Binding { Path = new PropertyPath("FontStretch"), Source = this }); tb.SetBinding(TextBlock.FontStyleProperty, new Binding { Path = new PropertyPath("FontStyle"), Source = this }); tb.SetBinding(TextBlock.FontWeightProperty, new Binding { Path = new PropertyPath("FontWeight"), Source = this }); tb.SetBinding(TextBlock.ForegroundProperty, new Binding { Path = new PropertyPath("Foreground"), Source = this }); return tb; } internal Line BindALine() { var l = new Line(); var s = Separator as Separator; if (s == null) return l; l.SetBinding(Shape.StrokeProperty, new Binding {Path = new PropertyPath("Stroke"), Source = s}); try { l.SetBinding(Shape.StrokeDashArrayProperty, new Binding { Path = new PropertyPath("StrokeDashArray"), Source = s }); } catch (Exception) { // temporarily ignore it } l.SetBinding(Shape.StrokeThicknessProperty, new Binding {Path = new PropertyPath("StrokeThickness"), Source = s}); l.SetBinding(VisibilityProperty, new Binding {Path = new PropertyPath("Visibility"), Source = s}); return l; } /// /// Updates the chart. /// /// if set to true [animate]. /// if set to true [update now]. /// protected static PropertyChangedCallback UpdateChart(bool animate = false, bool updateNow = false) { return (o, args) => { var wpfAxis = o as Axis; if (wpfAxis == null) return; wpfAxis.Model?.Chart.Updater.Run(animate, updateNow); }; } private static void LabelsVisibilityChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { var axis = (Axis) dependencyObject; if (axis.Model == null) return; foreach (var separator in axis.Model.CurrentSeparators) { var s = (AxisSeparatorElement) separator.View; s.TextBlock.Visibility = axis.ShowLabels ? Visibility.Visible : Visibility.Collapsed; } UpdateChart()(dependencyObject, dependencyPropertyChangedEventArgs); } /// /// Raises the event. /// /// The instance containing the event data. protected void OnRangeChanged(RangeChangedEventArgs e) { RangeChanged?.Invoke(e); if (RangeChangedCommand != null && RangeChangedCommand.CanExecute(e)) RangeChangedCommand.Execute(e); } /// /// Raises the event. /// /// The instance containing the event data. protected void OnPreviewRangeChanged(PreviewRangeChangedEventArgs e) { PreviewRangeChanged?.Invoke(e); if (PreviewRangeChangedCommand != null && PreviewRangeChangedCommand.CanExecute(e)) PreviewRangeChangedCommand.Execute(e); } } }