🕓 3 MINAdvanced Cross-Platform Data …
Ready to see the power of ScottPlot in action? In this tutorial, we’re going to push the limits of data visualization by plotting 5,000,000 datapoints—all within a sleek, cross-platform app powered by Uno Platform. Whether you’re working on real-time analytics, scientific data, or just curious about how to handle massive datasets, this guide will show you how to deliver high-performance plotting that works seamlessly across desktop, mobile, and web.
By the end, you’ll have built an app capable of visualizing millions of data points, connecting to an SQLite database, offering functionality for clearing chart data points, and allowing users to seamlessly switch between various ScottPlot chart types
To ensure you hit the ground running, begin by setting up your Uno Platform environment. Our Get Started guide will walk you through everything you need to know to get up and running.
If you’re new to ScottPlot, I recommend diving into the Uno Platform Quickstart provided by Scott. This guide is designed to give you a smooth introduction and help you leverage ScottPlot’s capabilities from the get-go.
For this tutorial, we’ll be using the following versions:
Let’s start by examining our MainPage.xaml
which defines our UI:
What’s going on here? We’re creating a simple layout with:
ScottPlot:WinUIPlot
control)Now, let’s make our app do something! We’re going to add the core logic that will drive our data visualization. Don’t worry if some parts seem complex – we’ll break it down together.
Importing Namespaces: At the top, we’re importing the namespaces we need. These give us access to Uno Platform controls, ScottPlot functionality, and some system utilities we’ll use.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using ScottPlot;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
private DataService _dbHelper;
private Random _random = new Random();
private string currentPlotType = "SignalPlot";
private int _currentChartTypeIndex = 0;
private readonly string[] _plotTypes = { "SignalPlot", "SignalConst", "Heatmap", "ScatterDownsample" };
_dbHelper
: This will help us interact with our database._random
: We’ll use this to generate random data.currentPlotType
and _currentChartTypeIndex
: These keep track of which type of chart we’re currently displaying._plotTypes
: This array holds all the different types of charts we can display. We’ll cycle through these when the user clicks the “Switch It Up” button.
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_Loaded;
}
private async void MainPage_Loaded(object sender, RoutedEventArgs e)
{
await InitializeDatabaseAsync();
currentPlotType = _dbHelper.GetLastUsedPlotType() ?? currentPlotType;
await InitializePlotAsync();
}
The constructor sets up our page, and we’ve added an event handler for when the page finishes loading. This MainPage_Loaded
method does three important things:
private async Task InitializeDatabaseAsync()
{
_dbHelper = new DataService();
await Task.CompletedTask;
}
This method creates our DataService
object, which we’ll use to interact with our SQLite database. We’re using async
and await
here to keep our app responsive, even if initializing the database takes a moment.
private async Task InitializePlotAsync()
{
WinUIPlot1.Plot.Title("Our Awesome Data Visualization");
WinUIPlot1.Plot.XLabel("X Axis (ooh, fancy!)");
WinUIPlot1.Plot.YLabel("Y Axis (even fancier!)");
await RefreshPlotAsync();
}
This method sets up our initial plot. We’re giving it a title and labels for the X and Y axes. The RefreshPlotAsync()
call at the end will populate the plot with any existing data (we’ll implement this method next).
Lets add this method to the MainPage
class:
private async Task RefreshPlotAsync()
{
WinUIPlot1.Plot.Clear();
var palette = new ScottPlot.Palettes.Category10();
var seriesList = _dbHelper.GetAllSeries();
switch (currentPlotType)
{
case "SignalPlot":
for (int i = 0; i < seriesList.Count; i++)
{
var series = seriesList[i];
var signalPlot = WinUIPlot1.Plot.Add.Signal(series.DataPoints);
signalPlot.Color = palette.GetColor(i);
}
break;
case "SignalConst":
for (int i = 0; i < seriesList.Count; i++)
{
var series = seriesList[i];
var signalConstPlot = WinUIPlot1.Plot.Add.SignalConst(series.DataPoints);
signalConstPlot.LineWidth = 2;
signalConstPlot.Color = palette.GetColor(i);
}
break;
case "Heatmap":
if (seriesList.Count > 0)
{
var series = seriesList[0];
double[,] heatmapData = GenerateHeatmapData(series.DataPoints);
WinUIPlot1.Plot.Add.Heatmap(heatmapData);
}
break;
case "ScatterDownsample":
for (int i = 0; i < seriesList.Count; i++)
{
var series = seriesList[i];
var xs = Enumerable.Range(0, series.DataPoints.Length).Select(x => (double)x).ToArray();
var scatterPlot = WinUIPlot1.Plot.Add.Scatter(xs, series.DataPoints);
scatterPlot.Color = palette.GetColor(i);
}
break;
}
WinUIPlot1.Plot.Axes.AutoScale();
WinUIPlot1.Refresh();
UpdateStatusText(_dbHelper.TotalDataPoints);
await Task.CompletedTask;
}
This method is central to our application’s functionality:
The method is asynchronous to keep the UI responsive, especially when dealing with large datasets.
Now let’s make our buttons do something and add some interaction to our app.
private async void AddRandomDataButton_Click(object sender, RoutedEventArgs e)
{
var newSeries = GenerateRandomWalk(100000, _dbHelper.TotalDataPoints);
_dbHelper.AddSeries(newSeries);
await RefreshPlotAsync();
}
private async void ClearPlotButton_Click(object sender, RoutedEventArgs e)
{
_dbHelper.ClearAllData();
await RefreshPlotAsync();
}
private async void ChangeChartTypeButton_Click(object sender, RoutedEventArgs e)
{
_currentChartTypeIndex = (_currentChartTypeIndex + 1) % _plotTypes.Length;
currentPlotType = _plotTypes[_currentChartTypeIndex];
_dbHelper.SaveLastUsedPlotType(currentPlotType);
await RefreshPlotAsync();
}
private void UpdateStatusText(int totalPoints)
{
StatusTextBlock.Text = $"Total data points: {totalPoints:N0} | Current chart: {currentPlotType}";
}
private Series GenerateRandomWalk(int length, double origin)
{
double[] data = new double[length];
double value = 0;
for (int i = 0; i < length; i++)
{
data[i] = value += _random.NextDouble() * 2 - 1;
}
return new Series
{
DataPoints = data,
Origin = origin
};
}
private double[,] GenerateHeatmapData(double[] dataPoints)
{
int size = (int)Math.Sqrt(dataPoints.Length);
double[,] heatmap = new double[size, size];
for (int i = 0, h = 0; i < size; i++)
{
for (int j = 0; j < size; j++, h++)
{
heatmap[i, j] = dataPoints[h];
}
}
return heatmap;
}
These methods handle user interactions and data generation:
AddRandomDataButton_Click
: Generates a new random walk series with 100,000 points, adds it to the database, and refreshes the plot.ClearPlotButton_Click
: Removes all data from the database and refreshes the plot.ChangeChartTypeButton_Click
: Cycles through available chart types, saves the new type, and refreshes the plot.UpdateStatusText
: Updates the status display with the current number of data points and chart type.GenerateRandomWalk
: Creates a new series of random walk data. Each point is based on the previous point plus a random value between -1 and 1.GenerateHeatmapData
: Converts a 1D array of data points into a 2D array for heatmap visualization.Create a new file DataService.cs
and add this code:
using SQLite;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace UnoScottPlotDataApp
{
public class DataService
{
private SQLiteConnection _db;
public DataService()
{
string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "seriesData.db");
_db = new SQLiteConnection(dbPath);
_db.CreateTable();
_db.CreateTable();
}
public void AddSeries(Series series)
{
_db.Insert(series);
}
public List GetAllSeries()
{
return _db.Table().ToList();
}
public void ClearAllData()
{
_db.DeleteAll();
}
public int TotalDataPoints => GetAllSeries().Sum(series => series.DataPoints.Length);
public void SaveLastUsedPlotType(string plotType)
{
var setting = _db.Table().FirstOrDefault();
if (setting == null)
{
_db.Insert(new PlotSettings { LastUsedPlotType = plotType });
}
else
{
setting.LastUsedPlotType = plotType;
_db.Update(setting);
}
}
public string GetLastUsedPlotType()
{
return _db.Table().FirstOrDefault()?.LastUsedPlotType;
}
}
public class Series
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string DataPointsSerialized { get; set; }
public double Origin { get; set; }
[Ignore]
public double[] DataPoints
{
get => JsonSerializer.Deserialize(DataPointsSerialized);
set => DataPointsSerialized = JsonSerializer.Serialize(value);
}
}
public class PlotSettings
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string LastUsedPlotType { get; set; }
}
}
The DataService
class manages data persistence using SQLite:
AddSeries
: Inserts a new data series into the database.GetAllSeries
: Retrieves all stored data series.ClearAllData
: Removes all data series from the database.TotalDataPoints
: Calculates the total number of data points across all series.SaveLastUsedPlotType
and GetLastUsedPlotType
: Manage the persistence of the last used chart type.The Series
class represents a single data series:
DataPoints
property provides easy access to the deserialized data.The PlotSettings
class stores application settings, specifically the last used plot type.
This approach allows efficient storage and retrieval of large datasets, and maintains the application state between sessions.
Congratulations! You’ve built a pretty cool data visualization app. But why stop here? Here are some ideas to take it further:
Remember, the best way to learn is by doing. So keep experimenting and have fun with it!
Happy coding
To help you further in your development with Uno Platform and ScottPlot, here are some valuable resources used in this project:
Tags: XAML, C#, Charts, Data, SQL
🕓 5 MINWhen developing applications, …
360 rue Saint-Jacques, suite G101,
Montréal, Québec, Canada
H2Y 1P5
USA/CANADA toll free: +1-877-237-0471
International: +1-514-312-6958
Uno Platform 5.2 LIVE Webinar – Today at 3 PM EST – Watch