OhlcSeries and ScatterLine demo added

This commit is contained in:
Vladimir Enchev
2026-03-13 11:49:02 +02:00
parent f1bde5d0bf
commit fd2d3b00d4
7 changed files with 493 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
@using Radzen.Blazor
@using Radzen.Blazor.Rendering
@typeparam TItem
@inherits Radzen.Blazor.CartesianSeries<TItem>
<CascadingValue Value="@this">
@ChildContent
</CascadingValue>
@code {
public override RenderFragment Render(ScaleBase categoryScale, ScaleBase valueScale)
{
var chart = RequireChart();
var category = ComposeCategory(categoryScale);
var style = $"clip-path: url(#{chart.ClipPath}); -webkit-clip-path: url(#{chart.ClipPath});";
var className = $"rz-ohlc-series rz-series-{chart.Series.IndexOf(this)}";
var tickWidth = BarTickWidth;
var open = Open;
var high = High;
var low = Low;
var close = Close;
return
@<g class="@className">
@foreach (var data in Items)
{
var cx = category(data);
var openValue = open(data);
var highValue = high(data);
var lowValue = low(data);
var closeValue = close(data);
var openY = valueScale.Scale(openValue, true);
var highY = valueScale.Scale(highValue, true);
var lowY = valueScale.Scale(lowValue, true);
var closeY = valueScale.Scale(closeValue, true);
var isBull = closeValue >= openValue;
var stroke = isBull ? BullStroke : BearStroke;
// Vertical line from High to Low
var verticalPath = $"M {cx.ToInvariantString()} {highY.ToInvariantString()} L {cx.ToInvariantString()} {lowY.ToInvariantString()}";
// Open tick (left side)
var openPath = $"M {(cx - tickWidth).ToInvariantString()} {openY.ToInvariantString()} L {cx.ToInvariantString()} {openY.ToInvariantString()}";
// Close tick (right side)
var closePath = $"M {cx.ToInvariantString()} {closeY.ToInvariantString()} L {(cx + tickWidth).ToInvariantString()} {closeY.ToInvariantString()}";
var index = Items.IndexOf(data);
<Path @key="@($"hl-{index}")" D="@verticalPath" Stroke="@stroke" StrokeWidth="@StrokeWidth" Fill="none" Style="@style" />
<Path @key="@($"o-{index}")" D="@openPath" Stroke="@stroke" StrokeWidth="@StrokeWidth" Fill="none" Style="@style" />
<Path @key="@($"c-{index}")" D="@closePath" Stroke="@stroke" StrokeWidth="@StrokeWidth" Fill="none" Style="@style" />
}
</g>;
}
}

View File

@@ -0,0 +1,292 @@
using Microsoft.AspNetCore.Components;
using Radzen.Blazor.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Radzen.Blazor
{
/// <summary>
/// A chart series that displays financial data as OHLC (Open-High-Low-Close) bars in a RadzenChart.
/// Each bar shows a vertical line from High to Low, with a left tick at the Open price and a right tick at the Close price.
/// Similar to <see cref="RadzenCandlestickSeries{TItem}"/> but uses tick marks instead of filled bodies.
/// </summary>
/// <typeparam name="TItem">The type of data items in the series. Each item represents one OHLC bar.</typeparam>
/// <example>
/// <code>
/// &lt;RadzenChart&gt;
/// &lt;RadzenOhlcSeries Data=@stockData CategoryProperty="Date"
/// OpenProperty="Open" HighProperty="High" LowProperty="Low" CloseProperty="Close"
/// Title="AAPL" /&gt;
/// &lt;RadzenCategoryAxis FormatString="{0:MM/dd}" /&gt;
/// &lt;/RadzenChart&gt;
/// </code>
/// </example>
public partial class RadzenOhlcSeries<TItem> : CartesianSeries<TItem>
{
/// <summary>
/// Gets or sets the name of the property of <typeparamref name="TItem"/> that provides the Open value.
/// </summary>
[Parameter]
public string? OpenProperty { get; set; }
/// <summary>
/// Gets or sets the name of the property of <typeparamref name="TItem"/> that provides the High value.
/// </summary>
[Parameter]
public string? HighProperty { get; set; }
/// <summary>
/// Gets or sets the name of the property of <typeparamref name="TItem"/> that provides the Low value.
/// </summary>
[Parameter]
public string? LowProperty { get; set; }
/// <summary>
/// Gets or sets the name of the property of <typeparamref name="TItem"/> that provides the Close value.
/// </summary>
[Parameter]
public string? CloseProperty { get; set; }
/// <summary>
/// Gets or sets the stroke color for bullish bars (Close &gt;= Open).
/// </summary>
/// <value>A CSS color value. Default is <c>#26A69A</c> (green).</value>
[Parameter]
public string BullStroke { get; set; } = "#26A69A";
/// <summary>
/// Gets or sets the stroke color for bearish bars (Close &lt; Open).
/// </summary>
/// <value>A CSS color value. Default is <c>#EF5350</c> (red).</value>
[Parameter]
public string BearStroke { get; set; } = "#EF5350";
/// <summary>
/// Gets or sets the width of the OHLC bar lines in pixels.
/// </summary>
/// <value>The stroke width in pixels. Default is 2.</value>
[Parameter]
public double StrokeWidth { get; set; } = 2;
/// <summary>
/// Gets or sets the width of the open/close tick marks in pixels.
/// If null, the width is calculated automatically based on the chart size and number of data points.
/// </summary>
[Parameter]
public double? TickWidth { get; set; }
/// <inheritdoc />
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>(nameof(CloseProperty), out var close) && close != CloseProperty)
{
ValueProperty = close;
}
await base.SetParametersAsync(parameters);
}
/// <inheritdoc />
public override string Color => BullStroke;
internal Func<TItem, double> Open
{
get
{
if (string.IsNullOrEmpty(OpenProperty))
{
throw new ArgumentException("OpenProperty should not be empty");
}
return PropertyAccess.Getter<TItem, double>(OpenProperty);
}
}
internal Func<TItem, double> High
{
get
{
if (string.IsNullOrEmpty(HighProperty))
{
throw new ArgumentException("HighProperty should not be empty");
}
return PropertyAccess.Getter<TItem, double>(HighProperty);
}
}
internal Func<TItem, double> Low
{
get
{
if (string.IsNullOrEmpty(LowProperty))
{
throw new ArgumentException("LowProperty should not be empty");
}
return PropertyAccess.Getter<TItem, double>(LowProperty);
}
}
internal Func<TItem, double> Close
{
get
{
if (string.IsNullOrEmpty(CloseProperty))
{
throw new ArgumentException("CloseProperty should not be empty");
}
return PropertyAccess.Getter<TItem, double>(CloseProperty);
}
}
/// <inheritdoc />
public override ScaleBase TransformValueScale(ScaleBase scale)
{
ArgumentNullException.ThrowIfNull(scale);
if (Items != null && Items.Any())
{
var high = High;
var low = Low;
var minValue = Items.Min(item => low(item));
var maxValue = Items.Max(item => high(item));
scale.Input.MergeWidth(new ScaleRange { Start = minValue, End = maxValue });
}
return scale;
}
internal double BarTickWidth
{
get
{
if (TickWidth.HasValue)
{
return TickWidth.Value;
}
var chart = RequireChart();
var availableWidth = chart.CategoryScale.OutputSize - (chart.CategoryAxis.Padding * 2);
var bands = Items.Count + 2;
return Math.Max(3, availableWidth / bands * 0.3);
}
}
private string FormatValue(double v)
{
var chart = RequireChart();
return chart.ValueAxis.Format(chart.ValueScale, chart.ValueScale.Value(v));
}
/// <inheritdoc />
protected override string TooltipValue(TItem item)
{
return FormatValue(Close(item));
}
/// <inheritdoc />
public override RenderFragment RenderTooltip(object data)
{
var chart = RequireChart();
var item = (TItem)data;
if (TooltipTemplate != null)
{
return base.RenderTooltip(data);
}
return builder =>
{
builder.OpenComponent<Rendering.ChartTooltip>(0);
builder.AddAttribute(1, nameof(Rendering.ChartTooltip.Class), TooltipClass(item));
builder.AddAttribute(2, nameof(Rendering.ChartTooltip.Style), TooltipStyle(item));
builder.AddAttribute(3, nameof(Rendering.ChartTooltip.ChildContent), (RenderFragment)(b =>
{
b.OpenElement(0, "div");
b.AddAttribute(1, "class", "rz-chart-tooltip-title");
b.AddContent(2, TooltipTitle(item));
b.CloseElement();
b.OpenElement(3, "div");
b.AddContent(4, $"Open: {FormatValue(Open(item))}");
b.CloseElement();
b.OpenElement(5, "div");
b.AddContent(6, $"High: {FormatValue(High(item))}");
b.CloseElement();
b.OpenElement(7, "div");
b.AddContent(8, $"Low: {FormatValue(Low(item))}");
b.CloseElement();
b.OpenElement(9, "div");
b.AddContent(10, $"Close: {FormatValue(Close(item))}");
b.CloseElement();
}));
builder.CloseComponent();
};
}
/// <inheritdoc />
protected override string TooltipStyle(TItem item)
{
var style = base.TooltipStyle(item);
var isBull = Close(item) >= Open(item);
var color = isBull ? BullStroke : BearStroke;
return $"{style}; border-color: {color};";
}
/// <inheritdoc />
internal override double TooltipY(TItem item)
{
var chart = RequireChart();
return chart.ValueScale.Scale(High(item), true);
}
/// <inheritdoc />
public override bool Contains(double x, double y, double tolerance)
{
return DataAt(x, y).Item1 != null;
}
/// <inheritdoc />
public override (object, Point) DataAt(double x, double y)
{
var chart = Chart;
if (chart == null || !Items.Any())
{
return (default!, new Point());
}
var category = ComposeCategory(chart.CategoryScale);
var tickWidth = BarTickWidth;
foreach (var data in Items)
{
var cx = category(data);
if (x >= cx - tickWidth && x <= cx + tickWidth)
{
var highY = chart.ValueScale.Scale(High(data), true);
var lowY = chart.ValueScale.Scale(Low(data), true);
var startY = Math.Min(highY, lowY);
var endY = Math.Max(highY, lowY);
if (y >= startY && y <= endY)
{
return (data!, new Point { X = cx, Y = y });
}
}
}
return (default!, new Point());
}
}
}

View File

@@ -0,0 +1,58 @@
@using System.Globalization
<RadzenStack class="rz-p-0 rz-p-md-6 rz-p-lg-12">
<RadzenRow>
<RadzenColumn Size="12">
<RadzenChart>
<RadzenOhlcSeries Data="@stockData" CategoryProperty="Date"
OpenProperty="Open" HighProperty="High" LowProperty="Low" CloseProperty="Close"
Title="ACME Corp" />
<RadzenCategoryAxis FormatString="{0:MM/dd}" />
<RadzenValueAxis Formatter="@FormatAsUSD">
<RadzenGridLines Visible="true" />
<RadzenAxisTitle Text="Price (USD)" />
</RadzenValueAxis>
</RadzenChart>
</RadzenColumn>
</RadzenRow>
</RadzenStack>
@code {
class StockDataItem
{
public DateTime Date { get; set; }
public double Open { get; set; }
public double High { get; set; }
public double Low { get; set; }
public double Close { get; set; }
}
string FormatAsUSD(object value)
{
return ((double)value).ToString("C0", CultureInfo.CreateSpecificCulture("en-US"));
}
StockDataItem[] stockData = new StockDataItem[]
{
new StockDataItem { Date = new DateTime(2024, 1, 2), Open = 170.0, High = 175.5, Low = 169.0, Close = 174.2 },
new StockDataItem { Date = new DateTime(2024, 1, 3), Open = 174.2, High = 176.8, Low = 172.1, Close = 173.5 },
new StockDataItem { Date = new DateTime(2024, 1, 4), Open = 173.5, High = 174.0, Low = 168.3, Close = 169.1 },
new StockDataItem { Date = new DateTime(2024, 1, 5), Open = 169.1, High = 171.2, Low = 166.5, Close = 167.8 },
new StockDataItem { Date = new DateTime(2024, 1, 8), Open = 168.0, High = 172.5, Low = 167.2, Close = 171.9 },
new StockDataItem { Date = new DateTime(2024, 1, 9), Open = 171.9, High = 175.3, Low = 171.0, Close = 174.8 },
new StockDataItem { Date = new DateTime(2024, 1, 10), Open = 174.8, High = 178.2, Low = 174.0, Close = 177.5 },
new StockDataItem { Date = new DateTime(2024, 1, 11), Open = 177.5, High = 179.0, Low = 175.3, Close = 176.1 },
new StockDataItem { Date = new DateTime(2024, 1, 12), Open = 176.1, High = 178.4, Low = 174.8, Close = 178.0 },
new StockDataItem { Date = new DateTime(2024, 1, 16), Open = 178.0, High = 180.5, Low = 176.2, Close = 179.3 },
new StockDataItem { Date = new DateTime(2024, 1, 17), Open = 179.3, High = 179.8, Low = 175.0, Close = 175.6 },
new StockDataItem { Date = new DateTime(2024, 1, 18), Open = 175.6, High = 177.4, Low = 173.9, Close = 176.8 },
new StockDataItem { Date = new DateTime(2024, 1, 19), Open = 176.8, High = 180.0, Low = 176.0, Close = 179.5 },
new StockDataItem { Date = new DateTime(2024, 1, 22), Open = 179.5, High = 183.2, Low = 179.0, Close = 182.7 },
new StockDataItem { Date = new DateTime(2024, 1, 23), Open = 182.7, High = 184.0, Low = 180.5, Close = 181.3 },
new StockDataItem { Date = new DateTime(2024, 1, 24), Open = 181.3, High = 182.8, Low = 178.6, Close = 179.0 },
new StockDataItem { Date = new DateTime(2024, 1, 25), Open = 179.0, High = 181.5, Low = 177.3, Close = 180.9 },
new StockDataItem { Date = new DateTime(2024, 1, 26), Open = 180.9, High = 185.0, Low = 180.2, Close = 184.3 },
new StockDataItem { Date = new DateTime(2024, 1, 29), Open = 184.3, High = 186.5, Low = 183.0, Close = 185.8 },
new StockDataItem { Date = new DateTime(2024, 1, 30), Open = 185.8, High = 186.2, Low = 182.4, Close = 183.1 },
};
}

View File

@@ -0,0 +1,12 @@
@page "/ohlc-chart"
<RadzenText TextStyle="TextStyle.H2" TagName="TagName.H1" class="rz-pt-8">
Radzen Blazor Chart OHLC series
</RadzenText>
<RadzenText TextStyle="TextStyle.Subtitle1" TagName="TagName.P" class="rz-pb-4">
Visualize financial Open-High-Low-Close data using OHLC bar charts.
</RadzenText>
<RadzenExample ComponentName="Chart" Example="OhlcChartConfig" DocumentationLink="https://blazor.radzen.com/docs/guides/components/chart.html">
<OhlcChartConfig />
</RadzenExample>

View File

@@ -0,0 +1,44 @@
<RadzenStack class="rz-p-0 rz-p-md-6 rz-p-lg-12">
<RadzenRow>
<RadzenColumn Size="12">
<RadzenChart>
<RadzenLineSeries Data="@temperatureData" CategoryProperty="Month" ValueProperty="AvgHigh" Title="Avg High" Stroke="rgb(239,83,80)" StrokeWidth="2">
<RadzenMarkers MarkerType="MarkerType.Circle" Size="6" Stroke="rgb(239,83,80)" Fill="white" StrokeWidth="2" />
</RadzenLineSeries>
<RadzenLineSeries Data="@temperatureData" CategoryProperty="Month" ValueProperty="AvgLow" Title="Avg Low" Stroke="rgb(38,166,154)" StrokeWidth="2">
<RadzenMarkers MarkerType="MarkerType.Square" Size="6" Stroke="rgb(38,166,154)" Fill="white" StrokeWidth="2" />
</RadzenLineSeries>
<RadzenCategoryAxis />
<RadzenValueAxis>
<RadzenGridLines Visible="true" />
<RadzenAxisTitle Text="Temperature (°C)" />
</RadzenValueAxis>
</RadzenChart>
</RadzenColumn>
</RadzenRow>
</RadzenStack>
@code {
class DataItem
{
public string Month { get; set; }
public double AvgHigh { get; set; }
public double AvgLow { get; set; }
}
DataItem[] temperatureData = new DataItem[]
{
new DataItem { Month = "Jan", AvgHigh = 6, AvgLow = -1 },
new DataItem { Month = "Feb", AvgHigh = 8, AvgLow = 0 },
new DataItem { Month = "Mar", AvgHigh = 12, AvgLow = 3 },
new DataItem { Month = "Apr", AvgHigh = 16, AvgLow = 6 },
new DataItem { Month = "May", AvgHigh = 21, AvgLow = 10 },
new DataItem { Month = "Jun", AvgHigh = 25, AvgLow = 14 },
new DataItem { Month = "Jul", AvgHigh = 28, AvgLow = 16 },
new DataItem { Month = "Aug", AvgHigh = 27, AvgLow = 15 },
new DataItem { Month = "Sep", AvgHigh = 23, AvgLow = 12 },
new DataItem { Month = "Oct", AvgHigh = 17, AvgLow = 8 },
new DataItem { Month = "Nov", AvgHigh = 11, AvgLow = 3 },
new DataItem { Month = "Dec", AvgHigh = 7, AvgLow = 0 },
};
}

View File

@@ -0,0 +1,12 @@
@page "/scatter-line-chart"
<RadzenText TextStyle="TextStyle.H2" TagName="TagName.H1" class="rz-pt-8">
Radzen Blazor Chart scatter line series
</RadzenText>
<RadzenText TextStyle="TextStyle.Subtitle1" TagName="TagName.P" class="rz-pb-4">
Combine line series with markers to create scatter line charts that show both data points and connecting lines.
</RadzenText>
<RadzenExample ComponentName="Chart" Example="ScatterLineChartConfig" DocumentationLink="https://blazor.radzen.com/docs/guides/components/chart.html">
<ScatterLineChartConfig />
</RadzenExample>

View File

@@ -2087,6 +2087,14 @@ namespace RadzenBlazorDemos
New = true
},
new Example
{
Name = "OHLC Chart",
Path = "ohlc-chart",
Description = "Radzen Blazor Chart with OHLC bar series for financial data.",
Tags = new [] { "chart", "graph", "ohlc", "financial", "stock", "open", "high", "low", "close" },
New = true
},
new Example
{
Name = "Donut Chart",
Path = "donut-chart",
@@ -2115,6 +2123,14 @@ namespace RadzenBlazorDemos
Tags = new [] { "chart", "graph", "scatter", "point", "xy" }
},
new Example
{
Name = "Scatter Line Chart",
Path = "scatter-line-chart",
Description = "Radzen Blazor Chart with line series and markers for scatter line visualization.",
Tags = new [] { "chart", "graph", "scatter", "line", "marker", "point" },
New = true
},
new Example
{
Name = "Bubble Chart",
Path = "bubble-chart",