Build together, debug together. Join the community on Discord.→

Let’s plot 5,000,000 datapoints with ScottPlot

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

Before Getting Started

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:

  • Uno Platform: 5.3.0
  • ScottPlot.WinUI: 5.0.38
 

Step 1: Setting Up the User Interface

Let’s start by examining our MainPage.xaml which defines our UI:

				
					<Page x:Class="UnoScottPlotDataApp.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:UnoScottPlotDataApp"
      xmlns:um="using:Uno.Material"
      xmlns:ScottPlot="using:ScottPlot.WinUI"
      Background="{ThemeResource BackgroundBrush}">

  <Grid>
    <Grid.RowDefinitions>
      <!-- Title -->
      <RowDefinition Height="Auto" />
      <!-- Plot -->
      <RowDefinition Height="*" />
      <!-- Buttons -->
      <RowDefinition Height="Auto" />
      <!-- Status Text -->
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <!-- Title -->
    <TextBlock Grid.Row="0"
               Text="Plotting 5 Million Points"
               FontSize="24"
               FontWeight="Bold"
               HorizontalAlignment="Center"
               Margin="0,10" />

    <!-- Plot -->
    <ScottPlot:WinUIPlot Grid.Row="1"
                         x:Name="WinUIPlot1" />

    <!-- Buttons -->
    <StackPanel Grid.Row="2"
                Orientation="Horizontal"
                HorizontalAlignment="Center"
                Margin="0,10"
                Spacing="5">

      <Button x:Name="AddRandomDataButton"
              Content="Add Random Data"
              Click="AddRandomDataButton_Click" />

      <Button x:Name="ClearPlotButton"
              Content="Clear Plot"
              Click="ClearPlotButton_Click" />

      <Button x:Name="ChangeChartTypeButton"
              Content="Change Chart Type"
              Click="ChangeChartTypeButton_Click" />
    </StackPanel>

    <!-- Status Text -->
    <TextBlock Grid.Row="3"
               x:Name="StatusTextBlock"
               HorizontalAlignment="Center"
               Margin="0,0,0,10" />
  </Grid>
</Page>

				
			

What’s going on here? We’re creating a simple layout with:

  • A title at the top of our chart
  • An area in the middle for our chart (ScottPlot:WinUIPlot control)
  • Three buttons for adding data, clearing the plot, and changing chart types
  • A status bar at the bottom to show keep track of our Chart type and total data points.

Step 2: Implement the main logic

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;
				
			

Class Variables

				
					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.

Constructor and Page Load:

				
					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:

  • Initializes our database
  • Retrieves the last chart type we used (or uses the default if it’s our first time)
  • Sets up our initial plot

Database Initialization:

				
					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.

Plot Initialization:

				
					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).

Step 3: Implement the RefreshPlotAsync method

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:

  1. It clears the existing plot to prepare for new data.
  2. It uses a color palette to ensure each data series has a distinct color.
  3. The switch statement handles different chart types:
    • SignalPlot: Efficient for large datasets with evenly spaced X-values.
    • SignalConst: Similar to SignalPlot but with constant X-value spacing.
    • Heatmap: Visualizes data as a 2D grid, useful for showing data density or patterns.
    • ScatterDownsample: Handles non-uniform X-values with downsampling for better performance.
  4. After adding the data, it autoscales the axes and refreshes the plot.
  5. Finally, it updates the status text with the current number of data points.

The method is asynchronous to keep the UI responsive, especially when dealing with large datasets.

Step 4: Add interactivity methods

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:

  1. AddRandomDataButton_Click: Generates a new random walk series with 100,000 points, adds it to the database, and refreshes the plot.
  2. ClearPlotButton_Click: Removes all data from the database and refreshes the plot.
  3. ChangeChartTypeButton_Click: Cycles through available chart types, saves the new type, and refreshes the plot.
  4. UpdateStatusText: Updates the status display with the current number of data points and chart type.
  5. 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.
  6. GenerateHeatmapData: Converts a 1D array of data points into a 2D array for heatmap visualization.

Step 5: Create the DataService class

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<Series>();
            _db.CreateTable<PlotSettings>();
        }

        public void AddSeries(Series series)
        {
            _db.Insert(series);
        }

        public List<Series> GetAllSeries()
        {
            return _db.Table<Series>().ToList();
        }

        public void ClearAllData()
        {
            _db.DeleteAll<Series>();
        }

        public int TotalDataPoints => GetAllSeries().Sum(series => series.DataPoints.Length);

        public void SaveLastUsedPlotType(string plotType)
        {
            var setting = _db.Table<PlotSettings>().FirstOrDefault();
            if (setting == null)
            {
                _db.Insert(new PlotSettings { LastUsedPlotType = plotType });
            }
            else
            {
                setting.LastUsedPlotType = plotType;
                _db.Update(setting);
            }
        }

        public string GetLastUsedPlotType()
        {
            return _db.Table<PlotSettings>().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<double[]>(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:

  1. The constructor sets up the SQLite database, creating tables if they don’t exist.
  2. AddSeries: Inserts a new data series into the database.
  3. GetAllSeries: Retrieves all stored data series.
  4. ClearAllData: Removes all data series from the database.
  5. TotalDataPoints: Calculates the total number of data points across all series.
  6. SaveLastUsedPlotType and GetLastUsedPlotType: Manage the persistence of the last used chart type.

The Series class represents a single data series:

  • It uses JSON serialization to store the data points as a string in the database.
  • The 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.

Time to Test It Out!

Congratulations! You’ve built a pretty cool data visualization app. But why stop here? Here are some ideas to take it further:

  • Add more chart types (pie charts, anyone?)
  • Let users input their own data
  • Add some animations to make the charts even cooler
  • Implement data export features

Remember, the best way to learn is by doing. So keep experimenting and have fun with it!

Happy coding

Resources Used

To help you further in your development with Uno Platform and ScottPlot, here are some valuable resources used in this project:

  1. Uno Platform
  2. ScottPlot
  3. SQLite-net (used for local database operations)

Tags: XAML, C#, Charts, Data, SQL

Related Posts

Sign Up

    Uno Platform 5.2 LIVE Webinar – Today at 3 PM EST – Watch