SciChart® the market leader in Fast WPF Charts, WPF 3D Charts, iOS Chart, Android Chart and JavaScript Chart Components
SciChart WPF ships with hundreds of WPF Chart Examples which you can browse, play with, view the source-code and even export each WPF Chart Example to a stand-alone Visual Studio solution. All of this is possible with the new and improved SciChart WPF Examples Suite, which ships as part of the SciChart WPF SDK.
Runs an NBody simulation with 100,000 entities using the Task Parallel Library. The results are plotted on a SciChart XyScatterRenderableSeries at 30 FPS.
Documentation Links
– Performance Tips and Tricks
– How Fast is SciChart’s WPF Chart? DirectX vs. Software Comparison
The C#/WPF source code for the WPF Chart Realtime 2D Scatter 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?
<UserControl x:Class="SciChart.Examples.Examples.PerformanceDemos2D.UpdateScatter.UpdateScatterPoints"
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"
Background="#333"
d:DesignHeight="400"
d:DesignWidth="600"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<s:SciChartSurface x:Name="sciChart"
Grid.Row="1"
ext:ExampleHelpers.LoadedEventCommand="{Binding RunExampleCommand}"
ext:ExampleHelpers.UnloadedEventCommand="{Binding StopExampleCommand}">
<s:SciChartSurface.RenderableSeries>
<s:XyScatterRenderableSeries DataSeries="{Binding Victims}">
<s:XyScatterRenderableSeries.PointMarker>
<s:EllipsePointMarker Width="1"
Height="1"
Fill="Green"
StrokeThickness="0" />
</s:XyScatterRenderableSeries.PointMarker>
</s:XyScatterRenderableSeries>
<s:XyScatterRenderableSeries DataSeries="{Binding Defenders}">
<s:XyScatterRenderableSeries.PointMarker>
<s:EllipsePointMarker Width="1"
Height="1"
Fill="Red"
StrokeThickness="0" />
</s:XyScatterRenderableSeries.PointMarker>
</s:XyScatterRenderableSeries>
</s:SciChartSurface.RenderableSeries>
<s:SciChartSurface.YAxis>
<s:NumericAxis AutoRange="Never" VisibleRange="0,1" />
</s:SciChartSurface.YAxis>
<s:SciChartSurface.XAxis>
<s:NumericAxis AutoRange="Never" VisibleRange="0,1" />
</s:SciChartSurface.XAxis>
</s:SciChartSurface>
<s:SciChartPerformanceOverlay Grid.Row="1"
Margin="0,0,50,0"
VerticalAlignment="Top"
Background="#33FFFFFF"
FontWeight="Bold"
Foreground="#FFF"
Padding="10"
IsHitTestVisible="False"
TargetSurface="{Binding Source={x:Reference Name=sciChart}}" />
</Grid>
</UserControl>
// *************************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2022. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// UpdateScatterPointsView.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 System.Windows.Controls;
namespace SciChart.Examples.Examples.PerformanceDemos2D.UpdateScatter
{
public partial class UpdateScatterPoints : UserControl
{
public UpdateScatterPoints()
{
InitializeComponent();
}
}
}
// *************************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2022. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// UpdateScatterPointsViewModel.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 System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Timers;
using SciChart.Charting.Common.Helpers;
using SciChart.Charting.Model.DataSeries;
using SciChart.Core;
using SciChart.Examples.ExternalDependencies.Common;
namespace SciChart.Examples.Examples.PerformanceDemos2D.UpdateScatter
{
public class UpdateScatterPointsViewModel : BaseViewModel
{
private readonly string _exampleTitle;
private readonly string _exampleSubtitle;
private XyDataSeries<double, double> _victims;
private XyDataSeries<double, double> _defenders;
private World _simulation;
private Timer _timer;
private readonly int _pointCount;
private volatile bool _working = false;
private ObjectPool<XyDataSeries<double>> _seriesPool;
public UpdateScatterPointsViewModel()
{
// SL does not support hardware acceleration, so the point count is lowered deliberately in this edition
_pointCount = FeaturesHelper.Instance.SupportsHardwareAcceleration ? 100000 : 30000;
_seriesPool = new ObjectPool<XyDataSeries<double>>(10, series =>
{
series.AcceptsUnsortedData = true;
series.Capacity = _pointCount;
return series;
});
RunExampleCommand = new ActionCommand(OnRunExample);
StopExampleCommand = new ActionCommand(OnStopExample);
}
public ActionCommand RunExampleCommand { get; private set; }
public ActionCommand StopExampleCommand { get; private set; }
public XyDataSeries<double, double> Victims
{
get { return _victims; }
set
{
if (_victims == value) return;
_victims = value;
OnPropertyChanged("Victims");
}
}
public XyDataSeries<double, double> Defenders
{
get { return _defenders; }
set
{
if (_defenders == value) return;
_defenders = value;
OnPropertyChanged("Defenders");
}
}
public void OnStopExample()
{
if (_timer != null)
{
lock (_timer)
{
_timer.Stop();
_timer = null;
}
}
if (Victims != null) Victims.Clear();
if (Defenders != null) Defenders.Clear();
}
private void OnRunExample()
{
OnStopExample();
_simulation = new World(_pointCount);
_simulation.Populate();
Victims = _seriesPool.Get();
Defenders = _seriesPool.Get();
// We can append on a background thread. the simulation is computationally expensive
// so we move this to another thread.
_timer = new Timer(1000.0 / 60.0);
_timer.Elapsed += OnTimerElapsed;
_timer.Start();
}
private void OnTimerElapsed(object sender, EventArgs e)
{
if (_working) return;
lock (_timer)
{
_working = true;
// The simulation is very computationally expensive, so we update in parallel
// What we observe is 400ms is required to update position of 1,000,000 elements
var sw = Stopwatch.StartNew();
_simulation.Tick(4);
sw.Stop();
//Console.WriteLine("Tick: {0}ms", sw.ElapsedMilliseconds);
// By creating and filling a new series per-frame, we avoid the need
// to call SuspendUpdates on the series, which will lock the parent chart.
//
// Therefore we can append on another thread (other than UI Thread) and the UI will
// draw the last series on the UI thread, while we are filling this series on the background thread
//
// The trade-off is memory usage. Each time you create and discard an XyDataSeries you are putting additional
// pressure on the garbage collector.
var victims = _seriesPool.Get();
var defenders = _seriesPool.Get();
using (victims.SuspendUpdates())
using (defenders.SuspendUpdates())
{
victims.Clear();
defenders.Clear();
var persons = _simulation.People;
for (int i = 0; i < persons.Length; ++i)
{
Person person = persons[i];
if (person.IsVictim)
victims.Append(person.Pos.X, person.Pos.Y);
else
defenders.Append(person.Pos.X, person.Pos.Y);
}
_seriesPool.Put((XyDataSeries<double>) Victims);
_seriesPool.Put((XyDataSeries<double>) Defenders);
Victims = victims;
Defenders = defenders;
}
_working = false;
}
}
}
}
// *************************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2022. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// Defender.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 System.Windows;
using System.Windows.Media;
namespace SciChart.Examples.Examples.PerformanceDemos2D.UpdateScatter
{
public class Defender : Person
{
public Defender(Point initialPos)
: base(initialPos)
{
PersonId = "D";
ShowColor = new SolidColorBrush(Colors.Red);
}
public Person Aggressor { get; private set; }
public Person Victim { get; private set; }
public override void Initialize(World world)
{
Aggressor = world.GetRandomOtherPerson(this);
Victim = world.GetRandomOtherPerson(this, Aggressor);
}
public override void CalculateTarget()
{
Point diff = new Point((Victim.Pos.X - Aggressor.Pos.X)* 0.5, (Victim.Pos.Y - Aggressor.Pos.Y) * 0.5);
Target = new Point(Aggressor.Pos.X + diff.X, Aggressor.Pos.Y + diff.Y);
}
public override bool IsVictim
{
get { return false; }
}
}
}
// *************************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2022. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// Person.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 System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
using SciChart.Drawing.Utility;
namespace SciChart.Examples.Examples.PerformanceDemos2D.UpdateScatter
{
public abstract class Person : INotifyPropertyChanged
{
private Point _pos;
public string PersonId { get; protected set; }
public Brush ShowColor { get; protected set; }
public Point Pos
{
get { return _pos; }
protected set
{
_pos = value;
PropChanged("Pos");
}
}
public Point Target { get; protected set; }
public const double Speed = 1;
protected Person(Point initialPos)
{
Pos = initialPos;
}
public abstract void Initialize(World world);
public abstract void CalculateTarget();
public abstract bool IsVictim { get; }
private static double To01(double arg)
{
if (arg < 0)
return 0;
if (arg > 1)
return 1;
return arg;
}
public void UpdatePosition(double deltaT)
{
//Constrain target to (0,0)-(1,1)
Point target2 = new Point(To01(Target.X), To01(Target.Y));
double distance = PointUtil.Distance(target2, Pos);
if (distance < Speed*deltaT)
{
Pos = target2;
}
else
{
// Get the velocity vector
Point velocity = new Point((target2.X - Pos.X), (target2.Y - Pos.Y));
// Normalize the vector
double length = Math.Sqrt(velocity.X*velocity.X + velocity.Y*velocity.Y);
velocity = new Point(velocity.X / length, velocity.Y / length);
// Compute updated position
var newPos = new Point(Pos.X + velocity.X*deltaT, Pos.Y + velocity.Y*deltaT);
Pos = newPos;
}
}
private void PropChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
// *************************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2022. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// Victim.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 System;
using System.Windows;
using System.Windows.Media;
namespace SciChart.Examples.Examples.PerformanceDemos2D.UpdateScatter
{
public sealed class Victim : Person
{
public Victim(Point initialPos)
: base(initialPos)
{
PersonId = "V";
ShowColor = new SolidColorBrush(Colors.Green);
}
public Person Aggressor { get; set; }
public Person Defender { get; set; }
public override void Initialize(World world)
{
Aggressor = world.GetRandomOtherPerson(this);
Defender = world.GetRandomOtherPerson(this, Aggressor);
}
public sealed override void CalculateTarget()
{
Point aggressorPos = Aggressor.Pos;
Point defenderPos = Defender.Pos;
Point threatAxis = new Point((aggressorPos.X - Pos.X), (aggressorPos.Y - Pos.Y));
Point flightAxis = new Point(-threatAxis.Y, threatAxis.X); // 90 degrees to Threat
double length = Math.Sqrt(flightAxis.X * flightAxis.X + flightAxis.Y * flightAxis.Y);
if (length == 0 || defenderPos == aggressorPos)
{
Target = defenderPos;
}
else
{
Point safeVector = new Point((defenderPos.X - aggressorPos.X), (defenderPos.Y - aggressorPos.Y));
double denom = (flightAxis.X*safeVector.Y - flightAxis.Y*safeVector.X);
double q = denom == 0 ? 1 : (safeVector.X*threatAxis.Y - safeVector.Y*threatAxis.X)/denom;
if (q < 1 || double.IsNaN(q))
{
q = 1; // Make sure we're at least *behind* the defender, rather than just in line
}
Target = new Point(Pos.X + q * flightAxis.X, Pos.Y + q*flightAxis.Y);
}
}
public override bool IsVictim
{
get { return true; }
}
}
}
// *************************************************************************************
// SCICHART® Copyright SciChart Ltd. 2011-2022. All rights reserved.
//
// Web: http://www.scichart.com
// Support: support@scichart.com
// Sales: sales@scichart.com
//
// World.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 System;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace SciChart.Examples.Examples.PerformanceDemos2D.UpdateScatter
{
public class World : INotifyPropertyChanged
{
public delegate void WorldTickHandler(object sender, EventArgs e);
private Random _rng;
private double _deltaT;
private int _population;
private int _seed;
private double _victimFraction;
public World(int population = 10000)
{
Seed = 1;
Population = population;
VictimFraction = 0.9;
_deltaT = 0.01;
}
public Person[] People { get; private set; }
// To use when repopulating, not necessarily current now
public int Seed
{
get { return _seed; }
set
{
_seed = value;
PropChanged("Seed");
}
}
public int Population
{
get { return _population; }
set
{
_population = value;
PropChanged("Population");
}
}
public double VictimFraction
{
get { return _victimFraction; }
set
{
_victimFraction = value;
PropChanged("VictimFraction");
}
}
public double DeltaT
{
get { return _deltaT; }
set
{
_deltaT = value;
PropChanged("DeltaT");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public event WorldTickHandler OnWorldTick = delegate { };
public void Test()
{
var v = new Victim(new Point(0.8, 0.3));
var a = new Victim(new Point(0.8, 0.8));
var d = new Victim(new Point(0.5, 0.5));
v.Aggressor = a;
v.Defender = d;
v.CalculateTarget();
//v.Target should be 0.3,0.3
}
public double NextRandomDouble()
{
lock (_rng)
{
return _rng.NextDouble();
}
}
public int NextRandomIndex()
{
lock (_rng)
{
return _rng.Next(People.Length);
}
}
public void Populate()
{
_rng = new Random(Seed);
People = new Person[Population];
#if !SILVERLIGHT
Parallel.For(0, People.Length, i =>
{
var pos = new Point(NextRandomDouble(), NextRandomDouble());
People[i] = NextRandomDouble() < VictimFraction ? (Person) new Victim(pos) : new Defender(pos);
});
#else
for (int i = 0; i < People.Length; i++)
{
var pos = new Point(NextRandomDouble(), NextRandomDouble());
People[i] = NextRandomDouble() < VictimFraction ? (Person)new Victim(pos) : new Defender(pos);
}
#endif
for (int i = 0; i < People.Length; ++i)
{
People[i].Initialize(this);
}
PropChanged("People");
}
public Person GetRandomOtherPerson(params Person[] notThese)
{
Person result;
do
{
int i = NextRandomIndex();
result = People[i];
} while (notThese.Contains(result));
return result;
}
public void Tick(int cpuCount = 2)
{
if (People != null)
{
int chunkSize = People.Length/cpuCount;
Parallel.For(0, cpuCount, cpu =>
{
int nextChunk = cpu*chunkSize + chunkSize;
for (int i = cpu*chunkSize; i < nextChunk; i++)
{
Person person = People[i];
person.CalculateTarget();
person.UpdatePosition(_deltaT);
}
});
if (OnWorldTick != null)
{
OnWorldTick(this, EventArgs.Empty);
}
}
}
private void PropChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}