[plots] Add QgsBarChart and QgsLineChart classes, implement category typed axis rendering

This commit is contained in:
Mathieu Pellerin 2025-06-22 12:32:37 +07:00 committed by Nyall Dawson
parent a2caa8ea75
commit a0c45d9380
13 changed files with 597 additions and 122 deletions

View File

@ -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):

View File

@ -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 *
************************************************************************/

View File

@ -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<QVariant> categories() const;
private:
QgsAbstractPlotSeries( const QgsAbstractPlotSeries &other );
};
class QgsXyPlotSeries : QgsAbstractPlotSeries
@ -72,9 +75,14 @@ class QgsXyPlotSeries : QgsAbstractPlotSeries
QgsXyPlotSeries();
~QgsXyPlotSeries();
virtual QList<QVariant> 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<QgsAbstractPlotSeries *> series() const;
void addSeries( QgsAbstractPlotSeries *series );
void addSeries( QgsAbstractPlotSeries *series /Transfer/ );
void clearSeries();
};

View File

@ -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

View File

@ -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):

View File

@ -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 *
************************************************************************/

View File

@ -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<QVariant> categories() const;
private:
QgsAbstractPlotSeries( const QgsAbstractPlotSeries &other );
};
class QgsXyPlotSeries : QgsAbstractPlotSeries
@ -72,9 +75,14 @@ class QgsXyPlotSeries : QgsAbstractPlotSeries
QgsXyPlotSeries();
~QgsXyPlotSeries();
virtual QList<QVariant> 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<QgsAbstractPlotSeries *> series() const;
void addSeries( QgsAbstractPlotSeries *series );
void addSeries( QgsAbstractPlotSeries *series /Transfer/ );
void clearSeries();
};

View File

@ -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

View File

@ -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

163
src/core/plot/qgschart.cpp Normal file
View File

@ -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<QgsAbstractPlotSeries *> seriesList = plotData.series();
if ( seriesList.isEmpty() )
{
return;
}
const QList<QVariant> 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<QgsFillSymbol *>( series->symbol() );
if ( !symbol )
{
continue;
}
symbol->startRender( context );
const double barStartAdjustement = -( barsWidth / 2 ) + barWidth * seriesIndex;
if ( const QgsXyPlotSeries *xySeries = dynamic_cast<const QgsXyPlotSeries *>( series ) )
{
const QList<std::pair<QVariant, double>> data = xySeries->data();
for ( const std::pair<QVariant, double> &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<QgsAbstractPlotSeries *> seriesList = plotData.series();
if ( seriesList.isEmpty() )
{
return;
}
const QList<QVariant> 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<QgsLineSymbol *>( series->symbol() );
if ( !symbol )
{
continue;
}
symbol->startRender( context );
if ( const QgsXyPlotSeries *xySeries = dynamic_cast<const QgsXyPlotSeries *>( series ) )
{
const QList<std::pair<QVariant, double>> data = xySeries->data();
QVector<QPointF> points;
QList<double> values;
points.fill( QPointF(), xAxis().type() == Qgis::PlotAxisType::ValueType ? data.size() : categories.size() );
int dataIndex = 0;
for ( const std::pair<QVariant, double> &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();
}

70
src/core/plot/qgschart.h Normal file
View File

@ -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

View File

@ -334,10 +334,18 @@ void Qgs2DXyPlot::render( QgsRenderContext &context, const QgsPlotData &plotData
QgsNumericFormatContext numericContext;
// categories
QList<QVariant> categories;
const QList<QgsAbstractPlotSeries *> 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>
{
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>
{
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>
{
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>
{
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<QVariant> QgsAbstractPlotSeries::categories() const
{
return QList<QVariant>();
}
//
// QgsXyPlotSeries
//
QList<QVariant> QgsXyPlotSeries::categories() const
{
QList<QVariant> categories;
for ( const std::pair<QVariant, double> &pair : std::as_const( mData ) )
{
categories << pair.first;
}
return categories;
}
QList<std::pair<QVariant, double>> QgsXyPlotSeries::data() const
{
return mData;

View File

@ -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<QVariant> categories() const;
private:
#ifdef SIP_RUN
QgsAbstractPlotSeries( const QgsAbstractPlotSeries &other );
#endif
QString mName;
QgsSymbol *mSymbol = nullptr;
std::unique_ptr<QgsSymbol> mSymbol;
};
class CORE_EXPORT QgsXyPlotSeries : public QgsAbstractPlotSeries
@ -88,11 +92,16 @@ class CORE_EXPORT QgsXyPlotSeries : public QgsAbstractPlotSeries
QgsXyPlotSeries() = default;
~QgsXyPlotSeries() = default;
QList<QVariant> categories() const override;
QList<std::pair<QVariant, double>> data() const SIP_SKIP;
void append( const QVariant &x, const double &y );
void clear();
private:
#ifdef SIP_RUN
QgsXyPlotSeries( const QgsXyPlotSeries &other );
#endif
QList<std::pair<QVariant, double>> mData;
};
@ -105,7 +114,7 @@ class CORE_EXPORT QgsPlotData
~QgsPlotData();
QList<QgsAbstractPlotSeries *> series() const;
void addSeries( QgsAbstractPlotSeries *series );
void addSeries( QgsAbstractPlotSeries *series SIP_TRANSFER );
void clearSeries();
private: