Chapter 31. XY (Scatter) Charts

This chapter continues using the Chart2 Views example from previous chapters, but looks at how various kinds of scatter charts can be generated from spreadsheet data.

A scatter chart is a good way to display (x, y) coordinate data since the x-axis values are treated as numbers not categories. In addition, regression functions can be calculated and displayed, the axis scales can be changed, and error bars added.

The relevant lines of chart_2_views.py are:

# Chart2View.main() ofchart_2_views.py
def main(self) -> None:
    _ = Lo.load_office(connector=Lo.ConnectPipe(), opt=Lo.Options(verbose=True))

    try:
        doc = Calc.open_doc(fnm=self._data_fnm)
        GUI.set_visible(is_visible=True, odoc=doc)
        sheet = Calc.get_sheet(doc=doc)

        chart_doc = None
        if self._chart_kind == ChartKind.AREA:
            chart_doc = self._area_chart(doc=doc, sheet=sheet
        elif self._chart_kind == ChartKind.BAR:
            chart_doc = self._bar_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.BUBBLE_LABELED:
            chart_doc = self._labeled_bubble_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.COLUMN:
            chart_doc = self._col_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.COLUMN_LINE:
            chart_doc = self._col_line_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.COLUMN_MULTI:
            chart_doc = self._mult_col_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.DONUT:
            chart_doc = self._donut_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.HAPPY_STOCK:
            chart_doc = self._happy_stock_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.LINE:
            chart_doc = self._line_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.LINES:
            chart_doc = self._lines_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.NET:
            chart_doc = self._net_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.PIE:
            chart_doc = self._pie_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.PIE_3D:
            chart_doc = self._pie_3d_chart(doc=doc, sheet=sheet)
        elif self._chart_kind == ChartKind.SCATTER:
            chart_doc = self._scatter_chart(doc=doc, sheet=sheet) # sections 1-3
        elif self._chart_kind == ChartKind.SCATTER_LINE_ERROR:
            chart_doc = self._scatter_line_error_chart(doc=doc, sheet=sheet) # section 5
        elif self._chart_kind == ChartKind.SCATTER_LINE_LOG:
            chart_doc = self._scatter_line_log_chart(doc=doc, sheet=sheet) # section 4
        elif self._chart_kind == ChartKind.STOCK_PRICES:
            chart_doc = self._stock_prices_chart(doc=doc, sheet=sheet)

        # ...

31.1 A Scatter Chart (with Regressions)

_scatter_chart() in chart_2_views.py utilizes the “Ice Cream Sales vs. Temperature” table in chartsData.ods (see Fig. 269) to generate the scatter chart in Fig. 270.

The Ice Cream Sales vs. Temperature Table

Fig. 269 :The “Ice Cream Sales vs. Temperature” Table.

Scatter Chart for the Table in previous figure.

Fig. 270 :Scatter Chart for the Table in Fig. 269.

Note that the x-axis in Fig. 269 is numerical, showing values ranging between 10.0 and 26.0. This range is calculated automatically by the template.

#
def _scatter_chart(
    self, doc: XSpreadsheetDocument, sheet: XSpreadsheet
) -> XChartDocument:
    # uses the "Ice Cream Sales vs Temperature" table
    range_addr = Calc.get_address(sheet=sheet, range_name="A110:B122")
    chart_doc = Chart2.insert_chart(
        sheet=sheet,
        cells_range=range_addr,
        cell_name="C109",
        width=16,
        height=11,
        diagram_name=ChartTypes.XY.TEMPLATE_LINE.SCATTER_SYMBOL,
    )
    Calc.goto_cell(cell_name="A104", doc=doc)

    Chart2.set_title(
        chart_doc=chart_doc, title=Calc.get_string(sheet=sheet, cell_name="A109")
    )
    Chart2.set_x_axis_title(
        chart_doc=chart_doc, title=Calc.get_string(sheet=sheet, cell_name="A110")
    )
    Chart2.set_y_axis_title(
        chart_doc=chart_doc, title=Calc.get_string(sheet=sheet, cell_name="B110")
    )
    Chart2.rotate_y_axis_title(chart_doc=chart_doc, angle=Angle(90))

    # Chart2.calc_regressions(chart_doc)
    # Chart2.draw_regression_curve(chart_doc=chart_doc, curve_kind=CurveKind.LINEAR)
    return XChartDocument

If the Chart2.calc_regressions() line is uncommented then several different regression functions are calculated using the chart’s data. Their equations and R2 values are printed as shown below:

Linear regression curve:
  Curve equation: f(x) = 30.09x - 159.5
  R^2 value: 0.917

Logarithmic regression curve:
  Curve equation: f(x) = 544.1 ln(x) - 1178
  R^2 value: 0.921

Exponential regression curve:
  Curve equation: f(x) = 81.62 exp( 0.0826 x )
  R^2 value: 0.865

Power regression curve:
  Curve equation: f(x) = 4.545 x^1.525
  R^2 value: 0.906

Polynomial regression curve:
  Curve equation: f(x) =  - 0.5384x^2 + 50.24x - 340.1
  R^2 value: 0.921

Moving average regression curve:
  Curve equation: Moving average trend line with period = %PERIOD
  R^2 value: NaN

A logarithmic or quadratic polynomial are the best matches, but linear is a close third. The “moving average” R2 result is NaN (Not-a-Number) since no average of period 2 matches the data.

If the Chart2.draw_regression_curve() call is uncommented, the chart drawing will include a linear regression line and its equation and R2 value (see Fig. 271).

Scatter Chart with Linear Regression Line for the Table in Figure 1 of this chapter

Fig. 271 :Scatter Chart with Linear Regression Line for the Table in Fig. 269.

The regression function is f(x) = 30.09x - 159.47, and the C value is 0.92 (to 2 dp). If the constant (curve_kind) is changed to CurveKind.LOGARITHMIC in the call to Chart2.draw_regression_curve() then the generated function is f(x) = 544.1 ln(x) 1178 with an R2 value of 0.92. Other regression curves are represented by constants CurveKind.EXPONENTIAL, CurveKind.POWER, CurveKind.POLYNOMIAL, and CurveKind.MOVING_AVERAGE.

31.2 Calculating Regressions

Chart2.calc_regressions() is:

# in Chart2 class
@classmethod
def calc_regressions(cls, chart_doc: XChartDocument) -> None:

    def curve_info(curve_kind: CurveKind) -> None:
        curve = cls.create_curve(curve_kind=curve_kind)
        print(f"{curve_kind.label} regression curve:")
        cls.eval_curve(chart_doc=chart_doc, curve=curve)
        print()

    curve_info(CurveKind.LINEAR)
    curve_info(CurveKind.LOGARITHMIC)
    curve_info(CurveKind.EXPONENTIAL)
    curve_info(CurveKind.POWER)
    curve_info(CurveKind.POLYNOMIAL)
    curve_info(CurveKind.MOVING_AVERAGE)

Chart2.create_curve() matches the regression constants defined in CurveKind to regression services offered by the API:

# in Chart2 class
@staticmethod
def create_curve(curve_kind: CurveKind) -> XRegressionCurve:
    try:
        rc = Lo.create_instance_mcf(XRegressionCurve, curve_kind.to_namespace(), raise_err=True)
        return rc
    except Exception as e:
        raise ChartError("Error creating curve") from e

There are seven regression curve services in the chart2 module, all of which support the XRegressionCurve interface, as shown in Fig. 272.

The Regression Curve Services

Fig. 272 :The RegressionCurve Services

The RegressionCurve service shown in Fig. 272 is not a superclass for the other services. Also note that the regression curve service for power functions is called PotentialRegressionCurve.

Chart2.eval_curve() uses XRegressionCurve.getCalculator() to access the XRegressionCurveCalculator interface. It sets up the data and parameters for a particular curve, and prints the results of curve fitting:

# in Chart2 class
@classmethod
def eval_curve(cls, chart_doc: XChartDocument, curve: XRegressionCurve) -> None:
    curve_calc = curve.getCalculator()
    degree = 1
    ct = cls.get_curve_type(curve)
    if ct != CurveKind.LINEAR:
        degree = 2  # assumes POLYNOMIAL trend has degree == 2

    curve_calc.setRegressionProperties(degree, False, 0.0, 2, 0)

    data_source = cls.get_data_source(chart_doc)
    # cls.print_labled_seqs(data_source)

    xvals = cls.get_chart_data(data_source=data_source, idx=0)
    yvals = cls.get_chart_data(data_source=data_source, idx=0)
    curve_calc.recalculateRegression(xvals, yvals)

    print(f"  Curve equations: {curve_calc.getRepresentation()}")
    cc = curve_calc.getCorrelationCoefficient()
    print(f"  R^2 value: {(cc*cc):.3f}")

The calculation is configured by calling XRegressionCurveCalculator.setRegressionProperties(), and carried out by XRegressionCurveCalculator.recalculateRegression().

The degree argument of setRegressionProperties() specifies the polynomial curve’s degree, which is hard coded to be quadratic (i.e. a degree of 2). The period argument is used when a moving average curve is being fitted.

recalculateRegression() requires two arrays of x and y axis values for the scatter points. These are obtained from the chart’s data source by calling Chart2.get_data_source() which returns the XDataSource interface for the DataSeries service.

Fig. 273 shows the XDataSource, XRegressionCurveContainer, and XDataSink interfaces of the DataSeries service.

More Detailed DataSeries Service.

Fig. 273 :More Detailed DataSeries Service.

In previous chapters, only used the XDataSeries interface, which offers access to the data points in the chart. The XDataSource interface, which is read-only, gives access to the underlying data that was used to create the points. The data is stored as an array of XLabeledDataSequence objects; each object contains a label and a sequence of data.

Chart2.get_data_source() is defined as:

# in Chart2 class
@classmethod
def get_data_source(
    cls, chart_doc: XChartDocument, chart_type: ChartTypeNameBase | str = ""
) -> XDataSource:
    try:
        dsa = cls.get_data_series(chart_doc=chart_doc, chart_type=chart_type)
        ds = Lo.qi(XDataSource, dsa[0], True)
        return ds
    except NotFoundError:
        raise
    except ChartError:
        raise
    except Exception as e:
        raise ChartError("Error getting data source for chart") from e

This method assumes that the programmer wants the first data source in the data series. This is adequate for most charts which only use one data source.

Chart2.print_labeled_seqs() is a diagnostic function for printing all the labeled data sequences stored in an XDataSource:

# in Chart2 class
@staticmethod
def print_labeled_seqs(data_source: XDataSource) -> None:
    data_seqs = data_source.getDataSequences()
    print(f"No. of sequeneces in data source: {len(data_seqs)}")
    for seq in data_seqs:
        label_seq = seq.getLabel().getData()
        print(f"{label_seq[0]} :")
        vals_seq = seq.getValues().getData()
        for val in vals_seq:
            print(f"  {val}")
        print()
        sr_rep = seq.getValues().getSourceRangeRepresentation()
        print(f"  Source range: {sr_rep}")
    print()

When these function is applied to the data source for the scatter chart, the following is printed:

No. of sequences in data source: 2
Temperature °C :  14.2  16.4  11.9  15.2  18.5  22.1  19.4
                  25.1  23.4  18.1  22.6  17.2
Source range: $examples.$A$111:$A$122

Ice Cream Sales :  215.0  325.0  185.0  332.0  406.0  522.0
                   412.0  614.0  544.0  421.0  445.0  408.0
Source range: $examples.$B$111:$B$122

This output shows that the data source consists of two XLabeledDataSequence objects, representing the x and y values in the data source (see Fig. 269). These objects’ data are extracted as arrays by calls to Chart2.get_chart_data():

# in Chart2 class part of eval_curve()
# ...
data_source = cls.get_data_source(chart_doc)
cls.print_labled_seqs(data_source)

xvals = cls.get_chart_data(data_source=data_source, idx=0)
yvals = cls.get_chart_data(data_source=data_source, idx=0)
curve_calc.recalculateRegression(xvals, yvals)
# ...

When recalculateRegression() has finished, various results about the fitted curve can be extracted from the XRegressionCurveCalculator variable, curve_calc. eval_curve() prints the function string (using getRepresentation()) and the R2 value (using getCorrelationCoefficient()).

31.3 Drawing a Regression Curve

One of the surprising things about drawing a regression curve is that there’s no need to explicitly calculate the curve’s function with XRegressionCurveCalculator. Instead Chart2.draw_regression_curve() only has to initialize the curve via the data series’ XRegressionCurveContainer interface (see Fig. 273).

draw_regression_curve() is:

# in Chart2 class
@classmethod
def draw_regression_curve(
    cls, chart_doc: XChartDocument, curve_kind: CurveKind
) -> None:
    try:
        data_series_arr = cls.get_data_series(chart_doc=chart_doc)
        rc_con = Lo.qi(XRegressionCurveContainer, data_series_arr[0], True)
        curve = cls.create_curve(curve_kind)
        rc_con.addRegressionCurve(curve)

        ps = curve.getEquationProperties()
        Props.set_property(ps, "ShowCorrelationCoefficient", True)
        Props.set_property(ps, "ShowEquation", True)

        key = cls.get_number_format_key(chart_doc=chart_doc, nf_str="0.00")  # 2 dp
        if key != -1:
            Props.set_property(ps, "NumberFormat", key)
    except ChartError:
        raise
    except Exception as e:
        raise ChartError("Error drawing regression curve") from e

The XDataSeries interface for the first data series in the chart is converted to XRegressionCurveContainer, and an XRegressionCurve instance added to it. This triggers the calculation of the curve’s function. The rest of draw_regression_curve() deals with how the function information is displayed on the chart.

XRegressionCurve.getEquationProperties() returns a property set which is an instance of the RegressionCurveEquation service class, shown in Fig. 274.

The Regression Curve Equation Property Class.

Fig. 274 :The RegressionCurveEquation Property Class.

RegressionCurveEquation inherits properties related to character, fill, and line, since it controls how the curve, function string, and R2 value are drawn on the chart. These last two are made visible by setting the ShowEquation and ShowCorrelationCoefficient properties to True, which are defined in RegressionCurveEquation.

Another useful property is NumberFormat which can be used to reduce the number of decimal places used when printing the function and R2 value.

Chart2.get_number_format_key() converts a number format string into a number format key, which is assigned to the NumberFormat property:

# in Chart2 class
@staticmethod
def get_number_format_key(chart_doc: XChartDocument, nf_str: str) -> int:
    try:
        xfs = Lo.qi(XNumberFormatsSupplier, chart_doc, True)
        n_formats = xfs.getNumberFormats()
        key = int(n_formats.queryKey(nf_str, Locale("en", "us", ""), False))
        if key == -1:
            Lo.print(f'Could not access key for number format: "{nf_str}"')
        return key
    except Exception as e:
        raise ChartError("Error getting number format key") from e

The string-to-key conversion is straight forward if you know what number format string to use, but there’s little documentation on them. Probably the best approach is to use the Format -> Cells menu item in a spreadsheet document, and examine the dialog in Fig. 275.

The Format Cells Dialog

Fig. 275 :The Format Cells Dialog.

When you select a given category and format, the number format string is shown in the “Format Code” field at the bottom of the dialog. Fig. 275 shows that the format string for two decimal place numbers is 0.00. This string should be passed to get_number_format_key() in draw_regression_curve():

key = cls.get_number_format_key(chart_doc=chart_doc, nf_str="0.00")

31.4 Changing Axis Scales

Another way to understand scatter data is by changing the chart’s axis scaling. Alternatives to linear are logarithmic, exponential, or power, although is seems that the latter two cause the chart to be drawn incorrectly.

_scatter_line_log_chart() in chart_2_views.py utilizes the “Power Function Test” table in chartsData.ods (see Fig. 276).

The Power Function Test Table.

Fig. 276 :The “Power Function Test” Table.

The formula =4.1*POWER(E<number>, 3.2) is used (i.e. 4.1x3.2) to generate the Actual column from the Input column’s cells. Then I manually rounded the results and copied them into the “Output” column.

The data range passed to the Chart.insert_chart() uses the Input and Output columns of the table in Fig. 276. The generated scatter chart in Fig. 277 uses log scaling for the axes, and fits a power function to the data points.

Scatter Chart for the Table in previous figure.

Fig. 277 :Scatter Chart for the Table in Fig. 276.

The power function fits the data so well that the black regression line lies over the blue data curve. The regression function is f(x) = 3.89 x^2.32 (i.e. 3.89x2.32 ) with R2 = 1.00, which is close to the power formula used to generate the Actual column data.

_scatter_line_log_chart() is:

# Chart2View._scatter_line_log_chart() in chart_2_views.py
def _scatter_line_log_chart(
    self, doc: XSpreadsheetDocument, sheet: XSpreadsheet
) -> XChartDocument:
    # uses the "Power Function Test" table
    range_addr = Calc.get_address(sheet=sheet, range_name="E110:F120")
    chart_doc = Chart2.insert_chart(
        sheet=sheet,
        cells_range=range_addr,
        cell_name="A121",
        width=20,
        height=11,
        diagram_name=ChartTypes.XY.TEMPLATE_LINE.SCATTER_LINE_SYMBOL,
    )
    Calc.goto_cell(cell_name="A121", doc=doc)

    Chart2.set_title(
        chart_doc=chart_doc, title=Calc.get_string(sheet=sheet, cell_name="E109")
    )
    Chart2.set_x_axis_title(
        chart_doc=chart_doc, title=Calc.get_string(sheet=sheet, cell_name="E110")
    )
    Chart2.set_y_axis_title(
        chart_doc=chart_doc, title=Calc.get_string(sheet=sheet, cell_name="F110")
    )
    Chart2.rotate_y_axis_title(chart_doc=chart_doc, angle=Angle(90))

    # change x- and y- axes to log scaling
    x_axis = Chart2.scale_x_axis(chart_doc=chart_doc, scale_type=CurveKind.LOGARITHMIC)
    _ = Chart2.scale_y_axis(chart_doc=chart_doc, scale_type=CurveKind.LOGARITHMIC)
    Chart2.draw_regression_curve(chart_doc=chart_doc, curve_kind=CurveKind.POWER)
    return chart_doc

Chart2.scale_x_axis() and scale_y_axis() call the more general scale_axis() method:

# in Chart2 Class
@classmethod
def scale_x_axis(cls, chart_doc: XChartDocument, scale_type: CurveKind) -> XAxis:
    return cls.scale_axis(chart_doc=chart_doc, axis_val=AxisKind.X, idx=0, scale_type=scale_type)

@classmethod
def scale_y_axis(cls, chart_doc: XChartDocument, scale_type: CurveKind) -> XAxis:
    return cls.scale_axis(chart_doc=chart_doc, axis_val=AxisKind.Y, idx=0, scale_type=scale_type)

Chart2.scale_axis() utilizes XAxis.getScaleData() and XAxis.setScaleData() to access and modify the axis scales:

# in Chart2 Class
@classmethod
def scale_axis(
    cls, chart_doc: XChartDocument, axis_val: AxisKind, idx: int, scale_type: CurveKind
) -> XAxis:
    try:
        axis = cls.get_axis(chart_doc=chart_doc, axis_val=axis_val, idx=idx)
        sd = axis.getScaleData()
        s = None
        if scale_type == CurveKind.LINEAR:
            s = "LinearScaling"
        elif scale_type == CurveKind.LOGARITHMIC:
            s = "LogarithmicScaling"
        elif scale_type == CurveKind.EXPONENTIAL:
            s = "ExponentialScaling"
        elif scale_type == CurveKind.POWER:
            s = "PowerScaling"
        if s is None:
            Lo.print(f'Did not reconize scaling type: "{scale_type}"')
        else:
            sd.Scaling = Lo.create_instance_mcf(
                XScaling, f"com.sun.star.chart2.{s}", raise_err=True
            )
        axis.setScaleData(sd)
        return axis
    except ChartError:
        raise
    except Exception as e:
        raise ChartError("Error setting axis scale") from e

The different scaling services all support the XScaling interface, as illustrated by Fig. 278.

The Scaling Services.

Fig. 278 :The Scaling Services.

31.5 Adding Error Bars

_scatter_line_error_chart() in chart_2_views.py employs the “Impact Data : 1018 Cold Rolled” table in chartsData.ods (see Fig. 279).

The Impact Data : 1018 Cold Rolled Table.

Fig. 279 :The “Impact Data : 1018 Cold Rolled” Table.

The data range passed to the Chart.insert_chart() uses the Temperature and Mean columns of the table; the Stderr column is added separately to generate error bars along the y-axis. The resulting scatter chart is shown in Fig. 280.

Scatter Chart with Error Bars for the Table in previous figure.

Fig. 280 :Scatter Chart with Error Bars for the Table in Fig. 279.

_scatter_line_error_chart() is:

# Chart2View._scatter_line_error_chart() in chart_2_views.py
def _scatter_line_error_chart(
    self, doc: XSpreadsheetDocument, sheet: XSpreadsheet
) -> XChartDocument:
    range_addr = Calc.get_address(sheet=sheet, range_name="A142:B146")
    chart_doc = Chart2.insert_chart(
        sheet=sheet,
        cells_range=range_addr,
        cell_name="F115",
        width=14,
        height=16,
        diagram_name=ChartTypes.XY.TEMPLATE_LINE.SCATTER_LINE_SYMBOL,
    )
    Calc.goto_cell(cell_name="A123", doc=doc)

    Chart2.set_title(
        chart_doc=chart_doc, title=Calc.get_string(sheet=sheet, cell_name="A141")
    )
    Chart2.set_x_axis_title(
        chart_doc=chart_doc, title=Calc.get_string(sheet=sheet, cell_name="A142")
        )
    Chart2.set_y_axis_title(
        chart_doc=chart_doc, title="Impact Energy (Joules)"
    )
    Chart2.rotate_y_axis_title(chart_doc=chart_doc, angle=Angle(90))

    Lo.print("Adding y-axis error bars")
    sheet_name = Calc.get_sheet_name(sheet)
    error_label = f"{sheet_name}.C142"
    error_range = f"{sheet_name}.C143:C146"
    Chart2.set_y_error_bars(
        chart_doc=chart_doc, data_label=error_label, data_range=error_range
    )
    return chart_doc

The new feature in _scatter_line_error_chart() is the call to Chart2.set_y_error_bars(), which is explained over the next four subsections.

31.5.1 Creating New Chart Data

The secret to adding extra data to a chart is XDataSink.setData(). XDataSink is yet another interface for the DataSeries service (see Fig. 273).

There are several stages required, which are depicted in Fig. 281.

Using XDataSink to Add Data to a Chart.

Fig. 281 :Using XDataSink to Add Data to a Chart.

The DataProvider service produces two XDataSequence objects which are combined to become a XLabeledDataSequence object. An array of these objects is passed to XDataSink.setData().

The DataProvider service is accessed with one line of code:

dp = chart_doc.getDataProvider() # XDataProvider

Chart2.create_ld_seq() creates a XLabeledDataSequence instance from two XDataSequence objects, one acting as a label the other as data. The XDataSequence object representing the data must have its Role property set to indicate the type of the data.

# in Chart2 class
@staticmethod
def create_ld_seq(
    dp: XDataProvider, role: DataRoleKind | str, data_label: str, data_range: str
) -> XLabeledDataSequence:
    try:
        # create data sequence for the label
        lbl_seq = dp.createDataSequenceByRangeRepresentation(data_label)

        # reate data sequence for the data and role
        data_seq = dp.createDataSequenceByRangeRepresentation(data_range)

        ds_ps = Lo.qi(XPropertySet, data_seq, True)

        # specify data role (type)
        Props.set_property(ds_ps, "Role", str(role))
        # Props.show_props("Data Sequence", ds_ps)

        # create new labeled data sequence using sequences
        ld_seq = Lo.create_instance_mcf(
            XLabeledDataSequence,
            "com.sun.star.chart2.data.LabeledDataSequence",
            raise_err=True
        )
        ld_seq.setLabel(lbl_seq)
        ld_seq.setValues(data_seq)
        return ld_seq
    except Exception as e:
        raise ChartError("Error creating LD sequence") from e

Four arguments are passed to create_ld_seq(): a reference to the XDataProvider interface, a role string, a label, and a data range. For example:

sheet_name = Calc.get_sheet_name(sheet)
data_label = f"{sheet_name}.C142"
data_range = f"{sheet_name}.C143:C146"
lds = Chart2.create_ld_seq(
    dp=dp, role=DataRoleKind.ERROR_BARS_Y_POSITIVE, data_label=data_label, data_range=data_range
)

role constants are defined in DataRoleKind.

XDataSink.setData() can accept multiple XLabeledDataSequence objects in an array, making it possible to add several kinds of data to the chart at once. This is just as well since it is easier to add two XLabeledDataSequence objects, one for the error bars above the data points (i.e. up the y-axis), and another for the error bars below the points (i.e. down the y-axis). The code for doing this:

# in Chart2.set_y_error_bars(); see section 5.4 below
# convert into data sink
data_sink = Lo.qi(XDataSink, error_bars_ps, True)

dp = chart_doc.getDataProvider()
pos_err_seq = cls.create_ld_seq(
    dp=dp,
    role=DataRoleKind.ERROR_BARS_Y_POSITIVE,
    data_label=data_label,
    data_range=data_range
)
neg_err_seq = cls.create_ld_seq(
    dp=dp,
    role=DataRoleKind.ERROR_BARS_Y_NEGATIVE,
    data_label=data_label,
    data_range=data_range
)

ld_seq = (pos_err_seq, neg_err_seq)
# store the error bar data sequences in the data sink
data_sink.setData(ld_seq)

# ...

This code fragment leaves two topics unexplained: how the data sink is initially created, and how the data sink is linked to the chart.

31.5.2 Creating the Data Sink

The data sink for error bars relies on the ErrorBar service, which is shown in Fig. 282.

The ErrorBar Service

Fig. 282 :The ErrorBar Service

The ErrorBar service stores error bar properties and implements the XDataSink interface. The following code fragment creates an instance of the ErrorBar service, sets some of its properties, and converts it to an XDataSink:

# in Chart2.set_y_error_bars(); see section 5.4 below
error_bars_ps = Lo.create_instance_mcf(
    XPropertySet, "com.sun.star.chart2.ErrorBar", raise_err=True
)
Props.set(
    error_bars_ps,
    ShowPositiveError=True,
    ShowNegativeError=True,
    ErrorBarStyle=ErrorBarStyle.FROM_DATA
)

# convert into data sink
data_sink = Lo.qi(XDataSink, error_bars_ps, True)

# ...

31.5.3 Linking the Data Sink to the Chart

Once the data sink has been filled with XLabeledDataSequence objects, it can be linked to the data series in the chart. For error bars this is done via the properties ErrorBarX and ErrorBarY. For example, the following code assigns a data sink to the data series’ ErrorBarY property:

# in Chart2.set_y_error_bars(); see section 5.4 below
# ...
# store error bar in data series
data_series_arr = cls.get_data_series(chart_doc=chart_doc)
data_series = data_series_arr[0]
Props.set(data_series, ErrorBarY=error_bars_ps)
# ...

Note that the value assigned to ErrorBarY is not an XDataSink interface (i.e. not data_sink from the earlier code fragment) but its property set (i.e. props).

31.5.4 Bringing it All Together

Chart2.set_y_error_bars() combines the previous code fragments into a single method: the data sink is created (as a property set), XLabeledDataSequence data is added to it, and then the sink is linked to the chart’s data series:

# in Chart2 class
@classmethod
def set_y_error_bars(
    cls, chart_doc: XChartDocument, data_label: str, data_range: str
) -> None:
    try:
        error_bars_ps = Lo.create_instance_mcf(
            XPropertySet, "com.sun.star.chart2.ErrorBar", raise_err=True
        )
        Props.set(
            error_bars_ps,
            ShowPositiveError=True,
            ShowNegativeError=True,
            ErrorBarStyle=ErrorBarStyle.FROM_DATA
        )

        # convert into data sink
        data_sink = Lo.qi(XDataSink, error_bars_ps, True)

        # use data provider to create labelled data sequences
        # for the +/- error ranges
        dp = chart_doc.getDataProvider()

        pos_err_seq = cls.create_ld_seq(
            dp=dp,
            role=DataRoleKind.ERROR_BARS_Y_POSITIVE,
            data_label=data_label,
            data_range=data_range
        )
        neg_err_seq = cls.create_ld_seq(
            dp=dp,
            role=DataRoleKind.ERROR_BARS_Y_NEGATIVE,
            data_label=data_label,
            data_range=data_range
        )

        ld_seq = (pos_err_seq, neg_err_seq)

        # store the error bar data sequences in the data sink
        data_sink.setData(ld_seq)

        # store error bar in data series
        data_series_arr = cls.get_data_series(chart_doc=chart_doc)
        # print(f'No. of data serice: {len(data_series_arr)}')
        data_series = data_series_arr[0]
        # Props.show_obj_props("Data Series 0", data_series)
        Props.set(data_series, ErrorBarY=error_bars_ps)
    except ChartError:
        raise
    except Exception as e:
        raise ChartError("Error Setting y error bars") from e

This is not our last visit to DataSink and XDataSink. Their features show up again in the next chapter.