SciChart.js JavaScript 2D Charts API > 2D Chart Types > Series Text DataPoint Labels API > Data Label Positioning
Data Label Positioning

New to SciChart.js v3! DataLabels allow per data-point text labels to be drawn on series, or arbitrary text labels at x,y positions on the chart.

You can see several datalabel examples on the SciChart.js demo:

Explore these for some rich examples of how to use this API.

Label Positioning

The text positioning rules vary a little for different series. For line series the default behaviour is to place the label above the line if it is moving down, and below if it is moving up. This avoids the text overlapping the line in many situations, but often you will want to take more control.

You can disable the default behviour by setting LineSeriesDataLabelProvider.aboveBelow false, then you can make use of the horizontalTextPosition and verticalTextPosition properties along with the padding on the style.

The position properties are where the text should be relative to the data point, so horizontalTextPosition: EHorizontalTextPosition.Left means place the text to the left of the point (ie the text is anchored on the right.) This example also demonstrates the use of the SkipIfSame option for skipMode on a digital line. The other skipMode options are discussed in the section on 'Labels for many points' below.

const {
    FastLineRenderableSeries,
    ELineType,
    EDataLabelSkipMode,
    EVerticalTextPosition,
    EHorizontalTextPosition,
    Thickness
} = SciChart;

// or for npm: import { FastLineRenderableSeries, ... } from "scichart"

// Shows optional positioning modes for data labels
const lineSeries = new FastLineRenderableSeries(wasmContext, {
    stroke: "SteelBlue",
    strokeThickness: 3,
    lineType: ELineType.Digital,
    pointMarker,
    dataSeries,
    // dataLabels style must be specified to show labels
    dataLabels: {
        skipMode: EDataLabelSkipMode.SkipIfSame,
        aboveBelow: false,
        verticalTextPosition: EVerticalTextPosition.Above,
        horizontalTextPosition: EHorizontalTextPosition.Left,
        style: {
            fontFamily: "Arial",
            fontSize: 18,
            padding: new Thickness(0,5,5,0)
        },
        color: "#EEE"
    }
});

This results in the following output:

<div id="scichart-root" ></div>
  
body { margin: 0; }
#scichart-root { width: 100%; height: 100vh; }
  
async function dataLabelSkipModes(divElementId) {
    const {
        SciChartSurface,
        NumericAxis,
        EllipsePointMarker,
        XyDataSeries,
        NumberRange,
        SciChartJsNavyTheme,
    } = SciChart;

    const { sciChartSurface, wasmContext } = await SciChartSurface.create(divElementId, {
        theme: new SciChartJsNavyTheme(),
        title: "Data Labels Positioning Modes",
        titleStyle: { fontSize: 20 }
    });

    sciChartSurface.xAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0.1, 0.1) }));
    sciChartSurface.yAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0.1, 0.1) }));

    const dataSeries = new XyDataSeries(wasmContext, {
        xValues: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
        yValues: [4.3, 5, 5, 6, 8, 6.8, 7, 7, 7.2, 6.5, 6.5, 7],
    });

    const pointMarker = new EllipsePointMarker(wasmContext, {
          width: 10,
          height: 10,
          strokeThickness: 2,
          stroke: "SteelBlue",
          fill: "LightSteelBlue"});

    // #region ExampleA
    const {
        FastLineRenderableSeries,
        ELineType,
        EDataLabelSkipMode,
        EVerticalTextPosition,
        EHorizontalTextPosition,
        Thickness
    } = SciChart;

    // or for npm: import { FastLineRenderableSeries, ... } from "scichart"

    // Shows optional positioning modes for data labels
    const lineSeries = new FastLineRenderableSeries(wasmContext, {
        stroke: "SteelBlue",
        strokeThickness: 3,
        lineType: ELineType.Digital,
        pointMarker,
        dataSeries,
        // dataLabels style must be specified to show labels
        dataLabels: {
            skipMode: EDataLabelSkipMode.SkipIfSame,
            aboveBelow: false,
            verticalTextPosition: EVerticalTextPosition.Above,
            horizontalTextPosition: EHorizontalTextPosition.Left,
            style: {
                fontFamily: "Arial",
                fontSize: 18,
                padding: new Thickness(0,5,5,0)
            },
            color: "#EEE"
        }
    });
    // #endregion

    sciChartSurface.renderableSeries.add(lineSeries);
}

dataLabelSkipModes('scichart-root')

  

 

Positioning Rules for Data Labels

This table summarises the built in positioning behaviour for the various series types.

 

Series Type DataLabelProvider type Positioning rules Type specific options
Line LineSeriesDataLabelProvider If aboveBelow is true (default), place the label above the line if it is moving down, and below if it is moving up. Otherwise use horizontalTextPosition and verticalTextPosition (default: Right, Above) aboveBelow: boolean
Column / Impulse ColumnSeriesDataLabelProvider Label is centered and outside the column (above for columns above the zeroLine, below if below). positionMode can be set to Inside to reverse this, or to position to use the value of the verticalTextPosition (or horizontalTextPosition for a vertical chart) property. positionMode: EColumnDataLabelPosition
Bubble BubbleSeriesDataLabelProvider Label is centered within the bubble. If horizontalTextPosition or verticalTextPosition is not Center, label is placed outside the bubble on the specified side
Band BandSeriesDataLabelProvider By default, each line of the band has its own label which follow the rules for line series. Set singleLabel true to render a single label in the middle of the band, containing both y and y1 values. singleLabel: boolean
Heatmap HeatmapDataLabelProvider Labels are centered in the cell
NonUniformHeatmap HeatmapDataLabelProvider Labels are centered in the cell
Contours ContoursDataLabelProvider This places 10 rows of labels on the contour lines. The rows are evenly spaced. Set labelRowCount to adjust the number of rows labelRowcount: number
Text TextDataLabelProvider Labels placed above, right of the point. Set calculateTextBounds false for a performance boost if you are rendering many labels and do not care about their size. calculateTextBounds: boolean
XYScatter DataLabelProvider Labels placed above, right of the point
CandleStick/Ohlc DataLabelProvider Labels placed above, right of the close value
Stacked Series DataLabels not yet supported

Custom Positioning

To take full control of label positioning, override the dataLabelProvider.getPosition() function. This takes DataLabelState and a TSRTextBounds (a WebAssembly exported type) which describes the size of the label.

It should return a Point { x: number, y: number } which will be the left, baseline point for the label. See Native Text Api for details on TSRTextBounds.

 

Positioning Labels from Multiple Series

Normally, the layout for dataLabels is done per series, so labels from different series could overlap. If you want to prevent this or want to do some other adjustment of label positioning after all labels for all series have been generated, but before they are drawn, you can create an IDataLabelLayoutManager and attach it to the sciChartSurface.dataLabelLayoutManager property.

This has a single method, performTextLayout where you can access and update the dataLabelProvider.dataLabels array on all the series.

Although you have access to the full surface and renderPassInfo in the performTextLayout function, be aware that this is run at the very end of the render process, so only changes to the contents of the dataLabels arrays will have an effect on what is drawn. Updating other things on the surface from this function is not advised.

The example below hides labels from the second series which overlap those on the first.

const {
    SciChartSurface,
    NumericAxis,
    FastLineRenderableSeries,
    EllipsePointMarker,
    XyDataSeries,
    NumberRange,
    testIsInBounds,
    SciChartJsNavyTheme
} = SciChart;

// or, for npm, import { SciChartSurface, ... } from "scichart"

// Create a chart with two line series
//
const { sciChartSurface, wasmContext } = await SciChartSurface.create(divElementId, {
    theme: new SciChartJsNavyTheme(),
    title: "Skip overlapping labels across series",
    titleStyle: { fontSize: 20 }
});

sciChartSurface.xAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0.1, 0.1) }));
sciChartSurface.yAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0.1, 0.1) }));

const lineSeries = new FastLineRenderableSeries(wasmContext, {
    stroke: "SteelBlue",
    strokeThickness: 3,
    pointMarker: new EllipsePointMarker(wasmContext, {
        width: 10,
        height: 10,
        strokeThickness: 2,
        stroke: "SteelBlue",
        fill: "LightSteelBlue"}),
    dataSeries: new XyDataSeries(wasmContext, {
        xValues: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
        yValues: [4.3, 5, 5, 6, 8, 6.8, 7, 7, 7.2, 6.5, 6.5, 7],
    }),
    // dataLabels style must be specified to show labels
    dataLabels: {
        style: {
            fontFamily: "Arial",
            fontSize: 18,
        },
        color: "SteelBlue"
    }
});

sciChartSurface.renderableSeries.add(lineSeries);

const lineSeries2 = new FastLineRenderableSeries(wasmContext, {
    stroke: "Darkorange",
    strokeThickness: 3,
    pointMarker: new EllipsePointMarker(wasmContext, {
        width: 10,
        height: 10,
        strokeThickness: 2,
        stroke: "Darkorange",
        fill: "Tan"}),
    dataSeries: new XyDataSeries(wasmContext, {
        xValues: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
        yValues: [4.5, 4.9, 5.1, 6.2, 7, 6.5, 7, 7.5, 7.1, 6.2, 5.5, 6],
    }),
    // dataLabels style must be specified to show labels
    dataLabels: {
        style: {
            fontFamily: "Arial",
            fontSize: 18,
        },
        color: "Darkorange"
    }
});

sciChartSurface.renderableSeries.add(lineSeries2);

// Override the default data label layout manager and perform custom label layout
sciChartSurface.dataLabelLayoutManager = {
    performTextLayout(sciChartSurface, renderPassInfo) {
        const firstLabels = renderPassInfo.renderableSeriesArray[0].dataLabelProvider.dataLabels;
        const secondLabels = renderPassInfo.renderableSeriesArray[1].dataLabelProvider.dataLabels
        for (const label of secondLabels) {
            let overlap = false;
            for (const existing of firstLabels) {
                const padding = 2;
                const top = existing.rect.top - padding;
                const bottom = existing.rect.bottom + padding;
                const left = existing.rect.left - padding;
                const right = existing.rect.right + padding;
                if (testIsInBounds(label.rect.left, label.rect.top, left, bottom, right, top ) ||
                    testIsInBounds(label.rect.right, label.rect.top, left, bottom, right, top) ||
                    testIsInBounds(label.rect.left, label.rect.bottom, left, bottom, right, top) ||
                    testIsInBounds(label.rect.right, label.rect.bottom, left, bottom, right, top)) {
                    // console.log(`Label ${label.text} overlaps ${existing.text}, skipping...`);
                    overlap = true;
                    break;
                }
            } 
            if (overlap) {
                // Labels overlaps another so blank it
                label.text = "";
            }
        }
    }
}

This results in the following output:

<div id="scichart-root" ></div>
  
body { margin: 0; }
#scichart-root { width: 100%; height: 100vh; }
  
async function dataLabelGlobalLayout(divElementId) {
    // #region ExampleA
    const {
        SciChartSurface,
        NumericAxis,
        FastLineRenderableSeries,
        EllipsePointMarker,
        XyDataSeries,
        NumberRange,
        testIsInBounds,
        SciChartJsNavyTheme
    } = SciChart;

    // or, for npm, import { SciChartSurface, ... } from "scichart"

    // Create a chart with two line series
    //
    const { sciChartSurface, wasmContext } = await SciChartSurface.create(divElementId, {
        theme: new SciChartJsNavyTheme(),
        title: "Skip overlapping labels across series",
        titleStyle: { fontSize: 20 }
    });

    sciChartSurface.xAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0.1, 0.1) }));
    sciChartSurface.yAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0.1, 0.1) }));

    const lineSeries = new FastLineRenderableSeries(wasmContext, {
        stroke: "SteelBlue",
        strokeThickness: 3,
        pointMarker: new EllipsePointMarker(wasmContext, {
            width: 10,
            height: 10,
            strokeThickness: 2,
            stroke: "SteelBlue",
            fill: "LightSteelBlue"}),
        dataSeries: new XyDataSeries(wasmContext, {
            xValues: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
            yValues: [4.3, 5, 5, 6, 8, 6.8, 7, 7, 7.2, 6.5, 6.5, 7],
        }),
        // dataLabels style must be specified to show labels
        dataLabels: {
            style: {
                fontFamily: "Arial",
                fontSize: 18,
            },
            color: "SteelBlue"
        }
    });

    sciChartSurface.renderableSeries.add(lineSeries);

    const lineSeries2 = new FastLineRenderableSeries(wasmContext, {
        stroke: "Darkorange",
        strokeThickness: 3,
        pointMarker: new EllipsePointMarker(wasmContext, {
            width: 10,
            height: 10,
            strokeThickness: 2,
            stroke: "Darkorange",
            fill: "Tan"}),
        dataSeries: new XyDataSeries(wasmContext, {
            xValues: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
            yValues: [4.5, 4.9, 5.1, 6.2, 7, 6.5, 7, 7.5, 7.1, 6.2, 5.5, 6],
        }),
        // dataLabels style must be specified to show labels
        dataLabels: {
            style: {
                fontFamily: "Arial",
                fontSize: 18,
            },
            color: "Darkorange"
        }
    });

    sciChartSurface.renderableSeries.add(lineSeries2);

    // Override the default data label layout manager and perform custom label layout
    sciChartSurface.dataLabelLayoutManager = {
        performTextLayout(sciChartSurface, renderPassInfo) {
            const firstLabels = renderPassInfo.renderableSeriesArray[0].dataLabelProvider.dataLabels;
            const secondLabels = renderPassInfo.renderableSeriesArray[1].dataLabelProvider.dataLabels
            for (const label of secondLabels) {
                let overlap = false;
                for (const existing of firstLabels) {
                    const padding = 2;
                    const top = existing.rect.top - padding;
                    const bottom = existing.rect.bottom + padding;
                    const left = existing.rect.left - padding;
                    const right = existing.rect.right + padding;
                    if (testIsInBounds(label.rect.left, label.rect.top, left, bottom, right, top ) ||
                        testIsInBounds(label.rect.right, label.rect.top, left, bottom, right, top) ||
                        testIsInBounds(label.rect.left, label.rect.bottom, left, bottom, right, top) ||
                        testIsInBounds(label.rect.right, label.rect.bottom, left, bottom, right, top)) {
                        // console.log(`Label ${label.text} overlaps ${existing.text}, skipping...`);
                        overlap = true;
                        break;
                    }
                } 
                if (overlap) {
                    // Labels overlaps another so blank it
                    label.text = "";
                }
            }
        }
    }
    // #endregion ExampleA
}

dataLabelGlobalLayout('scichart-root')

  
Above: Text layout is overridden to take into account label bounds across series. For each label, if the label overlaps an existing label (or is within 2 pixels of the edge of an existing label), skip drawing the label.