Pre loader

WPF Real-Time Vector Field Scattered Chart

WPF Real-Time Scattered Vector Field Chart

This example demonstrates a WPF Real-Time Scattered Vector Field Chart with particles at arbitrary positions, animated at up to ~50 FPS. The chart uses VectorFieldRenderableSeries and NonUniformVectorFieldDataSeries, which store flat (X, Y, DX, DY) tuples. Three Rankine vortices define the steady flow field. Each frame, every particle is advected using explicit Euler integration. Thread safety is maintained with a double-buffer: the background timer writes updated data into scratch arrays, and the UI handler copies them before InvalidateParentSurface.

Download the SDK

Key Features of SciChart’s Scattered Vector Field Chart WPF

  • Two update modes via toolbar:
    • Normal: a background thread-pool timer fires every 20 ms (~50 FPS).
    • Performance: runs inside CompositionTarget.Rendering every frame, with FPS overlay.
  • NonUniformVectorFieldDataSeries with scattered, time-varying particle positions.
  • Real-time update of both X/Y positions and DX/DY vectors via double-buffered arrays.
  • Scalable particle count via toolbar slider.
  • Color mapping by magnitude or by direction via HeatmapColorPalette

Try the Example for Yourself

Give the WPF Real-Time Vector Field Chart a try for yourself today. It’s ideal for displaying complex datasets for scientific research and engineering analysis. View the documentation or head straight to GitHub via the source code.

The C#/WPF source code for this example is included below.

Download the example or begin your free trial today to build high-performance WPF charts.

The C#/WPF source code for the WPF Realtime Vector Field Chart example is included below (Scroll down!).

Did you know you can also view the source code from one of the following sources as well?

  1. Clone the SciChart.WPF.Examples from GitHub.
  2. Or, view source in the SciChart WPF Examples suite.
  3. Also the SciChart WPF Trial contains the full source for the examples (link below).

DOWNLOAD THE WPF CHART EXAMPLES

RealtimeScatteredVectorFieldChartView.xaml
View source code
<UserControl x:Class="SciChart.Examples.Examples.CreateRealtimeChart.RealtimeScatteredVectorFieldChartView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:ext="http://schemas.abtsoftware.co.uk/scichart/exampleExternals"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:s="http://schemas.abtsoftware.co.uk/scichart"
             Loaded="OnExampleLoaded"
             Unloaded="OnExampleUnloaded"
             d:DesignHeight="400"
             d:DesignWidth="600"
             mc:Ignorable="d">

    <UserControl.Resources>
        <!-- Plasma palette: perceptually-uniform dark-blue → purple → pink → orange → yellow.
             Ideal for magnitude colouring. Range is set from code-behind after assignment. -->
        <s:HeatmapColorPalette x:Key="PlasmaColorPalette" Minimum="0" Maximum="1">
            <GradientStop Color="#FF0D0887" Offset="0.00"/>
            <GradientStop Color="#FF7E03A8" Offset="0.25"/>
            <GradientStop Color="#FFCC4778" Offset="0.50"/>
            <GradientStop Color="#FFF89540" Offset="0.75"/>
            <GradientStop Color="#FFF0F921" Offset="1.00"/>
        </s:HeatmapColorPalette>

        <!-- Plasma circular: same hues with first colour repeated at offset 1.0 so the
             palette wraps smoothly for ByDirection mode. -->
        <s:HeatmapColorPalette x:Key="PlasmaCircularColorPalette" Minimum="0" Maximum="1">
            <GradientStop Color="#FF0D0887" Offset="0.00"/>
            <GradientStop Color="#FF7E03A8" Offset="0.20"/>
            <GradientStop Color="#FFCC4778" Offset="0.40"/>
            <GradientStop Color="#FFF89540" Offset="0.60"/>
            <GradientStop Color="#FFF0F921" Offset="0.80"/>
            <GradientStop Color="#FF0D0887" Offset="1.00"/>
        </s:HeatmapColorPalette>

        <!-- Cool-Warm diverging palette: blue → neutral grey → red.
             Well-suited for signed or direction-based colouring. -->
        <s:HeatmapColorPalette x:Key="CoolWarmColorPalette" Minimum="0" Maximum="1">
            <GradientStop Color="#FF3B4CC0" Offset="0.00"/>
            <GradientStop Color="#FFDCDCF0" Offset="0.50"/>
            <GradientStop Color="#FFB40426" Offset="1.00"/>
        </s:HeatmapColorPalette>

        <!-- Cool-Warm circular: first colour repeated at offset 1.0 for ByDirection mode. -->
        <s:HeatmapColorPalette x:Key="CoolWarmCircularColorPalette" Minimum="0" Maximum="1">
            <GradientStop Color="#FF3B4CC0" Offset="0.00"/>
            <GradientStop Color="#FFDCDCF0" Offset="0.33"/>
            <GradientStop Color="#FFB40426" Offset="0.67"/>
            <GradientStop Color="#FF3B4CC0" Offset="1.00"/>
        </s:HeatmapColorPalette>
    </UserControl.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <!-- Toolbar: start/stop simulation and configure particle count and vector appearance -->
        <ext:SciChartInteractionToolbar Grid.Column="0">

            <ToggleButton x:Name="StartButton"
                          Click="OnStartClick"
                          Style="{StaticResource DefaultUncheckableToggleButtonStyle}"
                          ToolTipService.ToolTip="Start animation">
                <ext:ToolbarIcon Kind="Play" Height="14" Width="14" Style="{StaticResource ToolbarButtonIconStyle}"/>
            </ToggleButton>

            <ToggleButton x:Name="StopButton"
                          IsEnabled="False"
                          Click="OnStopClick"
                          Style="{StaticResource DefaultUncheckableToggleButtonStyle}"
                          ToolTipService.ToolTip="Stop animation">
                <ext:ToolbarIcon Kind="Stop" Height="12" Width="12" Style="{StaticResource ToolbarButtonIconStyle}"/>
            </ToggleButton>

            <!-- Toggles performance mode: on = update runs every composition frame (max FPS);
                 off = timer drives updates at ~50 fps, keeping the render thread free -->
            <ToggleButton x:Name="PerformanceModeButton"
                          IsChecked="False"
                          Click="OnPerformanceModeToggled"
                          Style="{StaticResource DefaultToggleButtonStyle}"
                          ToolTipService.ToolTip="Performance mode">
                <ext:ToolbarIcon Kind="Flash" Height="14" Width="14" Style="{StaticResource ToolbarButtonIconStyle}"/>
            </ToggleButton>

            <ext:FlyoutSeparator/>

            <!-- Particle count must be changed while the simulation is stopped -->
            <ext:FlyoutMenuButton x:Name="ChangeCountButton" ToolTipService.ToolTip="Simulation settings">
                <ext:ToolbarIcon Kind="Cog" Height="16" Width="16" Style="{StaticResource ToolbarButtonIconStyle}"/>
                <ext:FlyoutMenuButton.PopupContent>
                    <StackPanel Margin="5" MinWidth="200" Orientation="Vertical">
                        <TextBlock Text="Particle Count" Style="{StaticResource FlyoutMenuHeaderStyle}"/>
                        <StackPanel Orientation="Horizontal">
                            <Slider x:Name="ParticleCountSlider"
                                    Minimum="100" Maximum="10000" Value="2000"
                                    Width="130" VerticalAlignment="Center"
                                    IsSnapToTickEnabled="True" TickFrequency="100"
                                    ValueChanged="OnParticleCountUpdated"/>
                            <TextBlock Text="{Binding Source={x:Reference Name=ParticleCountSlider}, Path=Value, StringFormat={}{0:N0}}"
                                       VerticalAlignment="Center"
                                       Margin="6,0,0,0"/>
                        </StackPanel>
                    </StackPanel>
                </ext:FlyoutMenuButton.PopupContent>
            </ext:FlyoutMenuButton>

            <!-- Culling options: LOD cell size and density control -->
            <ext:FlyoutMenuButton ToolTipService.ToolTip="Culling options">
                <ext:ToolbarIcon Kind="Layers" Height="16" Width="16" Style="{StaticResource ToolbarButtonIconStyle}"/>
                <ext:FlyoutMenuButton.PopupContent>
                    <StackPanel Margin="5" MinWidth="200" Orientation="Vertical">

                        <!-- Cell size: 0 = disabled; higher values reduce draw calls more aggressively -->
                        <TextBlock Text="Culling Cell Size" Style="{StaticResource FlyoutMenuHeaderStyle}"/>
                        <StackPanel Orientation="Horizontal">
                            <Slider x:Name="LodCellSizeSlider"
                                    Minimum="0" Maximum="20"
                                    Value="{Binding Source={x:Reference Name=VectorSeries}, Path=CullingCellSizePx, Mode=TwoWay}"
                                    Width="130" VerticalAlignment="Center"
                                    IsSnapToTickEnabled="True" TickFrequency="0.5"/>
                            <TextBlock Text="{Binding Source={x:Reference Name=LodCellSizeSlider}, Path=Value, StringFormat={}{0:0.#} px}"
                                       VerticalAlignment="Center"
                                       Margin="6,0,0,0"/>
                        </StackPanel>

                        <!-- Max vectors per cell: 1 = single winner; higher values show more in dense regions -->
                        <TextBlock Text="Max Vectors Per Cell" Margin="0,10,0,4" Style="{StaticResource FlyoutMenuHeaderStyle}"/>
                        <StackPanel Orientation="Horizontal">
                            <Slider x:Name="MaxVectorsPerCellSlider"
                                    Minimum="1" Maximum="20"
                                    Value="{Binding Source={x:Reference Name=VectorSeries}, Path=CullingMaxVectorsPerCell, Mode=TwoWay}"
                                    Width="130" VerticalAlignment="Center"
                                    IsSnapToTickEnabled="True" TickFrequency="1"/>
                            <TextBlock Text="{Binding Source={x:Reference Name=MaxVectorsPerCellSlider}, Path=Value, StringFormat={}{0:0}}"
                                       VerticalAlignment="Center"
                                       Margin="6,0,0,0"/>
                        </StackPanel>

                    </StackPanel>
                </ext:FlyoutMenuButton.PopupContent>
            </ext:FlyoutMenuButton>

            <ext:FlyoutSeparator/>

            <!-- Vector options: colour mapping, arrow length/width, normalization -->
            <ext:FlyoutMenuButton ToolTipService.ToolTip="Vector options">
                <ext:ToolbarIcon Kind="TuneVariant" Height="16" Width="16" Style="{StaticResource ToolbarButtonIconStyle}"/>
                <ext:FlyoutMenuButton.PopupContent>
                    <StackPanel Margin="5" MinWidth="200" Orientation="Vertical">

                        <!-- Color map preset: selects between the palettes declared in resources above -->
                        <TextBlock Text="Color Map" Style="{StaticResource FlyoutMenuHeaderStyle}"/>
                        <ComboBox x:Name="ColorMapCombo"
                                  Margin="0,4,0,0"
                                  SelectionChanged="OnColorMapChanged"/>

                        <!-- Color mode: Solid, ByMagnitude or ByDirection -->
                        <TextBlock Text="Color Mode" Margin="0,10,0,4" Style="{StaticResource FlyoutMenuHeaderStyle}"/>
                        <ComboBox x:Name="ColorModeCombo"
                                  SelectionChanged="OnColorModeChanged"/>

                        <!-- Length mode: DataUnits, FixedPixels or MagnitudePixels -->
                        <TextBlock Text="Length Mode" Margin="0,10,0,4" Style="{StaticResource FlyoutMenuHeaderStyle}"/>
                        <ComboBox x:Name="LengthModeCombo"
                                  SelectionChanged="OnLengthModeChanged"/>

                        <!-- MagnitudeScale: visible only in MagnitudePixels mode -->
                        <TextBlock x:Name="MagnitudeScaleHeader" Text="Magnitude Scale" Margin="0,10,0,4" Visibility="Collapsed" Style="{StaticResource FlyoutMenuHeaderStyle}"/>
                        <StackPanel x:Name="MagnitudeScalePanel" Orientation="Horizontal" Visibility="Collapsed">
                            <Slider x:Name="MagnitudeScaleSlider"
                                    Minimum="1" Maximum="100"
                                    Value="{Binding Source={x:Reference Name=VectorSeries}, Path=MagnitudeScale, Mode=TwoWay}"
                                    Width="130" VerticalAlignment="Center"/>
                            <TextBlock Text="{Binding Source={x:Reference Name=MagnitudeScaleSlider}, Path=Value, StringFormat={}{0:F0}}"
                                       VerticalAlignment="Center"
                                       Margin="6,0,0,0"/>
                        </StackPanel>

                        <!-- PixelLength: visible only in FixedPixels mode -->
                        <TextBlock x:Name="PixelLengthHeader" Text="Pixel Length" Margin="0,10,0,4" Visibility="Collapsed" Style="{StaticResource FlyoutMenuHeaderStyle}"/>
                        <StackPanel x:Name="PixelLengthPanel" Orientation="Horizontal" Visibility="Collapsed">
                            <Slider x:Name="PixelLengthSlider"
                                    Minimum="5" Maximum="80"
                                    Value="{Binding Source={x:Reference Name=VectorSeries}, Path=PixelLength, Mode=TwoWay}"
                                    Width="130" VerticalAlignment="Center"/>
                            <TextBlock Text="{Binding Source={x:Reference Name=PixelLengthSlider}, Path=Value, StringFormat={}{0:F0}}"
                                       VerticalAlignment="Center"
                                       Margin="6,0,0,0"/>
                        </StackPanel>

                    </StackPanel>
                </ext:FlyoutMenuButton.PopupContent>
            </ext:FlyoutMenuButton>

        </ext:SciChartInteractionToolbar>

        <!-- Main chart surface. DataSeries is assigned from code-behind after loading. -->
        <s:SciChartSurface x:Name="SciChartSurface"
                           Grid.Column="1"
                           ChartTitle="Scattered Particle Vector Field">

            <s:SciChartSurface.RenderableSeries>
                <!-- VectorFieldRenderableSeries renders each particle as an arrow at its position.
                     ColorMap is assigned from code-behind and maps magnitude or direction to colour. -->
                <s:VectorFieldRenderableSeries x:Name="VectorSeries"
                                               Stroke="#4FC3F7"
                                               StrokeThickness="1"
                                               ArrowHeadLength="5"
                                               ArrowHeadWidth="3"
                                               IsArrowHeadFilled="False"
                                               LengthMode="MagnitudePixels"
                                               MagnitudeScale="30"
                                               ColorMode="ByMagnitude"
                                               AntiAliasing="True"
                                               ColorMap="{StaticResource PlasmaColorPalette}"/>
            </s:SciChartSurface.RenderableSeries>

            <s:SciChartSurface.XAxis>
                <s:NumericAxis DrawMajorBands="False"
                               DrawMinorGridLines="False"
                               VisibleRange="-16, 16"/>
            </s:SciChartSurface.XAxis>

            <s:SciChartSurface.YAxis>
                <s:NumericAxis DrawMajorBands="False"
                               DrawMinorGridLines="False"
                               VisibleRange="-11, 11"/>
            </s:SciChartSurface.YAxis>

            <s:SciChartSurface.ChartModifier>
                <s:ModifierGroup>
                    <s:ZoomPanModifier/>
                    <s:ZoomExtentsModifier/>
                    <s:MouseWheelZoomModifier/>
                </s:ModifierGroup>
            </s:SciChartSurface.ChartModifier>

        </s:SciChartSurface>

        <!-- Vertical colour legend bound to the VectorSeries colour map -->
        <s:HeatmapColorMap Grid.Column="1"
                           Margin="20,20,0,0"
                           HorizontalAlignment="Left"
                           VerticalAlignment="Top"
                           Height="400"
                           Background="{StaticResource ThemedSurfaceChartBackground}"
                           Foreground="{Binding Source={x:Reference Name=SciChartSurface}, Path=Foreground}"
                           ColorPalette="{Binding Source={x:Reference Name=VectorSeries}, Path=ColorMap}"
                           Minimum="{Binding Source={x:Reference Name=VectorSeries}, Path=ColorMap.Minimum, Mode=TwoWay}"
                           Maximum="{Binding Source={x:Reference Name=VectorSeries}, Path=ColorMap.Maximum, Mode=TwoWay}"
                           TextFormatting="0.00"
                           Opacity="0.8"
                           BorderBrush="#777"
                           BorderThickness="1"
                           Orientation="Vertical"/>

        <!-- Performance overlay: shows render FPS and draw call stats -->
        <s:SciChartPerformanceOverlay x:Name="ChartPerformanceOverlay"
                                      Grid.Column="1"
                                      Margin="0,0,50,0"
                                      VerticalAlignment="Top"
                                      Background="#33FFFFFF"
                                      FontWeight="Bold"
                                      Foreground="#FFF"
                                      Padding="10"
                                      IsHitTestVisible="False"
                                      Visibility="Collapsed"
                                      TargetSurface="{Binding ElementName=SciChartSurface}" />

    </Grid>
</UserControl>
RealtimeScatteredVectorFieldChartView.xaml.cs
View source code
// *************************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2026. All rights reserved.
//
// Web:     http://www.scichart.com
// Support: support@scichart.com
// Sales:   sales@scichart.com
//
// RealtimeScatteredVectorFieldChartView.xaml.cs is part of the SCICHART® Examples. Permission is hereby granted
// to modify, create derivative works, distribute and publish any part of this source
// code whether for commercial, private or personal use.
//
// The SCICHART® examples are distributed in the hope that they will be useful, but
// without any warranty. It is provided "AS IS" without warranty of any kind, either
// expressed or implied.
// *************************************************************************************
using SciChart.Charting.Model.DataSeries;
using SciChart.Charting.Visuals.RenderableSeries;
using SciChart.Data.Model;
using System;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace SciChart.Examples.Examples.CreateRealtimeChart
{
    /// <summary>
    /// Demonstrates a real-time animated <see cref="VectorFieldRenderableSeries"/> backed by a
    /// <see cref="NonUniformVectorFieldDataSeries"/> with arbitrarily-placed particles.
    /// Particles move along the flow field defined by three Rankine vortices (regularised point
    /// vortices), wrapping around the domain boundary when they leave it.
    ///
    /// Two update modes are supported:
    /// <list type="bullet">
    ///   <item><description>
    ///     <b>Normal</b> — a <see cref="System.Threading.Timer"/> fires every 20 ms on a thread-pool
    ///     thread to update the data arrays and sets a flag; <see cref="CompositionTarget.Rendering"/>
    ///     calls <c>InvalidateParentSurface</c> only when the flag is set. This caps updates at ~50 fps
    ///     while keeping the render thread free between updates.
    ///   </description></item>
    ///   <item><description>
    ///     <b>Performance</b> (<see cref="_enablePerformanceShowMode"/> = true) — the data update runs
    ///     directly inside the <see cref="CompositionTarget.Rendering"/> handler every frame, giving
    ///     the maximum achievable throughput with no artificial cap.
    ///   </description></item>
    /// </list>
    /// </summary>
    public partial class RealtimeScatteredVectorFieldChartView : UserControl
    {
        private const double _vectorScale = 0.35;
        private const double DomainX = 15.0;
        private const double DomainY = 10.0;

        // Epsilon regularises the vortex core singularity; smaller values produce tighter cores
        private const double Epsilon = 1.5;

        // Dt is the Euler integration time step used to advect particles each frame
        private const double Dt = 0.025;

        // Interval between data updates in normal (non-performance) mode
        private const int UpdateIntervalMs = 20;

        // Three Rankine vortices: (centre X, centre Y, circulation strength Γ)
        // Negative Γ = clockwise rotation; positive Γ = counter-clockwise
        private static readonly (double Cx, double Cy, double Gamma)[] Vortices =
        {
            (-6.0,  4.5,  8.0),
            ( 7.0, -3.0, -6.5),
            ( 0.5, -6.0,  5.5),
        };

        // Names and corresponding XAML resource keys for the selectable colour palettes.
        // The palette gradient stops are declared as HeatmapColorPalette resources in XAML.
        private static readonly string[] PaletteNames = { "Plasma", "Cool-Warm" };
        private static readonly string[] PaletteResourceKeys = { "PlasmaColorPalette", "CoolWarmColorPalette" };
        private static readonly string[] CircularPaletteResourceKeys = { "PlasmaCircularColorPalette", "CoolWarmCircularColorPalette" };

        private static readonly FieldDataDistributionArgs DistributionArgs = new FieldDataDistributionArgs
        {
            XRange = new DoubleRange(-DomainX, DomainX),
            YRange = new DoubleRange(-DomainY, DomainY),
            IsStationary = false
        };

        // Double-buffered particle state to prevent rendering artifacts caused by the background
        // timer writing positions while SciChart reads them mid-frame.
        //
        // The timer writes exclusively to the _*Buf arrays (under _dataLock).
        // OnRenderingTick (UI thread) copies _*Buf → _px/_py/_dxs/_dys under the same lock,
        // then calls InvalidateParentSurface. SciChart always reads a fully consistent snapshot.
        private double[] _px;
        private double[] _py;
        private double[] _dxs;
        private double[] _dys;
        private double[] _pxBuf;
        private double[] _pyBuf;
        private double[] _dxsBuf;
        private double[] _dysBuf;
        private readonly object _dataLock = new object();

        private NonUniformVectorFieldDataSeries _dataSeries;
        private Timer _updateTimer;
        private bool _isRunning;
        private volatile bool _newDataReady;

        private bool _enablePerformanceShowMode;

        /// <summary>
        /// Initializes a new instance of <see cref="RealtimeScatteredVectorFieldChartView"/>.
        /// </summary>
        public RealtimeScatteredVectorFieldChartView()
        {
            InitializeComponent();
            // Timer starts suspended; Change() activates it when the animation starts
            _updateTimer = new Timer(OnUpdateTimer, null, Timeout.Infinite, Timeout.Infinite);
        }

        /// <summary>
        /// When <c>true</c>, the data update runs directly inside the <see cref="CompositionTarget.Rendering"/>
        /// handler on every frame, giving maximum throughput with no rate cap.
        /// When <c>false</c>, a <see cref="System.Threading.Timer"/> drives updates at
        /// <see cref="UpdateIntervalMs"/> ms intervals, keeping the render thread free between updates.
        /// Also controls visibility of the <see cref="ChartPerformanceOverlay"/>.
        /// </summary>
        private bool EnablePerformanceShowMode
        {
            get => _enablePerformanceShowMode;
            set
            {
                _enablePerformanceShowMode = value;
                ChartPerformanceOverlay.Visibility = _enablePerformanceShowMode ? Visibility.Visible : Visibility.Collapsed;
            }
        }

        private void OnExampleLoaded(object sender, RoutedEventArgs e)
        {
            foreach (VectorColorMode mode in Enum.GetValues(typeof(VectorColorMode)))
                ColorModeCombo.Items.Add(mode);
            ColorModeCombo.SelectedItem = VectorColorMode.ByMagnitude;

            foreach (VectorLengthMode mode in Enum.GetValues(typeof(VectorLengthMode)))
                LengthModeCombo.Items.Add(mode);
            LengthModeCombo.SelectedItem = VectorLengthMode.MagnitudePixels;

            foreach (string name in PaletteNames)
                ColorMapCombo.Items.Add(name);
            ColorMapCombo.SelectedIndex = 0;

            OnStartClick(this, null);
        }

        private void OnExampleUnloaded(object sender, RoutedEventArgs e)
        {
            OnStopClick(this, null);
            _updateTimer?.Dispose();

            _dataSeries?.Clear(true);
            _px = _py = _dxs = _dys = null;
            _pxBuf = _pyBuf = _dxsBuf = _dysBuf = null;
            ColorModeCombo.Items.Clear();
            LengthModeCombo.Items.Clear();
            ColorMapCombo.Items.Clear();
        }


        /// <summary>
        /// Allocates particle state arrays and creates the initial <see cref="NonUniformVectorFieldDataSeries"/>.
        /// Particles are scattered randomly within the domain using a fixed seed for reproducibility.
        /// </summary>
        private void InitializeParticles(int count)
        {
            var rng = new Random(12345);
            _pxBuf = new double[count];
            _pyBuf = new double[count];
            _dxsBuf = new double[count];
            _dysBuf = new double[count];

            for (int i = 0; i < count; i++)
            {
                _pxBuf[i] = (rng.NextDouble() * 2.0 - 1.0) * DomainX;
                _pyBuf[i] = (rng.NextDouble() * 2.0 - 1.0) * DomainY;
                GetFlowFieldVector(_pxBuf[i], _pyBuf[i], out double vx, out double vy);
                _dxsBuf[i] = vx * _vectorScale;
                _dysBuf[i] = vy * _vectorScale;
            }

            _px = (double[])_pxBuf.Clone();
            _py = (double[])_pyBuf.Clone();
            _dxs = (double[])_dxsBuf.Clone();
            _dys = (double[])_dysBuf.Clone();

            _dataSeries = new NonUniformVectorFieldDataSeries(_px, _py, _dxs, _dys);
            VectorSeries.DataSeries = _dataSeries;
        }

        /// <summary>
        /// Computes the 2-D velocity induced at (x, y) by the superimposed regularised vortices.
        /// Uses the Rankine (cut-off) vortex model: v = Γ / (2π (r² + ε²)) × r⊥.
        /// </summary>
        private static void GetFlowFieldVector(double x, double y, out double vx, out double vy)
        {
            vx = 0.0;
            vy = 0.0;
            foreach (var (cx, cy, gamma) in Vortices)
            {
                double rx = x - cx;
                double ry = y - cy;
                double r2 = rx * rx + ry * ry + Epsilon * Epsilon;
                vx -= gamma * ry / r2;
                vy += gamma * rx / r2;
            }
        }

        /// <summary>
        /// Wraps a scalar value into the periodic domain [min, max].
        /// Used to keep particles inside the visible domain after advection.
        /// </summary>
        private static double Wrap(double value, double min, double max)
        {
            double range = max - min;
            double normalized = (value - min) / range;
            return min + (normalized - Math.Floor(normalized)) * range;
        }

        /// <summary>
        /// Timer callback — fires every 20 ms on a thread-pool thread.
        /// Advances each particle by one Euler step, evaluates the flow field at the new
        /// position, then signals the rendering handler via <see cref="_newDataReady"/>.
        /// </summary>
        private void OnUpdateTimer(object state)
        {
            if (_dataSeries == null) return;

            int n = _pxBuf.Length;
            lock (_dataLock)
            {
                for (int i = 0; i < n; i++)
                {
                    // Advect particle position using current flow velocity (explicit Euler)
                    GetFlowFieldVector(_pxBuf[i], _pyBuf[i], out double vx, out double vy);
                    _pxBuf[i] = Wrap(_pxBuf[i] + vx * Dt, -DomainX, DomainX);
                    _pyBuf[i] = Wrap(_pyBuf[i] + vy * Dt, -DomainY, DomainY);

                    // Re-sample field at new position for the arrow direction
                    GetFlowFieldVector(_pxBuf[i], _pyBuf[i], out vx, out vy);
                    _dxsBuf[i] = vx * _vectorScale;
                    _dysBuf[i] = vy * _vectorScale;
                }
            }

            // All writes complete — signal the rendering handler that fresh data is available
            _newDataReady = true;
        }

        /// <summary>
        /// Called every WPF composition frame.
        /// In performance mode the data update runs here directly, so the chart redraws every frame
        /// with no cap. In normal mode the chart is invalidated only when the timer has set the flag.
        /// </summary>
        private void OnRenderingTick(object sender, EventArgs e)
        {
            if (_enablePerformanceShowMode)
            {
                // No concurrent timer in this mode — update buf arrays then copy directly (no lock needed)
                OnUpdateTimer(null);
                Array.Copy(_pxBuf, _px, _px.Length);
                Array.Copy(_pyBuf, _py, _py.Length);
                Array.Copy(_dxsBuf, _dxs, _dxs.Length);
                Array.Copy(_dysBuf, _dys, _dys.Length);
                _dataSeries?.InvalidateParentSurface(RangeMode.None, DistributionArgs, hasDataChanges: true);
            }
            else
            {
                if (!_newDataReady) return;
                _newDataReady = false;
                lock (_dataLock)
                {
                    Array.Copy(_pxBuf, _px, _px.Length);
                    Array.Copy(_pyBuf, _py, _py.Length);
                    Array.Copy(_dxsBuf, _dxs, _dxs.Length);
                    Array.Copy(_dysBuf, _dys, _dys.Length);
                }
                _dataSeries.InvalidateParentSurface(RangeMode.None, DistributionArgs, hasDataChanges: true);
            }
        }

        private void OnStartClick(object sender, RoutedEventArgs e)
        {
            ChangeCountButton.IsEnabled = false;
            StartButton.IsEnabled = false;
            StartButton.IsChecked = true;
            StopButton.IsEnabled = true;
            StopButton.IsChecked = false;

            if (_dataSeries == null)
                InitializeParticles((int)ParticleCountSlider.Value);

            if (!_isRunning)
            {
                _isRunning = true;
                CompositionTarget.Rendering -= OnRenderingTick;
                CompositionTarget.Rendering += OnRenderingTick;
                // In performance mode the rendering handler drives updates itself every frame;
                // in normal mode the timer produces data independently at ~50 fps.
                if (!_enablePerformanceShowMode)
                    _updateTimer.Change(0, UpdateIntervalMs);
            }
        }

        private void OnStopClick(object sender, RoutedEventArgs e)
        {
            if (_isRunning)
            {
                _isRunning = false;
                if (!_enablePerformanceShowMode)
                    _updateTimer.Change(Timeout.Infinite, Timeout.Infinite);
                CompositionTarget.Rendering -= OnRenderingTick;
            }

            ChangeCountButton.IsEnabled = true;
            StartButton.IsEnabled = true;
            StartButton.IsChecked = false;
            StopButton.IsEnabled = false;
            StopButton.IsChecked = true;
        }

        private void OnPerformanceModeToggled(object sender, RoutedEventArgs e)
        {
            bool wasRunning = _isRunning;
            if (wasRunning) OnStopClick(this, null);
            EnablePerformanceShowMode = PerformanceModeButton.IsChecked == true;
            if (wasRunning) OnStartClick(this, null);
        }


        private void OnParticleCountUpdated(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            // Clear the existing data series so InitializeParticles creates a fresh one on next Start
            if (_dataSeries != null)
            {
                _dataSeries.Clear(true);
                _dataSeries.InvalidateParentSurface(RangeMode.None);
                _dataSeries = null;
            }
        }

        private void OnColorModeChanged(object sender, SelectionChangedEventArgs e)
        {
            if (VectorSeries == null || ColorModeCombo.SelectedItem == null) return;
            var mode = (VectorColorMode)ColorModeCombo.SelectedItem;
            VectorSeries.ColorMode = mode;
            if (ColorMapCombo.SelectedItem != null)
                ApplyColorMapPreset((string)ColorMapCombo.SelectedItem);
            else
                UpdateColorMapRange(mode);
        }

        /// <summary>
        /// Adjusts the colour map range to match the selected colour mode.
        /// Direction mode maps [0, 2π]; magnitude mode maps [-1, 1] (normalised).
        /// </summary>
        private void UpdateColorMapRange(VectorColorMode mode)
        {
            if (VectorSeries?.ColorMap == null) return;

            VectorSeries.ColorMap.Minimum = mode == VectorColorMode.ByDirection ? 0 : -1;
            VectorSeries.ColorMap.Maximum = mode == VectorColorMode.ByDirection ? 2 * Math.PI : 1;
        }

        private void OnColorMapChanged(object sender, SelectionChangedEventArgs e)
        {
            if (VectorSeries == null || ColorMapCombo.SelectedItem == null) return;
            ApplyColorMapPreset((string)ColorMapCombo.SelectedItem);
        }

        /// <summary>
        /// Assigns the <see cref="HeatmapColorPalette"/> resource with the given preset name
        /// to the vector series, then updates the range for the current colour mode.
        /// Selects the circular variant when <c>ByDirection</c> is active so the hue wraps
        /// smoothly at 0°/360°; otherwise uses the linear variant.
        /// Palettes are declared as XAML resources in the view's resource dictionary.
        /// </summary>
        private void ApplyColorMapPreset(string name)
        {
            int idx = Array.IndexOf(PaletteNames, name);
            if (idx < 0) return;

            string key = VectorSeries.ColorMode == VectorColorMode.ByDirection
                ? CircularPaletteResourceKeys[idx]
                : PaletteResourceKeys[idx];
            VectorSeries.ColorMap = (HeatmapColorPalette)Resources[key];
            UpdateColorMapRange(VectorSeries.ColorMode);
        }

        private void OnLengthModeChanged(object sender, SelectionChangedEventArgs e)
        {
            if (VectorSeries == null || LengthModeCombo.SelectedItem == null) return;
            var mode = (VectorLengthMode)LengthModeCombo.SelectedItem;
            VectorSeries.LengthMode = mode;
            if (MagnitudeScaleHeader != null)
                MagnitudeScaleHeader.Visibility = mode == VectorLengthMode.MagnitudePixels ? Visibility.Visible : Visibility.Collapsed;
            if (MagnitudeScalePanel != null)
                MagnitudeScalePanel.Visibility = mode == VectorLengthMode.MagnitudePixels ? Visibility.Visible : Visibility.Collapsed;
            if (PixelLengthHeader != null)
                PixelLengthHeader.Visibility = mode == VectorLengthMode.FixedPixels ? Visibility.Visible : Visibility.Collapsed;
            if (PixelLengthPanel != null)
                PixelLengthPanel.Visibility = mode == VectorLengthMode.FixedPixels ? Visibility.Visible : Visibility.Collapsed;
        }

    }
}
Back to WPF Chart Examples Examples