Visualization - C#

Quote

“The greatest value of a picture is when it forces us to notice what we never expected to see.”

John Tukey, Exploratory Data Analysis (1977)

C# Visualization Setup

.NET Interactive notebooks require a one-time warning suppression for NuGet version mismatches, package installation via #r "nuget:" directives, and data loading before any chart cell can run.

Suppress assembly version warnings

NuGet packages targeting .NET 8/9 trigger CS1701/CS1702 on .NET 10 — harmless version-mismatch noise. Run this cell once, before any cell that loads NuGet packages.

Suppress .NET notebook assembly-version warnings before loading NuGet packages.

using System.Reflection;
using Microsoft.DotNet.Interactive;
using Microsoft.DotNet.Interactive.CSharp;
 
var csharpKernel = (CSharpKernel)Kernel.Root.FindKernelByName("csharp");
var optionsField = typeof(CSharpKernel).GetField("_scriptOptions",
    BindingFlags.NonPublic | BindingFlags.Instance);
 
var scriptOptions = optionsField.GetValue(csharpKernel);
var withWarningLevel = scriptOptions.GetType().GetMethod("WithWarningLevel");
var newOptions = withWarningLevel.Invoke(scriptOptions, new object[] { 0 });
optionsField.SetValue(csharpKernel, newOptions);

NuGet Packages and Imports

Install the required packages: Polars.NET for data manipulation, Plotly.NET + Plotly.NET.CSharp for interactive charts, and ScottPlot + SkiaSharp for notebook-native SVG and static PNG export. The DATA variable points to the shared dataset folder.

Install charting packages and set the shared data path for the notebook session.

#r "nuget: Polars.NET, 0.4.0"
#r "nuget: Polars.NET.Native.win-x64, 0.4.0"
#r "nuget: Plotly.NET, 5.1.0"
#r "nuget: Plotly.NET.CSharp, 0.13.0"
#r "nuget: Plotly.NET.Interactive, 5.0.0"
#r "nuget: ScottPlot, 5.0.44"
#r "nuget: SkiaSharp, 2.88.9"
 
using System.IO;
using System.Linq;
using System.Text;
using Polars.CSharp;
using static Polars.CSharp.Polars;
using Plotly.NET;
using Chart = Plotly.NET.CSharp.Chart;
using Plotly.NET.LayoutObjects;
using Plotly.NET.TraceObjects;
 
var DATA = Path.Combine("..", "data");

Dataset Loading

Load the EuroStoxx 50 OHLCV dataset and extract typed arrays for use in chart traces. Polars.NET’s ToArray<T>() requires homogeneous column types — date columns must be cast to string first because ToArray<string>() does not coerce Date types directly.

Load the EuroStoxx dataset and prepare typed arrays for Plotly.NET traces.

var dfP = DataFrame.ReadCsv(Path.Combine(DATA, "eurostoxx50_ohlcv.csv"), tryParseDates: true);
display($"Loaded: {dfP.Shape}");
 
var asml = dfP.Filter(Col("symbol") == Lit("ASML.AS")).Sort("date");
var asmlDatesDf = asml.WithColumns(Col("date").Cast(DataType.String).Alias("date_str"));
var asmlDates = asmlDatesDf.Column("date_str").ToArray<string>();
var asmlClose = asml.Column("close").ToArray<double>();
var asmlOpen = asml.Column("open").ToArray<double>();
var asmlHigh = asml.Column("high").ToArray<double>();
var asmlLow = asml.Column("low").ToArray<double>();
var asmlVol = asml.Column("volume").ToArray<long>();
display($"ASML rows: {asmlDates.Length}");

Loaded: (66355, 12)

ASML rows: 1331

ScottPlot Helpers and Additional Datasets

ScottPlot examples in this note render inline SVG rather than iframe-based HTML. The helpers below preserve the notebook’s transparent theme, load the extra datasets used by the bubble / gauge / sector examples, and precompute the shared 60-trading-day windows reused across the ScottPlot cells.

Set up ScottPlot helpers, theme primitives, and reused data windows for the static charts.

var dfEU = dfP;
var dfUS = DataFrame.ReadCsv(Path.Combine(DATA, "stoxxusa50_ohlcv.csv"), tryParseDates: true);
var dfIdx = DataFrame.ReadCsv(Path.Combine(DATA, "index_performance.csv"), tryParseDates: true);
var dfSignals = DataFrame.ReadCsv(Path.Combine(DATA, "signals_daily.csv"), tryParseDates: true);
var dfScores = DataFrame.ReadCsv(Path.Combine(DATA, "scores_daily.csv"), tryParseDates: true);
 
var NotebookText = ScottPlot.Color.FromHex("#FFFFFF");
var NotebookGrid = ScottPlot.Color.FromHex("#D1D5DB").WithAlpha(128);
 
(int Width, int Height) FitSize(int width, int height, int maxWidth = 760)
{
    if (width <= maxWidth)
        return (width, height);
 
    double scale = (double)maxWidth / width;
    return (maxWidth, (int)Math.Round(height * scale));
}
 
string PrepareSvg(string svg, int maxWidth = 760)
{
    int svgIndex = svg.IndexOf("<svg", StringComparison.OrdinalIgnoreCase);
    if (svgIndex >= 0)
        svg = svg.Insert(svgIndex + 4, " style=\"max-width:100%;height:auto;display:block;\"");
 
    return $"<div style='max-width:{maxWidth}px;width:100%;margin:0 auto'>{svg}</div>";
}
 
void DisplaySvg(string svg, int maxWidth = 760)
{
    display(HTML(PrepareSvg(svg, maxWidth)));
}
 
void ShowPlot(ScottPlot.Plot plt, int width = 760, int height = 420)
{
    var size = FitSize(width, height);
    DisplaySvg(plt.GetSvgHtml(size.Width, size.Height), size.Width);
}
 
void ApplyAxisTheme(ScottPlot.Plot plt)
{
    plt.Axes.Color(NotebookText);
    plt.Axes.Title.Label.ForeColor = NotebookText;
    plt.Axes.Bottom.Label.ForeColor = NotebookText;
    plt.Axes.Bottom.TickLabelStyle.ForeColor = NotebookText;
    plt.Axes.Top.Label.ForeColor = NotebookText;
    plt.Axes.Top.TickLabelStyle.ForeColor = NotebookText;
    plt.Axes.Left.Label.ForeColor = NotebookText;
    plt.Axes.Left.TickLabelStyle.ForeColor = NotebookText;
    plt.Axes.Right.Label.ForeColor = NotebookText;
    plt.Axes.Right.TickLabelStyle.ForeColor = NotebookText;
}
 
void DarkTheme(ScottPlot.Plot plt)
{
    plt.FigureBackground.Color = ScottPlot.Color.FromARGB(0);
    plt.DataBackground.Color = ScottPlot.Color.FromARGB(0);
    ApplyAxisTheme(plt);
    plt.Grid.MajorLineColor = NotebookGrid;
}
 
var asmlDateTimes = asmlDates.Select(d => DateTime.Parse(d)).ToArray();
var asmlOADates = asmlDateTimes.Select(d => d.ToOADate()).ToArray();
 
var asmlReturns = new double[asmlClose.Length - 1];
for (int i = 1; i < asmlClose.Length; i++)
    asmlReturns[i - 1] = (asmlClose[i] - asmlClose[i - 1]) / asmlClose[i - 1] * 100.0;
 
var lineWindowDays = 60;
var asml60OADates = asmlOADates.TakeLast(lineWindowDays).ToArray();
var asml60Open = asmlOpen.TakeLast(lineWindowDays).ToArray();
var asml60High = asmlHigh.TakeLast(lineWindowDays).ToArray();
var asml60Low = asmlLow.TakeLast(lineWindowDays).ToArray();
var asml60Close = asmlClose.TakeLast(lineWindowDays).ToArray();
var asml60Vol = asmlVol.TakeLast(lineWindowDays).ToArray();
var asml60ReturnDates = asmlOADates.Skip(1).TakeLast(lineWindowDays).ToArray();
var asml60Returns = asmlReturns.TakeLast(lineWindowDays).ToArray();
 
var top5Df = dfEU.GroupBy("symbol")
    .Agg(Col("close").Mean().Alias("avg_close"))
    .Sort("avg_close", descending: true)
    .Head(5);
var top5Symbols = top5Df.Column("symbol").ToArray<string>();
var top5AvgClose = top5Df.Column("avg_close").ToArray<double>();
 
var top8VolDf = dfEU.GroupBy("symbol")
    .Agg(Col("volume").Cast(DataType.Float64).Sum().Alias("total_vol"))
    .Sort("total_vol", descending: true)
    .Head(8);
var top8Symbols = top8VolDf.Column("symbol").ToArray<string>();
var top8Volumes = top8VolDf.Column("total_vol").ToArray<double>();
 
display($"EU OHLCV: {dfEU.Shape}  |  US OHLCV: {dfUS.Shape}");
display($"Index perf: {dfIdx.Shape}  |  Signals: {dfSignals.Shape}  |  Scores: {dfScores.Shape}");
display($"ASML rows: {asmlDates.Length}  |  Top 5: {string.Join(", ", top5Symbols)}");

EU OHLCV: (66355, 12) | US OHLCV: (65100, 12)

Index perf: (5281, 15) | Signals: (466, 19) | Scores: (466, 36)

ASML rows: 1331 | Top 5: RMS.PA, ADYEN.AS, ASML.AS, MC.PA, RHM.DE


Line Charts

Line charts connect ordered data points to reveal trends, cycles, and rate of change over time. In Plotly.NET, Chart.Line<TX, TY, TName>() produces an interactive trace; Chart.Combine() merges multiple traces into a single figure. WithAxisAnchor() binds a trace to a specific y-axis for dual-axis layouts.

Best for: Time-series data — stock prices, sensor readings, cumulative returns. Avoid for unordered categories.

Single Line Chart

Plotly.NET | Single line — ASML close price over time

Plots ASML’s closing price as a single interactive trace. Date strings are parsed to DateTime to enable Plotly’s built-in date axis formatting.

Parses all 1,331 ASML date strings to DateTime, renders a single Chart.Line trace with custom transparent background and gray font, producing an interactive chart with Plotly’s built-in date axis formatting.

var dates = asmlDates.Select(d => DateTime.Parse(d)).ToArray();
Plotly.NET.CSharp.Chart.Line<DateTime, double, string>(x: dates, y: asmlClose)
    .WithTitle("ASML — Close Price")
    .WithXAxisStyle(Title.init("Date"))
    .WithYAxisStyle(Title.init("Close (EUR)"))
    .WithSize(900, 450)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))))

Multi-line Chart

Plotly.NET | Multi-line — overlay ASML, SAP, SIE close prices

Build one trace per symbol via LINQ and combine them with Chart.Combine(). Each trace is automatically colored by Plotly’s default palette; the legend identifies each symbol.

Iterates over three symbols (ASML.AS, SAP.DE, SIE.DE) with LINQ, builds one Chart.Line trace per symbol from its date-parsed close price array, and combines them into a single multi-series chart with an auto-colored legend.

var symbols = new[] { "ASML.AS", "SAP.DE", "SIE.DE" };
var traces = symbols.Select(sym =>
{
    var sub = dfP.Filter(Col("symbol") == Lit(sym)).Sort("date")
        .WithColumns(Col("date").Cast(DataType.String).Alias("date_str"));
    var dates = sub.Column("date_str").ToArray<string>().Select(d => DateTime.Parse(d)).ToArray();
    var close = sub.Column("close").ToArray<double>();
    return Plotly.NET.CSharp.Chart.Line<DateTime, double, string>(x: dates, y: close, Name: sym);
}).ToArray();
 
Plotly.NET.CSharp.Chart.Combine(traces)
    .WithTitle("Close Price Comparison — ASML vs SAP vs Siemens")
    .WithXAxisStyle(Title.init("Date"))
    .WithYAxisStyle(Title.init("Close (EUR)"))
    .WithSize(900, 450)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))))

Dual Y-Axis Chart

Plotly.NET | Dual y-axis — ASML price + volume

Overlays a price line (left axis, full height) with volume bars (right axis, scaled to occupy the bottom third). Plotly.NET exposes yaxis2 via DynamicObj since the typed API does not yet have a direct WithYAxis2 helper. Setting range to [0, maxVol * 4] effectively confines the bars to the lower quarter of the chart area.

Overlays ASML close price (left y-axis, EUR) and daily volume converted to millions (right y-axis) — uses DynamicObj to configure yaxis2 since the Plotly.NET typed API lacks a WithYAxis2 helper, and sets range: [0, maxVol * 4] to confine volume bars to the lower quarter of the chart area.

var dates = asmlDates.Select(d => DateTime.Parse(d)).ToArray();
var asmlVolDouble = asmlVol.Select(v => (double)v / 1_000_000.0).ToArray();
var maxVol = asmlVolDouble.Max();
 
var priceLine = Plotly.NET.CSharp.Chart.Line<DateTime, double, string>(
    x: dates, y: asmlClose, Name: "Close (EUR)")
    .WithAxisAnchor(Y: 1);
 
var volArea = Plotly.NET.CSharp.Chart.Column<double, DateTime, string>(
    values: asmlVolDouble, Keys: dates, Name: "Volume (M)")
    .WithAxisAnchor(Y: 2)
    .WithMarkerStyle(Color: Color.fromHex("#4a6cf7"), Opacity: 0.3);
 
var layout = Layout.init<string>(
    PaperBGColor: Color.fromString("transparent"),
    PlotBGColor: Color.fromString("transparent"),
    Font: Font.init(Color: Color.fromHex("#cccccc")));
 
var yaxis2 = new global::DynamicObj.DynamicObj();
yaxis2.SetValue("title", "Volume (M)");
yaxis2.SetValue("overlaying", "y");
yaxis2.SetValue("side", "right");
yaxis2.SetValue("range", new[] { 0.0, maxVol * 4.0 });
yaxis2.SetValue("showgrid", false);
layout.SetValue("yaxis2", yaxis2);
 
Plotly.NET.CSharp.Chart.Combine(new[] { volArea, priceLine })
    .WithTitle("ASML — Close Price + Volume")
    .WithYAxisStyle(Title.init("Close (EUR)"))
    .WithSize(900, 450)
    .WithLayout(layout)

Bar Charts

Bar charts compare discrete categories by encoding values as bar lengths. Chart.Column<TValue, TKeys, TName>() produces vertical bars; Chart.Combine() with BarMode.Stack produces stacked bars.

Best for: Ranking (top N symbols by price or volume), part-to-whole composition (up vs down day volume). Horizontal orientation works better when category labels are long.

Grouped Bar Chart

Plotly.NET | Grouped bar — average close price by top 5 symbols

Groups by symbol, computes mean close, sorts descending, and takes the top 5. Chart.Column maps barValues (heights) to barSymbols (x-axis categories).

Groups all EuroStoxx 50 symbols by mean close price using Polars.NET GroupBy().Agg(), takes the top 5 by descending average, and renders them as a Chart.Column bar chart with symbol labels on the x-axis.

var avgClose = dfP.GroupBy("symbol")
    .Agg(Col("close").Mean().Alias("avg_close"))
    .Sort("avg_close", descending: true)
    .Head(5);
 
var barSymbols = avgClose.Column("symbol").ToArray<string>();
var barValues = avgClose.Column("avg_close").ToArray<double>();
 
Plotly.NET.CSharp.Chart.Column<double, string, string>(barValues, Keys: barSymbols)
    .WithTitle("Top 5 Symbols — Average Close Price")
    .WithXAxisStyle(Title.init("Symbol"))
    .WithYAxisStyle(Title.init("Avg Close (EUR)"))
    .WithSize(900, 450)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))))

Stacked Bar Chart

Plotly.NET | Stacked bar — total volume by top 5 symbols

Splits each symbol’s total volume into “up days” (close > open) and “down days” (close ≤ open) to reveal whether buying or selling pressure dominates. BarMode.Stack stacks the two bar series; Chart.Combine() merges the two traces before applying layout.

For each of the top 5 symbols, sums daily volume separately into up-day (close > open) and down-day buckets using a for loop, renders two Chart.Column traces colored green (#26a69a) and red (#ef5350) respectively, and stacks them with BarMode.Stack to reveal per-symbol buying vs selling pressure.

var top5Syms = avgClose.Column("symbol").ToArray<string>();
 
var upVolumes = new double[top5Syms.Length];
var downVolumes = new double[top5Syms.Length];
 
for (int i = 0; i < top5Syms.Length; i++)
{
    var sub = dfP.Filter(Col("symbol") == Lit(top5Syms[i]));
    var upDays = sub.Filter(Col("close") > Col("open"));
    var downDays = sub.Filter(Col("close") <= Col("open"));
    upVolumes[i] = upDays.Column("volume").ToArray<long>().Select(v => (double)v).Sum();
    downVolumes[i] = downDays.Column("volume").ToArray<long>().Select(v => (double)v).Sum();
}
 
var upBar = Plotly.NET.CSharp.Chart.Column<double, string, string>(upVolumes, Keys: top5Syms, Name: "Up days")
    .WithMarkerStyle(Color: Color.fromHex("#26a69a"));
var downBar = Plotly.NET.CSharp.Chart.Column<double, string, string>(downVolumes, Keys: top5Syms, Name: "Down days")
    .WithMarkerStyle(Color: Color.fromHex("#ef5350"));
 
Plotly.NET.CSharp.Chart.Combine(new[] { upBar, downBar })
    .WithTitle("Top 5 Symbols — Volume by Day Type (Stacked)")
    .WithXAxisStyle(Title.init("Symbol"))
    .WithYAxisStyle(Title.init("Total Volume"))
    .WithSize(900, 450)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc")),
        BarMode: StyleParam.BarMode.Stack))

Scatter & Distribution

Scatter plots reveal relationships between two continuous variables. Histograms show distribution shape. Box plots summarize with quartiles and flag outliers. In Plotly.NET: Chart.Point for scatter, Chart.Histogram for distribution, Chart.BoxPlot for quartile summaries.

How to read a scatter plot: each dot is one observation. Dots rising left-to-right = positive correlation; no pattern = no linear relationship (non-linear patterns may still exist). Dense clusters = common value combinations.

Scatter Plot

Plotly.NET | Scatter — close vs volume (ASML)

Plots ASML’s closing price against daily volume to explore whether high-volume days correlate with price levels. Opacity: 0.6 reduces overplotting for overlapping points.

Casts ASML volume from long to double, then renders 1,331 data points as a Chart.Point scatter plot with volume on x and close price on y, using size-4 markers at 60% opacity to reduce overplotting.

var scatterVol = asmlVol.Select(v => (double)v).ToArray();
 
Plotly.NET.CSharp.Chart.Point<double, double, string>(scatterVol, asmlClose)
    .WithTitle("ASML — Close vs Volume")
    .WithXAxisStyle(Title.init("Volume"))
    .WithYAxisStyle(Title.init("Close (EUR)"))
    .WithMarkerStyle(Size: 4, Color: Color.fromHex("#42a5f5"), Opacity: 0.6)
    .WithSize(900, 450)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))))

Histogram

Plotly.NET | Histogram — ASML daily returns distribution

How to read: bar height = frequency (count of days) in each return bucket. A roughly symmetric bell shape centered near zero is expected for daily stock returns. Heavy tails (bars far from zero taller than expected) indicate fat-tail risk — more extreme moves than a normal distribution predicts.

Computes daily percentage returns as (close[i] - close[i-1]) / close[i-1] * 100. The array is one element shorter than the close price array.

Computes 1,330 daily percentage returns from ASML’s close price array using a for loop, then passes the resulting double[] to Chart.Histogram which auto-bins the distribution and renders frequency counts per return bucket.

var returns = new double[asmlClose.Length - 1];
for (int i = 1; i < asmlClose.Length; i++)
    returns[i - 1] = (asmlClose[i] - asmlClose[i - 1]) / asmlClose[i - 1] * 100.0;
 
Plotly.NET.CSharp.Chart.Histogram<double, double, string>(X: returns)
    .WithTitle("ASML — Daily Returns Distribution (%)")
    .WithXAxisStyle(Title.init("Daily Return (%)"))
    .WithYAxisStyle(Title.init("Frequency"))
    .WithMarkerStyle(Color: Color.fromHex("#66bb6a"))
    .WithSize(900, 450)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))))

Box Plot

Plotly.NET | Box plot — close price distribution by symbol (top 5)

How to read: the box spans Q1–Q3 (the middle 50% of values). The horizontal line inside is the median. Whiskers extend to the farthest point within 1.5× the IQR. Points beyond whiskers are outliers. Compare box positions to see which symbol trades at a higher median price; compare box widths to see which has more price dispersion.

Builds one BoxPlot trace per symbol using LINQ and combines them with Chart.Combine().

For each of the top 5 symbols, extracts the full close price array and constructs a Chart.BoxPlot trace with symbol-labeled x-axis categories; Chart.Combine() merges all 5 traces into a single grouped box plot showing median, IQR, whiskers, and outliers per symbol.

var boxTraces = top5Syms.Select(sym =>
{
    var sub = dfP.Filter(Col("symbol") == Lit(sym));
    var closes = sub.Column("close").ToArray<double>();
    var labels = Enumerable.Repeat(sym, closes.Length).ToArray();
    return Plotly.NET.CSharp.Chart.BoxPlot<string, double, string>(X: labels, Y: closes, Name: sym);
}).ToArray();
 
Plotly.NET.CSharp.Chart.Combine(boxTraces)
    .WithTitle("Close Price Distribution — Top 5 Symbols")
    .WithXAxisStyle(Title.init("Symbol"))
    .WithYAxisStyle(Title.init("Close (EUR)"))
    .WithSize(900, 450)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))))

Financial Charts

Candlestick and combined price+volume charts are the standard display for OHLCV data in financial analysis. Each candlestick encodes four values per period; adding volume below helps distinguish meaningful price moves from noise.

Related pattern

The financial metrics rendered in these charts — daily returns, OHLC spreads, volume — are defined in chart-metrics. For the dashboard-level KPIs these charts feed into, see index-snapshot-metrics.

Candlestick Chart

Plotly.NET | Candlestick — ASML OHLC

This section introduces the candlestick anatomy and the data arrays used to build the chart.

How to read: the body of each candle spans open to close. A green (hollow) body means close > open (bullish); a red (filled) body means close < open (bearish). The upper wick reaches the daily high; the lower wick reaches the daily low. Long wicks signal price rejection — buying or selling pressure reversed the move. A small body with long wicks (doji) indicates indecision.

Renders ASML’s full 1,331-day OHLC history as an interactive Plotly candlestick chart using pre-extracted asmlOpen, asmlHigh, asmlLow, and asmlClose arrays with date strings on the x-axis.

Plotly.NET.CSharp.Chart.Candlestick<double, string, string>(asmlOpen, asmlHigh, asmlLow, asmlClose, asmlDates)
    .WithTitle("ASML — Candlestick Chart")
    .WithXAxisStyle(Title.init("Date"))
    .WithYAxisStyle(Title.init("Price (EUR)"))
    .WithSize(900, 450)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))))

Candlestick with Volume Overlay

Plotly.NET | Candlestick + volume overlay

Renders candlestick and volume as two separate chart objects displayed sequentially via display() and the implicit return. Restricts to the last 130 trading days (~6 months) for readability — full history makes individual candles too narrow to read interactively.

Slices the last 130 trading days from the ASML arrays using TakeLast(), renders a candlestick for OHLC and a separate area chart for volume (in millions), and outputs both charts sequentially — the candlestick via display() and the volume area via implicit return.

var nCandle = Math.Min(130, asmlDates.Length);
var cDates = asmlDates.TakeLast(nCandle).Select(d => DateTime.Parse(d)).ToArray();
var cOpen = asmlOpen.TakeLast(nCandle).ToArray();
var cHigh = asmlHigh.TakeLast(nCandle).ToArray();
var cLow = asmlLow.TakeLast(nCandle).ToArray();
var cClose = asmlClose.TakeLast(nCandle).ToArray();
var cVol = asmlVol.TakeLast(nCandle).Select(v => (double)v / 1_000_000.0).ToArray();
 
var candle = Plotly.NET.CSharp.Chart.Candlestick<double, DateTime, string>(
        cOpen, cHigh, cLow, cClose, cDates)
    .WithTitle("ASML — Candlestick (last 6 months)")
    .WithYAxisStyle(Title.init("Price (EUR)"))
    .WithSize(900, 450)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))));
 
var volArea = Plotly.NET.CSharp.Chart.Area<DateTime, double, string>(
        x: cDates, y: cVol, Name: "Volume (M)")
    .WithTitle("Volume")
    .WithYAxisStyle(Title.init("Volume (millions)"))
    .WithSize(900, 300)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))));
 
display(candle);
volArea

Heatmap

Heatmaps encode a matrix of values as colors — ideal for correlation matrices and pivot tables. Plotly.NET’s Chart.Heatmap takes a List<double[]> as the z-matrix, with row and column labels provided as string arrays.

How to read a correlation matrix: values range from −1 to +1. Dark warm colors (close to +1) = variables move together; dark cool colors (close to −1) = variables move in opposite directions; near-zero = no linear relationship. The diagonal is always 1.0 (a variable is perfectly correlated with itself). Look for off-diagonal clusters of strong color — these reveal groups of collinear variables.

Correlation Heatmap

Plotly.NET | Correlation heatmap — OHLCV numeric columns (ASML)

Computes the 5×5 Pearson correlation matrix for OHLCV columns using a local helper function. Note that Polars.NET does not expose a built-in .corr() method, so the matrix is computed manually via array iteration.

Defines a local Pearson() function, builds a 5×5 correlation matrix over ASML’s open/high/low/close/volume arrays (volume cast from long to double), rounds each value to 3 decimal places, and passes the result to Chart.Heatmap with column names as both x and y labels.

var colNames = new[] { "open", "high", "low", "close", "volume" };
var arrays = colNames.Select(c =>
{
    if (c == "volume")
        return asml.Column(c).ToArray<long>().Select(v => (double)v).ToArray();
    return asml.Column(c).ToArray<double>();
}).ToArray();
 
// Compute Pearson correlation matrix
double Pearson(double[] x, double[] y)
{
    int n = x.Length;
    double mx = x.Average(), my = y.Average();
    double num = 0, dx = 0, dy = 0;
    for (int i = 0; i < n; i++)
    {
        double a = x[i] - mx, b = y[i] - my;
        num += a * b;
        dx += a * a;
        dy += b * b;
    }
    return num / Math.Sqrt(dx * dy);
}
 
int dim = colNames.Length;
var corrMatrix = new List<double[]>();
for (int r = 0; r < dim; r++)
{
    var row = new double[dim];
    for (int c = 0; c < dim; c++)
        row[c] = Math.Round(Pearson(arrays[r], arrays[c]), 3);
    corrMatrix.Add(row);
}
 
Plotly.NET.CSharp.Chart.Heatmap<double, string, string, string>(corrMatrix, X: colNames, Y: colNames)
    .WithTitle("ASML — OHLCV Correlation Matrix")
    .WithSize(600, 550)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))))

Pie, Donut & Radar

Pie and donut charts show part-to-whole composition for a single categorical variable. Radar charts (spider/polar charts) compare a single entity across multiple normalized dimensions. Use Chart.Pie, Chart.Doughnut, and Chart.ScatterPolar in Plotly.NET.CSharp.

Best for pie/donut: ≤6 slices with clearly distinct proportions (market share, budget allocation). For precise comparisons or many categories, prefer bar charts — humans judge bar lengths more accurately than angles.

Best for radar: profiling a single entity across 4–8 metrics (e.g., comparing OHLCV magnitude ratios across three stocks). Not suitable for many entities — overlapping polygons become unreadable.

Pie Chart

Plotly.NET | Pie chart — Volume share by top symbols

Groups by symbol, sums total traded volume, takes the top 8 by volume. Chart.Pie renders slices proportional to pieVals.

Groups all EuroStoxx 50 rows by symbol, sums volume cast to Float64, sorts descending, takes the top 8, and passes symbol labels and volume totals to Chart.Pie — each slice is sized proportionally to total traded volume.

var volBySymbol = dfP.GroupBy("symbol")
    .Agg(Col("volume").Cast(DataType.Float64).Sum().Alias("total_vol"))
    .Sort("total_vol", descending: true)
    .Head(8);
 
var pieLabels = volBySymbol.Column("symbol").ToArray<string>();
var pieVals = volBySymbol.Column("total_vol").ToArray<double>();
 
Plotly.NET.CSharp.Chart.Pie<double, string, string>(values: pieVals, Labels: pieLabels)
    .WithTitle("Volume Share — Top 8 Symbols")
    .WithSize(700, 500)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))))

Donut Chart

Plotly.NET | Donut chart — Trade count by exchange suffix

Extracts exchange suffix from each symbol ticker (e.g., .AS from ASML.AS) using LINQ string splitting, then counts rows per exchange. Chart.Doughnut is identical to Chart.Pie but adds a hole in the center — useful for placing a KPI label or total count.

Extracts the exchange suffix from all 66,355 symbol strings using Split('.').Last(), groups and counts rows per suffix with LINQ, sorts descending, and renders a Chart.Doughnut with proportional slices — the center hole is left available for a KPI total label.

var symbolArr = dfP.Column("symbol").ToArray<string>();
var exchangeCounts = symbolArr
    .Select(s => "." + s.Split('.').Last())
    .GroupBy(e => e)
    .Select(g => new { Exchange = g.Key, Count = (double)g.Count() })
    .OrderByDescending(x => x.Count)
    .ToArray();
 
var donutLabels = exchangeCounts.Select(x => x.Exchange).ToArray();
var donutVals = exchangeCounts.Select(x => x.Count).ToArray();
 
Plotly.NET.CSharp.Chart.Doughnut<double, string, string>(values: donutVals, Labels: donutLabels)
    .WithTitle("Row Count by Exchange")
    .WithSize(700, 500)
    .WithLayout(Layout.init<string>(
        PaperBGColor: Color.fromString("transparent"),
        PlotBGColor: Color.fromString("transparent"),
        Font: Font.init(Color: Color.fromHex("#cccccc"))))

Radar Chart

Plotly.NET | Radar chart — Normalized metrics for top 3 symbols

How to read: each spoke represents one metric. Distance from the center = normalized magnitude (0 = global minimum, 1 = global maximum for that metric). A larger polygon area = higher overall magnitude across all dimensions. Compare polygon shapes: if one stock is much larger on volume but similar on close, it trades with higher activity relative to its price level.

Normalizes each metric to [0, 1] by dividing the per-symbol average by the global maximum. Closes the polygon by appending vals[0] and metrics[0] at the end.

For ASML, SAP, and Siemens, computes average open/high/low/close/volume (volume averaged as double) and normalizes each to [0,1] by dividing by the global metric max; closes each polygon by appending the first value, then combines three Chart.ScatterPolar traces in Lines mode with a transparent polar background.

var radarSyms = new[] { "ASML.AS", "SAP.DE", "SIE.DE" };
var metrics = new[] { "open", "high", "low", "close", "volume" };
 
// Get global max per metric for normalization
var globalMax = metrics.Select(m =>
    m == "volume"
        ? dfP.Column(m).ToArray<long>().Max()
        : (long)dfP.Column(m).ToArray<double>().Max()
).ToArray();
 
var radarTraces = radarSyms.Select(sym =>
{
    var sub = dfP.Filter(Col("symbol") == Lit(sym));
    var vals = new double[metrics.Length];
    for (int j = 0; j < metrics.Length; j++)
    {
        double avg = metrics[j] == "volume"
            ? sub.Column(metrics[j]).ToArray<long>().Average(v => (double)v)
            : sub.Column(metrics[j]).ToArray<double>().Average();
        vals[j] = avg / globalMax[j];
    }
    return Plotly.NET.CSharp.Chart.ScatterPolar<double, string, string>(
        r: vals.Append(vals[0]).ToArray(),
        theta: metrics.Append(metrics[0]).ToArray(),
        mode: StyleParam.Mode.Lines, Name: sym);
}).ToArray();
 
// Set polar subplot background to transparent
var polar = new Plotly.NET.LayoutObjects.Polar();
polar.SetValue("bgcolor", "rgba(0,0,0,0)");
 
var layout = Layout.init<string>(
    PaperBGColor: Color.fromString("transparent"),
    PlotBGColor: Color.fromString("transparent"),
    Font: Font.init(Color: Color.fromHex("#cccccc")));
layout.SetValue("polar", polar);
 
Plotly.NET.CSharp.Chart.Combine(radarTraces)
    .WithTitle("Normalized OHLCV Metrics — Radar Comparison")
    .WithSize(700, 550)
    .WithLayout(layout)

ScottPlot

ScottPlot is the static counterpart to the Plotly.NET examples above: fast SVG/PNG generation, no JavaScript dependency, and straightforward control over axes, fills, and composite layouts. The cells below are extracted from the source ScottPlot notebook and keep their original inline SVG outputs so the note remains self-contained.

ScottPlot vs Plotly.NET

Use ScottPlot when you want notebook-native SVG, batch PNG export, server-side rendering, or dense series performance. Use Plotly.NET when hover, zoom, and pan are part of the analysis workflow.

Export Workflow

All ScottPlot examples here render inline SVG via ShowPlot(). To persist any of them to disk instead, call plt.SavePng(path, width, height) or plt.SaveSvg(path, width, height) after the plot configuration step.


Line Charts

ScottPlot line primitives focus on fast static rendering. Scatter, SignalXY, and twin-axis layouts cover single-series trends, overlays, dense series, and price-volume combinations without a browser runtime.

ScottPlot | Single line — ASML close price with dark theme and date axis

This section introduces the runnable chart cell and its supporting setup.

Render ASML close price as a static SVG line chart with a dark theme.

// Single line — ASML closing price over the last 60 trading days
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var sig = plt.Add.Scatter(asml60OADates, asml60Close);
sig.LineWidth = 2f;
sig.MarkerSize = 0;
sig.Color = ScottPlot.Color.FromHex("#42a5f5");
 
plt.Title("ASML — Close Price (Last 60 Trading Days)");
plt.XLabel("Date");
plt.YLabel("Close (EUR)");
plt.Axes.DateTimeTicksBottom();
ApplyAxisTheme(plt);
 
ShowPlot(plt);
15-Dec-25 29-Dec-25 12-Jan-26 26-Jan-26 09-Feb-26 23-Feb-26 09-Mar-26 Close (EUR) 850 900 950 1'000 1'050 1'100 1'150 1'200 1'250 1'300 ASML — Close Price (Last 60 Trading Days)

ScottPlot | Multi-line — ASML, SAP, Siemens with custom line styles

This section introduces the runnable chart cell and its supporting setup.

Compare ASML, SAP, and Siemens close prices as a static multi-series SVG chart.

// Multi-line overlay — three stocks over the last 60 trading days
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var symbols = new[] { "ASML.AS", "SAP.DE", "SIE.DE" };
var colors = new[] { "#42a5f5", "#66bb6a", "#ef5350" };
var widths = new[] { 2f, 2f, 2f };
var patterns = new[] { ScottPlot.LinePattern.Solid, ScottPlot.LinePattern.Dashed, ScottPlot.LinePattern.DenselyDashed };
 
for (int i = 0; i < symbols.Length; i++)
{
    var sub = dfEU.Filter(Col("symbol") == Lit(symbols[i])).Sort("date")
        .WithColumns(Col("date").Cast(DataType.String).Alias("date_str"));
    var dates = sub.Column("date_str").ToArray<string>().Select(d => DateTime.Parse(d).ToOADate()).TakeLast(lineWindowDays).ToArray();
    var close = sub.Column("close").ToArray<double>().TakeLast(lineWindowDays).ToArray();
 
    var line = plt.Add.Scatter(dates, close);
    line.LineWidth = widths[i];
    line.MarkerSize = 0;
    line.Color = ScottPlot.Color.FromHex(colors[i]);
    line.LinePattern = patterns[i];
    line.LegendText = symbols[i];
}
 
plt.Title("Close Price Comparison — ASML vs SAP vs Siemens (Last 60 Trading Days)");
plt.XLabel("Date");
plt.YLabel("Close (EUR)");
plt.Axes.DateTimeTicksBottom();
ApplyAxisTheme(plt);
plt.Axes.Color(NotebookText);
plt.ShowLegend(Alignment.UpperLeft);
 
ShowPlot(plt);
ASML.AS SAP.DE 15-Dec-25 29-Dec-25 12-Jan-26 26-Jan-26 09-Feb-26 23-Feb-26 09-Mar-26 Close (EUR) 100 200 300 400 500 600 700 800 900 1'000 1'100 1'200 1'300 Close Price Comparison — ASML vs SAP vs Siemens (Last 60 Trading Days)

ScottPlot | Dual Y-axis — close price + volume on twin axes

This section introduces the runnable chart cell and its supporting setup.

Overlay ASML price and volume on separate y-axes in a static chart.

// Dual Y-axis — price on left, volume on right (last 60 trading days)
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
// Volume bars on secondary Y axis, compressed into a low-profile bottom band
var volDouble = asml60Vol.Select(v => (double)v / 1_000_000.0).ToArray();
var maxVol = volDouble.Max();
var volBarsData = new List<ScottPlot.Bar>(volDouble.Length);
for (int i = 0; i < volDouble.Length; i++)
{
    var intensity = maxVol > 0 ? volDouble[i] / maxVol : 0;
    byte shade = (byte)(90 + intensity * 140);
    volBarsData.Add(new ScottPlot.Bar
    {
        Position = asml60OADates[i],
        Value = volDouble[i],
        ValueBase = 0,
        Size = 0.9,
        FillColor = new ScottPlot.Color(shade, shade, shade, 150),
        LineWidth = 0
    });
}
 
var volBars = plt.Add.Bars(volBarsData);
volBars.LegendText = "Volume (M)";
volBars.Axes.YAxis = plt.Axes.Right;
 
// Price line on primary Y axis so it stays visually above the volume bars
var priceLine = plt.Add.Scatter(asml60OADates, asml60Close);
priceLine.LineWidth = 2f;
priceLine.MarkerSize = 0;
priceLine.Color = ScottPlot.Color.FromHex("#42a5f5");
priceLine.LegendText = "Close (EUR)";
priceLine.Axes.YAxis = plt.Axes.Left;
 
plt.Axes.Right.Min = 0;
plt.Axes.Right.Max = maxVol / 0.15;
plt.Axes.Right.SetTicks(
    new[] { 0.0, maxVol / 2, maxVol },
    new[] { "0", $"{maxVol / 2:F1}", $"{maxVol:F1}" });
plt.Axes.Right.Label.Text = "Volume (M)";
 
plt.Title("ASML — Close Price + Volume (Last 60 Trading Days)");
plt.XLabel("Date");
plt.YLabel("Close (EUR)");
plt.Axes.DateTimeTicksBottom();
ApplyAxisTheme(plt);
plt.ShowLegend(Alignment.UpperLeft);
 
ShowPlot(plt);
Volume (M) Close (EUR) 15-Dec-25 29-Dec-25 12-Jan-26 26-Jan-26 09-Feb-26 23-Feb-26 09-Mar-26 Close (EUR) 850 900 950 1'000 1'050 1'100 1'150 1'200 1'250 1'300 Volume (M) 0 0.7 1.4 ASML — Close Price + Volume (Last 60 Trading Days)

ScottPlot | SignalXY — high-performance rendering of full 66k-row dataset

This section introduces the runnable chart cell and its supporting setup.

Render the full 66k-row dataset with ScottPlot’s high-performance signal trace.

// SignalXY — optimized for large datasets with a last-60-day viewport
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
// Build full-dataset arrays sorted by date
var allDatesDf = dfEU.Sort("date").WithColumns(Col("date").Cast(DataType.String).Alias("date_str"));
var allDates = allDatesDf.Column("date_str").ToArray<string>().Select(d => DateTime.Parse(d).ToOADate()).ToArray();
var allClose = dfEU.Sort("date").Column("close").ToArray<double>();
 
var signal = plt.Add.SignalXY(allDates, allClose);
signal.Color = ScottPlot.Color.FromHex("#ab47bc");
signal.LegendText = $"All EU symbols ({allDates.Length:N0} points)";
 
plt.Title($"Euro Stoxx 50 — All Close Prices (Last 60-Day View, {allDates.Length:N0} points via SignalXY)");
plt.XLabel("Date");
plt.YLabel("Close (EUR)");
plt.Axes.DateTimeTicksBottom();
ApplyAxisTheme(plt);
plt.Axes.Bottom.Min = asml60OADates.First();
plt.Axes.Bottom.Max = asml60OADates.Last();
plt.ShowLegend(Alignment.UpperRight);
 
ShowPlot(plt);
All EU symbols (66'355 points) 29-Dec-25 12-Jan-26 26-Jan-26 09-Feb-26 23-Feb-26 09-Mar-26 Close (EUR) 0 500 1'000 1'500 2'000 2'500 3'000 Euro Stoxx 50 — All Close Prices (Last 60-Day View, 66'355 points via SignalXY)

Bar Charts

Bar-oriented ScottPlot examples work well for ranked comparisons and grouped or stacked financial aggregates. These cells stay close to the underlying arrays, so it is easy to turn Polars results into labeled bars.

ScottPlot | Vertical bars — average close price for top 5 symbols

This section introduces the runnable chart cell and its supporting setup.

Compare average close price for the top 5 symbols with a static bar chart.

// Vertical bars — average close for top 5 symbols with value labels
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var barColors = new[] { "#42a5f5", "#66bb6a", "#ef5350", "#ab47bc", "#ffa726" };
var bars = new List<ScottPlot.Bar>();
for (int i = 0; i < top5Symbols.Length; i++)
{
    bars.Add(new ScottPlot.Bar
    {
        Position = i,
        Value = top5AvgClose[i],
        FillColor = ScottPlot.Color.FromHex(barColors[i]),
        Label = $"{top5AvgClose[i]:F0}"
    });
}
 
var barPlot = plt.Add.Bars(bars.ToArray());
barPlot.ValueLabelStyle.ForeColor = ScottPlot.Color.FromHex("#cccccc");
barPlot.ValueLabelStyle.Bold = true;
 
plt.Axes.Bottom.SetTicks(
    Enumerable.Range(0, top5Symbols.Length).Select(i => (double)i).ToArray(),
    top5Symbols);
 
plt.Title("Top 5 Symbols — Average Close Price");
plt.YLabel("Avg Close (EUR)");
plt.Axes.Margins(bottom: 0);
 
ShowPlot(plt);
1762 1546 671 662 545 RMS.PA ADYEN.AS ASML.AS MC.PA RHM.DE Avg Close (EUR) 0 200 400 600 800 1'000 1'200 1'400 1'600 1'800 Top 5 Symbols — Average Close Price

ScottPlot | Grouped bars — up-day vs down-day volume for top 5 symbols

This section introduces the runnable chart cell and its supporting setup.

Compare up-day and down-day volume across the top 5 symbols.

// Grouped bars — up-day vs down-day volume comparison
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var upVols = new double[top5Symbols.Length];
var downVols = new double[top5Symbols.Length];
for (int i = 0; i < top5Symbols.Length; i++)
{
    var sub = dfEU.Filter(Col("symbol") == Lit(top5Symbols[i]));
    upVols[i] = sub.Filter(Col("close") > Col("open")).Column("volume").ToArray<long>().Sum() / 1e9;
    downVols[i] = sub.Filter(Col("close") <= Col("open")).Column("volume").ToArray<long>().Sum() / 1e9;
}
 
var upBars = new List<ScottPlot.Bar>();
var downBars = new List<ScottPlot.Bar>();
for (int i = 0; i < top5Symbols.Length; i++)
{
    upBars.Add(new ScottPlot.Bar { Position = i * 3, Value = upVols[i], FillColor = ScottPlot.Color.FromHex("#26a69a") });
    downBars.Add(new ScottPlot.Bar { Position = i * 3 + 1, Value = downVols[i], FillColor = ScottPlot.Color.FromHex("#ef5350") });
}
 
var upPlot = plt.Add.Bars(upBars.ToArray());
upPlot.LegendText = "Up days";
var downPlot = plt.Add.Bars(downBars.ToArray());
downPlot.LegendText = "Down days";
 
plt.Axes.Bottom.SetTicks(
    Enumerable.Range(0, top5Symbols.Length).Select(i => i * 3 + 0.5).ToArray(),
    top5Symbols);
 
plt.Title("Top 5 — Volume by Day Type (Grouped)");
plt.YLabel("Total Volume (B)");
plt.ShowLegend(Alignment.UpperRight);
 
ShowPlot(plt);
Up days Down days RMS.PA ADYEN.AS ASML.AS MC.PA RHM.DE Total Volume (B) 0 0.05 0.1 0.15 0.2 0.25 0.3 0.35 0.4 0.45 0.5 Top 5 — Volume by Day Type (Grouped)

ScottPlot | Stacked bars — quarterly volume breakdown for ASML

This section introduces the runnable chart cell and its supporting setup.

Show ASML quarterly volume as a stacked bar chart over time.

// Stacked bars — ASML quarterly volume split by up/down days
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
// Group ASML data by year-quarter
var asmlWithQ = asml.WithColumns(
    Col("date").Cast(DataType.String).Alias("date_str"));
var asmlDateStrings = asmlWithQ.Column("date_str").ToArray<string>();
var asmlCloseArr = asml.Column("close").ToArray<double>();
var asmlOpenArr = asml.Column("open").ToArray<double>();
var asmlVolArr = asml.Column("volume").ToArray<long>();
 
var quarterData = new SortedDictionary<string, (double up, double down)>();
for (int i = 0; i < asmlDateStrings.Length; i++)
{
    var dt = DateTime.Parse(asmlDateStrings[i]);
    var key = $"{dt.Year}-Q{(dt.Month - 1) / 3 + 1}";
    if (!quarterData.ContainsKey(key)) quarterData[key] = (0, 0);
    var (u, d) = quarterData[key];
    if (asmlCloseArr[i] > asmlOpenArr[i])
        quarterData[key] = (u + asmlVolArr[i] / 1e6, d);
    else
        quarterData[key] = (u, d + asmlVolArr[i] / 1e6);
}
 
var qLabels = quarterData.Keys.ToArray();
var upBars = new List<ScottPlot.Bar>();
var downBars = new List<ScottPlot.Bar>();
for (int i = 0; i < qLabels.Length; i++)
{
    var (u, d) = quarterData[qLabels[i]];
    upBars.Add(new ScottPlot.Bar { Position = i, Value = u, FillColor = ScottPlot.Color.FromHex("#26a69a") });
    downBars.Add(new ScottPlot.Bar { Position = i, ValueBase = u, Value = u + d, FillColor = ScottPlot.Color.FromHex("#ef5350") });
}
 
var upPlot = plt.Add.Bars(upBars.ToArray());
upPlot.LegendText = "Up days";
var downPlot = plt.Add.Bars(downBars.ToArray());
downPlot.LegendText = "Down days";
 
plt.Axes.Bottom.SetTicks(
    Enumerable.Range(0, qLabels.Length).Select(i => (double)i).ToArray(),
    qLabels);
plt.Axes.Bottom.TickLabelStyle.Rotation = 45;
plt.Axes.Bottom.TickLabelStyle.Alignment = Alignment.MiddleLeft;
plt.Axes.Bottom.MinimumSize = 95;
 
plt.Title("ASML — Quarterly Volume (Stacked by Day Type)");
plt.YLabel("Volume (M)");
plt.ShowLegend(Alignment.UpperLeft);
 
ShowPlot(plt, 900, 500);
Up days Down days 2021-Q1 2021-Q2 2021-Q3 2021-Q4 2022-Q1 2022-Q2 2022-Q3 2022-Q4 2023-Q1 2023-Q2 2023-Q3 2023-Q4 2024-Q1 2024-Q2 2024-Q3 2024-Q4 2025-Q1 2025-Q2 2025-Q3 2025-Q4 2026-Q1 Volume (M) 0 10 20 30 40 50 60 ASML — Quarterly Volume (Stacked by Day Type)

ScottPlot | Horizontal bars — composite score ranking from scores_daily

This section introduces the runnable chart cell and its supporting setup.

Rank the composite scores with a horizontal bar chart for quick comparison.

// Horizontal bars — top 10 stocks by composite score
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var euScores = dfScores.Filter(Col("_index") == Lit("euro_stoxx_50"))
    .Sort("composite_score", descending: true)
    .Head(10);
var scoreSymbols = euScores.Column("symbol").ToArray<string>();
var scoreValues = euScores.Column("composite_score").ToArray<double>();
 
var bars = new List<ScottPlot.Bar>();
for (int i = 0; i < scoreSymbols.Length; i++)
{
    bars.Add(new ScottPlot.Bar
    {
        Position = i,
        Value = scoreValues[i],
        FillColor = ScottPlot.Color.FromHex("#42a5f5").WithAlpha((byte)(255 - i * 18)),
        Label = $"{scoreValues[i]:F2}"
    });
}
 
var barPlot = plt.Add.Bars(bars.ToArray());
barPlot.Horizontal = true;
barPlot.ValueLabelStyle.ForeColor = ScottPlot.Color.FromHex("#cccccc");
 
// Reverse order so highest is at top
plt.Axes.Left.SetTicks(
    Enumerable.Range(0, scoreSymbols.Length).Select(i => (double)i).ToArray(),
    scoreSymbols);
 
plt.Title("Euro Stoxx 50 — Top 10 by Composite Score");
plt.XLabel("Composite Score");
 
ShowPlot(plt, 900, 500);
0.68 0.68 0.66 0.58 0.52 0.52 0.51 0.49 0.46 0.42 Composite Score 0 0.05 0.1 0.15 0.2 0.25 0.3 0.35 0.4 0.45 0.5 0.55 0.6 0.65 0.7 BNP.PA BNP.PA BNP.PA VOW.DE DTE.DE DTE.DE IFX.DE DTE.DE VOW.DE ABI.BR Euro Stoxx 50 — Top 10 by Composite Score

Financial Charts

ScottPlot 5 includes dedicated OHLC and candlestick primitives for market data. The cells below layer technical indicators and shaded bands on top of the same ASML window to show how static trading views can still carry rich context.

ScottPlot | Candlestick — ASML OHLC last 60 trading days

This section introduces the runnable chart cell and its supporting setup.

Show the last 60 trading days as a candlestick chart with price context.

// Candlestick — ASML OHLC with 120-day context and last 60 trading days visible
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
int calcDays = Math.Min(120, asmlDates.Length);
int displayDays = Math.Min(60, calcDays);
int displayOffset = calcDays - displayDays;
 
var calcDates = asmlDateTimes.TakeLast(calcDays).ToArray();
var calcOpen = asmlOpen.TakeLast(calcDays).ToArray();
var calcHigh = asmlHigh.TakeLast(calcDays).ToArray();
var calcLow = asmlLow.TakeLast(calcDays).ToArray();
var calcClose = asmlClose.TakeLast(calcDays).ToArray();
 
var cDates = calcDates.Skip(displayOffset).ToArray();
var cOpen = calcOpen.Skip(displayOffset).ToArray();
var cHigh = calcHigh.Skip(displayOffset).ToArray();
var cLow = calcLow.Skip(displayOffset).ToArray();
var cClose = calcClose.Skip(displayOffset).ToArray();
 
var ohlcs = new List<ScottPlot.OHLC>();
for (int i = 0; i < displayDays; i++)
    ohlcs.Add(new ScottPlot.OHLC(cOpen[i], cHigh[i], cLow[i], cClose[i], cDates[i], TimeSpan.FromDays(1)));
 
var candle = plt.Add.Candlestick(ohlcs);
candle.RisingColor = ScottPlot.Color.FromHex("#26a69a");
candle.FallingColor = ScottPlot.Color.FromHex("#ef5350");
 
plt.Title("ASML — Candlestick (Last 60 Trading Days)");
plt.XLabel("Date");
plt.YLabel("Price (EUR)");
plt.Axes.DateTimeTicksBottom();
ApplyAxisTheme(plt);
plt.Axes.Color(NotebookText);
 
ShowPlot(plt);
15-Dec-25 29-Dec-25 12-Jan-26 26-Jan-26 09-Feb-26 23-Feb-26 09-Mar-26 Price (EUR) 850 900 950 1'000 1'050 1'100 1'150 1'200 1'250 1'300 ASML — Candlestick (Last 60 Trading Days)

ScottPlot | OHLC bars — alternative representation for the same 60-day window

This section introduces the runnable chart cell and its supporting setup.

Use OHLC bars to show the same 60-day price window in a compact static form.

// OHLC bars — same data, bar-style rendering
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var ohlcBars = plt.Add.OHLC(ohlcs);
ohlcBars.RisingStyle.Color = ScottPlot.Color.FromHex("#26a69a");
ohlcBars.FallingStyle.Color = ScottPlot.Color.FromHex("#ef5350");
 
plt.Title("ASML — OHLC Bars (Last 60 Trading Days)");
plt.XLabel("Date");
plt.YLabel("Price (EUR)");
plt.Axes.DateTimeTicksBottom();
ApplyAxisTheme(plt);
plt.Axes.Color(NotebookText);
 
ShowPlot(plt);
15-Dec-25 29-Dec-25 12-Jan-26 26-Jan-26 09-Feb-26 23-Feb-26 09-Mar-26 Price (EUR) 850 900 950 1'000 1'050 1'100 1'150 1'200 1'250 1'300 ASML — OHLC Bars (Last 60 Trading Days)

ScottPlot | Candlestick + SMA(20) and SMA(50) overlays

This section introduces the runnable chart cell and its supporting setup.

Overlay short and long moving averages on the candlestick chart.

// Candlestick + SMA(20) and SMA(50) moving average overlays
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var candle = plt.Add.Candlestick(ohlcs);
candle.RisingColor = ScottPlot.Color.FromHex("#26a69a");
candle.FallingColor = ScottPlot.Color.FromHex("#ef5350");
 
// Compute SMAs over the 120-day context window so the visible 60-day chart has full lines
double[] ComputeSMA(double[] data, int period)
{
    var sma = new double[data.Length];
    for (int i = 0; i < data.Length; i++)
    {
        if (i < period - 1) { sma[i] = double.NaN; continue; }
        double sum = 0;
        for (int j = i - period + 1; j <= i; j++) sum += data[j];
        sma[i] = sum / period;
    }
    return sma;
}
 
var sma20 = ComputeSMA(calcClose, 20);
var sma50 = ComputeSMA(calcClose, 50);
var calcOADates = calcDates.Select(d => d.ToOADate()).ToArray();
 
// Keep only points in the visible 60-day window
var sma20Valid = Enumerable.Range(displayOffset, displayDays).Where(i => !double.IsNaN(sma20[i])).ToArray();
var sma20Line = plt.Add.Scatter(
    sma20Valid.Select(i => calcOADates[i]).ToArray(),
    sma20Valid.Select(i => sma20[i]).ToArray());
sma20Line.LineWidth = 2f;
sma20Line.MarkerSize = 0;
sma20Line.Color = ScottPlot.Color.FromHex("#ffa726");
sma20Line.LegendText = "SMA(20)";
 
var sma50Valid = Enumerable.Range(displayOffset, displayDays).Where(i => !double.IsNaN(sma50[i])).ToArray();
var sma50Line = plt.Add.Scatter(
    sma50Valid.Select(i => calcOADates[i]).ToArray(),
    sma50Valid.Select(i => sma50[i]).ToArray());
sma50Line.LineWidth = 2f;
sma50Line.MarkerSize = 0;
sma50Line.Color = ScottPlot.Color.FromHex("#ab47bc");
sma50Line.LegendText = "SMA(50)";
 
plt.Title("ASML — Candlestick + Moving Averages");
plt.XLabel("Date");
plt.YLabel("Price (EUR)");
plt.Axes.DateTimeTicksBottom();
ApplyAxisTheme(plt);
plt.Axes.Color(NotebookText);
plt.ShowLegend(Alignment.UpperLeft);
 
ShowPlot(plt);
SMA(20) SMA(50) 15-Dec-25 29-Dec-25 12-Jan-26 26-Jan-26 09-Feb-26 23-Feb-26 09-Mar-26 Price (EUR) 850 900 950 1'000 1'050 1'100 1'150 1'200 1'250 1'300 ASML — Candlestick + Moving Averages

ScottPlot | Candlestick + Bollinger Bands with fill shading

This section introduces the runnable chart cell and its supporting setup.

Add Bollinger Bands to the candlestick chart with filled envelopes.

// Candlestick + Bollinger Bands (20-day, 2 std dev) with shaded fill
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var candle = plt.Add.Candlestick(ohlcs);
candle.RisingColor = ScottPlot.Color.FromHex("#26a69a");
candle.FallingColor = ScottPlot.Color.FromHex("#ef5350");
 
// Compute Bollinger Bands over the 120-day context window
int bbPeriod = 20;
var bbMid = new double[calcDays];
var bbUpper = new double[calcDays];
var bbLower = new double[calcDays];
for (int i = 0; i < calcDays; i++)
{
    if (i < bbPeriod - 1) { bbMid[i] = bbUpper[i] = bbLower[i] = double.NaN; continue; }
    var window = calcClose.Skip(i - bbPeriod + 1).Take(bbPeriod).ToArray();
    double mean = window.Average();
    double stdDev = Math.Sqrt(window.Select(v => (v - mean) * (v - mean)).Average());
    bbMid[i] = mean;
    bbUpper[i] = mean + 2 * stdDev;
    bbLower[i] = mean - 2 * stdDev;
}
 
var calcOADates = calcDates.Select(d => d.ToOADate()).ToArray();
var validIdx = Enumerable.Range(displayOffset, displayDays).Where(i => !double.IsNaN(bbMid[i])).ToArray();
var vDates = validIdx.Select(i => calcOADates[i]).ToArray();
var vMid = validIdx.Select(i => bbMid[i]).ToArray();
var vUpper = validIdx.Select(i => bbUpper[i]).ToArray();
var vLower = validIdx.Select(i => bbLower[i]).ToArray();
 
// Fill between upper and lower bands
var fill = plt.Add.FillY(vDates, vUpper, vLower);
fill.FillColor = ScottPlot.Color.FromHex("#42a5f5").WithAlpha(40);
fill.LineColor = ScottPlot.Colors.Transparent;
 
// Band lines
var midLine = plt.Add.Scatter(vDates, vMid);
midLine.LineWidth = 1.5f; midLine.MarkerSize = 0;
midLine.Color = ScottPlot.Color.FromHex("#42a5f5");
midLine.LegendText = "SMA(20)";
 
var upperLine = plt.Add.Scatter(vDates, vUpper);
upperLine.LineWidth = 1f; upperLine.MarkerSize = 0;
upperLine.Color = ScottPlot.Color.FromHex("#42a5f5").WithAlpha(150);
upperLine.LinePattern = ScottPlot.LinePattern.Dashed;
upperLine.LegendText = "Upper Band (+2σ)";
 
var lowerLine = plt.Add.Scatter(vDates, vLower);
lowerLine.LineWidth = 1f; lowerLine.MarkerSize = 0;
lowerLine.Color = ScottPlot.Color.FromHex("#42a5f5").WithAlpha(150);
lowerLine.LinePattern = ScottPlot.LinePattern.Dashed;
lowerLine.LegendText = "Lower Band (−2σ)";
 
plt.Title("ASML — Candlestick + Bollinger Bands");
plt.XLabel("Date");
plt.YLabel("Price (EUR)");
plt.Axes.DateTimeTicksBottom();
ApplyAxisTheme(plt);
plt.Axes.Color(NotebookText);
plt.ShowLegend(Alignment.UpperLeft);
 
ShowPlot(plt);
SMA(20) Upper Band (+2σ) 15-Dec-25 29-Dec-25 12-Jan-26 26-Jan-26 09-Feb-26 23-Feb-26 09-Mar-26 Price (EUR) 800 850 900 950 1'000 1'050 1'100 1'150 1'200 1'250 1'300 1'350 ASML — Candlestick + Bollinger Bands

Scatter & Bubble

Scatter-style ScottPlot charts cover point clouds, per-point coloring, and market-cap-weighted snapshots. These examples use direct coordinate arrays rather than Plotly traces, which keeps the rendering path lightweight.

ScottPlot | Scatter — ASML close vs volume with marker styling

This section introduces the runnable chart cell and its supporting setup.

Plot ASML close price against volume as a styled static scatter chart.

// Scatter — close price vs volume
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var volMil = asmlVol.Select(v => (double)v / 1e6).ToArray();
var scatter = plt.Add.Scatter(volMil, asmlClose);
scatter.LineWidth = 0;
scatter.MarkerSize = 4;
scatter.MarkerShape = ScottPlot.MarkerShape.FilledCircle;
scatter.Color = ScottPlot.Color.FromHex("#42a5f5").WithAlpha(120);
 
plt.Title("ASML — Close Price vs Volume");
plt.XLabel("Volume (M)");
plt.YLabel("Close (EUR)");
 
ShowPlot(plt);
Volume (M) 0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 Close (EUR) 400 500 600 700 800 900 1'000 1'100 1'200 1'300 ASML — Close Price vs Volume

ScottPlot | Color-mapped scatter — points colored by daily return magnitude

This section introduces the runnable chart cell and its supporting setup.

Color daily-return scatter points by return magnitude for fast outlier reading.

// Color-mapped scatter — each point colored by its daily return (diverging palette)
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
// Use close[1:] and returns aligned
var scatterDates = asmlOADates.Skip(1).ToArray();
var scatterClose = asmlClose.Skip(1).ToArray();
 
// Map returns to colors: green for positive, red for negative, intensity by magnitude
double maxAbsReturn = asmlReturns.Select(Math.Abs).Max();
var coords = new ScottPlot.Coordinates[asmlReturns.Length];
var scatterColors = new ScottPlot.Color[asmlReturns.Length];
for (int i = 0; i < asmlReturns.Length; i++)
{
    coords[i] = new ScottPlot.Coordinates(scatterDates[i], scatterClose[i]);
    double norm = Math.Min(Math.Abs(asmlReturns[i]) / maxAbsReturn, 1.0);
    byte alpha = (byte)(80 + norm * 175);
    scatterColors[i] = asmlReturns[i] >= 0
        ? ScottPlot.Color.FromHex("#26a69a").WithAlpha(alpha)
        : ScottPlot.Color.FromHex("#ef5350").WithAlpha(alpha);
}
 
// Add individual points with per-point colors
for (int i = 0; i < coords.Length; i++)
{
    var pt = plt.Add.Scatter(new[] { coords[i].X }, new[] { coords[i].Y });
    pt.LineWidth = 0;
    pt.MarkerSize = 4;
    pt.Color = scatterColors[i];
}
 
plt.Title("ASML — Close Price Colored by Daily Return (Green=Up, Red=Down)");
plt.XLabel("Date");
plt.YLabel("Close (EUR)");
plt.Axes.DateTimeTicksBottom();
ApplyAxisTheme(plt);
 
ShowPlot(plt);
2021 2022 2023 2024 2025 2026 Close (EUR) 400 500 600 700 800 900 1'000 1'100 1'200 1'300 ASML — Close Price Colored by Daily Return (Green=Up, Red=Down)

ScottPlot | Bubble — forward PE vs dividend yield, size = market cap

This section introduces the runnable chart cell and its supporting setup.

Use bubble size to encode market cap while comparing valuation and yield.

// Bubble chart — latest snapshot by symbol: forward PE vs dividend yield, bubble size = market cap
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var euSignals = dfSignals.Filter(Col("_index") == Lit("euro_stoxx_50")).Sort("signal_date");
var sigSymbols = euSignals.Column("symbol").ToArray<string>();
var sigPE = euSignals.Column("forward_pe").ToArray<double>();
var sigYield = euSignals.Column("dividend_yield").ToArray<double>();
var sigMcap = euSignals.Column("market_cap").ToArray<double>();
 
var latestRows = new List<(string Symbol, double PE, double Yield, double Mcap)>();
var seenSymbols = new HashSet<string>();
for (int i = sigSymbols.Length - 1; i >= 0; i--)
{
    if (seenSymbols.Contains(sigSymbols[i])) continue;
    if (double.IsNaN(sigPE[i]) || double.IsNaN(sigYield[i]) || double.IsNaN(sigMcap[i])) continue;
    if (sigPE[i] <= 0 || sigMcap[i] <= 0) continue;
 
    seenSymbols.Add(sigSymbols[i]);
    latestRows.Add((sigSymbols[i], sigPE[i], sigYield[i], sigMcap[i]));
}
 
double Median(double[] values)
{
    var sorted = values.OrderBy(v => v).ToArray();
    return sorted.Length % 2 == 0
        ? (sorted[sorted.Length / 2 - 1] + sorted[sorted.Length / 2]) / 2.0
        : sorted[sorted.Length / 2];
}
 
double maxMcap = latestRows.Max(r => r.Mcap);
double medianPe = Median(latestRows.Select(r => r.PE).ToArray());
double medianYield = Median(latestRows.Select(r => r.Yield).ToArray());
 
var labelSymbols = latestRows.OrderByDescending(r => r.Mcap).Take(10)
    .Concat(latestRows.OrderByDescending(r => r.Yield).Take(3))
    .Concat(latestRows.OrderBy(r => r.PE).Take(3))
    .Concat(latestRows.OrderByDescending(r => r.PE).Take(3))
    .Select(r => r.Symbol)
    .Distinct()
    .ToHashSet();
 
var medianPeLine = plt.Add.VerticalLine(medianPe, 1, NotebookGrid, ScottPlot.LinePattern.Dashed);
medianPeLine.LineColor = NotebookGrid;
medianPeLine.LineWidth = 1;
var medianYieldLine = plt.Add.HorizontalLine(medianYield, 1, NotebookGrid, ScottPlot.LinePattern.Dashed);
medianYieldLine.LineColor = NotebookGrid;
medianYieldLine.LineWidth = 1;
 
var unlabeledRows = latestRows.Where(r => !labelSymbols.Contains(r.Symbol)).OrderBy(r => r.Mcap).ToArray();
foreach (var row in unlabeledRows)
{
    float markerSize = (float)(7 + 13 * Math.Sqrt(row.Mcap / maxMcap));
    var pt = plt.Add.Scatter(new[] { row.PE }, new[] { row.Yield });
    pt.LineWidth = 0;
    pt.MarkerShape = MarkerShape.FilledCircle;
    pt.MarkerSize = markerSize;
    pt.MarkerFillColor = ScottPlot.Color.FromHex("#94a3b8").WithAlpha(110);
    pt.MarkerLineColor = ScottPlot.Color.FromHex("#cbd5e1").WithAlpha(140);
    pt.MarkerLineWidth = 1f;
}
 
var labeledRows = latestRows.Where(r => labelSymbols.Contains(r.Symbol)).OrderBy(r => r.Mcap).ToArray();
int labelIndex = 0;
foreach (var row in labeledRows)
{
    float markerSize = (float)(7 + 13 * Math.Sqrt(row.Mcap / maxMcap));
    var pt = plt.Add.Scatter(new[] { row.PE }, new[] { row.Yield });
    pt.LineWidth = 0;
    pt.MarkerShape = MarkerShape.FilledCircle;
    pt.MarkerSize = markerSize;
    pt.MarkerFillColor = ScottPlot.Color.FromHex("#42a5f5").WithAlpha(190);
    pt.MarkerLineColor = NotebookText;
    pt.MarkerLineWidth = 1.5f;
 
    bool placeRight = labelIndex % 2 == 0;
    float xOffset = markerSize / 2 + 4;
    var txt = plt.Add.Text(row.Symbol, row.PE, row.Yield);
    txt.LabelFontSize = 10;
    txt.LabelFontColor = NotebookText;
    txt.LabelAlignment = placeRight ? Alignment.MiddleLeft : Alignment.MiddleRight;
    txt.LabelOffsetX = placeRight ? xOffset : -xOffset;
    txt.LabelOffsetY = ((labelIndex % 4) - 1.5f) * 8;
    labelIndex++;
}
 
plt.Title("Euro Stoxx 50 — Forward PE vs Dividend Yield (Latest Snapshot, Size = Market Cap)");
plt.XLabel("Forward P/E");
plt.YLabel("Dividend Yield (%)");
plt.Axes.Margins(0.08, 0.12);
 
ShowPlot(plt, 900, 550);
VOW.DE MBG.DE RACE.MI ISP.MI BNP.PA SU.PA TTE.PA DTE.DE ITX.MC SIE.DE OR.PA SAP.DE RMS.PA MC.PA ASML.AS Forward P/E 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 Dividend Yield (%) 0 0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09 0.1 0.11 Euro Stoxx 50 — Forward PE vs Dividend Yield (Latest Snapshot, Size = Market Cap)

Distribution & Statistical

ScottPlot also handles statistical summaries such as histograms, box plots, error bars, and filled channels. These are useful when the goal is explanatory static output rather than interactive exploration.

ScottPlot | Histogram — ASML daily returns distribution

This section introduces the runnable chart cell and its supporting setup.

Summarize ASML daily return shape with a static histogram.

// Histogram — daily returns distribution with 50 bins
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
int binCount = 50;
double minRet = asmlReturns.Min();
double maxRet = asmlReturns.Max();
double binWidth = (maxRet - minRet) / binCount;
 
var counts = new int[binCount];
foreach (var r in asmlReturns)
{
    int bin = (int)((r - minRet) / binWidth);
    if (bin >= binCount) bin = binCount - 1;
    counts[bin]++;
}
 
var bars = new List<ScottPlot.Bar>();
for (int i = 0; i < binCount; i++)
{
    double center = minRet + (i + 0.5) * binWidth;
    var color = center >= 0
        ? ScottPlot.Color.FromHex("#26a69a").WithAlpha(200)
        : ScottPlot.Color.FromHex("#ef5350").WithAlpha(200);
    bars.Add(new ScottPlot.Bar
    {
        Position = center,
        Value = counts[i],
        Size = binWidth * 0.95,
        FillColor = color
    });
}
 
plt.Add.Bars(bars.ToArray());
 
plt.Title("ASML — Daily Returns Distribution (%)");
plt.XLabel("Daily Return (%)");
plt.YLabel("Frequency");
 
ShowPlot(plt);
Daily Return (%) -16 -14 -12 -10 -8 -6 -4 -2 0 2 4 6 8 10 Frequency 0 20 40 60 80 100 120 140 160 ASML — Daily Returns Distribution (%)

ScottPlot | Box & whisker — close price distribution for top 5 symbols

This section introduces the runnable chart cell and its supporting setup.

Compare close-price spread across the top 5 symbols with a box plot.

// Box & whisker — close price spread for top 5 symbols
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var boxColors = new[] { "#42a5f5", "#66bb6a", "#ef5350", "#ab47bc", "#ffa726" };
for (int i = 0; i < top5Symbols.Length; i++)
{
    var closes = dfEU.Filter(Col("symbol") == Lit(top5Symbols[i])).Column("close").ToArray<double>();
    Array.Sort(closes);
    int n = closes.Length;
    double q1 = closes[n / 4];
    double median = closes[n / 2];
    double q3 = closes[3 * n / 4];
    double iqr = q3 - q1;
    double whiskerLow = closes.First(v => v >= q1 - 1.5 * iqr);
    double whiskerHigh = closes.Last(v => v <= q3 + 1.5 * iqr);
 
    var box = plt.Add.Box(new ScottPlot.Box
    {
        Position = i,
        BoxMin = q1,
        BoxMax = q3,
        WhiskerMin = whiskerLow,
        WhiskerMax = whiskerHigh,
        BoxMiddle = median,
        FillColor = ScottPlot.Color.FromHex(boxColors[i]).WithAlpha(180),
        LineColor = NotebookText
    });
}
 
plt.Axes.Bottom.SetTicks(
    Enumerable.Range(0, top5Symbols.Length).Select(i => (double)i).ToArray(),
    top5Symbols);
 
plt.Title("Close Price Distribution — Top 5 Symbols");
plt.YLabel("Close (EUR)");
 
ShowPlot(plt);
RMS.PA ADYEN.AS ASML.AS MC.PA RHM.DE Close (EUR) 0 500 1'000 1'500 2'000 2'500 3'000 Close Price Distribution — Top 5 Symbols

ScottPlot | Error bars — mean ± std close price for top 5 symbols

This section introduces the runnable chart cell and its supporting setup.

Show mean close price with standard-deviation error bars for the top 5 symbols.

// Error bars — mean ± standard deviation for top 5 symbols
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var means = new double[top5Symbols.Length];
var stds = new double[top5Symbols.Length];
var positions = Enumerable.Range(0, top5Symbols.Length).Select(i => (double)i).ToArray();
 
for (int i = 0; i < top5Symbols.Length; i++)
{
    var closes = dfEU.Filter(Col("symbol") == Lit(top5Symbols[i])).Column("close").ToArray<double>();
    means[i] = closes.Average();
    stds[i] = Math.Sqrt(closes.Select(v => (v - means[i]) * (v - means[i])).Average());
}
 
// Point-range chart for mean and standard deviation
var errorBars = plt.Add.ErrorBar(positions, means, stds);
errorBars.LineColor = ScottPlot.Color.FromHex("#93c5fd").WithAlpha(170);
errorBars.LineWidth = 2f;
errorBars.CapSize = 10;
 
var meanPoints = plt.Add.ScatterPoints(positions, means, ScottPlot.Color.FromHex("#42a5f5"));
meanPoints.MarkerSize = 12;
meanPoints.MarkerFillColor = ScottPlot.Color.FromHex("#42a5f5");
meanPoints.MarkerLineColor = NotebookText;
meanPoints.MarkerLineWidth = 2f;
 
plt.Axes.Bottom.SetTicks(
    Enumerable.Range(0, top5Symbols.Length).Select(i => (double)i).ToArray(),
    top5Symbols);
 
plt.Title("Top 5 — Mean ± Std Close Price");
plt.YLabel("Close (EUR)");
plt.Axes.Margins(bottom: 0.1, top: 0.15);
 
ShowPlot(plt);
RMS.PA ADYEN.AS ASML.AS MC.PA RHM.DE Close (EUR) -200 0 200 400 600 800 1'000 1'200 1'400 1'600 1'800 2'000 2'200 2'400 Top 5 — Mean ± Std Close Price

ScottPlot | Fill between — ASML high/low price channel

This section introduces the runnable chart cell and its supporting setup.

Draw the ASML high/low channel as a filled band around price.

// Fill between — high/low price channel with close midline (last 60 trading days)
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var fill = plt.Add.FillY(asml60OADates, asml60High, asml60Low);
fill.FillColor = ScottPlot.Color.FromHex("#42a5f5").WithAlpha(40);
fill.LineColor = ScottPlot.Colors.Transparent;
fill.LegendText = "High-Low Range";
 
var highLine = plt.Add.Scatter(asml60OADates, asml60High);
highLine.LineWidth = 0.5f; highLine.MarkerSize = 0;
highLine.Color = ScottPlot.Color.FromHex("#42a5f5").WithAlpha(100);
 
var lowLine = plt.Add.Scatter(asml60OADates, asml60Low);
lowLine.LineWidth = 0.5f; lowLine.MarkerSize = 0;
lowLine.Color = ScottPlot.Color.FromHex("#42a5f5").WithAlpha(100);
 
var closeLine = plt.Add.Scatter(asml60OADates, asml60Close);
closeLine.LineWidth = 1.5f; closeLine.MarkerSize = 0;
closeLine.Color = ScottPlot.Color.FromHex("#ffa726");
closeLine.LegendText = "Close";
 
plt.Title("ASML — High/Low Price Channel (Last 60 Trading Days)");
plt.XLabel("Date");
plt.YLabel("Price (EUR)");
plt.Axes.DateTimeTicksBottom();
ApplyAxisTheme(plt);
plt.ShowLegend(Alignment.UpperLeft);
 
ShowPlot(plt);
High-Low Range Close 15-Dec-25 29-Dec-25 12-Jan-26 26-Jan-26 09-Feb-26 23-Feb-26 09-Mar-26 Price (EUR) 850 900 950 1'000 1'050 1'100 1'150 1'200 1'250 1'300 ASML — High/Low Price Channel (Last 60 Trading Days)

Heatmap & Specialty

The final ScottPlot group moves beyond basic Cartesian charts into heatmaps, radial views, coxcomb slices, and a hand-composed 2×2 dashboard panel rendered through SkiaSharp to a single inline SVG.

ScottPlot | Pie chart — volume share by top 8 symbols

This section introduces the runnable chart cell and its supporting setup.

Show top-8 volume share as a static pie chart.

// Pie chart — volume share by top 8 EU symbols
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var pieColors = new[] { "#42a5f5", "#66bb6a", "#ef5350", "#ab47bc", "#ffa726", "#26c6da", "#ec407a", "#78909c" };
var totalTop8Volume = top8Volumes.Sum();
var slices = new List<ScottPlot.PieSlice>();
for (int i = 0; i < top8Symbols.Length; i++)
{
    slices.Add(new ScottPlot.PieSlice
    {
        Value = top8Volumes[i],
        Label = $"{top8Symbols[i]}\n{top8Volumes[i] / totalTop8Volume:P1}",
        FillColor = ScottPlot.Color.FromHex(pieColors[i]),
        LabelFontColor = NotebookText,
        LabelFontSize = 11,
        LabelBold = true,
        LabelBackgroundColor = ScottPlot.Colors.Transparent,
        LabelBorderWidth = 0
    });
}
 
var pie = plt.Add.Pie(slices);
pie.SliceLabelDistance = 1.28;
 
plt.Title("Euro Stoxx 50 — Volume Share (Top 8)");
plt.Axes.Frameless();
plt.HideGrid();
 
ShowPlot(plt, 760, 600);
ISP.MI 39.3% SAN.MC 18.9% ENEL.MI 11.1% BBVA.MC 7.5% UCG.MI 6.2% ENI.MI 5.8% INGA.AS 5.8% IBE.MC 5.4%

ScottPlot | Coxcomb — sector composite scores

This section introduces the runnable chart cell and its supporting setup.

Compare sector composite scores with a radial coxcomb chart.

// Coxcomb (rose chart) — latest sector composite snapshot
var plt = new ScottPlot.Plot();
DarkTheme(plt);
 
var euScoresAll = dfScores
    .Filter(Col("_index") == Lit("euro_stoxx_50"))
    .Sort("score_date", descending: true);
 
var latestSymbols = euScoresAll.Column("symbol").ToArray<string>();
var latestSectors = euScoresAll.Column("sector").ToArray<string>();
var latestScores = euScoresAll.Column("composite_score").ToArray<double>();
var latestDates = euScoresAll.Column("score_date").ToArray<DateTime>();
 
var seenSymbols = new HashSet<string>();
var latestRows = new List<(string Symbol, string Sector, double Score, DateTime ScoreDate)>();
for (int i = 0; i < latestSymbols.Length; i++)
{
    if (!seenSymbols.Add(latestSymbols[i]))
        continue;
 
    latestRows.Add((latestSymbols[i], latestSectors[i], latestScores[i], latestDates[i]));
}
 
var latestScoreDate = latestRows.Max(x => x.ScoreDate);
var sectorSnapshot = latestRows
    .GroupBy(x => x.Sector)
    .Select(g => new
    {
        Sector = g.Key,
        Avg = g.Average(x => x.Score),
        Count = g.Count()
    })
    .OrderByDescending(x => x.Avg)
    .ToArray();
 
var minAvg = sectorSnapshot.Min(x => x.Avg);
var maxAvg = sectorSnapshot.Max(x => x.Avg);
var span = Math.Max(maxAvg - minAvg, 1e-9);
 
double RelativeRadius(double avg) => 0.42 + 0.58 * ((avg - minAvg) / span);
var coxColors = new[]
{
    "#42a5f5", "#66bb6a", "#ffa726", "#ab47bc", "#ef5350",
    "#26c6da", "#ec407a", "#9ccc65", "#8d6e63", "#78909c"
};
 
var slices = new List<ScottPlot.PieSlice>();
for (int i = 0; i < sectorSnapshot.Length; i++)
{
    var row = sectorSnapshot[i];
    slices.Add(new ScottPlot.PieSlice
    {
        Value = RelativeRadius(row.Avg),
        Label = string.Empty,
        FillColor = ScottPlot.Color.FromHex(coxColors[i % coxColors.Length]).WithAlpha(195)
    });
}
 
var coxcomb = plt.Add.Coxcomb(slices);
coxcomb.LineColor = NotebookText.WithAlpha(90);
coxcomb.LineWidth = 1f;
coxcomb.Padding = 0;
 
plt.Title($"Euro Stoxx 50 — Sector Composite Snapshot ({latestScoreDate:yyyy-MM-dd})");
plt.Axes.Frameless();
plt.HideGrid();
 
var chartSize = FitSize(500, 500);
var svg = PrepareSvg(plt.GetSvgHtml(chartSize.Width, chartSize.Height), chartSize.Width);
var legendItems = string.Join("", sectorSnapshot.Select((row, i) =>
    $"<div style='display:flex;align-items:flex-start;gap:10px;margin:0 0 10px 0;color:#fff;font:600 13px Segoe UI,sans-serif;line-height:1.25'>"
  + $"<span style='display:inline-block;width:12px;height:12px;border-radius:999px;background:{coxColors[i % coxColors.Length]};margin-top:2px;flex:0 0 12px'></span>"
  + $"<span>{row.Sector}<br><span style='font-weight:500;opacity:.85'>{row.Avg:+0.00;-0.00}</span></span>"
  + "</div>"));
 
display(HTML(
    $"<div style='max-width:760px;width:100%;margin:0 auto;display:grid;grid-template-columns:minmax(0,{chartSize.Width}px) 220px;column-gap:16px;align-items:center;justify-content:center'>"
  + $"<div style='min-width:0'>{svg}</div>"
  + $"<div style='width:220px;box-sizing:border-box;padding:14px 16px;border:1px solid rgba(255,255,255,.14);border-radius:10px;background:rgba(255,255,255,.03)'>{legendItems}</div>"
  + "</div>"));
Communication Services
+0.49
Energy
+0.33
Healthcare
+0.08
Technology
+0.05
Industrials
+0.05
Financial Services
-0.01
Basic Materials
-0.01
Consumer Defensive
-0.06
Consumer Cyclical
-0.10
Utilities
-0.10

ScottPlot | Radial gauge — YTD performance for top 5 symbols

This section introduces the runnable chart cell and its supporting setup.

Summarize YTD return for the top 5 symbols with a radial gauge.

// Radial gauge — YTD returns for top 5 symbols from scores_daily
var plt = new ScottPlot.Plot();
DarkTheme(plt);
var ytdSymbols = new List<string>();
var ytdValues = new List<double>();
var gaugeColors = new[] { "#42a5f5", "#66bb6a", "#ef5350", "#ab47bc", "#ffa726" };
foreach (var sym in top5Symbols)
{
    var row = dfScores.Filter(Col("symbol") == Lit(sym) & Col("_index") == Lit("euro_stoxx_50"));
    if (row.Shape.Len > 0)
    {
        var ytd = row.Column("ytd_change_pct").ToArray<double>();
        ytdSymbols.Add(sym);
        ytdValues.Add(ytd[0] * 100.0);
    }
}
var gauge = plt.Add.RadialGaugePlot(ytdValues.ToArray());
gauge.Labels = ytdSymbols.ToArray();
for (int i = 0; i < ytdValues.Count; i++)
    gauge.Colors[i] = ScottPlot.Color.FromHex(gaugeColors[i]);
plt.Title("YTD Return (%) — Top 5 Symbols (Radial Gauge)");
plt.Axes.Frameless();
plt.HideGrid();
plt.ShowLegend(Alignment.LowerRight);
ShowPlot(plt, 700, 550);
RMS.PA ADYEN.AS ASML.AS MC.PA RHM.DE

ScottPlot | Multi-plot 2×2 panel — ASML overview dashboard

This section introduces the runnable chart cell and its supporting setup.

Combine price, volume, returns, and distribution panels into one static dashboard.

// Multi-plot 2×2 panel — ASML overview: price, volume, returns histogram, daily returns
// ScottPlot 5 Multiplot: render 4 separate plots into one inline SVG via SkiaSharp
int panelW = 330, panelH = 245;
int outerPad = 18, gutterX = 24, gutterY = 28;
int totalW = outerPad * 2 + panelW * 2 + gutterX;
int totalH = outerPad * 2 + panelH * 2 + gutterY;
var svgStream = new MemoryStream();
var canvas = SkiaSharp.SKSvgCanvas.Create(SkiaSharp.SKRect.Create(totalW, totalH), svgStream);
canvas.Clear(SkiaSharp.SKColors.Transparent);
 
string MakeCompositeSvgTransparent(string svg)
{
    var lines = svg.Split('\n');
    for (int i = 0; i < lines.Length; i++)
    {
        if (lines[i].Contains("<rect") && lines[i].Contains("transform=") && !lines[i].Contains("fill=") && !lines[i].Contains("stroke="))
            lines[i] = lines[i].Replace("/>", " fill=\"white\" fill-opacity=\"0\"/>");
    }
 
    return string.Join("\n", lines);
}
 
// Helper to render one ScottPlot into a region
void RenderPanel(ScottPlot.Plot p, int x, int y)
{
    DarkTheme(p);
    p.FigureBackground.Color = ScottPlot.Color.FromARGB(0);
    p.DataBackground.Color = ScottPlot.Color.FromARGB(0);
    ApplyAxisTheme(p);
    canvas.Save();
    canvas.Translate(x, y);
    p.Render(canvas, panelW, panelH);
    canvas.Restore();
}
 
// Panel 1: Close price (last 60 trading days)
var p1 = new ScottPlot.Plot();
var line1 = p1.Add.Scatter(asml60OADates, asml60Close);
line1.LineWidth = 1.5f; line1.MarkerSize = 0; line1.Color = ScottPlot.Color.FromHex("#42a5f5");
p1.Title("Close Price (60D)"); p1.Axes.DateTimeTicksBottom();
RenderPanel(p1, outerPad, outerPad);
 
// Panel 2: Volume (last 60 trading days)
var p2 = new ScottPlot.Plot();
var volD = asml60Vol.Select(v => (double)v / 1e6).ToArray();
var volBars2 = p2.Add.Bars(asml60OADates, volD);
volBars2.Color = ScottPlot.Color.FromHex("#9ca3af").WithAlpha(180);
p2.Title("Volume (M, 60D)"); p2.Axes.DateTimeTicksBottom();
RenderPanel(p2, outerPad + panelW + gutterX, outerPad);
 
// Panel 3: Returns histogram (last 60 trading days)
var p3 = new ScottPlot.Plot();
var histBars = new List<ScottPlot.Bar>();
int nb = 30;
double bw = (asml60Returns.Max() - asml60Returns.Min()) / nb;
var cts = new int[nb];
foreach (var r in asml60Returns) { int b = Math.Min((int)((r - asml60Returns.Min()) / bw), nb - 1); cts[b]++; }
for (int i = 0; i < nb; i++)
    histBars.Add(new ScottPlot.Bar { Position = asml60Returns.Min() + (i + 0.5) * bw, Value = cts[i], Size = bw * 0.9,
        FillColor = ScottPlot.Color.FromHex("#9ca3af").WithAlpha(180) });
p3.Add.Bars(histBars.ToArray());
p3.Title("Returns Distribution (60D)");
RenderPanel(p3, outerPad, outerPad + panelH + gutterY);
 
// Panel 4: Daily returns time series (last 60 trading days)
var p4 = new ScottPlot.Plot();
var retLine = p4.Add.Scatter(asml60ReturnDates, asml60Returns);
retLine.LineWidth = 0.5f; retLine.MarkerSize = 1; retLine.Color = ScottPlot.Color.FromHex("#ef5350");
p4.Title("Daily Returns (60D)"); p4.Axes.DateTimeTicksBottom();
RenderPanel(p4, outerPad + panelW + gutterX, outerPad + panelH + gutterY);
 
canvas.Flush();
canvas.Dispose();
 
// Emit composite panel as inline SVG
var compositeSvg = MakeCompositeSvgTransparent(Encoding.UTF8.GetString(svgStream.ToArray()));
DisplaySvg(compositeSvg, totalW);
svgStream.Dispose();

Composite dashboard rendered as a 2x2 HTML grid of ScottPlot SVG panels.

29-Dec-25 26-Jan-26 23-Feb-26 900 1'000 1'100 1'200 1'300 Close Price (60D)
29-Dec-25 26-Jan-26 23-Feb-26 0 0.5 1 Volume (M, 60D)
-4 -3 -2 -1 0 1 2 3 4 5 6 7 0 2 4 6 Returns Distribution (60D)
29-Dec-25 26-Jan-26 23-Feb-26 -4 -2 0 2 4 6 Daily Returns (60D)

Operational Risks

The practical failure modes here are chart/input mismatch, axis-scale distortion, HTML-versus-SVG confusion, and export-target drift. The note’s data-loading cells depend on homogeneous column types, so ToArray<T>() and date-axis casting should be checked before the chart primitive itself.

C# Visualization Recommendations

Use Plotly.NET when hover, zoom, and HTML export are part of the analysis workflow. Use ScottPlot when the destination is notebook-native SVG, batch image export, or any static report that should render without a browser runtime. Keep axis labels, theme settings, and helper functions explicit so repeated chart cells stay visually consistent.

C# Visualization Troubleshooting

If a chart renders incorrectly, check typed-array preparation, date coercion, and axis scaling before changing the plotting code. If an interactive figure does not display, verify that the destination supports HTML and that the output was not pasted as a static image. If the dashboard panel appears blank, rerun the plotting cell and confirm that the SVG or HTML output was captured instead of omitted during export.