How to Place Chart Annotation Above Highest Series (Column or Line) for Each X Value in Multi-Series Syncfusion Blazor Chart?

I’m using a Syncfusion Blazor chart with both a column series and a line series, with the X axis as dates. For each date, I want to display an annotation label above the highest value—whether that’s the bar or the line.


Context:

- Sometimes the column is higher, sometimes the line is higher.

- The annotation should always sit above the highest point for that date, never overlapping the bar or the line.

- The annotation should never overflow outside the chart’s Y axis bounds.


Current Issue:

With `CoordinateUnits="Units.Point"` and the annotation Y set to the line value, it overlaps the bar when the bar is higher (and vice versa).

- As for overflow issue, the workaround I’ve found is to pad the Y axis maximums, but that does not feel elegant

- I don’t know how to programmatically place the annotation above the higher of the two series for each X value, without manually mapping between axes or using trial-and-error padding.

- As you can see below, for 11/11/2025, the label overlaps the bar which I don't want.

Question:

- What is the best, most robust way to always place an annotation above the highest value (bar or line) for each X value, without overlap and without the annotation overflowing the chart area?

- Is there a built-in way to get the maximum Y value for all series at a given X, or a recommended pattern for this scenario?

- Is there a way to ensure the annotation always fits within the Y axis bounds?


Minimal Repro (Syncfusion Blazor Playground):

https://blazorplayground.syncfusion.com/rtByMMsjwSLwIWeo


Thank You.


4 Replies

AK Ashish Khanal November 10, 2025 09:18 PM UTC

Repro code (for future reference): 


@using Syncfusion.Blazor.Charts

<SfChart Title="Column + Line Chart: How to put annotation above highest point (Two Y Axes)?" Width="100%">
    <ChartPrimaryXAxis ValueType="Syncfusion.Blazor.Charts.ValueType.Category" Interval="1" />
    <ChartPrimaryYAxis Name="Y1" Title="Value" Minimum="12000" Maximum="@ValueYAxisMaximum">
        <ChartAxisMajorTickLines Width="0"></ChartAxisMajorTickLines>
        <ChartAxisLineStyle Width="0"></ChartAxisLineStyle>
    </ChartPrimaryYAxis>
    <ChartPrimaryYAxis Name="Y2" OpposedPosition="true" Title="Temp" Minimum="0" Maximum="@TempYAxisMaximum">
        <ChartAxisLineStyle Width="0"></ChartAxisLineStyle>
        <ChartAxisMajorTickLines Width="0"></ChartAxisMajorTickLines>
        <ChartAxisMajorGridLines Width="0"></ChartAxisMajorGridLines>
        <ChartAxisMinorTickLines Width="0"></ChartAxisMinorTickLines>
        <ChartAxisMinorGridLines Width="0"></ChartAxisMinorGridLines>
    </ChartPrimaryYAxis>
    <ChartSeriesCollection>
        <ChartSeries DataSource="@Data"
                     XName="Date"
                     YName="BarValue"
                     Name="Bar"
                     Type="Syncfusion.Blazor.Charts.ChartSeriesType.Column"
                     YAxisName="Y1"
                     Fill="#2196F3" />
        <ChartSeries DataSource="@Data"
                     XName="Date"
                     YName="LineValue"
                     Name="Line"
                     Type="Syncfusion.Blazor.Charts.ChartSeriesType.Line"
                     YAxisName="Y2"
                     Fill="#FF9800">
                        <ChartMarker Visible="true" Height="5" Width="5">
                        <ChartDataLabel Visible="true" Position=Syncfusion.Blazor.Charts.LabelPosition.Bottom>
                            <ChartDataLabelFont Size="14px" FontWeight="Bold" Color="Red"></ChartDataLabelFont>
                        </ChartDataLabel>
                </ChartMarker>
        </ChartSeries>
    </ChartSeriesCollection>

    <ChartAnnotations>
        @foreach (var point in Data)
        {
            // CHALLENGE: How to place the annotation above the higher of the two series (Bar or Line) for each X value?
            // - The Bar series uses the "Value" axis (Y1, left), and the Line series uses the "Temp" axis (Y2, right).
            // - Currently, the annotation is placed 15% above the LineValue on the Temp axis (Y2).
            // - PROBLEM: If the BarValue (on Y1) is higher than the LineValue (on Y2), the annotation will overlap the bar.
            // - GOAL: For each X value, I want the annotation to always sit above the higher of the two series (Bar or Line), and never overlap either series.
            // - QUESTION: What is the best way to achieve this with two Y axes on different scales?
            <ChartAnnotation CoordinateUnits="Units.Point"
                             X="@point.Date"
                             Y="@((point.LineValue*1.15).ToString())"
                             YAxisName="Y2">
                <ContentTemplate>
                    <div style="background:#fff; border:1px solid #333; border-radius:4px; padding:2px 8px; font-size:13px;">
                        Label
                    </div>
                </ContentTemplate>
            </ChartAnnotation>
        }
    </ChartAnnotations>
</SfChart>

@code {
    private double TempYAxisMaximum => CalculateTempYAxisMaximum();
    private double ValueYAxisMaximum => CalculateMWYAxisMaximum();

    private double CalculateTempYAxisMaximum()
    {
        // If no data, use a sensible default for initial render
        if (!Data.Any()) return 80;

        // Find the highest AvgMaxTemp to ensure the annotation box is fully visible.
        var maxTemp = Data
            .Select(p => p.LineValue)
            .Max();

        // Add padding of 10 for annotation visibility, then round up to the next 10.
        // Example: If max temp is 66, padding makes it 76, rounding up gives 80.
        return Math.Ceiling((maxTemp + 10) / 10.0) * 10;
    }

    private double CalculateMWYAxisMaximum()
    {
        if (!Data.Any()) return 20_000;

        var maxValue = Data.Select(p => p.BarValue).Max();

        // Add padding of 2000 for annotation/data label visibility, then round up to the next 1000.
        return Math.Ceiling((maxValue + 2_000) / 1_000.0) * 1_000;
    }

    public class ChartPoint
    {
        public string Date { get; set; } = "";
        public double BarValue { get; set; } // Value axis (Y1)
        public double LineValue { get; set; } // Temp axis (Y2)
    }

    List<ChartPoint> Data = new()
    {
        new ChartPoint { Date = "Mon, 11/10/2025", BarValue = 14719, LineValue = 57 },
        new ChartPoint { Date = "Tue, 11/11/2025", BarValue = 18512, LineValue = 47 },
        new ChartPoint { Date = "Wed, 11/12/2025", BarValue = 17525, LineValue = 60 },
        new ChartPoint { Date = "Thu, 11/13/2025", BarValue = 16450, LineValue = 58 },
        new ChartPoint { Date = "Fri, 11/14/2025", BarValue = 16796, LineValue = 62 }
    };
}




DG Durga Gopalakrishnan Syncfusion Team November 11, 2025 02:02 PM UTC

Hi Ashish,


Thank you for reaching out.


We have analyzed your required scenario. Since column and line series uses different Y axes(Y1 and Y2) with different numeric ranges, so you need to normalize them to common coordinate system. We suggest you to convert both y values into the percentage of their respective axis range. Then this value can be used to determine which one is visually higher, and annotation can be placed slightly above the higher one.


We have attached the modified sample for your reference.


<ChartAnnotations>

    @foreach (var point in Data)

    {

        var barPercent = (point.BarValue - 12000) / (ValueYAxisMaximum - 12000);

        var linePercent = (point.LineValue - 0) / (TempYAxisMaximum - 0);

 

        bool isBarHigher = barPercent >= linePercent;

 

        // Convert back to actual Y value on the correct axis for annotation placement

        var yAxis = isBarHigher ? "Y1" : "Y2";

        double baseValue = isBarHigher ? point.BarValue : point.LineValue;

        double paddingFactor = isBarHigher ? 0.03 : 0.15; // Adjust separately for clarity

        double yValue = baseValue * (1 + paddingFactor);

 

        <ChartAnnotation CoordinateUnits="Units.Point"

                         X="@point.Date"

                         Y="@yValue.ToString("0.##")"

                         YAxisName="@yAxis">

            <ContentTemplate>

                <div style="background:#fff; border:1px solid #333; border-radius:4px; padding:2px 8px; font-size:13px;">

                    @($"Above {(isBarHigher ? "Bar" : "Line")}")

                </div>

            </ContentTemplate>

        </ChartAnnotation>

    }

</ChartAnnotations>




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


By default, when range is not specified, the axis interval will be calculated based on specified data points. To increase the additional range, you can specify RangePadding as Additional for Y axis.


Please let us know if you have any concerns.


Regards,

Durga Gopalakrishnan.



AK Ashish Khanal November 12, 2025 08:08 PM UTC

Hi Durga,


Thanks for your help and suggestions.


I’ve found it quite difficult to adjust the Y1/Y2 axes manually. 

- If I set the axis maximums a bit high, it leaves a large empty space at the top of the chart, making it look unbalanced. 

- Using `RangePadding="ChartRangePadding.Additional"` didn’t work well for me either—the annotations still didn’t get placed smartly, and the chart often looked awkward.


As you suggested, I’m currently using a percent-of-axis approach to decide which series is visually higher for annotation placement. However, because the Value (Y1) axis range is much larger than the Temp (Y2) axis, this method tends to favor the bar, even when the line is visually higher on the chart.

Image_3270_1762977700586

Notice for Wed, 11/12/2025, the label should have been placed above line chart, but it gets placed under it.


To compensate, I subtract 10% from the bar’s percent as a heuristic, but this is not mathematically robust and may not work for all data/range scenarios.


The only good enough solution I have so far is:


- Pad both Y axes maximums by 15% (for annotation space).

- Capture the actual plotted Y axis minimums and maximums using the `OnAxisActualRangeCalculated` event.

- Use these plotted min/max values for the percent calculation for annotation placement. Also, substract 10% from barPercent to avoid above mentioned issue.


https://blazorplayground.syncfusion.com/VjVeMMsVcBpPolMC


Is there a better way to have the chart auto-adjust the axes based on annotation placement, so I don’t have to manually pad the axis maximums or rely on this kind of heuristic? Or is this the best approach for now?


Here’s the relevant code:

@using Syncfusion.Blazor.Charts

<SfChart Title="Column + Line Chart: Annotation Above Highest Point (Two Y Axes)" Width="100%">
    <ChartEvents OnAxisActualRangeCalculated="AxisActualRangeCalculated" />
    <ChartPrimaryXAxis ValueType="Syncfusion.Blazor.Charts.ValueType.Category" Interval="1" />
    <ChartPrimaryYAxis Name="Y1" Title="Value" Minimum="12000" Maximum="@ValueYAxisMaximum">
        <ChartAxisMajorTickLines Width="0"></ChartAxisMajorTickLines>
        <ChartAxisLineStyle Width="0"></ChartAxisLineStyle>
    </ChartPrimaryYAxis>
   
    <ChartAxes>
        <ChartAxis Name="Y2" OpposedPosition="true" Maximum="@TempYAxisMaximum" Title="Temp">
            <ChartAxisLineStyle Width="0"></ChartAxisLineStyle>
            <ChartAxisMajorTickLines Width="0"></ChartAxisMajorTickLines>
            <ChartAxisMajorGridLines Width="0"></ChartAxisMajorGridLines>
            <ChartAxisMinorTickLines Width="0"></ChartAxisMinorTickLines>
            <ChartAxisMinorGridLines Width="0"></ChartAxisMinorGridLines>
        </ChartAxis>
    </ChartAxes>
    <ChartSeriesCollection>
        <ChartSeries DataSource="@Data" XName="Date" YName="BarValue" Name="Bar"
                     Type="Syncfusion.Blazor.Charts.ChartSeriesType.Column"
                     YAxisName="Y1" Fill="#2196F3" />

        <ChartSeries DataSource="@Data" XName="Date" YName="LineValue" Name="Line"
                     Type="Syncfusion.Blazor.Charts.ChartSeriesType.Line"
                     YAxisName="Y2" Fill="#FF9800">
            <ChartMarker Visible="true" Height="5" Width="5">
                <ChartDataLabel Visible="true" Position="Syncfusion.Blazor.Charts.LabelPosition.Bottom">
                    <ChartDataLabelFont Size="14px" FontWeight="Bold" Color="Red"></ChartDataLabelFont>
                </ChartDataLabel>
            </ChartMarker>
        </ChartSeries>
    </ChartSeriesCollection>

    <ChartAnnotations>
        @foreach (var point in Data)
        {
            var (yAxis, yValue) = GetAnnotationPlacementForPoint(point);
            <ChartAnnotation CoordinateUnits="Units.Point"
                             X="@point.Date"
                             Y="@yValue.ToString("0.##")"
                             YAxisName="@yAxis">
                <ContentTemplate>
                    <div style="background:#fff; border:1px solid #333; border-radius:4px; padding:2px 8px; font-size:13px;">
                        Label
                    </div>
                </ContentTemplate>
            </ChartAnnotation>
        }
    </ChartAnnotations>
</SfChart>

@code {
    private double plottedY1Min = 12000;
    private double plottedY1Max = 20000;
    private double plottedY2Min = 0;
    private double plottedY2Max = 100;

    private double TempYAxisMaximum => CalculateTempYAxisMaximum();
    private double ValueYAxisMaximum => CalculateValueYAxisMaximum();

    // Add 15% padding for space for annotation placement
    private double CalculateValueYAxisMaximum()
    {
        if (!Data.Any()) return 20000;
        var maxValue = Data.Select(p => p.BarValue).Max();
        return Math.Ceiling((maxValue * 1.15) / 1000.0) * 1000;
    }

    // Add 15% padding for space for annotation placement
    private double CalculateTempYAxisMaximum()
    {
        if (!Data.Any()) return 100;
        var maxTemp = Data.Select(p => p.LineValue).Max();
        return Math.Ceiling((maxTemp * 1.15) / 10.0) * 10;
    }

    private (string yAxis, double yValue) GetAnnotationPlacementForPoint(ChartPoint point)
    {
        // Heuristic: Subtract 10% from barPercent to compensate for the much larger Value (Y1) axis range.
        // Without this, the percent-of-axis comparison tends to favor the bar, even when the line is visually higher,
        var barPercent = (point.BarValue - plottedY1Min) / (plottedY1Max - plottedY1Min);
        var linePercent = (point.LineValue - plottedY2Min) / (plottedY2Max - plottedY2Min);

        bool isBarHigher = barPercent > linePercent;
        var yAxis = isBarHigher ? "Y1" : "Y2";
        var baseValue = isBarHigher ? point.BarValue : point.LineValue;
        var paddingFactor = isBarHigher ? 0.05 : 0.10;
        double yValue = baseValue * (1 + paddingFactor);

        // Clamp to axis max
        double axisMax = isBarHigher ? plottedY1Max : plottedY2Max;
        if (yValue > axisMax) yValue = axisMax;

        return (yAxis, yValue);
    }

    public void AxisActualRangeCalculated(AxisRangeCalculatedEventArgs args)
    {
        if (args.AxisName == "Y1")
        {
            plottedY1Min = args.Minimum;
            plottedY1Max = args.Maximum;
        }
        else if (args.AxisName == "Y2")
        {
            plottedY2Min = args.Minimum;
            plottedY2Max = args.Maximum;
        }
    }

    public class ChartPoint
    {
        public string Date { get; set; } = "";
        public double BarValue { get; set; } // Value axis (Y1)
        public double LineValue { get; set; } // Temp axis (Y2)
    }

    List<ChartPoint> Data = new()
    {
        new ChartPoint { Date = "Mon, 11/10/2025", BarValue = 14719, LineValue = 57 },
        new ChartPoint { Date = "Tue, 11/11/2025", BarValue = 18512, LineValue = 47 },
        new ChartPoint { Date = "Wed, 11/12/2025", BarValue = 17525, LineValue = 60 },
        new ChartPoint { Date = "Thu, 11/13/2025", BarValue = 16450, LineValue = 58 },
        new ChartPoint { Date = "Fri, 11/14/2025", BarValue = 16796, LineValue = 62 }
    };
}


Thanks again for your help!



DG Durga Gopalakrishnan Syncfusion Team November 13, 2025 12:38 PM UTC

Ashish,


Most welcome. We would like to clarify the behavior of axis range. By default, when an axis range is not explicitly provided, it is automatically calculated based on the available data points. When range is specified for axis, then the calculated range remains static even while updating data or any other dynamic process.


Unfortunately, we do not currently support automatically increasing the axis range based on annotation values.


For scenarios where annotations overlap or extend beyond the specified axis range, we recommend you to manually increase the axis range to ensure the annotations are fully visible without overlap.


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



Loader.
Up arrow icon