Skip to content

Commit

Permalink
Add api to set a label suffix and placement for labels in plot axis
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed May 29, 2023
1 parent 988b80e commit ddc0012
Show file tree
Hide file tree
Showing 11 changed files with 600 additions and 37 deletions.
8 changes: 8 additions & 0 deletions python/core/auto_additions/qgis.py
Expand Up @@ -2257,6 +2257,14 @@
# --
Qgis.GraduatedMethod.baseClass = Qgis
# monkey patching scoped based enum
Qgis.PlotAxisSuffixPlacement.EveryLabel.__doc__ = "Place suffix after every value label"
Qgis.PlotAxisSuffixPlacement.FirstLabel.__doc__ = "Place suffix after the first label value only"
Qgis.PlotAxisSuffixPlacement.LastLabel.__doc__ = "Place suffix after the last label value only"
Qgis.PlotAxisSuffixPlacement.FirstAndLastLabels.__doc__ = "Place suffix after the first and last label values only"
Qgis.PlotAxisSuffixPlacement.__doc__ = 'Placement options for suffixes in the labels for axis of plots.\n\n.. versionadded:: 3.32\n\n' + '* ``EveryLabel``: ' + Qgis.PlotAxisSuffixPlacement.EveryLabel.__doc__ + '\n' + '* ``FirstLabel``: ' + Qgis.PlotAxisSuffixPlacement.FirstLabel.__doc__ + '\n' + '* ``LastLabel``: ' + Qgis.PlotAxisSuffixPlacement.LastLabel.__doc__ + '\n' + '* ``FirstAndLastLabels``: ' + Qgis.PlotAxisSuffixPlacement.FirstAndLastLabels.__doc__
# --
Qgis.PlotAxisSuffixPlacement.baseClass = Qgis
# monkey patching scoped based enum
Qgis.DpiMode.All.__doc__ = "All"
Qgis.DpiMode.Off.__doc__ = "Off"
Qgis.DpiMode.QGIS.__doc__ = "QGIS"
Expand Down
44 changes: 44 additions & 0 deletions python/core/auto_generated/plot/qgsplot.sip.in
Expand Up @@ -180,6 +180,50 @@ Sets the numeric ``format`` used for the axis labels.
Ownership of ``format`` is transferred to the plot.

.. seealso:: :py:func:`numericFormat`
%End

QString labelSuffix() const;
%Docstring
Returns the axis label suffix, or an empty string if no label suffix is to be used.

.. seealso:: :py:func:`setLabelSuffix`

.. seealso:: :py:func:`labelSuffixPlacement`

.. versionadded:: 3.32
%End

void setLabelSuffix( const QString &suffix );
%Docstring
Sets the axis label ``suffix``. Set to an empty string if no label suffix is to be used.

.. seealso:: :py:func:`labelSuffix`

.. seealso:: :py:func:`setLabelSuffixPlacement`

.. versionadded:: 3.32
%End

Qgis::PlotAxisSuffixPlacement labelSuffixPlacement() const;
%Docstring
Returns the placement for the axis label suffixes.

.. seealso:: :py:func:`setLabelSuffixPlacement`

.. seealso:: :py:func:`labelSuffix`

.. versionadded:: 3.32
%End

void setLabelSuffixPlacement( Qgis::PlotAxisSuffixPlacement placement );
%Docstring
Sets the ``placement`` for the axis label suffixes.

.. seealso:: :py:func:`labelSuffixPlacement`

.. seealso:: :py:func:`setLabelSuffix`

.. versionadded:: 3.32
%End

private:
Expand Down
8 changes: 8 additions & 0 deletions python/core/auto_generated/qgis.sip.in
Expand Up @@ -1320,6 +1320,14 @@ The development version
Size,
};

enum class PlotAxisSuffixPlacement
{
EveryLabel,
FirstLabel,
LastLabel,
FirstAndLastLabels,
};

enum class DpiMode
{
All,
Expand Down
192 changes: 179 additions & 13 deletions src/core/plot/qgsplot.cpp
Expand Up @@ -57,6 +57,8 @@ bool QgsPlotAxis::writeXml( QDomElement &element, QDomDocument &document, const
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 );
Expand All @@ -82,6 +84,9 @@ bool QgsPlotAxis::readXml( const QDomElement &element, const QgsReadWriteContext
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::EveryLabel );

const QDomElement numericFormatElement = element.firstChildElement( QStringLiteral( "numericFormat" ) );
mNumericFormat.reset( QgsApplication::numericFormatRegistry()->createFromXml( numericFormatElement, context ) );

Expand All @@ -106,6 +111,26 @@ 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();
Expand Down Expand Up @@ -220,6 +245,9 @@ void Qgs2DPlot::render( QgsRenderContext &context )
const double firstXLabel = std::ceil( mMinX / mXAxis.labelInterval() ) * mXAxis.labelInterval();
const double firstYLabel = std::ceil( mMinY / mYAxis.labelInterval() ) * mYAxis.labelInterval();

const QString xAxisSuffix = mXAxis.labelSuffix();
const QString yAxisSuffix = mYAxis.labelSuffix();

const QRectF plotArea = interiorPlotArea( context );

const double xTolerance = mXAxis.gridIntervalMinor() / 100000;
Expand All @@ -230,11 +258,36 @@ void Qgs2DPlot::render( QgsRenderContext &context )
// calculate text metrics
double maxYAxisLabelWidth = 0;
plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "y" ), true ) );
for ( double currentY = firstYLabel; currentY <= mMaxY && !qgsDoubleNear( currentY, mMaxY, yTolerance ); currentY += mYAxis.labelInterval() )
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 ) );
const QString text = mYAxis.numericFormat()->formatDouble( currentY, numericContext );
QString text = mYAxis.numericFormat()->formatDouble( currentY, numericContext );
switch ( mYAxis.labelSuffixPlacement() )
{
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;
}

const double chartAreaLeft = plotArea.left();
Expand Down Expand Up @@ -312,26 +365,76 @@ void Qgs2DPlot::render( QgsRenderContext &context )
// x
plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "x" ), true ) );
objectNumber = 0;
for ( double currentX = firstXLabel; objectNumber < MAX_OBJECTS && ( currentX <= mMaxX || qgsDoubleNear( currentX, mMaxX, xTolerance ) ); currentX += mXAxis.labelInterval(), ++objectNumber )
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 ) );
const QString text = mXAxis.numericFormat()->formatDouble( currentX, numericContext );
QString text = mXAxis.numericFormat()->formatDouble( currentX, numericContext );
switch ( mXAxis.labelSuffixPlacement() )
{
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;
}

// y
plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "y" ), true ) );
objectNumber = 0;
for ( double currentY = firstYLabel; objectNumber < MAX_OBJECTS && ( currentY <= mMaxY || qgsDoubleNear( currentY, mMaxY, yTolerance ) ); currentY += mYAxis.labelInterval(), ++objectNumber )
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 ) );
const QString text = mYAxis.numericFormat()->formatDouble( currentY, numericContext );
QString text = mYAxis.numericFormat()->formatDouble( currentY, numericContext );
switch ( mYAxis.labelSuffixPlacement() )
{
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;
}

// give subclasses a chance to draw their content
Expand Down Expand Up @@ -380,6 +483,10 @@ QRectF Qgs2DPlot::interiorPlotArea( QgsRenderContext &context ) const
const double firstMinorYGrid = std::ceil( mMinY / mYAxis.gridIntervalMinor() ) * mYAxis.gridIntervalMinor();
const double firstXLabel = std::ceil( mMinX / mXAxis.labelInterval() ) * mXAxis.labelInterval();

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;
Expand All @@ -391,21 +498,74 @@ QRectF Qgs2DPlot::interiorPlotArea( QgsRenderContext &context ) const
int labelNumber = 0;
double maxXAxisLabelHeight = 0;
plotScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "plot_axis" ), QStringLiteral( "x" ), true ) );
for ( double currentX = firstXLabel; labelNumber < MAX_LABELS && ( currentX <= mMaxX || qgsDoubleNear( currentX, mMaxX, xTolerance ) ); currentX += mXAxis.labelInterval(), labelNumber++ )
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 ) );
const QString text = mXAxis.numericFormat()->formatDouble( currentX, numericContext );
QString text = mXAxis.numericFormat()->formatDouble( currentX, numericContext );
switch ( mXAxis.labelSuffixPlacement() )
{
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; labelNumber < MAX_LABELS && ( currentY <= mMaxY || qgsDoubleNear( currentY, mMaxY, yTolerance ) ); currentY += mYAxis.gridIntervalMinor(), labelNumber ++ )
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 );
maxYAxisLabelWidth = std::max( maxYAxisLabelWidth, QgsTextRenderer::textWidth( context, mYAxis.textFormat(), { text } ) );
double thisLabelWidth = QgsTextRenderer::textWidth( context, mYAxis.textFormat(), { text } );
if ( yAxisSuffixWidth > 0 )
{
switch ( mYAxis.labelSuffixPlacement() )
{
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 );
Expand Down Expand Up @@ -522,11 +682,14 @@ void Qgs2DPlot::calculateOptimisedIntervals( QgsRenderContext &context )
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, [ = ]( double position ) -> double
{
const QString text = mXAxis.numericFormat()->formatDouble( position, numericContext );
return QgsTextRenderer::textWidth( context, mXAxis.textFormat(), { text } );

// 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 );
Expand All @@ -538,10 +701,13 @@ void Qgs2DPlot::calculateOptimisedIntervals( QgsRenderContext &context )
double labelIntervalY = mYAxis.labelInterval();
double majorIntervalY = mYAxis.gridIntervalMajor();
double minorIntervalY = mYAxis.gridIntervalMinor();
const QString suffixY = mYAxis.labelSuffix();
refineIntervalForAxis( mMinY, mMaxY, [ = ]( double position ) -> double
{
const QString text = mYAxis.numericFormat()->formatDouble( position, numericContext );
return QgsTextRenderer::textHeight( context, mYAxis.textFormat(), { text } );
// 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 );
Expand Down

0 comments on commit ddc0012

Please sign in to comment.