The standard UWP XAML controls are covering most of the use case rather nicely, but there are gaps on support when it comes to visualization of data. For example there are no readily available controls showing data as a graph.
One of customers then needed a simple demo application, in which they wanted to show dome data with line graph, so I went to designing one for them. To develop and test this control, I decided to create an app that could read heartrate sensor and then visualize the data in the screen.
I did find a very good starting point for this exercise from Microsoft examples and downloaded the Bluetooth Generic Attribute Profile – Heart Rate Service example from code.msdn.microsoft.com
I did took the BLE service parts from it and did modify them to suit my requirements better, but the main thing from there was the ChartControl it implemented. It’s basically a very simple control that draws the heart rate sensor data into a UI, it is not implementing any additional functionality such as zooming etc.
All and all, it made a great starting point. I copied the file over to a new project, and started adding zooming into it. I also did quite a lot of re-structuring as well as changed how the background was drawn. While keeping most of the architecture intact I did add loads of constants, so it would be easier to customize the control for different usages. You can find the final version in ChartControl.cs file in GitHub.
Then I needed to add zooming into the control. I also wanted to have a way on showing the current heart rate as well as a way user could save the data gathered into a file. These generally would require some UI changes, and I also wanted to keep the original ChartControl control in its original form, just to be able to offer a very basic control for those users who would need one.
So I made another control I named ChartControlFull. This control has XAML part as well, so defining UI’s was just normal XAML stuff. I designed the data saving to be handled in a way that the page to which the control is added needs to implement delegate to be called once user presses the save button. Then in the delegate, the actual data can be fetched using getDataString() function. The function return comma separated string where the data is in ushort format. The data returned is always just the visible area in the control, so users can use zooming to select how much data will be stored. The actual file etc. storing need to be handled by the calling page’s logic. You can find the final versions for ChartControlFull.xaml, and ChartControlFull.xaml.cs files in GitHub. Video below shows how this control looks & behaves.
This control works fine when we are just showing the incoming Heart rate data, since in general the data amount is rather small and the rate the data is coming in is rather slow. I did also add 0.3 second delay on re-draw after zoom button presses, and if zoom is changed within the time, the previous issued re-drawing is cancelled, this is general makes the zooming effect to work faster, since all rapid changes are buffered before executing the slow full re-draw.
The slowness comes from the fact that the control will always re-draw all parts when anything changes, i.e. whether we change the zoom, or we get new data. This means that if we have loads of data (I tested with around 15’000 data points), it becomes really slow. Also while testing it, I noticed that I’m not getting the pointer events delivered consistently, making it rather impossible to implement any reliable gesture actions.
First though I had for fixing this was to try drawing the graph into a bigger sized canvas object, and then rendering the canvas into an image utilizing RenderTargetBitmap API. The API is great and simple to use, but as documented in XAML visuals and RenderTargetBitmap capture capabilities it can only render visible UI components, thus it would not work on my use case.
Luckily I did find an API which is 100% match for my requirements, and it’s available through NuGet. This API is called Win2D, and it allows creating off-screen canvas object, which can be drawn into the UI when needed. Also it does allows creating multiple objects and then combining them in the drawing function.
I named the new control as ChartWin2DControl, and you can find the implementation from ChartWin2DControl.xaml, and ChartWin2DControl.xaml.cs files in GitHub. Video below shows how this control looks & behaves.
The basic drawing implementation is still the same as was used with the original ChartControl, but with this implementation I’m using 2 off-screen canvas objects. The first object is having same size as the control has. This is required since it will be hosting the background lines as well as the value texts, which should not be scaled.
The second off-screen canvas then has transparent background, and only thing that gets drawn to it, is the actual line Graph data. Since the control is not implementing any vertical zoom effects, the height of the canvas is same as with the control. The optimal value for the width in pixels then would be at least the amount of data points we have. This would allow the drawing code to move the x-axis to the right with each data item.
Unfortunately different hardware’s are having different maximum width/height restrictions, thus the actual width of the canvas is determined by the value returned by CanvasDevice.MaximumBitmapSizeInPixels Property.
For gestures, as this control should work also with UWP apps run with devices not having touch screens, I decided that all gestures should work with single pointing device, and thus I’m not utilizing the actual gestures API in the control. Instead I opted on just implementing pointer move detection with PointerMoved event handler.
The needed action for the app was zoom in/out, and then while being zoomed in, we also need to be able to move the graph image horizontally.
The implementation is rather simple, in it each action is always started with PointerPressed event, which after there is separate threshold values defined for vertical and horizontal movements to determine which action should be performed. And once the movement is more than defined in the threshold value, then the action is started. If it was the horizontal threshold that was reached first, then all PointerMoved events would be used for moving the zoomed graph horizontally.
And if the vertical threshold was reached first, then all following PointerMoved events would be used for zooming the graph.
Any PointerReleased, PointerCanceled, PointerCaptureLost, PointerExited would end the action, and new action would then be started only after we get new PointerPressed event.
The actual zooming effect is then implemented by changing the drawing source rectangle inside the graph canvas. And as we are not implementing any vertical zooming, the zoom action will only change the width of the rectangle size.
And then once we get moving actions, it will simple reposition the source rectangle on the graph canvas.