/*************************************************************************** qgsplot.cpp --------------- begin : March 2022 copyright : (C) 2022 by Nyall Dawson email : nyall dot dawson at gmail dot com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "qgsplot.h" #include "qgslinesymbol.h" #include "qgsfillsymbol.h" #include "qgsfillsymbollayer.h" #include "qgslinesymbollayer.h" #include "qgstextrenderer.h" #include "qgsbasicnumericformat.h" #include "qgssymbollayerutils.h" #include "qgsapplication.h" #include "qgsnumericformatregistry.h" #include "qgsexpressioncontextutils.h" #include QgsPlot::~QgsPlot() = default; bool QgsPlot::writeXml( QDomElement &, QDomDocument &, const QgsReadWriteContext & ) const { return true; } bool QgsPlot::readXml( const QDomElement &, const QgsReadWriteContext & ) { return true; } // QgsPlotAxis QgsPlotAxis::QgsPlotAxis() { // setup default style mNumericFormat.reset( QgsPlotDefaultSettings::axisLabelNumericFormat() ); mGridMinorSymbol.reset( QgsPlotDefaultSettings::axisGridMinorSymbol() ); mGridMajorSymbol.reset( QgsPlotDefaultSettings::axisGridMajorSymbol() ); } QgsPlotAxis::~QgsPlotAxis() = default; Qgis::PlotAxisType QgsPlotAxis::type() const { return mType; } void QgsPlotAxis::setType( Qgis::PlotAxisType type ) { mType = type; } bool QgsPlotAxis::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const { element.setAttribute( QStringLiteral( "type" ), qgsEnumValueToKey( mType ) ); element.setAttribute( QStringLiteral( "gridIntervalMinor" ), qgsDoubleToString( mGridIntervalMinor ) ); element.setAttribute( QStringLiteral( "gridIntervalMajor" ), qgsDoubleToString( mGridIntervalMajor ) ); element.setAttribute( QStringLiteral( "labelInterval" ), qgsDoubleToString( mLabelInterval ) ); element.setAttribute( QStringLiteral( "suffix" ), mLabelSuffix ); element.setAttribute( QStringLiteral( "suffixPlacement" ), qgsEnumValueToKey( mSuffixPlacement ) ); QDomElement numericFormatElement = document.createElement( QStringLiteral( "numericFormat" ) ); mNumericFormat->writeXml( numericFormatElement, document, context ); element.appendChild( numericFormatElement ); QDomElement gridMajorElement = document.createElement( QStringLiteral( "gridMajorSymbol" ) ); gridMajorElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mGridMajorSymbol.get(), document, context ) ); element.appendChild( gridMajorElement ); QDomElement gridMinorElement = document.createElement( QStringLiteral( "gridMinorSymbol" ) ); gridMinorElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mGridMinorSymbol.get(), document, context ) ); element.appendChild( gridMinorElement ); QDomElement textFormatElement = document.createElement( QStringLiteral( "textFormat" ) ); textFormatElement.appendChild( mLabelTextFormat.writeXml( document, context ) ); element.appendChild( textFormatElement ); return true; } bool QgsPlotAxis::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { mType = qgsEnumKeyToValue( element.attribute( QStringLiteral( "type" ) ), Qgis::PlotAxisType::ValueType ); mGridIntervalMinor = element.attribute( QStringLiteral( "gridIntervalMinor" ) ).toDouble(); mGridIntervalMajor = element.attribute( QStringLiteral( "gridIntervalMajor" ) ).toDouble(); mLabelInterval = element.attribute( QStringLiteral( "labelInterval" ) ).toDouble(); mLabelSuffix = element.attribute( QStringLiteral( "suffix" ) ); mSuffixPlacement = qgsEnumKeyToValue( element.attribute( QStringLiteral( "suffixPlacement" ) ), Qgis::PlotAxisSuffixPlacement::NoLabels ); const QDomElement numericFormatElement = element.firstChildElement( QStringLiteral( "numericFormat" ) ); mNumericFormat.reset( QgsApplication::numericFormatRegistry()->createFromXml( numericFormatElement, context ) ); const QDomElement gridMajorElement = element.firstChildElement( QStringLiteral( "gridMajorSymbol" ) ).firstChildElement( QStringLiteral( "symbol" ) ); mGridMajorSymbol = QgsSymbolLayerUtils::loadSymbol< QgsLineSymbol >( gridMajorElement, context ); const QDomElement gridMinorElement = element.firstChildElement( QStringLiteral( "gridMinorSymbol" ) ).firstChildElement( QStringLiteral( "symbol" ) ); mGridMinorSymbol = QgsSymbolLayerUtils::loadSymbol< QgsLineSymbol >( gridMinorElement, context ); const QDomElement textFormatElement = element.firstChildElement( QStringLiteral( "textFormat" ) ); mLabelTextFormat.readXml( textFormatElement, context ); return true; } QgsNumericFormat *QgsPlotAxis::numericFormat() const { return mNumericFormat.get(); } void QgsPlotAxis::setNumericFormat( QgsNumericFormat *format ) { mNumericFormat.reset( format ); } QString QgsPlotAxis::labelSuffix() const { return mLabelSuffix; } void QgsPlotAxis::setLabelSuffix( const QString &suffix ) { mLabelSuffix = suffix; } Qgis::PlotAxisSuffixPlacement QgsPlotAxis::labelSuffixPlacement() const { return mSuffixPlacement; } void QgsPlotAxis::setLabelSuffixPlacement( Qgis::PlotAxisSuffixPlacement placement ) { mSuffixPlacement = placement; } QgsLineSymbol *QgsPlotAxis::gridMajorSymbol() { return mGridMajorSymbol.get(); } void QgsPlotAxis::setGridMajorSymbol( QgsLineSymbol *symbol ) { mGridMajorSymbol.reset( symbol ); } QgsLineSymbol *QgsPlotAxis::gridMinorSymbol() { return mGridMinorSymbol.get(); } void QgsPlotAxis::setGridMinorSymbol( QgsLineSymbol *symbol ) { mGridMinorSymbol.reset( symbol ); } QgsTextFormat QgsPlotAxis::textFormat() const { return mLabelTextFormat; } void QgsPlotAxis::setTextFormat( const QgsTextFormat &format ) { mLabelTextFormat = format; } // // Qgs2DPlot // Qgs2DPlot::Qgs2DPlot() : mMargins( 2, 2, 2, 2 ) { } bool Qgs2DPlot::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const { QgsPlot::writeXml( element, document, context ); element.setAttribute( QStringLiteral( "margins" ), mMargins.toString() ); return true; } bool Qgs2DPlot::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { QgsPlot::readXml( element, context ); mMargins = QgsMargins::fromString( element.attribute( QStringLiteral( "margins" ) ) ); return true; } void Qgs2DPlot::render( QgsRenderContext &context, const QgsPlotData &plotData ) { QgsExpressionContextScope *plotScope = new QgsExpressionContextScope( QStringLiteral( "plot" ) ); const QgsExpressionContextScopePopper scopePopper( context.expressionContext(), plotScope ); const QRectF plotArea = interiorPlotArea( context ); // give subclasses a chance to draw their content renderContent( context, plotArea, plotData ); } void Qgs2DPlot::renderContent( QgsRenderContext &, const QRectF &, const QgsPlotData & ) { } Qgs2DPlot::~Qgs2DPlot() = default; QSizeF Qgs2DPlot::size() const { return mSize; } void Qgs2DPlot::setSize( QSizeF size ) { mSize = size; } QRectF Qgs2DPlot::interiorPlotArea( QgsRenderContext & ) const { return QRectF( 0, 0, mSize.width(), mSize.height() ); } const QgsMargins &Qgs2DPlot::margins() const { return mMargins; } void Qgs2DPlot::setMargins( const QgsMargins &margins ) { mMargins = margins; } // // Qgs2DPlot // Qgs2DXyPlot::Qgs2DXyPlot() : Qgs2DPlot() { // setup default style mChartBackgroundSymbol.reset( QgsPlotDefaultSettings::chartBackgroundSymbol() ); mChartBorderSymbol.reset( QgsPlotDefaultSettings::chartBorderSymbol() ); } bool Qgs2DXyPlot::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const { Qgs2DPlot::writeXml( element, document, context ); element.setAttribute( QStringLiteral( "minX" ), qgsDoubleToString( mMinX ) ); element.setAttribute( QStringLiteral( "maxX" ), qgsDoubleToString( mMaxX ) ); element.setAttribute( QStringLiteral( "minY" ), qgsDoubleToString( mMinY ) ); element.setAttribute( QStringLiteral( "maxY" ), qgsDoubleToString( mMaxY ) ); QDomElement xAxisElement = document.createElement( QStringLiteral( "xAxis" ) ); mXAxis.writeXml( xAxisElement, document, context ); element.appendChild( xAxisElement ); QDomElement yAxisElement = document.createElement( QStringLiteral( "yAxis" ) ); mYAxis.writeXml( yAxisElement, document, context ); element.appendChild( yAxisElement ); QDomElement backgroundElement = document.createElement( QStringLiteral( "backgroundSymbol" ) ); backgroundElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mChartBackgroundSymbol.get(), document, context ) ); element.appendChild( backgroundElement ); QDomElement borderElement = document.createElement( QStringLiteral( "borderSymbol" ) ); borderElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mChartBorderSymbol.get(), document, context ) ); element.appendChild( borderElement ); return true; } bool Qgs2DXyPlot::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { Qgs2DPlot::readXml( element, context ); mMinX = element.attribute( QStringLiteral( "minX" ) ).toDouble(); mMaxX = element.attribute( QStringLiteral( "maxX" ) ).toDouble(); mMinY = element.attribute( QStringLiteral( "minY" ) ).toDouble(); mMaxY = element.attribute( QStringLiteral( "maxY" ) ).toDouble(); const QDomElement xAxisElement = element.firstChildElement( QStringLiteral( "xAxis" ) ); mXAxis.readXml( xAxisElement, context ); const QDomElement yAxisElement = element.firstChildElement( QStringLiteral( "yAxis" ) ); mYAxis.readXml( yAxisElement, context ); const QDomElement backgroundElement = element.firstChildElement( QStringLiteral( "backgroundSymbol" ) ).firstChildElement( QStringLiteral( "symbol" ) ); mChartBackgroundSymbol = QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( backgroundElement, context ); const QDomElement borderElement = element.firstChildElement( QStringLiteral( "borderSymbol" ) ).firstChildElement( QStringLiteral( "symbol" ) ); mChartBorderSymbol = QgsSymbolLayerUtils::loadSymbol< QgsFillSymbol >( borderElement, context ); return true; } void Qgs2DXyPlot::render( QgsRenderContext &context, const QgsPlotData &plotData ) { QgsExpressionContextScope *plotScope = new QgsExpressionContextScope( QStringLiteral( "plot" ) ); const QgsExpressionContextScopePopper scopePopper( context.expressionContext(), plotScope ); mChartBackgroundSymbol->startRender( context ); mChartBorderSymbol->startRender( context ); mXAxis.gridMinorSymbol()->startRender( context ); mYAxis.gridMinorSymbol()->startRender( context ); mXAxis.gridMajorSymbol()->startRender( context ); mYAxis.gridMajorSymbol()->startRender( context ); const double firstMinorXGrid = std::ceil( mMinX / mXAxis.gridIntervalMinor() ) * mXAxis.gridIntervalMinor(); const double firstMajorXGrid = std::ceil( mMinX / mXAxis.gridIntervalMajor() ) * mXAxis.gridIntervalMajor(); const double firstMinorYGrid = std::ceil( mMinY / mYAxis.gridIntervalMinor() ) * mYAxis.gridIntervalMinor(); const double firstMajorYGrid = std::ceil( mMinY / mYAxis.gridIntervalMajor() ) * mYAxis.gridIntervalMajor(); const double firstXLabel = mXAxis.labelInterval() > 0 ? std::ceil( mMinX / mXAxis.labelInterval() ) * mXAxis.labelInterval() : 0; const double firstYLabel = mYAxis.labelInterval() > 0 ? std::ceil( mMinY / mYAxis.labelInterval() ) * mYAxis.labelInterval() : 0; const QString xAxisSuffix = mXAxis.labelSuffix(); const QString yAxisSuffix = mYAxis.labelSuffix(); const QRectF plotArea = interiorPlotArea( context ); const double xTolerance = mXAxis.gridIntervalMinor() / 100000; const double yTolerance = mYAxis.gridIntervalMinor() / 100000; QgsNumericFormatContext numericContext; // categories QList categories; const QList seriesList = plotData.series(); if ( !seriesList.isEmpty() ) { categories = seriesList.at( 0 )->categories(); } // calculate text metrics double maxYAxisLabelWidth = 0; plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "y" ), true ) ); if ( mYAxis.type() == Qgis::PlotAxisType::ValueType && mYAxis.labelInterval() > 0 ) { for ( double currentY = firstYLabel; ; currentY += mYAxis.labelInterval() ) { const bool hasMoreLabels = currentY + mYAxis.labelInterval() <= mMaxY && !qgsDoubleNear( currentY + mYAxis.labelInterval(), mMaxY, yTolerance ); plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis_value" ), currentY, true ) ); QString text = mYAxis.numericFormat()->formatDouble( currentY, numericContext ); switch ( mYAxis.labelSuffixPlacement() ) { case Qgis::PlotAxisSuffixPlacement::NoLabels: break; case Qgis::PlotAxisSuffixPlacement::EveryLabel: text += yAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::FirstLabel: if ( currentY == firstYLabel ) text += yAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::LastLabel: if ( !hasMoreLabels ) text += yAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::FirstAndLastLabels: if ( currentY == firstYLabel || !hasMoreLabels ) text += yAxisSuffix; break; } maxYAxisLabelWidth = std::max( maxYAxisLabelWidth, QgsTextRenderer::textWidth( context, mYAxis.textFormat(), { text } ) ); if ( !hasMoreLabels ) break; } } else if ( mYAxis.type() == Qgis::PlotAxisType::CategoryType ) { for ( int i = 0; i < categories.size(); i++ ) { const QString text = categories.at( i ).toString(); maxYAxisLabelWidth = std::max( maxYAxisLabelWidth, QgsTextRenderer::textWidth( context, mYAxis.textFormat(), { text } ) ); } } const double chartAreaLeft = plotArea.left(); const double chartAreaRight = plotArea.right(); const double chartAreaTop = plotArea.top(); const double chartAreaBottom = plotArea.bottom(); // chart background mChartBackgroundSymbol->renderPolygon( QPolygonF( { QPointF( chartAreaLeft, chartAreaTop ), QPointF( chartAreaRight, chartAreaTop ), QPointF( chartAreaRight, chartAreaBottom ), QPointF( chartAreaLeft, chartAreaBottom ), QPointF( chartAreaLeft, chartAreaTop ) } ), nullptr, nullptr, context ); const double xScale = ( chartAreaRight - chartAreaLeft ) / ( mMaxX - mMinX ); const double yScale = ( chartAreaBottom - chartAreaTop ) / ( mMaxY - mMinY ); constexpr int MAX_OBJECTS = 1000; // grid lines // x if ( mXAxis.type() == Qgis::PlotAxisType::ValueType ) { plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "x" ), true ) ); double nextMajorXGrid = firstMajorXGrid; int objectNumber = 0; for ( double currentX = firstMinorXGrid; objectNumber < MAX_OBJECTS && ( currentX <= mMaxX && !qgsDoubleNear( currentX, mMaxX, xTolerance ) ); currentX += mXAxis.gridIntervalMinor(), ++objectNumber ) { bool isMinor = true; if ( qgsDoubleNear( currentX, nextMajorXGrid, xTolerance ) ) { isMinor = false; nextMajorXGrid += mXAxis.gridIntervalMajor(); } plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis_value" ), currentX, true ) ); QgsLineSymbol *currentGridSymbol = isMinor ? mXAxis.gridMinorSymbol() : mXAxis.gridMajorSymbol(); currentGridSymbol->renderPolyline( QPolygonF( QVector { QPointF( ( currentX - mMinX ) * xScale + chartAreaLeft, chartAreaBottom ), QPointF( ( currentX - mMinX ) * xScale + chartAreaLeft, chartAreaTop ) } ), nullptr, context ); } } // y if ( mYAxis.type() == Qgis::PlotAxisType::ValueType ) { plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "y" ), true ) ); double nextMajorYGrid = firstMajorYGrid; int objectNumber = 0; for ( double currentY = firstMinorYGrid; objectNumber < MAX_OBJECTS && ( currentY <= mMaxY && !qgsDoubleNear( currentY, mMaxY, yTolerance ) ); currentY += mYAxis.gridIntervalMinor(), ++objectNumber ) { bool isMinor = true; if ( qgsDoubleNear( currentY, nextMajorYGrid, yTolerance ) ) { isMinor = false; nextMajorYGrid += mYAxis.gridIntervalMajor(); } plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis_value" ), currentY, true ) ); QgsLineSymbol *currentGridSymbol = isMinor ? mYAxis.gridMinorSymbol() : mYAxis.gridMajorSymbol(); currentGridSymbol->renderPolyline( QPolygonF( QVector { QPointF( chartAreaLeft, chartAreaBottom - ( currentY - mMinY ) * yScale ), QPointF( chartAreaRight, chartAreaBottom - ( currentY - mMinY ) * yScale ) } ), nullptr, context ); } } // axis labels // x if ( mXAxis.type() == Qgis::PlotAxisType::ValueType ) { plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "x" ), true ) ); int objectNumber = 0; if ( mXAxis.labelInterval() > 0 ) { for ( double currentX = firstXLabel; ; currentX += mXAxis.labelInterval(), ++objectNumber ) { const bool hasMoreLabels = objectNumber + 1 < MAX_OBJECTS && ( currentX + mXAxis.labelInterval() <= mMaxX || qgsDoubleNear( currentX + mXAxis.labelInterval(), mMaxX, xTolerance ) ); plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis_value" ), currentX, true ) ); QString text = mXAxis.numericFormat()->formatDouble( currentX, numericContext ); switch ( mXAxis.labelSuffixPlacement() ) { case Qgis::PlotAxisSuffixPlacement::NoLabels: break; case Qgis::PlotAxisSuffixPlacement::EveryLabel: text += xAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::FirstLabel: if ( objectNumber == 0 ) text += xAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::LastLabel: if ( !hasMoreLabels ) text += xAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::FirstAndLastLabels: if ( objectNumber == 0 || !hasMoreLabels ) text += xAxisSuffix; break; } QgsTextRenderer::drawText( QPointF( ( currentX - mMinX ) * xScale + chartAreaLeft, mSize.height() - context.convertToPainterUnits( mMargins.bottom(), Qgis::RenderUnit::Millimeters ) ), 0, Qgis::TextHorizontalAlignment::Center, { text }, context, mXAxis.textFormat() ); if ( !hasMoreLabels ) break; } } } else if ( mXAxis.type() == Qgis::PlotAxisType::CategoryType ) { plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "x" ), true ) ); const double categoryWidth = plotArea.width() / categories.size(); for ( int i = 0; i < categories.size(); i++ ) { const double currentX = ( i * categoryWidth ) + categoryWidth / 2.0; plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis_value" ), categories.at( i ), true ) ); const QString text = categories.at( i ).toString(); QgsTextRenderer::drawText( QPointF( currentX + chartAreaLeft, mSize.height() - context.convertToPainterUnits( mMargins.bottom(), Qgis::RenderUnit::Millimeters ) ), 0, Qgis::TextHorizontalAlignment::Center, { text }, context, mXAxis.textFormat() ); } } // y if ( mYAxis.type() == Qgis::PlotAxisType::ValueType ) { plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "y" ), true ) ); int objectNumber = 0; if ( mYAxis.labelInterval() > 0 ) { for ( double currentY = firstYLabel; ; currentY += mYAxis.labelInterval(), ++objectNumber ) { const bool hasMoreLabels = objectNumber + 1 < MAX_OBJECTS && ( currentY + mYAxis.labelInterval() <= mMaxY || qgsDoubleNear( currentY + mYAxis.labelInterval(), mMaxY, yTolerance ) ); plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis_value" ), currentY, true ) ); QString text = mYAxis.numericFormat()->formatDouble( currentY, numericContext ); switch ( mYAxis.labelSuffixPlacement() ) { case Qgis::PlotAxisSuffixPlacement::NoLabels: break; case Qgis::PlotAxisSuffixPlacement::EveryLabel: text += yAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::FirstLabel: if ( objectNumber == 0 ) text += yAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::LastLabel: if ( !hasMoreLabels ) text += yAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::FirstAndLastLabels: if ( objectNumber == 0 || !hasMoreLabels ) text += yAxisSuffix; break; } const double height = QgsTextRenderer::textHeight( context, mYAxis.textFormat(), { text } ); QgsTextRenderer::drawText( QPointF( maxYAxisLabelWidth + context.convertToPainterUnits( mMargins.left(), Qgis::RenderUnit::Millimeters ), chartAreaBottom - ( currentY - mMinY ) * yScale + height / 2 ), 0, Qgis::TextHorizontalAlignment::Right, { text }, context, mYAxis.textFormat(), false ); if ( !hasMoreLabels ) break; } } } else if ( mYAxis.type() == Qgis::PlotAxisType::CategoryType ) { plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "y" ), true ) ); const double categoryHeight = plotArea.height() / categories.size(); for ( int i = 0; i < categories.size(); i++ ) { const double currentY = ( i * categoryHeight ) + categoryHeight / 2.0; plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis_value" ), categories.at( i ), true ) ); const QString text = categories.at( i ).toString(); const double height = QgsTextRenderer::textHeight( context, mYAxis.textFormat(), { text } ); QgsTextRenderer::drawText( QPointF( maxYAxisLabelWidth + context.convertToPainterUnits( mMargins.left(), Qgis::RenderUnit::Millimeters ), chartAreaBottom - currentY + height / 2 ), 0, Qgis::TextHorizontalAlignment::Right, { text }, context, mYAxis.textFormat(), false ); } } // give subclasses a chance to draw their content renderContent( context, plotArea, plotData ); // border mChartBorderSymbol->renderPolygon( QPolygonF( { QPointF( chartAreaLeft, chartAreaTop ), QPointF( chartAreaRight, chartAreaTop ), QPointF( chartAreaRight, chartAreaBottom ), QPointF( chartAreaLeft, chartAreaBottom ), QPointF( chartAreaLeft, chartAreaTop ) } ), nullptr, nullptr, context ); mChartBackgroundSymbol->stopRender( context ); mChartBorderSymbol->stopRender( context ); mXAxis.gridMinorSymbol()->stopRender( context ); mYAxis.gridMinorSymbol()->stopRender( context ); mXAxis.gridMajorSymbol()->stopRender( context ); mYAxis.gridMajorSymbol()->stopRender( context ); } Qgs2DXyPlot::~Qgs2DXyPlot() = default; QRectF Qgs2DXyPlot::interiorPlotArea( QgsRenderContext &context ) const { QgsExpressionContextScope *plotScope = new QgsExpressionContextScope( QStringLiteral( "plot" ) ); const QgsExpressionContextScopePopper scopePopper( context.expressionContext(), plotScope ); const double firstMinorYGrid = std::ceil( mMinY / mYAxis.gridIntervalMinor() ) * mYAxis.gridIntervalMinor(); const double firstXLabel = mXAxis.labelInterval() > 0 ? std::ceil( mMinX / mXAxis.labelInterval() ) * mXAxis.labelInterval() : 0; const QString xAxisSuffix = mXAxis.labelSuffix(); const QString yAxisSuffix = mYAxis.labelSuffix(); const double yAxisSuffixWidth = yAxisSuffix.isEmpty() ? 0 : QgsTextRenderer::textWidth( context, mYAxis.textFormat(), { yAxisSuffix } ); QgsNumericFormatContext numericContext; const double xTolerance = mXAxis.gridIntervalMinor() / 100000; const double yTolerance = mYAxis.gridIntervalMinor() / 100000; constexpr int MAX_LABELS = 1000; // calculate text metrics int labelNumber = 0; double maxXAxisLabelHeight = 0; plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "x" ), true ) ); if ( mXAxis.labelInterval() > 0 ) { for ( double currentX = firstXLabel; ; currentX += mXAxis.labelInterval(), labelNumber++ ) { const bool hasMoreLabels = labelNumber + 1 < MAX_LABELS && ( currentX + mXAxis.labelInterval() <= mMaxX || qgsDoubleNear( currentX + mXAxis.labelInterval(), mMaxX, xTolerance ) ); plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis_value" ), currentX, true ) ); QString text = mXAxis.numericFormat()->formatDouble( currentX, numericContext ); switch ( mXAxis.labelSuffixPlacement() ) { case Qgis::PlotAxisSuffixPlacement::NoLabels: break; case Qgis::PlotAxisSuffixPlacement::EveryLabel: text += xAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::FirstLabel: if ( labelNumber == 0 ) text += xAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::LastLabel: if ( !hasMoreLabels ) text += xAxisSuffix; break; case Qgis::PlotAxisSuffixPlacement::FirstAndLastLabels: if ( labelNumber == 0 || !hasMoreLabels ) text += xAxisSuffix; break; } maxXAxisLabelHeight = std::max( maxXAxisLabelHeight, QgsTextRenderer::textHeight( context, mXAxis.textFormat(), { text } ) ); if ( !hasMoreLabels ) break; } } double maxYAxisLabelWidth = 0; labelNumber = 0; plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "y" ), true ) ); for ( double currentY = firstMinorYGrid; ; currentY += mYAxis.gridIntervalMinor(), labelNumber ++ ) { const bool hasMoreLabels = labelNumber + 1 < MAX_LABELS && ( currentY + mYAxis.gridIntervalMinor() <= mMaxY || qgsDoubleNear( currentY + mYAxis.gridIntervalMinor(), mMaxY, yTolerance ) ); plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis_value" ), currentY, true ) ); const QString text = mYAxis.numericFormat()->formatDouble( currentY, numericContext ); double thisLabelWidth = QgsTextRenderer::textWidth( context, mYAxis.textFormat(), { text } ); if ( yAxisSuffixWidth > 0 ) { switch ( mYAxis.labelSuffixPlacement() ) { case Qgis::PlotAxisSuffixPlacement::NoLabels: break; case Qgis::PlotAxisSuffixPlacement::EveryLabel: thisLabelWidth += yAxisSuffixWidth; break; case Qgis::PlotAxisSuffixPlacement::FirstLabel: if ( labelNumber == 0 ) thisLabelWidth += yAxisSuffixWidth; break; case Qgis::PlotAxisSuffixPlacement::LastLabel: if ( !hasMoreLabels ) thisLabelWidth += yAxisSuffixWidth; break; case Qgis::PlotAxisSuffixPlacement::FirstAndLastLabels: if ( labelNumber == 0 || !hasMoreLabels ) thisLabelWidth += yAxisSuffixWidth; break; } } maxYAxisLabelWidth = std::max( maxYAxisLabelWidth, thisLabelWidth ); if ( !hasMoreLabels ) break; } const double leftTextSize = maxYAxisLabelWidth + context.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); const double rightTextSize = 0; const double bottomTextSize = maxXAxisLabelHeight + context.convertToPainterUnits( 0.5, Qgis::RenderUnit::Millimeters ); const double topTextSize = 0; const double leftMargin = context.convertToPainterUnits( mMargins.left(), Qgis::RenderUnit::Millimeters ) + leftTextSize; const double rightMargin = context.convertToPainterUnits( mMargins.right(), Qgis::RenderUnit::Millimeters ) + rightTextSize; const double topMargin = context.convertToPainterUnits( mMargins.top(), Qgis::RenderUnit::Millimeters ) + topTextSize; const double bottomMargin = context.convertToPainterUnits( mMargins.bottom(), Qgis::RenderUnit::Millimeters ) + bottomTextSize; return QRectF( leftMargin, topMargin, mSize.width() - rightMargin - leftMargin, mSize.height() - bottomMargin - topMargin ); } void Qgs2DXyPlot::calculateOptimisedIntervals( QgsRenderContext &context ) { if ( !mSize.isValid() ) return; // aim for about 40% coverage of label text to available space constexpr double IDEAL_WIDTH = 0.4; constexpr double TOLERANCE = 0.04; constexpr int MAX_LABELS = 1000; const double leftMargin = context.convertToPainterUnits( mMargins.left(), Qgis::RenderUnit::Millimeters ); const double rightMargin = context.convertToPainterUnits( mMargins.right(), Qgis::RenderUnit::Millimeters ); const double topMargin = context.convertToPainterUnits( mMargins.top(), Qgis::RenderUnit::Millimeters ); const double bottomMargin = context.convertToPainterUnits( mMargins.bottom(), Qgis::RenderUnit::Millimeters ); const double availableWidth = mSize.width() - leftMargin - rightMargin; const double availableHeight = mSize.height() - topMargin - bottomMargin; QgsNumericFormatContext numericContext; auto refineIntervalForAxis = [&]( double axisMinimum, double axisMaximum, const std::function< double( double ) > &sizeForLabel, double availableSize, double idealSizePercent, double sizeTolerancePercent, double & labelInterval, double & majorInterval, double & minorInterval ) { auto roundBase10 = []( double value )->double { return std::pow( 10, std::floor( std::log10( value ) ) ); }; // if the current interval is good enough, don't change it! double totalSize = 0; int initialLabelCount = 0; { const double firstLabelPos = std::ceil( axisMinimum / labelInterval ) * labelInterval; for ( double currentPos = firstLabelPos; initialLabelCount <= MAX_LABELS && currentPos <= axisMaximum; currentPos += labelInterval, ++initialLabelCount ) { totalSize += sizeForLabel( currentPos ); } } // we consider the current interval as "good enough" if it results in somewhere between 20-60% label text coverage over the size if ( initialLabelCount >= MAX_LABELS || ( totalSize / availableSize < ( idealSizePercent - sizeTolerancePercent ) ) || ( totalSize / availableSize > ( idealSizePercent + sizeTolerancePercent ) ) ) { // we start with trying to fit 30 labels in and then raise the interval till we're happy int numberLabelsInitial = std::floor( availableSize / 30 ); double labelIntervalTest = ( axisMaximum - axisMinimum ) / numberLabelsInitial; double baseValue = roundBase10( labelIntervalTest ); double candidate = baseValue; int currentMultiplier = 1; int numberLabels = 0; while ( true ) { const double firstLabelPosition = std::ceil( axisMinimum / candidate ) * candidate; double totalSize = 0; numberLabels = 0; for ( double currentPos = firstLabelPosition; currentPos <= axisMaximum; currentPos += candidate ) { totalSize += sizeForLabel( currentPos ); numberLabels += 1; if ( numberLabels > MAX_LABELS ) // avoid hangs if candidate size is very small break; } if ( numberLabels <= MAX_LABELS && totalSize <= availableSize * idealSizePercent ) break; if ( currentMultiplier == 1 ) currentMultiplier = 2; else if ( currentMultiplier == 2 ) currentMultiplier = 5; else if ( currentMultiplier == 5 ) { baseValue *= 10; currentMultiplier = 1; } candidate = baseValue * currentMultiplier; } labelInterval = candidate; if ( numberLabels < 10 ) { minorInterval = labelInterval / 2; majorInterval = minorInterval * 4; } else { minorInterval = labelInterval; majorInterval = minorInterval * 5; } } }; { double labelIntervalX = mXAxis.labelInterval(); double majorIntervalX = mXAxis.gridIntervalMajor(); double minorIntervalX = mXAxis.gridIntervalMinor(); const QString suffixX = mXAxis.labelSuffix(); const double suffixWidth = !suffixX.isEmpty() ? QgsTextRenderer::textWidth( context, mXAxis.textFormat(), { suffixX } ) : 0; refineIntervalForAxis( mMinX, mMaxX, [this, &context, suffixWidth, &numericContext]( double position ) -> double { const QString text = mXAxis.numericFormat()->formatDouble( position, numericContext ); // this isn't accurate, as we're always considering the suffix to be present... but it's too tricky to actually consider // the suffix placement! return QgsTextRenderer::textWidth( context, mXAxis.textFormat(), { text } ) + suffixWidth; }, availableWidth, IDEAL_WIDTH, TOLERANCE, labelIntervalX, majorIntervalX, minorIntervalX ); mXAxis.setLabelInterval( labelIntervalX ); mXAxis.setGridIntervalMajor( majorIntervalX ); mXAxis.setGridIntervalMinor( minorIntervalX ); } { double labelIntervalY = mYAxis.labelInterval(); double majorIntervalY = mYAxis.gridIntervalMajor(); double minorIntervalY = mYAxis.gridIntervalMinor(); const QString suffixY = mYAxis.labelSuffix(); refineIntervalForAxis( mMinY, mMaxY, [this, &context, suffixY, &numericContext]( double position ) -> double { const QString text = mYAxis.numericFormat()->formatDouble( position, numericContext ); // this isn't accurate, as we're always considering the suffix to be present... but it's too tricky to actually consider // the suffix placement! return QgsTextRenderer::textHeight( context, mYAxis.textFormat(), { text + suffixY } ); }, availableHeight, IDEAL_WIDTH, TOLERANCE, labelIntervalY, majorIntervalY, minorIntervalY ); mYAxis.setLabelInterval( labelIntervalY ); mYAxis.setGridIntervalMajor( majorIntervalY ); mYAxis.setGridIntervalMinor( minorIntervalY ); } } QgsFillSymbol *Qgs2DXyPlot::chartBackgroundSymbol() { return mChartBackgroundSymbol.get(); } void Qgs2DXyPlot::setChartBackgroundSymbol( QgsFillSymbol *symbol ) { mChartBackgroundSymbol.reset( symbol ); } QgsFillSymbol *Qgs2DXyPlot::chartBorderSymbol() { return mChartBorderSymbol.get(); } void Qgs2DXyPlot::setChartBorderSymbol( QgsFillSymbol *symbol ) { mChartBorderSymbol.reset( symbol ); } // // QgsPlotDefaultSettings // QgsNumericFormat *QgsPlotDefaultSettings::axisLabelNumericFormat() { return new QgsBasicNumericFormat(); } QgsLineSymbol *QgsPlotDefaultSettings::axisGridMajorSymbol() { auto gridMajor = std::make_unique< QgsSimpleLineSymbolLayer >( QColor( 20, 20, 20, 150 ), 0.1 ); gridMajor->setPenCapStyle( Qt::FlatCap ); return new QgsLineSymbol( QgsSymbolLayerList( { gridMajor.release() } ) ); } QgsLineSymbol *QgsPlotDefaultSettings::axisGridMinorSymbol() { auto gridMinor = std::make_unique< QgsSimpleLineSymbolLayer >( QColor( 20, 20, 20, 50 ), 0.1 ); gridMinor->setPenCapStyle( Qt::FlatCap ); return new QgsLineSymbol( QgsSymbolLayerList( { gridMinor.release() } ) ); } QgsFillSymbol *QgsPlotDefaultSettings::chartBackgroundSymbol() { auto chartFill = std::make_unique< QgsSimpleFillSymbolLayer >( QColor( 255, 255, 255 ) ); return new QgsFillSymbol( QgsSymbolLayerList( { chartFill.release() } ) ); } QgsFillSymbol *QgsPlotDefaultSettings::chartBorderSymbol() { auto chartBorder = std::make_unique< QgsSimpleLineSymbolLayer >( QColor( 20, 20, 20 ), 0.1 ); return new QgsFillSymbol( QgsSymbolLayerList( { chartBorder.release() } ) ); } // // QgsPlotData // QgsPlotData::~QgsPlotData() { clearSeries(); } QList QgsPlotData::series() const { return mSeries; } void QgsPlotData::addSeries( QgsAbstractPlotSeries *series ) { if ( !mSeries.contains( series ) ) { mSeries << series; } } void QgsPlotData::clearSeries() { qDeleteAll( mSeries ); mSeries.clear(); } // // QgsAbstractPlotSeries // QString QgsAbstractPlotSeries::name() const { return mName; } void QgsAbstractPlotSeries::setName( const QString &name ) { mName = name; } QgsSymbol *QgsAbstractPlotSeries::symbol() const { return mSymbol.get(); } void QgsAbstractPlotSeries::setSymbol( QgsSymbol *symbol ) { mSymbol.reset( symbol ); } QList QgsAbstractPlotSeries::categories() const { return QList(); } // // QgsXyPlotSeries // QList QgsXyPlotSeries::categories() const { QList categories; for ( const std::pair &pair : std::as_const( mData ) ) { categories << pair.first; } return categories; } QList> QgsXyPlotSeries::data() const { return mData; } void QgsXyPlotSeries::append( const QVariant &x, const double &y ) { mData << std::make_pair( x, y ); } void QgsXyPlotSeries::clear() { mData.clear(); }