File indexing completed on 2024-09-22 04:37:26

0001 /*
0002     SPDX-FileCopyrightText: 2005 Ace Jones <acejones@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2005-2018 Thomas Baumgart <tbaumgart@kde.org>
0004     SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0005     SPDX-FileCopyrightText: 2018 Michael Kiefer <mlkiefer@users.noreply.github.com>
0006     SPDX-FileCopyrightText: 2020 Robert Szczesiak <dev.rszczesiak@gmail.com>
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "kreportchartview.h"
0011 
0012 // ----------------------------------------------------------------------------
0013 // QT Includes
0014 #include <QStandardItemModel>
0015 #include <QFontDatabase>
0016 #include <QtMath>
0017 
0018 // ----------------------------------------------------------------------------
0019 // KDE Includes
0020 #include <KColorScheme>
0021 #include <KLocalizedString>
0022 
0023 // ----------------------------------------------------------------------------
0024 // Project Includes
0025 #include <KChartBackgroundAttributes>
0026 #include <KChartDataValueAttributes>
0027 #include <KChartMarkerAttributes>
0028 #include <KChartGridAttributes>
0029 #include <KChartHeaderFooter>
0030 #include <KChartLegend>
0031 #include <KChartLineDiagram>
0032 #include <KChartBarDiagram>
0033 #include <KChartPieDiagram>
0034 #include <KChartRingDiagram>
0035 #include <KChartCartesianAxis>
0036 #include <KChartFrameAttributes>
0037 #include "kmymoneysettings.h"
0038 #include "kreportcartesianaxis.h"
0039 #include "mymoneyfile.h"
0040 #include "mymoneysecurity.h"
0041 #include "mymoneyenums.h"
0042 
0043 using namespace reports;
0044 
0045 KReportChartView::KReportChartView(QWidget* parent) :
0046     KChart::Chart(parent),
0047     m_accountSeries(0),
0048     m_seriesTotals(0),
0049     m_numColumns(0),
0050     m_skipZero(0),
0051     m_backgroundBrush(KColorScheme(QPalette::Current).background()),
0052     m_foregroundBrush(KColorScheme(QPalette::Current).foreground()),
0053     m_precision(2)
0054 {
0055     // ********************************************************************
0056     // Set KMyMoney's Chart Parameter Defaults
0057     // ********************************************************************
0058 
0059     //Set the background obtained from the color scheme
0060     BackgroundAttributes backAttr(backgroundAttributes());
0061     backAttr.setBrush(m_backgroundBrush);
0062     backAttr.setVisible(true);
0063     setBackgroundAttributes(backAttr);
0064 }
0065 
0066 void KReportChartView::drawPivotChart(const PivotGrid &grid, const MyMoneyReport &config, int numberColumns, const QStringList& columnHeadings, const QList<ERowType>& rowTypeList, const QStringList& columnTypeHeaderList)
0067 {
0068     if (numberColumns == 0)
0069         return;
0070     //set the number of columns
0071     setNumColumns(numberColumns);
0072 
0073     //set skipZero
0074     m_skipZero = config.isSkippingZero();
0075 
0076     //remove existing headers
0077     const HeaderFooterList hfList = headerFooters();
0078     for (const auto& hf : hfList)
0079         delete hf;
0080 
0081     //remove existing legends
0082     const LegendList lgList = legends();
0083     for (const auto& lg : lgList)
0084         delete lg;
0085 
0086     //make sure the model is clear
0087     m_model.clear();
0088     const bool blocked = m_model.blockSignals(true); // don't Q_EMIT dataChanged() signal during each drawPivotRowSet
0089 
0090     //set the new header
0091     HeaderFooter* header = new HeaderFooter;
0092     // adding some extra spaces at the end here avoids truncation
0093     // of the label on the diagram and solves bug #399260
0094     header->setText(config.name().append(QLatin1String("     ")));
0095     header->setType(HeaderFooter::Header);
0096     header->setPosition(Position::North);
0097     TextAttributes headerTextAttr(header->textAttributes());
0098     headerTextAttr.setPen(m_foregroundBrush.color());
0099     header->setTextAttributes(headerTextAttr);
0100     addHeaderFooter(header);
0101 
0102     // whether to limit the chart to use series totals only.  Used for reports which only
0103     // show one dimension (pie).
0104     setSeriesTotals(false);
0105 
0106     // whether series (rows) are accounts (true) or months (false). This causes a lot
0107     // of complexity in the charts.  The problem is that circular reports work best with
0108     // an account in a COLUMN, while line/bar prefer it in a ROW.
0109     setAccountSeries(true);
0110 
0111     switch (config.chartType()) {
0112     case eMyMoney::Report::ChartType::Line:
0113     case eMyMoney::Report::ChartType::Bar:
0114     case eMyMoney::Report::ChartType::StackedBar: {
0115         CartesianCoordinatePlane* cartesianPlane = new CartesianCoordinatePlane(this);
0116         cartesianPlane->setAutoAdjustVerticalRangeToData(2);
0117         cartesianPlane->setRubberBandZoomingEnabled(true);
0118         replaceCoordinatePlane(cartesianPlane);
0119 
0120         // set-up axis type
0121         if (config.isLogYAxis())
0122             cartesianPlane->setAxesCalcModeY(KChart::AbstractCoordinatePlane::Logarithmic);
0123         else
0124             cartesianPlane->setAxesCalcModeY(KChart::AbstractCoordinatePlane::Linear);
0125 
0126         QLocale loc = locale();
0127         // set-up grid
0128         GridAttributes ga = cartesianPlane->gridAttributes(Qt::Vertical);
0129         ga.setGridVisible(config.isChartCHGridLines());
0130         ga.setGridStepWidth(config.isDataUserDefined() ? loc.toDouble(config.dataMajorTick()) : 0.0);
0131         ga.setGridSubStepWidth(config.isDataUserDefined() ? loc.toDouble(config.dataMinorTick()) : 0.0);
0132         cartesianPlane->setGridAttributes(Qt::Vertical, ga);
0133 
0134         ga = cartesianPlane->gridAttributes(Qt::Horizontal);
0135         ga.setGridVisible(config.isChartSVGridLines());
0136         cartesianPlane->setGridAttributes(Qt::Horizontal, ga);
0137 
0138         // set-up data range
0139         cartesianPlane->setVerticalRange(qMakePair(config.isDataUserDefined() ? loc.toDouble(config.dataRangeStart()) : 0.0,
0140                                          config.isDataUserDefined() ? loc.toDouble(config.dataRangeEnd()) : 0.0));
0141 
0142         //set-up x axis
0143         CartesianAxis *xAxis = new CartesianAxis();
0144         xAxis->setPosition(CartesianAxis::Bottom);
0145         xAxis->setTitleText(i18n("Time"));
0146         TextAttributes xAxisTitleTextAttr(xAxis->titleTextAttributes());
0147         xAxisTitleTextAttr.setMinimalFontSize(QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize());
0148         xAxisTitleTextAttr.setPen(m_foregroundBrush.color());
0149         xAxis->setTitleTextAttributes(xAxisTitleTextAttr);
0150         TextAttributes xAxisTextAttr(xAxis->textAttributes());
0151         xAxisTextAttr.setPen(m_foregroundBrush.color());
0152         xAxis->setTextAttributes(xAxisTextAttr);
0153         RulerAttributes xAxisRulerAttr(xAxis->rulerAttributes());
0154         xAxisRulerAttr.setTickMarkPen(m_foregroundBrush.color());
0155         xAxisRulerAttr.setShowRulerLine(true);
0156         xAxis->setRulerAttributes(xAxisRulerAttr);
0157 
0158         //set-up y axis
0159         KReportCartesianAxis *yAxis = new KReportCartesianAxis(loc, config.yLabelsPrecision());
0160         yAxis->setPosition(CartesianAxis::Left);
0161 
0162         if (config.isIncludingPrice())
0163             yAxis->setTitleText(i18n("Price"));
0164         else
0165             yAxis->setTitleText(i18n("Balance"));
0166 
0167         TextAttributes yAxisTitleTextAttr(yAxis->titleTextAttributes());
0168         yAxisTitleTextAttr.setMinimalFontSize(QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize());
0169         yAxisTitleTextAttr.setPen(m_foregroundBrush.color());
0170         yAxis->setTitleTextAttributes(yAxisTitleTextAttr);
0171         TextAttributes yAxisTextAttr(yAxis->textAttributes());
0172         yAxisTextAttr.setPen(m_foregroundBrush.color());
0173         yAxis->setTextAttributes(yAxisTextAttr);
0174         RulerAttributes yAxisRulerAttr(yAxis->rulerAttributes());
0175         yAxisRulerAttr.setTickMarkPen(m_foregroundBrush.color());
0176         yAxisRulerAttr.setShowRulerLine(true);
0177         yAxis->setRulerAttributes(yAxisRulerAttr);
0178 
0179         switch (config.chartType()) {
0180         case eMyMoney::Report::ChartType::End:
0181         case eMyMoney::Report::ChartType::Line: {
0182             KChart::LineDiagram* diagram = new KChart::LineDiagram(this, cartesianPlane);
0183 
0184             if (config.isSkippingZero()) {
0185                 LineAttributes attributes = diagram->lineAttributes();
0186                 attributes.setMissingValuesPolicy(LineAttributes::MissingValuesAreBridged);
0187                 diagram->setLineAttributes(attributes);
0188             }
0189             cartesianPlane->replaceDiagram(diagram);
0190             diagram->addAxis(xAxis);
0191             diagram->addAxis(yAxis);
0192             break;
0193         }
0194         case eMyMoney::Report::ChartType::Bar: {
0195             KChart::BarDiagram* diagram = new KChart::BarDiagram(this, cartesianPlane);
0196             cartesianPlane->replaceDiagram(diagram);
0197             diagram->addAxis(xAxis);
0198             diagram->addAxis(yAxis);
0199             break;
0200         }
0201         case eMyMoney::Report::ChartType::StackedBar: {
0202             KChart::BarDiagram* diagram = new KChart::BarDiagram(this, cartesianPlane);
0203             diagram->setType(BarDiagram::Stacked);
0204             cartesianPlane->replaceDiagram(diagram);
0205             diagram->addAxis(xAxis);
0206             diagram->addAxis(yAxis);
0207             break;
0208         }
0209         default:
0210             break;
0211         }
0212         break;
0213     }
0214     case eMyMoney::Report::ChartType::Pie:
0215     case eMyMoney::Report::ChartType::Ring: {
0216         PolarCoordinatePlane* polarPlane = new PolarCoordinatePlane(this);
0217         replaceCoordinatePlane(polarPlane);
0218 
0219         // set-up grid
0220         GridAttributes ga = polarPlane->gridAttributes(true);
0221         ga.setGridVisible(config.isChartCHGridLines());
0222         polarPlane->setGridAttributes(true, ga);
0223 
0224         ga = polarPlane->gridAttributes(false);
0225         ga.setGridVisible(config.isChartSVGridLines());
0226         polarPlane->setGridAttributes(false, ga);
0227 
0228         setAccountSeries(false);
0229 
0230         switch (config.chartType()) {
0231         case eMyMoney::Report::ChartType::Pie: {
0232             KChart::PieDiagram* diagram = new KChart::PieDiagram(this, polarPlane);
0233             polarPlane->replaceDiagram(diagram);
0234             setSeriesTotals(true);
0235             break;
0236         }
0237         case eMyMoney::Report::ChartType::Ring: {
0238             KChart::RingDiagram* diagram = new KChart::RingDiagram(this, polarPlane);
0239             polarPlane->replaceDiagram(diagram);
0240             break;
0241         }
0242         default:
0243             break;
0244         }
0245 
0246         break;
0247     }
0248     default:  // no valid chart types
0249         return;
0250     }
0251     //get the coordinate plane  and the diagram for later use
0252     AbstractCoordinatePlane* cPlane = coordinatePlane();
0253     AbstractDiagram* planeDiagram = cPlane->diagram();
0254     planeDiagram->setAntiAliasing(true);
0255 
0256     // setup chart color palette
0257     switch (config.chartPalette()) {
0258     case eMyMoney::Report::ChartPalette::Application:
0259         switch (KMyMoneySettings::chartsPalette()) {
0260         case 0:
0261             planeDiagram->useDefaultColors();
0262             break;
0263         case 1:
0264             planeDiagram->useRainbowColors();
0265             break;
0266         case 2:
0267         default:
0268             planeDiagram->useSubduedColors();
0269             break;
0270         }
0271         break;
0272     case eMyMoney::Report::ChartPalette::Default:
0273         planeDiagram->useDefaultColors();
0274         break;
0275     case eMyMoney::Report::ChartPalette::Rainbow:
0276         planeDiagram->useRainbowColors();
0277         break;
0278     default:
0279     case eMyMoney::Report::ChartPalette::Subdued:
0280         planeDiagram->useSubduedColors();
0281         break;
0282     }
0283 
0284     int eBudgetDiffIdx = rowTypeList.indexOf(eBudgetDiff);
0285     QList<ERowType> myRowTypeList = rowTypeList;
0286     if (eBudgetDiffIdx != -1)
0287         myRowTypeList.removeAt(eBudgetDiffIdx);
0288     QStringList myColumnTypeHeaderList = columnTypeHeaderList;
0289     if (eBudgetDiffIdx != -1)
0290         myColumnTypeHeaderList.removeAt(eBudgetDiffIdx);
0291     int myRowTypeListSize = myRowTypeList.size();
0292     MyMoneyFile* file = MyMoneyFile::instance();
0293     int precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction());
0294     int rowNum = 0;
0295     QStringList legendNames;
0296     QMultiMap<MyMoneyMoney, int> legendTotal;
0297 
0298     switch (config.detailLevel()) {
0299     case eMyMoney::Report::DetailLevel::None:
0300     case eMyMoney::Report::DetailLevel::All: {
0301         // iterate over outer groups
0302         PivotGrid::const_iterator it_outergroup = grid.begin();
0303         while (it_outergroup != grid.end()) {
0304             //determine whether expenses should be displayed as negative
0305             const bool invertValue = (config.isNegExpenses() && (*it_outergroup).m_inverted);
0306             // iterate over inner groups
0307             PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin();
0308             while (it_innergroup != (*it_outergroup).end()) {
0309                 // iterate over accounts
0310                 PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin();
0311                 while (it_row != (*it_innergroup).end()) {
0312                     //Do not include investments accounts in the chart because they are merely container of stock and other accounts
0313                     if (it_row.key().accountType() != eMyMoney::Account::Type::Investment) {
0314 
0315                         // get displayed precision
0316                         int currencyPrecision = precision;
0317                         int securityPrecision = precision;
0318                         if (!it_row.key().id().isEmpty()) {
0319                             const MyMoneyAccount acc = file->account(it_row.key().id());
0320                             if (acc.isInvest()) {
0321                                 securityPrecision = file->currency(acc.currencyId()).pricePrecision();
0322                                 // stock account isn't evaluated in currency, so take investment account instead
0323                                 currencyPrecision = MyMoneyMoney::denomToPrec(file->account(acc.parentAccountId()).fraction());
0324                             } else
0325                                 currencyPrecision = MyMoneyMoney::denomToPrec(acc.fraction());
0326                         }
0327 
0328                         // iterate row types
0329                         for (int i = 0 ; i < myRowTypeListSize; ++i) {
0330                             QString legendText;
0331 
0332                             //only show the column type in the header if there is more than one type
0333                             if (myRowTypeListSize > 1)
0334                                 legendText = QString(myColumnTypeHeaderList.at(i) + QLatin1String(" - ") + it_row.key().name());
0335                             else
0336                                 legendText = it_row.key().name();
0337 
0338                             //set the legend text
0339                             legendNames.append(legendText);
0340                             legendTotal.insert(it_row.value().value(myRowTypeList.at(i)).m_total.abs(), rowNum);
0341 
0342                             precision = myRowTypeList.at(i) == ePrice ? securityPrecision : currencyPrecision;
0343 
0344                             //set the cell value and tooltip
0345                             rowNum = drawPivotGridRow(rowNum, it_row.value().value(myRowTypeList.at(i)),
0346                                                       config.isChartDataLabels() ? legendText : QString(),
0347                                                       0, numberColumns, precision, invertValue);
0348                         }
0349                     }
0350                     ++it_row;
0351                 }
0352                 ++it_innergroup;
0353             }
0354             ++it_outergroup;
0355         }
0356     }
0357     break;
0358 
0359     case eMyMoney::Report::DetailLevel::Top: {
0360         // iterate over outer groups
0361         PivotGrid::const_iterator it_outergroup = grid.begin();
0362         while (it_outergroup != grid.end()) {
0363             //determine whether expenses should be displayed as negative
0364             const bool invertValue = (config.isNegExpenses() && (*it_outergroup).m_inverted);
0365             // iterate over inner groups
0366             PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin();
0367             while (it_innergroup != (*it_outergroup).end()) {
0368                 // iterate row types
0369                 for (int i = 0 ; i < myRowTypeListSize; ++i) {
0370                     QString legendText;
0371 
0372                     //only show the column type in the header if there is more than one type
0373                     if (myRowTypeListSize > 1)
0374                         legendText = QString(myColumnTypeHeaderList.at(i) + QLatin1String(" - ") + it_innergroup.key());
0375                     else
0376                         legendText = it_innergroup.key();
0377 
0378                     //set the legend text
0379                     legendNames.append(legendText);
0380                     legendTotal.insert((*it_innergroup).m_total.value(myRowTypeList.at(i)).m_total.abs(), rowNum);
0381 
0382                     //set the cell value and tooltip
0383                     rowNum = drawPivotGridRow(rowNum, (*it_innergroup).m_total.value(myRowTypeList.at(i)),
0384                                               config.isChartDataLabels() ? legendText : QString(),
0385                                               0, numberColumns, precision, invertValue);
0386                 }
0387                 ++it_innergroup;
0388             }
0389             ++it_outergroup;
0390         }
0391     }
0392     break;
0393 
0394     case eMyMoney::Report::DetailLevel::Group: {
0395         // iterate over outer groups
0396         PivotGrid::const_iterator it_outergroup = grid.begin();
0397         while (it_outergroup != grid.end()) {
0398             //determine whether expenses should be displayed as negative
0399             const bool invertValue = (config.isNegExpenses() && (*it_outergroup).m_inverted);
0400             // iterate row types
0401             for (int i = 0 ; i < myRowTypeListSize; ++i) {
0402                 QString legendText;
0403 
0404                 //only show the column type in the header if there is more than one type
0405                 if (myRowTypeListSize > 1)
0406                     legendText = QString(myColumnTypeHeaderList.at(i) + QLatin1String(" - ") + it_outergroup.key());
0407                 else
0408                     legendText = it_outergroup.key();
0409 
0410                 //set the legend text
0411                 legendNames.append(legendText);
0412                 legendTotal.insert((*it_outergroup).m_total.value(myRowTypeList.at(i)).m_total.abs(), rowNum);
0413 
0414                 //set the cell value and tooltip
0415                 rowNum = drawPivotGridRow(rowNum, (*it_outergroup).m_total.value(myRowTypeList.at(i)),
0416                                           config.isChartDataLabels() ? legendText : QString(),
0417                                           0, numberColumns, precision, invertValue);
0418             }
0419             ++it_outergroup;
0420         }
0421 
0422         //if selected, show totals too
0423         if (config.isShowingRowTotals()) {
0424             // iterate row types
0425             for (int i = 0 ; i < myRowTypeListSize; ++i) {
0426                 QString legendText;
0427 
0428                 //only show the column type in the header if there is more than one type
0429                 if (myRowTypeListSize > 1)
0430                     legendText = QString(myColumnTypeHeaderList.at(i) + QLatin1String(" - ") + i18nc("Total balance", "Total"));
0431                 else
0432                     legendText = QString(i18nc("Total balance", "Total"));
0433 
0434                 //set the legend text
0435                 legendNames.append(legendText);
0436                 legendTotal.insert(grid.m_total.value(myRowTypeList.at(i)).m_total.abs(), rowNum);
0437 
0438                 //set the cell value and tooltip
0439                 rowNum = drawPivotGridRow(rowNum, grid.m_total.value(myRowTypeList.at(i)),
0440                                           config.isChartDataLabels() ? legendText : QString(),
0441                                           0, numberColumns, precision, false);
0442             }
0443         }
0444     }
0445     break;
0446 
0447     case eMyMoney::Report::DetailLevel::Total: {
0448         // iterate row types
0449         for (int i = 0 ; i < myRowTypeListSize; ++i) {
0450             QString legendText;
0451 
0452             //only show the column type in the header if there is more than one type
0453             if (myRowTypeListSize > 1)
0454                 legendText = QString(myColumnTypeHeaderList.at(i) + QLatin1String(" - ") + i18nc("Total balance", "Total"));
0455             else
0456                 legendText = QString(i18nc("Total balance", "Total"));
0457 
0458             //set the legend text
0459             legendNames.append(legendText);
0460             legendTotal.insert(grid.m_total.value(myRowTypeList.at(i)).m_total.abs(), rowNum);
0461 
0462             //set the cell value and tooltip
0463             if (config.isMixedTime()) {
0464                 if (myRowTypeList.at(i) == eActual)
0465                     rowNum = drawPivotGridRow(rowNum, grid.m_total.value(myRowTypeList.at(i)),
0466                                               config.isChartDataLabels() ? legendText : QString(),
0467                                               0, config.currentDateColumn(), precision, false);
0468                 else if (myRowTypeList.at(i)== eForecast) {
0469                     rowNum = drawPivotGridRow(rowNum, grid.m_total.value(myRowTypeList.at(i)),
0470                                               config.isChartDataLabels() ? legendText : QString(),
0471                                               config.currentDateColumn(), numberColumns - config.currentDateColumn(), precision, false);
0472 
0473                 } else
0474                     rowNum = drawPivotGridRow(rowNum, grid.m_total.value(myRowTypeList.at(i)),
0475                                               config.isChartDataLabels() ? legendText : QString(),
0476                                               0, numberColumns, precision, false);
0477             } else
0478                 rowNum = drawPivotGridRow(rowNum, grid.m_total.value(myRowTypeList.at(i)),
0479                                           config.isChartDataLabels() ? legendText : QString(),
0480                                           0, numberColumns, precision, false);
0481         }
0482 
0483     }
0484     break;
0485     default:
0486     case eMyMoney::Report::DetailLevel::End:
0487         return;
0488     }
0489 
0490     auto legendRows = legendTotal.values();                     // list of legend rows sorted ascending by total value
0491     for (auto i = 0; i < legendRows.count(); ++i) {
0492         const auto ixRow = legendRows.count() - 1 - i;            // take row with the highest total value i.e. form the bottom
0493         const auto row = legendRows.at(ixRow);
0494         if ( row != i) {                                          // if legend isn't sorted by total value, then rearrange model
0495             if ((accountSeries() && !seriesTotals()) ||
0496                     (seriesTotals() && !accountSeries()))
0497                 m_model.insertColumn(i, m_model.takeColumn(row));
0498             else
0499                 m_model.insertRow(i, m_model.takeRow(row));
0500 
0501             for (auto j = i; j < ixRow; ++j) {                      // fix invalid indexes after above move operation
0502                 if (legendRows.at(j) < row)
0503                     ++legendRows[j];
0504             }
0505             legendRows[ixRow] = i;
0506             legendNames.move(row, i);
0507         }
0508     }
0509 
0510     // Set up X axis labels (ie "abscissa" to use the technical term)
0511     if (accountSeries()) { // if not, we will set these up while putting in the chart values.
0512         QStringList xLabels;
0513         for (const auto& colHeading : columnHeadings)
0514             xLabels.append(QString(colHeading).replace(QLatin1String("&nbsp;"), QLatin1String(" ")));
0515         m_model.setVerticalHeaderLabels(xLabels);
0516     }
0517     m_model.setHorizontalHeaderLabels(legendNames);
0518 
0519     // set line width for line chart
0520     if (config.chartType() == eMyMoney::Report::ChartType::Line) {
0521         AttributesModel* diagramAttributes = planeDiagram->attributesModel();
0522         int penWidth = config.chartLineWidth();
0523         for (int i = 0 ; i < rowNum ; ++i) {
0524             QPen pen = diagramAttributes->headerData(i, Qt::Horizontal, DatasetPenRole).value< QPen >();
0525             pen.setWidth(penWidth);
0526             m_model.setHeaderData(i, Qt::Horizontal, QVariant::fromValue(pen), DatasetPenRole);
0527         }
0528     }
0529 
0530     // set the text attributes after calling replaceLegend() otherwise fon sizes will get overwritten
0531     qreal generalFontSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSizeF();
0532     if (generalFontSize == -1)
0533         generalFontSize = 8; // this is a fallback if the fontsize was specified in pixels
0534 
0535     // the legend is needed only if at least two data sets are rendered
0536     if (qMin(static_cast<int>(KMyMoneySettings::maximumLegendItems()), rowNum) > 1) {
0537         //the legend will be used later
0538         Legend* legend = new Legend(planeDiagram, this);
0539         legend->setTitleText(i18nc("Chart legend title", "Legend"));
0540 
0541         //set the legend basic attributes
0542         //this is done after adding the legend because the values are overridden when adding the legend to the chart
0543         const auto maxLegendItems = KMyMoneySettings::maximumLegendItems();
0544         auto legendItems = legendNames.count();
0545 
0546         auto i = 0;
0547         while (legendItems > maxLegendItems) {
0548             legend->setDatasetHidden(legendRows.at(i++), true);
0549             --legendItems;
0550         }
0551 
0552         legend->setUseAutomaticMarkerSize(false);
0553         FrameAttributes legendFrameAttr(legend->frameAttributes());
0554         legendFrameAttr.setPen(m_foregroundBrush.color());
0555         // leave some space between the content and the frame
0556         legendFrameAttr.setPadding(2);
0557         legend->setFrameAttributes(legendFrameAttr);
0558         legend->setPosition(Position::East);
0559         legend->setTextAlignment(Qt::AlignLeft);
0560         if (config.isChartDataLabels())
0561             legend->setLegendStyle(KChart::Legend::MarkersAndLines);
0562         else
0563             legend->setLegendStyle(KChart::Legend::LinesOnly);
0564         replaceLegend(legend);
0565 
0566         TextAttributes legendTextAttr(legend->textAttributes());
0567         legendTextAttr.setPen(m_foregroundBrush.color());
0568         legendTextAttr.setFontSize(KChart::Measure(generalFontSize, KChartEnums::MeasureCalculationModeAbsolute));
0569         legend->setTextAttributes(legendTextAttr);
0570 
0571         TextAttributes legendTitleTextAttr(legend->titleTextAttributes());
0572         legendTitleTextAttr.setPen(m_foregroundBrush.color());
0573         legendTitleTextAttr.setFontSize(KChart::Measure(generalFontSize + 4, KChartEnums::MeasureCalculationModeAbsolute));
0574         legend->setTitleTextAttributes(legendTitleTextAttr);
0575     }
0576 
0577     //set data value attributes
0578     //make sure to show only the required number of fractional digits on the labels of the graph
0579     DataValueAttributes dataValueAttr(planeDiagram->dataValueAttributes());
0580     MarkerAttributes markerAttr(dataValueAttr.markerAttributes());
0581     markerAttr.setVisible(true);
0582     markerAttr.setMarkerStyle(MarkerAttributes::MarkerCircle);
0583     dataValueAttr.setMarkerAttributes(markerAttr);
0584     TextAttributes dataValueTextAttr(dataValueAttr.textAttributes());
0585     dataValueTextAttr.setPen(m_foregroundBrush.color());
0586     dataValueTextAttr.setFontSize(KChart::Measure(generalFontSize, KChartEnums::MeasureCalculationModeAbsolute));
0587     dataValueAttr.setTextAttributes(dataValueTextAttr);
0588     m_precision = config.yLabelsPrecision();
0589     dataValueAttr.setDecimalDigits(config.yLabelsPrecision());
0590     dataValueAttr.setVisible(config.isChartDataLabels());
0591     planeDiagram->setDataValueAttributes(dataValueAttr);
0592     planeDiagram->setAllowOverlappingDataValueTexts(false);
0593 
0594     m_model.blockSignals(blocked); // reenable dataChanged() signal
0595 
0596     //assign model to the diagram
0597     planeDiagram->setModel(&m_model);
0598 
0599     // use MyMoney::formatMoney to format the values
0600     const auto rows = m_model.rowCount();
0601     const auto cols = m_model.columnCount();
0602     for (int col = 0; col < cols; ++col) {
0603         for (int row = 0; row < rows; ++row) {
0604             const auto idx = m_model.index(row, 0);
0605             auto attrs = planeDiagram->dataValueAttributes(idx);
0606             const MyMoneyMoney value(idx.data(Qt::DisplayRole).toDouble());
0607             attrs.setDataLabel(value.formatMoney(QString(), m_precision));
0608             planeDiagram->setDataValueAttributes(idx, attrs);
0609         }
0610     }
0611     adjustVerticalRange(config.yLabelsPrecision());
0612 }
0613 
0614 void KReportChartView::adjustVerticalRange(const int precision)
0615 {
0616     KChart::CartesianCoordinatePlane* cartesianPlane = qobject_cast<CartesianCoordinatePlane*>(coordinatePlane());
0617     if (!cartesianPlane)
0618         return; // only Cartesian planes are supported
0619 
0620     const qreal gridStart = cartesianPlane->gridDimensionsList().at(1).start;
0621     const qreal gridEnd = cartesianPlane->gridDimensionsList().at(1).end;
0622     const qreal precisionLimit = qPow(10, -precision);
0623     qreal dataRangeStart = cartesianPlane->verticalRange().first;
0624     qreal dataRangeEnd = cartesianPlane->verticalRange().second;
0625 
0626     bool dataRangeStartAdjusted = false;
0627     bool dataRangeEndAdjusted = false;
0628 
0629     if (cartesianPlane->axesCalcModeY() == KChart::AbstractCoordinatePlane::Logarithmic) {
0630         if (dataRangeStart == dataRangeEnd) {
0631             // if data model is represented by a horizontal line,
0632             // extend vertical range by two orders of magnitude upwards and downwards
0633             // if the whole data range is equal to zero,
0634             // at least draw an empty chart with four orders of magnitude on the vertical exis
0635             dataRangeStart = dataRangeStart > 0 ? qPow(10, qFloor(log10(dataRangeStart)) - 2) : precisionLimit;
0636             dataRangeEnd = dataRangeEnd > 0 ? qPow(10, qCeil(log10(dataRangeEnd)) + 2) : qPow(10, -precision + 4);
0637             dataRangeStartAdjusted = true;
0638             dataRangeEndAdjusted = true;
0639         }
0640 
0641         if (dataRangeStart < precisionLimit) {
0642             // if data range starts below the precision limit,
0643             // crop the chart to the precision limit
0644             dataRangeStart = precisionLimit;
0645             dataRangeStartAdjusted = true;
0646         }
0647         if (dataRangeEnd < precisionLimit) {
0648             // if data range ends below the precision limit,
0649             // at least draw an empty chart with four orders of magnitude on the vertical axis
0650             dataRangeEnd = qPow(10, -precision + 4);
0651             dataRangeEndAdjusted = true;
0652         }
0653 
0654         // if data range boundary needed no adjustment,
0655         // make sure it matches the original grid boundary
0656         if (!dataRangeStartAdjusted)
0657             dataRangeStart = gridStart;
0658         if (!dataRangeEndAdjusted)
0659             dataRangeEnd = gridEnd;
0660         cartesianPlane->setVerticalRange(qMakePair(dataRangeStart, dataRangeEnd));
0661     } else if (dataRangeStart == dataRangeEnd) // vertical axis type must be Linear then
0662         // if data model is represented by a horizontal line,
0663         // extend vertical range by two upwards and downwards
0664         cartesianPlane->setVerticalRange(qMakePair(dataRangeStart - 2, dataRangeEnd + 2));
0665 }
0666 
0667 int reports::KReportChartView::drawPivotGridRow ( int rowNum, const reports::PivotGridRow& gridRow, const QString& legendText, const int startColumn, const int columnsToDraw, const int precision, const bool invertValue )
0668 {
0669     // Columns
0670     const QString toolTip = QStringLiteral("<h2>%1</h2><strong>%2</strong><br>");
0671     const bool isToolTip = !legendText.isEmpty();
0672     if (seriesTotals()) {
0673         QStandardItem* item = new QStandardItem();
0674         double value = gridRow.m_total.toDouble();
0675         item->setData(QVariant(value), Qt::DisplayRole);
0676         if (isToolTip) {
0677             const MyMoneyMoney monetaryValue(value);
0678             item->setToolTip(toolTip.arg(legendText, monetaryValue.formatMoney(QString(), precision)));
0679         }
0680         //set the cell value
0681         if (accountSeries()) {
0682             m_model.insertRows(rowNum, 1);
0683             m_model.setItem(rowNum, 0, item);
0684         } else {
0685             m_model.insertColumns(rowNum, 1);
0686             m_model.setItem(0, rowNum, item);
0687         }
0688 
0689     } else {
0690         QList<QStandardItem*> itemList;
0691         for (int i = 0; i < startColumn-1; ++i) {
0692             itemList.append(new QStandardItem);
0693         }
0694         for (int i = startColumn; i < startColumn + columnsToDraw; ++i) {
0695             QStandardItem* item = new QStandardItem();
0696             if (!m_skipZero || !gridRow.at(i).isZero()) {
0697                 double value = gridRow.at(i).toDouble();
0698                 if (invertValue)
0699                     value = -value;
0700                 item->setData(QVariant(value), Qt::DisplayRole);
0701                 if (isToolTip) {
0702                     const MyMoneyMoney monetaryValue(value);
0703                     item->setToolTip(toolTip.arg(legendText, monetaryValue.formatMoney(QString(), precision)));
0704                 }
0705             }
0706             itemList.append(item);
0707         }
0708         if (accountSeries())
0709             m_model.appendColumn(itemList);
0710         else
0711             m_model.appendRow(itemList);
0712     }
0713     return ++rowNum;
0714 }
0715 
0716 void KReportChartView::setDataCell(int row, int column, const double value, QString tip)
0717 {
0718     QMap<int, QVariant> cellMap;
0719     cellMap.insert(Qt::DisplayRole, QVariant(value));
0720     if (!tip.isEmpty())
0721         cellMap.insert(Qt::ToolTipRole, QVariant(tip));
0722     const QModelIndex index = m_model.index(row, column);
0723     m_model.setItemData(index, cellMap);
0724 }
0725 
0726 /**
0727  * Justifies the model, so that the given rows and columns fit into it.
0728  */
0729 void KReportChartView::justifyModelSize(int rows, int columns)
0730 {
0731     const int currentRows = m_model.rowCount();
0732     const int currentCols = m_model.columnCount();
0733 
0734     if (currentCols < columns)
0735         if (! m_model.insertColumns(currentCols, columns - currentCols))
0736             qDebug() << "justifyModelSize: could not increase model size.";
0737     if (currentRows < rows)
0738         if (! m_model.insertRows(currentRows, rows - currentRows))
0739             qDebug() << "justifyModelSize: could not increase model size.";
0740 
0741     Q_ASSERT(m_model.rowCount() >= rows);
0742     Q_ASSERT(m_model.columnCount() >= columns);
0743 }
0744 
0745 void KReportChartView::setLineWidth(const int lineWidth)
0746 {
0747     LineDiagram* lineDiagram = qobject_cast<LineDiagram*>(coordinatePlane()->diagram());
0748     if (lineDiagram) {
0749         QList <QPen> pens;
0750         pens = lineDiagram->datasetPens();
0751         for (int i = 0; i < pens.count(); ++i) {
0752             pens[i].setWidth(lineWidth);
0753             lineDiagram->setPen(i, pens.at(i));
0754         }
0755     }
0756 }
0757 
0758 void KReportChartView::drawLimitLine(const double limit)
0759 {
0760     if (coordinatePlane()->diagram()->datasetDimension() != 1)
0761         return;
0762     // temporarily disconnect the view from the model to avoid update of view on
0763     // emission of the dataChanged() signal for each call of setDataCell().
0764     // This speeds up the runtime of drawLimitLine() by a factor of
0765     // approx. 60 on my box (1831ms vs. 31ms).
0766     AbstractDiagram* planeDiagram = coordinatePlane()->diagram();
0767     planeDiagram->setModel(0);
0768 
0769     //we get the current number of rows and we add one after that
0770     int row = m_model.rowCount();
0771     justifyModelSize(m_numColumns, row + 1);
0772     for (int col = 0; col < m_numColumns; ++col) {
0773         setDataCell(col, row, limit);
0774     }
0775 
0776     planeDiagram->setModel(&m_model);
0777 
0778 //TODO: add format to the line
0779 }
0780 
0781 void KReportChartView::removeLegend()
0782 {
0783     Legend* chartLegend = Chart::legend();
0784     delete chartLegend;
0785 }