Home
+ +Test content
+ +Test content
+ +| index | Name | Age | Height |
|---|---|---|---|
0 | Oliver | 23 | 1.91 |
1 | Charlotte | 19 | 1.62 |
2 | Henry | 42 | 1.72 |
3 | Amelia | 64 | 1.57 |
4 | Owen | 35 | 1.85 |
404 Error
+This page does not exist
+
+
+> ๐ก Although this page describes a Windows Forms application, ScottPlot has a matching WPF control which can be used to achieve a similar result. See the [ScottPlot WPF Quickstart](https://scottplot.net/quickstart/wpf/) guide to get started.
+
+## Capture Audio Input
+
+You may find these prerequisite pages useful references:
+
+* [Getting Started with NAudio](../naudio) - how to setup NAudio to capture microphone data
+
+* [Realtime Audio Level Monitor](../level) - how to display an audio waveform in real time in a Windows Forms application
+
+I begin with a top-level fixed-size array containing audio values from the latest buffer.
+
+```cs
+readonly double[] AudioValues;
+```
+
+In my program initialization I use information about the sample rate and buffer size to determine the size of the array.
+
+```cs
+int SampleRate = 44100;
+int BufferMilliseconds = 20;
+AudioValues = new double[SampleRate * BufferMilliseconds / 1000];
+```
+
+I also configure the NAudio WaveIn event to call a function whenever new data arrives. That function converts bytes in the buffer to audio levels which it places in the top-level fixed-size array.
+
+```cs
+NAudio.Wave.WaveInEvent Wave = new ()
+{
+ DeviceNumber = comboBox1.SelectedIndex,
+ WaveFormat = new NAudio.Wave.WaveFormat(SampleRate, BitDepth, ChannelCount),
+ BufferMilliseconds = BufferMilliseconds
+};
+
+Wave.DataAvailable += WaveIn_DataAvailable;
+Wave.StartRecording();
+```
+
+```cs
+void WaveIn_DataAvailable(object? sender, NAudio.Wave.WaveInEventArgs e)
+{
+ for (int i = 0; i < e.Buffer.Length / 2; i++)
+ AudioValues[i] = BitConverter.ToInt16(e.Buffer, i * 2);
+}
+```
+
+> ๐ก No plotting or UI objects should be interacted with directly from this method because it may not run on the UI thread. Populating values in an already-created fixed-size array alleviates this concern.
+
+## Plot Setup
+
+I create a top-level fixed-size array to contain FFT values, plot the array, then later can update the values inside it and request a re-plot using a timer.
+
+One important consideration is that FFTs must be calculated using input data whose length is a power of 2. FftSharp has a `ZeroPad()` helper method which will pad any array with zeros to achieve this. I simulate zero-padding the audio data once in my initializer, calculate its FFT, then use that length to determine what size the `FftValues` array should be fixed to.
+
+```cs
+readonly double[] FftValues;
+double[] paddedAudio = FftSharp.Pad.ZeroPad(AudioValues);
+double[] fftMag = FftSharp.Transform.FFTmagnitude(paddedAudio);
+FftValues = new double[fftMag.Length];
+```
+
+I also use the `FFTfreqPeriod()` helper function to determine the frequency resolution of the FFT that is created. The total FFT spans a frequency between 0 and half of the sample rate (the Nyquist frequency), and this helper function just calculates the frequency spacing between each data point in that FFT.
+
+```cs
+double fftPeriod = FftSharp.Transform.FFTfreqPeriod(SampleRate, fftMag.Length);
+```
+
+I can then add the FFT array to the plot once, and simply request a refresh using a timer once the program is running.
+
+```cs
+formsPlot1.Plot.AddSignal(FftValues, 1.0 / fftPeriod);
+formsPlot1.Plot.YLabel("Spectral Power");
+formsPlot1.Plot.XLabel("Frequency (kHz)");
+formsPlot1.Refresh();
+```
+
+## Setup the FFT Calculator
+
+A timer is used to periodically re-calculate the FFT from the latest audio values and update the plot. The full source code at the bottom of the page demonstrates extra functionality I add here to perform peak frequency detection.
+
+```cs
+private void timer1_Tick(object sender, EventArgs e)
+{
+ // calculate FFT
+ double[] paddedAudio = FftSharp.Pad.ZeroPad(AudioValues);
+ double[] fft = FftSharp.Transform.FFTpower(paddedAudio);
+
+ // copy FFT into top-level FFT array
+ Array.Copy(fft, FftValues, fft.Length);
+
+ // request a redraw using a non-blocking render queue
+ formsPlot1.RefreshRequest();
+}
+```
+
+> ๐ก `FFTpower()` is log-transformed alternative to `FFTmagnitude()` which reports frequency power as Decibels (dB)
+
+## Increase FFT Size to Increase Frequency Resolution
+
+To increase the frequency resolution, more data must be included in the FFT. This could be achieved by increasing the sample rate, but 44100 Hz is pretty standard so let's consider alternate ways we could increase spectral resolution.
+
+### Option 1: Increase Buffer Length
+
+The current system creates a FFT sized according to the buffer length, so increasing the buffer size will result in more data entering the FFT calculation and improving spectral resolution. This will come at the expense of decrease frame rate though, because larger buffers means fewer buffers per second.
+
+### Option 2: Analyze FFT Across Multiple Buffers
+
+Increasing the amount of data entering the FFT increases the spectral resolution of the whole operation. The best way to achieve this is to have a data structure that accumulates samples across multiple buffers, then when the GUI requests an update the most recent N points are pulled from the growing accumulation of audio values that were obtained across multiple buffers. An ideal system would analyze the last N points where N is an even power of 2.
+
+## Use a Window to Improve Accuracy
+
+Assuming our signal is centered at 0, applying a window function can taper the edges of the input waveform so they approach 0, and the result is a reduction in the DC (low frequency) component of the FFT, and an increase in the spectral segregation properties of the waveform as well. Windowing is an important topic for anyone serious about creating FFTs from audio data, but is outside the scope of this article. See the [FftSharp windowing](https://github.com/swharden/FftSharp#windowing) page for details.
+
+## Full Source Code
+
+This project may be downloaded from GitHub: [AudioMonitor](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/audio/AudioMonitor)
+
+```cs
+public partial class FftMonitorForm : Form
+{
+ NAudio.Wave.WaveInEvent? Wave;
+
+ readonly double[] AudioValues;
+ readonly double[] FftValues;
+
+ readonly int SampleRate = 44100;
+ readonly int BitDepth = 16;
+ readonly int ChannelCount = 1;
+ readonly int BufferMilliseconds = 20;
+
+ public FftMonitorForm()
+ {
+ InitializeComponent();
+
+ AudioValues = new double[SampleRate * BufferMilliseconds / 1000];
+ double[] paddedAudio = FftSharp.Pad.ZeroPad(AudioValues);
+ double[] fftMag = FftSharp.Transform.FFTmagnitude(paddedAudio);
+ FftValues = new double[fftMag.Length];
+
+ double fftPeriod = FftSharp.Transform.FFTfreqPeriod(SampleRate, fftMag.Length);
+
+ formsPlot1.Plot.AddSignal(FftValues, 1.0 / fftPeriod);
+ formsPlot1.Plot.YLabel("Spectral Power");
+ formsPlot1.Plot.XLabel("Frequency (kHz)");
+
+ formsPlot1.Refresh();
+ }
+
+ private void FftMonitorForm_Load(object sender, EventArgs e)
+ {
+ for (int i = 0; i < NAudio.Wave.WaveIn.DeviceCount; i++)
+ {
+ var caps = NAudio.Wave.WaveIn.GetCapabilities(i);
+ comboBox1.Items.Add(caps.ProductName);
+ }
+ }
+
+ private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ if (Wave is not null)
+ {
+ Wave.StopRecording();
+ Wave.Dispose();
+
+ for (int i = 0; i < AudioValues.Length; i++)
+ AudioValues[i] = 0;
+ formsPlot1.Plot.AxisAuto();
+ }
+
+ if (comboBox1.SelectedIndex == -1)
+ return;
+
+ Wave = new NAudio.Wave.WaveInEvent()
+ {
+ DeviceNumber = comboBox1.SelectedIndex,
+ WaveFormat = new NAudio.Wave.WaveFormat(SampleRate, BitDepth, ChannelCount),
+ BufferMilliseconds = BufferMilliseconds
+ };
+
+ Wave.DataAvailable += WaveIn_DataAvailable;
+ Wave.StartRecording();
+
+ formsPlot1.Plot.Title(comboBox1.SelectedItem.ToString());
+ }
+
+ void WaveIn_DataAvailable(object? sender, NAudio.Wave.WaveInEventArgs e)
+ {
+ for (int i = 0; i < e.Buffer.Length / 2; i++)
+ AudioValues[i] = BitConverter.ToInt16(e.Buffer, i * 2);
+ }
+
+ private void timer1_Tick(object sender, EventArgs e)
+ {
+ double[] paddedAudio = FftSharp.Pad.ZeroPad(AudioValues);
+ double[] fftMag = FftSharp.Transform.FFTpower(paddedAudio);
+ Array.Copy(fftMag, FftValues, fftMag.Length);
+
+ // find the frequency peak
+ int peakIndex = 0;
+ for (int i = 0; i < fftMag.Length; i++)
+ {
+ if (fftMag[i] > fftMag[peakIndex])
+ peakIndex = i;
+ }
+ double fftPeriod = FftSharp.Transform.FFTfreqPeriod(SampleRate, fftMag.Length);
+ double peakFrequency = fftPeriod * peakIndex;
+ label1.Text = $"Peak Frequency: {peakFrequency:N0} Hz";
+
+ // auto-scale the plot Y axis limits
+ double fftPeakMag = fftMag.Max();
+ double plotYMax = formsPlot1.Plot.GetAxisLimits().YMax;
+ formsPlot1.Plot.SetAxisLimits(
+ xMin: 0,
+ xMax: 6,
+ yMin: 0,
+ yMax: Math.Max(fftPeakMag, plotYMax));
+
+ // request a redraw using a non-blocking render queue
+ formsPlot1.RefreshRequest();
+ }
+}
+```
+
+## Resources
+* Source code for this project: [AudioMonitor](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/audio/AudioMonitor)
+* [ScottPlot](https://ScottPlot.NET) - interactive plotting library for .NET
+* [ScottPlot Cookbook](https://scottplot.net/cookbook/)
+* [ScottPlot FAQ: How to plot live data](https://scottplot.net/faq/live-data/)
diff --git a/website/content/audio/level/index.md b/website/content/audio/level/index.md
new file mode 100644
index 0000000..1ffe5ea
--- /dev/null
+++ b/website/content/audio/level/index.md
@@ -0,0 +1,203 @@
+---
+title: Plot Audio Waveform with C#
+description: Display microphone audio in real time using NAudio and ScottPlot
+date: 2022-05-08
+weight: 2
+---
+
+**This page describes how I created a Windows Forms application to plot microphone audio in real time.** I used NAudio to capture the audio, and as new data arrived from the audio input device I converted its values to `double` and stored them in a `readonly double[]` used for plotting with [ScottPlot](https://scottplot.net). The result is a simple application that displays audio data in real time at a high framerate.
+
+
+
+> ๐ก Although this page describes a Windows Forms application, ScottPlot has a matching WPF control which can be used to achieve a similar result. See the [ScottPlot WPF Quickstart](https://scottplot.net/quickstart/wpf/) guide to get started.
+
+### Prepare an Array to Store Audio Data
+
+I started by creating an array the class level which will hold the values to be plotted. This array has been designed to hold the exact number of audio values that arrive from a single recorded buffer.
+
+```cs
+readonly double[] AudioValues;
+```
+
+The length of the array can be calculated from the recording parameters I chose:
+
+```cs
+AudioValues = new double[SampleRate * BufferMilliseconds / 1000];
+```
+
+### Setup the Plot
+
+I add the audio value array to the plot even though it contains all zeros now, but later I will populate the array with real data and simply request the plot to refresh itself. I'm using ScottPlot's _Signal Plot_ type which is performance-optimized to display evenly-spaced data. See the [ScottPlot Cookbook](https://scottplot.net/cookbook/) for details.
+
+```cs
+formsPlot1.Plot.AddSignal(AudioValues);
+```
+
+### List and Activate Recording Devices
+
+When the program starts, a ComboBox is populated with recording devices:
+
+```cs
+for (int i = 0; i < NAudio.Wave.WaveIn.DeviceCount; i++)
+{
+ var caps = NAudio.Wave.WaveIn.GetCapabilities(i);
+ comboBox1.Items.Add(caps.ProductName);
+}
+```
+
+> ๐ก NAudio considers `-1` a valid device ID. It typically maps to the Microsoft Sound Mapper, but I often omit it for simplicity.
+
+Selecting a recording device activates it. If a device was previously selected, that one is unloaded first.
+
+```cs
+int SampleRate = 44100;
+int BitDepth = 16;
+int ChannelCount = 1;
+int BufferMilliseconds = 20;
+
+NAudio.Wave.WaveInEvent? Wave;
+
+private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
+{
+ if (Wave is not null)
+ {
+ Wave.StopRecording();
+ Wave.Dispose();
+ }
+
+ Wave = new NAudio.Wave.WaveInEvent()
+ {
+ DeviceNumber = comboBox1.SelectedIndex,
+ WaveFormat = new NAudio.Wave.WaveFormat(SampleRate, BitDepth, ChannelCount),
+ BufferMilliseconds = BufferMilliseconds
+ };
+
+ Wave.DataAvailable += WaveIn_DataAvailable;
+ Wave.StartRecording();
+}
+```
+
+### Update Values when New Data is Available
+
+Since functions in the `DataAvailable` event handler may be invoked on threads other than the UI thread, it is important not to make any UI updates from these functions. Instead, this function translates the bytes from the buffer into values that are stored in the array we created when the Form was constructed.
+
+```cs
+void WaveIn_DataAvailable(object? sender, NAudio.Wave.WaveInEventArgs e)
+{
+ for (int i = 0; i < e.Buffer.Length / 2; i++)
+ AudioValues[i] = BitConverter.ToInt16(e.Buffer, i * 2);
+}
+```
+
+### Update the UI with a Timer
+
+I configured a `Timer` to run every 20 milliseconds which inspects data in the `AudioValues` array and updates the level meter and the plot.
+
+```cs
+private void timer1_Tick(object sender, EventArgs e)
+{
+ int level = (int)AudioValues.Max();
+ pbVolume.Value = (int)Math.Max(level, pbVolume.Maximum);
+ formsPlot1.RefreshRequest();
+}
+```
+
+> ๐ก ScottPlot's `RefreshRequest()` is a non-blocking alternative to `Refresh()` designed to keep UI windows interactive during redraws.
+
+## Full Source Code
+
+The extended source code shown here has a few additional features (such as automatic scaling of plots) that was not described in the code samples above. The source code here is what was used to create the animated screenshot at the top of the page. Full project source code is available for download on GitHub: [AudioMonitor](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/audio/AudioMonitor)
+
+```cs
+public partial class Form1 : Form
+{
+ NAudio.Wave.WaveInEvent? Wave;
+
+ readonly double[] AudioValues;
+
+ int SampleRate = 44100;
+ int BitDepth = 16;
+ int ChannelCount = 1;
+ int BufferMilliseconds = 20;
+
+ public Form1()
+ {
+ InitializeComponent();
+
+ AudioValues = new double[SampleRate * BufferMilliseconds / 1000];
+
+ formsPlot1.Plot.AddSignal(AudioValues, SampleRate / 1000);
+ formsPlot1.Plot.YLabel("Level");
+ formsPlot1.Plot.XLabel("Time (milliseconds)");
+ formsPlot1.Refresh();
+ }
+
+ private void Form1_Load(object sender, EventArgs e)
+ {
+ for (int i = 0; i < NAudio.Wave.WaveIn.DeviceCount; i++)
+ {
+ var caps = NAudio.Wave.WaveIn.GetCapabilities(i);
+ comboBox1.Items.Add(caps.ProductName);
+ }
+ }
+
+ private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ if (Wave is not null)
+ {
+ Wave.StopRecording();
+ Wave.Dispose();
+
+ for (int i = 0; i < AudioValues.Length; i++)
+ AudioValues[i] = 0;
+ formsPlot1.Plot.AxisAuto();
+ }
+
+ if (comboBox1.SelectedIndex == -1)
+ return;
+
+ Wave = new NAudio.Wave.WaveInEvent()
+ {
+ DeviceNumber = comboBox1.SelectedIndex,
+ WaveFormat = new NAudio.Wave.WaveFormat(SampleRate, BitDepth, ChannelCount),
+ BufferMilliseconds = BufferMilliseconds
+ };
+
+ Wave.DataAvailable += WaveIn_DataAvailable;
+ Wave.StartRecording();
+
+ formsPlot1.Plot.Title(comboBox1.SelectedItem.ToString());
+ }
+
+ void WaveIn_DataAvailable(object? sender, NAudio.Wave.WaveInEventArgs e)
+ {
+ for (int i = 0; i < e.Buffer.Length / 2; i++)
+ AudioValues[i] = BitConverter.ToInt16(e.Buffer, i * 2);
+ }
+
+ private void timer1_Tick(object sender, EventArgs e)
+ {
+ int level = (int)AudioValues.Max();
+
+ // auto-scale the maximum progressbar level
+ if (level > pbVolume.Maximum)
+ pbVolume.Maximum = level;
+ pbVolume.Value = level;
+
+ // auto-scale the plot Y axis limits
+ var currentLimits = formsPlot1.Plot.GetAxisLimits();
+ formsPlot1.Plot.SetAxisLimits(
+ yMin: Math.Min(currentLimits.YMin, -level),
+ yMax: Math.Max(currentLimits.YMax, level));
+
+ // request a redraw using a non-blocking render queue
+ formsPlot1.RefreshRequest();
+ }
+}
+```
+
+## Resources
+* Source code for this project: [AudioMonitor](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/audio/AudioMonitor)
+* [ScottPlot](https://ScottPlot.NET) - interactive plotting library for .NET
+* [ScottPlot Cookbook](https://scottplot.net/cookbook/)
+* [ScottPlot FAQ: How to plot live data](https://scottplot.net/faq/live-data/)
\ No newline at end of file
diff --git a/website/content/audio/level/winforms-audio-level-meter.gif b/website/content/audio/level/winforms-audio-level-meter.gif
new file mode 100644
index 0000000..4784ed5
Binary files /dev/null and b/website/content/audio/level/winforms-audio-level-meter.gif differ
diff --git a/website/content/audio/naudio/console-audio-level-meter.gif b/website/content/audio/naudio/console-audio-level-meter.gif
new file mode 100644
index 0000000..24fb9d9
Binary files /dev/null and b/website/content/audio/naudio/console-audio-level-meter.gif differ
diff --git a/website/content/audio/naudio/index.md b/website/content/audio/naudio/index.md
new file mode 100644
index 0000000..72542e6
--- /dev/null
+++ b/website/content/audio/naudio/index.md
@@ -0,0 +1,125 @@
+---
+title: Access Microphone Audio with C#
+description: How to use NAudio to sample microphone audio in a C# application
+date: 2022-05-07
+weight: 1
+---
+
+**This page demonstrates how to use NAudio to access microphone audio in a C# application.** See the [Audio Analysis and Visualization Page](../) for additional code examples.
+
+## Project Setup
+
+* **Add the NAudio package:** `dotnet add package naudio`
+
+* **Target Windows:** NAudio only supports Windows, so edit your project file to append `-windows` to your target framework
+
+```xml
+Vmeter = Vcell + LJP
+ +**To correct for LJP,** the electrophysiologist must calculate LJP mathematically (using software like LJPcalc) or estimate it experimentally (see the section on this topic below). Once the LJP is known it can be compensated for experimentally to improve accuracy of recorded and clamped voltages. + +Vcell = Vmeter - LJP
+ +> โ ๏ธ This method assumes that the amplifier voltage was zeroed at the start of the experiment when the pipette was in open-tip configuration +with the bath, and that concentration of chloride (if using Ag/AgCl electrodes) in the internal and bath solutions are stable throughout experiments. + +Vcell = Vmeasured - LJP
+ +Vcell = -48.13 - 16.05 mV
+ +Vcell = -64.18 mV
+ +We now know our cell rests at -64.18 mV. + +### Zeroed Voltage = LJP + Two Electrode Half-Cell Potentials + +The patch-clamp amplifier is typically zeroed at the start of every experiment when the patch pipette is in open-tip configuration +with the bath solution. An offset voltage (Voffset) is applied such that the Vmeasured is zero. +This process incorporates 3 potentials into the offset voltage: + +* **liquid junction potential (LJP)** between the **pipette** solution and the **bath** solution (mostly from small mobile ions) +* **half-cell potential (HCP)** between the **reference electrode** and the **bath** solution (mostly from Cl) +* **half-cell potential (HCP)** between the **recording electrode** and the **pipette** solution (mostly from Cl) + +When the amplifier is zeroed before to experiments, all 3 voltages are incorporated into the offset voltage. +Since the LJP is the only one that changes after forming whole-cell configuration with a patched cell (it is eliminated), +it is the only one that needs to be known and compensated for to achieve a true zero offset (the rest remain constant). + +However, if the [Cl] of the internal or bath solutions change during the course of an experiment +(most likely to occur when an Ag/AgCl pellet is immersed in a flowing bath solution), +the half-cell potentials become significant and affect Vmeasured as they change. +This is why agar bridge references are preferred over Ag/AgCl pellets. +See [Figl et al., 2003](https://medicalsciences.med.unsw.edu.au/sites/default/files/soms/page/ElectroPhysSW/AxoBits39New.pdf) +for more information about LJPs as they relate to electrophysiological experiments. + +### Measuring LJP Experimentally + +It is possible to measure LJP experimentally, but this technique is often discouraged because issues with KCl reference electrodes make it difficult +to accurately measure ([Barry and Diamond, 1970](https://link.springer.com/article/10.1007/BF01868010)). However, experimental measurement +may be the only option to calculate LJP for solutions containing ions with unknown mobilities. + +**To measure LJP Experimentally:**
+
+## Resources
+
+* [Download the source code for this project](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/maui-graphics)
+* [Use Microsoft.Maui.Graphics to Draw 2D Graphics in Any .NET Application](https://swharden.com/blog/2022-05-25-maui-graphics)
+* [https://maui.graphics](https://maui.graphics)
\ No newline at end of file
diff --git a/website/content/maui.graphics/quickstart-maui/index.md b/website/content/maui.graphics/quickstart-maui/index.md
new file mode 100644
index 0000000..50cb864
--- /dev/null
+++ b/website/content/maui.graphics/quickstart-maui/index.md
@@ -0,0 +1,44 @@
+---
+Title: Maui.Graphics Maui Quickstart
+Description: How to draw on a graphics view using Maui.Graphics in a MAUI application
+Date: 2021-10-27
+---
+
+```xml
+
+
+## Benchmark Strategy
+
+* **Size:** `800x600` - A fixed size (and intentionally _not_ supporting dynamic resizing as the window changes) will simplify the code for our performance tests.
+
+* **Background:** `#003366` will be used instead of black to ensure our background is indeed drawn.
+
+* **Line Position:** Random _integer_ coordinates for every line. Lines must me contained entirely within the drawing area.
+
+* **Line Width:** Random integer (1-10px) for every line.
+
+* **Line Color:** Random color (and alpha) for every line.
+
+* **Line Count:** `10`, `1k`, `10k`, or `100k` - Speed of some rendering systems depends on the _total area_ of drawn structures, while other systems may depend on the _total number of objects_ drawn.
+
+* **Clear Lines:** Lines from one render will not be visible in the next render.
+
+* **Random number generator:** Re-seed at 0 for every test.
+
+* **Repetitions:** We will report the mean render time of the first `10` frames rendered.
+
+* **Anti-aliasing:** Will be used if available.
+
+* **Alpha Type:** Premultiplied (8, 8, 8, 8)
+
+* **Display:** Display time will be included in the benchmark. For example, if buffered display requires creating or copying a Bitmap, the copy time will be included in the benchmark. However, we won't force our application to actually draw to the screen (so render + display time is not limited by the screen's refresh rate).
+
+* **Optimizations:** Realistic optimizations will be employed for this test. These will vary by library and be library-specific, but in general:
+ * Rendering must occur in a single threaded
+ * Bitmaps will be created once (not on every render)
+ * Styling objects are to be created inside the render loop but can be reused across lines (rather than get new'd for every line)
+
+* **Platform:** .NET Framework 4.7.2, 64-bit, debug mode. While all libraries in these experiments support .NET Core, the benchmark test application will target .NET Framework because at the time of writing the Windows Forms visual editor is not available for .NET Core.
+
+## Benchmark Results
+
+* Values shown are the time required to render 1 image.
+* Times < 5 sec are the mean of 10 renders
+* Times preceded by ~ are predicted times and not measured
+
+
+
+
+
+
+
+## Price
+
+According to the [DevExpress pricing section](https://www.devexpress.com/Products/NET/Controls/WPF/Charting/#Pricing) in 2022 the controls cost:
+
+* **$899/year for WPF charting controls only**
+* **$1,499/year for WPF and WinForms charting controls**
+* **$2,199/year for all charting controls (including mobile) and source code**
+
+## Examples and Documentation
+
+**The [WPF Controls Documentation](https://docs.devexpress.com/WPF/114223/controls-and-libraries/charts-suite/chart-control/fundamentals/series-fundamentals/2d-series-types) is an extensive collection of images and ~~code~~ notes describing how to create every graph type.** From what I gather this is another MVVC-style view system. You create some type of series object (model), pass it into a diagram type (view), then assign/modify the model's data.
+
+**Unfortunately I'm struggling to find concise start-to-finish code examples** that show complimentary XAML, CS, and an output figure. The GitHub projects that demonstrate primary plot types are _almost_ useful, but since they have only source code and not an image of their output they're not particularly helpful unless they're downloaded, compiled, and executed.
+
+Bar Chart | Line Graph
+---|---
+ | 
+
+**Flipping through the demos on the [WPF Charting website](https://www.devexpress.com/Products/NET/Controls/WPF/Charting/) I'm struck by how much _movement_ these charts contain.** I thought I'd know what a bar graph would look like, but when I click it to see the demo I see animated bars growing into position. When I click a scatter plot I see lines growing and markers popping into view.
+
+**I respect the effort that goes into coding animated graphs like that, but I can't picture what type of application would actually benefit from _animated_ scatter plots appearing in view.** It's hard to emphasize how off-putting I find this. A graph that takes 5 seconds to animate to achieve its final position means I have to sit there and wait for the animation to stop before I can even assess the data represented by the graph. Such an effect may fit in as a score history chart in a video game, or maybe some metric in a weather app, but is noting I'd want to see in a serious scientific application.
+
+## Demo Application
+
+**I was initially attracted to this project because the [WPF Charting](https://www.devexpress.com/Products/NET/Controls/WPF/Charting/) page looked so interesting.** I tried for a very long time to run a meaningful demo to check out this charting library, but the only demos I could find weren't very impressive the charting department.
+
+**Most demos on the [DevExpress demo page](https://www.devexpress.com/support/demos/) require activating a trial subscription.** Demo links direct to `dxdemo://` URLs which apparently requires the DevExpress trial program to be installed on your personal computer and configured in your browser. I find this absolutely revolting. Curiosity overcame my frustration though, and I threw in the towel and downloaded and installed the trial so I could view the demos. Props to DevExpress for letting me opt-out of phone-home notifications to their server during the installation process.
+
+Revenue Demo | Stock Demo
+---|---
+ | 
+
+This is interesting, but I'm not particularly impressed compared to some of the other options out there.
+
+DevExpress has a demo program in the Microsoft Store. Maybe some of the charting controls will be prominently displayed in it...
+
+Windows Store Installer | Budgets Demo
+---|---
+ | 
+
+**This demo app is crazy! I'm so confused right now.** It scrolls horizontally instead of vertically, and I almost didn't notice! The vertical content goes off the page too and there's no way to access it. Maybe I need to buy a larger monitor? These are the only graphs in the app. I'm guessing this app isn't intended to showcase the charting controls after all.
+
+## Conclusion
+
+Due to the lack of a meaningful demo I wasn't really able to assess the charting controls. The lack of such a demo is... perhaps an indication the charting controls are under-developed, or at least not the primary focus of the development team.
\ No newline at end of file
diff --git a/website/content/plotting-commercial/infragistics-chart/index.md b/website/content/plotting-commercial/infragistics-chart/index.md
new file mode 100644
index 0000000..2b359fe
--- /dev/null
+++ b/website/content/plotting-commercial/infragistics-chart/index.md
@@ -0,0 +1,34 @@
+---
+title: Infragistics Chart
+description: Investigating charting controls provided by Infragistics
+date: 2020-04-01
+weight: 20
+---
+
+**Infragistics has [charting tools for a variety of platforms](https://www.infragistics.com/products/ultimate) including Windows Forms and WPF.** They seem to actively develop a wide variety of controls for many frameworks (including web platforms), and that diffusion of effort leaves them with a charting library that is interesting and looks nice, but isn't really the type of thing I'd want to integrate into a scientific desktop application.
+
+
+
+## Price
+
+According to their [pricing page](https://www.infragistics.com/how-to-buy/product-pricing) in 2022 they have two plans that include WPF and WinForms controls. The only difference is that the "ultimate" plan contains "Indigo.Design" which according to their [getting started guide](https://www.infragistics.com/products/indigo-design/help/getting-started) seems to be a tool to generate tests and Angular code from Sketch and Adobe XD files.
+
+* **Professional: $1,295 / year per developer**
+* **Ultimate: $1,495 / year per developer**
+
+It looks like platform controls can be [purchased individually](https://www.infragistics.com/how-to-buy/product-pricing#individual-products):
+
+* **Windows Forms: $995 / year per developer**
+* **WPF: $995 / year per developer**
+
+## Demo
+
+**Infragistics has a [Windows Forms Reference Application](https://www.infragistics.com/products/ultimate#reference-apps) you can use to check out their controls.** It's huge (351 MB) and contains a ZIP of a MSI you must install before you can use. When it launches it tries to get you to review a modern-UI sample application, and you have to drill down into their legacy demo to access details about their charting control.
+
+
+
+**I didn't find any of these charts to be particularly performant or interactive,** so after a quick flip through these examples I was happy to put this project down and move on. I'm sure it's great for data grids and UI-focused application development, but as a scientist trying to graph data that's just not what I'm looking for.
+
+## Conclusions
+
+The charting demos work, but they're not particularly interactive, and clearly not the primary focus of this development team. This seems to be a miscellaneous control product, not really one that focuses specifically on charting.
\ No newline at end of file
diff --git a/website/content/plotting-commercial/infragistics-chart/infragistics-line-graph.png b/website/content/plotting-commercial/infragistics-chart/infragistics-line-graph.png
new file mode 100644
index 0000000..56b676b
Binary files /dev/null and b/website/content/plotting-commercial/infragistics-chart/infragistics-line-graph.png differ
diff --git a/website/content/plotting-commercial/infragistics-chart/infragistics-ultraWinChart.png b/website/content/plotting-commercial/infragistics-chart/infragistics-ultraWinChart.png
new file mode 100644
index 0000000..752e5c3
Binary files /dev/null and b/website/content/plotting-commercial/infragistics-chart/infragistics-ultraWinChart.png differ
diff --git a/website/content/plotting-commercial/lightningchart/index.md b/website/content/plotting-commercial/lightningchart/index.md
new file mode 100644
index 0000000..460fa9a
--- /dev/null
+++ b/website/content/plotting-commercial/lightningchart/index.md
@@ -0,0 +1,92 @@
+---
+title: LightningChart
+description: A closer look LightningChart's .NET charts for desktop applications
+date: 2023-07-08
+weight: 80
+---
+
+**LightningChart is a commercial .NET charting library for Windows.** It renders using DirectX and can be used in systems with or without hardware acceleration. Because of its high price I imagine LightningChart targets developers creating high-earning applications, and that casual individual developers are not its intended audience.
+
+
+
+## Usage
+
+**I cannot locate any source code samples on the LightningChart website,** so I am unable to comment on its API. It does not have a publicly accessible demo either, so I cannot comment on its performance or usability. They do have a 30 day free trial which probably comes with some example source code, but it is not accessible without a name and phone number so I will not be evaluating that.
+
+* Perhaps LightningChart's [JavaScript examples](https://lightningchart.com/lightningchart-js-interactive-examples/) are similar to their .NET offerings? They run in the browser without requiring a sign-up, they are more easily evaluated.
+
+* The [LightningChart user manual](https://lightningchart.com/wp-content/uploads/LightningChart-User%27s-manual-rev10.4.pdf) has some source code examples, but it is focused more on styling and customization than simple demonstrations of how to use the control.
+
+* I ended-up asking ChatGPT how to use LightningChart, and it provided some useful code examples (although I'm unable to evaluate if they compile or behave as expected). The following code is adapted from the response to my prompt: "I have x data [1, 2, 3, 4] and y data [1, 4, 9, 16]. How do I display these data as a line plot using the LightningChart .NET control for Windows Forms?"
+
+```cs
+public partial class Form1 : Form
+{
+ readonly LightningChartUltimate MyChart;
+
+ public Form1()
+ {
+ InitializeComponent();
+
+ // create a chart control and add it to the Form
+ MyChart = new() { Dock = DockStyle.Fill };
+ Controls.Add(MyChart);
+
+ // crete a X/Y data series
+ XYChartSeries series = new(
+ MyChart.ViewXY,
+ MyChart.ViewXY.XAxes[0],
+ MyChart.ViewXY.YAxes[0]);
+
+ // populate the data series with values
+ double[] xData = { 1, 2, 3, 4 };
+ double[] yData = { 1, 4, 9, 16 };
+ for (int i = 0; i < xData.Length; i++)
+ series.Points.AddXY(xData[i], yData[i]);
+
+ // add the data series to the chart
+ MyChart.ViewXY.PointLineSeries.Add(series);
+
+ // style the chart
+ MyChart.ViewXY.XAxes[0].Title.Text = "Horizontal Label";
+ MyChart.ViewXY.YAxes[0].Title.Text = "Vertical Label";
+
+ // refresh the chart to update the display
+ MyChart.ViewXY.AxisLayout.Suspend();
+ MyChart.ViewXY.AxisLayout.AutoAdjustMargins = true;
+ MyChart.ViewXY.AxisLayout.Resume();
+ MyChart.ViewXY.UpdateLayout();
+ MyChart.Update();
+ }
+}
+```
+
+## Pricing
+
+There is not a simple price for LightningChart packages, but instead a [matrix of customizations](https://lightningchart.com/net-charts/pricing/) that determines the price depending on various platform, chart type, and support options users wish to purchase. At the time of writing (mid 2023), here's how I would summarize it:
+
+* **Base product: $1,375/year** to develop for a single platform (e.g., WinForms) with support for only X/Y plots. The base price is multiplied by a factor if you want support for other plot types:
+
+* **Additional Chart Types:** It seems the base product comes with only support for X/Y plots, and additional packages including more chart types are available at additional cost:
+ * 3D: **1.4x price**
+ * 3D, polar, smith, maps, and signal plots: **1.75x price**
+ * 3D, polar, smith, maps, signal, and volume rendering: **2.3x price**
+
+* **Support: getting ten support tickets is 2x the price**, but unlimited support tickets are available for 2.5x the price. Note that every product comes with 2 support tickets included. It looks like support tickets can also be purchased in packs of 3 or 5 at $100 each.
+
+* **Multi-platform licenses are about 1.4x the price**, allowing developers to target WPF, WinForms, and UWP.
+
+**If I were developing a scientific application today** I would be interested in the package with multi-platform charting with support for signal plots and unlimited support tickets, which would run me **$5,775/year**.
+
+**Enterprise Pricing:** I started writing this without scrolling down their [pricing page](https://lightningchart.com/net-charts/pricing/) and now I see there's a whole different pricing system for Enterprise Packages, and it doesn't seem to be public and the only way to discover pricing is to request a quote.
+
+## Conclusions
+
+LightningChart seems like a strong product at a high price aimed at professional application developers creating lucrative products. As an independent open-source developer, I'm not their primary audience. Without a demo application I can run without signing-up, it's hard to evaluate its performance on my system. Without source code examples available on their website, it's hard to evaluate their API or documentation.
+
+If you have experience using LightningChart, let me know what you think! I would love to include more information here from somebody more familiar with their product.
+
+## Resources
+
+* [LightningChart Gallery](https://lightningchart.com/net-charts/gallery/)
+* [LightningChart YouTube demo of X/Y plots](https://www.youtube.com/watch?v=9CA-6o4ybmQ)
\ No newline at end of file
diff --git a/website/content/plotting-commercial/lightningchart/lightning-chart-1.png b/website/content/plotting-commercial/lightningchart/lightning-chart-1.png
new file mode 100644
index 0000000..ec80f1b
Binary files /dev/null and b/website/content/plotting-commercial/lightningchart/lightning-chart-1.png differ
diff --git a/website/content/plotting-commercial/scichart/index.md b/website/content/plotting-commercial/scichart/index.md
new file mode 100644
index 0000000..6ac69a9
--- /dev/null
+++ b/website/content/plotting-commercial/scichart/index.md
@@ -0,0 +1,97 @@
+---
+title: SciChart
+description: A closer look at interactive scientific charting provided by SciChart
+date: 2020-04-01
+lastmod: 2022-03-24
+weight: 10
+---
+
+**SciChart is a commercial charting library for .NET for high performance interactive charting.** Although they have mobile (iOS and Android) libraries, here we will take a closer look at their WPF charts library. It may be possible to use this control in Windows Forms applications with [ElementHost](https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-3.5/ms754008(v=vs.90)) or [more modern](https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/walkthrough-hosting-a-wpf-composite-control-in-windows-forms) techniques, but their core product is intended for use in WPF applications.
+
+
+
+**SciChart strikes a nice balance of performance and aesthetic.** These charts benefit from DirectX (Windows), OpenGL (Android), or Metal (iOS) hardware acceleration, and controls are extensively themeable and have many customizable behaviors. The SciChart trial application demonstrates many plot types, and the intuitive controls, well-balanced visuals, and subtle animations are the best I've seen in this class of software.
+
+## Price
+According to the [SciChart store](https://store.scichart.com/) in 2022:
+
+* $1,699 / year per developer for binaries for 2D charts for Windows
+* $1,999 / year per developer for binaries for 2D charts for all platforms
+* $2,299 / year per developer for binaries for 2D and 3D charts for Windows
+* $2,999 / year per developer for binaries for 2D and 3D charts for all platforms
+* $3,999 / year per developer for full source code
+
+## Demo
+
+The [SciChart examples website](https://www.scichart.com/example/) shows many sample charts with source code. Let's take a closer look at the [line chart example](https://www.scichart.com/example/wpf-line-chart-example/) shown in the screenshot at the top of the page.
+
+
+
+Most styling and behavior customization is achieved by writing XAML:
+
+```xml
+
+
+## Conclusions
+
+**SciChart's excellent performance, extensive documentation, and options for paid support make it an appealing option for integration into commercial software products.** It has a hefty price tag, but the cost may be worth it for companies whose earning potential is pinned to the quality of the charting controls in their software products.
+
+SciChart's [Comparison of SciChart vs. Open Source Chart controls](https://www.scichart.com/comparison-of-scichart-vs-open-source-chart-controls/) page makes a strong case that "free" software comes with drawbacks like limited bugfixes, risk of project abandonment, no guarantee of support, etc. While these may not be compelling arguments against using open-source software for hobby projects, paying for a commercial charting control run by a team of developers and support staff has benefits which appeal to enterprise clients that profit directly from their software.
+
+> ๐ก **Update:** In March, 2022 I had a brief conversation with with SciChart's founder [Dr. Andrew Burnett-Thompson](https://www.scichart.com/about-us/) about the role of SciChart in an environment where open-source alternatives exist. Andrew was very supportive of open-source projects and their maintainers, and noted that SciChart is a professional product created for enterprises which stand to benefit from the superior quality and support that paying for a commercial charting library can provide.
+
+## Resources
+* [SciChart WPF SDK Documentation](https://www.scichart.com/documentation/win/current/SciChart_WPF_SDK_User_Manual.html)
+* [SciChart WPF Tutorials](https://www.scichart.com/documentation/v5.x/Tutorial%2001%20-%20Referencing%20SciChart%20DLLs.html)
+* [SciChart Community FAQs (Forums)](https://www.scichart.com/questions)
+* [WPF Chart Examples with Source-Code](https://www.scichart.com/wpf-chart-examples)
\ No newline at end of file
diff --git a/website/content/plotting-commercial/scichart/scichart-demo.gif b/website/content/plotting-commercial/scichart/scichart-demo.gif
new file mode 100644
index 0000000..8fd6fdd
Binary files /dev/null and b/website/content/plotting-commercial/scichart/scichart-demo.gif differ
diff --git a/website/content/plotting-commercial/scichart/scichart-demos.jpg b/website/content/plotting-commercial/scichart/scichart-demos.jpg
new file mode 100644
index 0000000..d4f36b2
Binary files /dev/null and b/website/content/plotting-commercial/scichart/scichart-demos.jpg differ
diff --git a/website/content/plotting-commercial/scichart/scichart-performance.gif b/website/content/plotting-commercial/scichart/scichart-performance.gif
new file mode 100644
index 0000000..6f93b59
Binary files /dev/null and b/website/content/plotting-commercial/scichart/scichart-performance.gif differ
diff --git a/website/content/plotting-commercial/syncfusion-wpf-charts/index.md b/website/content/plotting-commercial/syncfusion-wpf-charts/index.md
new file mode 100644
index 0000000..0eace70
--- /dev/null
+++ b/website/content/plotting-commercial/syncfusion-wpf-charts/index.md
@@ -0,0 +1,103 @@
+---
+title: Syncfusion WPF Charts
+description: Using SyncFusion's SfChart to display data in WPF applications
+date: 2020-04-01
+lastmod: 2022-03-17
+weight: 60
+---
+
+**Syncfusion's SfChart control is available for many platforms including Windows Forms, WPF, UWP, Xamarin, and even JavaScript (Angular, React, and Vue).** Their [demos page](https://www.syncfusion.com/demos) is quite impressive, and it's particularly interesting that the same types of plots available in the WPF controls are [viewable in your browser](https://ej2.syncfusion.com/vue/demos/#/material/chart/histogram.html) using JavaScript.
+
+
+
+**SfChart's documentation is easy and accessible.** They have working demos paired with source code showing how to generate all types of plots (e.g., [Bubble and Scatter in WPF Charts](https://help.syncfusion.com/wpf/charts/seriestypes/bubbleandscatter)). They also have controls for more specialized charting, like the [SfSmithChart](https://help.syncfusion.com/wpf/smith-chart/overview) for making [Smith Charts](https://en.wikipedia.org/wiki/Smith_chart).
+
+
+The [Migrating from Microsoft Chart to SfChart](https://help.syncfusion.com/wpf/charts/migrating-from-chart-to-sfchart) guide makes it easy to learn how to use SfChart for those who already have a strong grasp of Microsoft's Chart module.
+
+## Price
+
+According to their [pricing page](https://www.syncfusion.com/sales/products) in 2022
+
+* $995 / year per developer for one set of controls (e.g., WinForms or WPF)
+* $2,495 / year per developer for all controls
+* prices decrease after the first year
+
+## Demo
+
+A WPF demo can now be downloaded from [appcenter](https://install.appcenter.ms/orgs/syncfusion-demos/apps/wpf-demos/distribution_groups/release) and run as described in [this help page](https://help.syncfusion.com/wpf/samples). Plots are smooth, but do not seem to be interactive to allow panning or zooming with the mouse.
+
+
+
+## Line Plot
+
+Let's take a closer look at their "fast charts" line plot demo.
+
+
+
+Most of the styling and behavior of charts is controlled with XAML:
+
+```xml
+
+
+## Price
+
+According to the [WPF](https://www.telerik.com/purchase/individual/wpf.aspx) and [WinForms](https://www.telerik.com/purchase/individual/winforms.aspx) pricing pages in 2022:
+
+* $999 / year per developer for WPF controls
+* $999 / year per developer for Windows Forms controls
+
+## Demo
+
+The Telerik UI for WPF has a demo you have to download and install. I chose to take a look at the WPF control because the screenshots of the Windows Forms control didn't look very chart-rich.
+
+
+
+The `ChartView` control has several example uses. In all cases the plots are "pretty", but look like they're designed more for simple appearance and theming support rather than performance. I don't think this charting library is suitable for high-performance scientific charting, though I did find the demo to be very insightful and a good way to inspect all the controls provided with this package.
+
+## Default Interactivity
+* left-click-drag: zoom region (or pan)
+* scroll-wheel: zoom
+
+## Resources
+* WPF Control: https://www.telerik.com/products/wpf/chart.aspx
+* WinForms Control: https://www.telerik.com/products/winforms.aspx
\ No newline at end of file
diff --git a/website/content/plotting-commercial/telerik-ui-chart/telerik-live-data.gif b/website/content/plotting-commercial/telerik-ui-chart/telerik-live-data.gif
new file mode 100644
index 0000000..072692d
Binary files /dev/null and b/website/content/plotting-commercial/telerik-ui-chart/telerik-live-data.gif differ
diff --git a/website/content/plotting-commercial/telerik-ui-chart/telerik-ui-wpf-demo.jpg b/website/content/plotting-commercial/telerik-ui-chart/telerik-ui-wpf-demo.jpg
new file mode 100644
index 0000000..dc68471
Binary files /dev/null and b/website/content/plotting-commercial/telerik-ui-chart/telerik-ui-wpf-demo.jpg differ
diff --git a/website/content/plotting-free/_index.md b/website/content/plotting-free/_index.md
new file mode 100644
index 0000000..6d037b7
--- /dev/null
+++ b/website/content/plotting-free/_index.md
@@ -0,0 +1,3 @@
+---
+Title: Free Plotting Libraries
+---
\ No newline at end of file
diff --git a/website/content/plotting-free/interactive-data-display/D3-WPF-Version-2.pdf b/website/content/plotting-free/interactive-data-display/D3-WPF-Version-2.pdf
new file mode 100644
index 0000000..e9a73bb
Binary files /dev/null and b/website/content/plotting-free/interactive-data-display/D3-WPF-Version-2.pdf differ
diff --git a/website/content/plotting-free/interactive-data-display/index.md b/website/content/plotting-free/interactive-data-display/index.md
new file mode 100644
index 0000000..3248cd0
--- /dev/null
+++ b/website/content/plotting-free/interactive-data-display/index.md
@@ -0,0 +1,232 @@
+---
+title: Plot Data with Interactive Data Display
+description: Getting Started with Microsoft's Interactive Data Display Library for WPF
+date: 2020-05-18
+lastmod: 2022-03-16
+weight: 80
+---
+
+**Interactive Data Display is a set of WPF controls developed by Microsoft for interactively displaying data in WPF applications.** It supports line graphs, bubble charts, heat maps and other complex 2D plots which are very common in scientific software. The last commit on the [GitHub page](https://github.com/Microsoft/InteractiveDataDisplay.WPF) is from 2018 and the last published NuGet package [`InteractiveDataDisplay.WPF`](https://www.nuget.org/packages/InteractiveDataDisplay.WPF/) is from 2017, so it seems **this project is no longer maintained**.
+
+
+
+### Platform Support
+
+**The official package only supports .NET Framework:** The NuGet package (version 1.0.0) does not support .NET Core. More specifically, the NuGet package has complex antiquated dependencies and the official NuGet package only works out of the box on `.NET Framework 4.5.2`
+
+**Unofficial support for .NET Core:** Issue [#34](https://github.com/microsoft/InteractiveDataDisplay.WPF/issues/32) from 2019 links to [a fork by Jeff Mastry](https://github.com/mastry/InteractiveDataDisplay.WPF/tree/core) that supports .NET Core, but it's not available on NuGet and Microsoft never responded to the issue.
+
+### Interactivity
+* left-click-drag to pan
+* left-click-drag on an axis to pan just that axis
+* scroll-wheel to zoom
+* double-click to fit axis limits to data
+
+## Quickstart
+
+1. Create a .NET Framework WPF Application
+
+2. Add the `InteractiveDataDisplay.WPF` NuGet package
+
+3. Add a chart control to your layout
+
+4. `Add()` graph types to your chart
+
+## MainWindow.xaml
+
+```xml
+
+
+```cs
+// generate some random Y data
+int pointCount = 5;
+double[] xs1 = Consecutive(pointCount, offset: 0);
+double[] xs2 = Consecutive(pointCount, offset: .4);
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create the series and describe their styling
+var bar1 = new InteractiveDataDisplay.WPF.BarGraph()
+{
+ Color = Brushes.Blue,
+ Description = "Group A",
+ BarsWidth = .35,
+};
+
+var bar2 = new InteractiveDataDisplay.WPF.BarGraph()
+{
+ Color = Brushes.Red,
+ Description = "Group B",
+ BarsWidth = .35,
+};
+
+// load data into each series
+bar1.PlotBars(xs1, ys1);
+bar2.PlotBars(xs2, ys2);
+
+// add the series to the grid
+myGrid.Children.Clear();
+myGrid.Children.Add(bar1);
+myGrid.Children.Add(bar2);
+
+// customize styling
+myChart.Title = $"Bar Graph";
+myChart.BottomTitle = $"Horizontal Axis Label";
+myChart.LeftTitle = $"Vertical Axis Label";
+myChart.IsAutoFitEnabled = false;
+myChart.LegendVisibility = Visibility.Visible;
+
+// set axis limits manually
+myChart.PlotOriginX = .5;
+myChart.PlotWidth = 5.5;
+myChart.PlotOriginY = 0;
+myChart.PlotHeight = 200;
+```
+
+## Scatter Plot
+
+
+
+```cs
+// generate some random X and Y data
+int pointCount = 500;
+double[] xs1 = RandomWalk(pointCount);
+double[] ys1 = RandomWalk(pointCount);
+double[] xs2 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+double[] sizes = Consecutive(pointCount, 10, 0);
+
+// create the lines and describe their styling
+var line1 = new InteractiveDataDisplay.WPF.CircleMarkerGraph()
+{
+ Color = new SolidColorBrush(Colors.Blue),
+ Description = "Group A",
+ StrokeThickness = 1
+};
+
+var line2 = new InteractiveDataDisplay.WPF.CircleMarkerGraph()
+{
+ Color = new SolidColorBrush(Colors.Red),
+ Description = "Group B",
+ StrokeThickness = 1
+};
+
+// load data into the lines
+line1.PlotSize(xs1, ys1, sizes);
+line2.PlotSize(xs2, ys2, sizes);
+
+// add lines into the grid
+myGrid.Children.Clear();
+myGrid.Children.Add(line1);
+myGrid.Children.Add(line2);
+
+// customize styling
+myChart.Title = $"Line Plot ({pointCount:n0} points each)";
+myChart.BottomTitle = $"Horizontal Axis Label";
+myChart.LeftTitle = $"Vertical Axis Label";
+myChart.IsAutoFitEnabled = true;
+myChart.LegendVisibility = Visibility.Hidden;
+```
+
+## Line Plot
+
+I can display lines with about 100 thousand lines points performance starts to greatly suffer.
+
+
+
+```cs
+int pointCount = 10_000;
+double[] xs = Consecutive(pointCount);
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create the lines and describe their styling
+var line1 = new InteractiveDataDisplay.WPF.LineGraph
+{
+ Stroke = new SolidColorBrush(Colors.Blue),
+ Description = "Line A",
+ StrokeThickness = 2
+};
+
+var line2 = new InteractiveDataDisplay.WPF.LineGraph
+{
+ Stroke = new SolidColorBrush(Colors.Red),
+ Description = "Line B",
+ StrokeThickness = 2
+};
+
+// load data into the lines
+line1.Plot(xs, ys1);
+line2.Plot(xs, ys2);
+
+// add lines into the grid
+myGrid.Children.Clear();
+myGrid.Children.Add(line1);
+myGrid.Children.Add(line2);
+
+// customize styling
+myChart.Title = $"Line Plot ({pointCount:n0} points each)";
+myChart.BottomTitle = $"Horizontal Axis Label";
+myChart.LeftTitle = $"Vertical Axis Label";
+myChart.IsAutoFitEnabled = true;
+myChart.LegendVisibility = Visibility.Visible;
+```
+
+## Resources
+
+* GitHub: [Interactive Data Display for WPF](https://github.com/microsoft/InteractiveDataDisplay.WPF)
+
+* NuGet: [`InteractiveDataDisplay.WPF`](https://www.nuget.org/packages/InteractiveDataDisplay.WPF/)
+
+* Documentation: [Dynamic Data Display library (D3) Core Algorithms and Architecture (PDF)](D3-WPF-Version-2.pdf) is the closest thing to documentation I think there is for this library. I found it [here](https://github.com/artemiusgreat/InteractiveDataDisplay.WPF/) under a MIT license and am copying it to this website so search engines can index it.
+
+## Source Code
+
+* [interactive-data-display](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/plotting/interactive-data-display)
\ No newline at end of file
diff --git a/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart-bar-graph.png b/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart-bar-graph.png
new file mode 100644
index 0000000..0a21422
Binary files /dev/null and b/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart-bar-graph.png differ
diff --git a/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart-line-plot.png b/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart-line-plot.png
new file mode 100644
index 0000000..fed63a9
Binary files /dev/null and b/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart-line-plot.png differ
diff --git a/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart-scatter-plot.png b/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart-scatter-plot.png
new file mode 100644
index 0000000..1805fd5
Binary files /dev/null and b/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart-scatter-plot.png differ
diff --git a/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart.gif b/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart.gif
new file mode 100644
index 0000000..f538034
Binary files /dev/null and b/website/content/plotting-free/interactive-data-display/interactive-data-display-quickstart.gif differ
diff --git a/website/content/plotting-free/livecharts/index.md b/website/content/plotting-free/livecharts/index.md
new file mode 100644
index 0000000..5ae8dde
--- /dev/null
+++ b/website/content/plotting-free/livecharts/index.md
@@ -0,0 +1,158 @@
+---
+title: Plot Data with LiveCharts
+description: How to plot data in a .NET application using the LiveCharts library
+date: 2020-05-17
+lastmod: 2022-03-16
+weight: 40
+---
+
+**LiveCharts is a charing library that updates and animates automatically as data changes in real time.** LiveCharts is up-front about the that performance is a secondary concern. LiveCharts used to sell an enhancement called the "Geared package" which improved performance using DirectX (although [SharpDX is no longer maintained](http://sharpdx.org/)). LiveCharts2 began development in 2021 and uses an alternative rendering system (SkiaSharp) to improve support and performance across multiple platforms.
+
+
+
+### Project Status
+
+**LiveCharts appears mostly abandoned.** The [LiveCharts GitHub page](https://github.com/Live-Charts/Live-Charts) claimed version 1.0 was "about to be released" in 2018 but that text is now crossed-out. The [last commit to the project](https://github.com/Live-Charts/Live-Charts/commits/) (other than updates to the readme) was in 2018. The last NuGet published was in 2017.
+
+**LiveCharts2 is in preview and is being actively developed.** Work is in the [LiveCharts2 GitHub repository](https://github.com/beto-rodriguez/LiveCharts2) but there are not yet packages on NuGet.
+
+## Quickstart
+
+LiveCharts is designed to work in WPF applications, but for consistency with the other examples in this series the quickstart will be made using Windows Forms
+
+* Install the `LiveCharts.WinForms` package
+* Drag a `CartesianChart` from the toolbox onto your form
+
+### Sample Data
+
+This code generates random data we can practice plotting
+
+```cs
+private Random rand = new Random(0);
+private double[] RandomWalk(int points = 5, double start = 100, double mult = 50)
+{
+ // return an array of difting random numbers
+ double[] values = new double[points];
+ values[0] = start;
+ for (int i = 1; i < points; i++)
+ values[i] = values[i - 1] + (rand.NextDouble() - .5) * mult;
+ return values;
+}
+```
+
+### Bar Graph
+
+
+
+```cs
+// generate some random Y data
+int pointCount = 5;
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create series and populate them with data
+var series1 = new LiveCharts.Wpf.ColumnSeries
+{
+ Title = "Group A",
+ Values = new LiveCharts.ChartValues
+
+```cs
+// generate some random XY data
+int pointCount = 100;
+double[] xs1 = RandomWalk(pointCount);
+double[] ys1 = RandomWalk(pointCount);
+double[] xs2 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create series and populate them with data
+var series1 = new LiveCharts.Wpf.ScatterSeries
+{
+ Title = "Group A",
+ Values = new LiveCharts.ChartValues
+
+```cs
+// generate some random Y data
+int pointCount = 200;
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create series and populate them with data
+var series1 = new LiveCharts.Wpf.LineSeries()
+{
+ Title = "Group A",
+ Values = new LiveCharts.ChartValues
+
+### Disadvantages
+* It is not mouse-interactive
+* It only supports Windows Forms
+* It is only available on .NET Framework
+* It is no longer actively developed
+
+## Quickstart
+
+### Sample Data
+
+This code generates random data we can practice plotting
+
+```cs
+private Random rand = new Random(0);
+private double[] RandomWalk(int points = 5, double start = 100, double mult = 50)
+{
+ // return an array of difting random numbers
+ double[] values = new double[points];
+ values[0] = start;
+ for (int i = 1; i < points; i++)
+ values[i] = values[i - 1] + (rand.NextDouble() - .5) * mult;
+ return values;
+}
+```
+
+### Bar Graph (Column Chart)
+
+Microsoft calls a horizontal bar graph a "bar chart", while a bar graph with vertical bars is called a "column chart". This program can be made with the following code.
+
+
+
+```cs
+// generate some random Y data
+int pointCount = 5;
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create a series for each line
+Series series1 = new Series("Group A");
+series1.Points.DataBindY(ys1);
+series1.ChartType = SeriesChartType.Column;
+
+Series series2 = new Series("Group B");
+series2.Points.DataBindY(ys2);
+series2.ChartType = SeriesChartType.Column;
+
+// add each series to the chart
+chart1.Series.Clear();
+chart1.Series.Add(series1);
+chart1.Series.Add(series2);
+
+// additional styling
+chart1.ResetAutoValues();
+chart1.Titles.Clear();
+chart1.Titles.Add($"Column Chart ({pointCount} points per series)");
+chart1.ChartAreas[0].AxisX.Title = "Horizontal Axis Label";
+chart1.ChartAreas[0].AxisY.Title = "Vertical Axis Label";
+chart1.ChartAreas[0].AxisY.MajorGrid.LineColor = Color.LightGray;
+chart1.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.LightGray;
+```
+
+### Scatter Plot (Line Chart)
+
+The look of a scatter plot can be achieved by binding X and Y data.
+
+
+
+```cs
+// generate some random XY data
+int pointCount = 1_000;
+double[] xs1 = RandomWalk(pointCount);
+double[] ys1 = RandomWalk(pointCount);
+double[] xs2 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create a series for each line
+Series series1 = new Series("Group A");
+series1.Points.DataBindXY(xs1, ys1);
+series1.ChartType = SeriesChartType.Line;
+series1.MarkerStyle = MarkerStyle.Circle;
+
+Series series2 = new Series("Group B");
+series2.Points.DataBindXY(xs2, ys2);
+series2.ChartType = SeriesChartType.Line;
+series2.MarkerStyle = MarkerStyle.Circle;
+
+// add each series to the chart
+chart1.Series.Clear();
+chart1.Series.Add(series1);
+chart1.Series.Add(series2);
+
+// additional styling
+chart1.ResetAutoValues();
+chart1.Titles.Clear();
+chart1.Titles.Add($"Scatter Plot ({pointCount:N0} points per series)");
+chart1.ChartAreas[0].AxisX.Title = "Horizontal Axis Label";
+chart1.ChartAreas[0].AxisY.Title = "Vertical Axis Label";
+chart1.ChartAreas[0].AxisY.MajorGrid.LineColor = Color.LightGray;
+chart1.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.LightGray;
+```
+
+### Line Plot (Fast Line Chart)
+
+The `FastLine` chart is optimized for speed. On my system I can comfortably display 100,000 points in real time. When I tried to display 1 million points interaction became very sluggish.
+
+> **๐ก What makes FastLine fast?** According to Microsoft, "The FastLine chart type is a variation of the Line chart that significantly reduces the drawing time of a series that contains a very large number of data points. Use this chart in situations where very large data sets are used and rendering speed is critical. Some features are omitted (control of point level visual attributes, markers, data point labels, and shadows) to improve performance."
+
+
+
+```cs
+// generate some random Y data
+int pointCount = 100_000;
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create a series for each line
+Series series1 = new Series("Group A");
+series1.Points.DataBindY(ys1);
+series1.ChartType = SeriesChartType.FastLine;
+
+Series series2 = new Series("Group B");
+series2.Points.DataBindY(ys2);
+series2.ChartType = SeriesChartType.FastLine;
+
+// add each series to the chart
+chart1.Series.Clear();
+chart1.Series.Add(series1);
+chart1.Series.Add(series2);
+
+// additional styling
+chart1.ResetAutoValues();
+chart1.Titles.Clear();
+chart1.Titles.Add($"Fast Line Plot ({pointCount:N0} points per series)");
+chart1.ChartAreas[0].AxisX.Title = "Horizontal Axis Label";
+chart1.ChartAreas[0].AxisY.Title = "Vertical Axis Label";
+chart1.ChartAreas[0].AxisY.MajorGrid.LineColor = Color.LightGray;
+chart1.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.LightGray;
+```
+
+## Source Code
+
+* [Microsoft Charting Quickstart](https://github.com/swharden/Csharp-Data-Visualization/tree/main/dev/old/plotting/microsoft-charting/ChartingQuickstart)
diff --git a/website/content/plotting-free/microsoft-charting/microsoft-charting-toolbox.png b/website/content/plotting-free/microsoft-charting/microsoft-charting-toolbox.png
new file mode 100644
index 0000000..033c407
Binary files /dev/null and b/website/content/plotting-free/microsoft-charting/microsoft-charting-toolbox.png differ
diff --git a/website/content/plotting-free/microsoft-charting/quickstart-bar.png b/website/content/plotting-free/microsoft-charting/quickstart-bar.png
new file mode 100644
index 0000000..ca8cf9b
Binary files /dev/null and b/website/content/plotting-free/microsoft-charting/quickstart-bar.png differ
diff --git a/website/content/plotting-free/microsoft-charting/quickstart-fast-line.png b/website/content/plotting-free/microsoft-charting/quickstart-fast-line.png
new file mode 100644
index 0000000..6cbb28a
Binary files /dev/null and b/website/content/plotting-free/microsoft-charting/quickstart-fast-line.png differ
diff --git a/website/content/plotting-free/microsoft-charting/quickstart-scatter.png b/website/content/plotting-free/microsoft-charting/quickstart-scatter.png
new file mode 100644
index 0000000..c87f1b5
Binary files /dev/null and b/website/content/plotting-free/microsoft-charting/quickstart-scatter.png differ
diff --git a/website/content/plotting-free/nplot/index.md b/website/content/plotting-free/nplot/index.md
new file mode 100644
index 0000000..d382a47
--- /dev/null
+++ b/website/content/plotting-free/nplot/index.md
@@ -0,0 +1,101 @@
+---
+title: Plot Data with NPlot
+description: How to plot data using NPlot in C# projects
+date: 2020-05-15
+lastmod: 2022-03-16
+weight: 60
+---
+
+**NPlot is a charting library for .NET Framework applications.** It can be used to render graphs as Bitmaps (suitable for use in console applications) and it has a mouse-interactive user control for Windows Forms. NPlot is simple, but it lacks many features, and it only targets .NET Framework.
+
+
+
+### NPlot is no longer developed
+* [NPlot WIKI](http://netcontrols.org/nplot/wiki/index.php) has been inactive since 2014
+* [NPlot on GitHub](https://github.com/mhowlett/nplot) has a single commit from 2016
+* [NPlot on NuGet](https://www.nuget.org/packages/NPlot/) has a single package uploaded in 2016
+
+## Quickstart
+
+* Get the `NPlot` NuGet package
+
+* Drag a `PlotSurface2D` from the toolbox onto your form
+
+### Sample Data
+
+This code generates random data we can practice plotting
+
+```cs
+private Random rand = new Random(0);
+private double[] RandomWalk(int points = 5, double start = 100, double mult = 50)
+{
+ // return an array of difting random numbers
+ double[] values = new double[points];
+ values[0] = start;
+ for (int i = 1; i < points; i++)
+ values[i] = values[i - 1] + (rand.NextDouble() - .5) * mult;
+ return values;
+}
+```
+
+### Interactive Line Plot
+
+I found I could display lines of 10,000 points, but by 100,000 points the program became very sluggish.
+
+```cs
+// generate some random Y data
+int pointCount = 10_000;
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+double[] ys3 = RandomWalk(pointCount);
+
+// create a line plot containing the data
+var linePlot1 = new NPlot.LinePlot { DataSource = ys1, Color = Color.Red };
+var linePlot2 = new NPlot.LinePlot { DataSource = ys2, Color = Color.Green };
+var linePlot3 = new NPlot.LinePlot { DataSource = ys3, Color = Color.Blue };
+
+// add the line plot to the plot surface (user control)
+plotSurface2D1.Clear();
+plotSurface2D1.Add(linePlot1);
+plotSurface2D1.Add(linePlot2);
+plotSurface2D1.Add(linePlot3);
+plotSurface2D1.Title = $"Line Plot ({pointCount:n0} points each)";
+plotSurface2D1.YAxis1.Label = "Vertical Axis Label";
+plotSurface2D1.XAxis1.Label = "Horizontal Axis Label";
+plotSurface2D1.Refresh();
+
+// allow the plot to be mouse-interactive
+plotSurface2D1.AddInteraction(new NPlot.Windows.PlotSurface2D.Interactions.HorizontalDrag());
+plotSurface2D1.AddInteraction(new NPlot.Windows.PlotSurface2D.Interactions.VerticalDrag());
+plotSurface2D1.AddInteraction(new NPlot.Windows.PlotSurface2D.Interactions.AxisDrag(true));
+```
+
+## Console Application Support
+
+Users who aren't using Windows Forms (or the PlotSurface2D user control) can render directly on `Bitmap` objects using the `Bitmap.PlotSurface2D` class. This allows NPlot to be used in console applications (or in server applications such as ASP.NET)
+
+
+
+```cs
+var linePlot = new NPlot.PointPlot { DataSource = RandomWalk(20) };
+var surface = new NPlot.Bitmap.PlotSurface2D(400, 300);
+surface.BackColor = Color.White;
+surface.Add(linePlot);
+surface.Title = $"Scatter Plot from a Console Application";
+surface.YAxis1.Label = "Vertical Axis Label";
+surface.XAxis1.Label = "Horizontal Axis Label";
+surface.Refresh();
+surface.Bitmap.Save("nplot-console-quickstart.png");
+```
+
+## WPF Support
+
+NPlot is designed for Windows Forms, but the author noted [it can be used in WPF](http://netcontrols.org/nplot/wiki/index.php?n=Main.WPFNotes) applications too. This seems to be accomplished by rendering plots to Bitmap objects and displaying them in WPF. [WChart](https://github.com/mhowlett/WChart) is a modified version of NPlot designed for WPF, but it hasn't been updated since 2015.
+
+
+## Resources
+* [Creating Graphs and Plot Charts Quickly with NPlot](https://www.oreilly.com/library/view/windows-developer-power/0596527543/ch04s08.html) (O'Reilly)
+
+## Source Code
+
+* [NPlot Quickstart](https://github.com/swharden/Csharp-Data-Visualization/tree/main/dev/old/plotting/nplot/NPlotQuickstart)
diff --git a/website/content/plotting-free/nplot/nplot-console-quickstart.png b/website/content/plotting-free/nplot/nplot-console-quickstart.png
new file mode 100644
index 0000000..b3e53b4
Binary files /dev/null and b/website/content/plotting-free/nplot/nplot-console-quickstart.png differ
diff --git a/website/content/plotting-free/nplot/nplot-quickstart.gif b/website/content/plotting-free/nplot/nplot-quickstart.gif
new file mode 100644
index 0000000..bbae325
Binary files /dev/null and b/website/content/plotting-free/nplot/nplot-quickstart.gif differ
diff --git a/website/content/plotting-free/oxyplot/index.md b/website/content/plotting-free/oxyplot/index.md
new file mode 100644
index 0000000..7de1914
--- /dev/null
+++ b/website/content/plotting-free/oxyplot/index.md
@@ -0,0 +1,208 @@
+---
+title: Plot Data with OxyPlot
+description: How to plot data in C# projects using OxyPlot
+date: 2020-05-17
+lastmod: 2022-03-16
+weight: 20
+---
+
+**OxyPlot is a 2D plotting library for .NET that has been actively developed since 2010.** OxyPlot is MIT-licensed and has components for an impressive number of modern platforms (WinForms, WPF, UWP, Xamarin, XWT) and some legacy ones too (Silveright and Windows Phone). The WinForms control ([PlotView.cs](https://github.com/oxyplot/oxyplot/blob/develop/Source/OxyPlot.WindowsForms/PlotView.cs)) renders using System.Drawing, but a rendering systems using SkiaSharp and ImageSharp also exist. OxyPlot was created to plot 2D data which is why it has "xy" in its name.
+
+
+
+### Interactive Controls
+
+* left click to show X/Y value of the point under the cursor
+* right-click-drag to pan
+* mouse-wheel-scroll to zoom
+* mouse-wheel-scroll over an axis to zoom in one axis
+
+## Quickstart
+
+* Create a Windows Forms application
+
+* Add the `OxyPlot.WindowsForms` NuGet package
+
+* Drag a `PlotView` from the Toolbox onto your form
+
+### Generate Sample Data
+
+This code generates random data we can practice plotting
+
+```cs
+private Random rand = new Random(0);
+private double[] RandomWalk(int points = 5, double start = 100, double mult = 50)
+{
+ // return an array of difting random numbers
+ double[] values = new double[points];
+ values[0] = start;
+ for (int i = 1; i < points; i++)
+ values[i] = values[i - 1] + (rand.NextDouble() - .5) * mult;
+ return values;
+}
+```
+
+### Scatter Plot
+
+**Interacting with OxyPlot is achieved by constructing data objects and passing them around.** Graphing data requires creating data series objects (like lines and bars), populating them with data, then putting them into a plot model, then loading that model into a view (like a user control).
+
+
+
+```cs
+// generate some random XY data
+int pointCount = 1_000;
+double[] xs1 = RandomWalk(pointCount);
+double[] ys1 = RandomWalk(pointCount);
+double[] xs2 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create lines and fill them with data points
+var line1 = new OxyPlot.Series.LineSeries()
+{
+ Title = $"Series 1",
+ Color = OxyPlot.OxyColors.Blue,
+ StrokeThickness = 1,
+ MarkerSize = 2,
+ MarkerType = OxyPlot.MarkerType.Circle
+};
+
+var line2 = new OxyPlot.Series.LineSeries()
+{
+ Title = $"Series 2",
+ Color = OxyPlot.OxyColors.Red,
+ StrokeThickness = 1,
+ MarkerSize = 2,
+ MarkerType = OxyPlot.MarkerType.Circle
+};
+
+for (int i = 0; i < pointCount; i++)
+{
+ line1.Points.Add(new OxyPlot.DataPoint(xs1[i], ys1[i]));
+ line2.Points.Add(new OxyPlot.DataPoint(xs2[i], ys2[i]));
+}
+
+// create the model and add the lines to it
+var model = new OxyPlot.PlotModel
+{
+ Title = $"Scatter Plot ({pointCount:N0} points each)"
+};
+model.Series.Add(line1);
+model.Series.Add(line2);
+
+// load the model into the user control
+plotView1.Model = model;
+```
+
+### Bar Graph
+
+
+
+```cs
+// generate some random Y data
+int pointCount = 5;
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create a series of bars and populate them with data
+var seriesA = new OxyPlot.Series.ColumnSeries()
+{
+ Title = "Series A",
+ StrokeColor = OxyPlot.OxyColors.Black,
+ FillColor = OxyPlot.OxyColors.Red,
+ StrokeThickness = 1
+};
+
+var seriesB = new OxyPlot.Series.ColumnSeries()
+{
+ Title = "Series B",
+ StrokeColor = OxyPlot.OxyColors.Black,
+ FillColor = OxyPlot.OxyColors.Blue,
+ StrokeThickness = 1
+};
+
+for (int i = 0; i < pointCount; i++)
+{
+ seriesA.Items.Add(new OxyPlot.Series.ColumnItem(ys1[i], i));
+ seriesB.Items.Add(new OxyPlot.Series.ColumnItem(ys2[i], i));
+}
+
+// create a model and add the bars into it
+var model = new OxyPlot.PlotModel
+{
+ Title = "Bar Graph (Column Series)"
+};
+model.Axes.Add(new OxyPlot.Axes.CategoryAxis());
+model.Series.Add(seriesA);
+model.Series.Add(seriesB);
+
+// load the model into the user control
+plotView1.Model = model;
+```
+
+## Performance
+
+I found the line plot to be relatively fast (1 million points display at a rate of few FPS).
+
+
+
+```cs
+// generate some random Y data
+int pointCount = 1000_000;
+double[] xs = Consecutive(pointCount);
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// create lines and fill them with data points
+var line1 = new OxyPlot.Series.LineSeries()
+{
+ Title = $"Series 1",
+ Color = OxyPlot.OxyColors.Blue,
+ StrokeThickness = 1,
+};
+
+var line2 = new OxyPlot.Series.LineSeries()
+{
+ Title = $"Series 2",
+ Color = OxyPlot.OxyColors.Red,
+ StrokeThickness = 1,
+};
+
+for (int i = 0; i < pointCount; i++)
+{
+ line1.Points.Add(new OxyPlot.DataPoint(xs[i], ys1[i]));
+ line2.Points.Add(new OxyPlot.DataPoint(xs[i], ys2[i]));
+}
+
+// create the model and add the lines to it
+var model = new OxyPlot.PlotModel
+{
+ Title = $"Line Plot ({pointCount:N0} points each)"
+};
+model.Series.Add(line1);
+model.Series.Add(line2);
+
+// load the model into the user control
+plotView1.Model = model;
+```
+
+## Create Plots in Console Applications
+
+To use OxyPlot in a console application create your model the same as before, but use a file exporter to save your model to a file rather than display it in a user control.
+
+```cs
+OxyPlot.WindowsForms.PngExporter.Export(model, "test.png", 400, 300, OxyPlot.OxyColors.White);
+```
+
+A platform-specific `PngExporter` is provided with whatever NuGet package you installed. This code example uses the `PngExporter` in the `OxyPlot.WindowsForms` package, but installing `OxyPlot.Core.Drawing` may be the way to go for true console applications.
+
+
+
+## Resources
+* Documentation: [Introduction to OxyPlot](https://oxyplot.readthedocs.io/en/latest/introduction/introduction.html)
+* GitHub: [OxyPlot](https://github.com/oxyplot/oxyplot)
+* Graph types: [/models/index.html](https://oxyplot.readthedocs.io/en/latest/models/index.html)
+* Documentation: GitHub [oxyplot/documentation-examples](https://github.com/oxyplot/documentation-examples)
+* Note: the domain oxyplot.org is no longer associated with this project
+
+## Source Code
+* [oxyplot quickstart](https://github.com/swharden/Csharp-Data-Visualization/tree/main/dev/old/plotting/oxyplot/OxyPlotQuickstart)
\ No newline at end of file
diff --git a/website/content/plotting-free/oxyplot/oxyplot-console-quickstart.png b/website/content/plotting-free/oxyplot/oxyplot-console-quickstart.png
new file mode 100644
index 0000000..7341e8c
Binary files /dev/null and b/website/content/plotting-free/oxyplot/oxyplot-console-quickstart.png differ
diff --git a/website/content/plotting-free/oxyplot/oxyplot-quickstart-bar-graph.png b/website/content/plotting-free/oxyplot/oxyplot-quickstart-bar-graph.png
new file mode 100644
index 0000000..02cd5fc
Binary files /dev/null and b/website/content/plotting-free/oxyplot/oxyplot-quickstart-bar-graph.png differ
diff --git a/website/content/plotting-free/oxyplot/oxyplot-quickstart-line-plot.png b/website/content/plotting-free/oxyplot/oxyplot-quickstart-line-plot.png
new file mode 100644
index 0000000..1e1c6d9
Binary files /dev/null and b/website/content/plotting-free/oxyplot/oxyplot-quickstart-line-plot.png differ
diff --git a/website/content/plotting-free/oxyplot/oxyplot-quickstart-scatter-plot.png b/website/content/plotting-free/oxyplot/oxyplot-quickstart-scatter-plot.png
new file mode 100644
index 0000000..562594c
Binary files /dev/null and b/website/content/plotting-free/oxyplot/oxyplot-quickstart-scatter-plot.png differ
diff --git a/website/content/plotting-free/oxyplot/oxyplot-quickstart.gif b/website/content/plotting-free/oxyplot/oxyplot-quickstart.gif
new file mode 100644
index 0000000..780d8b6
Binary files /dev/null and b/website/content/plotting-free/oxyplot/oxyplot-quickstart.gif differ
diff --git a/website/content/plotting-free/scottplot/index.md b/website/content/plotting-free/scottplot/index.md
new file mode 100644
index 0000000..abdfedb
--- /dev/null
+++ b/website/content/plotting-free/scottplot/index.md
@@ -0,0 +1,81 @@
+---
+title: Plot Data with ScottPlot
+description: How to use ScottPlot to interactively display data in C# applications
+date: 2020-05-17
+lastmod: 2022-03-16
+weight: 10
+---
+
+**[ScottPlot](https://scottplot.net) is a free and open-source interactive plotting library for .NET.** It has user controls for Windows Forms, WPF, Avalonia, and Eto Forms, and it can even generate plots as image files in server environments or console applications. ScottPlot targets .NET Standard 2.0 so it can be used in both .NET Framework and .NET Core applications. ScottPlot's API mimics [Matplotlib](https://matplotlib.org/) for Python, and most plots can be created with a single line of code (using optional arguments to customize styling).
+
+
+
+### Interactive Controls
+* left-click-drag: pan
+* right-click-drag: zoom
+* middle-click-drag: zoom region
+* scroll-wheel: zoom
+* middle-click: fit data
+* right-click: deploy menu
+
+### ScottPlot Cookbook
+
+The [ScottPlot Cookbook](https://scottplot.net/cookbook) is an extensive collection of sample plots paired with the source code used to create them. Reviewing the cookbook is the best way to survey ScottPlot's capabilities and learn how to use it. An interactive version of each cookbook figure is presented in the [ScottPlot demo](https://scottplot.net/demo) application.
+
+[](https://scottplot.net/cookbook)
+
+## Quickstart (Console)
+
+* Install the `ScottPlot.WinForms` NuGet package
+
+* Add the following to your start-up sequence
+
+```cs
+double[] dataX = new double[] { 1, 2, 3, 4, 5 };
+double[] dataY = new double[] { 1, 4, 9, 16, 25 };
+var plt = new ScottPlot.Plot(400, 300);
+plt.AddScatter(dataX, dataY);
+plt.SaveFig("quickstart.png");
+```
+
+
+
+## QuickStart (Windows Forms)
+
+* Install the `ScottPlot.WinForms` NuGet package
+
+* Drag a `FormsPlot` from the toolbox onto your form
+
+* Add the following to your start-up sequence
+
+```cs
+// generate some random X/Y data
+int pointCount = 500;
+Random rand = new Random(0);
+double[] xs1 = ScottPlot.DataGen.RandomWalk(rand, pointCount);
+double[] ys1 = ScottPlot.DataGen.RandomWalk(rand, pointCount);
+double[] xs2 = ScottPlot.DataGen.RandomWalk(rand, pointCount);
+double[] ys2 = ScottPlot.DataGen.RandomWalk(rand, pointCount);
+
+// plot the data
+formsPlot1.Plot.PlotScatter(xs1, ys1);
+formsPlot1.Plot.PlotScatter(xs2, ys2);
+
+// additional styling
+formsPlot1.Plot.Title($"Scatter Plot ({pointCount} points per group)");
+formsPlot1.Plot.XLabel("Horizontal Axis Label");
+formsPlot1.Plot.YLabel("Vertical Axis Label");
+formsPlot1.Refresh();
+```
+
+
+
+## Resources
+* ScottPlot website: https://scottplot.net
+* ScottPlot Cookbook: https://scottplot.net/cookbook
+* ScottPlot Demo: https://scottplot.net/demo
+* ScottPlot Quickstart: https://scottplot.net/quickstart
+* ScottPlot on GitHub: https://github.com/scottplot/scottplot
+
+## Source Code
+* [ScottPlot Quickstart](https://scottplot.net/quickstart)
\ No newline at end of file
diff --git a/website/content/plotting-free/scottplot/scottplot-cookbook.png b/website/content/plotting-free/scottplot/scottplot-cookbook.png
new file mode 100644
index 0000000..1c5403e
Binary files /dev/null and b/website/content/plotting-free/scottplot/scottplot-cookbook.png differ
diff --git a/website/content/plotting-free/scottplot/scottplot-quickstart-bar-graph.png b/website/content/plotting-free/scottplot/scottplot-quickstart-bar-graph.png
new file mode 100644
index 0000000..6080c71
Binary files /dev/null and b/website/content/plotting-free/scottplot/scottplot-quickstart-bar-graph.png differ
diff --git a/website/content/plotting-free/scottplot/scottplot-quickstart-console.png b/website/content/plotting-free/scottplot/scottplot-quickstart-console.png
new file mode 100644
index 0000000..47437e2
Binary files /dev/null and b/website/content/plotting-free/scottplot/scottplot-quickstart-console.png differ
diff --git a/website/content/plotting-free/scottplot/scottplot-quickstart-line-plot.png b/website/content/plotting-free/scottplot/scottplot-quickstart-line-plot.png
new file mode 100644
index 0000000..6509c6b
Binary files /dev/null and b/website/content/plotting-free/scottplot/scottplot-quickstart-line-plot.png differ
diff --git a/website/content/plotting-free/scottplot/scottplot-quickstart-scatter-plot.png b/website/content/plotting-free/scottplot/scottplot-quickstart-scatter-plot.png
new file mode 100644
index 0000000..cf9644c
Binary files /dev/null and b/website/content/plotting-free/scottplot/scottplot-quickstart-scatter-plot.png differ
diff --git a/website/content/plotting-free/scottplot/scottplot-quickstart-simple-bar-graph.png b/website/content/plotting-free/scottplot/scottplot-quickstart-simple-bar-graph.png
new file mode 100644
index 0000000..ea69308
Binary files /dev/null and b/website/content/plotting-free/scottplot/scottplot-quickstart-simple-bar-graph.png differ
diff --git a/website/content/plotting-free/scottplot/scottplot-quickstart.gif b/website/content/plotting-free/scottplot/scottplot-quickstart.gif
new file mode 100644
index 0000000..bc4b0ae
Binary files /dev/null and b/website/content/plotting-free/scottplot/scottplot-quickstart.gif differ
diff --git a/website/content/plotting-free/zedgraph/index.md b/website/content/plotting-free/zedgraph/index.md
new file mode 100644
index 0000000..3e9034c
--- /dev/null
+++ b/website/content/plotting-free/zedgraph/index.md
@@ -0,0 +1,216 @@
+---
+title: Plot Data with ZedGraph
+description: How to plot data in C# projects using ZedGraph
+date: 2020-05-16
+lastmod: 2022-03-16
+weight: 50
+---
+
+**ZedGraph is a C# plotting library for drawing 2D Line, Bar, and Pie Charts.** ZedGraph was created in the early 2000s but development appears to have ceased in 2008 according to the [ZedGraph SourceForge page](https://sourceforge.net/projects/zedgraph/files/). ZedGraph is available as a [NuGet package](https://www.nuget.org/packages/ZedGraph) and it has a [GitHub page](https://github.com/ZedGraph/ZedGraph), but neither have been active for many years.
+
+
+
+> **โ ๏ธ WARNING:** ZedGraph has a LGPL license! ๐ Depending on how you use it in your project, you may be forced to open-source part of your application. Read more about the [LGPL 2.1 license](https://opensource.org/license/lgpl-2-1/) if you are considering using this library in a commercial application.
+
+## Supported Platforms
+
+* The [`ZedGraph 5.1.7` NuGet package](https://www.nuget.org/packages/ZedGraph/5.1.7) (2015) targets **.NET Framework 3.5** so it can only be used in .NET Framework projects.
+
+* The [`ZedGraph 6.0.0-alpha0001` NuGet package](https://www.nuget.org/packages/ZedGraph/6.0.0-alpha0001) (2018) targets **.NET Standard 2.0** so it can be used in .NET Framework and .NET Core projects.
+
+## Quickstart
+
+* Install the `ZedGraph` NuGet package
+
+* Drag a `ZedGraphControl` from the toolbox onto your form
+
+### Sample Data
+
+This code generates random data we can practice plotting
+
+```cs
+private Random rand = new Random(0);
+private double[] RandomWalk(int points = 5, double start = 100, double mult = 50)
+{
+ // return an array of difting random numbers
+ double[] values = new double[points];
+ values[0] = start;
+ for (int i = 1; i < points; i++)
+ values[i] = values[i - 1] + (rand.NextDouble() - .5) * mult;
+ return values;
+}
+```
+
+### Bar Graph
+
+
+
+```cs
+// generate some random Y data
+int pointCount = 5;
+double[] xs = Consecutive(pointCount);
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// clear old curves
+zedGraphControl1.GraphPane.CurveList.Clear();
+
+// plot the data as bars
+zedGraphControl1.GraphPane.AddBar("Group A", xs, ys1, Color.Blue);
+zedGraphControl1.GraphPane.AddBar("Group B", xs, ys2, Color.Red);
+
+// style the plot
+zedGraphControl1.GraphPane.Title.Text = $"Bar Plot ({pointCount:n0} points)";
+zedGraphControl1.GraphPane.XAxis.Title.Text = "Horizontal Axis Label";
+zedGraphControl1.GraphPane.YAxis.Title.Text = "Vertical Axis Label";
+
+// auto-axis and update the display
+zedGraphControl1.GraphPane.XAxis.ResetAutoScale(zedGraphControl1.GraphPane, CreateGraphics());
+zedGraphControl1.GraphPane.YAxis.ResetAutoScale(zedGraphControl1.GraphPane, CreateGraphics());
+zedGraphControl1.Refresh();
+```
+
+### Scatter Plot
+
+
+
+```cs
+// generate some random Y data
+int pointCount = 100;
+double[] xs1 = RandomWalk(pointCount);
+double[] ys1 = RandomWalk(pointCount);
+double[] xs2 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// clear old curves
+zedGraphControl1.GraphPane.CurveList.Clear();
+
+// plot the data as curves
+var curve1 = zedGraphControl1.GraphPane.AddCurve("Series A", xs1, ys1, Color.Blue);
+curve1.Line.IsAntiAlias = true;
+
+var curve2 = zedGraphControl1.GraphPane.AddCurve("Series B", xs2, ys2, Color.Red);
+curve2.Line.IsAntiAlias = true;
+
+// style the plot
+zedGraphControl1.GraphPane.Title.Text = $"Scatter Plot ({pointCount:n0} points)";
+zedGraphControl1.GraphPane.XAxis.Title.Text = "Horizontal Axis Label";
+zedGraphControl1.GraphPane.YAxis.Title.Text = "Vertical Axis Label";
+
+// auto-axis and update the display
+zedGraphControl1.GraphPane.XAxis.ResetAutoScale(zedGraphControl1.GraphPane, CreateGraphics());
+zedGraphControl1.GraphPane.YAxis.ResetAutoScale(zedGraphControl1.GraphPane, CreateGraphics());
+zedGraphControl1.Refresh();
+```
+
+### Line Graph
+
+
+
+```cs
+// generate some random Y data
+int pointCount = 100_000;
+double[] xs = Consecutive(pointCount);
+double[] ys1 = RandomWalk(pointCount);
+double[] ys2 = RandomWalk(pointCount);
+
+// clear old curves
+zedGraphControl1.GraphPane.CurveList.Clear();
+
+// plot the data as curves
+var curve1 = zedGraphControl1.GraphPane.AddCurve("Series A", xs, ys1, Color.Blue);
+curve1.Line.IsAntiAlias = true;
+curve1.Symbol.IsVisible = false;
+
+var curve2 = zedGraphControl1.GraphPane.AddCurve("Series B", xs, ys2, Color.Red);
+curve2.Line.IsAntiAlias = true;
+curve2.Symbol.IsVisible = false;
+
+// style the plot
+zedGraphControl1.GraphPane.Title.Text = $"Scatter Plot ({pointCount:n0} points)";
+zedGraphControl1.GraphPane.XAxis.Title.Text = "Horizontal Axis Label";
+zedGraphControl1.GraphPane.YAxis.Title.Text = "Vertical Axis Label";
+
+// auto-axis and update the display
+zedGraphControl1.GraphPane.XAxis.ResetAutoScale(zedGraphControl1.GraphPane, CreateGraphics());
+zedGraphControl1.GraphPane.YAxis.ResetAutoScale(zedGraphControl1.GraphPane, CreateGraphics());
+zedGraphControl1.Refresh();
+```
+
+## Console Applications
+
+
+
+```cs
+var pane = new ZedGraph.GraphPane();
+var curve1 = pane.AddCurve(
+ label: "demo",
+ x: new double[] { 1, 2, 3, 4, 5 },
+ y: new double[] { 1, 4, 9, 16, 25 },
+ color:Color.Blue);
+curve1.Line.IsAntiAlias = true;
+pane.AxisChange();
+Bitmap bmp = pane.GetImage(400, 300, dpi: 100, isAntiAlias: true);
+bmp.Save("zedgraph-console-quickstart.png", ImageFormat.Png);
+```
+
+## Multiple Y Axes
+
+
+
+```cs
+// generate some random Y data
+int pointCount = 10;
+double[] xs = Random(pointCount);
+double[] ys1 = Random(pointCount);
+double[] ys2 = Random(pointCount, 0, 1000);
+
+// clear old curves
+zedGraphControl1.GraphPane.CurveList.Clear();
+
+// clear old Y axes and manually add new ones
+zedGraphControl1.GraphPane.YAxisList.Clear();
+
+// add a traditional Y axis
+zedGraphControl1.GraphPane.AddYAxis("First Axis");
+var firstAxis = zedGraphControl1.GraphPane.YAxisList[0];
+firstAxis.Color = Color.Blue;
+
+// create another Y axis and customize it
+zedGraphControl1.GraphPane.AddYAxis("Second Axis");
+var secondAxis = zedGraphControl1.GraphPane.YAxisList[1];
+secondAxis.Scale.Max = 1000;
+secondAxis.Scale.Min = -1000;
+secondAxis.Scale.FontSpec.FontColor = Color.Green;
+secondAxis.Title.FontSpec.FontColor = Color.Green;
+secondAxis.Color = Color.Green;
+
+// plot the data as curves
+var curve1 = zedGraphControl1.GraphPane.AddCurve("Small", xs, ys1, Color.Blue);
+var curve2 = zedGraphControl1.GraphPane.AddCurve("Big", xs, ys2, Color.Green);
+
+// specify which curve is to use which axis
+curve1.YAxisIndex = 0;
+curve2.YAxisIndex = 1;
+
+// style the plot
+zedGraphControl1.GraphPane.Title.Text = $"Multiple Y Axes";
+zedGraphControl1.GraphPane.XAxis.Title.Text = "Horizontal Axis Label";
+
+// auto-axis and update the display
+zedGraphControl1.GraphPane.XAxis.ResetAutoScale(zedGraphControl1.GraphPane, CreateGraphics());
+zedGraphControl1.GraphPane.YAxis.ResetAutoScale(zedGraphControl1.GraphPane, CreateGraphics());
+zedGraphControl1.Refresh();
+```
+
+## Resources
+* [ZedGraph home page](http://zedgraph.sourceforge.net/samples.html) (Sourceforge)
+* [ZedGraph NuGet package](https://www.nuget.org/packages/ZedGraph/) (uploaded by [discomurray](https://github.com/discomurray), inactive on GitHub since 2018)
+* [ZedGraph on GitHub](https://github.com/ZedGraph/ZedGraph) (inactive since 2018)
+* [revision history summary](http://zedgraph.sourceforge.net/revision.html)
+* [full revision history](http://zedgraph.sourceforge.net/revision_history.txt)
+* [ZedGraph Wiki](https://github.com/ZedGraph/ZedGraph/wiki) on GitHub
+
+## Source Code
+
+* [ZedGraph Quickstart](https://github.com/swharden/Csharp-Data-Visualization/tree/main/dev/old/plotting/zedgraph/ZedGraphQuickstart)
diff --git a/website/content/plotting-free/zedgraph/zedgraph-console-quickstart.png b/website/content/plotting-free/zedgraph/zedgraph-console-quickstart.png
new file mode 100644
index 0000000..6be01ec
Binary files /dev/null and b/website/content/plotting-free/zedgraph/zedgraph-console-quickstart.png differ
diff --git a/website/content/plotting-free/zedgraph/zedgraph-multiple-y-axes.png b/website/content/plotting-free/zedgraph/zedgraph-multiple-y-axes.png
new file mode 100644
index 0000000..7154bbb
Binary files /dev/null and b/website/content/plotting-free/zedgraph/zedgraph-multiple-y-axes.png differ
diff --git a/website/content/plotting-free/zedgraph/zedgraph-quickstart-bar.png b/website/content/plotting-free/zedgraph/zedgraph-quickstart-bar.png
new file mode 100644
index 0000000..4af76d1
Binary files /dev/null and b/website/content/plotting-free/zedgraph/zedgraph-quickstart-bar.png differ
diff --git a/website/content/plotting-free/zedgraph/zedgraph-quickstart-line.png b/website/content/plotting-free/zedgraph/zedgraph-quickstart-line.png
new file mode 100644
index 0000000..1db8d70
Binary files /dev/null and b/website/content/plotting-free/zedgraph/zedgraph-quickstart-line.png differ
diff --git a/website/content/plotting-free/zedgraph/zedgraph-quickstart-scatter.png b/website/content/plotting-free/zedgraph/zedgraph-quickstart-scatter.png
new file mode 100644
index 0000000..0c7f96c
Binary files /dev/null and b/website/content/plotting-free/zedgraph/zedgraph-quickstart-scatter.png differ
diff --git a/website/content/plotting-free/zedgraph/zedgraph-quickstart.gif b/website/content/plotting-free/zedgraph/zedgraph-quickstart.gif
new file mode 100644
index 0000000..c108a82
Binary files /dev/null and b/website/content/plotting-free/zedgraph/zedgraph-quickstart.gif differ
diff --git a/website/content/simulations/_index.md b/website/content/simulations/_index.md
new file mode 100644
index 0000000..97a71a5
--- /dev/null
+++ b/website/content/simulations/_index.md
@@ -0,0 +1,3 @@
+---
+title: Simulations
+---
\ No newline at end of file
diff --git a/website/content/simulations/boids/Boids-Csharp-OpenGL.png b/website/content/simulations/boids/Boids-Csharp-OpenGL.png
new file mode 100644
index 0000000..2987d01
Binary files /dev/null and b/website/content/simulations/boids/Boids-Csharp-OpenGL.png differ
diff --git a/website/content/simulations/boids/boids.zip b/website/content/simulations/boids/boids.zip
new file mode 100644
index 0000000..65f60d9
Binary files /dev/null and b/website/content/simulations/boids/boids.zip differ
diff --git a/website/content/simulations/boids/csharp-boids.gif b/website/content/simulations/boids/csharp-boids.gif
new file mode 100644
index 0000000..99daacb
Binary files /dev/null and b/website/content/simulations/boids/csharp-boids.gif differ
diff --git a/website/content/simulations/boids/index.md b/website/content/simulations/boids/index.md
new file mode 100644
index 0000000..5c91bad
--- /dev/null
+++ b/website/content/simulations/boids/index.md
@@ -0,0 +1,340 @@
+---
+title: Boids in C#
+description: A procedural animation demonstrating emergent flocking behavior implemented in C#
+date: 2020-05-11
+weight: 2
+---
+
+**This project implements the [Boids flocking algorithm](https://en.wikipedia.org/wiki/Boids) in C# to create an interesting procedural animation of bird-drones (boids) scurrying about the screen.** This simulation produces complex emergent flocking behavior from a system where individual boids follow a simple set of rules. Code shown on this page uses `System.Drawing` in a Windows Forms application, but source is available for an OpenGL-accelerated version using SkiaSharp.
+
+
+
\ No newline at end of file
diff --git a/website/content/simulations/life/game-of-life-csharp-glider-gun.gif b/website/content/simulations/life/game-of-life-csharp-glider-gun.gif
new file mode 100644
index 0000000..4428749
Binary files /dev/null and b/website/content/simulations/life/game-of-life-csharp-glider-gun.gif differ
diff --git a/website/content/simulations/life/game-of-life-csharp.gif b/website/content/simulations/life/game-of-life-csharp.gif
new file mode 100644
index 0000000..17b5731
Binary files /dev/null and b/website/content/simulations/life/game-of-life-csharp.gif differ
diff --git a/website/content/simulations/life/glider-gun.png b/website/content/simulations/life/glider-gun.png
new file mode 100644
index 0000000..703da84
Binary files /dev/null and b/website/content/simulations/life/glider-gun.png differ
diff --git a/website/content/simulations/life/index.md b/website/content/simulations/life/index.md
new file mode 100644
index 0000000..0d109c3
--- /dev/null
+++ b/website/content/simulations/life/index.md
@@ -0,0 +1,245 @@
+---
+title: Life in C#
+description: Conway's Game of Life implemented using C#
+date: 2020-05-10
+weight: 1
+---
+
+**Conway's _Game of Life_ is a zero-player video game that uses a few basic rules to determine if cells live or die based on the density of their neighbors.** In this project we create _Life_ using C# and System.Drawing and display the animated graphics model using Windows Forms.
+
+
+
+## Code
+
+We divide this project into two libraries, one for the graphics model (containing classes for `Cell` and `Booard`) and another that handles the graphical environment and rendering.
+
+### Cell Model
+
+I decided not to make cells position-aware. Instead each cell simply tracks its own `IsAlive` state, and knows about its 8 neighbors.
+
+When `DetermineNextLiveState()` is called, the neighbors are inspected to determine how many are alive, then _Life_ rules are used to determine if this cell will be alive or not after advancement.
+
+```cs
+public class Cell
+{
+ public bool IsAlive;
+ public readonly List
+
+### Defining Initial State
+
+I found it useful to define the starting condition as a multi-line string, then write a method to translate the string into a _Life_ starting condition.
+
+```cs
+private void StartWithPattern(string pattern)
+{
+ board.KillAll();
+
+ string[] lines = pattern.Split('\n');
+ int yOffset = (board.Rows - lines.Length) / 2;
+ int xOffset = (board.Columns - lines[0].Length) / 2;
+
+ for (int y = 0; y < lines.Length; y++)
+ for (int x = 0; x < lines[y].Length; x++)
+ board.Cells[x + xOffset, y + yOffset].IsAlive = lines[y].Substring(x, 1) == "X";
+}
+```
+
+Now I can design patterns visually in code.
+
+```cs
+private void GunButton_Click(object sender, EventArgs e)
+{
+ string gliderGun =
+ "-------------------------X----------\n" +
+ "----------------------XXXX----X-----\n" +
+ "-------------X-------XXXX-----X-----\n" +
+ "------------X-X------X--X---------XX\n" +
+ "-----------X---XX----XXXX---------XX\n" +
+ "XX---------X---XX-----XXXX----------\n" +
+ "XX---------X---XX--------X----------\n" +
+ "------------X-X---------------------\n" +
+ "-------------X----------------------";
+ StartWithPattern(gliderGun);
+}
+```
+
+
+
+This "glider gun" starting pattern could run infinitely, but since our universe wraps around the edges the gliders eventually wrap around and destroy the gun.
+
+## Source Code
+
+* GitHub: [game-of-life C# project](https://github.com/swharden/Csharp-Data-Visualization/tree/main/dev/old/drawing/game-of-life)
+
+## Resources
+
+* [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) (Wikipedia)
+* [Game of Life News](http://pentadecathlon.com/lifeNews/2006/06/single_glider_collision_survey.html)
+* [ConwayLife](https://www.conwaylife.com/wiki/Spaceship) (Wiki)
diff --git a/website/content/simulations/mystify/index.md b/website/content/simulations/mystify/index.md
new file mode 100644
index 0000000..4abad8c
--- /dev/null
+++ b/website/content/simulations/mystify/index.md
@@ -0,0 +1,142 @@
+---
+Title: Mystify Your Mind with C#
+Description: A C# implementation of the classic screensaver
+Date: 2021-01-09
+weight: 999
+---
+
+This project recreates the classic screensaver _Mystify_ using C# and `System.Drawing` in a Windows Forms application.
+
+> ๐ก An updated version of this article is at https://swharden.com/blog/2022-04-06-mystify/
+
+
+
+### Field.cs
+```cs
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MystifySharp.Mystify
+{
+ public class Field
+ {
+ Random rand = new Random();
+ public readonly List
+
+## What about Unsafe Code?
+
+While the example above does not use the `unsafe` keyword, many of the examples in the [official documentation](https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/pixel-bits) accomplish similar tasks using `unsafe` code blocks. This certainly works, and it's up to you whether you want to permit `unsafe` code in your project.
+
+```cs
+SKBitmap bitmap = new SKBitmap(256, 256);
+IntPtr pixelsAddr = bitmap.GetPixels();
+
+unsafe
+{
+ for (int rep = 0; rep < REPS; rep++)
+ {
+ byte* ptr = (byte*)pixelsAddr.ToPointer();
+ for (int row = 0; row < 256; row++)
+ for (int col = 0; col < 256; col++)
+ {
+ *ptr++ = (byte)(col); // red
+ *ptr++ = 0; // green
+ *ptr++ = (byte)(row); // blue
+ *ptr++ = 0xFF; // alpha
+ }
+ }
+}
+```
+
+## What about `SKBitmap.Pixels[]`?
+
+It is true that `SKBitmap` has a `SKColor Pixels[]`, but interacting with this property is _extremely_ slow. This example is included for educational purposes, but in practice it is not useful beyond getting or setting an extremely small number of pixels.
+
+```cs
+[Obsolete("WARNING: This is extremely slow")]
+private static SKBitmap GetBitmapSLOW(byte[,,] pixelArray)
+{
+ int width = pixelArray.GetLength(1);
+ int height = pixelArray.GetLength(0);
+ SKBitmap bitmap = new(width, height);
+
+ for (int y = 0; y < height; y++)
+ {
+ Console.WriteLine($"Row {y}");
+ for (int x = 0; x < width; x++)
+ {
+ byte r = pixelArray[y, x, 0];
+ byte g = pixelArray[y, x, 1];
+ byte b = pixelArray[y, x, 2];
+ bitmap.Pixels[y * width + x] = new SKColor(r, g, b);
+ }
+ }
+
+ return bitmap;
+}
+```
+
+## Resources
+* [C# Data Visualization on GitHib](https://github.com/swharden/Csharp-Data-Visualization)
+* Multi-platform project source: [array-to-image](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/array-to-image)
+* Official documentation: [Accessing SkiaSharp bitmap pixel bits
+](https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/pixel-bits)
\ No newline at end of file
diff --git a/website/content/skiasharp/array-to-image/output.png b/website/content/skiasharp/array-to-image/output.png
new file mode 100644
index 0000000..9c29166
Binary files /dev/null and b/website/content/skiasharp/array-to-image/output.png differ
diff --git a/website/content/skiasharp/drawtext-rectangle/index.md b/website/content/skiasharp/drawtext-rectangle/index.md
new file mode 100644
index 0000000..3ba5c93
--- /dev/null
+++ b/website/content/skiasharp/drawtext-rectangle/index.md
@@ -0,0 +1,62 @@
+---
+title: Draw Text in a Rectangle with SkiaSharp
+description: How to draw a string within the boundaries if a rectangle using SkiaSharp
+date: 2022-09-07
+---
+
+> ๐ก **TIP:** If you just want to measure a string to draw a border around it, check out the [Drawing Text with SkiaSharp](../text) page
+
+SkiaSharp can render text in a straight using the [SKCanvas.DrawText Method](https://docs.microsoft.com/en-us/dotnet/api/skiasharp.skcanvas.drawtext), but developers will find that there is no built-in ability to wrap text to fit a rectangle. This page demonstrates how to draw a text inside a rectangle using SkiaSharp.
+
+
+
+## Code
+
+This code breaks the string into words and renders each word left-to-right until it runs out of room, then it drops down a line and starts over.
+
+```cs
+void DrawText(SKCanvas canvas, string text, SKRect rect, SKPaint paint)
+{
+ float spaceWidth = paint.MeasureText(" ");
+ float wordX = rect.Left;
+ float wordY = rect.Top + paint.TextSize;
+ foreach (string word in text.Split(' '))
+ {
+ float wordWidth = paint.MeasureText(word);
+ if (wordWidth <= rect.Right - wordX)
+ {
+ canvas.DrawText(word, wordX, wordY, paint);
+ wordX += wordWidth + spaceWidth;
+ }
+ else
+ {
+ wordY += paint.FontSpacing;
+ wordX = rect.Left;
+ }
+ }
+}
+```
+
+This minimal code example doesn't break after hyphens or insert an extra break after `\n`, but these features are easy to implement as needed.
+
+## Improvements
+
+**Store word sizes:** As words are measured add them to a `Dictionary
+
+## Resources
+* Source code: [projects/skiasharp-quickstart](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/skiasharp-quickstart)
+* NuGet: [SkiaSharp](https://www.nuget.org/packages/SkiaSharp)
+* Official: [Creating and drawing on SkiaSharp bitmaps](https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/drawing) and [Saving SkiaSharp bitmaps to files
+](https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/saving)
\ No newline at end of file
diff --git a/website/content/skiasharp/quickstart-console/quickstart.jpg b/website/content/skiasharp/quickstart-console/quickstart.jpg
new file mode 100644
index 0000000..99167d1
Binary files /dev/null and b/website/content/skiasharp/quickstart-console/quickstart.jpg differ
diff --git a/website/content/skiasharp/skiasharp-opengl-starfield/index.md b/website/content/skiasharp/skiasharp-opengl-starfield/index.md
new file mode 100644
index 0000000..d4def44
--- /dev/null
+++ b/website/content/skiasharp/skiasharp-opengl-starfield/index.md
@@ -0,0 +1,63 @@
+---
+title: Animating Graphics with SkiaSharp and OpenGL
+description: How to animate 2D graphics in C# using SkiaSharp & OpenGL
+date: 2020-04-01
+weight: 40
+---
+
+**This project uses C# and the SkiaSharp drawing library to create high-performance animations with hardware-acceleration provided by OpenGL.** We will use the same Starfield graphics model we created earlier in this series.
+
+
+
+While high framerates are unsurprising at small screen sizes (System.Drawing didn't do too bad a job there), the strength of SkiaSharp and OpenGL really shines when this application is run at full-screen dimensions, especially on a 4K monitor. Unlike rendering libraries that get slow as image area increases, this system is largely insensitive to screen size.
+
+## Code
+
+This project is really just a mash-up of code from the [Animating Graphics in Windows Forms](../../system.drawing/animate-winforms/) and [GPU-Accelerated Drawing with SkiaSharp & OpenGL](../skiasharp-opengl/) pages which describe the techniques for creating animations and drawing image with SkiaSharp in more detail.
+
+### Render on the `PaintSurface` Event
+
+This program simply renders the starfield whenever the control is painted. We don't use the [Rendering Graphics without Blocking GUI Thread](../../system.drawing/threading/) method in this example, but we could have if we wanted to.
+
+```cs
+private void skglControl1_PaintSurface(object sender, SkiaSharp.Views.Desktop.SKPaintGLSurfaceEventArgs e)
+{
+ e.Surface.Canvas.Clear(SKColors.Black);
+ var starColor = new SKColor(255, 255, 255, starAlpha);
+ var starPaint = new SKPaint() { IsAntialias = true, Color = starColor };
+ foreach (var star in field.GetStars())
+ {
+ float xPixel = (float)star.x * skglControl1.Width;
+ float yPixel = (float)star.y * skglControl1.Height;
+ float radius = (float)star.size - 1;
+ var point = new SKPoint(xPixel, yPixel);
+ e.Surface.Canvas.DrawCircle(point, radius, starPaint);
+ }
+}
+```
+
+### Trigger Renders with a Timer
+
+The simplest way to achieve continuous animation is to trigger re-painting by invalidating the control from a Timer that ticks every 1ms. Even though our paint renders extremely fast (capable of achieving thousands of renders a second), using the timer this way limits our frame-rate to around 60 FPS.
+
+```cs
+private void timer1_Tick(object sender, EventArgs e)
+{
+ field.Advance();
+ skglControl1.Invalidate();
+}
+```
+
+### Enable VSync
+
+In the SKGLControl properties ensure to set VSync to True. This will reduce frame tearing and skipping during animations. The result is very subtle, but enabling this will result in smoother animations.
+
+## Conclusions
+
+* SkiaSharp + OpenGL is extremely fast rendering reasonable numbers of objects
+* Render speed is maintained even at full-screen sizes
+* Large numbers of objects (100k) are still slow
+
+## Source Code
+
+GitHub: [WinFormsSkiaSharp](https://github.com/swharden/Csharp-Data-Visualization/tree/master/dev/old/drawing/starfield/Starfield.WinFormsSkiaSharp)
\ No newline at end of file
diff --git a/website/content/skiasharp/skiasharp-opengl-starfield/starfield.gif b/website/content/skiasharp/skiasharp-opengl-starfield/starfield.gif
new file mode 100644
index 0000000..5ae169b
Binary files /dev/null and b/website/content/skiasharp/skiasharp-opengl-starfield/starfield.gif differ
diff --git a/website/content/skiasharp/skiasharp-opengl-starfield/starfield.png b/website/content/skiasharp/skiasharp-opengl-starfield/starfield.png
new file mode 100644
index 0000000..7d618fc
Binary files /dev/null and b/website/content/skiasharp/skiasharp-opengl-starfield/starfield.png differ
diff --git a/website/content/skiasharp/skiasharp-opengl/drawing-with-skiasharp-opengl.png b/website/content/skiasharp/skiasharp-opengl/drawing-with-skiasharp-opengl.png
new file mode 100644
index 0000000..e8c9082
Binary files /dev/null and b/website/content/skiasharp/skiasharp-opengl/drawing-with-skiasharp-opengl.png differ
diff --git a/website/content/skiasharp/skiasharp-opengl/index.md b/website/content/skiasharp/skiasharp-opengl/index.md
new file mode 100644
index 0000000..725fd7d
--- /dev/null
+++ b/website/content/skiasharp/skiasharp-opengl/index.md
@@ -0,0 +1,80 @@
+---
+title: Drawing with SkiaSharp & OpenGL
+description: How to draw 2D graphics in C# using SkiaSharp & OpenGL
+date: 2020-04-01
+weight: 30
+---
+
+The [drawing with SkiaSharp](../skiasharp) page described how to use Skia to render graphics on a Bitmap and display them in a `PictureBox`. **In this project we will use the `skglControl` user control to create an OpenGL-accelerated surface for us to draw on.** OpenGL will offer an excellent performance enhancement, especially when creating large or full-screen animations.
+
+
+
+
+
+## Font and Styling Options
+
+```cs
+SKBitmap bmp = new(400, 200);
+using SKCanvas canvas = new(bmp);
+canvas.Clear(SKColors.Navy);
+
+using SKPaint paint = new()
+{
+ Color = SKColors.Yellow,
+ IsAntialias = true,
+ TextSize = 64,
+ Typeface = SKTypeface.FromFamilyName(
+ familyName: "Impact",
+ weight: SKFontStyleWeight.SemiBold,
+ width: SKFontStyleWidth.Normal,
+ slant: SKFontStyleSlant.Italic),
+};
+
+canvas.DrawText("Hello, World", 20, 100, paint);
+
+using SKFileWStream fs = new("options.png");
+bmp.Encode(fs, SKEncodedImageFormat.Png, quality: 100);
+```
+
+
+
+## Measure a String with SkiaSharp
+
+The best way to measure a string's width and height is to create a `SKRect`, pass it into `MeasureText` by reference, and then it holds the dimensions of the text. You can then offset it by the location of the text to get a rectangle containing the text.
+
+```cs
+SKBitmap bmp = new(400, 200);
+using SKCanvas canvas = new(bmp);
+canvas.Clear(SKColors.Navy);
+
+using SKPaint paint = new()
+{
+ Color = SKColors.Yellow,
+ IsAntialias = true,
+ TextSize = 64,
+};
+
+string text = "Hello, World";
+SKRect rect = new();
+paint.MeasureText(text, ref rect);
+Console.WriteLine($"Width={rect.Width}, Height={rect.Height}");
+
+SKPoint pt = new(20, 100);
+canvas.DrawText(text, pt, paint);
+
+rect.Offset(pt);
+paint.IsStroke = true;
+paint.Color = SKColors.Magenta;
+canvas.DrawRect(rect, paint);
+
+using SKFileWStream fs = new("measure.png");
+bmp.Encode(fs, SKEncodedImageFormat.Png, quality: 100);
+```
+
+
+
+## Rotating Text with SkiaSharp
+
+You can't draw rotated text, but you can rotate the _canvas_
+
+```cs
+SKBitmap bmp = new(400, 400);
+using SKCanvas canvas = new(bmp);
+canvas.Clear(SKColors.Navy);
+
+canvas.Translate(200, 200);
+canvas.RotateDegrees(-45);
+
+using SKPaint paint = new()
+{
+ Color = SKColors.Yellow,
+ IsAntialias = true,
+ TextSize = 36,
+};
+
+canvas.DrawText("Hello, World", 0, 0, paint);
+
+SKFileWStream fs = new("rotation.png");
+bmp.Encode(fs, SKEncodedImageFormat.Png, quality: 100);
+```
+
+
+
+If you intend to revert transformations and draw on the original canvas again, use [SKCanvas.Save](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skcanvas.save) and [SKCanvas.Restore](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skcanvas.restore)
+
+```cs
+canvas.Save(); // remember the original state
+canvas.Translate(200, 200);
+canvas.RotateDegrees(-45);
+canvas.DrawText("Hello, World", 0, 0, paint);
+canvas.Restore(); // revert to the original state
+```
+
+Advanced users will appreciate [SKAutoCanvasRestore](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skautocanvasrestore) which restores state from its destructor when the object goes out of scope
+
+```cs
+using SKAutoCanvasRestore _ = new(surface.Canvas);
+```
+
+## Fix Pixelated Rotated Text
+
+SkiaSharp renders text poorly when rendered 90ยบ, most noticeable in the "V" characters:
+
+A solution is to rotate the text by 90.1ยบ instead of 90.0ยบ
+
+
+
+## Code
+
+A `Star` has a location and size, and a `Field` contains a collection of `Star` objects. Every time the `Advance()` method is called, stars are moved forward in time.
+
+```cs
+public struct Star
+{
+ public double x;
+ public double y;
+ public double size;
+}
+```
+
+```cs
+public class Starfield
+{
+ Random rand = new Random();
+ Star[] stars;
+
+ public Field(int starCount)
+ {
+ Reset(starCount);
+ }
+
+ public void Reset(int starCount)
+ {
+ stars = new Star[starCount];
+ for (int i = 0; i < starCount; i++)
+ stars[i] = GetRandomStar();
+ }
+
+ private Star GetRandomStar(bool randomSize = true)
+ {
+ double starSize = 1;
+ if (randomSize)
+ starSize += rand.NextDouble() * 5;
+
+ return new Star
+ {
+ x = rand.NextDouble(),
+ y = rand.NextDouble(),
+ size = starSize
+ };
+ }
+
+ public void Advance(double step = .01)
+ {
+ for (int i = 0; i < stars.Length; i++)
+ {
+ stars[i].x += (stars[i].x - .5) * stars[i].size * step;
+ stars[i].y += (stars[i].y - .5) * stars[i].size * step;
+ stars[i].size += stars[i].size * step * 2;
+
+ // reset stars that went out of bounds
+ if (stars[i].x < 0 || stars[i].x > 1 ||
+ stars[i].y < 0 || stars[i].y > 1)
+ stars[i] = GetRandomStar(randomSize: false);
+ }
+ }
+
+ public void Render(Bitmap bmp, Color? starColor = null)
+ {
+ starColor = starColor ?? Color.White;
+ using (var brush = new SolidBrush(starColor.Value))
+ using (var gfx = Graphics.FromImage(bmp))
+ {
+ gfx.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
+ gfx.Clear(Color.Black);
+ for (int i = 0; i < stars.Length; i++)
+ {
+ var star = stars[i];
+ float xPixel = (float)star.x * bmp.Width;
+ float yPixel = (float)star.y * bmp.Height;
+ float radius = (float)star.size - 1;
+ float diameter = radius * 2;
+ gfx.FillEllipse(brush, xPixel - radius, yPixel - radius, diameter, diameter);
+ }
+ }
+ }
+}
+```
+
+## Testing the Graphics Model
+
+Since the `Starfield` project is a class library it cannot be executed directly. The best way to start executing the code it contains is to create a small set of tests. I prefer NUnit test projects but any test system that allows you to initialize a `Starfield`, `Advance()` several times, and save the output will be fine.
+
+
+
+The image at the top of this page was generated by one of these tests. If you've never used tests before, this is a great project to take a look at. In Visual Studio open the "view" menu and select "Test Explorer". Right-click on any test or group of tests and hit Run. In the test output window you can see the output of the tests, including paths to PNG files ready to view.
+
+> **๐ก Tip:** When I write tests that render graphics I have the output saved as an image, then I load that image in my browser. Every time the test runs I hit F5 to refresh my browser and update the image. Using tabs and keyboard shortcuts to flip between them I can easily compare between images.
+
+## Source Code
+
+* GitHub: [Starfield Graphics Model](https://github.com/swharden/Csharp-Data-Visualization/blob/master/dev/old/drawing/starfield/Starfield/)
\ No newline at end of file
diff --git a/website/content/system.drawing/animate-starfield/starfield-tests.png b/website/content/system.drawing/animate-starfield/starfield-tests.png
new file mode 100644
index 0000000..990446c
Binary files /dev/null and b/website/content/system.drawing/animate-starfield/starfield-tests.png differ
diff --git a/website/content/system.drawing/animate-starfield/starfield.png b/website/content/system.drawing/animate-starfield/starfield.png
new file mode 100644
index 0000000..5b1b1a5
Binary files /dev/null and b/website/content/system.drawing/animate-starfield/starfield.png differ
diff --git a/website/content/system.drawing/animate-winforms/csharp-starfield-windows-forms.gif b/website/content/system.drawing/animate-winforms/csharp-starfield-windows-forms.gif
new file mode 100644
index 0000000..6f9728d
Binary files /dev/null and b/website/content/system.drawing/animate-winforms/csharp-starfield-windows-forms.gif differ
diff --git a/website/content/system.drawing/animate-winforms/index.md b/website/content/system.drawing/animate-winforms/index.md
new file mode 100644
index 0000000..7d26003
--- /dev/null
+++ b/website/content/system.drawing/animate-winforms/index.md
@@ -0,0 +1,63 @@
+---
+Title: Animating Graphics in Windows Forms
+Description: Update and render a platform-agnostic graphics model using Windows Forms
+date: 2020-04-20
+weight: 50
+---
+
+**This page demonstrates a simple method for animating graphics in Windows Forms.** In this example we use the [Starfield graphics model](../animate-starfield) which keeps track of star positions and has a `Render()` method that will draw the field onto an existing `Bitmap`. Controls on the Form let the user adjust the number of stars and the transparency of stars in real time. These controls serve a secondary purpose as tools to assess GUI responsiveness when the rendering system is under load.
+
+
+
+> ๐ก **Separating graphics operations from GUI management facilitates testing and promotes maintainability.** In this design rendering tasks are performed in the graphics model and platform-specific GUI tasks are isolated in their own project. This strategy allows us to create identical WPF and WinForms applications that use the same `Render()` method in the graphics model.
+
+## Code
+
+By isolating all rendering methods in our graphics model library, our GUI code remains refreshingly simple. This is all it takes to continuously render our starfield animation and display it in the Form:
+
+```cs
+readonly Field field = new Field(500);
+
+public Form1()
+{
+ InitializeComponent();
+}
+
+private void timer1_Tick(object sender, EventArgs e)
+{
+ // advance the graphics model and render a new Bitmap
+ field.Advance();
+ Bitmap bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);
+ byte alpha = (byte)(trackBar1.Value * 255 / 100);
+ Color starColor = Color.FromArgb(alpha, Color.White);
+ field.Render(bmp, starColor);
+
+ // replace the old Bitmap and dispose of the old one
+ var oldImage = pictureBox1.Image;
+ pictureBox1.Image = bmp;
+ oldImage?.Dispose();
+}
+```
+
+> ๐ก **Manual double-buffering may improve performance.** Creating a new `Bitmap` and disposing an old one on every `Render()` is costly. It's not a good idea to render directly on the `Bitmap` currently displayed in a `PictureBox` because it can produce tearing artifacts. Instead you could continuously hold two `Bitmap` objects in memory and alternate between the two (displaying one while drawing on the other). This strategy is called [multiple buffering](https://en.wikipedia.org/wiki/Multiple_buffering).
+
+### Theory
+
+The `Timer` triggers this sequence of events:
+
+* Advance the starfield model in time
+* Create a `Bitmap` the size of the `Picturebox`
+* Call the starfield's `Render()` method (passing-in the `Bitmap` to be drawn on)
+* Apply the `Bitmap` to the `Picturebox.Image` property
+
+> **โ ๏ธ WARNING: This method blocks the GUI thread while rendering.** This is not noticeable when renders are fast (500 stars), but this results in an unresponsive application when the render takes a lot of CPU effort (100,000 stars). We will explore how to render graphics without blocking the GUI thread in a future article.
+
+> ๐ก Assigning a `Bitmap` to the `Image` property of a `Picturebox` lets us take advantage of built-in double-buffering capabilities for flicker-free animations.
+
+> ๐ก 1 ms is a good value to use for your render timer. Since the timer isn't multi-threaded, it will take as long as it needs to perform the render but leave 1 ms free between renders to respond to mouse events and other GUI updates.
+
+> ๐ก Install the `System.Drawing.Common` NuGet package even if you don't think you need it. Using the common library instead of native `System.Drawing` will ensure your program can be compiled for .NET Core if you decide to upgrade later. It also ensures you can pass `System.Drawing` objects to .NET Standard libraries which use `System.Drawing.Common` under the hood.
+
+## Source Code
+
+* GitHub: [Starfield Windows Forms GUI](https://github.com/swharden/Csharp-Data-Visualization/blob/master/dev/old/drawing/starfield/Starfield.WinForms)
\ No newline at end of file
diff --git a/website/content/system.drawing/animate-wpf/csharp-starfield-windows-wpf.gif b/website/content/system.drawing/animate-wpf/csharp-starfield-windows-wpf.gif
new file mode 100644
index 0000000..d805434
Binary files /dev/null and b/website/content/system.drawing/animate-wpf/csharp-starfield-windows-wpf.gif differ
diff --git a/website/content/system.drawing/animate-wpf/index.md b/website/content/system.drawing/animate-wpf/index.md
new file mode 100644
index 0000000..0e3934a
--- /dev/null
+++ b/website/content/system.drawing/animate-wpf/index.md
@@ -0,0 +1,95 @@
+---
+Title: Animating Graphics in WPF
+Description: Update and render a platform-agnostic graphics model using WPF
+date: 2020-04-20
+weight: 60
+---
+
+**This page demonstrates a simple method for animating graphics in WPF.** In this example we use the [Starfield graphics model](../animate-starfield/) to keep track of star positions and sizes and implement a renderer inside our Window which uses System.Drawing to draw stars on a `Bitmap` displayed on an `Image` inside a `Canvas`. A `DispatchTimer` will be used to continuously advance the graphics model and trigger rendering. Controls let the user adjust the number of stars and their transparency, but also serve as a way to assess GUI responsiveness.
+
+
+
+### MainWindow.xaml
+
+```xml
+
+
+## Resources
+* My blog post from 2021: [Representing Images in Memory](https://swharden.com/blog/2021-06-03-images-in-memory/)
+* Multi-platform project source: [array-to-image](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/array-to-image)
+* Official documentation: [Accessing SkiaSharp bitmap pixel bits
+](https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/pixel-bits)
\ No newline at end of file
diff --git a/website/content/system.drawing/array-to-image/output.png b/website/content/system.drawing/array-to-image/output.png
new file mode 100644
index 0000000..65ebe4e
Binary files /dev/null and b/website/content/system.drawing/array-to-image/output.png differ
diff --git a/website/content/system.drawing/benchmark/index.md b/website/content/system.drawing/benchmark/index.md
new file mode 100644
index 0000000..8f0a34a
--- /dev/null
+++ b/website/content/system.drawing/benchmark/index.md
@@ -0,0 +1,68 @@
+---
+Title: Benchmarking and Performance Analysis
+Description: How you can benchmark your graphics model and rendering system and use Visual Studio's CPU profiling tools to identify bottlenecks and figure out which parts of your program you can refactor to improve performance
+weight: 70
+date: 2020-04-01
+---
+
+**This page investigates ways to benchmark your graphics model and rendering system** using Visual Studio's CPU profiling tools to identify bottlenecks and figure out which parts of your program you can refactor to improve performance.
+
+
+
+> **๐ก TIP:** Use the Form's `Title` to display benchmark information in the application's title bar while developing. This is an easy way to display flicker-free text without having to create new controls or modify your layout.
+
+## Measure FPS with a Stopwatch
+
+We can use a class-level Stopwatch to time how long it takes to draw a frame, then we can wrap our render sequence with a timer reset and measurement.
+
+```cs
+Stopwatch stopwatch = new Stopwatch();
+private void timer1_Tick(object sender, EventArgs e)
+{
+ stopwatch.Restart();
+ field.Advance();
+ /* drawing code */
+ double elapsedSec = (double)stopwatch.ElapsedTicks / Stopwatch.Frequency;
+ Text = $"Starfield in Windows Forms - {elapsedSec * 1000:0.00} ms ({1 / elapsedSec:0.00} FPS)";
+}
+```
+
+Using this method we find that when the application is switched to use a starfield with 100 thousand stars there is an immediate drop in GUI responsiveness as the frame-rate drops below 5 FPS.
+
+500 Stars (~700 FPS) | 100,000 Stars (~4.5 FPS)
+---|---
+|
+
+> **๐ก TIP:** Use two timers to separately measure performance of the model advancement vs. the graphics rendering system.
+
+## CPU Performance Analysis with Visual Studio
+
+
+Using [Visual Studio's performance monitoring tools](https://docs.microsoft.com/en-us/visualstudio/profiling/quickstart-cpu-usage-managed) we can profile our CPU usage while this application runs to get a better idea of where the slowdown is coming from.
+
+
+
+This reveals that the "hot path" (the single method taking the most CPU time) is the call to `System.Drawing.Graphics.FillEllipse()` at over 70% of total CPU time. In contrast our graphics model advancement method takes less than 1% of CPU time.
+
+This is insightful because it indicates that optimizing our graphics renderer, and not our graphics model, is where we should focus our efforts to maximize performance.
+
+> **๐ค Think about it:** If your drawing calls are the rate-limiting factor, the best way to increase performance is to use a faster graphics library (like SkiaSharp and OpenGL). On the other hand, if your graphics model calls are the rate-limiting factor, the biggest performance increases will come from optimizing the calculations in your model.
+
+## Improve Performance with a Premultiplied PixelFormat
+
+A slight performance improvement can be realized by instantiating `Bitmap` objects which utilize the _premultiplied alpha_ pixel formats.
+
+This holds true with most graphics libraries, not just System.Drawing.
+
+In benchmark tests drawing 10,000 random lines use of premultiplied pixel format increased render speed by about 10%.
+
+```cs
+var pixelFormat = System.Drawing.Imaging.PixelFormat.Format32bppPArgb;
+var bmp = new Bitmap(width, height, pixelFormat);
+```
+
+> **๐ก Deep dive:** Learn more about [straight vs. premultiplied alpha composition](https://en.wikipedia.org/wiki/Alpha_compositing#Straight_versus_premultiplied) theory on Wikipedia
+
+## Source Code
+
+* GitHub: [Form1.cs](https://github.com/swharden/Csharp-Data-Visualization/blob/master/dev/old/drawing/starfield/Starfield.WinForms/Form1.cs)
\ No newline at end of file
diff --git a/website/content/system.drawing/benchmark/starfield-hot-path.png b/website/content/system.drawing/benchmark/starfield-hot-path.png
new file mode 100644
index 0000000..8033179
Binary files /dev/null and b/website/content/system.drawing/benchmark/starfield-hot-path.png differ
diff --git a/website/content/system.drawing/benchmark/windows-forms-benchmark.png b/website/content/system.drawing/benchmark/windows-forms-benchmark.png
new file mode 100644
index 0000000..fd7afa1
Binary files /dev/null and b/website/content/system.drawing/benchmark/windows-forms-benchmark.png differ
diff --git a/website/content/system.drawing/benchmark/windows-forms-benchmark2.png b/website/content/system.drawing/benchmark/windows-forms-benchmark2.png
new file mode 100644
index 0000000..7f41a29
Binary files /dev/null and b/website/content/system.drawing/benchmark/windows-forms-benchmark2.png differ
diff --git a/website/content/system.drawing/cross-platform/index.md b/website/content/system.drawing/cross-platform/index.md
new file mode 100644
index 0000000..f57cb27
--- /dev/null
+++ b/website/content/system.drawing/cross-platform/index.md
@@ -0,0 +1,88 @@
+---
+Title: Cross-Platform Support for System.Drawing.Common
+Description: About the end of cross-platform support for System.Drawing.Common and what to do about it
+Date: 2022-03-11 7:00:00
+weight: 999
+---
+
+## History
+
+* `System.Drawing` started as a Windows-Only namespace
+* In 2018 the [`System.Drawing.Common` package](https://www.nuget.org/packages/System.Drawing.Common/) brought `System.Drawing` to cross-platform .NET Core projects
+* In 2021 the dotnet design team [decided to only support Windows](https://github.com/dotnet/designs/blob/main/accepted/2021/system-drawing-win-only/system-drawing-win-only.md) as a [breaking change](https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/system-drawing-common-windows-only)
+* In 2021 .NET 6 projects began warning `CA1416: This call site is reachable on all platforms`
+* In 2022 .NET 7 projects using `System.Drawing.Common` began throwing `PlatformNotSupportedException`
+
+## Why was cross-platform support dropped?
+
+Summarizing Microsoft's [Reason for change](https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/system-drawing-common-windows-only),
+
+* Cross-platform support for `System.Drawing.Common` depends on `libgdiplus`
+
+* `libgdiplus` is a mess and Microsoft does not want to support it. It a large (30k line) C code base without tests and has numerous external dependencies (`cairo`, `pango`, etc.)
+
+* _"It's not viable to get `libgdiplus` to the point where its feature set and quality is on par with the rest of the .NET stack."_
+
+* _"From analysis of NuGet packages ... we haven't noticed heavy graphics usage ... [these projects are] typically well supported with SkiaSharp and ImageSharp."_
+
+* `System.Drawing.Common` will now only be developed for Windows Forms
+
+## What to do about it?
+
+### Target Windows
+
+This problem is resolved if you update your csproj file to only target Windows:
+
+```xml
+
+
+## Respect `IDisposable`
+
+Many System.Drawing objects inherit from `IDisposable` and it is ***critical*** that they are disposed of properly to avoid memory issues. This means calling `Dispose()` after you're done using an object, or better yet throwing its instantiation inside a `using` statement.
+
+> ๐ก **Deep Dive:** Read Microsoft's documentation about [using objects that implement IDisposable](https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/using-objects)
+
+Take care to properly dispose of these common System.Drawing objects:
+* [`System.Drawing.Image`](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.image)
+* [`System.Drawing.Bitmap`](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.bitmap)
+* [`System.Drawing.Graphics`](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.graphics)
+* [`System.Drawing.Pen`](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.pen)
+* [`System.Drawing.Brush`](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.brush)
+* [`System.Drawing.Font`](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.font)
+* [`System.Drawing.FontFamily`](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.fontfamily)
+* [`System.Drawing.StringFormat`](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.stringformat)
+
+
+## Anti-Aliased Graphics and Text
+**Anti-aliasing is OFF by default.** Enabling anti-aliasing significantly slows render time but produces superior images when drawing angled lines and edges. Anti-aliasing can be enabled separately for shapes and text.
+
+
+
+```cs
+// Configure anti-aliasing mode for graphics
+gfx.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
+gfx.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighSpeed;
+
+// Configure anti-aliasing mode for text
+gfx.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixelGridFit;
+gfx.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
+gfx.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
+```
+
+ClearType typically looks the best, but when drawn on a transparent background it looks poor ([because](https://devblogs.microsoft.com/oldnewthing/20150129-00/?p=44803) [reasons](https://devblogs.microsoft.com/oldnewthing/20060614-00/?p=30873)).
+
+## Resources
+
+* Source code: [projects/system-drawing/quickstart-console/](https://github.com/swharden/Csharp-Data-Visualization/tree/main/projects/system-drawing/quickstart-console)
+
+* [Cross-Platform Support for `System.Drawing`](../cross-platform)
+
+* [Official documentation for `System.Drawing`](https://docs.microsoft.com/en-us/dotnet/api/system.drawing)
+
+* [Pitfalls of transparent rendering of anti-aliased fonts](https://devblogs.microsoft.com/oldnewthing/20060614-00/?p=30873)
+
+* [Color-aware ClearType requires access to fixed background pixels, which is a problem if you don't know what the background pixels are, or if they aren't fixed](https://devblogs.microsoft.com/oldnewthing/20150129-00/?p=44803)
\ No newline at end of file
diff --git a/website/content/system.drawing/quickstart-winforms/drawing-in-windows-forms-bad.gif b/website/content/system.drawing/quickstart-winforms/drawing-in-windows-forms-bad.gif
new file mode 100644
index 0000000..b4d0350
Binary files /dev/null and b/website/content/system.drawing/quickstart-winforms/drawing-in-windows-forms-bad.gif differ
diff --git a/website/content/system.drawing/quickstart-winforms/drawing-in-windows-forms-good.gif b/website/content/system.drawing/quickstart-winforms/drawing-in-windows-forms-good.gif
new file mode 100644
index 0000000..1809fc9
Binary files /dev/null and b/website/content/system.drawing/quickstart-winforms/drawing-in-windows-forms-good.gif differ
diff --git a/website/content/system.drawing/quickstart-winforms/drawing-in-windows-forms.png b/website/content/system.drawing/quickstart-winforms/drawing-in-windows-forms.png
new file mode 100644
index 0000000..be76f43
Binary files /dev/null and b/website/content/system.drawing/quickstart-winforms/drawing-in-windows-forms.png differ
diff --git a/website/content/system.drawing/quickstart-winforms/index.md b/website/content/system.drawing/quickstart-winforms/index.md
new file mode 100644
index 0000000..49a5295
--- /dev/null
+++ b/website/content/system.drawing/quickstart-winforms/index.md
@@ -0,0 +1,137 @@
+---
+title: System.Drawing Windows Forms Quickstart
+Description: A quickstart guide to using System.Drawing in Windows Forms applications
+date: 2020-04-20
+weight: 20
+---
+
+**This example demonstrates how to draw graphics in Windows Forms.** We create a `Bitmap` and draw on it using a `Graphics` object like before, but then we apply it to the `Image` property of a `PictureBox`. A key difference here is that the Bitmap will be sized to exactly fit the `PictureBox`, and we will use events to create new properly-sized `Bitmap` objects every time the form is resized.
+
+**Drawing on a `PictureBox` is preferred** over other methods (such as drawing on the background of a `Form`) because the background of a `PictureBox` is double-buffered and will produce flicker-free rendering.
+
+
+
+## Coding Strategy
+
+The strategy here is to place a `PictureBox` where we want our graphics to go, then we will create a `Bitmap` the size of the `PictureBox`, render onto it, then assign it to the `Image` property of the `PictureBox`.
+
+> **๐ก Tip:** Make your `PictureBox` magenta (or some other obnoxious color) so it's easy to adjust in the designer and obvious when it's been drawn on.
+
+### 1. Create a Render Method
+
+The code used to draw lines is similar to what we saw in the previous example, but with a few key differences:
+
+* The `Bitmap` is sized to be exactly the size of the Picturebox
+* The `pictureBox1.Image` is disposed of before receiving a clone of the Bitmap
+
+```cs
+Random rand = new Random();
+void Render()
+{
+ using (var bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height))
+ using (var gfx = Graphics.FromImage(bmp))
+ using (var pen = new Pen(Color.White))
+ {
+ // draw one thousand random white lines on a dark blue background
+ gfx.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
+ gfx.Clear(Color.Navy);
+ for (int i = 0; i < 1000; i++)
+ {
+ var pt1 = new Point(rand.Next(bmp.Width), rand.Next(bmp.Height));
+ var pt2 = new Point(rand.Next(bmp.Width), rand.Next(bmp.Height));
+ gfx.DrawLine(pen, pt1, pt2);
+ }
+
+ // copy the bitmap to the picturebox
+ pictureBox1.Image?.Dispose();
+ pictureBox1.Image = (Bitmap)bmp.Clone();
+ }
+}
+```
+
+> **โ ๏ธ WARNING:** Respect objects that implement `IDisposable` by properly disposing them before overwriting them. Improper disposal of these objects can result in serious stability and performance issues! See the discussion on the [System.Drawing Fundamentals](../quickstart-console) article for details.
+
+### 2. Call the Render Method
+
+The Render method won't run unless we tell it to. Let's have it run when the program launches, when the window is resized, and when we click on the picturebox.
+
+```cs
+public Form1()
+{
+ InitializeComponent();
+ Render();
+}
+```
+
+```cs
+private void pictureBox1_SizeChanged(object sender, EventArgs e)
+{
+ Render();
+}
+```
+
+```cs
+private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
+{
+ Render();
+}
+```
+
+## Rendering in a Separate Thread
+
+This is a good start at drawing in a Windows Forms application, but it has a flaw: calls to the render method block the GUI thread. This isn't a problem if your renderer is very fast, but as it gets slow your GUI will become less responsive. A later article will demonstrate how to move the renderer into another thread so you can draw and animate graphics without blocking the GUI thread.
+
+## Why is `IDisposable` such a big deal?
+
+Code on this page has `using` statements like those below, and this article has warnings about respecting `IDisposable`. How much does it _really_ matter?
+
+**Bad Code Ignores Disposal:** If you don't properly dispose of old Bitmaps and Graphics objects they accumulate in memory and cause a lot of work for your garbage collector or put your application at risk for a memory error.
+
+```cs
+void ResizeBitmap()
+{
+ // improper treatment of IDisposable objects
+ var bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height))
+ var gfx = Graphics.FromImage(bmp))
+ /* rendering code */
+}
+```
+
+Look how this bad code causes a new Bitmap to be created every time the window is resized without properly disposing the old one. The process memory increases by hundreds of megabytes in just a few seconds.
+
+
+
+**Good Code Uses Disposal:** Good code properly disposes of `IDisposable` objects. The easiest way to do this is to `new` one inside a `using` statement (which automatically calls `Dispose()` at the end). This is all it takes to fix the memory error from the previous example.
+
+```cs
+void ResizeBitmap()
+{
+ // properly disposing IDisposable objects
+ using (var bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height))
+ using (var gfx = Graphics.FromImage(bmp))
+ {
+ /* rendering code */
+ }
+}
+```
+
+
+
+## Why I avoid the `Paint` event.
+
+Some Windows Forms applications use the [Control.Paint event](https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.control.paint) to trigger rendering graphics on a control. This is convenient for fine-tuning the look of Windows Forms controls because you can obtain a `Graphics` object right from the `PaintEventArgs` argument and don't have to create a `Bitmap` yourself.
+
+However, I strive to write platform-agnostic software. Use of the `Paint` event and WinForms-specific calls (like `Control.CreateGraphics`) couples your rendering system to Windows Forms, making it harder to develop your rendering system in other environments in the future (like WPF, UWP, or WinUI).
+
+By creating Bitmaps and Graphics programmatically, controlling when renders occur, and displaying the images using Picturebox controls, the rendering system can remain largely decoupled from GUI-specific implementation.
+
+## Why Display in a `PictureBox`?
+
+The short answer is "double buffering".
+
+Imagine your goal is to animate white text scrolling on a black background. To achieve this you repeatedly fill the image all black, then render text at a slightly different location each time. If the screen displays your Bitmap at random times, some of the time the Bitmap will be all black (before the text has been added). When animated this will appear as flickering.
+
+Double buffering is a technique that uses one Bitmap to render on and a separate Bitmap to display from. Picturebox controls support double buffering automatically, so they don't suffer from flickering due to incomplete renders. The advantage of using Picturebox controls will become evident as we begin animating graphics in windows forms.
+
+## Source Code
+* [Form1.cs](https://github.com/swharden/Csharp-Data-Visualization/blob/master/dev/old/drawing/quickstart-winforms/Form1.cs)
\ No newline at end of file
diff --git a/website/content/system.drawing/quickstart-wpf/drawing-in-wpf.png b/website/content/system.drawing/quickstart-wpf/drawing-in-wpf.png
new file mode 100644
index 0000000..15ed4eb
Binary files /dev/null and b/website/content/system.drawing/quickstart-wpf/drawing-in-wpf.png differ
diff --git a/website/content/system.drawing/quickstart-wpf/index.md b/website/content/system.drawing/quickstart-wpf/index.md
new file mode 100644
index 0000000..c8c9d01
--- /dev/null
+++ b/website/content/system.drawing/quickstart-wpf/index.md
@@ -0,0 +1,104 @@
+---
+title: System.Drawing WPF Quickstart
+Description: Using System.Drawing to Draw Graphics in WPF Applications
+date: 2020-04-20
+weight: 30
+---
+**This example demonstrates how to draw graphics in WPF.** We lay-out a `Canvas` with an `Image` inside it, then create a render method which creates a `Bitmap` the size of the canvas, draws on it using a `Graphics` object, then copies the output from a `Bitmap` (which System.Drawing produces) to a `BitmapImage` (which WPF can display).
+
+
+
+## Code
+
+### 1. Add a `Canvas` to your `Window`
+
+Add a `Canvas` to your layout and inside it include an `Image`.
+
+Add add a `SizeChanged` method to your `Window` and `Loaded` and `MouseDown` methods in your `Canvas`. We will use these methods later to trigger renders.
+
+```xml
+
+
+**The result is a GUI which remains responsive even if the rendering system becomes extremely slow.** Notice how the trackbar and the alpha label update smoothly even through the graphics only renders at a rate of a couple frames per second.
+
+
+
+**This method also improves the reverse case, allowing high-speed graphics model updates and rendering which is not slowed down by the GUI.** In cases where the rendering system can run exceedingly fast, the graphics model can be throttled with a FPS limiter.
+
+## Code
+
+The overall strategy here is to spin-off a renderer thread which will continuously work to draw on a rendering Bitmap (`bmpLive`), then copy the output to a display Bitmap (`bmpLast`) every time a render completes.
+
+We use the [`lock` statement](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statement) to ensure the Bitmap being written in one thread does not get simultaneously read by another thread. Notice that we lock the _display_ Bitmap and not the _rendering_ Bitmap to avoid ever having our GUI to wait on the renderer.
+
+### Use a Trackbar to Adjust Alpha
+
+* Threads can only interact with `static` properties, so `starColor` will be defined as a class-level property rather than be determined inside the render method.
+
+```cs
+static Color starColor = Color.White;
+private void trackBar1_Scroll(object sender, EventArgs e)
+{
+ label1.Text = $"{trackBar1.Value}%";
+ byte alpha = (byte)(trackBar1.Value * 255 / 100);
+ starColor = Color.FromArgb(alpha, Color.White);
+}
+```
+
+### Startup
+
+* Threads can only interact with `static` properties, so `static Bitmap` objects will be used to store active (`bmpLive`) and display (`bmpLast`) images. Same goes for the `field`.
+
+* You can't lock `null` objects, so both `Bitmap` objects are populated with the program starts.
+
+* The renderer thread will get started as soon as the program starts.
+
+```cs
+static readonly Field field = new Field(500);
+static Bitmap bmpLive;
+static Bitmap bmpLast;
+public Form1()
+{
+ InitializeComponent();
+
+ bmpLive = new Bitmap(pictureBox1.Width, pictureBox1.Height);
+ bmpLast = (Bitmap)bmpLive.Clone();
+
+ var renderThread = new Thread(new ThreadStart(RenderForever));
+ renderThread.Start();
+}
+```
+
+### Threaded Renderer
+
+The renderer thread runs forever and performs these tasks:
+* advances the graphics model
+* draws on the _render_ Bitmap
+* locks the _display_ Bitmap as it copies the _render_ Bitmap onto it
+* performs FPS rate-limiting* (optional)
+
+*FPS rate limiting prevents the renderer from advancing the model and creating Bitmaps too frequently. If the renderer thread can render 1000 FPS but your GUI only displays 60 FPS, the FPS limiter can be used to prevent unnecessary renders.
+
+> ๐ก We use the [`lock` statement](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statement) to ensure the render thread doesn't write to the Bitmap at the same time as the GUI thread is reading from it.
+
+```cs
+private static void RenderForever()
+{
+ double maxFPS = 100;
+ double minFramePeriodMsec = 1000.0 / maxFPS;
+
+ Stopwatch stopwatch = Stopwatch.StartNew();
+ while (true)
+ {
+ // advance the model
+ field.Advance();
+
+ // Render on the "live" Bitmap
+ field.Render(bmpLive, starColor);
+
+ // Lock and update the "display" Bitmap
+ lock (bmpLast)
+ {
+ bmpLast.Dispose();
+ bmpLast = (Bitmap)bmpLive.Clone();
+ }
+
+ // FPS limiter
+ double msToWait = minFramePeriodMsec - stopwatch.ElapsedMilliseconds;
+ if (msToWait > 0)
+ Thread.Sleep((int)msToWait);
+ stopwatch.Restart();
+ }
+}
+```
+
+
+### Update the GUI with a Timer
+
+> **โ ๏ธ You must `Clone()` images you display in a Picturebox:** The Picturebox class natively supports double buffering. This is excellent for preventing flicker, but it also means you can't control when the image is painted (and its data read). To ensure a paint doesn't occur on a Bitmap locked by another thread, clone the `Bitmap` as you assign it to the Picturebox. Be sure to properly dispose of the old `Image` which may already be assigned to it.
+
+```cs
+private void timer1_Tick(object sender, EventArgs e)
+{
+ lock (bmpLast)
+ {
+ pictureBox1.Image?.Dispose();
+ pictureBox1.Image = (Bitmap)bmpLast.Clone();
+ }
+}
+```
+
+### Consider also Threading your Graphics Model
+
+If your model advancement takes a lot of time, consider placing it in its own thread as well. This way your model, rendering system, and GUI can all be on separate threads.
+
+## Source Code
+
+* GitHub: [Form1.cs](https://github.com/swharden/Csharp-Data-Visualization/blob/master/dev/old/drawing/starfield/Starfield.WinFormsNoBlock/Form1.cs)
\ No newline at end of file
diff --git a/website/content/system.drawing/video/index.md b/website/content/system.drawing/video/index.md
new file mode 100644
index 0000000..e6a6a26
--- /dev/null
+++ b/website/content/system.drawing/video/index.md
@@ -0,0 +1,95 @@
+---
+title: Render Video with System.Drawing
+description: How to draw graphics for each frame and use ffmpeg to save the result as a video file
+date: 2020-05-02
+weight: 100
+---
+
+**This page describes how to create video files using System.Drawing and ffmpeg.** The method described here uses [System.Drawing.Common](../cross-platform) which is no longer supported on non-Windows platforms. Check out the [rendering video with SkiaSharp](../../skiasharp/video) page for a cross-platform solution.
+
+
+
+### 1. Get FFMpegCore
+
+Create a new project:
+
+```bash
+dotnet new console
+```
+
+Add the necessary packages to your project:
+
+```bash
+dotnet add package System.Drawing.Common
+dotnet add package FFMpegCore
+dotnet add package FFMpegCore.Extensions.System.Drawing.Common
+```
+
+### 2. Add Using Statements
+
+```cs
+using FFMpegCore;
+using FFMpegCore.Pipes;
+using FFMpegCore.Extensions.System.Drawing.Common;
+```
+
+### 3. Create a Frame Generator
+
+Create a function to `yield` frames by creating and returning `Bitmap` images one at a time. This is where the logic goes that determines what will be drawn in each frame. This example draws a green rectangle that moves and grows as a function of frame number, and also displays frame information as text at the top of the image.
+
+```cs
+IEnumerableRedirecting to index.xml ...
+ + + \ No newline at end of file diff --git a/website/static/img/banner.jpg b/website/static/img/banner.jpg new file mode 100644 index 0000000..a6bdc7c Binary files /dev/null and b/website/static/img/banner.jpg differ diff --git a/website/static/img/highlights/boids.gif b/website/static/img/highlights/boids.gif new file mode 100644 index 0000000..99daacb Binary files /dev/null and b/website/static/img/highlights/boids.gif differ diff --git a/website/static/img/highlights/life.gif b/website/static/img/highlights/life.gif new file mode 100644 index 0000000..4428749 Binary files /dev/null and b/website/static/img/highlights/life.gif differ diff --git a/website/static/img/highlights/mystify.gif b/website/static/img/highlights/mystify.gif new file mode 100644 index 0000000..8c14d7c Binary files /dev/null and b/website/static/img/highlights/mystify.gif differ diff --git a/website/static/manifest.webmanifest b/website/static/manifest.webmanifest new file mode 100644 index 0000000..a91fafc --- /dev/null +++ b/website/static/manifest.webmanifest @@ -0,0 +1,6 @@ +{ + "icons": [ + { "src": "/favicon-192.png", "type": "image/png", "sizes": "192x192" }, + { "src": "/favicon-512.png", "type": "image/png", "sizes": "512x512" } + ] +} \ No newline at end of file