Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/296-pinch-and-zoom #297

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0a86788
Merge pull request #1 from brett-estabrook/feature/36-null-value-support
brett-estabrook Oct 11, 2021
fe9ab42
Initial commit of max value caching
brett-estabrook Oct 11, 2021
00782b9
Cleanup Data and Logging
brett-estabrook Oct 11, 2021
c9f8fde
Updating min/max value handling
brett-estabrook Oct 13, 2021
5ea7ae9
Fixing property changed events
brett-estabrook Oct 13, 2021
5725af3
yAxisShift fix for bar
brett-estabrook Oct 13, 2021
72e41df
Adding some comments
brett-estabrook Oct 13, 2021
ede48dd
Making the values match the labels
brett-estabrook Oct 13, 2021
2d6f4e5
Fixing issue with null values
brett-estabrook Oct 13, 2021
e0f1c37
Adding some comments about min/max values
brett-estabrook Oct 13, 2021
257e712
Merge branch 'feature/36-null-value-support' into feature/290-max-val…
brett-estabrook Oct 17, 2021
9a570c7
Merge branch 'feature/36-null-value-support' into feature/290-max-val…
brett-estabrook Oct 18, 2021
695537c
Merge branch 'feature/36-null-value-support' into feature/290-max-val…
brett-estabrook Oct 18, 2021
744dda3
Revert "Merge branch 'feature/36-null-value-support' into feature/290…
brett-estabrook Oct 18, 2021
59659ab
Making labels match values
brett-estabrook Oct 18, 2021
db4d0e7
Initial support for ClipRect and Pinch to Zoom
brett-estabrook Oct 18, 2021
5d932b1
Merge branch 'main' into feature/290-max-value-perf-fixes
brett-estabrook Oct 20, 2021
f73549d
Addressing an issue with line charts a indexing into draw points
brett-estabrook Oct 20, 2021
223b2a7
Merge branch 'feature/290-max-value-perf-fixes' into poc/min-value-cl…
brett-estabrook Oct 20, 2021
f64c4c2
Handle scaling drawing of axis labels
brett-estabrook Oct 21, 2021
52dd9de
Fixing some PR related bugs
brett-estabrook Oct 29, 2021
583dd54
Merge branch 'feature/290-max-value-perf-fixes' into poc/min-value-cl…
brett-estabrook Oct 31, 2021
f9d9e6a
Calculate height of Y Axis Labels and pad cliprect
brett-estabrook Oct 31, 2021
3eddd6c
Label cleanup, and option to disable zoom
brett-estabrook Oct 31, 2021
684c92c
Fixing YAxis format on zoom
brett-estabrook Oct 31, 2021
5fd878f
Adjusting yAxis based on scale
brett-estabrook Oct 31, 2021
1ad1196
Adding support for panning
brett-estabrook Oct 31, 2021
0819d5d
Don't enable for every chart by default
brett-estabrook Oct 31, 2021
f6fd13a
Standardizing on precision
brett-estabrook Nov 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions Sources/Microcharts.Forms/ChartView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ namespace Microcharts.Forms
using Xamarin.Forms;
using SkiaSharp.Views.Forms;
using SkiaSharp;
using System;
using Xamarin.Forms.Internals;

public class ChartView : SKCanvasView
{
Expand All @@ -17,6 +19,8 @@ public ChartView()
this.PaintSurface += OnPaintCanvas;
}

public event EventHandler<SKPaintSurfaceEventArgs> ChartPainted;

#endregion

#region Static fields
Expand Down Expand Up @@ -60,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();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be moved into the AxisBasedChart and then have the ChartView call an init function on the chart.

pinchGesture.PinchUpdated += view.OnPinchUpdated;
view.GestureRecognizers.Add(pinchGesture);
}

view.handler = view.chart.ObserveInvalidate(view, (v) => v.InvalidateSurface());
}
}
Expand All @@ -74,6 +87,81 @@ private void OnPaintCanvas(object sender, SKPaintSurfaceEventArgs e)
{
e.Surface.Canvas.Clear(SKColors.Transparent);
}

ChartPainted?.Invoke(sender, e);
}


float zoomCurScale = 1;
float 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)
{
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.
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)
{
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 += (eScale - 1) * zoomStartScale;
Copy link

@Ghostbird Ghostbird Apr 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked into this too much, but this seems overcomplicated.
It seems you're treating scaling as addition here, which means you need to do a -1 and treat it as an additive delta relative to a start. Scale operations are multiplicative. I made a very similar component once, and in my code I had a single line that said currentScale *= e.Scale. I think you can apply that logic to your code too. Treat e.Scale directly as a multiplicative delta and I think you can simplify.

zoomCurScale = Math.Max(1, zoomCurScale);
zoomCurScale = Math.Min(5, zoomCurScale);

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.
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.
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.
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 * (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();

}

if (e.Status == GestureStatus.Completed)
{
InvalidateSurface();
}
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -51,14 +51,16 @@ protected void GenerateDynamicData()
{
var label = DateTime.Now.ToString("mm:ss");

int idx = 0;
foreach (var curSeries in series)
{
var entries = curSeries.Entries.ToList();

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);
entries.AddRange(entry);

if (entries.Count() > count * 1.5) entries.RemoveAt(0);
}
else
Expand All @@ -67,21 +69,27 @@ protected void GenerateDynamicData()
entries.Add(entry);
if (entries.Count() > count * 1.5) entries.RemoveAt(0);
}

curSeries.Entries = entries;
idx++;
}

if (!lc.IsAnimating)
{
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);
}
}

Expand All @@ -90,7 +98,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 )
Expand Down
80 changes: 60 additions & 20 deletions Sources/Microcharts.Samples/Data.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,28 +244,28 @@ private static ChartEntry[] GenerateDefaultXamarinEntries()
{
return new[]
{
new ChartEntry(212)
new ChartEntry(112)
{
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)
Expand Down Expand Up @@ -1338,28 +1338,55 @@ private static IEnumerable<ExampleChartItem> GenerateLineSeriesChartExample()
Chart = new LineChart
{
LabelOrientation = Orientation.Vertical,
ValueLabelOrientation = Orientation.Horizontal,
ValueLabelOrientation = Orientation.Vertical,
LabelTextSize = 14,
EnableZoom = true,
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,
XAxisMaxLabels = 20,
LegendOption = SeriesLegendOption.Bottom,

Series = new List<ChartSerie>()
{
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)
}
}
}
Expand Down Expand Up @@ -1466,20 +1493,33 @@ private static IEnumerable<ExampleChartItem> GenerateBarChartExample()
yield break;
}

private static IEnumerable<ChartEntry> GenerateTimeSeriesEntry( Random r, bool withNulls = true)
public static IEnumerable<ChartEntry> GenerateTimeSeriesEntry( Random r, int idx, int seconds, bool withNulls = true)
{
List<ChartEntry> entries = new List<ChartEntry>();

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 =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));
label = label.AddSeconds(1);
count++;
}
while (label <= end);

Expand Down
Loading