From fe9ab421abb337e3a157d9f7ef9cc09b71e0c0f5 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Mon, 11 Oct 2021 10:06:26 -0500 Subject: [PATCH 01/22] Initial commit of max value caching Cache maxvalue, minvalue, and valuerange to prevent constant recalculations during drawing loops --- Sources/Microcharts.Samples/Data.cs | 6 ++- Sources/Microcharts/Charts/AxisBasedChart.cs | 52 ++++++++++++------- .../Charts/Legacy/LegacyPointChart.cs | 2 +- Sources/Microcharts/Charts/LineChart.cs | 7 +++ Sources/Microcharts/Helpers/MeasureHelper.cs | 6 +-- 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/Sources/Microcharts.Samples/Data.cs b/Sources/Microcharts.Samples/Data.cs index 22076848..62fd5dc5 100644 --- a/Sources/Microcharts.Samples/Data.cs +++ b/Sources/Microcharts.Samples/Data.cs @@ -1321,6 +1321,8 @@ private static IEnumerable GenerateLineSeriesChartExample() SerieLabelTextSize = 42, ShowYAxisLines = true, ShowYAxisText = true, + MaxValue = 150, + MinValue = -50, YAxisPosition = Position.Left, LegendOption = SeriesLegendOption.Bottom, @@ -1447,8 +1449,9 @@ private static IEnumerable GenerateTimeSeriesEntry( Random r, bool w { List entries = new List(); + Console.WriteLine("Generating Data"); DateTime end = DateTime.Now; - DateTime label = end.AddSeconds(-30); + DateTime label = end.AddSeconds(-1000); int? value = r.Next(0, 100); do @@ -1460,6 +1463,7 @@ private static IEnumerable GenerateTimeSeriesEntry( Random r, bool w } while (label <= end); + Console.WriteLine("Data Generated"); return entries; } diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index b68d4acb..a0df774b 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -167,9 +167,14 @@ public float SerieLabelTextSize /// The height of the chart. public override void DrawContent(SKCanvas canvas, int width, int height) { + Console.WriteLine("DrawContent"); if (Series != null && entries != null) { - width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, out float yAxisXShift, out List yAxisIntervalLabels); + float maxValue = MaxValue; + float minValue = MinValue; + float valRange = maxValue - minValue; + + width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, maxValue, minValue, out float yAxisXShift, out List yAxisIntervalLabels); var firstSerie = Series.FirstOrDefault(); var labels = firstSerie.Entries.Select(x => x.Label).ToArray(); int nbItems = labels.Length; @@ -190,42 +195,51 @@ public override void DrawContent(SKCanvas canvas, int width, int height) float headerHeight = CalculateHeaderHeight(valueLabelSizes); var headerWithLegendHeight = headerHeight + (LegendOption == SeriesLegendOption.Top ? legendHeight : 0); + + var itemSize = CalculateItemSize(nbItems, width, height, footerHeight + headerHeight + legendHeight); var barSize = CalculateBarSize(itemSize, Series.Count()); - var origin = CalculateYOrigin(itemSize.Height, headerWithLegendHeight); - DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, Margin, AnimationProgress, MaxValue, ValueRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); + var origin = CalculateYOrigin(itemSize.Height, headerWithLegendHeight, maxValue, minValue, valRange); + DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, Margin, AnimationProgress, maxValue, valRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); + Console.WriteLine("Begin Points"); + int nbSeries = series.Count(); - for (int i = 0; i < labels.Length; i++) + for (int serieIndex = 0; serieIndex < nbSeries; serieIndex++) { - string label = labels[i]; - SKRect labelSize = labelSizes[i]; + ChartSerie serie = Series.ElementAt(serieIndex); + IEnumerable entries = serie.Entries; + int entryCount = entries.Count(); - var itemX = Margin + (itemSize.Width / 2) + (i * (itemSize.Width + Margin)); - for (int serieIndex = 0; serieIndex < nbSeries; serieIndex++) + Console.WriteLine("Drawing Series: " + serieIndex); + for (int i = 0; i < labels.Length; i++) { - ChartSerie serie = Series.ElementAt(serieIndex); - if (i >= serie.Entries.Count()) continue; + if (i >= entryCount) break; - ChartEntry entry = serie.Entries.ElementAt(i); + ChartEntry entry = entries.ElementAt(i); if (!entry.Value.HasValue) continue; + string label = labels[i]; + SKRect labelSize = labelSizes[i]; + + var itemX = Margin + (itemSize.Width / 2) + (i * (itemSize.Width + Margin)); + float value = entry?.Value ?? 0; float marge = serieIndex < nbSeries ? Margin / 2 : 0; float totalBarMarge = serieIndex * Margin / 2; float barX = itemX + serieIndex * barSize.Width + totalBarMarge; - float barY = headerWithLegendHeight + ((1 - AnimationProgress) * (origin - headerWithLegendHeight) + (((MaxValue - value) / ValueRange) * itemSize.Height) * AnimationProgress); + float barY = headerWithLegendHeight + ((1 - AnimationProgress) * (origin - headerWithLegendHeight) + (((maxValue - value) / valRange) * itemSize.Height) * AnimationProgress); DrawBarArea(canvas, headerWithLegendHeight, itemSize, barSize, serie.Color ?? entry.Color, origin, value, barX, barY); DrawBar(serie, canvas, headerWithLegendHeight, itemX, itemSize, barSize, origin, barX, barY, serie.Color ?? entry.Color); DrawValueLabel(canvas, valueLabelSizes, headerWithLegendHeight, itemSize, barSize, entry, barX, barY, itemX, origin); + if (!string.IsNullOrEmpty(label)) + DrawHelper.DrawLabel(canvas, LabelOrientation, YPositionBehavior.None, itemSize, new SKPoint(itemX, height - footerWithLegendHeight + Margin), LabelColor, labelSize, label, LabelTextSize, Typeface); } - - if(!string.IsNullOrEmpty(label)) - DrawHelper.DrawLabel(canvas, LabelOrientation, YPositionBehavior.None, itemSize, new SKPoint(itemX, height - footerWithLegendHeight + Margin), LabelColor, labelSize, label, LabelTextSize, Typeface); } + Console.WriteLine("Begin Legend"); DrawLegend(canvas, seriesSizes, legendHeight, height, width); OnDrawContentEnd(canvas, itemSize, origin, valueLabelSizes); } @@ -382,19 +396,19 @@ private float CalculateLegendSize(SKRect[] seriesSizes, float serieLabelTextSize return nbLine * height + nbLine * Margin; } - private float CalculateYOrigin(float itemHeight, float headerHeight) + private float CalculateYOrigin(float itemHeight, float headerHeight, float max, float min, float range) { - if (MaxValue <= 0) + if (max <= 0) { return headerHeight; } - if (MinValue > 0) + if (min > 0) { return headerHeight + itemHeight; } - return headerHeight + ((MaxValue / ValueRange) * itemHeight); + return headerHeight + ((max / range) * itemHeight); } private Dictionary MeasureValueLabels() diff --git a/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs b/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs index eaf772e0..86c6783c 100644 --- a/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs +++ b/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs @@ -128,7 +128,7 @@ public override void DrawContent(SKCanvas canvas, int width, int height) { if (Entries != null) { - width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, Entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, out float yAxisXShift, out List yAxisIntervalLabels); + width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, Entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, MaxValue, MinValue, out float yAxisXShift, out List yAxisIntervalLabels); var labels = Entries.Select(x => x.Label).ToArray(); var labelSizes = MeasureHelper.MeasureTexts(labels, LabelTextSize); var footerHeight = MeasureHelper.CalculateFooterHeaderHeight(Margin, LabelTextSize, labelSizes, LabelOrientation); diff --git a/Sources/Microcharts/Charts/LineChart.cs b/Sources/Microcharts/Charts/LineChart.cs index 0857a40e..b1bd72fe 100644 --- a/Sources/Microcharts/Charts/LineChart.cs +++ b/Sources/Microcharts/Charts/LineChart.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using SkiaSharp; @@ -78,14 +79,20 @@ protected override void OnDrawContentEnd(SKCanvas canvas, SKSize itemSize, float { base.OnDrawContentEnd(canvas, itemSize, origin, valueLabelSizes); + Console.WriteLine("DrawContentEnd"); foreach (var pps in pointsPerSerie) { DrawLineArea(canvas, pps.Key, pps.Value.ToArray(), itemSize, origin); } + Console.WriteLine("DrawSeriesLine"); DrawSeriesLine(canvas, itemSize); + Console.WriteLine("DrawPoints"); DrawPoints(canvas); + Console.WriteLine("DrawValueLabels"); DrawValueLabels(canvas, itemSize, valueLabelSizes); + Console.WriteLine("Done"); + Console.WriteLine(""); } private void DrawPoints(SKCanvas canvas) diff --git a/Sources/Microcharts/Helpers/MeasureHelper.cs b/Sources/Microcharts/Helpers/MeasureHelper.cs index 91996ee7..94a9c718 100644 --- a/Sources/Microcharts/Helpers/MeasureHelper.cs +++ b/Sources/Microcharts/Helpers/MeasureHelper.cs @@ -68,7 +68,7 @@ internal static float CalculateFooterHeaderHeight(float margin, float textSize, return result; } - internal static int CalculateYAxis(bool showYAxisText, bool showYAxisLines, IEnumerable entries, int yAxisMaxTicks, SKPaint yAxisTextPaint, Position yAxisPosition, int width, out float yAxisXShift, out List yAxisIntervalLabels) + internal static int CalculateYAxis(bool showYAxisText, bool showYAxisLines, IEnumerable entries, int yAxisMaxTicks, SKPaint yAxisTextPaint, Position yAxisPosition, int width, float maxValue, float minValue, out float yAxisXShift, out List yAxisIntervalLabels) { yAxisXShift = 0.0f; yAxisIntervalLabels = new List(); @@ -76,9 +76,7 @@ internal static int CalculateYAxis(bool showYAxisText, bool showYAxisLines, IEnu { var yAxisWidth = width; - var enumerable = entries.ToList(); // to avoid double enumeration - var minValue = enumerable.Where(e=>e.Value.HasValue).Min(e => e.Value.Value); - var maxValue = enumerable.Where(e => e.Value.HasValue).Max(e => e.Value.Value); + //var enumerable = entries.ToList(); // to avoid double enumeration if(minValue == maxValue) { if (minValue >= 0) From 00782b9c056509a1289000b0d20271dfaa32ddb1 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Mon, 11 Oct 2021 12:55:25 -0500 Subject: [PATCH 02/22] Cleanup Data and Logging Removing excess logging and adding better data generation --- .../ChartPage.xaml.cs | 15 +++++-- Sources/Microcharts.Samples/Data.cs | 43 ++++++++++++++----- Sources/Microcharts/Charts/AxisBasedChart.cs | 6 --- Sources/Microcharts/Charts/LineChart.cs | 6 --- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs b/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs index b6168c3d..ca7531b0 100644 --- a/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs +++ b/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs @@ -51,23 +51,32 @@ protected void GenerateDynamicData() { var label = DateTime.Now.ToString("mm:ss"); + int idx = 0; foreach (var curSeries in series) { var entries = curSeries.Entries.ToList(); + bool addLabel = (entries.Count % 100) == 0; + if (s == curSeries) { - var value = r.Next(rMin, rMax); - var entry = new ChartEntry(value) { ValueLabel = value.ToString(), Label = label }; - entries.Add(entry); + var entry = Data.GenerateTimeSeriesEntry(r, idx, 1); + if (!addLabel) entry.First().Label = null; + + entries.AddRange(entry); + if (entries.Count() > count * 1.5) entries.RemoveAt(0); } else { var entry = new ChartEntry(null) { ValueLabel = null, Label = label }; + if (!addLabel) entry.Label = null; + entries.Add(entry); if (entries.Count() > count * 1.5) entries.RemoveAt(0); } + curSeries.Entries = entries; + idx++; } if (!lc.IsAnimating) diff --git a/Sources/Microcharts.Samples/Data.cs b/Sources/Microcharts.Samples/Data.cs index 62fd5dc5..c1ebf978 100644 --- a/Sources/Microcharts.Samples/Data.cs +++ b/Sources/Microcharts.Samples/Data.cs @@ -1317,12 +1317,15 @@ private static IEnumerable GenerateLineSeriesChartExample() LabelOrientation = Orientation.Vertical, ValueLabelOrientation = Orientation.Horizontal, LabelTextSize = 14, + LineMode = LineMode.Straight, + PointMode = PointMode.None, ValueLabelTextSize = 14, SerieLabelTextSize = 42, + ValueLabelOption = ValueLabelOption.None, ShowYAxisLines = true, ShowYAxisText = true, MaxValue = 150, - MinValue = -50, + MinValue = -150, YAxisPosition = Position.Left, LegendOption = SeriesLegendOption.Bottom, @@ -1332,14 +1335,21 @@ private static IEnumerable GenerateLineSeriesChartExample() { Name = "Sensor 1", Color = SKColor.Parse("#2c3e50"), - Entries = GenerateTimeSeriesEntry(r), + Entries = GenerateTimeSeriesEntry(r, 0, 1000), }, new ChartSerie() { Name = "Sensor 2", Color = SKColor.Parse("#77d065"), - Entries = GenerateTimeSeriesEntry(r), + Entries = GenerateTimeSeriesEntry(r, 1, 1000), + }, + new ChartSerie() + { + Name = "Sensor 3", + Color = SKColor.Parse("#b455b6"), + Entries = GenerateTimeSeriesEntry(r, 2, 1000) } + } } }; @@ -1445,25 +1455,36 @@ private static IEnumerable GenerateBarChartExample() yield break; } - private static IEnumerable GenerateTimeSeriesEntry( Random r, bool withNulls = true) + public static IEnumerable GenerateTimeSeriesEntry( Random r, int idx, int seconds, bool withNulls = true) { List entries = new List(); - Console.WriteLine("Generating Data"); - DateTime end = DateTime.Now; - DateTime label = end.AddSeconds(-1000); - int? value = r.Next(0, 100); + DateTime end = DateTime.Now; + DateTime label = end.AddSeconds(-seconds); + DateTime baseTime = DateTime.Today; + + float amp = 100.0f; + float ampScale = 0.001f + (idx*0.0005f); + float valScale = 0.05f + (idx*0.01f); + int phase = (idx * 33333); + double valOffset = ((label - baseTime).TotalSeconds + phase) * valScale; + double ampOffset = ((label - baseTime).TotalSeconds + phase) * ampScale; + float? value = (float)(Math.Sin(valOffset) * (Math.Cos(ampOffset) *amp)); + + int count = 0; do { if (withNulls && (value.Value % 10) == 0) value = null; - entries.Add(new ChartEntry(value) { ValueLabel = value.ToString(), Label = label.ToString("mm:ss") }); - value = r.Next(0, 100); + entries.Add(new ChartEntry(value) { ValueLabel = value.ToString(), Label = count % 100 == 0 ? label.ToString("mm:ss") : null }); + valOffset = ((label - baseTime).TotalSeconds + phase) * valScale; + ampOffset = ((label - baseTime).TotalSeconds + phase) * ampScale; + value = (float)(Math.Sin(valOffset) * (Math.Cos(ampOffset) * amp)); label = label.AddSeconds(1); + count++; } while (label <= end); - Console.WriteLine("Data Generated"); return entries; } diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index a0df774b..ed6f4bed 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -167,7 +167,6 @@ public float SerieLabelTextSize /// The height of the chart. public override void DrawContent(SKCanvas canvas, int width, int height) { - Console.WriteLine("DrawContent"); if (Series != null && entries != null) { float maxValue = MaxValue; @@ -195,13 +194,10 @@ public override void DrawContent(SKCanvas canvas, int width, int height) float headerHeight = CalculateHeaderHeight(valueLabelSizes); var headerWithLegendHeight = headerHeight + (LegendOption == SeriesLegendOption.Top ? legendHeight : 0); - - var itemSize = CalculateItemSize(nbItems, width, height, footerHeight + headerHeight + legendHeight); var barSize = CalculateBarSize(itemSize, Series.Count()); var origin = CalculateYOrigin(itemSize.Height, headerWithLegendHeight, maxValue, minValue, valRange); DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, Margin, AnimationProgress, maxValue, valRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); - Console.WriteLine("Begin Points"); int nbSeries = series.Count(); @@ -212,7 +208,6 @@ public override void DrawContent(SKCanvas canvas, int width, int height) int entryCount = entries.Count(); - Console.WriteLine("Drawing Series: " + serieIndex); for (int i = 0; i < labels.Length; i++) { if (i >= entryCount) break; @@ -239,7 +234,6 @@ public override void DrawContent(SKCanvas canvas, int width, int height) } } - Console.WriteLine("Begin Legend"); DrawLegend(canvas, seriesSizes, legendHeight, height, width); OnDrawContentEnd(canvas, itemSize, origin, valueLabelSizes); } diff --git a/Sources/Microcharts/Charts/LineChart.cs b/Sources/Microcharts/Charts/LineChart.cs index b1bd72fe..37e0f650 100644 --- a/Sources/Microcharts/Charts/LineChart.cs +++ b/Sources/Microcharts/Charts/LineChart.cs @@ -79,20 +79,14 @@ protected override void OnDrawContentEnd(SKCanvas canvas, SKSize itemSize, float { base.OnDrawContentEnd(canvas, itemSize, origin, valueLabelSizes); - Console.WriteLine("DrawContentEnd"); foreach (var pps in pointsPerSerie) { DrawLineArea(canvas, pps.Key, pps.Value.ToArray(), itemSize, origin); } - Console.WriteLine("DrawSeriesLine"); DrawSeriesLine(canvas, itemSize); - Console.WriteLine("DrawPoints"); DrawPoints(canvas); - Console.WriteLine("DrawValueLabels"); DrawValueLabels(canvas, itemSize, valueLabelSizes); - Console.WriteLine("Done"); - Console.WriteLine(""); } private void DrawPoints(SKCanvas canvas) From c9f8fde871a0847ce0894cadac3116823a073ff2 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Wed, 13 Oct 2021 15:32:06 -0500 Subject: [PATCH 03/22] Updating min/max value handling Better test data. Fixing issue with user defined min/max values Adding an event for chart rendering complete --- Sources/Microcharts.Forms/ChartView.cs | 5 +++ .../ChartPage.xaml.cs | 19 +++++--- Sources/Microcharts.Samples/Data.cs | 45 ++++++++++++------- Sources/Microcharts/Charts/AxisBasedChart.cs | 4 +- Sources/Microcharts/Charts/Chart.cs | 2 +- .../Charts/Legacy/LegacyPointChart.cs | 4 +- Sources/Microcharts/Helpers/MeasureHelper.cs | 36 ++++++++++----- 7 files changed, 80 insertions(+), 35 deletions(-) diff --git a/Sources/Microcharts.Forms/ChartView.cs b/Sources/Microcharts.Forms/ChartView.cs index fb4daf91..e21098e5 100644 --- a/Sources/Microcharts.Forms/ChartView.cs +++ b/Sources/Microcharts.Forms/ChartView.cs @@ -6,6 +6,7 @@ namespace Microcharts.Forms using Xamarin.Forms; using SkiaSharp.Views.Forms; using SkiaSharp; + using System; public class ChartView : SKCanvasView { @@ -17,6 +18,8 @@ public ChartView() this.PaintSurface += OnPaintCanvas; } + public event EventHandler ChartPainted; + #endregion #region Static fields @@ -74,6 +77,8 @@ private void OnPaintCanvas(object sender, SKPaintSurfaceEventArgs e) { e.Surface.Canvas.Clear(SKColors.Transparent); } + + ChartPainted?.Invoke(sender, e); } #endregion diff --git a/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs b/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs index ca7531b0..92e10302 100644 --- a/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs +++ b/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs @@ -29,13 +29,13 @@ protected override void OnDisappearing() Running = false; base.OnDisappearing(); } - + bool IsDrawing = false; protected void GenerateDynamicData() { Random r = new Random((int)DateTime.Now.Ticks); LineChart lc = (LineChart)chartView.Chart; - int ticks = (int)(1100 * TimeSpan.TicksPerMillisecond); + int ticks = (int)(250 * TimeSpan.TicksPerMillisecond); var series = lc.Series; @@ -83,14 +83,18 @@ protected void GenerateDynamicData() { lc.IsAnimated = false; lc.Series = series; - chartView.InvalidateSurface(); + if (!IsDrawing) + { + IsDrawing = true; + chartView.InvalidateSurface(); + } } }).ContinueWith(t => { if (t.IsFaulted) Console.WriteLine(t.Exception); }); return Running; }); - ticks += (int)(1100 * TimeSpan.TicksPerMillisecond); + ticks += (int)(250 * TimeSpan.TicksPerMillisecond); } } @@ -99,7 +103,12 @@ protected override void OnAppearing() base.OnAppearing(); chartView.Chart = ExampleChartItem.Chart; - if(!chartView.Chart.IsAnimating) + chartView.ChartPainted += (sender, args) => + { + IsDrawing = false; + }; + + if (!chartView.Chart.IsAnimating) chartView.Chart.AnimateAsync(true).ConfigureAwait(false); if (ExampleChartItem.IsDynamic && (chartView.Chart as LineChart) != null ) diff --git a/Sources/Microcharts.Samples/Data.cs b/Sources/Microcharts.Samples/Data.cs index c1ebf978..aa5eeb1f 100644 --- a/Sources/Microcharts.Samples/Data.cs +++ b/Sources/Microcharts.Samples/Data.cs @@ -1315,10 +1315,12 @@ private static IEnumerable GenerateLineSeriesChartExample() Chart = new LineChart { LabelOrientation = Orientation.Vertical, - ValueLabelOrientation = Orientation.Horizontal, + ValueLabelOrientation = Orientation.Vertical, LabelTextSize = 14, LineMode = LineMode.Straight, PointMode = PointMode.None, + LineAreaAlpha = 0, + PointAreaAlpha = 0, ValueLabelTextSize = 14, SerieLabelTextSize = 42, ValueLabelOption = ValueLabelOption.None, @@ -1333,23 +1335,34 @@ private static IEnumerable GenerateLineSeriesChartExample() { new ChartSerie() { - Name = "Sensor 1", - Color = SKColor.Parse("#2c3e50"), - Entries = GenerateTimeSeriesEntry(r, 0, 1000), + Name = "S1", + Color = Data.Colors[0], + Entries = GenerateTimeSeriesEntry(r, 0, 10000), }, new ChartSerie() { - Name = "Sensor 2", - Color = SKColor.Parse("#77d065"), - Entries = GenerateTimeSeriesEntry(r, 1, 1000), + Name = "S2", + Color = Data.Colors[1], + Entries = GenerateTimeSeriesEntry(r, 1, 10000), }, new ChartSerie() { - Name = "Sensor 3", - Color = SKColor.Parse("#b455b6"), - Entries = GenerateTimeSeriesEntry(r, 2, 1000) + Name = "S3", + Color = Data.Colors[2], + Entries = GenerateTimeSeriesEntry(r, 2, 10000) + }, + new ChartSerie() + { + Name = "S4", + Color = Data.Colors[3], + Entries = GenerateTimeSeriesEntry(r, 3, 10000) + }, + new ChartSerie() + { + Name = "S5", + Color = Data.Colors[4], + Entries = GenerateTimeSeriesEntry(r, 4, 10000) } - } } }; @@ -1464,22 +1477,22 @@ public static IEnumerable GenerateTimeSeriesEntry( Random r, int idx DateTime label = end.AddSeconds(-seconds); DateTime baseTime = DateTime.Today; - float amp = 100.0f; + float amp = 25.0f; float ampScale = 0.001f + (idx*0.0005f); float valScale = 0.05f + (idx*0.01f); int phase = (idx * 33333); double valOffset = ((label - baseTime).TotalSeconds + phase) * valScale; double ampOffset = ((label - baseTime).TotalSeconds + phase) * ampScale; - float? value = (float)(Math.Sin(valOffset) * (Math.Cos(ampOffset) *amp)); - + float valueShift = (amp * 0.75f * (idx-2)); + float? value = valueShift + (float)(Math.Sin(valOffset) * (Math.Cos(ampOffset) *amp)); int count = 0; do { if (withNulls && (value.Value % 10) == 0) value = null; - entries.Add(new ChartEntry(value) { ValueLabel = value.ToString(), Label = count % 100 == 0 ? label.ToString("mm:ss") : null }); + entries.Add(new ChartEntry(value) { ValueLabel = value.ToString(), Label = count % 1000 == 0 ? label.ToString("mm:ss") : null }); valOffset = ((label - baseTime).TotalSeconds + phase) * valScale; ampOffset = ((label - baseTime).TotalSeconds + phase) * ampScale; - value = (float)(Math.Sin(valOffset) * (Math.Cos(ampOffset) * amp)); + value = valueShift + (float)(Math.Sin(valOffset) * (Math.Cos(ampOffset) * amp)); label = label.AddSeconds(1); count++; } diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index ed6f4bed..fee69f58 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -169,11 +169,13 @@ public override void DrawContent(SKCanvas canvas, int width, int height) { if (Series != null && entries != null) { + bool fixedRange = InternalMaxValue != null || InternalMinValue != null; float maxValue = MaxValue; float minValue = MinValue; + + width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); float valRange = maxValue - minValue; - width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, maxValue, minValue, out float yAxisXShift, out List yAxisIntervalLabels); var firstSerie = Series.FirstOrDefault(); var labels = firstSerie.Entries.Select(x => x.Label).ToArray(); int nbItems = labels.Length; diff --git a/Sources/Microcharts/Charts/Chart.cs b/Sources/Microcharts/Charts/Chart.cs index ea4c0bd1..441a4866 100644 --- a/Sources/Microcharts/Charts/Chart.cs +++ b/Sources/Microcharts/Charts/Chart.cs @@ -434,7 +434,7 @@ protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs case nameof(MaxValue): case nameof(MinValue): case nameof(BackgroundColor): - PlanifyInvalidate(); + Invalidate(); break; default: break; diff --git a/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs b/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs index 86c6783c..40d066a9 100644 --- a/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs +++ b/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs @@ -128,7 +128,9 @@ public override void DrawContent(SKCanvas canvas, int width, int height) { if (Entries != null) { - width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, Entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, MaxValue, MinValue, out float yAxisXShift, out List yAxisIntervalLabels); + float maxValue = MaxValue; + float minValue = MinValue; + width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, Entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, false, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); var labels = Entries.Select(x => x.Label).ToArray(); var labelSizes = MeasureHelper.MeasureTexts(labels, LabelTextSize); var footerHeight = MeasureHelper.CalculateFooterHeaderHeight(Margin, LabelTextSize, labelSizes, LabelOrientation); diff --git a/Sources/Microcharts/Helpers/MeasureHelper.cs b/Sources/Microcharts/Helpers/MeasureHelper.cs index 94a9c718..5d048db5 100644 --- a/Sources/Microcharts/Helpers/MeasureHelper.cs +++ b/Sources/Microcharts/Helpers/MeasureHelper.cs @@ -68,26 +68,38 @@ internal static float CalculateFooterHeaderHeight(float margin, float textSize, return result; } - internal static int CalculateYAxis(bool showYAxisText, bool showYAxisLines, IEnumerable entries, int yAxisMaxTicks, SKPaint yAxisTextPaint, Position yAxisPosition, int width, float maxValue, float minValue, out float yAxisXShift, out List yAxisIntervalLabels) + internal static int CalculateYAxis(bool showYAxisText, bool showYAxisLines, IEnumerable entries, int yAxisMaxTicks, SKPaint yAxisTextPaint, Position yAxisPosition, int width, bool fixedRange, ref float maxValue, ref float minValue, out float yAxisXShift, out List yAxisIntervalLabels) { yAxisXShift = 0.0f; yAxisIntervalLabels = new List(); if (showYAxisText || showYAxisLines) { var yAxisWidth = width; + double range, niceMin, niceMax, tickSpacing; + int ticks; - //var enumerable = entries.ToList(); // to avoid double enumeration - if(minValue == maxValue) + if (!fixedRange) { - if (minValue >= 0) - maxValue += 100; - else - maxValue = 0; - } - - NiceScale.Calculate(minValue, maxValue, yAxisMaxTicks, out var range, out var tickSpacing, out var niceMin, out var niceMax); + //var enumerable = entries.ToList(); // to avoid double enumeration + if (minValue == maxValue) + { + if (minValue >= 0) + maxValue += 100; + else + maxValue = 0; + } - var ticks = (int)(range / tickSpacing); + NiceScale.Calculate(minValue, maxValue, yAxisMaxTicks, out range, out tickSpacing, out niceMin, out niceMax); + ticks = (int)(range / tickSpacing); + } + else + { + niceMin = minValue; + niceMax = maxValue; + range = niceMax - niceMin; + tickSpacing = range / (yAxisMaxTicks-1); + ticks = yAxisMaxTicks; + } yAxisIntervalLabels = Enumerable.Range(0, ticks) .Select(i => (float)(niceMax - (i * tickSpacing))) @@ -103,6 +115,8 @@ internal static int CalculateYAxis(bool showYAxisText, bool showYAxisLines, IEnu // to reduce chart width width = yAxisWidth; + maxValue = (float)niceMax; + minValue = (float)niceMin; } return width; From 5ea7ae99648ef7dbc34dd90852048c4043baced7 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Wed, 13 Oct 2021 15:57:35 -0500 Subject: [PATCH 04/22] Fixing property changed events Proper set comparison Don't double change animation progress when updating series --- Sources/Microcharts/Charts/Chart.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Microcharts/Charts/Chart.cs b/Sources/Microcharts/Charts/Chart.cs index 441a4866..99180e81 100644 --- a/Sources/Microcharts/Charts/Chart.cs +++ b/Sources/Microcharts/Charts/Chart.cs @@ -554,6 +554,10 @@ protected async void UpdateEntries(IEnumerable value) { await AnimateAsync(false, cancellation.Token); } + else if( !IsAnimated ) + { + AnimationProgress = 1; //This prevents an extra property change on dynamic data + } else { AnimationProgress = 0; @@ -611,7 +615,7 @@ protected void RaisePropertyChanged([CallerMemberName] string property = null) /// The 1st type parameter. protected bool Set(ref T field, T value, [CallerMemberName] string property = null) { - if (!EqualityComparer.Equals(field, property)) + if ((field == null && value != null) || !field.Equals(value)) { field = value; RaisePropertyChanged(property); From 5725af3d479f0ba5a5ee5a8aaf2d0a4d5bc7c2b6 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Wed, 13 Oct 2021 16:44:40 -0500 Subject: [PATCH 05/22] yAxisShift fix for bar The lines and bar charts were drawing too close to left labels --- Sources/Microcharts/Charts/AxisBasedChart.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index fee69f58..9cb265ed 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -201,7 +201,6 @@ public override void DrawContent(SKCanvas canvas, int width, int height) var origin = CalculateYOrigin(itemSize.Height, headerWithLegendHeight, maxValue, minValue, valRange); DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, Margin, AnimationProgress, maxValue, valRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); - int nbSeries = series.Count(); for (int serieIndex = 0; serieIndex < nbSeries; serieIndex++) { @@ -220,7 +219,7 @@ public override void DrawContent(SKCanvas canvas, int width, int height) string label = labels[i]; SKRect labelSize = labelSizes[i]; - var itemX = Margin + (itemSize.Width / 2) + (i * (itemSize.Width + Margin)); + var itemX = Margin + (itemSize.Width / 2) + (i * (itemSize.Width + Margin)) + yAxisXShift; float value = entry?.Value ?? 0; float marge = serieIndex < nbSeries ? Margin / 2 : 0; From 72e41dfb28850d24f8ac32cbffddfabb81c4a2d7 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Wed, 13 Oct 2021 17:22:47 -0500 Subject: [PATCH 06/22] Adding some comments Adding some comments --- Sources/Microcharts/Charts/AxisBasedChart.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index 9cb265ed..53b87bf0 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -169,10 +169,12 @@ public override void DrawContent(SKCanvas canvas, int width, int height) { if (Series != null && entries != null) { + //Caching the min/max values for performance bool fixedRange = InternalMaxValue != null || InternalMinValue != null; float maxValue = MaxValue; float minValue = MinValue; + //This function might change the min/max value width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); float valRange = maxValue - minValue; From ede48dda01451fbe7a9d92d2b8258b60ac84814d Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Wed, 13 Oct 2021 18:02:46 -0500 Subject: [PATCH 07/22] Making the values match the labels Making the values match the labels --- Sources/Microcharts.Samples/Data.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Microcharts.Samples/Data.cs b/Sources/Microcharts.Samples/Data.cs index aa5eeb1f..5f85434a 100644 --- a/Sources/Microcharts.Samples/Data.cs +++ b/Sources/Microcharts.Samples/Data.cs @@ -245,7 +245,7 @@ private static ChartEntry[] GenerateDefaultXamarinEntries() ValueLabel = "112", Color = SKColor.Parse("#2c3e50"), }, - new ChartEntry(248) + new ChartEntry(648) { Label = "Android", ValueLabel = "648", @@ -254,10 +254,10 @@ private static ChartEntry[] GenerateDefaultXamarinEntries() new ChartEntry(null) { Label = "React", - ValueLabel = "214", + ValueLabel = "", Color = SKColor.Parse("#db3498"), }, - new ChartEntry(128) + new ChartEntry(428) { Label = "iOS", ValueLabel = "428", @@ -266,7 +266,7 @@ private static ChartEntry[] GenerateDefaultXamarinEntries() new ChartEntry(514) { Label = "Forms", - ValueLabel = "214", + ValueLabel = "514", Color = SKColor.Parse("#3498db"), } }; From 2d6f4e58ee7f7fe59c9dc6d15f43ffa153a0fc26 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Wed, 13 Oct 2021 18:03:05 -0500 Subject: [PATCH 08/22] Fixing issue with null values Fixing issue with null values --- Sources/Microcharts/Charts/AxisBasedChart.cs | 30 +++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index 53b87bf0..f21dbb43 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -214,26 +214,28 @@ public override void DrawContent(SKCanvas canvas, int width, int height) for (int i = 0; i < labels.Length; i++) { if (i >= entryCount) break; + var itemX = Margin + (itemSize.Width / 2) + (i * (itemSize.Width + Margin)) + yAxisXShift; ChartEntry entry = entries.ElementAt(i); - if (!entry.Value.HasValue) continue; + if (entry != null && entry.Value.HasValue) + { + float value = entry.Value.Value; + float marge = serieIndex < nbSeries ? Margin / 2 : 0; + float totalBarMarge = serieIndex * Margin / 2; + float barX = itemX + serieIndex * barSize.Width + totalBarMarge; + float barY = headerWithLegendHeight + ((1 - AnimationProgress) * (origin - headerWithLegendHeight) + (((maxValue - value) / valRange) * itemSize.Height) * AnimationProgress); + + DrawBarArea(canvas, headerWithLegendHeight, itemSize, barSize, serie.Color ?? entry.Color, origin, value, barX, barY); + DrawBar(serie, canvas, headerWithLegendHeight, itemX, itemSize, barSize, origin, barX, barY, serie.Color ?? entry.Color); + DrawValueLabel(canvas, valueLabelSizes, headerWithLegendHeight, itemSize, barSize, entry, barX, barY, itemX, origin); + } string label = labels[i]; - SKRect labelSize = labelSizes[i]; - - var itemX = Margin + (itemSize.Width / 2) + (i * (itemSize.Width + Margin)) + yAxisXShift; - - float value = entry?.Value ?? 0; - float marge = serieIndex < nbSeries ? Margin / 2 : 0; - float totalBarMarge = serieIndex * Margin / 2; - float barX = itemX + serieIndex * barSize.Width + totalBarMarge; - float barY = headerWithLegendHeight + ((1 - AnimationProgress) * (origin - headerWithLegendHeight) + (((maxValue - value) / valRange) * itemSize.Height) * AnimationProgress); - - DrawBarArea(canvas, headerWithLegendHeight, itemSize, barSize, serie.Color ?? entry.Color, origin, value, barX, barY); - DrawBar(serie, canvas, headerWithLegendHeight, itemX, itemSize, barSize, origin, barX, barY, serie.Color ?? entry.Color); - DrawValueLabel(canvas, valueLabelSizes, headerWithLegendHeight, itemSize, barSize, entry, barX, barY, itemX, origin); if (!string.IsNullOrEmpty(label)) + { + SKRect labelSize = labelSizes[i]; DrawHelper.DrawLabel(canvas, LabelOrientation, YPositionBehavior.None, itemSize, new SKPoint(itemX, height - footerWithLegendHeight + Margin), LabelColor, labelSize, label, LabelTextSize, Typeface); + } } } From e0f1c371287389c2e11b61f6831b892faa0fa49c Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Wed, 13 Oct 2021 18:20:40 -0500 Subject: [PATCH 09/22] Adding some comments about min/max values Adding some comments about min/max values --- Sources/Microcharts/Charts/AxisBasedChart.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index f21dbb43..bbc66b15 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -170,7 +170,10 @@ public override void DrawContent(SKCanvas canvas, int width, int height) if (Series != null && entries != null) { //Caching the min/max values for performance - bool fixedRange = InternalMaxValue != null || InternalMinValue != null; + bool fixedRange = InternalMaxValue.HasValue || InternalMinValue.HasValue; + + //Ideally we'd use the internal values here, but the drawing does not crop to the bounds + //So the min and min cannot be set less than the actual min/max of the values float maxValue = MaxValue; float minValue = MinValue; From 744dda3eb98df155b0af010b4d6bc0b47e1e3e35 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Sun, 17 Oct 2021 19:16:45 -0500 Subject: [PATCH 10/22] Revert "Merge branch 'feature/36-null-value-support' into feature/290-max-value-perf-fixes" This reverts commit 695537c7463ff8fe2938fd9daa3667becdecc282, reversing changes made to 9a570c7c0cbac07609c25ca1e5a6440868be16ad. --- Sources/Microcharts.Samples/Data.cs | 76 +++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/Sources/Microcharts.Samples/Data.cs b/Sources/Microcharts.Samples/Data.cs index 127b105f..5f85434a 100644 --- a/Sources/Microcharts.Samples/Data.cs +++ b/Sources/Microcharts.Samples/Data.cs @@ -242,25 +242,25 @@ private static ChartEntry[] GenerateDefaultXamarinEntries() new ChartEntry(212) { Label = "UWP", - ValueLabel = "212", + ValueLabel = "112", Color = SKColor.Parse("#2c3e50"), }, - new ChartEntry(248) + new ChartEntry(648) { Label = "Android", - ValueLabel = "248", + ValueLabel = "648", Color = SKColor.Parse("#77d065"), }, new ChartEntry(null) { Label = "React", - ValueLabel = null, + ValueLabel = "", Color = SKColor.Parse("#db3498"), }, - new ChartEntry(128) + new ChartEntry(428) { Label = "iOS", - ValueLabel = "128", + ValueLabel = "428", Color = SKColor.Parse("#b455b6"), }, new ChartEntry(514) @@ -1315,12 +1315,19 @@ private static IEnumerable GenerateLineSeriesChartExample() Chart = new LineChart { LabelOrientation = Orientation.Vertical, - ValueLabelOrientation = Orientation.Horizontal, + ValueLabelOrientation = Orientation.Vertical, LabelTextSize = 14, + LineMode = LineMode.Straight, + PointMode = PointMode.None, + LineAreaAlpha = 0, + PointAreaAlpha = 0, ValueLabelTextSize = 14, SerieLabelTextSize = 42, + ValueLabelOption = ValueLabelOption.None, ShowYAxisLines = true, ShowYAxisText = true, + MaxValue = 150, + MinValue = -150, YAxisPosition = Position.Left, LegendOption = SeriesLegendOption.Bottom, @@ -1328,15 +1335,33 @@ private static IEnumerable GenerateLineSeriesChartExample() { new ChartSerie() { - Name = "Sensor 1", - Color = SKColor.Parse("#2c3e50"), - Entries = GenerateTimeSeriesEntry(r), + Name = "S1", + Color = Data.Colors[0], + Entries = GenerateTimeSeriesEntry(r, 0, 10000), }, new ChartSerie() { - Name = "Sensor 2", - Color = SKColor.Parse("#77d065"), - Entries = GenerateTimeSeriesEntry(r), + Name = "S2", + Color = Data.Colors[1], + Entries = GenerateTimeSeriesEntry(r, 1, 10000), + }, + new ChartSerie() + { + Name = "S3", + Color = Data.Colors[2], + Entries = GenerateTimeSeriesEntry(r, 2, 10000) + }, + new ChartSerie() + { + Name = "S4", + Color = Data.Colors[3], + Entries = GenerateTimeSeriesEntry(r, 3, 10000) + }, + new ChartSerie() + { + Name = "S5", + Color = Data.Colors[4], + Entries = GenerateTimeSeriesEntry(r, 4, 10000) } } } @@ -1443,20 +1468,33 @@ private static IEnumerable GenerateBarChartExample() yield break; } - private static IEnumerable GenerateTimeSeriesEntry( Random r, bool withNulls = true) + public static IEnumerable GenerateTimeSeriesEntry( Random r, int idx, int seconds, bool withNulls = true) { List entries = new List(); - DateTime end = DateTime.Now; - DateTime label = end.AddSeconds(-30); - int? value = r.Next(0, 100); + DateTime end = DateTime.Now; + DateTime label = end.AddSeconds(-seconds); + DateTime baseTime = DateTime.Today; + + float amp = 25.0f; + float ampScale = 0.001f + (idx*0.0005f); + float valScale = 0.05f + (idx*0.01f); + int phase = (idx * 33333); + double valOffset = ((label - baseTime).TotalSeconds + phase) * valScale; + double ampOffset = ((label - baseTime).TotalSeconds + phase) * ampScale; + float valueShift = (amp * 0.75f * (idx-2)); + float? value = valueShift + (float)(Math.Sin(valOffset) * (Math.Cos(ampOffset) *amp)); + int count = 0; do { if (withNulls && (value.Value % 10) == 0) value = null; - entries.Add(new ChartEntry(value) { ValueLabel = value.ToString(), Label = label.ToString("mm:ss") }); - value = r.Next(0, 100); + entries.Add(new ChartEntry(value) { ValueLabel = value.ToString(), Label = count % 1000 == 0 ? label.ToString("mm:ss") : null }); + valOffset = ((label - baseTime).TotalSeconds + phase) * valScale; + ampOffset = ((label - baseTime).TotalSeconds + phase) * ampScale; + value = valueShift + (float)(Math.Sin(valOffset) * (Math.Cos(ampOffset) * amp)); label = label.AddSeconds(1); + count++; } while (label <= end); From 59659ab09e480750fb895ac6008a48768bd97374 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Sun, 17 Oct 2021 19:18:21 -0500 Subject: [PATCH 11/22] Making labels match values Making labels match values --- Sources/Microcharts.Samples/Data.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Microcharts.Samples/Data.cs b/Sources/Microcharts.Samples/Data.cs index 5f85434a..af752c53 100644 --- a/Sources/Microcharts.Samples/Data.cs +++ b/Sources/Microcharts.Samples/Data.cs @@ -239,7 +239,7 @@ private static ChartEntry[] GenerateDefaultXamarinEntries() { return new[] { - new ChartEntry(212) + new ChartEntry(112) { Label = "UWP", ValueLabel = "112", From db4d0e7dfa3355d5d9d1eab131d8449121de65fe Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Sun, 17 Oct 2021 21:03:04 -0500 Subject: [PATCH 12/22] Initial support for ClipRect and Pinch to Zoom Initial support for ClipRect and Pinch to Zoom --- Sources/Microcharts.Forms/ChartView.cs | 69 ++++++++++++++++++++ Sources/Microcharts/Charts/AxisBasedChart.cs | 43 +++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/Sources/Microcharts.Forms/ChartView.cs b/Sources/Microcharts.Forms/ChartView.cs index e21098e5..04848709 100644 --- a/Sources/Microcharts.Forms/ChartView.cs +++ b/Sources/Microcharts.Forms/ChartView.cs @@ -7,6 +7,7 @@ namespace Microcharts.Forms using SkiaSharp.Views.Forms; using SkiaSharp; using System; + using Xamarin.Forms.Internals; public class ChartView : SKCanvasView { @@ -16,6 +17,10 @@ public ChartView() { this.BackgroundColor = Color.Transparent; this.PaintSurface += OnPaintCanvas; + + var pinchGesture = new PinchGestureRecognizer(); + pinchGesture.PinchUpdated += OnPinchUpdated; + GestureRecognizers.Add(pinchGesture); } public event EventHandler ChartPainted; @@ -81,6 +86,70 @@ private void OnPaintCanvas(object sender, SKPaintSurfaceEventArgs e) ChartPainted?.Invoke(sender, e); } + + float currentScale = 1; + float startScale = 1; + SKPoint offsetPosition = new SKPoint(0, 0); + SKPoint translation = new SKPoint(0, 0); + + void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) + { + AxisBasedChart axisChart = this.Chart as AxisBasedChart; + if (axisChart == null) return; + + if (e.Status == GestureStatus.Started) + { + // Store the current scale factor applied to the wrapped user interface element, + // and zero the components for the center point of the translate transform. + startScale = currentScale; + } + if (e.Status == GestureStatus.Running) + { + // Calculate the scale factor to be applied. + currentScale += (float)((e.Scale - 1) * startScale); + currentScale = Math.Max(1, currentScale); + currentScale = Math.Min(3, currentScale); + + // The ScaleOrigin is in relative coordinates to the wrapped user interface element, + // so get the X pixel coordinate. + double renderedX = X + offsetPosition.X; + double deltaX = renderedX / CanvasSize.Width; + double deltaWidth = CanvasSize.Width / (CanvasSize.Width * startScale); + double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth; + + // The ScaleOrigin is in relative coordinates to the wrapped user interface element, + // so get the Y pixel coordinate. + double renderedY = Y + offsetPosition.Y; + double deltaY = renderedY / CanvasSize.Height; + double deltaHeight = CanvasSize.Height / (CanvasSize.Height * startScale); + double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight; + + // Calculate the transformed element pixel coordinates. + double targetX = offsetPosition.X - (originX * CanvasSize.Width) * (currentScale - startScale); + double targetY = offsetPosition.Y - (originY * CanvasSize.Height) * (currentScale - startScale); + + // Apply translation based on the change in origin. + translation.X = (float)targetX.Clamp(-CanvasSize.Width * (currentScale - 1), 0); + translation.Y = (float)targetY.Clamp(-CanvasSize.Height * (currentScale - 1), 0); + + Console.WriteLine("{0}, {1}", translation.X, translation.Y); + + axisChart.XForm.Scale = currentScale; + axisChart.XForm.Offset = translation; + InvalidateSurface(); + + } + if (e.Status == GestureStatus.Completed) + { + // Store the translation delta's of the wrapped user interface element. + offsetPosition = translation; + + axisChart.XForm.Scale = currentScale; + axisChart.XForm.Offset = translation; + InvalidateSurface(); + } + } + #endregion } } diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index bbc66b15..a1ecd426 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -8,6 +8,12 @@ namespace Microcharts { + public class ChartXForm + { + public float Scale = 1.0f; + public SKPoint Offset = new SKPoint(0, 0); + } + /// /// Base chart for Series chart based on Axis work /// @@ -155,6 +161,9 @@ public float SerieLabelTextSize /// public SKPaint YAxisLinesPaint { get; set; } + + public ChartXForm XForm { get; } = new ChartXForm(); + #endregion #region Methods @@ -167,6 +176,7 @@ public float SerieLabelTextSize /// The height of the chart. public override void DrawContent(SKCanvas canvas, int width, int height) { + SKRect canvasRect = new SKRect(0, 0, width, height); if (Series != null && entries != null) { //Caching the min/max values for performance @@ -174,8 +184,8 @@ public override void DrawContent(SKCanvas canvas, int width, int height) //Ideally we'd use the internal values here, but the drawing does not crop to the bounds //So the min and min cannot be set less than the actual min/max of the values - float maxValue = MaxValue; - float minValue = MinValue; + float maxValue = InternalMaxValue.HasValue ? InternalMaxValue.Value : MaxValue; + float minValue = InternalMinValue.HasValue ? InternalMinValue.Value : MinValue; //This function might change the min/max value width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); @@ -206,6 +216,20 @@ public override void DrawContent(SKCanvas canvas, int width, int height) var origin = CalculateYOrigin(itemSize.Height, headerWithLegendHeight, maxValue, minValue, valRange); DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, Margin, AnimationProgress, maxValue, valRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); + + SKRect chartRect = new SKRect(canvasRect.Left+yAxisXShift, canvasRect.Top+headerWithLegendHeight, canvasRect.Right-yAxisXShift, canvasRect.Bottom-footerWithLegendHeight); + + + //Clear chart bounds for testing + /* + canvas.Save(); + canvas.Clear(SKColors.Purple); + canvas.ClipRect(chartRect); + canvas.Clear(SKColors.Pink); + canvas.Restore(); + */ + + canvas.Save(); int nbSeries = series.Count(); for (int serieIndex = 0; serieIndex < nbSeries; serieIndex++) { @@ -219,6 +243,7 @@ public override void DrawContent(SKCanvas canvas, int width, int height) if (i >= entryCount) break; var itemX = Margin + (itemSize.Width / 2) + (i * (itemSize.Width + Margin)) + yAxisXShift; + ChartEntry entry = entries.ElementAt(i); if (entry != null && entry.Value.HasValue) { @@ -228,11 +253,17 @@ public override void DrawContent(SKCanvas canvas, int width, int height) float barX = itemX + serieIndex * barSize.Width + totalBarMarge; float barY = headerWithLegendHeight + ((1 - AnimationProgress) * (origin - headerWithLegendHeight) + (((maxValue - value) / valRange) * itemSize.Height) * AnimationProgress); + canvas.Save(); + canvas.ClipRect(chartRect); + canvas.Translate(XForm.Offset); + canvas.Scale(XForm.Scale); DrawBarArea(canvas, headerWithLegendHeight, itemSize, barSize, serie.Color ?? entry.Color, origin, value, barX, barY); DrawBar(serie, canvas, headerWithLegendHeight, itemX, itemSize, barSize, origin, barX, barY, serie.Color ?? entry.Color); DrawValueLabel(canvas, valueLabelSizes, headerWithLegendHeight, itemSize, barSize, entry, barX, barY, itemX, origin); + canvas.Restore(); } + string label = labels[i]; if (!string.IsNullOrEmpty(label)) { @@ -242,8 +273,16 @@ public override void DrawContent(SKCanvas canvas, int width, int height) } } + canvas.Restore(); + DrawLegend(canvas, seriesSizes, legendHeight, height, width); + + canvas.Save(); + canvas.ClipRect(chartRect); + canvas.Translate(XForm.Offset); + canvas.Scale(XForm.Scale); OnDrawContentEnd(canvas, itemSize, origin, valueLabelSizes); + canvas.Restore(); } } From f73549dcd9c197cf2540a0680fb23f73fd1d3266 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Wed, 20 Oct 2021 18:27:05 -0500 Subject: [PATCH 13/22] Addressing an issue with line charts a indexing into draw points Addressing an issue with line charts a indexing into draw points --- Sources/Microcharts/Charts/AxisBasedChart.cs | 11 +++++++++++ Sources/Microcharts/Charts/LineChart.cs | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index bbc66b15..68730736 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -232,6 +232,10 @@ public override void DrawContent(SKCanvas canvas, int width, int height) DrawBar(serie, canvas, headerWithLegendHeight, itemX, itemSize, barSize, origin, barX, barY, serie.Color ?? entry.Color); DrawValueLabel(canvas, valueLabelSizes, headerWithLegendHeight, itemSize, barSize, entry, barX, barY, itemX, origin); } + else + { + DrawNullPoint(serie, canvas); + } string label = labels[i]; if (!string.IsNullOrEmpty(label)) @@ -454,6 +458,13 @@ private Dictionary MeasureValueLabels() /// protected abstract void DrawBar(ChartSerie serie, SKCanvas canvas, float headerHeight, float itemX, SKSize itemSize, SKSize barSize, float origin, float barX, float barY, SKColor color); + + /// + /// Called during the draw cycle when encountering a point with a null value + /// + protected virtual void DrawNullPoint(ChartSerie serie, SKCanvas canvas) { } + + /// /// Draw bar (or point) area of an entry /// diff --git a/Sources/Microcharts/Charts/LineChart.cs b/Sources/Microcharts/Charts/LineChart.cs index 37e0f650..a12b20b4 100644 --- a/Sources/Microcharts/Charts/LineChart.cs +++ b/Sources/Microcharts/Charts/LineChart.cs @@ -74,6 +74,12 @@ public override void DrawContent(SKCanvas canvas, int width, int height) base.DrawContent(canvas, width, height); } + protected override void DrawNullPoint(ChartSerie serie, SKCanvas canvas) { + //Some of the drawing algorithms index into pointsPerSerie + var point = new SKPoint(float.MinValue, float.MinValue); + pointsPerSerie[serie].Add(point); + } + /// protected override void OnDrawContentEnd(SKCanvas canvas, SKSize itemSize, float origin, Dictionary valueLabelSizes) { From f64c4c2e4845f9df3387f5779873eeb6351b7cc9 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Wed, 20 Oct 2021 20:47:34 -0500 Subject: [PATCH 14/22] Handle scaling drawing of axis labels Handle scaling drawing of axis labels Improved clip rects --- Sources/Microcharts.Forms/ChartView.cs | 4 +-- Sources/Microcharts/Charts/AxisBasedChart.cs | 28 +++++++++++++++---- .../Charts/Legacy/LegacyPointChart.cs | 2 +- Sources/Microcharts/Helpers/DrawHelper.cs | 4 +-- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Sources/Microcharts.Forms/ChartView.cs b/Sources/Microcharts.Forms/ChartView.cs index 04848709..0ccefc8d 100644 --- a/Sources/Microcharts.Forms/ChartView.cs +++ b/Sources/Microcharts.Forms/ChartView.cs @@ -108,7 +108,7 @@ void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) // Calculate the scale factor to be applied. currentScale += (float)((e.Scale - 1) * startScale); currentScale = Math.Max(1, currentScale); - currentScale = Math.Min(3, currentScale); + currentScale = Math.Min(5, currentScale); // The ScaleOrigin is in relative coordinates to the wrapped user interface element, // so get the X pixel coordinate. @@ -132,7 +132,7 @@ void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) translation.X = (float)targetX.Clamp(-CanvasSize.Width * (currentScale - 1), 0); translation.Y = (float)targetY.Clamp(-CanvasSize.Height * (currentScale - 1), 0); - Console.WriteLine("{0}, {1}", translation.X, translation.Y); + //Console.WriteLine("{0}, {1}", translation.X, translation.Y); axisChart.XForm.Scale = currentScale; axisChart.XForm.Offset = translation; diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index 5912b44b..fb84b488 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -214,21 +214,34 @@ public override void DrawContent(SKCanvas canvas, int width, int height) var itemSize = CalculateItemSize(nbItems, width, height, footerHeight + headerHeight + legendHeight); var barSize = CalculateBarSize(itemSize, Series.Count()); var origin = CalculateYOrigin(itemSize.Height, headerWithLegendHeight, maxValue, minValue, valRange); - DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, Margin, AnimationProgress, maxValue, valRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); + var chartMinY = MeasureHelper.CalculatePoint(Margin, 1.0f, maxValue, valRange, minValue, 0, itemSize, origin, headerHeight).Y; + + SKRect chartRect = new SKRect(canvasRect.Left+yAxisXShift+Margin, canvasRect.Top+headerWithLegendHeight, width, chartMinY); + SKRect labelRect = new SKRect(chartRect.Left, canvasRect.Top + chartMinY, chartRect.Right, canvasRect.Bottom); + SKRect yAxisRect = new SKRect(canvasRect.Left, chartRect.Top, canvasRect.Right, chartRect.Bottom); - - SKRect chartRect = new SKRect(canvasRect.Left+yAxisXShift, canvasRect.Top+headerWithLegendHeight, canvasRect.Right-yAxisXShift, canvasRect.Bottom-footerWithLegendHeight); //Clear chart bounds for testing /* canvas.Save(); canvas.Clear(SKColors.Purple); - canvas.ClipRect(chartRect); + canvas.ClipRect(yAxisRect); canvas.Clear(SKColors.Pink); canvas.Restore(); + + canvas.Save(); + canvas.ClipRect(labelRect); + canvas.Clear(SKColors.Blue); + canvas.Restore(); */ + canvas.Save(); + canvas.ClipRect(yAxisRect); + DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, XForm.Offset, XForm.Scale, Margin, AnimationProgress, maxValue, valRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); + canvas.Restore(); + + canvas.Save(); int nbSeries = series.Count(); for (int serieIndex = 0; serieIndex < nbSeries; serieIndex++) @@ -272,7 +285,12 @@ public override void DrawContent(SKCanvas canvas, int width, int height) if (!string.IsNullOrEmpty(label)) { SKRect labelSize = labelSizes[i]; - DrawHelper.DrawLabel(canvas, LabelOrientation, YPositionBehavior.None, itemSize, new SKPoint(itemX, height - footerWithLegendHeight + Margin), LabelColor, labelSize, label, LabelTextSize, Typeface); + float labelX = XForm.Offset.X + (itemX * XForm.Scale); + + canvas.Save(); + canvas.ClipRect(labelRect); + DrawHelper.DrawLabel(canvas, LabelOrientation, YPositionBehavior.None, itemSize, new SKPoint(labelX, height - footerWithLegendHeight + Margin), LabelColor, labelSize, label, LabelTextSize, Typeface); + canvas.Restore(); } } } diff --git a/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs b/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs index 9b67288d..2c021724 100644 --- a/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs +++ b/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs @@ -142,7 +142,7 @@ public override void DrawContent(SKCanvas canvas, int width, int height) var itemSize = CalculateItemSize(width, height, footerHeight, headerHeight); var origin = CalculateYOrigin(itemSize.Height, headerHeight); var points = CalculatePoints(itemSize, origin, headerHeight, yAxisXShift); - DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, Margin, AnimationProgress, MaxValue, ValueRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); + DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, new SKPoint(0,0), 1.0f, Margin, AnimationProgress, MaxValue, ValueRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); DrawAreas(canvas, points, itemSize, origin, headerHeight); DrawPoints(canvas, points); diff --git a/Sources/Microcharts/Helpers/DrawHelper.cs b/Sources/Microcharts/Helpers/DrawHelper.cs index 06a8fcdb..5f17d9cb 100644 --- a/Sources/Microcharts/Helpers/DrawHelper.cs +++ b/Sources/Microcharts/Helpers/DrawHelper.cs @@ -92,7 +92,7 @@ internal static void DrawLabel(SKCanvas canvas, Orientation orientation, YPositi } } - internal static void DrawYAxis(bool showYAxisText, bool showYAxisLines, Position yAxisPosition, SKPaint yAxisTextPaint, SKPaint yAxisLinesPaint, float margin, float animationProgress, float maxValue, float valueRange, SKCanvas canvas, int width, float yAxisXShift, List yAxisIntervalLabels, float headerHeight, SKSize itemSize, float origin) + internal static void DrawYAxis(bool showYAxisText, bool showYAxisLines, Position yAxisPosition, SKPaint yAxisTextPaint, SKPaint yAxisLinesPaint, SKPoint offset, float scale, float margin, float animationProgress, float maxValue, float valueRange, SKCanvas canvas, int width, float yAxisXShift, List yAxisIntervalLabels, float headerHeight, SKSize itemSize, float origin) { if (showYAxisText || showYAxisLines) { @@ -101,7 +101,7 @@ internal static void DrawYAxis(bool showYAxisText, bool showYAxisLines, Position .Select(t => new ValueTuple ( t.ToString(), - new SKPoint(yAxisPosition == Position.Left ? yAxisXShift : width, MeasureHelper.CalculatePoint(margin, animationProgress, maxValue, valueRange, t, cnt++, itemSize, origin, headerHeight).Y) + new SKPoint(yAxisPosition == Position.Left ? yAxisXShift : width, offset.Y+(scale * MeasureHelper.CalculatePoint(margin, animationProgress, maxValue, valueRange, t, cnt++, itemSize, origin, headerHeight).Y)) )) .ToList(); From 52dd9de84d4e2debab5d9b24d4cfc55f89440938 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Fri, 29 Oct 2021 18:12:05 -0500 Subject: [PATCH 15/22] Fixing some PR related bugs Applying correct equality check Moving label drawing into its own loop Fix label count in chart dynamic data --- .../ChartPage.xaml.cs | 2 +- Sources/Microcharts/Charts/AxisBasedChart.cs | 19 ++++++++++++------- Sources/Microcharts/Charts/Chart.cs | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs b/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs index 92e10302..644757ba 100644 --- a/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs +++ b/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs @@ -55,7 +55,7 @@ protected void GenerateDynamicData() foreach (var curSeries in series) { var entries = curSeries.Entries.ToList(); - bool addLabel = (entries.Count % 100) == 0; + bool addLabel = (entries.Count % 1000) == 0; if (s == curSeries) { diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index 68730736..b47923f1 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -213,6 +213,18 @@ public override void DrawContent(SKCanvas canvas, int width, int height) IEnumerable entries = serie.Entries; int entryCount = entries.Count(); + for (int i = 0; i < labels.Length; i++) + { + var itemX = Margin + (itemSize.Width / 2) + (i * (itemSize.Width + Margin)) + yAxisXShift; + + string label = labels[i]; + if (!string.IsNullOrEmpty(label)) + { + SKRect labelSize = labelSizes[i]; + DrawHelper.DrawLabel(canvas, LabelOrientation, YPositionBehavior.None, itemSize, new SKPoint(itemX, height - footerWithLegendHeight + Margin), LabelColor, labelSize, label, LabelTextSize, Typeface); + } + } + for (int i = 0; i < labels.Length; i++) { @@ -236,13 +248,6 @@ public override void DrawContent(SKCanvas canvas, int width, int height) { DrawNullPoint(serie, canvas); } - - string label = labels[i]; - if (!string.IsNullOrEmpty(label)) - { - SKRect labelSize = labelSizes[i]; - DrawHelper.DrawLabel(canvas, LabelOrientation, YPositionBehavior.None, itemSize, new SKPoint(itemX, height - footerWithLegendHeight + Margin), LabelColor, labelSize, label, LabelTextSize, Typeface); - } } } diff --git a/Sources/Microcharts/Charts/Chart.cs b/Sources/Microcharts/Charts/Chart.cs index 99180e81..edf0801d 100644 --- a/Sources/Microcharts/Charts/Chart.cs +++ b/Sources/Microcharts/Charts/Chart.cs @@ -615,7 +615,7 @@ protected void RaisePropertyChanged([CallerMemberName] string property = null) /// The 1st type parameter. protected bool Set(ref T field, T value, [CallerMemberName] string property = null) { - if ((field == null && value != null) || !field.Equals(value)) + if(!EqualityComparer.Default.Equals(field, value)) { field = value; RaisePropertyChanged(property); From f9d9e6a65164352dd85b8246429e4bdd12b5b02b Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Sun, 31 Oct 2021 15:05:19 -0500 Subject: [PATCH 16/22] Calculate height of Y Axis Labels and pad cliprect Calculate height of Y Axis Labels and pad cliprect --- Sources/Microcharts/Charts/AxisBasedChart.cs | 11 ++++++----- .../Microcharts/Charts/Legacy/LegacyPointChart.cs | 4 +++- Sources/Microcharts/Helpers/MeasureHelper.cs | 13 ++++++++----- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index aedd3eff..8414925a 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -188,7 +188,9 @@ public override void DrawContent(SKCanvas canvas, int width, int height) float minValue = InternalMinValue.HasValue ? InternalMinValue.Value : MinValue; //This function might change the min/max value - width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); + var yAxisSize = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); + width = (int)yAxisSize.Width; + float valRange = maxValue - minValue; var firstSerie = Series.FirstOrDefault(); @@ -218,9 +220,7 @@ public override void DrawContent(SKCanvas canvas, int width, int height) SKRect chartRect = new SKRect(canvasRect.Left+yAxisXShift+Margin, canvasRect.Top+headerWithLegendHeight, width, chartMinY); SKRect labelRect = new SKRect(chartRect.Left, canvasRect.Top + chartMinY, chartRect.Right, canvasRect.Bottom); - SKRect yAxisRect = new SKRect(canvasRect.Left, chartRect.Top, canvasRect.Right, chartRect.Bottom); - - + SKRect yAxisRect = new SKRect(canvasRect.Left, chartRect.Top - (yAxisSize.Height/2.0f), canvasRect.Right, chartRect.Bottom + (yAxisSize.Height/2.0f)); //Clear chart bounds for testing /* @@ -229,7 +229,8 @@ public override void DrawContent(SKCanvas canvas, int width, int height) canvas.ClipRect(yAxisRect); canvas.Clear(SKColors.Pink); canvas.Restore(); - + */ + /* canvas.Save(); canvas.ClipRect(labelRect); canvas.Clear(SKColors.Blue); diff --git a/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs b/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs index 2c021724..8b3da23a 100644 --- a/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs +++ b/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs @@ -130,7 +130,9 @@ public override void DrawContent(SKCanvas canvas, int width, int height) { float maxValue = MaxValue; float minValue = MinValue; - width = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, Entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, false, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); + var yAxisRect = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, Entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, false, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); + width = (int)yAxisRect.Width; + var labels = Entries.Select(x => x.Label).ToArray(); var labelSizes = MeasureHelper.MeasureTexts(labels, LabelTextSize); var footerHeight = MeasureHelper.CalculateFooterHeaderHeight(Margin, LabelTextSize, labelSizes, LabelOrientation); diff --git a/Sources/Microcharts/Helpers/MeasureHelper.cs b/Sources/Microcharts/Helpers/MeasureHelper.cs index 5d048db5..cb11efb1 100644 --- a/Sources/Microcharts/Helpers/MeasureHelper.cs +++ b/Sources/Microcharts/Helpers/MeasureHelper.cs @@ -68,8 +68,9 @@ internal static float CalculateFooterHeaderHeight(float margin, float textSize, return result; } - internal static int CalculateYAxis(bool showYAxisText, bool showYAxisLines, IEnumerable entries, int yAxisMaxTicks, SKPaint yAxisTextPaint, Position yAxisPosition, int width, bool fixedRange, ref float maxValue, ref float minValue, out float yAxisXShift, out List yAxisIntervalLabels) + internal static SKSize CalculateYAxis(bool showYAxisText, bool showYAxisLines, IEnumerable entries, int yAxisMaxTicks, SKPaint yAxisTextPaint, Position yAxisPosition, int width, bool fixedRange, ref float maxValue, ref float minValue, out float yAxisXShift, out List yAxisIntervalLabels) { + float height = 0; yAxisXShift = 0.0f; yAxisIntervalLabels = new List(); if (showYAxisText || showYAxisLines) @@ -106,20 +107,22 @@ internal static int CalculateYAxis(bool showYAxisText, bool showYAxisLines, IEnu .ToList(); var longestYAxisLabel = yAxisIntervalLabels.Aggregate(string.Empty, (max, cur) => max.Length > cur.ToString().Length ? max : cur.ToString()); - var longestYAxisLabelWidth = MeasureHelper.MeasureTexts(new string[] { longestYAxisLabel }, yAxisTextPaint).Select(b => b.Width).FirstOrDefault(); - yAxisWidth = (int)(width - longestYAxisLabelWidth); + var longestYAxisLabelRect = MeasureHelper.MeasureTexts(new string[] { longestYAxisLabel }, yAxisTextPaint).FirstOrDefault(); + + yAxisWidth = (int)(width - longestYAxisLabelRect.Width); if (yAxisPosition == Position.Left) { - yAxisXShift = longestYAxisLabelWidth; + yAxisXShift = longestYAxisLabelRect.Width; } // to reduce chart width width = yAxisWidth; + height = longestYAxisLabelRect.Height; maxValue = (float)niceMax; minValue = (float)niceMin; } - return width; + return new SKSize( width, height ); } internal static SKPoint CalculatePoint(float margin, float animationProgress, float maxValue, float valueRange, float value, int i, SKSize itemSize, float origin, float headerHeight, float originX = 0) From 3eddd6cd8f296044db715a882203e96792045364 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Sun, 31 Oct 2021 16:09:58 -0500 Subject: [PATCH 17/22] Label cleanup, and option to disable zoom EnableZoom property, XAxisMaxLabels allows to not draw all labels when over dense data points Zoom on Y Axis ticks and X Axis labels --- Sources/Microcharts.Forms/ChartView.cs | 13 ++++-- .../ChartPage.xaml.cs | 5 --- Sources/Microcharts.Samples/Data.cs | 4 +- Sources/Microcharts/Charts/AxisBasedChart.cs | 42 ++++++++++++++----- 4 files changed, 43 insertions(+), 21 deletions(-) diff --git a/Sources/Microcharts.Forms/ChartView.cs b/Sources/Microcharts.Forms/ChartView.cs index 0ccefc8d..c8ec1604 100644 --- a/Sources/Microcharts.Forms/ChartView.cs +++ b/Sources/Microcharts.Forms/ChartView.cs @@ -17,10 +17,6 @@ public ChartView() { this.BackgroundColor = Color.Transparent; this.PaintSurface += OnPaintCanvas; - - var pinchGesture = new PinchGestureRecognizer(); - pinchGesture.PinchUpdated += OnPinchUpdated; - GestureRecognizers.Add(pinchGesture); } public event EventHandler ChartPainted; @@ -68,6 +64,15 @@ private static void OnChartChanged(BindableObject d, object oldValue, object val if (view.chart != null) { + AxisBasedChart axisChart = view.chart as AxisBasedChart; + if( axisChart != null && axisChart.EnableZoom ) + { + //FIXME: how to handle disable zoom after already enabled + var pinchGesture = new PinchGestureRecognizer(); + pinchGesture.PinchUpdated += view.OnPinchUpdated; + view.GestureRecognizers.Add(pinchGesture); + } + view.handler = view.chart.ObserveInvalidate(view, (v) => v.InvalidateSurface()); } } diff --git a/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs b/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs index 644757ba..cc3c6df7 100644 --- a/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs +++ b/Sources/Microcharts.Samples.Forms/Microcharts.Samples.Forms/ChartPage.xaml.cs @@ -55,13 +55,10 @@ protected void GenerateDynamicData() foreach (var curSeries in series) { var entries = curSeries.Entries.ToList(); - bool addLabel = (entries.Count % 1000) == 0; if (s == curSeries) { var entry = Data.GenerateTimeSeriesEntry(r, idx, 1); - if (!addLabel) entry.First().Label = null; - entries.AddRange(entry); if (entries.Count() > count * 1.5) entries.RemoveAt(0); @@ -69,8 +66,6 @@ protected void GenerateDynamicData() else { var entry = new ChartEntry(null) { ValueLabel = null, Label = label }; - if (!addLabel) entry.Label = null; - entries.Add(entry); if (entries.Count() > count * 1.5) entries.RemoveAt(0); } diff --git a/Sources/Microcharts.Samples/Data.cs b/Sources/Microcharts.Samples/Data.cs index f5da69a5..ab5c00f9 100644 --- a/Sources/Microcharts.Samples/Data.cs +++ b/Sources/Microcharts.Samples/Data.cs @@ -1340,6 +1340,7 @@ private static IEnumerable GenerateLineSeriesChartExample() LabelOrientation = Orientation.Vertical, ValueLabelOrientation = Orientation.Vertical, LabelTextSize = 14, + EnableZoom = true, LineMode = LineMode.Straight, PointMode = PointMode.None, LineAreaAlpha = 0, @@ -1352,6 +1353,7 @@ private static IEnumerable GenerateLineSeriesChartExample() MaxValue = 150, MinValue = -150, YAxisPosition = Position.Left, + XAxisMaxLabels = 20, LegendOption = SeriesLegendOption.Bottom, Series = new List() @@ -1512,7 +1514,7 @@ public static IEnumerable GenerateTimeSeriesEntry( Random r, int idx do { if (withNulls && (value.Value % 10) == 0) value = null; - entries.Add(new ChartEntry(value) { ValueLabel = value.ToString(), Label = count % 1000 == 0 ? label.ToString("mm:ss") : null }); + entries.Add(new ChartEntry(value) { ValueLabel = value.ToString(), Label =label.ToString("mm:ss") }); valOffset = ((label - baseTime).TotalSeconds + phase) * valScale; ampOffset = ((label - baseTime).TotalSeconds + phase) * ampScale; value = valueShift + (float)(Math.Sin(valOffset) * (Math.Cos(ampOffset) * amp)); diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index 8414925a..0a156259 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -130,6 +130,13 @@ public float SerieLabelTextSize set => Set(ref serieLabelTextSize, value); } + /// + /// Determines whether pinch to zoom will work on the chart + /// + public bool EnableZoom { get; set; } = false; + + public ChartXForm XForm { get; } = new ChartXForm(); + /// /// Show Y Axis Text? /// @@ -162,7 +169,10 @@ public float SerieLabelTextSize public SKPaint YAxisLinesPaint { get; set; } - public ChartXForm XForm { get; } = new ChartXForm(); + /// + /// How many labels to draw, -1 means all of them + /// + public int XAxisMaxLabels { get; set; } = -1; #endregion @@ -188,7 +198,8 @@ public override void DrawContent(SKCanvas canvas, int width, int height) float minValue = InternalMinValue.HasValue ? InternalMinValue.Value : MinValue; //This function might change the min/max value - var yAxisSize = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); + int yMaxTicks = (int)(YAxisMaxTicks * XForm.Scale); + var yAxisSize = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, yMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); width = (int)yAxisSize.Width; float valRange = maxValue - minValue; @@ -238,7 +249,7 @@ public override void DrawContent(SKCanvas canvas, int width, int height) */ canvas.Save(); - canvas.ClipRect(yAxisRect); + if(EnableZoom) canvas.ClipRect(yAxisRect); DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, XForm.Offset, XForm.Scale, Margin, AnimationProgress, maxValue, valRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); canvas.Restore(); @@ -252,9 +263,11 @@ public override void DrawContent(SKCanvas canvas, int width, int height) int entryCount = entries.Count(); canvas.Save(); - canvas.ClipRect(labelRect); + if (EnableZoom) canvas.ClipRect(labelRect); - for (int i = 0; i < labels.Length; i++) + int xMaxLabels = (int)(XAxisMaxLabels * XForm.Scale); + int xlabelSkip = XAxisMaxLabels > 0 ? labels.Length / xMaxLabels : 1; + for (int i = 0; i < labels.Length; i+= xlabelSkip) { var itemX = Margin + (itemSize.Width / 2) + (i * (itemSize.Width + Margin)) + yAxisXShift; float labelX = XForm.Offset.X + (itemX * XForm.Scale); @@ -269,9 +282,13 @@ public override void DrawContent(SKCanvas canvas, int width, int height) canvas.Restore(); canvas.Save(); - canvas.ClipRect(chartRect); - canvas.Translate(XForm.Offset); - canvas.Scale(XForm.Scale); + if (EnableZoom) + { + canvas.ClipRect(chartRect); + canvas.Translate(XForm.Offset); + canvas.Scale(XForm.Scale); + } + for (int i = 0; i < labels.Length; i++) { if (i >= entryCount) break; @@ -304,9 +321,12 @@ public override void DrawContent(SKCanvas canvas, int width, int height) DrawLegend(canvas, seriesSizes, legendHeight, height, width); canvas.Save(); - canvas.ClipRect(chartRect); - canvas.Translate(XForm.Offset); - canvas.Scale(XForm.Scale); + if (EnableZoom) + { + canvas.ClipRect(chartRect); + canvas.Translate(XForm.Offset); + canvas.Scale(XForm.Scale); + } OnDrawContentEnd(canvas, itemSize, origin, valueLabelSizes); canvas.Restore(); } From 684c92ca2cc69fc0f4897d52c128072b75f89871 Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Sun, 31 Oct 2021 16:32:57 -0500 Subject: [PATCH 18/22] Fixing YAxis format on zoom --- Sources/Microcharts/Charts/AxisBasedChart.cs | 5 +++-- Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs | 7 ++++--- Sources/Microcharts/Helpers/DrawHelper.cs | 4 ++-- Sources/Microcharts/Helpers/MeasureHelper.cs | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index 0a156259..426b0bc8 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -198,8 +198,9 @@ public override void DrawContent(SKCanvas canvas, int width, int height) float minValue = InternalMinValue.HasValue ? InternalMinValue.Value : MinValue; //This function might change the min/max value + string yAxisFormat = XForm.Scale == 1.0f ? "G" : "F1"; int yMaxTicks = (int)(YAxisMaxTicks * XForm.Scale); - var yAxisSize = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, entries, yMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); + var yAxisSize = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, yAxisFormat, yMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); width = (int)yAxisSize.Width; float valRange = maxValue - minValue; @@ -250,7 +251,7 @@ public override void DrawContent(SKCanvas canvas, int width, int height) canvas.Save(); if(EnableZoom) canvas.ClipRect(yAxisRect); - DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, XForm.Offset, XForm.Scale, Margin, AnimationProgress, maxValue, valRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); + DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, yAxisFormat, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, XForm.Offset, XForm.Scale, Margin, AnimationProgress, maxValue, valRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); canvas.Restore(); diff --git a/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs b/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs index 8b3da23a..d3ff206c 100644 --- a/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs +++ b/Sources/Microcharts/Charts/Legacy/LegacyPointChart.cs @@ -130,8 +130,9 @@ public override void DrawContent(SKCanvas canvas, int width, int height) { float maxValue = MaxValue; float minValue = MinValue; - var yAxisRect = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, Entries, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, false, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); - width = (int)yAxisRect.Width; + string yAxisFormat = "G"; + var yAxisSize = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, yAxisFormat, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, false, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); + width = (int)yAxisSize.Width; var labels = Entries.Select(x => x.Label).ToArray(); var labelSizes = MeasureHelper.MeasureTexts(labels, LabelTextSize); @@ -144,7 +145,7 @@ public override void DrawContent(SKCanvas canvas, int width, int height) var itemSize = CalculateItemSize(width, height, footerHeight, headerHeight); var origin = CalculateYOrigin(itemSize.Height, headerHeight); var points = CalculatePoints(itemSize, origin, headerHeight, yAxisXShift); - DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, new SKPoint(0,0), 1.0f, Margin, AnimationProgress, MaxValue, ValueRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); + DrawHelper.DrawYAxis(ShowYAxisText, ShowYAxisLines, yAxisFormat, YAxisPosition, YAxisTextPaint, YAxisLinesPaint, new SKPoint(0,0), 1.0f, Margin, AnimationProgress, MaxValue, ValueRange, canvas, width, yAxisXShift, yAxisIntervalLabels, headerHeight, itemSize, origin); DrawAreas(canvas, points, itemSize, origin, headerHeight); DrawPoints(canvas, points); diff --git a/Sources/Microcharts/Helpers/DrawHelper.cs b/Sources/Microcharts/Helpers/DrawHelper.cs index 5f17d9cb..833305cf 100644 --- a/Sources/Microcharts/Helpers/DrawHelper.cs +++ b/Sources/Microcharts/Helpers/DrawHelper.cs @@ -92,7 +92,7 @@ internal static void DrawLabel(SKCanvas canvas, Orientation orientation, YPositi } } - internal static void DrawYAxis(bool showYAxisText, bool showYAxisLines, Position yAxisPosition, SKPaint yAxisTextPaint, SKPaint yAxisLinesPaint, SKPoint offset, float scale, float margin, float animationProgress, float maxValue, float valueRange, SKCanvas canvas, int width, float yAxisXShift, List yAxisIntervalLabels, float headerHeight, SKSize itemSize, float origin) + internal static void DrawYAxis(bool showYAxisText, bool showYAxisLines, string yAxisFormat, Position yAxisPosition, SKPaint yAxisTextPaint, SKPaint yAxisLinesPaint, SKPoint offset, float scale, float margin, float animationProgress, float maxValue, float valueRange, SKCanvas canvas, int width, float yAxisXShift, List yAxisIntervalLabels, float headerHeight, SKSize itemSize, float origin) { if (showYAxisText || showYAxisLines) { @@ -100,7 +100,7 @@ internal static void DrawYAxis(bool showYAxisText, bool showYAxisLines, Position var intervals = yAxisIntervalLabels .Select(t => new ValueTuple ( - t.ToString(), + t.ToString(yAxisFormat), new SKPoint(yAxisPosition == Position.Left ? yAxisXShift : width, offset.Y+(scale * MeasureHelper.CalculatePoint(margin, animationProgress, maxValue, valueRange, t, cnt++, itemSize, origin, headerHeight).Y)) )) .ToList(); diff --git a/Sources/Microcharts/Helpers/MeasureHelper.cs b/Sources/Microcharts/Helpers/MeasureHelper.cs index cb11efb1..7fc78d3d 100644 --- a/Sources/Microcharts/Helpers/MeasureHelper.cs +++ b/Sources/Microcharts/Helpers/MeasureHelper.cs @@ -68,7 +68,7 @@ internal static float CalculateFooterHeaderHeight(float margin, float textSize, return result; } - internal static SKSize CalculateYAxis(bool showYAxisText, bool showYAxisLines, IEnumerable entries, int yAxisMaxTicks, SKPaint yAxisTextPaint, Position yAxisPosition, int width, bool fixedRange, ref float maxValue, ref float minValue, out float yAxisXShift, out List yAxisIntervalLabels) + internal static SKSize CalculateYAxis(bool showYAxisText, bool showYAxisLines, string yAxisFormat, int yAxisMaxTicks, SKPaint yAxisTextPaint, Position yAxisPosition, int width, bool fixedRange, ref float maxValue, ref float minValue, out float yAxisXShift, out List yAxisIntervalLabels) { float height = 0; yAxisXShift = 0.0f; @@ -106,7 +106,7 @@ internal static SKSize CalculateYAxis(bool showYAxisText, bool showYAxisLines, I .Select(i => (float)(niceMax - (i * tickSpacing))) .ToList(); - var longestYAxisLabel = yAxisIntervalLabels.Aggregate(string.Empty, (max, cur) => max.Length > cur.ToString().Length ? max : cur.ToString()); + var longestYAxisLabel = yAxisIntervalLabels.Aggregate(string.Empty, (max, cur) => max.Length > cur.ToString(yAxisFormat).Length ? max : cur.ToString(yAxisFormat)); var longestYAxisLabelRect = MeasureHelper.MeasureTexts(new string[] { longestYAxisLabel }, yAxisTextPaint).FirstOrDefault(); yAxisWidth = (int)(width - longestYAxisLabelRect.Width); From 5fd878fd65dca78530158ce9811ffafce9f8ab9b Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Sun, 31 Oct 2021 17:12:23 -0500 Subject: [PATCH 19/22] Adjusting yAxis based on scale --- Sources/Microcharts/Charts/AxisBasedChart.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index 426b0bc8..7b8ec7d9 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -133,7 +133,7 @@ public float SerieLabelTextSize /// /// Determines whether pinch to zoom will work on the chart /// - public bool EnableZoom { get; set; } = false; + public bool EnableZoom { get; set; } = true; //FIXME: This should be off by default, but easier to test charts with it on public ChartXForm XForm { get; } = new ChartXForm(); @@ -197,10 +197,22 @@ public override void DrawContent(SKCanvas canvas, int width, int height) float maxValue = InternalMaxValue.HasValue ? InternalMaxValue.Value : MaxValue; float minValue = InternalMinValue.HasValue ? InternalMinValue.Value : MinValue; - //This function might change the min/max value - string yAxisFormat = XForm.Scale == 1.0f ? "G" : "F1"; + //FIXME: If you don't mark it as fixed range, the vertical scale can change while zooming based on rounding in NiceNum + //This causes a POP in scale, so we calculated the Min/Max based on a scale of 1.0 and mark it has fixed + + float yAxisXShift; + List yAxisIntervalLabels; + string yAxisFormat = XForm.Scale == 1.0f ? "G" : "F1"; //As we scale the yAxis we get more decimal places + if (EnableZoom && !fixedRange) + { + //WARNING: This will change Min/Max based on a scale of 1.0 + MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, yAxisFormat, YAxisMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out yAxisXShift, out yAxisIntervalLabels); + fixedRange = true; + } + + //WARNING: This will change Min/Max based on a scale of XForm.Scale int yMaxTicks = (int)(YAxisMaxTicks * XForm.Scale); - var yAxisSize = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, yAxisFormat, yMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out float yAxisXShift, out List yAxisIntervalLabels); + var yAxisSize = MeasureHelper.CalculateYAxis(ShowYAxisText, ShowYAxisLines, yAxisFormat, yMaxTicks, YAxisTextPaint, YAxisPosition, width, fixedRange, ref maxValue, ref minValue, out yAxisXShift, out yAxisIntervalLabels); width = (int)yAxisSize.Width; float valRange = maxValue - minValue; From 1ad1196f93ae52ca69132e4f704b8ae7a28ddb6f Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Sun, 31 Oct 2021 18:52:52 -0500 Subject: [PATCH 20/22] Adding support for panning Adding support for panning --- Sources/Microcharts.Forms/ChartView.cs | 59 +++++++++++++++----------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/Sources/Microcharts.Forms/ChartView.cs b/Sources/Microcharts.Forms/ChartView.cs index c8ec1604..c89c5c96 100644 --- a/Sources/Microcharts.Forms/ChartView.cs +++ b/Sources/Microcharts.Forms/ChartView.cs @@ -92,10 +92,12 @@ private void OnPaintCanvas(object sender, SKPaintSurfaceEventArgs e) } - float currentScale = 1; - float startScale = 1; - SKPoint offsetPosition = new SKPoint(0, 0); - SKPoint translation = new SKPoint(0, 0); + double zoomCurScale = 1; + double zoomStartScale = 1; + + SKPoint zoomStartOrigin = new SKPoint(0, 0); + SKPoint zoomStartOffset = new SKPoint(0, 0); + SKPoint zoomTranslation = new SKPoint(0, 0); void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) { @@ -106,51 +108,60 @@ void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) { // Store the current scale factor applied to the wrapped user interface element, // and zero the components for the center point of the translate transform. - startScale = currentScale; + zoomStartScale = zoomCurScale = axisChart.XForm.Scale; + zoomStartOrigin = new SKPoint( (float)e.ScaleOrigin.X, (float)e.ScaleOrigin.Y); + zoomStartOffset = axisChart.XForm.Offset; } + if (e.Status == GestureStatus.Running) { + // e.Scale is the delta to be applied for the current frame // Calculate the scale factor to be applied. - currentScale += (float)((e.Scale - 1) * startScale); - currentScale = Math.Max(1, currentScale); - currentScale = Math.Min(5, currentScale); + zoomCurScale += (e.Scale - 1) * zoomStartScale; + zoomCurScale = Math.Max(1, zoomCurScale); + zoomCurScale = Math.Min(5, zoomCurScale); + + + SKPoint zoomPanDelta = new SKPoint((float)((e.ScaleOrigin.X - zoomStartOrigin.X) * CanvasSize.Width), (float)((e.ScaleOrigin.Y - zoomStartOrigin.Y) * CanvasSize.Height)); // The ScaleOrigin is in relative coordinates to the wrapped user interface element, // so get the X pixel coordinate. - double renderedX = X + offsetPosition.X; + double renderedX = X + zoomStartOffset.X; double deltaX = renderedX / CanvasSize.Width; - double deltaWidth = CanvasSize.Width / (CanvasSize.Width * startScale); + double deltaWidth = CanvasSize.Width / (CanvasSize.Width * zoomStartScale); double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth; + //Console.WriteLine("ScaleOrigin: {0}, {1}", e.ScaleOrigin.X, e.ScaleOrigin.Y); + // The ScaleOrigin is in relative coordinates to the wrapped user interface element, // so get the Y pixel coordinate. - double renderedY = Y + offsetPosition.Y; + double renderedY = Y + zoomStartOffset.Y; double deltaY = renderedY / CanvasSize.Height; - double deltaHeight = CanvasSize.Height / (CanvasSize.Height * startScale); + double deltaHeight = CanvasSize.Height / (CanvasSize.Height * zoomStartScale); double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight; // Calculate the transformed element pixel coordinates. - double targetX = offsetPosition.X - (originX * CanvasSize.Width) * (currentScale - startScale); - double targetY = offsetPosition.Y - (originY * CanvasSize.Height) * (currentScale - startScale); + double targetX = zoomStartOffset.X - (originX * CanvasSize.Width) * (zoomCurScale - zoomStartScale); + double targetY = zoomStartOffset.Y - (originY * CanvasSize.Height) * (zoomCurScale - zoomStartScale); // Apply translation based on the change in origin. - translation.X = (float)targetX.Clamp(-CanvasSize.Width * (currentScale - 1), 0); - translation.Y = (float)targetY.Clamp(-CanvasSize.Height * (currentScale - 1), 0); + zoomTranslation.X = (float)targetX; + zoomTranslation.Y = (float)targetY; + - //Console.WriteLine("{0}, {1}", translation.X, translation.Y); + SKPoint final = zoomTranslation + zoomPanDelta; + final.X = Math.Min(Math.Max(final.X, -CanvasSize.Width * (float)(zoomCurScale - 1)), 0); + final.Y = Math.Min(Math.Max(final.Y, -CanvasSize.Height * (float)(zoomCurScale - 1)), 0); + - axisChart.XForm.Scale = currentScale; - axisChart.XForm.Offset = translation; + axisChart.XForm.Scale = (float)zoomCurScale; + axisChart.XForm.Offset = final; InvalidateSurface(); } + if (e.Status == GestureStatus.Completed) { - // Store the translation delta's of the wrapped user interface element. - offsetPosition = translation; - - axisChart.XForm.Scale = currentScale; - axisChart.XForm.Offset = translation; InvalidateSurface(); } } From 0819d5de277a5c3be160fca68a024e180960c57b Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Sun, 31 Oct 2021 18:53:44 -0500 Subject: [PATCH 21/22] Don't enable for every chart by default --- Sources/Microcharts/Charts/AxisBasedChart.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Microcharts/Charts/AxisBasedChart.cs b/Sources/Microcharts/Charts/AxisBasedChart.cs index 7b8ec7d9..40e1bfb1 100644 --- a/Sources/Microcharts/Charts/AxisBasedChart.cs +++ b/Sources/Microcharts/Charts/AxisBasedChart.cs @@ -133,7 +133,7 @@ public float SerieLabelTextSize /// /// Determines whether pinch to zoom will work on the chart /// - public bool EnableZoom { get; set; } = true; //FIXME: This should be off by default, but easier to test charts with it on + public bool EnableZoom { get; set; } = false; //FIXME: This should be off by default, but easier to test charts with it on public ChartXForm XForm { get; } = new ChartXForm(); From f6fd13a5104b97ed9898b905882dd29255f8502c Mon Sep 17 00:00:00 2001 From: Brett Estabrook Date: Sun, 31 Oct 2021 19:13:25 -0500 Subject: [PATCH 22/22] Standardizing on precision --- Sources/Microcharts.Forms/ChartView.cs | 44 ++++++++++++-------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/Sources/Microcharts.Forms/ChartView.cs b/Sources/Microcharts.Forms/ChartView.cs index c89c5c96..40876c80 100644 --- a/Sources/Microcharts.Forms/ChartView.cs +++ b/Sources/Microcharts.Forms/ChartView.cs @@ -92,8 +92,8 @@ private void OnPaintCanvas(object sender, SKPaintSurfaceEventArgs e) } - double zoomCurScale = 1; - double zoomStartScale = 1; + float zoomCurScale = 1; + float zoomStartScale = 1; SKPoint zoomStartOrigin = new SKPoint(0, 0); SKPoint zoomStartOffset = new SKPoint(0, 0); @@ -115,45 +115,43 @@ void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) if (e.Status == GestureStatus.Running) { + float eScale = (float)e.Scale; + SKPoint eScaleOrigin = new SKPoint((float)e.ScaleOrigin.X, (float)e.ScaleOrigin.Y); + SKPoint canvasSize = new SKPoint(CanvasSize.Width, CanvasSize.Height); + // e.Scale is the delta to be applied for the current frame // Calculate the scale factor to be applied. - zoomCurScale += (e.Scale - 1) * zoomStartScale; + zoomCurScale += (eScale - 1) * zoomStartScale; zoomCurScale = Math.Max(1, zoomCurScale); zoomCurScale = Math.Min(5, zoomCurScale); - - SKPoint zoomPanDelta = new SKPoint((float)((e.ScaleOrigin.X - zoomStartOrigin.X) * CanvasSize.Width), (float)((e.ScaleOrigin.Y - zoomStartOrigin.Y) * CanvasSize.Height)); + SKPoint zoomPanDelta = new SKPoint((eScaleOrigin.X - zoomStartOrigin.X) * CanvasSize.Width, (eScaleOrigin.Y - zoomStartOrigin.Y) * CanvasSize.Height); // The ScaleOrigin is in relative coordinates to the wrapped user interface element, // so get the X pixel coordinate. - double renderedX = X + zoomStartOffset.X; - double deltaX = renderedX / CanvasSize.Width; - double deltaWidth = CanvasSize.Width / (CanvasSize.Width * zoomStartScale); - double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth; + float renderedX = (float)X + zoomStartOffset.X; + float deltaX = renderedX / CanvasSize.Width; + float deltaWidth = CanvasSize.Width / (CanvasSize.Width * zoomStartScale); + float originX = (eScaleOrigin.X - deltaX) * deltaWidth; //Console.WriteLine("ScaleOrigin: {0}, {1}", e.ScaleOrigin.X, e.ScaleOrigin.Y); // The ScaleOrigin is in relative coordinates to the wrapped user interface element, // so get the Y pixel coordinate. - double renderedY = Y + zoomStartOffset.Y; - double deltaY = renderedY / CanvasSize.Height; - double deltaHeight = CanvasSize.Height / (CanvasSize.Height * zoomStartScale); - double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight; + float renderedY = (float)Y + zoomStartOffset.Y; + float deltaY = renderedY / CanvasSize.Height; + float deltaHeight = CanvasSize.Height / (CanvasSize.Height * zoomStartScale); + float originY = (eScaleOrigin.Y - deltaY) * deltaHeight; // Calculate the transformed element pixel coordinates. - double targetX = zoomStartOffset.X - (originX * CanvasSize.Width) * (zoomCurScale - zoomStartScale); - double targetY = zoomStartOffset.Y - (originY * CanvasSize.Height) * (zoomCurScale - zoomStartScale); - - // Apply translation based on the change in origin. - zoomTranslation.X = (float)targetX; - zoomTranslation.Y = (float)targetY; - + zoomTranslation.X = zoomStartOffset.X - (originX * CanvasSize.Width) * (zoomCurScale - zoomStartScale); + zoomTranslation.Y = zoomStartOffset.Y - (originY * CanvasSize.Height) * (zoomCurScale - zoomStartScale); + // Calculate final translation with pan, and clamp the whole thing SKPoint final = zoomTranslation + zoomPanDelta; - final.X = Math.Min(Math.Max(final.X, -CanvasSize.Width * (float)(zoomCurScale - 1)), 0); - final.Y = Math.Min(Math.Max(final.Y, -CanvasSize.Height * (float)(zoomCurScale - 1)), 0); + final.X = Math.Min(Math.Max(final.X, -CanvasSize.Width * (zoomCurScale - 1)), 0); + final.Y = Math.Min(Math.Max(final.Y, -CanvasSize.Height * (zoomCurScale - 1)), 0); - axisChart.XForm.Scale = (float)zoomCurScale; axisChart.XForm.Offset = final; InvalidateSurface();