mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
Fix multiple value axes with RadzenBarSeries behaving like category axes. Fixes #2597
This commit is contained in:
176
Radzen.Blazor.Tests/BarSeriesMultipleAxesTests.cs
Normal file
176
Radzen.Blazor.Tests/BarSeriesMultipleAxesTests.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Bunit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests;
|
||||
|
||||
public class BarSeriesMultipleAxesTests
|
||||
{
|
||||
private class Item
|
||||
{
|
||||
public string Month { get; set; } = "";
|
||||
public double Revenue { get; set; }
|
||||
public double Count { get; set; }
|
||||
}
|
||||
|
||||
private static readonly Item[] Data =
|
||||
{
|
||||
new() { Month = "Jan", Revenue = 234000, Count = 320 },
|
||||
new() { Month = "Feb", Revenue = 269000, Count = 358 },
|
||||
new() { Month = "Mar", Revenue = 233000, Count = 302 },
|
||||
};
|
||||
|
||||
private static TestContext CreateChartContext()
|
||||
{
|
||||
var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.Setup<Radzen.Blazor.Rendering.Rect>("Radzen.createChart", _ => true)
|
||||
.SetResult(new Radzen.Blazor.Rendering.Rect { Left = 0, Top = 0, Width = 600, Height = 400 });
|
||||
ctx.Services.AddScoped<TooltipService>();
|
||||
return ctx;
|
||||
}
|
||||
|
||||
private static IRenderedComponent<RadzenChart> RenderChart(TestContext ctx)
|
||||
{
|
||||
return ctx.RenderComponent<RadzenChart>(parameters => parameters
|
||||
.AddChildContent<RadzenBarSeries<Item>>(series => series
|
||||
.Add(p => p.Title, "Revenue")
|
||||
.Add(p => p.CategoryProperty, nameof(Item.Month))
|
||||
.Add(p => p.ValueProperty, nameof(Item.Revenue))
|
||||
.Add(p => p.Data, Data))
|
||||
.AddChildContent<RadzenBarSeries<Item>>(series => series
|
||||
.Add(p => p.Title, "Order Count")
|
||||
.Add(p => p.CategoryProperty, nameof(Item.Month))
|
||||
.Add(p => p.ValueProperty, nameof(Item.Count))
|
||||
.Add(p => p.ValueAxisName, "count")
|
||||
.Add(p => p.Data, Data))
|
||||
.AddChildContent<RadzenValueAxis>(a => a
|
||||
.Add(p => p.Name, "count")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Named_Axis_Scale_Is_Numeric_And_Horizontal()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
var chart = RenderChart(ctx);
|
||||
var instance = chart.Instance;
|
||||
|
||||
var additionalScale = instance.GetValueScale("count");
|
||||
|
||||
Assert.IsNotType<OrdinalScale>(additionalScale);
|
||||
Assert.True(additionalScale.Input.End <= 500, $"Expected a numeric scale fitted to the count values but input was {additionalScale.Input.Start}..{additionalScale.Input.End}");
|
||||
Assert.Equal(instance.CategoryScale.Output.Start, additionalScale.Output.Start);
|
||||
Assert.Equal(instance.CategoryScale.Output.End, additionalScale.Output.End);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Primary_Scale_Does_Not_Include_Named_Series_Values()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
var chart = RenderChart(ctx);
|
||||
var instance = chart.Instance;
|
||||
|
||||
var revenue = instance.Series.First(s => (s as IChartValueAxisSeries)?.ValueAxisName == null);
|
||||
var count = instance.Series.First(s => (s as IChartValueAxisSeries)?.ValueAxisName == "count");
|
||||
|
||||
var revenueX = revenue.GetTooltipPosition(Data[1]).X;
|
||||
var countX = count.GetTooltipPosition(Data[1]).X;
|
||||
|
||||
Assert.True(countX > revenueX * 0.8, $"Expected the count bar (value 358) to scale on its own axis near the revenue bar (value 269000) but got countX={countX}, revenueX={revenueX}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Named_Axis_Renders_Horizontally_On_Top_With_Value_Labels()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
var chart = RenderChart(ctx);
|
||||
|
||||
Assert.Empty(chart.FindAll("g.rz-value-axis-right"));
|
||||
|
||||
var topAxisLabels = chart.FindAll("g.rz-value-axis-top .rz-tick-text")
|
||||
.Select(t => t.TextContent.Trim())
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToList();
|
||||
|
||||
Assert.NotEmpty(topAxisLabels);
|
||||
Assert.All(topAxisLabels, label => Assert.True(double.TryParse(label, out _), $"Expected numeric tick label but got '{label}'"));
|
||||
|
||||
var categories = Data.Select(d => d.Month);
|
||||
Assert.DoesNotContain(topAxisLabels, label => categories.Contains(label));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shared_Category_Axis_Contains_All_Categories()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
var chart = RenderChart(ctx);
|
||||
var instance = chart.Instance;
|
||||
|
||||
Assert.IsType<OrdinalScale>(instance.ValueScale);
|
||||
|
||||
var leftAxisLabels = chart.FindAll("g.rz-value-axis:not(.rz-value-axis-top) .rz-tick-text")
|
||||
.Select(t => t.TextContent.Trim())
|
||||
.ToList();
|
||||
|
||||
foreach (var month in Data.Select(d => d.Month))
|
||||
{
|
||||
Assert.Contains(month, leftAxisLabels);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Named_Axis_Tooltip_Formats_With_Named_Axis()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
|
||||
var chart = ctx.RenderComponent<RadzenChart>(parameters => parameters
|
||||
.AddChildContent<RadzenBarSeries<Item>>(series => series
|
||||
.Add(p => p.Title, "Revenue")
|
||||
.Add(p => p.CategoryProperty, nameof(Item.Month))
|
||||
.Add(p => p.ValueProperty, nameof(Item.Revenue))
|
||||
.Add(p => p.Data, Data))
|
||||
.AddChildContent<RadzenBarSeries<Item>>(series => series
|
||||
.Add(p => p.Title, "Order Count")
|
||||
.Add(p => p.CategoryProperty, nameof(Item.Month))
|
||||
.Add(p => p.ValueProperty, nameof(Item.Count))
|
||||
.Add(p => p.ValueAxisName, "count")
|
||||
.Add(p => p.Data, Data))
|
||||
.AddChildContent<RadzenValueAxis>(a => a
|
||||
.Add(p => p.Name, "count")
|
||||
.Add(p => p.FormatString, "{0} orders")));
|
||||
|
||||
var count = chart.Instance.Series.First(s => (s as IChartValueAxisSeries)?.ValueAxisName == "count");
|
||||
var dataLabels = count.GetDataLabels(0, 0).ToList();
|
||||
|
||||
Assert.NotEmpty(dataLabels);
|
||||
Assert.All(dataLabels, label => Assert.EndsWith("orders", label.Text));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Column_Series_Multiple_Axes_Still_Render_On_Right()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
|
||||
var chart = ctx.RenderComponent<RadzenChart>(parameters => parameters
|
||||
.AddChildContent<RadzenColumnSeries<Item>>(series => series
|
||||
.Add(p => p.CategoryProperty, nameof(Item.Month))
|
||||
.Add(p => p.ValueProperty, nameof(Item.Revenue))
|
||||
.Add(p => p.Data, Data))
|
||||
.AddChildContent<RadzenColumnSeries<Item>>(series => series
|
||||
.Add(p => p.CategoryProperty, nameof(Item.Month))
|
||||
.Add(p => p.ValueProperty, nameof(Item.Count))
|
||||
.Add(p => p.ValueAxisName, "count")
|
||||
.Add(p => p.Data, Data))
|
||||
.AddChildContent<RadzenValueAxis>(a => a
|
||||
.Add(p => p.Name, "count")));
|
||||
|
||||
Assert.NotEmpty(chart.FindAll("g.rz-value-axis-right"));
|
||||
Assert.Empty(chart.FindAll("g.rz-value-axis-top"));
|
||||
|
||||
var additionalScale = chart.Instance.GetValueScale("count");
|
||||
Assert.Equal(chart.Instance.ValueScale.Output.Start, additionalScale.Output.Start);
|
||||
Assert.Equal(chart.Instance.ValueScale.Output.End, additionalScale.Output.End);
|
||||
}
|
||||
}
|
||||
@@ -788,9 +788,10 @@ namespace Radzen.Blazor
|
||||
var vs = chart.GetValueScale(ValueAxisName);
|
||||
if (chart.ShouldInvertAxes())
|
||||
{
|
||||
var categoryAccessor = Category(vs);
|
||||
X = e => chart.CategoryScale.Scale(Value(e));
|
||||
Y = e => vs.Scale(categoryAccessor(e));
|
||||
var invertedValueScale = chart.GetInvertedValueScale(ValueAxisName);
|
||||
var categoryAccessor = Category(chart.ValueScale);
|
||||
X = e => invertedValueScale.Scale(Value(e));
|
||||
Y = e => chart.ValueScale.Scale(categoryAccessor(e));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -833,11 +834,12 @@ namespace Radzen.Blazor
|
||||
var vs = chart.GetValueScale(ValueAxisName);
|
||||
if (chart.ShouldInvertAxes())
|
||||
{
|
||||
var categoryAccessor = Category(vs);
|
||||
var invertedValueScale = chart.GetInvertedValueScale(ValueAxisName);
|
||||
var categoryAccessor = Category(chart.ValueScale);
|
||||
foreach (var item in Items)
|
||||
{
|
||||
var px = chart.CategoryScale.Scale(Value(item));
|
||||
var py = vs.Scale(categoryAccessor(item));
|
||||
var px = invertedValueScale.Scale(Value(item));
|
||||
var py = chart.ValueScale.Scale(categoryAccessor(item));
|
||||
result.Add(new Point { X = px, Y = py });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
return _ => { };
|
||||
}
|
||||
|
||||
var value = ComposeValue(categoryScale);
|
||||
var category = ComposeCategory(valueScale);
|
||||
var ticks = chart.CategoryScale.Ticks(chart.ValueAxis.TickDistance);
|
||||
var x0 = chart.CategoryScale.Scale(Math.Max(0, ticks.Start));
|
||||
var vs = chart.GetInvertedValueScale(ValueAxisName);
|
||||
var va = chart.GetValueAxis(ValueAxisName);
|
||||
var value = ComposeValue(vs);
|
||||
var category = ComposeCategory(chart.ValueScale);
|
||||
var ticks = vs.Ticks(va.TickDistance);
|
||||
var x0 = vs.Scale(Math.Max(0, ticks.Start));
|
||||
var style = $"clip-path: url(#{chart.ClipPath}); -webkit-clip-path: url(#{chart.ClipPath});";
|
||||
|
||||
var barSeries = VisibleBarSeries;
|
||||
|
||||
@@ -218,7 +218,7 @@ namespace Radzen.Blazor
|
||||
return 0;
|
||||
}
|
||||
|
||||
var value = chart.CategoryScale.Compose(Value);
|
||||
var value = chart.GetInvertedValueScale(ValueAxisName).Compose(Value);
|
||||
var x = value(item);
|
||||
|
||||
return x;
|
||||
@@ -233,7 +233,8 @@ namespace Radzen.Blazor
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return chart.ValueAxis.Format(chart.CategoryScale, chart.CategoryScale.Value(Value(item)));
|
||||
var scale = chart.GetInvertedValueScale(ValueAxisName);
|
||||
return chart.GetValueAxis(ValueAxisName).Format(scale, scale.Value(Value(item)));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -258,10 +259,11 @@ namespace Radzen.Blazor
|
||||
return (default!, new Point());
|
||||
}
|
||||
|
||||
var value = ComposeValue(chart.CategoryScale);
|
||||
var vs = chart.GetInvertedValueScale(ValueAxisName);
|
||||
var value = ComposeValue(vs);
|
||||
var category = ComposeCategory(chart.ValueScale);
|
||||
var ticks = chart.CategoryScale.Ticks(chart.ValueAxis.TickDistance);
|
||||
var x0 = chart.CategoryScale.Scale(Math.Max(0, ticks.Start));
|
||||
var ticks = vs.Ticks(chart.GetValueAxis(ValueAxisName).TickDistance);
|
||||
var x0 = vs.Scale(Math.Max(0, ticks.Start));
|
||||
|
||||
var barSeries = VisibleBarSeries;
|
||||
var index = barSeries.IndexOf(this);
|
||||
@@ -336,8 +338,9 @@ namespace Radzen.Blazor
|
||||
{
|
||||
const double gap = 8;
|
||||
const double verticalGap = 16;
|
||||
var ticks = chart.CategoryScale.Ticks(chart.ValueAxis.TickDistance);
|
||||
var x0 = chart.CategoryScale.Scale(Math.Max(0, ticks.Start));
|
||||
var vs = chart.GetInvertedValueScale(ValueAxisName);
|
||||
var ticks = vs.Ticks(chart.GetValueAxis(ValueAxisName).TickDistance);
|
||||
var x0 = vs.Scale(Math.Max(0, ticks.Start));
|
||||
|
||||
foreach (var d in Data)
|
||||
{
|
||||
@@ -361,7 +364,7 @@ namespace Radzen.Blazor
|
||||
Anchor = new Point() { X = end, Y = anchorY },
|
||||
Value = value,
|
||||
TextAnchor = textAnchor,
|
||||
Text = chart.ValueAxis.Format(chart.CategoryScale, value)
|
||||
Text = chart.GetValueAxis(ValueAxisName).Format(vs, value)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
|
||||
@@ -475,6 +476,17 @@ namespace Radzen.Blazor
|
||||
|
||||
return AdditionalValueAxes.TryGetValue(name, out var axis) ? axis : ValueAxis;
|
||||
}
|
||||
|
||||
internal ScaleBase GetInvertedValueScale(string? name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return CategoryScale;
|
||||
}
|
||||
|
||||
return AdditionalValueScales.TryGetValue(name, out var scale) ? scale : CategoryScale;
|
||||
}
|
||||
|
||||
internal void AddSeries(IChartSeries series)
|
||||
{
|
||||
if (!Series.Contains(series))
|
||||
@@ -557,6 +569,8 @@ namespace Radzen.Blazor
|
||||
visibleSeries.Add(invisibleSeries.Last());
|
||||
}
|
||||
|
||||
var invertAxes = ShouldInvertAxes();
|
||||
|
||||
// Partition series: primary axis vs named axes
|
||||
var primarySeries = new List<IChartSeries>();
|
||||
var namedAxisSeries = new Dictionary<string, List<IChartSeries>>();
|
||||
@@ -579,16 +593,30 @@ namespace Radzen.Blazor
|
||||
}
|
||||
}
|
||||
|
||||
// All series contribute to category scale
|
||||
// All series contribute to the shared category scale (bar-family series swap their transforms when axes are inverted)
|
||||
foreach (var series in visibleSeries)
|
||||
{
|
||||
CategoryScale = series.TransformCategoryScale(CategoryScale);
|
||||
if (invertAxes)
|
||||
{
|
||||
ValueScale = series.TransformValueScale(ValueScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
CategoryScale = series.TransformCategoryScale(CategoryScale);
|
||||
}
|
||||
}
|
||||
|
||||
// Primary series contribute to primary value scale
|
||||
foreach (var series in primarySeries)
|
||||
{
|
||||
ValueScale = series.TransformValueScale(ValueScale);
|
||||
if (invertAxes)
|
||||
{
|
||||
CategoryScale = series.TransformCategoryScale(CategoryScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
ValueScale = series.TransformValueScale(ValueScale);
|
||||
}
|
||||
}
|
||||
|
||||
// Named-axis series contribute to their own scales
|
||||
@@ -630,7 +658,7 @@ namespace Radzen.Blazor
|
||||
|
||||
foreach (var series in entry.Value)
|
||||
{
|
||||
additionalScale = series.TransformValueScale(additionalScale);
|
||||
additionalScale = invertAxes ? series.TransformCategoryScale(additionalScale) : series.TransformValueScale(additionalScale);
|
||||
}
|
||||
|
||||
AdditionalValueScales[axisName] = additionalScale;
|
||||
@@ -648,7 +676,7 @@ namespace Radzen.Blazor
|
||||
AxisBase xAxis = CategoryAxis;
|
||||
AxisBase yAxis = ValueAxis;
|
||||
|
||||
if (ShouldInvertAxes())
|
||||
if (invertAxes)
|
||||
{
|
||||
xAxis = ValueAxis;
|
||||
yAxis = CategoryAxis;
|
||||
@@ -699,20 +727,29 @@ namespace Radzen.Blazor
|
||||
var valueAxisSize = ValueAxis.Measure(this);
|
||||
var categoryAxisSize = CategoryAxis.Measure(this);
|
||||
|
||||
// Measure additional axes for right margin
|
||||
// Measure additional axes for the right margin (top margin when axes are inverted)
|
||||
double additionalAxesWidth = 0;
|
||||
double additionalAxesHeight = 0;
|
||||
foreach (var entry in AdditionalValueAxes)
|
||||
{
|
||||
additionalAxesWidth += entry.Value.Measure(this, GetValueScale(entry.Key));
|
||||
if (invertAxes)
|
||||
{
|
||||
additionalAxesHeight += entry.Value.MeasureHorizontal(GetValueScale(entry.Key));
|
||||
}
|
||||
else
|
||||
{
|
||||
additionalAxesWidth += entry.Value.Measure(this, GetValueScale(entry.Key));
|
||||
}
|
||||
}
|
||||
|
||||
if (!ShouldRenderAxes())
|
||||
{
|
||||
valueAxisSize = categoryAxisSize = 0;
|
||||
additionalAxesWidth = 0;
|
||||
additionalAxesHeight = 0;
|
||||
}
|
||||
|
||||
MarginTop = 32;
|
||||
MarginTop = 32 + additionalAxesHeight;
|
||||
|
||||
if (IsRTL)
|
||||
{
|
||||
@@ -746,7 +783,7 @@ namespace Radzen.Blazor
|
||||
{
|
||||
if (legendPosition == LegendPosition.Top)
|
||||
{
|
||||
MarginTop = legendSize + 16;
|
||||
MarginTop = legendSize + 16 + additionalAxesHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -816,11 +853,23 @@ namespace Radzen.Blazor
|
||||
foreach (var entry in AdditionalValueScales)
|
||||
{
|
||||
var axis = GetValueAxis(entry.Key);
|
||||
entry.Value.Output = new ScaleRange
|
||||
if (invertAxes)
|
||||
{
|
||||
Start = axis.Inverted ? valueEnd : valueStart,
|
||||
End = axis.Inverted ? valueStart : valueEnd
|
||||
};
|
||||
var reversed = axis.Inverted != IsRTL;
|
||||
entry.Value.Output = new ScaleRange
|
||||
{
|
||||
Start = reversed ? categoryEnd : categoryStart,
|
||||
End = reversed ? categoryStart : categoryEnd
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
entry.Value.Output = new ScaleRange
|
||||
{
|
||||
Start = axis.Inverted ? valueEnd : valueStart,
|
||||
End = axis.Inverted ? valueStart : valueEnd
|
||||
};
|
||||
}
|
||||
entry.Value.Fit(axis.TickDistance);
|
||||
}
|
||||
|
||||
@@ -857,6 +906,20 @@ namespace Radzen.Blazor
|
||||
Start = catReversed ? ce : cs,
|
||||
End = catReversed ? cs : ce
|
||||
};
|
||||
|
||||
if (ShouldInvertAxes())
|
||||
{
|
||||
foreach (var entry in AdditionalValueScales)
|
||||
{
|
||||
var axis = GetValueAxis(entry.Key);
|
||||
var reversed = axis.Inverted != IsRTL;
|
||||
entry.Value.Output = new ScaleRange
|
||||
{
|
||||
Start = reversed ? ce : cs,
|
||||
End = reversed ? cs : ce
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (height != Height)
|
||||
@@ -873,14 +936,17 @@ namespace Radzen.Blazor
|
||||
End = valReversed ? vs : ve
|
||||
};
|
||||
|
||||
foreach (var entry in AdditionalValueScales)
|
||||
if (!ShouldInvertAxes())
|
||||
{
|
||||
var axis = GetValueAxis(entry.Key);
|
||||
entry.Value.Output = new ScaleRange
|
||||
foreach (var entry in AdditionalValueScales)
|
||||
{
|
||||
Start = axis.Inverted ? ve : vs,
|
||||
End = axis.Inverted ? vs : ve
|
||||
};
|
||||
var axis = GetValueAxis(entry.Key);
|
||||
entry.Value.Output = new ScaleRange
|
||||
{
|
||||
Start = axis.Inverted ? ve : vs,
|
||||
End = axis.Inverted ? vs : ve
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1615,17 +1681,48 @@ namespace Radzen.Blazor
|
||||
/// The plot area and axes are included; legends and tooltips are HTML overlays and are not included.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The download file name. Default is <c>chart.png</c>.</param>
|
||||
public async Task ToPng(string fileName = "chart.png")
|
||||
/// <param name="width">The PNG width in pixels. When omitted the rendered size scaled by the device pixel ratio is used.</param>
|
||||
/// <param name="height">The PNG height in pixels. When omitted it is derived from <paramref name="width"/> preserving the aspect ratio.</param>
|
||||
public async Task ToPng(string fileName = "chart.png", int? width = null, int? height = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fileName);
|
||||
|
||||
if (IsJSRuntimeAvailable && JSRuntime != null)
|
||||
{
|
||||
var svg = await ToSvg();
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.downloadSvgAsPng", svg, fileName);
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.downloadSvgAsPng", svg, fileName, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the chart as a PNG image and returns the image data.
|
||||
/// The plot area and axes are included; legends and tooltips are HTML overlays and are not included.
|
||||
/// Use this to store the chart in a database, embed it in documents, or send it to an API instead of downloading it.
|
||||
/// </summary>
|
||||
/// <param name="width">The PNG width in pixels. When <c>null</c> the rendered size scaled by the device pixel ratio is used.</param>
|
||||
/// <param name="height">The PNG height in pixels. When <c>null</c> it is derived from <paramref name="width"/> preserving the aspect ratio.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task{TResult}"/> representing the asynchronous operation. The task result contains the PNG image data.
|
||||
/// </returns>
|
||||
public async Task<byte[]> ToPng(int? width, int? height = null)
|
||||
{
|
||||
if (IsJSRuntimeAvailable && JSRuntime != null)
|
||||
{
|
||||
var svg = await ToSvg();
|
||||
|
||||
if (!string.IsNullOrEmpty(svg))
|
||||
{
|
||||
await using var png = await JSRuntime.InvokeAsync<IJSStreamReference>("Radzen.svgToPng", svg, width, height);
|
||||
using var stream = await png.OpenReadStreamAsync(maxAllowedSize: 32 * 1024 * 1024);
|
||||
using var memoryStream = new MemoryStream();
|
||||
await stream.CopyToAsync(memoryStream);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dispose()
|
||||
{
|
||||
@@ -1882,7 +1979,7 @@ namespace Radzen.Blazor
|
||||
|
||||
var categoryCrosshair = CategoryAxis?.Crosshair;
|
||||
var hoveredValueAxis = GetValueAxis((HoveredSeries as IChartValueAxisSeries)?.ValueAxisName);
|
||||
var hoveredValueScale = GetValueScale((HoveredSeries as IChartValueAxisSeries)?.ValueAxisName);
|
||||
var hoveredValueScale = ShouldInvertAxes() ? ValueScale : GetValueScale((HoveredSeries as IChartValueAxisSeries)?.ValueAxisName);
|
||||
var valueCrosshair = hoveredValueAxis?.Crosshair;
|
||||
|
||||
var snapX = categoryCrosshair?.Snap ?? true;
|
||||
|
||||
@@ -35,6 +35,16 @@
|
||||
return AxisMeasurer.YAxis(scale, this, Title);
|
||||
}
|
||||
|
||||
internal double MeasureHorizontal(ScaleBase scale)
|
||||
{
|
||||
if (Visible == false)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return AxisMeasurer.XAxis(scale, this, Title);
|
||||
}
|
||||
|
||||
internal override double Size
|
||||
{
|
||||
get
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
|
||||
@inherits RadzenChartComponentBase
|
||||
@if (YAxis?.Visible == true)
|
||||
@if (YAxis?.Visible == true && renderHorizontal)
|
||||
{
|
||||
<g class="rz-axis rz-value-axis rz-value-axis-top">
|
||||
<Line class="rz-line" X1="@Math.Min(x1, x2)" Y1="@axisY" X2="@Math.Max(x1, x2)" Y2="@axisY" Stroke="@YAxis.Stroke" StrokeWidth="@YAxis.StrokeWidth" LineType="@YAxis.LineType" />
|
||||
@foreach (var idx in tickValues)
|
||||
{
|
||||
var value = scale?.Value(idx);
|
||||
var x = scale?.Scale(idx) ?? 0;
|
||||
var text = value != null && YAxis != null && scale != null ? YAxis.Format(scale, value) : "";
|
||||
|
||||
if (YAxis?.Ticks?.Template != null)
|
||||
{
|
||||
var context = new TickTemplateContext { Text = text, Value = value, X = x, Y = axisY };
|
||||
|
||||
<CategoryAxisTick X="@x" Y="@axisY" Text="@text" Stroke="@(YAxis.Ticks.Stroke ?? YAxis.Stroke)" StrokeWidth="@YAxis.Ticks.StrokeWidth" LineType="@YAxis.Ticks.LineType" TopSide="true">
|
||||
@YAxis.Ticks.Template(context)
|
||||
</CategoryAxisTick>
|
||||
}
|
||||
else if (!String.IsNullOrEmpty(text))
|
||||
{
|
||||
<CategoryAxisTick X="@x" Y="@axisY" Text="@text" Stroke="@(YAxis?.Ticks?.Stroke ?? YAxis?.Stroke)" StrokeWidth="@(YAxis?.Ticks?.StrokeWidth ?? 0)" LineType="@(YAxis?.Ticks?.LineType ?? LineType.Solid)" TopSide="true" />
|
||||
}
|
||||
|
||||
if (YAxis?.GridLines?.Visible == true && idx > start)
|
||||
{
|
||||
<Line class="rz-grid-line" X1="@x" Y1="@Math.Min(gridY1, gridY2)" X2="@x" Y2="@Math.Max(gridY1, gridY2)" Stroke="@(YAxis.GridLines.Stroke)" StrokeWidth="@(YAxis.GridLines.StrokeWidth)" LineType="@(YAxis.GridLines.LineType)" />
|
||||
}
|
||||
}
|
||||
@if (!String.IsNullOrEmpty(YAxis?.Title?.Text))
|
||||
{
|
||||
<CategoryAxisTitle X="@(Math.Abs(x2 - x1) / 2)" Y="@(axisY - horizontalSize + (YAxis?.Title?.Size ?? 0))" Text="@(YAxis?.Title?.Text ?? "")" />
|
||||
}
|
||||
</g>
|
||||
}
|
||||
else if (YAxis?.Visible == true)
|
||||
{
|
||||
<g class="rz-axis rz-value-axis @(RenderAsRightSide ? "rz-value-axis-right" : "")">
|
||||
<Line class="rz-line" X1="@axisX" Y1="@Math.Min(y1, y2)" X2="@axisX" Y2="@Math.Max(y1, y2)" Stroke="@YAxis.Stroke" StrokeWidth="@YAxis.StrokeWidth" LineType="@YAxis.LineType" />
|
||||
@@ -57,12 +91,17 @@
|
||||
double y1;
|
||||
double y2;
|
||||
double axisX;
|
||||
double axisY;
|
||||
double gridX1;
|
||||
double gridX2;
|
||||
double gridY1;
|
||||
double gridY2;
|
||||
double titleX;
|
||||
double horizontalSize;
|
||||
ScaleBase? scale;
|
||||
bool isLogarithmic;
|
||||
bool isFlippedToRight;
|
||||
bool renderHorizontal;
|
||||
private AxisBase? XAxis { get; set; }
|
||||
private AxisBase? YAxis { get; set; }
|
||||
|
||||
@@ -73,6 +112,39 @@
|
||||
return;
|
||||
}
|
||||
|
||||
renderHorizontal = IsRightSide && Chart.ShouldInvertAxes();
|
||||
|
||||
if (renderHorizontal)
|
||||
{
|
||||
var axis = Chart.GetValueAxis(AxisName);
|
||||
YAxis = axis;
|
||||
scale = Chart.GetValueScale(AxisName);
|
||||
|
||||
var horizontalTicks = scale.Ticks(axis.TickDistance);
|
||||
start = horizontalTicks.Start;
|
||||
end = horizontalTicks.End;
|
||||
step = horizontalTicks.Step;
|
||||
tickValues = scale.TickValues(axis.TickDistance).ToList();
|
||||
|
||||
x1 = scale.Scale(start);
|
||||
x2 = scale.Scale(end);
|
||||
|
||||
var verticalTicks = Chart.ValueScale.Ticks(Chart.CategoryAxis.TickDistance);
|
||||
gridY1 = Chart.ValueScale.Scale(verticalTicks.Start);
|
||||
gridY2 = Chart.ValueScale.Scale(verticalTicks.End);
|
||||
|
||||
axisY = 0;
|
||||
foreach (var entry in Chart.AdditionalValueAxes)
|
||||
{
|
||||
if (entry.Key == AxisName) break;
|
||||
axisY -= entry.Value.MeasureHorizontal(Chart.GetValueScale(entry.Key));
|
||||
}
|
||||
|
||||
horizontalSize = axis.MeasureHorizontal(scale);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsRightSide)
|
||||
{
|
||||
YAxis = Chart.GetValueAxis(AxisName);
|
||||
|
||||
Reference in New Issue
Block a user