How to Dynamically Color Syncfusion Blazor Chart Data Labels Based on Background for Readability

I have a scenario where the data label for a point sometimes appears on a white background (for example, when the line value is outside the range column), and the default label color is white (because most of the time it falls inside a dark blue background box so white is the most readable color for it). This makes the label invisible or very hard to read (white text on white background).


Example:

For "Wed 8/27/2025 PM", the "Prime Model" value is 15000, which falls outside the colored range box, so the label appears on the chart's white background. Since the label font is also white, it is not visible.


Minimal Repro:

https://blazorplayground.syncfusion.com/hXhyjlLhsABCnFTj


Requirement:

- I want to dynamically set the label color based on the background it appears on, so that the label is always readable (e.g., use black text on a white background, white text on a dark background, etc.).

- I know how to compute a contrasting text color if I know the background color. For example, I can use this method:

// Pass background hexColor to get appropriate text color
public static string GetContrastTextColor(string? hexColor)
{
    if (string.IsNullOrWhiteSpace(hexColor))
        return "black";
    if (!hexColor.StartsWith("#"))
    {
        return hexColor.ToLower() switch
        {
            "white" => "black",
            "black" => "white",
            "red" => "white",
            "blue" => "white",
            "yellow" => "black",
            _ => "black"
        };
    }
    hexColor = hexColor.TrimStart('#');
    if (hexColor.Length != 6) return "black";
    int r = Convert.ToInt32(hexColor.Substring(0, 2), 16);
    int g = Convert.ToInt32(hexColor.Substring(2, 2), 16);
    int b = Convert.ToInt32(hexColor.Substring(4, 2), 16);
    double luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
    return luminance > 0.6 ? "black" : "white";
}

- However, I do not know how to get the actual background color for each label at runtime, especially when the label is rendered outside the range column (on the chart background).

- I have many series and dynamic data, so I need a solution that works for any number of series and does not require hardcoding colors for each point.


Questions:

1. How can I determine the background color behind each data label (especially for line series points that may be outside the range column)?

2. Is there a built-in way in Syncfusion Blazor Charts to automatically set a readable label color based on the background, or a recommended approach for this scenario?

3. If not, is there an event or callback where I can dynamically set the label color for each point based on its position/background?


Request:

Please provide guidance or an example on how to achieve dynamic, readable label coloring based on the background in Syncfusion Blazor Charts.


Thank you!


5 Replies

AK Ashish Khanal August 22, 2025 03:43 AM UTC

Repro code (for future reference):

// __Index.razor

<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 = 15000 }
            },
            // 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

<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="Chart Label Color Question" 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; }
    }
}



DG Durga Gopalakrishnan Syncfusion Team August 22, 2025 11:47 AM UTC

Hi Ashish,


You can check the background color of chart using ChartArea Background color. For your reference, we have attached the modified sample and screenshot.


<Syncfusion.Blazor.Charts.SfChart>

    <Syncfusion.Blazor.Charts.ChartArea Background="@bgColor"></Syncfusion.Blazor.Charts.ChartArea>

</Syncfusion.Blazor.Charts.SfChart>

@code {

    public string bgColor = "white";

    public void LabelRenderEvt(Syncfusion.Blazor.Charts.TextRenderEventArgs args)

   {

    if (args.Series.Type == Syncfusion.Blazor.Charts.ChartSeriesType.Line)

    {

        if ((bgColor == "white" || bgColor == "transparent") && args.Point.Index == 6)

        {

            args.Font.Color = "black";

        }

        else

        {

            args.Font.Color = "white";

        }

    }

   }

}



Sample : https://blazorplayground.syncfusion.com/hNByDFVhqOrbYvXN


UG :

https://blazor.syncfusion.com/documentation/chart/chart-appearance#chart-area-customization

https://blazor.syncfusion.com/documentation/chart/events#ondatalabelrender


Please let us know if you have any concerns.


Regards,

Durga Gopalakrishnan.



AK Ashish Khanal August 22, 2025 02:33 PM UTC

Hi Durga,

Thank you for the response, but it looks like you misunderstood the question.

I didn't expect you to hardcode args.Point.Index because we can't know where the line label will fall into a white background.

I have added comments about this on your code:

https://blazorplayground.syncfusion.com/VDLeXlhLIrKJPbuP

    public void LabelRenderEvt(Syncfusion.Blazor.Charts.TextRenderEventArgs args)
    {
        if (args.Series.Type == Syncfusion.Blazor.Charts.ChartSeriesType.Line)
        {
            // Don't hardcode "args.Point.Index == 6" because we don't know where the line label will fall on the white background
            // So check if the background of the line label is white or transparent
            // If yes, give it black font, otherwise give it white font
            // if ((bgColor == "white" || bgColor == "transparent") && args.Point.Index == 6)
            if (bgColor == "white" || bgColor == "transparent")
            {
                args.Font.Color = "black";
            }
            else
            {
                args.Font.Color = "white";
            }
        }
    }

And when you remove that hardcoding, it doesn't work correctly.

I only need black font color for line label for Thu, 08/28/2025 PM because that label falls on white background, but it gives black font to every label in the line chart. 

Image_5862_1755873120155




AK Ashish Khanal August 23, 2025 07:19 PM UTC

In addition to detecting background color to come up with label color (the most safe and desirable approach), maybe also explore this approach:

Check the label value of the line chart and if it falls outside the high and low values of the range box, color it differently.


---


Also how to smartly place the label if it's close to getting outside the box?

Instead of putting that 17,351 on the top of line chart marker, it could have been placed on the bottom, and it'd have looked much better:

Image_3559_1755976523337

I tried this but that didn't make it any better:

<ChartDataLabel Visible="true" Position="Syncfusion.Blazor.Charts.LabelPosition.Auto">


Keep in mind I can't put  LabelPosition.Bottom  because I'll face similar issue there as well, so I need a robust solution for this.

Image_1070_1755976710318

Please help making these labels easy to read.



DG Durga Gopalakrishnan Syncfusion Team August 25, 2025 12:36 PM UTC

Ashish,


# 1 : In addition to detecting background color to come up with label color (the most safe and desirable approach), maybe also explore this approach: Check the label value of the line chart and if it falls outside the high and low values of the range box, color it differently.


We have changed the line series label color dynamically based on whether Y value lies inside or outside the High or Low value of range column series. We have attached modified sample for your reference.  


public void LabelRenderEvt(Syncfusion.Blazor.Charts.TextRenderEventArgs args)

{

    // Apply only for Line series

    if (args.Series.Type == Syncfusion.Blazor.Charts.ChartSeriesType.Line)

    {

        int pointIndex = args.Point.Index;

 

        // Get the Y value of current Line point

        double val = args.Point.YValue;

 

        // Get corresponding High/Low values from RangeColumn series

        var highVal = RangeData[pointIndex].High;

        var lowVal = RangeData[pointIndex].Low;

 

        // If line point falls inside the range box → use White

        if (val <= highVal && val >= lowVal)

        {

            args.Font.Color = "white";

        }

        else

        {

            // Falls outside → switch to Black for visibility

            args.Font.Color = "black";

        }

    }

}


Sample : https://blazorplayground.syncfusion.com/rNLItPrIAPHHpOpM


# 2 :  how to smartly place the label if it's close to getting outside the box?


Currently, we do not support comparing data labels across different series. By default, overlap detection is performed only within the data labels of the same series.


Please let us know if you have any further questions or concerns.


Loader.
Up arrow icon