Axis crossing support added

This commit is contained in:
Vladimir Enchev
2026-03-16 21:03:01 +02:00
parent 4456e22ef6
commit 0087b379fe
6 changed files with 115 additions and 6 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using Microsoft.AspNetCore.Components;
namespace Radzen.Blazor
@@ -109,6 +110,16 @@ namespace Radzen.Blazor
/// </summary>
[Parameter]
public bool Inverted { get; set; }
/// <summary>
/// Specifies the value on the perpendicular axis where this axis should cross.
/// When set, the axis line and ticks are positioned at the corresponding location
/// instead of the chart edge. For example, setting <c>CrossesAt="0"</c> on the category axis
/// positions it where 0 is on the value axis.
/// </summary>
[Parameter]
public object? CrossesAt { get; set; }
/// <summary>
/// Specifies the label rotation angle in degrees. Set to <c>null</c> by default which means no rotation is applied. Has higher precedence than <see cref="LabelAutoRotation"/>.
/// </summary>
@@ -130,6 +141,7 @@ namespace Radzen.Blazor
DidParameterChange(parameters, nameof(LabelRotation), LabelRotation) ||
DidParameterChange(parameters, nameof(LabelAutoRotation), LabelAutoRotation) ||
DidParameterChange(parameters, nameof(Inverted), Inverted) ||
DidParameterChange(parameters, nameof(CrossesAt), CrossesAt) ||
DidParameterChange(parameters, nameof(Step), Step);
}
@@ -152,6 +164,12 @@ namespace Radzen.Blazor
}
}
internal double? GetCrossesAtValue()
{
if (CrossesAt == null) return null;
return Convert.ToDouble(CrossesAt, CultureInfo.InvariantCulture);
}
internal abstract double Size { get; }
}
}

View File

@@ -26,18 +26,18 @@
if (XAxis?.GridLines?.Visible == true && idx > start)
{
<Line class="rz-grid-line" X1="@x" Y1="@y1" X2="@x" Y2="@y2" Stroke="@(XAxis.GridLines.Stroke ?? "")" StrokeWidth="@(XAxis?.GridLines.StrokeWidth ?? 0)" LineType="@(XAxis?.GridLines.LineType ?? LineType.Solid)" />
<Line class="rz-grid-line" X1="@x" Y1="@gridY1" X2="@x" Y2="@gridY2" Stroke="@(XAxis.GridLines.Stroke ?? "")" StrokeWidth="@(XAxis?.GridLines.StrokeWidth ?? 0)" LineType="@(XAxis?.GridLines.LineType ?? LineType.Solid)" />
}
}
@if (!String.IsNullOrEmpty(XAxis?.Title?.Text))
{
if (isTopSide)
{
<CategoryAxisTitle X="@(Math.Abs(x2 - x1)/2)" Y="@(y1 - (Chart?.CategoryAxis?.Size ?? 0) + (XAxis?.Title?.Size ?? 0))" Text="@(XAxis?.Title?.Text ?? "")" />
<CategoryAxisTitle X="@(Math.Abs(x2 - x1)/2)" Y="@(titleY - (Chart?.CategoryAxis?.Size ?? 0) + (XAxis?.Title?.Size ?? 0))" Text="@(XAxis?.Title?.Text ?? "")" />
}
else
{
<CategoryAxisTitle X="@(Math.Abs(x2 - x1)/2)" Y="@(y1 + (Chart?.CategoryAxis?.Size ?? 0) - (XAxis?.Title?.Size ?? 0))" Text="@(XAxis?.Title?.Text ?? "")" />
<CategoryAxisTitle X="@(Math.Abs(x2 - x1)/2)" Y="@(titleY + (Chart?.CategoryAxis?.Size ?? 0) - (XAxis?.Title?.Size ?? 0))" Text="@(XAxis?.Title?.Text ?? "")" />
}
}
</g>
@@ -50,6 +50,9 @@
double x2;
double y1;
double y2;
double gridY1;
double gridY2;
double titleY;
private AxisBase? XAxis { get; set; }
private AxisBase? YAxis { get; set; }
@@ -85,8 +88,19 @@
y1 = Chart.ValueScale.Scale(valueTicks.Start);
y2 = Chart.ValueScale.Scale(valueTicks.End);
gridY1 = y1;
gridY2 = y2;
titleY = y1;
isTopSide = y1 < y2;
var crossesAt = XAxis?.GetCrossesAtValue();
if (crossesAt.HasValue)
{
var clampedValue = Math.Max(Math.Min(crossesAt.Value, valueTicks.End), valueTicks.Start);
y1 = Chart.ValueScale.Scale(clampedValue);
}
if (XAxis?.LabelRotation != null)
{
rotate = XAxis.LabelRotation;

View File

@@ -25,18 +25,18 @@
if (YAxis?.GridLines?.Visible == true && idx > start)
{
<Line class="rz-grid-line" X1="@Math.Min(x1, x2)" Y1="@y" X2="@Math.Max(x1, x2)" Y2="@y" Stroke="@(YAxis.GridLines.Stroke)" StrokeWidth="@(YAxis.GridLines.StrokeWidth)" LineType="@(YAxis.GridLines.LineType)" />
<Line class="rz-grid-line" X1="@Math.Min(gridX1, gridX2)" Y1="@y" X2="@Math.Max(gridX1, gridX2)" Y2="@y" Stroke="@(YAxis.GridLines.Stroke)" StrokeWidth="@(YAxis.GridLines.StrokeWidth)" LineType="@(YAxis.GridLines.LineType)" />
}
}
@if (!String.IsNullOrEmpty(YAxis?.Title?.Text))
{
if (RenderAsRightSide)
{
<ValueAxisTitle X="@(axisX + (YAxis?.Size ?? 0) - (YAxis?.Title?.Size ?? 0))" Y="@(Math.Abs(y1 - y2)/2)" Text="@(YAxis?.Title?.Text ?? "")" RightSide="true" />
<ValueAxisTitle X="@(titleX + (YAxis?.Size ?? 0) - (YAxis?.Title?.Size ?? 0))" Y="@(Math.Abs(y1 - y2)/2)" Text="@(YAxis?.Title?.Text ?? "")" RightSide="true" />
}
else
{
<ValueAxisTitle X="@(axisX - (Chart?.ValueAxis?.Size ?? 0) + (YAxis?.Title?.Size ?? 0))" Y="@(Math.Abs(y1 - y2)/2)" Text="@(YAxis?.Title?.Text ?? "")" />
<ValueAxisTitle X="@(titleX - (Chart?.ValueAxis?.Size ?? 0) + (YAxis?.Title?.Size ?? 0))" Y="@(Math.Abs(y1 - y2)/2)" Text="@(YAxis?.Title?.Text ?? "")" />
}
}
</g>
@@ -56,6 +56,9 @@
double y1;
double y2;
double axisX;
double gridX1;
double gridX2;
double titleX;
ScaleBase? scale;
bool isFlippedToRight;
private AxisBase? XAxis { get; set; }
@@ -105,6 +108,9 @@
y1 = scale.Scale(start);
y2 = scale.Scale(end);
gridX1 = x1;
gridX2 = x2;
// Detect if the primary value axis has been flipped to the right side due to category inversion
isFlippedToRight = string.IsNullOrEmpty(AxisName) && x1 > x2;
@@ -121,6 +127,16 @@
else
{
axisX = x1;
var crossesAt = YAxis?.GetCrossesAtValue();
if (crossesAt.HasValue)
{
var clampedValue = Math.Max(Math.Min(crossesAt.Value, categoryTicks.End), categoryTicks.Start);
axisX = Chart.CategoryScale.Scale(clampedValue);
}
}
// Title stays at the chart edge, not the crossing position
titleX = IsRightSide ? axisX : (YAxis?.GetCrossesAtValue() != null ? x1 : axisX);
}
}

View File

@@ -0,0 +1,38 @@
<RadzenStack class="rz-p-0 rz-p-md-6 rz-p-lg-12">
<RadzenRow>
<RadzenColumn Size="12">
<RadzenChart Style="height: 480px">
<RadzenLineSeries Data="@data" CategoryProperty="X" ValueProperty="Y" Title="Cubic Spline"
Stroke="var(--rz-series-1)" LineType="LineType.Solid" Smooth="true">
<RadzenMarkers MarkerType="MarkerType.Diamond" Fill="var(--rz-series-2)" Stroke="var(--rz-series-2)" Size="8" />
</RadzenLineSeries>
<RadzenCategoryAxis CrossesAt="0" FormatString="{0}" />
<RadzenValueAxis CrossesAt="0">
<RadzenGridLines Visible="true" />
</RadzenValueAxis>
<RadzenChartTooltipOptions Visible="true" />
<RadzenLegend Position="LegendPosition.Bottom" />
</RadzenChart>
</RadzenColumn>
</RadzenRow>
</RadzenStack>
@code {
class DataPoint
{
public double X { get; set; }
public double Y { get; set; }
}
DataPoint[] data = new DataPoint[]
{
new DataPoint { X = -6, Y = 3 },
new DataPoint { X = -3, Y = -5 },
new DataPoint { X = -1, Y = -1 },
new DataPoint { X = 0, Y = 0 },
new DataPoint { X = 1, Y = 1 },
new DataPoint { X = 3, Y = 3.5 },
new DataPoint { X = 5, Y = 4 },
new DataPoint { X = 7, Y = 3.8 },
};
}

View File

@@ -0,0 +1,15 @@
@page "/axis-crossing-chart"
<RadzenText TextStyle="TextStyle.H2" TagName="TagName.H1" class="rz-pt-8">
Radzen Blazor Chart with axis crossing
</RadzenText>
<RadzenText TextStyle="TextStyle.Subtitle1" TagName="TagName.P" class="rz-pb-4">
Set <code>CrossesAt</code> on <code>RadzenValueAxis</code> or <code>RadzenCategoryAxis</code> to position the axis
at a specific value on the perpendicular axis. For example, setting <code>CrossesAt="0"</code> on both axes
creates a standard mathematical coordinate system where axes intersect at the origin.
</RadzenText>
<RadzenExample ComponentName="Chart" Example="AxisCrossingChartConfig"
DocumentationLink="https://blazor.radzen.com/docs/guides/components/chart.html">
<AxisCrossingChartConfig />
</RadzenExample>

View File

@@ -2388,6 +2388,14 @@ namespace RadzenBlazorDemos
New = true
},
new Example
{
Name = "Axis Crossing",
Path = "axis-crossing-chart",
Description = "Radzen Blazor Chart with axis crossing to position axes at specific values instead of chart edges.",
Tags = new [] { "chart", "graph", "axis", "crossing", "origin", "intersect" },
New = true
},
new Example
{
Toc = [ new () { Text = "Legend position", Anchor = "#legend-position" }, new () { Text = "Hide the legend", Anchor = "#hide-the-legend" } ],
Name = "Legend",