diff --git a/python/PyQt6/core/auto_additions/qgsplot.py b/python/PyQt6/core/auto_additions/qgsplot.py index b230d3f6bac..c0197505b84 100644 --- a/python/PyQt6/core/auto_additions/qgsplot.py +++ b/python/PyQt6/core/auto_additions/qgsplot.py @@ -13,25 +13,27 @@ try: QgsPlot.__group__ = ['plot'] except (NameError, AttributeError): pass +try: + QgsAbstractPlotSeries.__virtual_methods__ = ['categories'] + QgsAbstractPlotSeries.__group__ = ['plot'] +except (NameError, AttributeError): + pass try: Qgs2DPlot.__virtual_methods__ = ['render', 'renderContent', 'interiorPlotArea'] Qgs2DPlot.__overridden_methods__ = ['writeXml', 'readXml'] Qgs2DPlot.__group__ = ['plot'] except (NameError, AttributeError): pass +try: + QgsXyPlotSeries.__overridden_methods__ = ['categories'] + QgsXyPlotSeries.__group__ = ['plot'] +except (NameError, AttributeError): + pass try: Qgs2DXyPlot.__overridden_methods__ = ['writeXml', 'readXml', 'render', 'interiorPlotArea'] Qgs2DXyPlot.__group__ = ['plot'] except (NameError, AttributeError): pass -try: - QgsAbstractPlotSeries.__group__ = ['plot'] -except (NameError, AttributeError): - pass -try: - QgsXyPlotSeries.__group__ = ['plot'] -except (NameError, AttributeError): - pass try: QgsPlotData.__group__ = ['plot'] except (NameError, AttributeError): diff --git a/python/PyQt6/core/auto_generated/plot/qgschart.sip.in b/python/PyQt6/core/auto_generated/plot/qgschart.sip.in new file mode 100644 index 00000000000..d8357668d0d --- /dev/null +++ b/python/PyQt6/core/auto_generated/plot/qgschart.sip.in @@ -0,0 +1,69 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/plot/qgschart.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + +class QgsBarChart : Qgs2DXyPlot +{ +%Docstring(signature="appended") +A simple bar chart class. + +.. warning:: + + This class is not considered stable API, and may change in future! + +.. versionadded:: 4.0 +%End + +%TypeHeaderCode +#include "qgschart.h" +%End + public: + + QgsBarChart(); + ~QgsBarChart(); + + virtual void renderContent( QgsRenderContext &context, const QRectF &plotArea, const QgsPlotData &plotData = QgsPlotData() ); + + +}; + + +class QgsLineChart : Qgs2DXyPlot +{ +%Docstring(signature="appended") +A simple line chart class. + +.. warning:: + + This class is not considered stable API, and may change in future! + +.. versionadded:: 4.0 +%End + +%TypeHeaderCode +#include "qgschart.h" +%End + public: + + QgsLineChart(); + ~QgsLineChart(); + + virtual void renderContent( QgsRenderContext &context, const QRectF &plotArea, const QgsPlotData &plotData = QgsPlotData() ); + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/plot/qgschart.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/plot/qgsplot.sip.in b/python/PyQt6/core/auto_generated/plot/qgsplot.sip.in index 0354ca7e90b..1f24db57085 100644 --- a/python/PyQt6/core/auto_generated/plot/qgsplot.sip.in +++ b/python/PyQt6/core/auto_generated/plot/qgsplot.sip.in @@ -52,13 +52,16 @@ class QgsAbstractPlotSeries public: QgsAbstractPlotSeries(); - ~QgsAbstractPlotSeries(); + virtual ~QgsAbstractPlotSeries(); QString name() const; void setName( const QString &name ); QgsSymbol *symbol() const; - void setSymbol( QgsSymbol *symbol ); + void setSymbol( QgsSymbol *symbol /Transfer/ ); + virtual QList categories() const; + private: + QgsAbstractPlotSeries( const QgsAbstractPlotSeries &other ); }; class QgsXyPlotSeries : QgsAbstractPlotSeries @@ -72,9 +75,14 @@ class QgsXyPlotSeries : QgsAbstractPlotSeries QgsXyPlotSeries(); ~QgsXyPlotSeries(); + virtual QList categories() const; + + void append( const QVariant &x, const double &y ); void clear(); + private: + QgsXyPlotSeries( const QgsXyPlotSeries &other ); }; class QgsPlotData @@ -89,7 +97,7 @@ class QgsPlotData ~QgsPlotData(); QList series() const; - void addSeries( QgsAbstractPlotSeries *series ); + void addSeries( QgsAbstractPlotSeries *series /Transfer/ ); void clearSeries(); }; diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index 5422f9f79ce..87681cfff13 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -518,6 +518,7 @@ %Include auto_generated/painting/qgspaintenginehack.sip %Include auto_generated/painting/qgspainting.sip %Include auto_generated/pdf/qgspdfrenderer.sip +%Include auto_generated/plot/qgschart.sip %Include auto_generated/plot/qgsplot.sip %Include auto_generated/pointcloud/qgspointcloudattribute.sip %Include auto_generated/pointcloud/qgspointcloudattributebyramprenderer.sip diff --git a/python/core/auto_additions/qgsplot.py b/python/core/auto_additions/qgsplot.py index b230d3f6bac..c0197505b84 100644 --- a/python/core/auto_additions/qgsplot.py +++ b/python/core/auto_additions/qgsplot.py @@ -13,25 +13,27 @@ try: QgsPlot.__group__ = ['plot'] except (NameError, AttributeError): pass +try: + QgsAbstractPlotSeries.__virtual_methods__ = ['categories'] + QgsAbstractPlotSeries.__group__ = ['plot'] +except (NameError, AttributeError): + pass try: Qgs2DPlot.__virtual_methods__ = ['render', 'renderContent', 'interiorPlotArea'] Qgs2DPlot.__overridden_methods__ = ['writeXml', 'readXml'] Qgs2DPlot.__group__ = ['plot'] except (NameError, AttributeError): pass +try: + QgsXyPlotSeries.__overridden_methods__ = ['categories'] + QgsXyPlotSeries.__group__ = ['plot'] +except (NameError, AttributeError): + pass try: Qgs2DXyPlot.__overridden_methods__ = ['writeXml', 'readXml', 'render', 'interiorPlotArea'] Qgs2DXyPlot.__group__ = ['plot'] except (NameError, AttributeError): pass -try: - QgsAbstractPlotSeries.__group__ = ['plot'] -except (NameError, AttributeError): - pass -try: - QgsXyPlotSeries.__group__ = ['plot'] -except (NameError, AttributeError): - pass try: QgsPlotData.__group__ = ['plot'] except (NameError, AttributeError): diff --git a/python/core/auto_generated/plot/qgschart.sip.in b/python/core/auto_generated/plot/qgschart.sip.in new file mode 100644 index 00000000000..d8357668d0d --- /dev/null +++ b/python/core/auto_generated/plot/qgschart.sip.in @@ -0,0 +1,69 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/plot/qgschart.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + +class QgsBarChart : Qgs2DXyPlot +{ +%Docstring(signature="appended") +A simple bar chart class. + +.. warning:: + + This class is not considered stable API, and may change in future! + +.. versionadded:: 4.0 +%End + +%TypeHeaderCode +#include "qgschart.h" +%End + public: + + QgsBarChart(); + ~QgsBarChart(); + + virtual void renderContent( QgsRenderContext &context, const QRectF &plotArea, const QgsPlotData &plotData = QgsPlotData() ); + + +}; + + +class QgsLineChart : Qgs2DXyPlot +{ +%Docstring(signature="appended") +A simple line chart class. + +.. warning:: + + This class is not considered stable API, and may change in future! + +.. versionadded:: 4.0 +%End + +%TypeHeaderCode +#include "qgschart.h" +%End + public: + + QgsLineChart(); + ~QgsLineChart(); + + virtual void renderContent( QgsRenderContext &context, const QRectF &plotArea, const QgsPlotData &plotData = QgsPlotData() ); + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/plot/qgschart.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/core/auto_generated/plot/qgsplot.sip.in b/python/core/auto_generated/plot/qgsplot.sip.in index 0354ca7e90b..1f24db57085 100644 --- a/python/core/auto_generated/plot/qgsplot.sip.in +++ b/python/core/auto_generated/plot/qgsplot.sip.in @@ -52,13 +52,16 @@ class QgsAbstractPlotSeries public: QgsAbstractPlotSeries(); - ~QgsAbstractPlotSeries(); + virtual ~QgsAbstractPlotSeries(); QString name() const; void setName( const QString &name ); QgsSymbol *symbol() const; - void setSymbol( QgsSymbol *symbol ); + void setSymbol( QgsSymbol *symbol /Transfer/ ); + virtual QList categories() const; + private: + QgsAbstractPlotSeries( const QgsAbstractPlotSeries &other ); }; class QgsXyPlotSeries : QgsAbstractPlotSeries @@ -72,9 +75,14 @@ class QgsXyPlotSeries : QgsAbstractPlotSeries QgsXyPlotSeries(); ~QgsXyPlotSeries(); + virtual QList categories() const; + + void append( const QVariant &x, const double &y ); void clear(); + private: + QgsXyPlotSeries( const QgsXyPlotSeries &other ); }; class QgsPlotData @@ -89,7 +97,7 @@ class QgsPlotData ~QgsPlotData(); QList series() const; - void addSeries( QgsAbstractPlotSeries *series ); + void addSeries( QgsAbstractPlotSeries *series /Transfer/ ); void clearSeries(); }; diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 5422f9f79ce..87681cfff13 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -518,6 +518,7 @@ %Include auto_generated/painting/qgspaintenginehack.sip %Include auto_generated/painting/qgspainting.sip %Include auto_generated/pdf/qgspdfrenderer.sip +%Include auto_generated/plot/qgschart.sip %Include auto_generated/plot/qgsplot.sip %Include auto_generated/pointcloud/qgspointcloudattribute.sip %Include auto_generated/pointcloud/qgspointcloudattributebyramprenderer.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index dd4aa29ce5e..e8428beb52c 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -90,6 +90,7 @@ set(QGIS_CORE_SRCS gps/qgssatelliteinformation.cpp gps/qgsvectorlayergpslogger.cpp + plot/qgschart.cpp plot/qgsplot.cpp symbology/qgs25drenderer.cpp @@ -1739,6 +1740,7 @@ set(QGIS_CORE_HDRS pdf/qgspdfrenderer.h + plot/qgschart.h plot/qgsplot.h pointcloud/qgspointcloudattribute.h diff --git a/src/core/plot/qgschart.cpp b/src/core/plot/qgschart.cpp new file mode 100644 index 00000000000..b3ec744e267 --- /dev/null +++ b/src/core/plot/qgschart.cpp @@ -0,0 +1,163 @@ +/*************************************************************************** + 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 "qgschart.h" +#include "qgsexpressioncontextutils.h" +#include "qgssymbol.h" +#include "qgssymbollayer.h" +#include "qgsfillsymbol.h" +#include "qgslinesymbol.h" + +void QgsBarChart::renderContent( QgsRenderContext &context, const QRectF &plotArea, const QgsPlotData &plotData ) +{ + const QList seriesList = plotData.series(); + if ( seriesList.isEmpty() ) + { + return; + } + + const QList categories = seriesList.at( 0 )->categories(); + if ( categories.isEmpty() ) + { + return; + } + + QgsExpressionContextScope *chartScope = new QgsExpressionContextScope( QStringLiteral( "chart" ) ); + const QgsExpressionContextScopePopper scopePopper( context.expressionContext(), chartScope ); + + context.painter()->save(); + context.painter()->setClipRect( plotArea ); + + const double xScale = plotArea.width() / ( xMaximum() - xMinimum() ); + const double yScale = plotArea.height() / ( yMaximum() - yMinimum() ); + const double categoriesWidth = plotArea.width() / categories.size(); + const double barsWidth = categoriesWidth / 2; + const double barWidth = barsWidth / seriesList.size(); + int seriesIndex = 0; + for ( const QgsAbstractPlotSeries *series : seriesList ) + { + QgsFillSymbol *symbol = dynamic_cast( series->symbol() ); + if ( !symbol ) + { + continue; + } + symbol->startRender( context ); + + const double barStartAdjustement = -( barsWidth / 2 ) + barWidth * seriesIndex; + if ( const QgsXyPlotSeries *xySeries = dynamic_cast( series ) ) + { + const QList> data = xySeries->data(); + for ( const std::pair &pair : data ) + { + double x, y; + if ( xAxis().type() == Qgis::PlotAxisType::ValueType ) + { + x = ( pair.first.toDouble() ) * xScale + barStartAdjustement; + } + else if ( xAxis().type() == Qgis::PlotAxisType::CategoryType ) + { + x = ( categoriesWidth * categories.indexOf( pair.first ) ) + ( categoriesWidth / 2 ) + barStartAdjustement; + } + y = ( pair.second - yMinimum() ) * yScale; + + const double zero = ( 0.0 - yMinimum() ) * yScale; + const QPoint topLeft( plotArea.left() + x, + plotArea.y() + plotArea.height() - y ); + const QPoint bottomRight( plotArea.left() + x + barWidth, + plotArea.y() + plotArea.height() - zero ); + + chartScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "chart_value" ), pair.second, true ) ); + symbol->renderPolygon( QPolygonF( QRectF( topLeft, bottomRight ) ), nullptr, nullptr, context ); + } + } + + symbol->stopRender( context ); + seriesIndex++; + } + + context.painter()->restore(); +} + +void QgsLineChart::renderContent( QgsRenderContext &context, const QRectF &plotArea, const QgsPlotData &plotData ) +{ + const QList seriesList = plotData.series(); + if ( seriesList.isEmpty() ) + { + return; + } + + const QList categories = seriesList.at( 0 )->categories(); + if ( categories.isEmpty() ) + { + return; + } + + QgsExpressionContextScope *chartScope = new QgsExpressionContextScope( QStringLiteral( "chart" ) ); + const QgsExpressionContextScopePopper scopePopper( context.expressionContext(), chartScope ); + + context.painter()->save(); + context.painter()->setClipRect( plotArea ); + + const double xScale = plotArea.width() / ( xMaximum() - xMinimum() ); + const double yScale = plotArea.height() / ( yMaximum() - yMinimum() ); + const double categoriesWidth = plotArea.width() / categories.size(); + int seriesIndex = 0; + for ( const QgsAbstractPlotSeries *series : seriesList ) + { + QgsLineSymbol *symbol = dynamic_cast( series->symbol() ); + if ( !symbol ) + { + continue; + } + symbol->startRender( context ); + + if ( const QgsXyPlotSeries *xySeries = dynamic_cast( series ) ) + { + const QList> data = xySeries->data(); + QVector points; + QList values; + points.fill( QPointF(), xAxis().type() == Qgis::PlotAxisType::ValueType ? data.size() : categories.size() ); + int dataIndex = 0; + for ( const std::pair &pair : data ) + { + double x, y; + if ( xAxis().type() == Qgis::PlotAxisType::ValueType ) + { + x = ( pair.first.toDouble() ) * xScale; + } + else if ( xAxis().type() == Qgis::PlotAxisType::CategoryType ) + { + x = ( categoriesWidth * categories.indexOf( pair.first ) ) + ( categoriesWidth / 2 ); + } + y = ( pair.second - yMinimum() ) * yScale; + + values << pair.second; + points.replace( xAxis().type() == Qgis::PlotAxisType::ValueType ? dataIndex : categories.indexOf( pair.first ), QPointF( plotArea.x() + x, + plotArea.y() + plotArea.height() - y ) ); + dataIndex++; + } + + chartScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "chart_value" ), QVariant::fromValue( values ), true ) ); + symbol->renderPolyline( QPolygonF( points ), nullptr, context ); + } + + symbol->stopRender( context ); + seriesIndex++; + } + + context.painter()->restore(); +} diff --git a/src/core/plot/qgschart.h b/src/core/plot/qgschart.h new file mode 100644 index 00000000000..45480577f1e --- /dev/null +++ b/src/core/plot/qgschart.h @@ -0,0 +1,70 @@ +/*************************************************************************** + qgschart.h + --------------- + begin : June 2025 + copyright : (C) 2025 by Mathieu + email : mathieu at opengis dot ch + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +#ifndef QGSCHART_H +#define QGSCHART_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgsplot.h" + + +/** + * \brief A simple bar chart class. + * + * \warning This class is not considered stable API, and may change in future! + * + * \ingroup core + * \since QGIS 4.0 + */ +class CORE_EXPORT QgsBarChart : public Qgs2DXyPlot +{ + public: + + QgsBarChart() = default; + ~QgsBarChart() = default; + + void renderContent( QgsRenderContext &context, const QRectF &plotArea, const QgsPlotData &plotData = QgsPlotData() ) override; + + private: + + +}; + + +/** + * \brief A simple line chart class. + * + * \warning This class is not considered stable API, and may change in future! + * + * \ingroup core + * \since QGIS 4.0 + */ +class CORE_EXPORT QgsLineChart : public Qgs2DXyPlot +{ + public: + + QgsLineChart() = default; + ~QgsLineChart() = default; + + void renderContent( QgsRenderContext &context, const QRectF &plotArea, const QgsPlotData &plotData = QgsPlotData() ) override; + + private: + + +}; + +#endif diff --git a/src/core/plot/qgsplot.cpp b/src/core/plot/qgsplot.cpp index 79c0e02f155..e4b9d8adb86 100644 --- a/src/core/plot/qgsplot.cpp +++ b/src/core/plot/qgsplot.cpp @@ -334,10 +334,18 @@ void Qgs2DXyPlot::render( QgsRenderContext &context, const QgsPlotData &plotData 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.labelInterval() > 0 ) + if ( mYAxis.type() == Qgis::PlotAxisType::ValueType && mYAxis.labelInterval() > 0 ) { for ( double currentY = firstYLabel; ; currentY += mYAxis.labelInterval() ) { @@ -374,6 +382,14 @@ void Qgs2DXyPlot::render( QgsRenderContext &context, const QgsPlotData &plotData 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(); @@ -398,139 +414,180 @@ void Qgs2DXyPlot::render( QgsRenderContext &context, const QgsPlotData &plotData // grid lines // x - 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 ) + if ( mXAxis.type() == Qgis::PlotAxisType::ValueType ) { - bool isMinor = true; - if ( qgsDoubleNear( currentX, nextMajorXGrid, xTolerance ) ) + 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 ) { - isMinor = false; - nextMajorXGrid += mXAxis.gridIntervalMajor(); + 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 ); } - - 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 - plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "y" ), true ) ); - double nextMajorYGrid = firstMajorYGrid; - objectNumber = 0; - for ( double currentY = firstMinorYGrid; objectNumber < MAX_OBJECTS && ( currentY <= mMaxY && !qgsDoubleNear( currentY, mMaxY, yTolerance ) ); currentY += mYAxis.gridIntervalMinor(), ++objectNumber ) + if ( mYAxis.type() == Qgis::PlotAxisType::ValueType ) { - bool isMinor = true; - if ( qgsDoubleNear( currentY, nextMajorYGrid, yTolerance ) ) + 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 ) { - isMinor = false; - nextMajorYGrid += mYAxis.gridIntervalMajor(); + 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 ); } - - 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 - plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "x" ), true ) ); - objectNumber = 0; - if ( mXAxis.labelInterval() > 0 ) + if ( mXAxis.type() == Qgis::PlotAxisType::ValueType ) { - for ( double currentX = firstXLabel; ; currentX += mXAxis.labelInterval(), ++objectNumber ) + plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "x" ), true ) ); + int objectNumber = 0; + if ( mXAxis.labelInterval() > 0 ) { - 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() ) + for ( double currentX = firstXLabel; ; currentX += mXAxis.labelInterval(), ++objectNumber ) { - case Qgis::PlotAxisSuffixPlacement::NoLabels: - break; + 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 ) + case Qgis::PlotAxisSuffixPlacement::EveryLabel: text += xAxisSuffix; - break; + break; - case Qgis::PlotAxisSuffixPlacement::LastLabel: - if ( !hasMoreLabels ) - text += xAxisSuffix; - break; + case Qgis::PlotAxisSuffixPlacement::FirstLabel: + if ( objectNumber == 0 ) + text += xAxisSuffix; + break; - case Qgis::PlotAxisSuffixPlacement::FirstAndLastLabels: - if ( objectNumber == 0 || !hasMoreLabels ) - text += xAxisSuffix; + 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; } - - QgsTextRenderer::drawText( QPointF( ( currentX - mMinX ) * xScale + chartAreaLeft, mSize.height() - context.convertToPainterUnits( mMargins.bottom(), Qgis::RenderUnit::Millimeters ) ), + } + } + 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() ); - if ( !hasMoreLabels ) - break; } } // y - plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "y" ), true ) ); - objectNumber = 0; - if ( mYAxis.labelInterval() > 0 ) + if ( mYAxis.type() == Qgis::PlotAxisType::ValueType ) { - for ( double currentY = firstYLabel; ; currentY += mYAxis.labelInterval(), ++objectNumber ) + plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "y" ), true ) ); + int objectNumber = 0; + if ( mYAxis.labelInterval() > 0 ) { - 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() ) + for ( double currentY = firstYLabel; ; currentY += mYAxis.labelInterval(), ++objectNumber ) { - case Qgis::PlotAxisSuffixPlacement::NoLabels: - break; + 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 ) + case Qgis::PlotAxisSuffixPlacement::EveryLabel: text += yAxisSuffix; - break; + break; - case Qgis::PlotAxisSuffixPlacement::LastLabel: - if ( !hasMoreLabels ) - text += yAxisSuffix; - break; + case Qgis::PlotAxisSuffixPlacement::FirstLabel: + if ( objectNumber == 0 ) + text += yAxisSuffix; + break; - case Qgis::PlotAxisSuffixPlacement::FirstAndLastLabels: - if ( objectNumber == 0 || !hasMoreLabels ) - text += yAxisSuffix; + 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 - mMinY ) * yScale + height / 2 ), + chartAreaBottom - currentY + height / 2 ), 0, Qgis::TextHorizontalAlignment::Right, { text }, context, mYAxis.textFormat(), false ); - if ( !hasMoreLabels ) - break; } } @@ -906,19 +963,33 @@ void QgsAbstractPlotSeries::setName( const QString &name ) QgsSymbol *QgsAbstractPlotSeries::symbol() const { - return mSymbol; + return mSymbol.get(); } void QgsAbstractPlotSeries::setSymbol( QgsSymbol *symbol ) { - delete mSymbol; - mSymbol = 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; diff --git a/src/core/plot/qgsplot.h b/src/core/plot/qgsplot.h index 65b9c5b23ff..ce8dd4d8659 100644 --- a/src/core/plot/qgsplot.h +++ b/src/core/plot/qgsplot.h @@ -68,17 +68,21 @@ class CORE_EXPORT QgsAbstractPlotSeries public: QgsAbstractPlotSeries() = default; - ~QgsAbstractPlotSeries() = default; + virtual ~QgsAbstractPlotSeries() = default; QString name() const; void setName( const QString &name ); QgsSymbol *symbol() const; - void setSymbol( QgsSymbol *symbol ); + void setSymbol( QgsSymbol *symbol SIP_TRANSFER ); + virtual QList categories() const; private: +#ifdef SIP_RUN + QgsAbstractPlotSeries( const QgsAbstractPlotSeries &other ); +#endif QString mName; - QgsSymbol *mSymbol = nullptr; + std::unique_ptr mSymbol; }; class CORE_EXPORT QgsXyPlotSeries : public QgsAbstractPlotSeries @@ -88,11 +92,16 @@ class CORE_EXPORT QgsXyPlotSeries : public QgsAbstractPlotSeries QgsXyPlotSeries() = default; ~QgsXyPlotSeries() = default; + QList categories() const override; + QList> data() const SIP_SKIP; void append( const QVariant &x, const double &y ); void clear(); private: +#ifdef SIP_RUN + QgsXyPlotSeries( const QgsXyPlotSeries &other ); +#endif QList> mData; }; @@ -105,7 +114,7 @@ class CORE_EXPORT QgsPlotData ~QgsPlotData(); QList series() const; - void addSeries( QgsAbstractPlotSeries *series ); + void addSeries( QgsAbstractPlotSeries *series SIP_TRANSFER ); void clearSeries(); private: