/*
	File                 : BoxPlot.cpp
	Project              : LabPlot
	Description          : Box Plot
	--------------------------------------------------------------------
	SPDX-FileCopyrightText: 2021-2022 Alexander Semke <alexander.semke@web.de>

	SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "BoxPlot.h"
#include "BoxPlotPrivate.h"
#include "backend/core/AbstractColumn.h"
#include "backend/core/Folder.h"
#include "backend/core/column/Column.h"
#include "backend/lib/XmlStreamReader.h"
#include "backend/lib/commandtemplates.h"
#include "backend/lib/trace.h"
#include "backend/spreadsheet/Spreadsheet.h"
#include "backend/worksheet/Background.h"
#include "backend/worksheet/Line.h"
#include "backend/worksheet/Worksheet.h"
#include "backend/worksheet/plots/cartesian/CartesianCoordinateSystem.h"
#include "backend/worksheet/plots/cartesian/CartesianPlot.h"
#include "backend/worksheet/plots/cartesian/Symbol.h"
#include "tools/ImageTools.h"

#include <QActionGroup>
#include <QApplication>
#include <QGraphicsSceneMouseEvent>
#include <QMenu>
#include <QPainter>

#include <KConfig>
#include <KConfigGroup>
#include <KLocalizedString>

/**
 * \class BoxPlot
 * \brief Box Plot
 */

BoxPlot::BoxPlot(const QString& name)
	: Plot(name, new BoxPlotPrivate(this), AspectType::BoxPlot) {
	init();
}

BoxPlot::BoxPlot(const QString& name, BoxPlotPrivate* dd)
	: Plot(name, dd, AspectType::BoxPlot) {
	init();
}

// no need to delete the d-pointer here - it inherits from QGraphicsItem
// and is deleted during the cleanup in QGraphicsScene
BoxPlot::~BoxPlot() = default;

void BoxPlot::init() {
	Q_D(BoxPlot);

	KConfig config;
	KConfigGroup group = config.group("BoxPlot");

	// general
	d->ordering = (BoxPlot::Ordering)group.readEntry("Ordering", (int)BoxPlot::Ordering::None);
	d->whiskersType = (BoxPlot::WhiskersType)group.readEntry("WhiskersType", (int)BoxPlot::WhiskersType::IQR);
	d->whiskersRangeParameter = group.readEntry("WhiskersIQRParameter", 1.5);
	d->orientation = (BoxPlot::Orientation)group.readEntry("Orientation", (int)BoxPlot::Orientation::Vertical);
	d->variableWidth = group.readEntry("VariableWidth", false);
	d->widthFactor = group.readEntry("WidthFactor", 1.0);
	d->notchesEnabled = group.readEntry("NotchesEnabled", false);

	// box
	d->addBackground(group);
	d->addBorderLine(group);
	d->addMedianLine(group);

	// markers
	d->symbolMean = new Symbol(QStringLiteral("symbolMean"));
	addChild(d->symbolMean);
	d->symbolMean->setHidden(true);
	d->symbolMean->init(group);
	d->symbolMean->setStyle(Symbol::Style::Square);
	connect(d->symbolMean, &Symbol::updateRequested, [=] {
		d->recalcShapeAndBoundingRect();
	});
	connect(d->symbolMean, &Symbol::updatePixmapRequested, [=] {
		d->updatePixmap();
	});

	d->symbolMedian = new Symbol(QStringLiteral("symbolMedian"));
	addChild(d->symbolMedian);
	d->symbolMedian->setHidden(true);
	d->symbolMedian->init(group);
	d->symbolMedian->setStyle(Symbol::Style::NoSymbols);
	connect(d->symbolMedian, &Symbol::updateRequested, [=] {
		d->recalcShapeAndBoundingRect();
	});
	connect(d->symbolMedian, &Symbol::updatePixmapRequested, [=] {
		d->updatePixmap();
	});

	d->symbolOutlier = new Symbol(QStringLiteral("symbolOutlier"));
	addChild(d->symbolOutlier);
	d->symbolOutlier->setHidden(true);
	d->symbolOutlier->init(group);
	connect(d->symbolOutlier, &Symbol::updateRequested, [=] {
		d->recalcShapeAndBoundingRect();
	});
	connect(d->symbolOutlier, &Symbol::updatePixmapRequested, [=] {
		d->updatePixmap();
	});

	d->symbolFarOut = new Symbol(QStringLiteral("symbolFarOut"));
	addChild(d->symbolFarOut);
	d->symbolFarOut->setHidden(true);
	d->symbolFarOut->init(group);
	d->symbolFarOut->setStyle(Symbol::Style::Plus);
	connect(d->symbolFarOut, &Symbol::updateRequested, [=] {
		d->recalcShapeAndBoundingRect();
	});
	connect(d->symbolFarOut, &Symbol::updatePixmapRequested, [=] {
		d->updatePixmap();
	});

	d->symbolData = new Symbol(QStringLiteral("symbolData"));
	addChild(d->symbolData);
	d->symbolData->setHidden(true);
	d->symbolData->init(group);
	d->symbolData->setStyle(Symbol::Style::NoSymbols);
	d->symbolData->setOpacity(0.5);
	connect(d->symbolData, &Symbol::updateRequested, [=] {
		d->recalcShapeAndBoundingRect();
	});
	connect(d->symbolData, &Symbol::updatePixmapRequested, [=] {
		d->updatePixmap();
	});

	d->symbolWhiskerEnd = new Symbol(QStringLiteral("symbolWhiskerEnd"));
	addChild(d->symbolWhiskerEnd);
	d->symbolWhiskerEnd->setHidden(true);
	d->symbolWhiskerEnd->init(group);
	d->symbolWhiskerEnd->setStyle(Symbol::Style::NoSymbols);
	connect(d->symbolWhiskerEnd, &Symbol::updateRequested, [=] {
		d->recalcShapeAndBoundingRect();
	});
	connect(d->symbolWhiskerEnd, &Symbol::updatePixmapRequested, [=] {
		d->updatePixmap();
	});

	d->jitteringEnabled = group.readEntry("JitteringEnabled", true);

	// whiskers
	d->whiskersLine = new Line(QString());
	d->whiskersLine->setPrefix(QLatin1String("Whiskers"));
	d->whiskersLine->setCreateXmlElement(false); // whiskers element is created in BoxPlot::save()
	d->whiskersLine->setHidden(true);
	addChild(d->whiskersLine);
	d->whiskersLine->init(group);
	connect(d->whiskersLine, &Line::updatePixmapRequested, [=] {
		d->updatePixmap();
	});
	connect(d->whiskersLine, &Line::updateRequested, [=] {
		d->recalcShapeAndBoundingRect();
	});

	d->whiskersCapSize = group.readEntry("WhiskersCapSize", Worksheet::convertToSceneUnits(5.0, Worksheet::Unit::Point));
	d->whiskersCapLine = new Line(QString());
	d->whiskersCapLine->setPrefix(QLatin1String("WhiskersCap"));
	d->whiskersCapLine->setCreateXmlElement(false); // whiskers cap element is created in BoxPlot::save()
	d->whiskersCapLine->setHidden(true);
	addChild(d->whiskersCapLine);
	d->whiskersCapLine->init(group);
	connect(d->whiskersCapLine, &Line::updatePixmapRequested, [=] {
		d->updatePixmap();
	});
	connect(d->whiskersCapLine, &Line::updateRequested, [=] {
		d->recalcShapeAndBoundingRect();
	});

	// marginal plots (rug, BoxPlot, boxplot)
	d->rugEnabled = group.readEntry("RugEnabled", false);
	d->rugLength = group.readEntry("RugLength", Worksheet::convertToSceneUnits(5, Worksheet::Unit::Point));
	d->rugWidth = group.readEntry("RugWidth", 0.0);
	d->rugOffset = group.readEntry("RugOffset", 0.0);
}

/*!
	Returns an icon to be used in the project explorer.
*/
QIcon BoxPlot::icon() const {
	// 	return QIcon::fromTheme(QLatin1String("draw-line"));
	return BoxPlot::staticIcon();
}

QIcon BoxPlot::staticIcon() {
	QPainter pa;
	pa.setRenderHint(QPainter::Antialiasing);
	int iconSize = 20;
	QPixmap pm(iconSize, iconSize);

	QPen pen(Qt::SolidLine);
	pen.setColor(DARKMODE ? Qt::white : Qt::black);
	pen.setWidthF(0.0);

	pm.fill(Qt::transparent);
	pa.begin(&pm);
	pa.setPen(pen);
	pa.drawRect(6, 6, 8, 8); // box
	pa.drawLine(10, 6, 10, 0); // upper whisker
	pa.drawLine(10, 14, 10, 20); // lower whisker
	pa.end();

	return {pm};
}

void BoxPlot::initActions() {
	visibilityAction = new QAction(QIcon::fromTheme(QStringLiteral("view-visible")), i18n("Visible"), this);
	visibilityAction->setCheckable(true);
	connect(visibilityAction, &QAction::triggered, this, &BoxPlot::visibilityChangedSlot);

	// Orientation
	auto* orientationActionGroup = new QActionGroup(this);
	orientationActionGroup->setExclusive(true);
	connect(orientationActionGroup, &QActionGroup::triggered, this, &BoxPlot::orientationChangedSlot);

	orientationHorizontalAction = new QAction(QIcon::fromTheme(QLatin1String("transform-move-horizontal")), i18n("Horizontal"), orientationActionGroup);
	orientationHorizontalAction->setCheckable(true);

	orientationVerticalAction = new QAction(QIcon::fromTheme(QLatin1String("transform-move-vertical")), i18n("Vertical"), orientationActionGroup);
	orientationVerticalAction->setCheckable(true);
}

void BoxPlot::initMenus() {
	this->initActions();

	// Orientation
	orientationMenu = new QMenu(i18n("Orientation"));
	orientationMenu->setIcon(QIcon::fromTheme(QLatin1String("draw-cross")));
	orientationMenu->addAction(orientationHorizontalAction);
	orientationMenu->addAction(orientationVerticalAction);
}

QMenu* BoxPlot::createContextMenu() {
	if (!orientationMenu)
		initMenus();

	QMenu* menu = WorksheetElement::createContextMenu();
	QAction* firstAction = menu->actions().at(1); // skip the first action because of the "title-action"

	// Visibility
	visibilityAction->setChecked(isVisible());
	menu->insertAction(firstAction, visibilityAction);

	// Orientation
	Q_D(const BoxPlot);
	if (d->orientation == Orientation::Horizontal)
		orientationHorizontalAction->setChecked(true);
	else
		orientationVerticalAction->setChecked(true);
	menu->insertMenu(firstAction, orientationMenu);
	menu->insertSeparator(firstAction);

	return menu;
}

QGraphicsItem* BoxPlot::graphicsItem() const {
	return d_ptr;
}

void BoxPlot::retransform() {
	Q_D(BoxPlot);
	d->retransform();
}

void BoxPlot::recalc() {
	Q_D(BoxPlot);
	d->recalc();
}

void BoxPlot::handleResize(double /*horizontalRatio*/, double /*verticalRatio*/, bool /*pageResize*/) {
}

bool BoxPlot::activatePlot(QPointF mouseScenePos, double maxDist) {
	Q_D(BoxPlot);
	return d->activatePlot(mouseScenePos, maxDist);
}

void BoxPlot::setHover(bool on) {
	Q_D(BoxPlot);
	d->setHover(on);
}

/*!
 * creates a new spreadsheet having the data with the positions and the values of the bins.
 * the new spreadsheet is added to the current folder.
 */
void BoxPlot::createDataSpreadsheet() {
	if (dataColumns().isEmpty())
		return;

	// create a new spreadsheet for the following 9 metrics:
	// index
	// 1st quartile
	// 3rd quartile
	// median
	// whiskers min
	// whiskers max
	// data points count
	// outliers count
	// far out points count

	auto* spreadsheet = new Spreadsheet(i18n("%1 - Data", name()));
	spreadsheet->setColumnCount(9);
	spreadsheet->setRowCount(dataColumns().count());
	spreadsheet->column(0)->setColumnMode(AbstractColumn::ColumnMode::Integer);

	spreadsheet->column(0)->setName(i18n("index"));
	spreadsheet->column(1)->setName(i18n("1st quartile"));
	spreadsheet->column(2)->setName(i18n("3rd quartile"));
	spreadsheet->column(3)->setName(i18n("median"));
	spreadsheet->column(4)->setName(i18n("whiskers min"));
	spreadsheet->column(5)->setName(i18n("whiskers max"));
	spreadsheet->column(6)->setName(i18n("data points count"));
	spreadsheet->column(7)->setName(i18n("outliers count"));
	spreadsheet->column(8)->setName(i18n("far out points count"));

	Q_D(const BoxPlot);
	d->fillDataSpreadsheet(spreadsheet);

	// add the new spreadsheet to the current folder
	folder()->addChild(spreadsheet);
}

/* ============================ getter methods ================= */
// general
BASIC_SHARED_D_READER_IMPL(BoxPlot, QVector<const AbstractColumn*>, dataColumns, dataColumns)
BASIC_SHARED_D_READER_IMPL(BoxPlot, BoxPlot::Ordering, ordering, ordering)
BASIC_SHARED_D_READER_IMPL(BoxPlot, BoxPlot::Orientation, orientation, orientation)
BASIC_SHARED_D_READER_IMPL(BoxPlot, bool, variableWidth, variableWidth)
BASIC_SHARED_D_READER_IMPL(BoxPlot, double, widthFactor, widthFactor)
BASIC_SHARED_D_READER_IMPL(BoxPlot, bool, notchesEnabled, notchesEnabled)

// box filling
Background* BoxPlot::backgroundAt(int index) const {
	Q_D(const BoxPlot);
	if (index < d->backgrounds.size())
		return d->backgrounds.at(index);
	else
		return nullptr;
}

// box border line
Line* BoxPlot::borderLineAt(int index) const {
	Q_D(const BoxPlot);
	if (index < d->borderLines.size())
		return d->borderLines.at(index);
	else
		return nullptr;
}

// median line
Line* BoxPlot::medianLineAt(int index) const {
	Q_D(const BoxPlot);
	if (index < d->medianLines.size())
		return d->medianLines.at(index);
	else
		return nullptr;
}

// markers
Symbol* BoxPlot::symbolMean() const {
	Q_D(const BoxPlot);
	return d->symbolMean;
}

Symbol* BoxPlot::symbolMedian() const {
	Q_D(const BoxPlot);
	return d->symbolMedian;
}

Symbol* BoxPlot::symbolOutlier() const {
	Q_D(const BoxPlot);
	return d->symbolOutlier;
}

Symbol* BoxPlot::symbolFarOut() const {
	Q_D(const BoxPlot);
	return d->symbolFarOut;
}

BASIC_SHARED_D_READER_IMPL(BoxPlot, bool, jitteringEnabled, jitteringEnabled)

Symbol* BoxPlot::symbolData() const {
	Q_D(const BoxPlot);
	return d->symbolData;
}

Symbol* BoxPlot::symbolWhiskerEnd() const {
	Q_D(const BoxPlot);
	return d->symbolWhiskerEnd;
}

// whiskers
BASIC_SHARED_D_READER_IMPL(BoxPlot, BoxPlot::WhiskersType, whiskersType, whiskersType)
BASIC_SHARED_D_READER_IMPL(BoxPlot, double, whiskersRangeParameter, whiskersRangeParameter)
BASIC_SHARED_D_READER_IMPL(BoxPlot, double, whiskersCapSize, whiskersCapSize)

Line* BoxPlot::whiskersLine() const {
	Q_D(const BoxPlot);
	return d->whiskersLine;
}

Line* BoxPlot::whiskersCapLine() const {
	Q_D(const BoxPlot);
	return d->whiskersCapLine;
}

// margin plots
BASIC_SHARED_D_READER_IMPL(BoxPlot, bool, rugEnabled, rugEnabled)
BASIC_SHARED_D_READER_IMPL(BoxPlot, double, rugLength, rugLength)
BASIC_SHARED_D_READER_IMPL(BoxPlot, double, rugWidth, rugWidth)
BASIC_SHARED_D_READER_IMPL(BoxPlot, double, rugOffset, rugOffset)

QVector<QString>& BoxPlot::dataColumnPaths() const {
	D(BoxPlot);
	return d->dataColumnPaths;
}

double BoxPlot::minimum(const Dimension dim) const {
	Q_D(const BoxPlot);
	switch (dim) {
	case Dimension::X:
		return d->xMin;
	case Dimension::Y:
		return d->yMin;
	}
	return NAN;
}

double BoxPlot::maximum(const Dimension dim) const {
	Q_D(const BoxPlot);
	switch (dim) {
	case Dimension::X:
		return d->xMax;
	case Dimension::Y:
		return d->yMax;
	}
	return NAN;
}

/* ============================ setter methods and undo commands ================= */

// General
STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetDataColumns, QVector<const AbstractColumn*>, dataColumns, recalc)
void BoxPlot::setDataColumns(const QVector<const AbstractColumn*> columns) {
	Q_D(BoxPlot);
	if (columns != d->dataColumns) {
		exec(new BoxPlotSetDataColumnsCmd(d, columns, ki18n("%1: set data columns")));

		for (auto* column : columns) {
			if (!column)
				continue;

			connect(column, &AbstractColumn::dataChanged, this, &BoxPlot::dataChanged);
			connect(column, &AbstractAspect::aspectDescriptionChanged, this, &Plot::updateLegendRequested);

			// update the curve itself on changes
			connect(column, &AbstractColumn::dataChanged, this, &BoxPlot::recalc);
			connect(column->parentAspect(), &AbstractAspect::aspectAboutToBeRemoved, this, &BoxPlot::dataColumnAboutToBeRemoved);
			// TODO: add disconnect in the undo-function
		}
	}
}

STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetOrdering, BoxPlot::Ordering, ordering, recalc)
void BoxPlot::setOrdering(BoxPlot::Ordering ordering) {
	Q_D(BoxPlot);
	if (ordering != d->ordering)
		exec(new BoxPlotSetOrderingCmd(d, ordering, ki18n("%1: set ordering")));
}

STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetOrientation, BoxPlot::Orientation, orientation, recalc)
void BoxPlot::setOrientation(BoxPlot::Orientation orientation) {
	Q_D(BoxPlot);
	if (orientation != d->orientation)
		exec(new BoxPlotSetOrientationCmd(d, orientation, ki18n("%1: set orientation")));
}

STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetVariableWidth, bool, variableWidth, recalc)
void BoxPlot::setVariableWidth(bool variableWidth) {
	Q_D(BoxPlot);
	if (variableWidth != d->variableWidth)
		exec(new BoxPlotSetVariableWidthCmd(d, variableWidth, ki18n("%1: variable width changed")));
}

STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetWidthFactor, double, widthFactor, recalc)
void BoxPlot::setWidthFactor(double widthFactor) {
	Q_D(BoxPlot);
	if (widthFactor != d->widthFactor)
		exec(new BoxPlotSetWidthFactorCmd(d, widthFactor, ki18n("%1: width factor changed")));
}

STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetNotchesEnabled, bool, notchesEnabled, recalc)
void BoxPlot::setNotchesEnabled(bool notchesEnabled) {
	Q_D(BoxPlot);
	if (notchesEnabled != d->notchesEnabled)
		exec(new BoxPlotSetNotchesEnabledCmd(d, notchesEnabled, ki18n("%1: changed notches")));
}

// whiskers
STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetWhiskersType, BoxPlot::WhiskersType, whiskersType, recalc)
void BoxPlot::setWhiskersType(BoxPlot::WhiskersType type) {
	Q_D(BoxPlot);
	if (type != d->whiskersType)
		exec(new BoxPlotSetWhiskersTypeCmd(d, type, ki18n("%1: set whiskers type")));
}

STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetWhiskersRangeParameter, double, whiskersRangeParameter, recalc)
void BoxPlot::setWhiskersRangeParameter(double k) {
	Q_D(BoxPlot);
	if (k != d->whiskersRangeParameter)
		exec(new BoxPlotSetWhiskersRangeParameterCmd(d, k, ki18n("%1: set whiskers range parameter")));
}

STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetWhiskersCapSize, double, whiskersCapSize, recalc)
void BoxPlot::setWhiskersCapSize(double size) {
	Q_D(BoxPlot);
	if (size != d->whiskersCapSize)
		exec(new BoxPlotSetWhiskersCapSizeCmd(d, size, ki18n("%1: set whiskers cap size")));
}

// Symbols
STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetJitteringEnabled, bool, jitteringEnabled, recalc)
void BoxPlot::setJitteringEnabled(bool enabled) {
	Q_D(BoxPlot);
	if (enabled != d->jitteringEnabled)
		exec(new BoxPlotSetJitteringEnabledCmd(d, enabled, ki18n("%1: jitterring changed")));
}

// margin plots
STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetRugEnabled, bool, rugEnabled, updateRug)
void BoxPlot::setRugEnabled(bool enabled) {
	Q_D(BoxPlot);
	if (enabled != d->rugEnabled)
		exec(new BoxPlotSetRugEnabledCmd(d, enabled, ki18n("%1: change rug enabled")));
}

STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetRugWidth, double, rugWidth, updatePixmap)
void BoxPlot::setRugWidth(double width) {
	Q_D(BoxPlot);
	if (width != d->rugWidth)
		exec(new BoxPlotSetRugWidthCmd(d, width, ki18n("%1: change rug width")));
}

STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetRugLength, double, rugLength, updateRug)
void BoxPlot::setRugLength(double length) {
	Q_D(BoxPlot);
	if (length != d->rugLength)
		exec(new BoxPlotSetRugLengthCmd(d, length, ki18n("%1: change rug length")));
}

STD_SETTER_CMD_IMPL_F_S(BoxPlot, SetRugOffset, double, rugOffset, updateRug)
void BoxPlot::setRugOffset(double offset) {
	Q_D(BoxPlot);
	if (offset != d->rugOffset)
		exec(new BoxPlotSetRugOffsetCmd(d, offset, ki18n("%1: change rug offset")));
}

//##############################################################################
//#################################  SLOTS  ####################################
//##############################################################################

void BoxPlot::dataColumnAboutToBeRemoved(const AbstractAspect* aspect) {
	Q_D(BoxPlot);
	for (int i = 0; i < d->dataColumns.size(); ++i) {
		if (aspect == d->dataColumns.at(i)) {
			d->dataColumns[i] = nullptr;
			d->retransform();
			break;
		}
	}
}

//##############################################################################
//######  SLOTs for changes triggered via QActions in the context menu  ########
//##############################################################################
void BoxPlot::orientationChangedSlot(QAction* action) {
	if (action == orientationHorizontalAction)
		this->setOrientation(Axis::Orientation::Horizontal);
	else
		this->setOrientation(Axis::Orientation::Vertical);
}

void BoxPlot::visibilityChangedSlot() {
	Q_D(const BoxPlot);
	this->setVisible(!d->isVisible());
}

//##############################################################################
//####################### Private implementation ###############################
//##############################################################################
BoxPlotPrivate::BoxPlotPrivate(BoxPlot* owner)
	: PlotPrivate(owner)
	, q(owner) {
	setFlag(QGraphicsItem::ItemIsSelectable);
	setAcceptHoverEvents(false);
}

bool BoxPlotPrivate::activatePlot(QPointF mouseScenePos, double /*maxDist*/) {
	if (!isVisible())
		return false;

	return shape().contains(mouseScenePos);
}

void BoxPlotPrivate::setHover(bool on) {
	if (on == m_hovered)
		return; // don't update if state not changed

	m_hovered = on;
	on ? Q_EMIT q->hovered() : emit q->unhovered();
	update();
}

Background* BoxPlotPrivate::addBackground(const KConfigGroup& group) {
	auto* background = new Background(QString());
	background->setPrefix(QLatin1String("Filling"));
	background->setEnabledAvailable(true);
	background->setHidden(true);
	q->addChild(background);

	if (!q->isLoading())
		background->init(group);

	q->connect(background, &Background::updateRequested, [=] {
		updatePixmap();
		// TODO: Q_EMIT q->updateLegendRequested();
	});

	backgrounds << background;

	return background;
}

Line* BoxPlotPrivate::addBorderLine(const KConfigGroup& group) {
	auto* line = new Line(QString());
	line->setPrefix(QLatin1String("Border"));
	line->setHidden(true);
	q->addChild(line);
	if (!q->isLoading())
		line->init(group);

	q->connect(line, &Line::updatePixmapRequested, [=] {
		updatePixmap();
		// Q_EMIT q->updateLegendRequested();
	});

	q->connect(line, &Line::updateRequested, [=] {
		recalcShapeAndBoundingRect();
		// Q_EMIT q->updateLegendRequested();
	});

	borderLines << line;

	return line;
}

Line* BoxPlotPrivate::addMedianLine(const KConfigGroup& group) {
	auto* line = new Line(QString());
	line->setPrefix(QLatin1String("MedianLine"));
	line->setHidden(true);
	q->addChild(line);
	if (!q->isLoading())
		line->init(group);

	q->connect(line, &Line::updatePixmapRequested, [=] {
		updatePixmap();
		// Q_EMIT q->updateLegendRequested();
	});

	q->connect(line, &Line::updateRequested, [=] {
		recalcShapeAndBoundingRect();
		// Q_EMIT q->updateLegendRequested();
	});

	medianLines << line;

	return line;
}

void BoxPlotPrivate::fillDataSpreadsheet(Spreadsheet* spreadsheet) const {
	// index
	// 1st quartile
	// 3rd quartile
	// median
	// whiskers min
	// whiskers max
	// data points count
	// outliers count
	// far out points count

	for (int i = 0; i < q->dataColumns().count(); ++i) {
		const auto* column = static_cast<const Column*>(q->dataColumns().at(i));
		const auto& statistics = column->statistics();

		spreadsheet->column(0)->setIntegerAt(i, i + 1);
		spreadsheet->column(1)->setValueAt(i, statistics.firstQuartile);
		spreadsheet->column(2)->setValueAt(i, statistics.thirdQuartile);
		spreadsheet->column(3)->setValueAt(i, statistics.median);
		spreadsheet->column(4)->setValueAt(i, m_whiskerMin.at(i));
		spreadsheet->column(5)->setValueAt(i, m_whiskerMax.at(i));
		spreadsheet->column(6)->setValueAt(i, m_dataPointsLogical.at(i).count());
		spreadsheet->column(7)->setValueAt(i, m_outlierPointsLogical.at(i).count());
		spreadsheet->column(8)->setValueAt(i, m_farOutPointsLogical.at(i).count());
	}
}

/*!
 * adds additional elements for background and line properties according to the current
 * number of data columns to be plotted.
 */
void BoxPlotPrivate::adjustPropertiesContainers() {
	int diff = dataColumns.size() - backgrounds.size();
	if (diff > 0) {
		// one more box needs to be added
		KConfig config;
		KConfigGroup group = config.group(QLatin1String("XYCurve"));
		const auto* plot = static_cast<const CartesianPlot*>(q->parentAspect());

		for (int i = 0; i < diff; ++i) {
			// box filling and border line
			auto* background = addBackground(group);
			auto* borderLine = addBorderLine(group);
			auto* medianLine = addMedianLine(group);

			if (plot) {
				const auto& themeColor = plot->themeColorPalette(backgrounds.count() - 1);
				background->setFirstColor(themeColor);
				borderLine->setColor(themeColor);
				medianLine->setColor(themeColor);
			}
		}
	}
}

/*!
  called when the size of the plot or its data ranges (manual changes, zooming, etc.) were changed.
  recalculates the position of the scene points to be drawn.
  triggers the update of lines, drop lines, symbols etc.
*/
void BoxPlotPrivate::retransform() {
	if (suppressRetransform || !isVisible() || q->isLoading())
		return;

	PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));

	const int count = dataColumns.size();
	if (!count || m_boxRect.size() != count) {
		// no columns or recalc() was not called yet, nothing to do
		recalcShapeAndBoundingRect();
		return;
	}

	// clear the containers holding the information in scene coordinates
	for (int i = 0; i < count; ++i) {
		m_boxRect[i].clear();
		m_medianLine[i] = QLineF();
		m_whiskersPath[i] = QPainterPath();
		m_whiskersCapPath[i] = QPainterPath();
		m_rugPath[i] = QPainterPath();
		m_outlierPoints[i].clear();
		m_dataPoints[i].clear();
		m_farOutPoints[i].clear();
		m_whiskerEndPoints[i].clear();
	}

	if (count) {
		if (orientation == BoxPlot::Orientation::Vertical) {
			for (int i = 0; i < count; ++i) {
				if (dataColumns.at(i))
					verticalBoxPlot(i);
			}
		} else {
			for (int i = 0; i < count; ++i) {
				if (dataColumns.at(i))
					horizontalBoxPlot(i);
			}
		}

		updateRug();
	}

	recalcShapeAndBoundingRect();
}

void BoxPlotPrivate::recalc() {
	PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));

	// resize the internal containers
	const int count = dataColumns.size();
	m_boxRect.resize(count);
	m_fillPolygon.resize(count);
	m_xMinBox.resize(count);
	m_xMaxBox.resize(count);
	m_yMinBox.resize(count);
	m_yMaxBox.resize(count);
	m_median.resize(count);
	m_medianLine.resize(count);
	m_whiskersPath.resize(count);
	m_whiskersCapPath.resize(count);
	m_rugPath.resize(count);
	m_whiskerMin.resize(count);
	m_whiskerMax.resize(count);
	m_outlierPointsLogical.resize(count);
	m_outlierPoints.resize(count);
	m_dataPointsLogical.resize(count);
	m_dataPoints.resize(count);
	m_farOutPointsLogical.resize(count);
	m_farOutPoints.resize(count);
	m_whiskerEndPointsLogical.resize(count);
	m_whiskerEndPoints.resize(count);
	m_mean.resize(count);
	m_meanPointLogical.resize(count);
	m_meanPoint.resize(count);
	m_meanPointVisible.resize(count);
	m_medianPointLogical.resize(count);
	m_medianPoint.resize(count);
	m_medianPointVisible.resize(count);

	// box properties
	int diff = count - backgrounds.size();
	if (diff > 0)
		adjustPropertiesContainers();
	else if (diff < 0) {
		// the last box was deleted
		// TODO:
	}

	// calculate the new min and max values of the box plot
	// for the current sizes of the box and of the whiskers
	if (orientation == BoxPlot::Orientation::Vertical) {
		xMin = 0.5;
		xMax = count + 0.5;
		yMin = INFINITY;
		yMax = -INFINITY;
	} else { // horizontal
		xMin = INFINITY;
		xMax = -INFINITY;
		yMin = 0.5;
		yMax = count + 0.5;
	}

	if (variableWidth) {
		m_widthScaleFactor = -INFINITY;
		for (const auto* col : dataColumns) {
			auto* column = static_cast<const Column*>(col);
			if (column->statistics().size > m_widthScaleFactor)
				m_widthScaleFactor = column->statistics().size;
		}
		m_widthScaleFactor = std::sqrt(m_widthScaleFactor);
	}

	if (ordering == BoxPlot::Ordering::None)
		dataColumnsOrdered = dataColumns;
	else {
		std::vector<std::pair<double, int>> newOrdering;

		if (ordering == BoxPlot::Ordering::MedianAscending || ordering == BoxPlot::Ordering::MedianDescending) {
			for (int i = 0; i < count; ++i) {
				auto* column = static_cast<const Column*>(dataColumns.at(i));
				newOrdering.push_back(std::make_pair(column->statistics().median, i));
			}
		} else {
			for (int i = 0; i < count; ++i) {
				auto* column = static_cast<const Column*>(dataColumns.at(i));
				newOrdering.push_back(std::make_pair(column->statistics().arithmeticMean, i));
			}
		}

		std::sort(newOrdering.begin(), newOrdering.end());
		dataColumnsOrdered.clear();

		if (ordering == BoxPlot::Ordering::MedianAscending || ordering == BoxPlot::Ordering::MeanAscending) {
			for (int i = 0; i < count; ++i)
				dataColumnsOrdered << dataColumns.at(newOrdering.at(i).second);
		} else {
			for (int i = count - 1; i >= 0; --i)
				dataColumnsOrdered << dataColumns.at(newOrdering.at(i).second);
		}
	}

	for (int i = 0; i < count; ++i)
		recalc(i);

	// the size of the boxplot changed because of the actual
	// data changes or because of new boxplot settings.
	// Q_EMIT dataChanged() in order to recalculate everything
	// in the parent plot with the new size/shape of the boxplot
	Q_EMIT q->dataChanged();
}

QPointF BoxPlotPrivate::setOutlierPoint(double pos, double value) {
	QPointF point;
	if (orientation == BoxPlot::Orientation::Vertical) {
		point.setX(pos);
		point.setY(value);

		if (value > yMax)
			yMax = value;
		else if (value < yMin)
			yMin = value;
	} else {
		point.setX(value);
		point.setY(pos);

		if (value > xMax)
			xMax = value;
		else if (value < xMin)
			xMin = value;
	}

	return point;
}

void BoxPlotPrivate::recalc(int index) {
	PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));
	auto* column = static_cast<const Column*>(dataColumnsOrdered.at(index));
	if (!column)
		return;

	// clear the containers for outliers, etc. since their number
	// can be changed because of the new settings for whiskers, etc.
	m_outlierPointsLogical[index].clear();
	m_outlierPoints[index].clear();
	m_dataPointsLogical[index].clear();
	m_dataPoints[index].clear();
	m_farOutPointsLogical[index].clear();
	m_farOutPoints[index].clear();
	m_whiskerEndPointsLogical[index].clear();
	m_whiskerEndPoints[index].clear();

	const auto& statistics = column->statistics();
	double width = 0.5 * widthFactor;
	if (variableWidth && m_widthScaleFactor != 0)
		width *= std::sqrt(statistics.size) / m_widthScaleFactor;

	const double x = index + 1.0;

	// box
	if (orientation == BoxPlot::Orientation::Vertical) {
		m_xMinBox[index] = x - width / 2;
		m_xMaxBox[index] = x + width / 2;
		m_yMinBox[index] = statistics.firstQuartile;
		m_yMaxBox[index] = statistics.thirdQuartile;
	} else {
		m_xMinBox[index] = statistics.firstQuartile;
		m_xMaxBox[index] = statistics.thirdQuartile;
		m_yMinBox[index] = x - width / 2;
		m_yMaxBox[index] = x + width / 2;
	}

	// mean and median
	m_median[index] = statistics.median;
	m_mean[index] = statistics.arithmeticMean;

	// whiskers
	switch (whiskersType) {
	case BoxPlot::WhiskersType::MinMax: {
		m_whiskerMax[index] = statistics.maximum;
		m_whiskerMin[index] = statistics.minimum;
		break;
	}
	case BoxPlot::WhiskersType::IQR: {
		m_whiskerMax[index] = statistics.thirdQuartile + whiskersRangeParameter * statistics.iqr;
		m_whiskerMin[index] = statistics.firstQuartile - whiskersRangeParameter * statistics.iqr;
		break;
	}
	case BoxPlot::WhiskersType::SD: {
		m_whiskerMax[index] = statistics.arithmeticMean + whiskersRangeParameter * statistics.standardDeviation;
		m_whiskerMin[index] = statistics.arithmeticMean - whiskersRangeParameter * statistics.standardDeviation;
		break;
	}
	case BoxPlot::WhiskersType::MAD: {
		m_whiskerMax[index] = statistics.median + whiskersRangeParameter * statistics.meanDeviationAroundMedian;
		m_whiskerMin[index] = statistics.median - whiskersRangeParameter * statistics.meanDeviationAroundMedian;
		break;
	}
	case BoxPlot::WhiskersType::PERCENTILES_1_99: {
		m_whiskerMax[index] = statistics.percentile_99;
		m_whiskerMin[index] = statistics.percentile_1;
		break;
	}
	case BoxPlot::WhiskersType::PERCENTILES_5_95: {
		m_whiskerMax[index] = statistics.percentile_95;
		m_whiskerMin[index] = statistics.percentile_5;
		break;
	}
	case BoxPlot::WhiskersType::PERCENTILES_10_90: {
		m_whiskerMax[index] = statistics.percentile_90;
		m_whiskerMin[index] = statistics.percentile_10;
		break;
	}
	}

	// outliers symbols
	if (orientation == BoxPlot::Orientation::Vertical) {
		if (m_whiskerMax[index] > yMax)
			yMax = m_whiskerMax[index];
		if (m_whiskerMin[index] < yMin)
			yMin = m_whiskerMin[index];
	} else {
		if (m_whiskerMax[index] > xMax)
			xMax = m_whiskerMax[index];
		if (m_whiskerMin[index] < xMin)
			xMin = m_whiskerMin[index];
	}

	double whiskerMax = -INFINITY; // upper adjacent value
	double whiskerMin = INFINITY; // lower adjacent value
	const double outerFenceMax = statistics.thirdQuartile + 3.0 * statistics.iqr;
	const double outerFenceMin = statistics.firstQuartile - 3.0 * statistics.iqr;

	for (int row = 0; row < column->rowCount(); ++row) {
		if (!column->isValid(row) || column->isMasked(row))
			continue;

		double value = 0.0;
		switch (column->columnMode()) {
		case AbstractColumn::ColumnMode::Double:
		case AbstractColumn::ColumnMode::Integer:
		case AbstractColumn::ColumnMode::BigInt:
			value = column->valueAt(row);
			break;
		case AbstractColumn::ColumnMode::DateTime:
			value = column->dateTimeAt(row).toMSecsSinceEpoch();
			break;
		case AbstractColumn::ColumnMode::Text:
		case AbstractColumn::ColumnMode::Month:
		case AbstractColumn::ColumnMode::Day:
			break;
		}

		double rand = 0.5;
		if (jitteringEnabled)
			rand = (double)std::rand() / ((double)RAND_MAX + 1);

		if (value > m_whiskerMax.at(index) || value < m_whiskerMin.at(index)) {
			if (whiskersType == BoxPlot::WhiskersType::IQR && (value > outerFenceMax || value < outerFenceMin))
				m_farOutPointsLogical[index] << setOutlierPoint(x - width / 2 + rand * width, value);
			else
				m_outlierPointsLogical[index] << setOutlierPoint(x - width / 2 + rand * width, value);
		} else {
			if (orientation == BoxPlot::Orientation::Vertical)
				m_dataPointsLogical[index] << QPointF(x - width / 2 + rand * width, value);
			else
				m_dataPointsLogical[index] << QPointF(value, x - width / 2 + rand * width);

			// determine the upper/lower adjucent values
			if (whiskersType == BoxPlot::WhiskersType::IQR) {
				if (value > whiskerMax)
					whiskerMax = value;

				if (value < whiskerMin)
					whiskerMin = value;
			}
		}
	}

	// set the whisker ends at the upper and lower adjacent values
	if (whiskersType == BoxPlot::WhiskersType::IQR) {
		if (whiskerMax != -INFINITY)
			m_whiskerMax[index] = whiskerMax;
		if (whiskerMin != INFINITY)
			m_whiskerMin[index] = whiskerMin;
	}
}

void BoxPlotPrivate::verticalBoxPlot(int index) {
	PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));

	QVector<QLineF> lines;
	const double x = index + 1.0;
	const double xMinBox = m_xMinBox.at(index);
	const double xMaxBox = m_xMaxBox.at(index);
	const double yMinBox = m_yMinBox.at(index);
	const double yMaxBox = m_yMaxBox.at(index);
	const double median = m_median.at(index);

	// box
	if (!notchesEnabled) {
		// first line starting at the top left corner of the box
		lines << QLineF(xMinBox, yMaxBox, xMaxBox, yMaxBox);
		lines << QLineF(xMaxBox, yMaxBox, xMaxBox, yMinBox);
		lines << QLineF(xMaxBox, yMinBox, xMinBox, yMinBox);
		lines << QLineF(xMinBox, yMinBox, xMinBox, yMaxBox);
	} else {
		auto* column = static_cast<const Column*>(dataColumnsOrdered.at(index));
		const auto& statistics = column->statistics();
		const double notch = 1.7 * 1.25 * statistics.iqr / 1.35 / std::sqrt(statistics.size);
		const double notchMax = median + notch;
		const double notchMin = median - notch;
		double width = xMaxBox - xMinBox;

		// first line starting at the top left corner of the box
		lines << QLineF(xMinBox, yMaxBox, xMaxBox, yMaxBox);
		lines << QLineF(xMaxBox, yMaxBox, xMaxBox, notchMax);
		lines << QLineF(xMaxBox, notchMax, xMinBox + 0.9 * width, median);
		lines << QLineF(xMinBox + 0.9 * width, median, xMaxBox, notchMin);
		lines << QLineF(xMaxBox, notchMin, xMaxBox, yMinBox);
		lines << QLineF(xMaxBox, yMinBox, xMinBox, yMinBox);
		lines << QLineF(xMinBox, yMinBox, xMinBox, notchMin);
		lines << QLineF(xMinBox, notchMin, xMinBox + 0.1 * width, median);
		lines << QLineF(xMinBox + 0.1 * width, median, xMinBox, notchMax);
		lines << QLineF(xMinBox, notchMax, xMinBox, yMaxBox);
	}

	m_boxRect[index] = q->cSystem->mapLogicalToScene(lines);
	updateFillingRect(index, lines);

	// median line
	lines.clear();
	if (!notchesEnabled)
		lines << QLineF(xMinBox, median, xMaxBox, median);
	else {
		double width = xMaxBox - xMinBox;
		lines << QLineF(xMinBox + 0.1 * width, median, m_xMaxBox.at(index) - 0.1 * width, median);
	}

	lines = q->cSystem->mapLogicalToScene(lines);
	if (!lines.isEmpty())
		m_medianLine[index] = lines.first();

	// whisker lines
	lines.clear();
	lines << QLineF(x, m_yMaxBox.at(index), x, m_whiskerMax.at(index)); // upper whisker
	lines << QLineF(x, m_yMinBox.at(index), x, m_whiskerMin.at(index)); // lower whisker
	lines = q->cSystem->mapLogicalToScene(lines);
	for (const auto& line : qAsConst(lines)) {
		m_whiskersPath[index].moveTo(line.p1());
		m_whiskersPath[index].lineTo(line.p2());
	}

	// whisker caps
	if (!m_whiskersPath[index].isEmpty()) {
		bool visible = false;
		QPointF maxPoint = q->cSystem->mapLogicalToScene(QPointF(x, m_whiskerMax.at(index)), visible);
		if (visible) {
			m_whiskersCapPath[index].moveTo(QPointF(maxPoint.x() - whiskersCapSize / 2., maxPoint.y()));
			m_whiskersCapPath[index].lineTo(QPointF(maxPoint.x() + whiskersCapSize / 2., maxPoint.y()));
			m_whiskerEndPointsLogical[index] << QPointF(x, m_whiskerMax.at(index));
		}

		QPointF minPoint = q->cSystem->mapLogicalToScene(QPointF(x, m_whiskerMin.at(index)), visible);
		if (visible) {
			m_whiskersCapPath[index].moveTo(QPointF(minPoint.x() - whiskersCapSize / 2., minPoint.y()));
			m_whiskersCapPath[index].lineTo(QPointF(minPoint.x() + whiskersCapSize / 2., minPoint.y()));
			m_whiskerEndPointsLogical[index] << QPointF(x, m_whiskerMin.at(index));
		}
	}

	// map the logical points of symbols to scene coordinates
	m_meanPointLogical[index] = QPointF(x, m_mean.at(index));
	m_medianPointLogical[index] = QPointF(x, m_median.at(index));
	mapSymbolsToScene(index);
}

void BoxPlotPrivate::horizontalBoxPlot(int index) {
	PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));

	QVector<QLineF> lines;
	const double y = index + 1.0;
	const double xMinBox = m_xMinBox.at(index);
	const double xMaxBox = m_xMaxBox.at(index);
	const double yMinBox = m_yMinBox.at(index);
	const double yMaxBox = m_yMaxBox.at(index);
	const double median = m_median.at(index);

	// box
	if (!notchesEnabled) {
		// first line starting at the top left corner of the box
		lines << QLineF(xMinBox, yMaxBox, xMaxBox, yMaxBox);
		lines << QLineF(xMaxBox, yMaxBox, xMaxBox, yMinBox);
		lines << QLineF(xMaxBox, yMinBox, xMinBox, yMinBox);
		lines << QLineF(xMinBox, yMinBox, xMinBox, yMaxBox);
	} else {
		auto* column = static_cast<const Column*>(dataColumnsOrdered.at(index));
		const auto& statistics = column->statistics();
		const double notch = 1.7 * 1.25 * statistics.iqr / 1.35 / std::sqrt(statistics.size);
		const double notchMax = median + notch;
		const double notchMin = median - notch;
		double width = yMaxBox - yMinBox;

		lines << QLineF(xMinBox, yMaxBox, notchMin, yMaxBox);
		lines << QLineF(notchMin, yMaxBox, median, yMaxBox - 0.1 * width);
		lines << QLineF(median, yMaxBox - 0.1 * width, notchMax, yMaxBox);
		lines << QLineF(notchMax, yMaxBox, xMaxBox, yMaxBox);
		lines << QLineF(xMaxBox, yMaxBox, xMaxBox, yMinBox);
		lines << QLineF(xMaxBox, yMinBox, notchMax, yMinBox);
		lines << QLineF(notchMax, yMinBox, median, yMinBox + 0.1 * width);
		lines << QLineF(median, yMinBox + 0.1 * width, notchMin, yMinBox);
		lines << QLineF(notchMin, yMinBox, xMinBox, yMinBox);
		lines << QLineF(xMinBox, yMinBox, xMinBox, yMaxBox);
	}

	m_boxRect[index] = q->cSystem->mapLogicalToScene(lines);
	updateFillingRect(index, lines);

	// median line
	lines.clear();
	if (!notchesEnabled)
		lines << QLineF(median, yMinBox, median, yMaxBox);
	else {
		double width = yMaxBox - yMinBox;
		lines << QLineF(median, yMinBox + 0.1 * width, median, yMaxBox - 0.1 * width);
	}

	lines = q->cSystem->mapLogicalToScene(lines);
	if (!lines.isEmpty())
		m_medianLine[index] = lines.first();

	// whisker lines
	lines.clear();
	lines << QLineF(m_xMaxBox.at(index), y, m_whiskerMax.at(index), y); // upper whisker
	lines << QLineF(m_xMinBox.at(index), y, m_whiskerMin.at(index), y); // lower whisker
	lines = q->cSystem->mapLogicalToScene(lines);
	for (const auto& line : qAsConst(lines)) {
		m_whiskersPath[index].moveTo(line.p1());
		m_whiskersPath[index].lineTo(line.p2());
	}

	// whisker caps
	if (!m_whiskersPath[index].isEmpty()) {
		bool visible = false;
		QPointF maxPoint = q->cSystem->mapLogicalToScene(QPointF(m_whiskerMax.at(index), y), visible);
		if (visible) {
			m_whiskersCapPath[index].moveTo(QPointF(maxPoint.x(), maxPoint.y() - whiskersCapSize / 2));
			m_whiskersCapPath[index].lineTo(QPointF(maxPoint.x(), maxPoint.y() + whiskersCapSize / 2));
			m_whiskerEndPointsLogical[index] << QPointF(m_whiskerMax.at(index), y);
		}

		QPointF minPoint = q->cSystem->mapLogicalToScene(QPointF(m_whiskerMin.at(index), y), visible);
		if (visible) {
			m_whiskersCapPath[index].moveTo(QPointF(minPoint.x(), minPoint.y() - whiskersCapSize / 2));
			m_whiskersCapPath[index].lineTo(QPointF(minPoint.x(), minPoint.y() + whiskersCapSize / 2));
			m_whiskerEndPointsLogical[index] << QPointF(m_whiskerMin.at(index), y);
		}
	}

	// outliers symbols
	m_meanPointLogical[index] = QPointF(m_mean.at(index), y);
	m_medianPointLogical[index] = QPointF(m_median.at(index), y);
	mapSymbolsToScene(index);
}

void BoxPlotPrivate::updateRug() {
	if (!rugEnabled || !q->plot()) {
		recalcShapeAndBoundingRect();
		return;
	}

	auto cs = q->plot()->coordinateSystem(q->coordinateSystemIndex());
	const double xMin = q->plot()->range(Dimension::X, cs->index(Dimension::X)).start();
	const double yMin = q->plot()->range(Dimension::Y, cs->index(Dimension::Y)).start();

	QPainterPath rugPath;
	QVector<QPointF> points;

	for (int i = 0; i < q->dataColumns().count(); ++i) {
		const auto* column = static_cast<const Column*>(q->dataColumns().at(i));
#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
		rugPath.clear();
#else
		rugPath = QPainterPath();
#endif
		points.clear();

		if (orientation == BoxPlot::Orientation::Horizontal) {
			for (int row = 0; row < column->rowCount(); ++row) {
				if (column->isValid(row) && !column->isMasked(row))
					points << QPointF(column->valueAt(row), yMin);
			}

			// map the points to scene coordinates
			points = q->cSystem->mapLogicalToScene(points);

			// path for the vertical rug lines
			for (const auto& point : qAsConst(points)) {
				rugPath.moveTo(point.x(), point.y() - rugOffset);
				rugPath.lineTo(point.x(), point.y() - rugOffset - rugLength);
			}
		} else { // horizontal
			for (int row = 0; row < column->rowCount(); ++row) {
				if (column->isValid(row) && !column->isMasked(row))
					points << QPointF(xMin, column->valueAt(row));
			}

			// map the points to scene coordinates
			points = q->cSystem->mapLogicalToScene(points);

			// path for the horizontal rug lines
			for (const auto& point : qAsConst(points)) {
				rugPath.moveTo(point.x() + rugOffset, point.y());
				rugPath.lineTo(point.x() + rugOffset + rugLength, point.y());
			}
		}

		m_rugPath[i] = rugPath;
	}

	recalcShapeAndBoundingRect();
}

void BoxPlotPrivate::updateFillingRect(int index, const QVector<QLineF>& lines) {
	const auto& unclippedLines = q->cSystem->mapLogicalToScene(lines, AbstractCoordinateSystem::MappingFlag::SuppressPageClipping);

	if (unclippedLines.isEmpty()) {
		m_fillPolygon[index] = QPolygonF();
		return;
	}

	// we have four unclipped lines for the box.
	// clip the points to the plot data rect and create a new polygon
	// out of them that will be filled out.
	QPolygonF polygon;
	const QRectF& dataRect = static_cast<CartesianPlot*>(q->parentAspect())->dataRect();
	int i = 0;
	for (const auto& line : unclippedLines) {
		// clip the first point of the line
		QPointF p1 = line.p1();
		if (p1.x() < dataRect.left())
			p1.setX(dataRect.left());
		else if (p1.x() > dataRect.right())
			p1.setX(dataRect.right());

		if (p1.y() < dataRect.top())
			p1.setY(dataRect.top());
		else if (p1.y() > dataRect.bottom())
			p1.setY(dataRect.bottom());

		// clip the second point of the line
		QPointF p2 = line.p2();
		if (p2.x() < dataRect.left())
			p2.setX(dataRect.left());
		else if (p2.x() > dataRect.right())
			p2.setX(dataRect.right());

		if (p2.y() < dataRect.top())
			p2.setY(dataRect.top());
		else if (p2.y() > dataRect.bottom())
			p2.setY(dataRect.bottom());

		if (i != unclippedLines.size() - 1)
			polygon << p1;
		else {
			// close the polygon for the last line
			polygon << p1;
			polygon << p2;
		}

		++i;
	}

	m_fillPolygon[index] = polygon;
}

/*!
 * map the outlier points from logical to scene coordinates,
 * avoid drawing overlapping points, logic similar to
 * //XYCurvePrivate::retransform()
 */
void BoxPlotPrivate::mapSymbolsToScene(int index) {
	// outliers
	int size = m_outlierPointsLogical[index].size();
	if (size > 0) {
		const int startIndex = 0;
		const int endIndex = m_outlierPointsLogical[index].size() - 1;
		std::vector<bool> m_pointVisible;
		m_pointVisible.resize(size);

		q->cSystem->mapLogicalToScene(startIndex, endIndex, m_outlierPointsLogical[index], m_outlierPoints[index], m_pointVisible);
	}

	// data points
	size = m_dataPointsLogical[index].size();
	if (size > 0) {
		const int startIndex = 0;
		const int endIndex = m_dataPointsLogical[index].size() - 1;
		std::vector<bool> pointVisible;
		pointVisible.resize(size);

		q->cSystem->mapLogicalToScene(startIndex, endIndex, m_dataPointsLogical[index], m_dataPoints[index], pointVisible);
	}

	// far out points
	size = m_farOutPointsLogical[index].size();
	if (size > 0) {
		const int startIndex = 0;
		const int endIndex = m_farOutPointsLogical[index].size() - 1;
		std::vector<bool> pointVisible;
		pointVisible.resize(size);

		q->cSystem->mapLogicalToScene(startIndex, endIndex, m_farOutPointsLogical[index], m_farOutPoints[index], pointVisible);
	}

	// whisker ends
	size = m_whiskerEndPointsLogical[index].size();
	if (size > 0) {
		const int startIndex = 0;
		const int endIndex = m_whiskerEndPointsLogical[index].size() - 1;
		std::vector<bool> pointVisible;
		pointVisible.resize(size);

		q->cSystem->mapLogicalToScene(startIndex, endIndex, m_whiskerEndPointsLogical[index], m_whiskerEndPoints[index], pointVisible);
	}

	// mean
	bool visible;
	m_meanPoint[index] = q->cSystem->mapLogicalToScene(m_meanPointLogical[index], visible);
	m_meanPointVisible[index] = visible;

	// median
	m_medianPoint[index] = q->cSystem->mapLogicalToScene(m_medianPointLogical[index], visible);
	m_medianPointVisible[index] = visible;
}

/*!
	Returns the outer bounds of the item as a rectangle.
 */
QRectF BoxPlotPrivate::boundingRect() const {
	return m_boundingRectangle;
}

/*!
	Returns the shape of this item as a QPainterPath in local coordinates.
*/
QPainterPath BoxPlotPrivate::shape() const {
	return m_boxPlotShape;
}

/*!
  recalculates the outer bounds and the shape of the item.
*/
void BoxPlotPrivate::recalcShapeAndBoundingRect() {
	prepareGeometryChange();
	m_boxPlotShape = QPainterPath();

	for (int i = 0; i < dataColumnsOrdered.size(); ++i) {
		if (!dataColumnsOrdered.at(i) || static_cast<const Column*>(dataColumnsOrdered.at(i))->statistics().size == 0)
			continue;

		QPainterPath boxPath;
		for (const auto& line : qAsConst(m_boxRect.at(i))) {
			boxPath.moveTo(line.p1());
			boxPath.lineTo(line.p2());
		}
		m_boxPlotShape.addPath(WorksheetElement::shapeFromPath(boxPath, borderLines.at(i)->pen()));

		m_boxPlotShape.addPath(WorksheetElement::shapeFromPath(m_whiskersPath.at(i), whiskersLine->pen()));
		m_boxPlotShape.addPath(WorksheetElement::shapeFromPath(m_whiskersCapPath.at(i), whiskersCapLine->pen()));

		m_boxPlotShape.addPath(WorksheetElement::shapeFromPath(m_rugPath.at(i), borderLines.at(i)->pen()));

		// add symbols outlier, jitter and far out values
		QPainterPath symbolsPath = QPainterPath();

		// outlier values
		if (symbolOutlier->style() != Symbol::Style::NoSymbols && !m_outlierPoints.at(i).isEmpty()) {
			QPainterPath path = Symbol::stylePath(symbolOutlier->style());
			QTransform trafo;
			trafo.scale(symbolOutlier->size(), symbolOutlier->size());
			path = trafo.map(path);
			trafo.reset();

			if (symbolOutlier->rotationAngle() != 0.) {
				trafo.rotate(symbolOutlier->rotationAngle());
				path = trafo.map(path);
			}

			for (const auto& point : qAsConst(m_outlierPoints.at(i))) {
				trafo.reset();
				trafo.translate(point.x(), point.y());
				symbolsPath.addPath(trafo.map(path));
			}
		}

		// jitter values
		if (symbolData->style() != Symbol::Style::NoSymbols && !m_dataPoints.at(i).isEmpty()) {
			QPainterPath path = Symbol::stylePath(symbolData->style());
			QTransform trafo;
			trafo.scale(symbolData->size(), symbolData->size());
			path = trafo.map(path);
			trafo.reset();

			if (symbolData->rotationAngle() != 0.) {
				trafo.rotate(symbolData->rotationAngle());
				path = trafo.map(path);
			}

			for (const auto& point : qAsConst(m_dataPoints.at(i))) {
				trafo.reset();
				trafo.translate(point.x(), point.y());
				symbolsPath.addPath(trafo.map(path));
			}
		}

		// far out values
		if (symbolFarOut->style() != Symbol::Style::NoSymbols && !m_farOutPoints.at(i).isEmpty()) {
			QPainterPath path = Symbol::stylePath(symbolFarOut->style());
			QTransform trafo;
			trafo.scale(symbolFarOut->size(), symbolFarOut->size());
			path = trafo.map(path);
			trafo.reset();

			if (symbolFarOut->rotationAngle() != 0.) {
				trafo.rotate(symbolFarOut->rotationAngle());
				path = trafo.map(path);
			}

			for (const auto& point : qAsConst(m_farOutPoints.at(i))) {
				trafo.reset();
				trafo.translate(point.x(), point.y());
				symbolsPath.addPath(trafo.map(path));
			}
		}

		// whisker ends
		if (symbolWhiskerEnd->style() != Symbol::Style::NoSymbols && !m_whiskerEndPoints.at(i).isEmpty()) {
			QPainterPath path = Symbol::stylePath(symbolWhiskerEnd->style());
			QTransform trafo;
			trafo.scale(symbolWhiskerEnd->size(), symbolWhiskerEnd->size());
			path = trafo.map(path);
			trafo.reset();

			if (symbolWhiskerEnd->rotationAngle() != 0.) {
				trafo.rotate(symbolWhiskerEnd->rotationAngle());
				path = trafo.map(path);
			}

			for (const auto& point : qAsConst(m_whiskerEndPoints.at(i))) {
				trafo.reset();
				trafo.translate(point.x(), point.y());
				symbolsPath.addPath(trafo.map(path));
			}
		}

		m_boxPlotShape.addPath(symbolsPath);
	}

	m_boundingRectangle = m_boxPlotShape.boundingRect();
	updatePixmap();
}

void BoxPlotPrivate::updatePixmap() {
	PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));
	QPixmap pixmap(m_boundingRectangle.width(), m_boundingRectangle.height());
	if (m_boundingRectangle.width() == 0. || m_boundingRectangle.height() == 0.) {
		m_pixmap = pixmap;
		m_hoverEffectImageIsDirty = true;
		m_selectionEffectImageIsDirty = true;
		return;
	}
	pixmap.fill(Qt::transparent);
	QPainter painter(&pixmap);
	painter.setRenderHint(QPainter::Antialiasing, true);
	painter.translate(-m_boundingRectangle.topLeft());

	draw(&painter);
	painter.end();

	m_pixmap = pixmap;
	m_hoverEffectImageIsDirty = true;
	m_selectionEffectImageIsDirty = true;
	update();
}

void BoxPlotPrivate::draw(QPainter* painter) {
	PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));

	for (int i = 0; i < dataColumnsOrdered.size(); ++i) {
		if (!dataColumnsOrdered.at(i))
			continue;

		// no need to draw anything if the column doesn't have any valid values
		if (static_cast<const Column*>(dataColumnsOrdered.at(i))->statistics().size == 0)
			continue;

		if (!m_boxRect.at(i).isEmpty()) {
			// draw the box filling
			if (backgrounds.at(i)->enabled()) {
				painter->setOpacity(backgrounds.at(i)->opacity());
				painter->setPen(Qt::NoPen);
				drawFilling(painter, i);
			}

			// draw the border
			const auto* borderLine = borderLines.at(i);
			if (borderLine->pen().style() != Qt::NoPen) {
				painter->setPen(borderLine->pen());
				painter->setBrush(Qt::NoBrush);
				painter->setOpacity(borderLine->opacity());
				for (const auto& line : m_boxRect.at(i))
					painter->drawLine(line);
			}

			// draw the median line
			const auto* medianLine = medianLines.at(i);
			if (medianLine->pen().style() != Qt::NoPen) {
				painter->setPen(medianLine->pen());
				painter->setBrush(Qt::NoBrush);
				painter->setOpacity(medianLine->opacity());
				painter->drawLine(m_medianLine.at(i));
			}
		}

		// draw the whiskers
		if (whiskersLine->pen().style() != Qt::NoPen && !m_whiskersPath.at(i).isEmpty()) {
			painter->setPen(whiskersLine->pen());
			painter->setBrush(Qt::NoBrush);
			painter->setOpacity(whiskersLine->opacity());
			painter->drawPath(m_whiskersPath.at(i));
		}

		// draw the whiskers cap
		if (whiskersCapLine->pen().style() != Qt::NoPen && !m_whiskersCapPath.at(i).isEmpty()) {
			painter->setPen(whiskersCapLine->pen());
			painter->setBrush(Qt::NoBrush);
			painter->setOpacity(whiskersCapLine->opacity());
			painter->drawPath(m_whiskersCapPath.at(i));
		}

		// draw rug
		if (rugEnabled && !m_rugPath.at(i).isEmpty()) {
			QPen pen;
			pen.setColor(borderLines.at(i)->pen().color());
			pen.setWidthF(rugWidth);
			painter->setPen(pen);
			painter->setOpacity(borderLines.at(i)->opacity());
			painter->drawPath(m_rugPath.at(i));
		}

		// draw the symbols
		drawSymbols(painter, i);
	}
}

void BoxPlotPrivate::drawSymbols(QPainter* painter, int index) {
	// outlier values
	symbolOutlier->draw(painter, m_outlierPoints.at(index));

	// mean value
	if (m_meanPointVisible.at(index))
		symbolMean->draw(painter, m_meanPoint.at(index));

	// median value
	if (m_medianPointVisible.at(index))
		symbolMedian->draw(painter, m_medianPoint.at(index));

	// jitter values
	symbolData->draw(painter, m_dataPoints.at(index));

	// far out values
	symbolFarOut->draw(painter, m_farOutPoints.at(index));

	// whisker end points
	symbolWhiskerEnd->draw(painter, m_whiskerEndPoints.at(index));
}

void BoxPlotPrivate::drawFilling(QPainter* painter, int index) {
	PERFTRACE(name() + QLatin1String(Q_FUNC_INFO));

	const QPolygonF& polygon = m_fillPolygon.at(index);
	const QRectF& rect = polygon.boundingRect();

	const auto* background = backgrounds.at(index);
	if (background->type() == Background::Type::Color) {
		switch (background->colorStyle()) {
		case Background::ColorStyle::SingleColor: {
			painter->setBrush(QBrush(background->firstColor()));
			break;
		}
		case Background::ColorStyle::HorizontalLinearGradient: {
			QLinearGradient linearGrad(rect.topLeft(), rect.topRight());
			linearGrad.setColorAt(0, background->firstColor());
			linearGrad.setColorAt(1, background->secondColor());
			painter->setBrush(QBrush(linearGrad));
			break;
		}
		case Background::ColorStyle::VerticalLinearGradient: {
			QLinearGradient linearGrad(rect.topLeft(), rect.bottomLeft());
			linearGrad.setColorAt(0, background->firstColor());
			linearGrad.setColorAt(1, background->secondColor());
			painter->setBrush(QBrush(linearGrad));
			break;
		}
		case Background::ColorStyle::TopLeftDiagonalLinearGradient: {
			QLinearGradient linearGrad(rect.topLeft(), rect.bottomRight());
			linearGrad.setColorAt(0, background->firstColor());
			linearGrad.setColorAt(1, background->secondColor());
			painter->setBrush(QBrush(linearGrad));
			break;
		}
		case Background::ColorStyle::BottomLeftDiagonalLinearGradient: {
			QLinearGradient linearGrad(rect.bottomLeft(), rect.topRight());
			linearGrad.setColorAt(0, background->firstColor());
			linearGrad.setColorAt(1, background->secondColor());
			painter->setBrush(QBrush(linearGrad));
			break;
		}
		case Background::ColorStyle::RadialGradient: {
			QRadialGradient radialGrad(rect.center(), rect.width() / 2);
			radialGrad.setColorAt(0, background->firstColor());
			radialGrad.setColorAt(1, background->secondColor());
			painter->setBrush(QBrush(radialGrad));
			break;
		}
		}
	} else if (background->type() == Background::Type::Image) {
		if (!background->fileName().trimmed().isEmpty()) {
			QPixmap pix(background->fileName());
			switch (background->imageStyle()) {
			case Background::ImageStyle::ScaledCropped:
				pix = pix.scaled(rect.size().toSize(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
				painter->setBrush(QBrush(pix));
				painter->setBrushOrigin(pix.size().width() / 2, pix.size().height() / 2);
				break;
			case Background::ImageStyle::Scaled:
				pix = pix.scaled(rect.size().toSize(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
				painter->setBrush(QBrush(pix));
				painter->setBrushOrigin(pix.size().width() / 2, pix.size().height() / 2);
				break;
			case Background::ImageStyle::ScaledAspectRatio:
				pix = pix.scaled(rect.size().toSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
				painter->setBrush(QBrush(pix));
				painter->setBrushOrigin(pix.size().width() / 2, pix.size().height() / 2);
				break;
			case Background::ImageStyle::Centered: {
				QPixmap backpix(rect.size().toSize());
				backpix.fill();
				QPainter p(&backpix);
				p.drawPixmap(QPointF(0, 0), pix);
				p.end();
				painter->setBrush(QBrush(backpix));
				painter->setBrushOrigin(-pix.size().width() / 2, -pix.size().height() / 2);
				break;
			}
			case Background::ImageStyle::Tiled:
				painter->setBrush(QBrush(pix));
				break;
			case Background::ImageStyle::CenterTiled:
				painter->setBrush(QBrush(pix));
				painter->setBrushOrigin(pix.size().width() / 2, pix.size().height() / 2);
			}
		}
	} else if (background->type() == Background::Type::Pattern)
		painter->setBrush(QBrush(background->firstColor(), background->brushStyle()));

	painter->drawPolygon(polygon);
}

void BoxPlotPrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget*) {
	if (!isVisible())
		return;

	painter->setPen(Qt::NoPen);
	painter->setBrush(Qt::NoBrush);
	painter->setRenderHint(QPainter::SmoothPixmapTransform, true);

	if (KSharedConfig::openConfig()->group("Settings_Worksheet").readEntry<bool>("DoubleBuffering", true))
		painter->drawPixmap(m_boundingRectangle.topLeft(), m_pixmap); // draw the cached pixmap (fast)
	else
		draw(painter); // draw directly again (slow)

	if (m_hovered && !isSelected() && !q->isPrinting()) {
		if (m_hoverEffectImageIsDirty) {
			QPixmap pix = m_pixmap;
			QPainter p(&pix);
			p.setCompositionMode(QPainter::CompositionMode_SourceIn); // source (shadow) pixels merged with the alpha channel of the destination (m_pixmap)
			p.fillRect(pix.rect(), QApplication::palette().color(QPalette::Shadow));
			p.end();

			m_hoverEffectImage = ImageTools::blurred(pix.toImage(), m_pixmap.rect(), 5);
			m_hoverEffectImageIsDirty = false;
		}

		painter->drawImage(m_boundingRectangle.topLeft(), m_hoverEffectImage, m_pixmap.rect());
		return;
	}

	if (isSelected() && !q->isPrinting()) {
		if (m_selectionEffectImageIsDirty) {
			QPixmap pix = m_pixmap;
			QPainter p(&pix);
			p.setCompositionMode(QPainter::CompositionMode_SourceIn);
			p.fillRect(pix.rect(), QApplication::palette().color(QPalette::Highlight));
			p.end();

			m_selectionEffectImage = ImageTools::blurred(pix.toImage(), m_pixmap.rect(), 5);
			m_selectionEffectImageIsDirty = false;
		}

		painter->drawImage(m_boundingRectangle.topLeft(), m_selectionEffectImage, m_pixmap.rect());
		return;
	}
}

void BoxPlotPrivate::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) {
	q->createContextMenu()->exec(event->screenPos());
}

void BoxPlotPrivate::hoverEnterEvent(QGraphicsSceneHoverEvent*) {
	if (!isSelected()) {
		m_hovered = true;
		Q_EMIT q->hovered();
		update();
	}
}

void BoxPlotPrivate::hoverLeaveEvent(QGraphicsSceneHoverEvent*) {
	if (m_hovered) {
		m_hovered = false;
		Q_EMIT q->unhovered();
		update();
	}
}

//##############################################################################
//##################  Serialization/Deserialization  ###########################
//##############################################################################
//! Save as XML
void BoxPlot::save(QXmlStreamWriter* writer) const {
	Q_D(const BoxPlot);

	writer->writeStartElement(QStringLiteral("boxPlot"));
	writeBasicAttributes(writer);
	writeCommentElement(writer);

	// general
	writer->writeStartElement(QStringLiteral("general"));
	writer->writeAttribute(QStringLiteral("ordering"), QString::number(static_cast<int>(d->ordering)));
	writer->writeAttribute(QStringLiteral("orientation"), QString::number(static_cast<int>(d->orientation)));
	writer->writeAttribute(QStringLiteral("variableWidth"), QString::number(d->variableWidth));
	writer->writeAttribute(QStringLiteral("widthFactor"), QString::number(d->widthFactor));
	writer->writeAttribute(QStringLiteral("notches"), QString::number(d->notchesEnabled));
	writer->writeAttribute(QStringLiteral("jitteringEnabled"), QString::number(d->jitteringEnabled));
	writer->writeAttribute(QStringLiteral("plotRangeIndex"), QString::number(m_cSystemIndex));
	writer->writeAttribute(QStringLiteral("xMin"), QString::number(d->xMin));
	writer->writeAttribute(QStringLiteral("xMax"), QString::number(d->xMax));
	writer->writeAttribute(QStringLiteral("yMin"), QString::number(d->yMin));
	writer->writeAttribute(QStringLiteral("yMax"), QString::number(d->yMax));
	writer->writeAttribute(QStringLiteral("visible"), QString::number(d->isVisible()));
	for (auto* column : d->dataColumns) {
		writer->writeStartElement(QStringLiteral("column"));
		writer->writeAttribute(QStringLiteral("path"), column->path());
		writer->writeEndElement();
	}
	writer->writeEndElement();

	// box
	for (auto* background : d->backgrounds)
		background->save(writer);

	for (auto* line : d->borderLines)
		line->save(writer);

	for (auto* line : d->medianLines)
		line->save(writer);

	// symbols for the outliers, mean, far out and jitter values
	d->symbolMean->save(writer);
	d->symbolMedian->save(writer);
	d->symbolOutlier->save(writer);
	d->symbolFarOut->save(writer);
	d->symbolData->save(writer);
	d->symbolWhiskerEnd->save(writer);

	// whiskers
	writer->writeStartElement(QStringLiteral("whiskers"));
	writer->writeAttribute(QStringLiteral("type"), QString::number(static_cast<int>(d->whiskersType)));
	writer->writeAttribute(QStringLiteral("rangeParameter"), QString::number(d->whiskersRangeParameter));
	d->whiskersLine->save(writer);
	writer->writeEndElement();

	writer->writeStartElement(QStringLiteral("whiskersCap"));
	writer->writeAttribute(QStringLiteral("size"), QString::number(d->whiskersCapSize));
	d->whiskersCapLine->save(writer);
	writer->writeEndElement();

	// margin plots
	writer->writeStartElement(QStringLiteral("margins"));
	writer->writeAttribute(QStringLiteral("rugEnabled"), QString::number(d->rugEnabled));
	writer->writeAttribute(QStringLiteral("rugLength"), QString::number(d->rugLength));
	writer->writeAttribute(QStringLiteral("rugWidth"), QString::number(d->rugWidth));
	writer->writeAttribute(QStringLiteral("rugOffset"), QString::number(d->rugOffset));
	writer->writeEndElement();

	writer->writeEndElement(); // close "BoxPlot" section
}

//! Load from XML
bool BoxPlot::load(XmlStreamReader* reader, bool preview) {
	Q_D(BoxPlot);

	if (!readBasicAttributes(reader))
		return false;

	KLocalizedString attributeWarning = ki18n("Attribute '%1' missing or empty, default value is used");
	QXmlStreamAttributes attribs;
	QString str;
	bool firstBackgroundRead = false;
	bool firstBorderLineRead = false;
	bool firstMedianLineRead = false;

	while (!reader->atEnd()) {
		reader->readNext();
		if (reader->isEndElement() && reader->name() == QLatin1String("boxPlot"))
			break;

		if (!reader->isStartElement())
			continue;

		if (!preview && reader->name() == QLatin1String("comment")) {
			if (!readCommentElement(reader))
				return false;
		} else if (!preview && reader->name() == QLatin1String("general")) {
			attribs = reader->attributes();

			READ_INT_VALUE("ordering", ordering, BoxPlot::Ordering);
			READ_INT_VALUE("orientation", orientation, BoxPlot::Orientation);
			READ_INT_VALUE("variableWidth", variableWidth, bool);
			READ_DOUBLE_VALUE("widthFactor", widthFactor);
			READ_INT_VALUE("notches", notchesEnabled, bool);
			READ_INT_VALUE("jitteringEnabled", jitteringEnabled, bool);
			READ_INT_VALUE_DIRECT("plotRangeIndex", m_cSystemIndex, int);

			READ_DOUBLE_VALUE("xMin", xMin);
			READ_DOUBLE_VALUE("xMax", xMax);
			READ_DOUBLE_VALUE("yMin", yMin);
			READ_DOUBLE_VALUE("yMax", yMax);

			str = attribs.value(QStringLiteral("visible")).toString();
			if (str.isEmpty())
				reader->raiseWarning(attributeWarning.subs(QStringLiteral("visible")).toString());
			else
				d->setVisible(str.toInt());
		} else if (reader->name() == QLatin1String("column")) {
			attribs = reader->attributes();

			str = attribs.value(QStringLiteral("path")).toString();
			if (!str.isEmpty())
				d->dataColumnPaths << str;
			// 			READ_COLUMN(dataColumn);
		} else if (!preview && reader->name() == QLatin1String("filling"))
			if (!firstBackgroundRead) {
				auto* background = d->backgrounds.at(0);
				background->load(reader, preview);
				firstBackgroundRead = true;
			} else {
				auto* background = d->addBackground(KConfigGroup());
				background->load(reader, preview);
			}
		else if (!preview && reader->name() == QLatin1String("border")) {
			if (!firstBorderLineRead) {
				auto* line = d->borderLines.at(0);
				line->load(reader, preview);
				firstBorderLineRead = true;
			} else {
				auto* line = d->addBorderLine(KConfigGroup());
				line->load(reader, preview);
			}
		} else if (!preview && reader->name() == QLatin1String("medianLine")) {
			if (!firstMedianLineRead) {
				auto* line = d->medianLines.at(0);
				line->load(reader, preview);
				firstMedianLineRead = true;
			} else {
				auto* line = d->addMedianLine(KConfigGroup());
				line->load(reader, preview);
			}
		} else if (!preview && reader->name() == QLatin1String("symbolMean"))
			d->symbolMean->load(reader, preview);
		else if (!preview && reader->name() == QLatin1String("symbolMedian"))
			d->symbolMedian->load(reader, preview);
		else if (!preview && reader->name() == QLatin1String("symbolOutlier"))
			d->symbolOutlier->load(reader, preview);
		else if (!preview && reader->name() == QLatin1String("symbolFarOut"))
			d->symbolFarOut->load(reader, preview);
		else if (!preview && reader->name() == QLatin1String("symbolData"))
			d->symbolData->load(reader, preview);
		else if (!preview && reader->name() == QLatin1String("symbolWhiskerEnd"))
			d->symbolWhiskerEnd->load(reader, preview);
		else if (!preview && reader->name() == QLatin1String("whiskers")) {
			attribs = reader->attributes();

			READ_INT_VALUE("type", whiskersType, BoxPlot::WhiskersType);
			READ_DOUBLE_VALUE("rangeParameter", whiskersRangeParameter);
			d->whiskersLine->load(reader, preview);
		} else if (!preview && reader->name() == QLatin1String("whiskersCap")) {
			attribs = reader->attributes();

			READ_DOUBLE_VALUE("size", whiskersCapSize);
			d->whiskersCapLine->load(reader, preview);
		} else if (!preview && reader->name() == QLatin1String("margins")) {
			attribs = reader->attributes();

			READ_INT_VALUE("rugEnabled", rugEnabled, bool);
			READ_DOUBLE_VALUE("rugLength", rugLength);
			READ_DOUBLE_VALUE("rugWidth", rugWidth);
			READ_DOUBLE_VALUE("rugOffset", rugOffset);
		} else { // unknown element
			reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString()));
			if (!reader->skipToEndElement())
				return false;
		}
	}

	d->dataColumns.resize(d->dataColumnPaths.size());

	// in case we're loading an older project where it was not possible to change the properties
	// for each data column independently of each other and there was only one single Background, etc.
	// add here additional elements to fit the current number of data columns after the project load
	// and set the saved properties for all newly added objects.
	int diff = d->dataColumns.size() - d->backgrounds.size();
	if (diff > 0) {
		KConfig config;
		KConfigGroup group = config.group(QLatin1String("XYCurve"));

		const auto* background = d->backgrounds.constFirst();
		const auto* borderLine = d->borderLines.constFirst();
		const auto* medianLine = d->medianLines.constFirst();
		for (int i = 0; i < diff; ++i) {
			auto* newBackground = d->addBackground(group);
			newBackground->setEnabled(background->enabled());
			newBackground->setType(background->type());
			newBackground->setColorStyle(background->colorStyle());
			newBackground->setImageStyle(background->imageStyle());
			newBackground->setBrushStyle(background->brushStyle());
			newBackground->setFirstColor(background->firstColor());
			newBackground->setSecondColor(background->secondColor());
			newBackground->setFileName(background->fileName());
			newBackground->setOpacity(background->opacity());

			auto* newBorderLine = d->addBorderLine(group);
			newBorderLine->setStyle(borderLine->style());
			newBorderLine->setColor(borderLine->color());
			newBorderLine->setWidth(borderLine->width());
			newBorderLine->setOpacity(borderLine->opacity());

			auto* newMedianLine = d->addMedianLine(group);
			newMedianLine->setStyle(medianLine->style());
			newMedianLine->setColor(medianLine->color());
			newMedianLine->setWidth(medianLine->width());
			newMedianLine->setOpacity(medianLine->opacity());
		}
	}

	return true;
}

//##############################################################################
//#########################  Theme management ##################################
//##############################################################################
void BoxPlot::loadThemeConfig(const KConfig& config) {
	KConfigGroup group;
	if (config.hasGroup(QLatin1String("Theme")))
		group = config.group("XYCurve"); // when loading from the theme config, use the same properties as for XYCurve
	else
		group = config.group("BoxPlot");

	const auto* plot = static_cast<const CartesianPlot*>(parentAspect());
	int index = plot->curveChildIndex(this);
	const QColor themeColor = plot->themeColorPalette(index);

	Q_D(BoxPlot);
	d->m_suppressRecalc = true;

	// box fillings
	for (int i = 0; i < d->backgrounds.count(); ++i) {
		auto* background = d->backgrounds.at(i);
		background->loadThemeConfig(group, plot->themeColorPalette(i));
	}

	// box border lines
	for (int i = 0; i < d->borderLines.count(); ++i) {
		auto* line = d->borderLines.at(i);
		line->loadThemeConfig(group, plot->themeColorPalette(i));
	}

	// median lines
	for (int i = 0; i < d->medianLines.count(); ++i) {
		auto* line = d->medianLines.at(i);
		line->loadThemeConfig(group, plot->themeColorPalette(i));
	}

	// whiskers
	d->whiskersLine->loadThemeConfig(group, themeColor);
	d->whiskersCapLine->loadThemeConfig(group, themeColor);

	// symbols
	d->symbolMean->loadThemeConfig(group, themeColor);
	d->symbolMedian->loadThemeConfig(group, themeColor);
	d->symbolOutlier->loadThemeConfig(group, themeColor);
	d->symbolFarOut->loadThemeConfig(group, themeColor);
	d->symbolData->loadThemeConfig(group, themeColor);

	// Tufte's theme goes beyond what we can implement when using the theme properties of XYCurve.
	// So, instead of introducing a dedicated section for BoxPlot, which would be a big overkill
	// for all other themes, we add here a special handling for "Tufte".
	if (plot->theme() == QLatin1String("Tufte")) {
		for (auto* background : d->backgrounds)
			background->setEnabled(false);

		for (auto* line : d->borderLines)
			line->setStyle(Qt::NoPen);

		for (auto* line : d->medianLines)
			line->setStyle(Qt::NoPen);

		d->symbolMean->setStyle(Symbol::Style::NoSymbols);
		d->symbolMedian->setStyle(Symbol::Style::Circle);
		d->symbolOutlier->setStyle(Symbol::Style::NoSymbols);
		d->symbolFarOut->setStyle(Symbol::Style::NoSymbols);
		d->symbolData->setStyle(Symbol::Style::NoSymbols);
		setWhiskersCapSize(0.0);
	}

	d->m_suppressRecalc = false;
	d->recalcShapeAndBoundingRect();
}
