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.
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).
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
# 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 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.
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).
# 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.
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.
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 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.
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.
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 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_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.
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 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.