//The MIT License(MIT) //Copyright(c) 2016 Alberto Rodriguez & LiveCharts Contributors //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.CodeDom.Compiler; using System.Collections.Generic; using System.Linq; using LiveCharts.Definitions.Charts; using LiveCharts.Definitions.Series; using LiveCharts.Dtos; namespace LiveCharts.Charts { /// /// /// public abstract class ChartCore { #region Constructors /// /// Initializes a new instance of the class. /// /// The view. /// The updater. protected ChartCore(IChartView view, ChartUpdater updater) { View = view; Updater = updater; DrawMargin = new CoreRectangle(); DrawMargin.SetHeight += view.SetDrawMarginHeight; DrawMargin.SetWidth += view.SetDrawMarginWidth; DrawMargin.SetTop += view.SetDrawMarginTop; DrawMargin.SetLeft += view.SetDrawMarginLeft; } /// /// Initializes the class. /// static ChartCore() { Configurations = new Charting(); } #endregion #region Properties /// /// Gets or sets the configurations. /// /// /// The configurations. /// public static Charting Configurations { get; set; } /// /// Gets or sets a value indicating whether [series initialized]. /// /// /// true if [series initialized]; otherwise, false. /// public bool SeriesInitialized { get; set; } /// /// Gets or sets a value indicating whether [are components loaded]. /// /// /// true if [are components loaded]; otherwise, false. /// public bool AreComponentsLoaded { get; set; } /// /// Gets or sets the view. /// /// /// The view. /// public IChartView View { get; set; } /// /// Gets or sets the updater. /// /// /// The updater. /// public ChartUpdater Updater { get; set; } /// /// Gets or sets the size of the control. /// /// /// The size of the control. /// public CoreSize ControlSize { get; set; } /// /// Gets or sets the draw margin. /// /// /// The draw margin. /// public CoreRectangle DrawMargin { get; set; } /// /// Gets or sets a value indicating whether this instance has unitary points. /// /// /// true if this instance has unitary points; otherwise, false. /// public bool HasUnitaryPoints { get; set; } /// /// Gets a value indicating whether [requires hover shape]. /// /// /// true if [requires hover shape]; otherwise, false. /// public bool RequiresHoverShape { get { return View != null && (View.HasTooltip || View.HasDataClickEventAttached || View.Hoverable); } } /// /// Gets or sets the axis x. /// /// /// The axis x. /// public List AxisX { get; set; } /// /// Gets or sets the axis y. /// /// /// The axis y. /// public List AxisY { get; set; } /// /// Gets or sets the x limit. /// /// /// The x limit. /// public CoreLimit XLimit { get; set; } /// /// Gets or sets the y limit. /// /// /// The y limit. /// public CoreLimit YLimit { get; set; } /// /// Gets or sets the w limit. /// /// /// The w limit. /// public CoreLimit WLimit { get; set; } /// /// Gets or sets the index of the current color. /// /// /// The index of the current color. /// public int CurrentColorIndex { get; set; } /// /// Gets or sets the pan origin. /// /// /// The pan origin. /// public CorePoint PanOrigin { get; set; } #endregion #region Public Methods /// /// Prepares the axes. /// public virtual void PrepareAxes() { for (var index = 0; index < AxisX.Count; index++) { SetAxisLimits( AxisX[index], View.ActualSeries // ReSharper disable once AccessToModifiedClosure .Where(series => series.Values != null && series.ScalesXAt == index).ToArray(), AxisOrientation.X); } for (var index = 0; index < AxisY.Count; index++) { SetAxisLimits( AxisY[index], View.ActualSeries // ReSharper disable once AccessToModifiedClosure .Where(series => series.Values != null && series.ScalesYAt == index).ToArray(), AxisOrientation.Y); } } /// /// Runs the specialized chart components. /// public virtual void RunSpecializedChartComponents() { } /// /// Calculates the components and margin. /// public void CalculateComponentsAndMargin() { var curSize = new CoreRectangle(0, 0, ControlSize.Width, ControlSize.Height); curSize = PlaceLegend(curSize); const double padding = 4; for (int index = 0; index < AxisY.Count; index++) { var ax = AxisY[index]; var titleSize = ax.View.UpdateTitle(this, -90d); var biggest = ax.PrepareChart(AxisOrientation.Y, this); var x = curSize.Left; if (ax.Position == AxisPosition.LeftBottom) { ax.View.SetTitleLeft(x); curSize.Left += titleSize.Height + biggest.Width + padding; curSize.Width -= (titleSize.Height + biggest.Width + padding); ax.Tab = curSize.Left; } else { ax.View.SetTitleLeft(x + curSize.Width - titleSize.Height); curSize.Width -= (titleSize.Height + biggest.Width + padding); ax.Tab = curSize.Left + curSize.Width; } var uw = ax.EvaluatesUnitWidth ? ChartFunctions.GetUnitWidth(AxisOrientation.Y, this, index)/2 : 0; var topE = biggest.Top - uw; if (topE> curSize.Top) { var dif = topE - curSize.Top; curSize.Top += dif; curSize.Height -= dif; } var botE = biggest.Bottom - uw; if (botE > ControlSize.Height - (curSize.Top + curSize.Height)) { var dif = botE - (ControlSize.Height - (curSize.Top + curSize.Height)); curSize.Height -= dif; } } for (var index = 0; index < AxisX.Count; index++) { var xi = AxisX[index]; var titleSize = xi.View.UpdateTitle(this); var biggest = xi.PrepareChart(AxisOrientation.X, this); var top = curSize.Top; if (xi.Position == AxisPosition.LeftBottom) { xi.View.SetTitleTop(top + curSize.Height - titleSize.Height); curSize.Height -= (titleSize.Height + biggest.Height); xi.Tab = curSize.Top + curSize.Height; } else { xi.View.SetTitleTop(top); curSize.Top += titleSize.Height + biggest.Height; curSize.Height -= (titleSize.Height + biggest.Height); xi.Tab = curSize.Top; } //Notice the unit width is not exact at this point... var uw = xi.EvaluatesUnitWidth ? ChartFunctions.GetUnitWidth(AxisOrientation.X, this, index)/2 : 0; var leftE = biggest.Left - uw > 0 ? biggest.Left - uw : 0; if (leftE > curSize.Left) { var dif = leftE - curSize.Left; curSize.Left += dif; curSize.Width -= dif; foreach (var correctedAxis in AxisY .Where(correctedAxis => correctedAxis.Position == AxisPosition.LeftBottom)) { correctedAxis.Tab += dif; } } var rightE = biggest.Right - uw > 0 ? biggest.Right - uw : 0; if (rightE > ControlSize.Width - (curSize.Left + curSize.Width)) { var dif = rightE - (ControlSize.Width - (curSize.Left + curSize.Width)); curSize.Width -= dif; foreach (var correctedAxis in AxisY .Where(correctedAxis => correctedAxis.Position == AxisPosition.RightTop)) { correctedAxis.Tab -= dif; } } } DrawMargin.Top = curSize.Top; DrawMargin.Left = curSize.Left; DrawMargin.Width = curSize.Width; DrawMargin.Height = curSize.Height; for (var index = 0; index < AxisY.Count; index++) { var ax = AxisY[index]; var pr = ChartFunctions.FromPlotArea(ax.MaxPointRadius, AxisOrientation.Y, this, index) - ChartFunctions.FromPlotArea(0, AxisOrientation.Y, this, index); if (!double.IsNaN(pr)) { ax.BotLimit += pr; ax.TopLimit -= pr; } ax.UpdateSeparators(AxisOrientation.Y, this, index); ax.View.SetTitleTop(curSize.Top + curSize.Height*.5 + ax.View.GetLabelSize().Width*.5); } for (var index = 0; index < AxisX.Count; index++) { var xi = AxisX[index]; var pr = ChartFunctions.FromPlotArea(xi.MaxPointRadius, AxisOrientation.X, this, index) - ChartFunctions.FromPlotArea(0, AxisOrientation.X, this, index); if (!double.IsNaN(pr)) { xi.BotLimit -= pr; xi.TopLimit += pr; } xi.UpdateSeparators(AxisOrientation.X, this, index); xi.View.SetTitleLeft(curSize.Left + curSize.Width*.5 - xi.View.GetLabelSize().Width*.5); } } /// /// Places the legend. /// /// The draw margin. /// /// public CoreRectangle PlaceLegend(CoreRectangle drawMargin) { var legendSize = View.LoadLegend(); const int padding = 10; switch (View.LegendLocation) { case LegendLocation.None: View.HideLegend(); break; case LegendLocation.Top: drawMargin.Top += legendSize.Height; drawMargin.Height -= legendSize.Height; View.ShowLegend(new CorePoint(ControlSize.Width * .5 - legendSize.Width * .5, 0)); break; case LegendLocation.Bottom: var bot = new CorePoint(ControlSize.Width*.5 - legendSize.Width*.5, ControlSize.Height - legendSize.Height); drawMargin.Height -= legendSize.Height; View.ShowLegend(new CorePoint(bot.X, ControlSize.Height - legendSize.Height)); break; case LegendLocation.Left: drawMargin.Left = drawMargin.Left + legendSize.Width; View.ShowLegend(new CorePoint(0, ControlSize.Height*.5 - legendSize.Height*.5)); break; case LegendLocation.Right: drawMargin.Width -= legendSize.Width + padding; View.ShowLegend(new CorePoint(ControlSize.Width - legendSize.Width, ControlSize.Height*.5 - legendSize.Height*.5)); break; default: throw new ArgumentOutOfRangeException(); } return drawMargin; } /// /// Zooms the in. /// /// The pivot. public void ZoomIn(CorePoint pivot) { if (AxisX == null || AxisY == null) return; View.HideTooltip(); var speed = View.ZoomingSpeed < 0.1 ? 0.1 : (View.ZoomingSpeed > 0.95 ? 0.95 : View.ZoomingSpeed); if (View.Zoom == ZoomingOptions.X || View.Zoom == ZoomingOptions.Xy) { for (var index = 0; index < AxisX.Count; index++) { var xi = AxisX[index]; var px = ChartFunctions.FromPlotArea(pivot.X, AxisOrientation.X, this, index); var max = double.IsNaN(xi.View.MaxValue) ? xi.TopLimit : xi.View.MaxValue; var min = double.IsNaN(xi.View.MinValue) ? xi.BotLimit : xi.View.MinValue; var l = max - min; var rMin = (px - min) / l; var rMax = 1 - rMin; var target = l * speed; if (target < xi.View.MinRange) return; var mint = px - target * rMin; var maxt = px + target * rMax; xi.View.SetRange(mint, maxt); } } if (View.Zoom == ZoomingOptions.Y || View.Zoom == ZoomingOptions.Xy) { for (var index = 0; index < AxisY.Count; index++) { var ax = AxisY[index]; var py = ChartFunctions.FromPlotArea(pivot.Y, AxisOrientation.Y, this, index); var max = double.IsNaN(ax.View.MaxValue) ? ax.TopLimit : ax.View.MaxValue; var min = double.IsNaN(ax.View.MinValue) ? ax.BotLimit : ax.View.MinValue; var l = max - min; var rMin = (py - min) / l; var rMax = 1 - rMin; var target = l * speed; if (target < ax.View.MinRange) return; var mint = py - target * rMin; var maxt = py + target * rMax; ax.View.SetRange(mint, maxt); } } } /// /// Zooms the out. /// /// The pivot. public void ZoomOut(CorePoint pivot) { View.HideTooltip(); var speed = View.ZoomingSpeed < 0.1 ? 0.1 : (View.ZoomingSpeed > 0.95 ? 0.95 : View.ZoomingSpeed); if (View.Zoom == ZoomingOptions.X || View.Zoom == ZoomingOptions.Xy) { for (var index = 0; index < AxisX.Count; index++) { var xi = AxisX[index]; var px = ChartFunctions.FromPlotArea(pivot.X, AxisOrientation.X, this, index); var max = double.IsNaN(xi.View.MaxValue) ? xi.TopLimit : xi.View.MaxValue; var min = double.IsNaN(xi.View.MinValue) ? xi.BotLimit : xi.View.MinValue; var l = max - min; var rMin = (px - min) / l; var rMax = 1 - rMin; var target = l * (1 / speed); if (target > xi.View.MaxRange) return; var mint = px- target * rMin; var maxt = px + target * rMax; xi.View.SetRange(mint, maxt); } } if (View.Zoom == ZoomingOptions.Y || View.Zoom == ZoomingOptions.Xy) { for (var index = 0; index < AxisY.Count; index++) { var ax = AxisY[index]; var py = ChartFunctions.FromPlotArea(pivot.Y, AxisOrientation.Y, this, index); var max = double.IsNaN(ax.View.MaxValue) ? ax.TopLimit : ax.View.MaxValue; var min = double.IsNaN(ax.View.MinValue) ? ax.BotLimit : ax.View.MinValue; var l = max - min; var rMin = (py - min) / l; var rMax = 1 - rMin; var target = l * (1 / speed); if (target > ax.View.MaxRange) return; var mint = py - target * rMin; var maxt = py + target * rMax; ax.View.SetRange(mint, maxt); } } } /// /// Clears the zoom. /// public void ClearZoom() { foreach (var xi in AxisX) xi.View.SetRange(double.NaN, double.NaN); foreach (var ax in AxisY) ax.View.SetRange(double.NaN, double.NaN); } /// /// Drags the specified delta. /// /// The delta. public void Drag(CorePoint delta) { if (View.Pan == PanningOptions.Unset && View.Zoom == ZoomingOptions.None || View.Pan == PanningOptions.None) return; var px = View.Pan == PanningOptions.Unset && (View.Zoom == ZoomingOptions.X || View.Zoom == ZoomingOptions.Xy); px = px || View.Pan == PanningOptions.X || View.Pan == PanningOptions.Xy; if (px) { for (var index = 0; index < AxisX.Count; index++) { var xi = AxisX[index]; var dx = ChartFunctions.FromPlotArea(delta.X, AxisOrientation.X, this, index) - ChartFunctions.FromPlotArea(0, AxisOrientation.X, this, index); xi.View.SetRange((double.IsNaN(xi.View.MinValue) ? xi.BotLimit : xi.View.MinValue) + dx, (double.IsNaN(xi.View.MaxValue) ? xi.TopLimit : xi.View.MaxValue) + dx); } } var py = View.Pan == PanningOptions.Unset && (View.Zoom == ZoomingOptions.Y || View.Zoom == ZoomingOptions.Xy); py = py || View.Pan == PanningOptions.Y || View.Pan == PanningOptions.Xy; if (py) { for (var index = 0; index < AxisY.Count; index++) { var ax = AxisY[index]; var dy = ChartFunctions.FromPlotArea(delta.Y, AxisOrientation.Y, this, index) - ChartFunctions.FromPlotArea(0, AxisOrientation.Y, this, index); ax.View.SetRange((double.IsNaN(ax.View.MinValue) ? ax.BotLimit : ax.View.MinValue) + dy, (double.IsNaN(ax.View.MaxValue) ? ax.TopLimit : ax.View.MaxValue) + dy); } } } #endregion #region Protected /// /// Stacks the points. /// /// The stackables. /// The stack at. /// Index of the stack. /// The mode. protected void StackPoints(IEnumerable stackables, AxisOrientation stackAt, int stackIndex, StackMode mode = StackMode.Values) { var stackedColumns = stackables.SelectMany(x => x.ActualValues.GetPoints(x)) .GroupBy(x => stackAt == AxisOrientation.X ? x.Y : x.X); double mostLeft = 0, mostRight = 0; foreach (var column in stackedColumns) { double sumLeft = 0, sumRight = 0; foreach (var item in column) { var s = stackAt == AxisOrientation.X ? item.X : item.Y; if (s < 0) sumLeft += s; else sumRight += s; } var lastLeft = 0d; var lastRight = 0d; var leftPart = 0d; var rightPart = 0d; foreach (var point in column) { var pulled = stackAt == AxisOrientation.X ? point.X : point.Y; //notice using (pulled < 0) or (pulled <= 0) could cause an issue similar to //https://github.com/beto-rodriguez/Live-Charts/issues/231 //from that issue I changed <= to < //only because it is more common to use positive values than negative //you could face a similar issue if you are stacking only negative values //a work around is forcing (pulled < 0) to be true, //instead of using zero values, use -0.000000001/ if (pulled < 0) { point.From = lastLeft; point.To = lastLeft + pulled; point.Sum = sumLeft; point.Participation = (point.To - point.From) / point.Sum; point.Participation = double.IsNaN(point.Participation) ? 0 : point.Participation; leftPart += point.Participation; point.StackedParticipation = leftPart; lastLeft = point.To; } else { point.From = lastRight; point.To = lastRight + pulled; point.Sum = sumRight; point.Participation = (point.To - point.From) / point.Sum; point.Participation = double.IsNaN(point.Participation) ? 0 : point.Participation; rightPart += point.Participation; point.StackedParticipation = rightPart; lastRight = point.To; } } if (sumLeft < mostLeft) mostLeft = sumLeft; if (sumRight > mostRight) mostRight = sumRight; } if (stackAt == AxisOrientation.X) { var ax = AxisX[stackIndex]; if (mode == StackMode.Percentage) { if (double.IsNaN(ax.MinValue)) ax.BotLimit = 0; if (double.IsNaN(ax.MaxValue)) ax.TopLimit = 1; } else { if (mostLeft < ax.BotLimit) // ReSharper disable once CompareOfFloatsByEqualityOperator if (double.IsNaN(ax.MinValue)) ax.BotLimit = mostLeft == 0 ? 0 : ((int) (mostLeft/ax.S) - 1)*ax.S; if (mostRight > ax.TopLimit) // ReSharper disable once CompareOfFloatsByEqualityOperator if (double.IsNaN(ax.MaxValue)) ax.TopLimit = mostRight == 0 ? 0 : ((int) (mostRight/ax.S) + 1)*ax.S; } } if (stackAt == AxisOrientation.Y) { var ay = AxisY[stackIndex]; if (mode == StackMode.Percentage) { if (double.IsNaN(ay.MinValue)) ay.BotLimit = 0; if (double.IsNaN(ay.MaxValue)) ay.TopLimit = 1; } else { if (mostLeft < ay.BotLimit) // ReSharper disable once CompareOfFloatsByEqualityOperator if (double.IsNaN(ay.MinValue)) ay.BotLimit = mostLeft == 0 ? 0 : ((int) (mostLeft/ay.S) - 1)*ay.S; if (mostRight > ay.TopLimit) // ReSharper disable once CompareOfFloatsByEqualityOperator if (double.IsNaN(ay.MaxValue)) ay.TopLimit = mostRight == 0 ? 0 : ((int) (mostRight/ay.S) + 1)*ay.S; } } } #endregion #region Privates private static void SetAxisLimits(AxisCore ax, IList series, AxisOrientation orientation) { var first = new CoreLimit(); var firstR = 0d; if (series.Count > 0) { first = orientation == AxisOrientation.X ? series[0].Values.GetTracker(series[0]).XLimit : series[0].Values.GetTracker(series[0]).YLimit; var view = series[0] as IAreaPoint; firstR = view != null ? view.GetPointDiameter() : 0; } // [ max, min, pointRadius ] var boundries = new[] {first.Max, first.Min, firstR}; for (var index = 1; index < series.Count; index++) { var seriesView = series[index]; var tracker = seriesView.Values.GetTracker(seriesView); var limit = orientation == AxisOrientation.X ? tracker.XLimit : tracker.YLimit; var view = seriesView as IAreaPoint; var radius = view != null ? view.GetPointDiameter() : 0; if (limit.Max > boundries[0]) boundries[0] = limit.Max; if (limit.Min < boundries[1]) boundries[1] = limit.Min; if (radius > boundries[2]) boundries[2] = radius; } ax.TopSeriesLimit = boundries[0]; ax.BotSeriesLimit = boundries[1]; ax.TopLimit = double.IsNaN(ax.MaxValue) ? boundries[0] : ax.MaxValue; ax.BotLimit = double.IsNaN(ax.MinValue) ? boundries[1] : ax.MinValue; ax.MaxPointRadius = boundries[2]; } #endregion } }