Repro code (for future reference):
// __Index.razor
<h3>Parent: Fetches Data, Passes to Child</h3>
<style>
.my-page-stack {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.my-page-stack {
display: flex;
flex-direction: column;
gap: 16px;
padding-bottom: 16px;
}
</style>
<div class="my-page-stack">
@if (loading)
{
<p>Loading data...</p>
}
else
{
<Test Forecasts="@forecasts" AsOfDate="@asOfDate" OnExported="OnExported" @ref="testRef" />
<button class="e-btn" @onclick="ExportChart" disabled="@exporting">Export Chart as PNG</button>
}
</div>
@if (!string.IsNullOrEmpty(exportedImage))
{
<h4>Exported PNG Output (from parent):</h4>
<div style="border:1px solid #ccc; margin-top:8px; padding:8px;">
<img src="@exportedImage" style="width:100%;" />
</div>
}
@if (!string.IsNullOrEmpty(errorMessage))
{
<div style="color:red; margin-top:10px;">@errorMessage</div>
}
@code {
private bool loading = true;
private bool exporting = false;
private List<List<MyDto>> forecasts = new();
private DateTime asOfDate = DateTime.Today;
private Test? testRef;
private string? exportedImage;
private string? errorMessage;
protected override async Task OnInitializedAsync()
{
loading = true;
// Fetch real data from production service later
forecasts = await GetFakeDataAsync();
loading = false;
}
private async Task ExportChart()
{
exporting = true;
errorMessage = null;
if (testRef is not null)
{
try
{
await testRef.ExportChart();
}
catch (Exception ex)
{
errorMessage = ex.ToString();
}
}
exporting = false;
}
private void OnExported(string? base64)
{
exportedImage = !string.IsNullOrEmpty(base64) ? $"data:image/png;base64,{base64}" : null;
}
private async Task<List<List<MyDto>>> GetFakeDataAsync()
{
await Task.Delay(50);
return new List<List<MyDto>>
{
// Prime Model (will be filtered out)
new List<MyDto>
{
new MyDto { DATE = DateTime.Today, MODEL = "Prime Model", PM_VALUE = 15000 },
new MyDto { DATE = DateTime.Today.AddDays(1), MODEL = "Prime Model", PM_VALUE = 15500 },
new MyDto { DATE = DateTime.Today.AddDays(2), MODEL = "Prime Model", PM_VALUE = 16000 },
new MyDto { DATE = DateTime.Today.AddDays(3), MODEL = "Prime Model", PM_VALUE = 16500 },
new MyDto { DATE = DateTime.Today.AddDays(4), MODEL = "Prime Model", PM_VALUE = 17000 },
new MyDto { DATE = DateTime.Today.AddDays(5), MODEL = "Prime Model", PM_VALUE = 17500 },
new MyDto { DATE = DateTime.Today.AddDays(6), MODEL = "Prime Model", PM_VALUE = 14150 }
},
// Model 1
new List<MyDto>
{
new MyDto { DATE = DateTime.Today, MODEL = "Model 1", PM_VALUE = 16100 },
new MyDto { DATE = DateTime.Today.AddDays(1), MODEL = "Model 1", PM_VALUE = 16600 },
new MyDto { DATE = DateTime.Today.AddDays(2), MODEL = "Model 1", PM_VALUE = 16500 },
new MyDto { DATE = DateTime.Today.AddDays(3), MODEL = "Model 1", PM_VALUE = 16600 },
new MyDto { DATE = DateTime.Today.AddDays(4), MODEL = "Model 1", PM_VALUE = 17100 },
new MyDto { DATE = DateTime.Today.AddDays(5), MODEL = "Model 1", PM_VALUE = 17600 },
new MyDto { DATE = DateTime.Today.AddDays(6), MODEL = "Model 1", PM_VALUE = 14100 }
},
// Model 2
new List<MyDto>
{
new MyDto { DATE = DateTime.Today, MODEL = "Model 2", PM_VALUE = 14000 },
new MyDto { DATE = DateTime.Today.AddDays(1), MODEL = "Model 2", PM_VALUE = 14400 },
new MyDto { DATE = DateTime.Today.AddDays(2), MODEL = "Model 2", PM_VALUE = 14900 },
new MyDto { DATE = DateTime.Today.AddDays(3), MODEL = "Model 2", PM_VALUE = 14400 },
new MyDto { DATE = DateTime.Today.AddDays(4), MODEL = "Model 2", PM_VALUE = 15900 },
new MyDto { DATE = DateTime.Today.AddDays(5), MODEL = "Model 2", PM_VALUE = 14400 },
new MyDto { DATE = DateTime.Today.AddDays(6), MODEL = "Model 2", PM_VALUE = 12900 }
}
};
}
public class MyDto
{
public DateTime DATE { get; set; }
public string MODEL { get; set; } = "";
public double? AM_VALUE { get; set; }
public double? PM_VALUE { get; set; }
}
}
=================================================================
=================================================================
=================================================================
// Test.razor
@using Microsoft.AspNetCore.Components
@using static Playground.User.__Index
<h3>Child: Receives Data, Transforms, Handles Export</h3>
<style>
.my-range-chart * {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
</style>
<div class="my-range-chart">
<Syncfusion.Blazor.Charts.SfChart @ref="chartRef" Title="Range + Line Chart Export Bug" Width="100%">
<Syncfusion.Blazor.Charts.ChartPrimaryXAxis ValueType="Syncfusion.Blazor.Charts.ValueType.Category" />
<Syncfusion.Blazor.Charts.ChartPrimaryYAxis Title="Value" Interval="@_mWInterval" Minimum="@AxisMinimum" />
<Syncfusion.Blazor.Charts.ChartSeriesCollection>
<Syncfusion.Blazor.Charts.ChartSeries DataSource="@RangeData"
XName="Category"
High="High"
Low="Low"
Name="Range"
Type="Syncfusion.Blazor.Charts.ChartSeriesType.RangeColumn"
Fill="#2196F3"
Width="2">
<Syncfusion.Blazor.Charts.ChartMarker Visible="true" Height="5" Width="5">
<Syncfusion.Blazor.Charts.ChartDataLabel Visible="true" Position="Syncfusion.Blazor.Charts.LabelPosition.Outer">
<Syncfusion.Blazor.Charts.ChartDataLabelFont Size="13px" FontWeight="Bold" Color="Black"></Syncfusion.Blazor.Charts.ChartDataLabelFont>
</Syncfusion.Blazor.Charts.ChartDataLabel>
</Syncfusion.Blazor.Charts.ChartMarker>
</Syncfusion.Blazor.Charts.ChartSeries>
<Syncfusion.Blazor.Charts.ChartSeries DataSource="@LineData"
XName="Category"
YName="Value"
Name="Line"
Type="Syncfusion.Blazor.Charts.ChartSeriesType.Line"
Fill="#FF9800"
Width="2">
<Syncfusion.Blazor.Charts.ChartMarker Visible="true" Height="5" Width="5">
<Syncfusion.Blazor.Charts.ChartDataLabel Visible="true" Position="Syncfusion.Blazor.Charts.LabelPosition.Auto">
<Syncfusion.Blazor.Charts.ChartDataLabelFont Size="13px" FontWeight="Bold" Color="White"></Syncfusion.Blazor.Charts.ChartDataLabelFont>
</Syncfusion.Blazor.Charts.ChartDataLabel>
</Syncfusion.Blazor.Charts.ChartMarker>
</Syncfusion.Blazor.Charts.ChartSeries>
</Syncfusion.Blazor.Charts.ChartSeriesCollection>
<Syncfusion.Blazor.Charts.ChartLegendSettings Visible="true" EnableHighlight="true" ShapeWidth="9" ShapeHeight="9" Padding="15" />
<Syncfusion.Blazor.Charts.ChartEvents OnExportComplete="OnExportComplete" />
</Syncfusion.Blazor.Charts.SfChart>
</div>
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<div style="color:red; margin-top:10px;">@ErrorMessage</div>
}
@code {
private Syncfusion.Blazor.Charts.SfChart? chartRef;
private string? ErrorMessage;
[Parameter] public List<List<MyDto>> Forecasts { get; set; } = new();
[Parameter] public DateTime AsOfDate { get; set; }
[Parameter] public EventCallback<string?> OnExported { get; set; }
private List<RangeChartPointTest> RangeData = new();
private List<PrimeModelPointTest> LineData = new();
private double _mWInterval = 2000;
private double AxisMinimum => RangeData.Any()
? Math.Ceiling((RangeData.Min(r => r.Low) - _mWInterval) / 1000) * 1000
: 8000;
protected override void OnParametersSet()
{
RangeData = ToRangeChartPoints(AsOfDate, Forecasts);
LineData = ToPrimeModelPoints(AsOfDate, Forecasts, RangeData.Select(r => r.Category).ToList());
}
public async Task ExportChart()
{
ErrorMessage = null;
try
{
if (chartRef is not null)
{
await chartRef.ExportAsync(Syncfusion.Blazor.Charts.ExportType.PNG, "ChartExport", null, false, true);
}
}
catch (Exception ex)
{
ErrorMessage = ex.ToString();
}
StateHasChanged();
}
private async void OnExportComplete(Syncfusion.Blazor.Charts.ExportEventArgs args)
{
try
{
if (!string.IsNullOrEmpty(args.Base64))
{
await OnExported.InvokeAsync(args.Base64);
}
else
{
ErrorMessage = "Export failed: No image data returned.";
}
}
catch (Exception ex)
{
ErrorMessage = ex.ToString();
}
StateHasChanged();
}
public static List<RangeChartPointTest> ToRangeChartPoints(DateTime date, List<List<MyDto>> groupedModels)
{
var points = new List<RangeChartPointTest>();
// Only use normal models (exclude "Prime Model")
var normalGroups = groupedModels.Where(g => !g.All(m => m.MODEL.Trim().ToUpper().StartsWith("PRIME")));
if (!normalGroups.Any()) return points;
string[] periods = { "AM", "PM" };
for (int i = 0; i < 7; i++)
{
var day = date.AddDays(i).Date;
foreach (var period in periods)
{
var values = normalGroups
.Select(vg => vg.FirstOrDefault(dto => dto.DATE.Date == day))
.Select(dto => period == "AM" ? dto?.AM_VALUE : dto?.PM_VALUE)
.Where(val => val.HasValue)
.Select(val => val.Value)
.ToList();
if (values.Count >= 2)
{
points.Add(new RangeChartPointTest
{
Category = $"{day:ddd, MM/dd/yyyy} {period}",
High = Math.Round(values.Max()),
Low = Math.Round(values.Min())
});
}
}
}
return points;
}
public static List<PrimeModelPointTest> ToPrimeModelPoints(DateTime date, List<List<MyDto>> groupedModels, List<string> rangeCategories)
{
// Use the "Prime Model" as the reference line (or change as needed)
var primeGroup = groupedModels.FirstOrDefault(g => g.All(m => m.MODEL.Trim().ToUpper() == "PRIME MODEL"));
if (primeGroup is null) return new();
var dtoByDate = primeGroup.ToDictionary(d => d.DATE.Date);
var points = new List<PrimeModelPointTest>();
string[] periods = { "AM", "PM" };
for (int i = 0; i < 7; i++)
{
var day = date.AddDays(i).Date;
if (!dtoByDate.TryGetValue(day, out var dto)) continue;
foreach (var period in periods)
{
var category = $"{day:ddd, MM/dd/yyyy} {period}";
if (!rangeCategories.Contains(category)) continue;
double? value = period == "AM" ? dto.AM_VALUE : dto.PM_VALUE;
if (value.HasValue)
{
points.Add(new PrimeModelPointTest
{
Category = category,
Value = Math.Round(value.Value)
});
}
}
}
return points;
}
public class RangeChartPointTest
{
public string Category { get; set; } = default!;
public double High { get; set; }
public double Low { get; set; }
}
public class PrimeModelPointTest
{
public string Category { get; set; } = default!;
public double Value { get; set; }
}
}