///////////////////////////////////////////////////////////////////////////////// // // Copyright (C) 2020 Equinor ASA // // ResInsight 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 3 of the License, or // (at your option) any later version. // // ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY // WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. // // See the GNU General Public License at // for more details. // ///////////////////////////////////////////////////////////////////////////////// #include "RiuGroupedBarChartBuilder.h" #include "RiaColorTables.h" #include "qwt_column_symbol.h" #include "qwt_legend.h" #include "qwt_painter.h" #include "qwt_plot.h" #include "qwt_plot_barchart.h" #include "qwt_plot_grid.h" #include "qwt_plot_scaleitem.h" #include "qwt_scale_draw.h" #include "qwt_scale_widget.h" #include #include //-------------------------------------------------------------------------------------------------- /// Overriding to avoid one-pixel overlap of bars. //-------------------------------------------------------------------------------------------------- class RiuAvoidPixelOverlapColumnSymbol : public QwtColumnSymbol { public: RiuAvoidPixelOverlapColumnSymbol( Style style ) : QwtColumnSymbol( style ) { } void draw( QPainter* painter, const QwtColumnRect& rect ) const override { painter->save(); switch ( this->style() ) { case QwtColumnSymbol::Box: { switch ( this->frameStyle() ) { case QwtColumnSymbol::NoFrame: { QRectF r = rect.toRect(); if ( QwtPainter::roundingAlignment( painter ) ) { r.setLeft( qRound( r.left() ) ); r.setRight( qRound( r.right() ) ); r.setTop( qRound( r.top() ) ); r.setBottom( qRound( r.bottom() ) ); } painter->fillRect( r, this->palette().window() ); // This line here is the difference. Qwt adds a 1 // to width and height. } break; default: QwtColumnSymbol::drawBox( painter, rect ); } break; } default:; } painter->restore(); } }; //-------------------------------------------------------------------------------------------------- /// Overridden ScaleDraw to add labels for med and min ticks, and to add newlines to get the /// tick texts on different height according to tick level //-------------------------------------------------------------------------------------------------- class RiuBarChartScaleDraw : public QwtScaleDraw { public: RiuBarChartScaleDraw( const std::map>& posTickTypeAndTexts ) : m_posTickTypeAndTexts( posTickTypeAndTexts ) { this->setTickLength( QwtScaleDiv::MajorTick, 0 ); this->setTickLength( QwtScaleDiv::MediumTick, 0 ); this->setTickLength( QwtScaleDiv::MinorTick, 0 ); bool hasMinorTickText = false; bool hasMediumTickText = false; int minTickMaxTextSize = 0; int medTickMaxTextSize = 0; for ( const auto& posTickTypeText : m_posTickTypeAndTexts ) { if ( posTickTypeText.second.first == QwtScaleDiv::MediumTick ) { hasMediumTickText = true; medTickMaxTextSize = std::max( posTickTypeText.second.second.size(), medTickMaxTextSize ); } if ( posTickTypeText.second.first == QwtScaleDiv::MinorTick ) { hasMinorTickText = true; minTickMaxTextSize = std::max( posTickTypeText.second.second.size(), minTickMaxTextSize ); } } m_medLineBreak = hasMinorTickText ? "\n" : ""; m_majLineBreak = m_medLineBreak + ( hasMediumTickText ? QString( "\n" ) : QString( "" ) ); m_medSpacing.fill( ' ', 2 * minTickMaxTextSize ); m_majSpacing = m_medSpacing + QString().fill( ' ', 2 * medTickMaxTextSize ); } /// Override to add new lines to the labels according to the tick level QwtText label( double v ) const override { auto posTypeTextPairIt = m_posTickTypeAndTexts.find( v ); if ( posTypeTextPairIt != m_posTickTypeAndTexts.end() ) { if ( this->alignment() == BottomScale ) { if ( posTypeTextPairIt->second.first == QwtScaleDiv::MediumTick ) { return m_medLineBreak + posTypeTextPairIt->second.second; } else if ( posTypeTextPairIt->second.first == QwtScaleDiv::MajorTick ) { return m_majLineBreak + posTypeTextPairIt->second.second; } else { return posTypeTextPairIt->second.second; } } else if ( this->alignment() == LeftScale ) { if ( posTypeTextPairIt->second.first == QwtScaleDiv::MediumTick ) { return posTypeTextPairIt->second.second + m_medSpacing; } else if ( posTypeTextPairIt->second.first == QwtScaleDiv::MajorTick ) { return posTypeTextPairIt->second.second + m_majSpacing; } else { return posTypeTextPairIt->second.second; } } else { return posTypeTextPairIt->second.second; } } else { return QwtText( QString( "X" ) ); // Just for debugging } } // Override to draw text labels at medium and minor ticks also void draw( QPainter* painter, const QPalette& palette ) const override { QwtScaleDraw::draw( painter, palette ); if ( hasComponent( QwtAbstractScaleDraw::Labels ) ) { painter->save(); painter->setPen( palette.color( QPalette::Text ) ); const QList& mediumTicks = scaleDiv().ticks( QwtScaleDiv::MediumTick ); for ( int i = 0; i < mediumTicks.count(); i++ ) { const double v = mediumTicks[i]; if ( scaleDiv().contains( v ) ) drawLabel( painter, mediumTicks[i] ); } const QList& minorTicks = scaleDiv().ticks( QwtScaleDiv::MinorTick ); for ( int i = 0; i < minorTicks.count(); i++ ) { const double v = minorTicks[i]; if ( scaleDiv().contains( v ) ) drawLabel( painter, minorTicks[i] ); } painter->restore(); } } protected: virtual void drawBackbone( QPainter* ) const override {} private: std::map> m_posTickTypeAndTexts; QString m_medLineBreak; QString m_majLineBreak; QString m_medSpacing; QString m_majSpacing; }; //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RiuGroupedBarChartBuilder::RiuGroupedBarChartBuilder( bool sortGroupsByMaxValueInGroup ) : m_isSortingByMaxValueInGroups( sortGroupsByMaxValueInGroup ) { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RiuGroupedBarChartBuilder::addBarEntry( const QString& majorTickText, const QString& midTickText, const QString& minTickText, const double sortValue, const QString& legendText, const QString& barText, const double value ) { m_sortedBarEntries.insert( BarEntry( majorTickText, midTickText, minTickText, sortValue, legendText, barText, value ) ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RiuGroupedBarChartBuilder::setLegendColorMap( const std::map& legendColors ) { m_legendColors = legendColors; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RiuGroupedBarChartBuilder::addBarChartToPlot( QwtPlot* plot, Qt::Orientation barOrientation, int maxBarCount ) { const double majGroupSpacing = 1.6; const double midGroupSpacing = 0.5; const double minGroupSpacing = 0.2; std::set majTickTexts; std::set midTickTexts; std::set minTickTexts; double currentBarPosition = 1.0; double currentMajGroupStartPos = 1.0; double currentMidGroupStartPos = 1.0; double currentMinGroupStartPos = 1.0; QString previousMajText; QString previousMidText; QString previousMinText; std::map> legendToBarPointsMap; std::map> groupPositionedAxisTexts; std::map> positionedBarLabels; QList majTickPositions; QList midTickPositions; QList minTickPositions; QList majDividerPositions; QList midDividerPositions; QList minDividerPositions; // Filter the entries according to value std::multiset filteredBarEntries; if ( maxBarCount >= 0 ) { std::map valueFilteredBarEntries; int mapSize = 0; for ( const BarEntry& barDef : m_sortedBarEntries ) { if ( mapSize < maxBarCount ) { if ( valueFilteredBarEntries.insert( std::make_pair( fabs( barDef.m_value ), barDef ) ).second ) { mapSize++; } } else if ( fabs( barDef.m_value ) > valueFilteredBarEntries.begin()->first ) { if ( valueFilteredBarEntries.insert( std::make_pair( fabs( barDef.m_value ), barDef ) ).second ) { valueFilteredBarEntries.erase( valueFilteredBarEntries.begin() ); } } } for ( auto valEntryPair : valueFilteredBarEntries ) { filteredBarEntries.insert( valEntryPair.second ); } } else { // No filtering filteredBarEntries = m_sortedBarEntries; } // Establish the max value within each group std::multiset filteredSortedBarEntries; if ( m_isSortingByMaxValueInGroups ) { std::map> maxValuesPerMidGroup; std::map maxValuesPerMajGroup; for ( const BarEntry& barDef : filteredBarEntries ) { if ( !barDef.m_majTickText.isEmpty() ) { auto it_IsInsertedPair = maxValuesPerMajGroup.insert( std::make_pair( barDef.m_majTickText, barDef.m_sortValue ) ); if ( !it_IsInsertedPair.second ) it_IsInsertedPair.first->second = std::max( it_IsInsertedPair.first->second, barDef.m_sortValue ); } if ( !barDef.m_midTickText.isEmpty() ) { auto it_IsInsertedPair = maxValuesPerMidGroup[barDef.m_majTickText].insert( std::make_pair( barDef.m_midTickText, barDef.m_sortValue ) ); if ( !it_IsInsertedPair.second ) it_IsInsertedPair.first->second = std::max( it_IsInsertedPair.first->second, barDef.m_sortValue ); } } for ( BarEntry barDef : filteredBarEntries ) { { auto it = maxValuesPerMajGroup.find( barDef.m_majTickText ); if ( it != maxValuesPerMajGroup.end() ) { barDef.m_majorSortValue = it->second; } } { auto mapIt = maxValuesPerMidGroup.find( barDef.m_majTickText ); if ( mapIt != maxValuesPerMidGroup.end() ) { auto it = mapIt->second.find( barDef.m_midTickText ); if ( it != mapIt->second.end() ) { barDef.m_midSortValue = it->second; } } } filteredSortedBarEntries.insert( barDef ); } } else { // No sorting by max group member filteredSortedBarEntries = filteredBarEntries; } // clang-format off auto addGroupTickText = [&]( double groupStartPos, QString tickText, QList& groupTickPosList ) { if( tickText.isEmpty() ) return; double tickPos = midPoint( groupStartPos, currentBarPosition ); QwtScaleDiv::TickType ttyp = (&groupTickPosList == &majTickPositions ) ? QwtScaleDiv::MajorTick : ( &groupTickPosList == &midTickPositions ) ? QwtScaleDiv::MediumTick : QwtScaleDiv::MinorTick; // Make sure we do not get ticks of different level exactly at the same spot, // so that the drawing is able to distinguish if( ttyp == QwtScaleDiv::MinorTick ) tickPos += 2e-4; if( ttyp == QwtScaleDiv::MediumTick ) tickPos += 1e-4; groupPositionedAxisTexts[tickPos] = { ttyp, tickText }; groupTickPosList.push_back( tickPos ); }; auto addGroupDivider = [&]( double position, QList& groupDividerPosList) { QwtScaleDiv::TickType ttyp = (&groupDividerPosList == &majDividerPositions) ? QwtScaleDiv::MajorTick : (&groupDividerPosList == &midDividerPositions) ? QwtScaleDiv::MediumTick : QwtScaleDiv::MinorTick; // Make sure we do not get ticks of different level exactly at the same spot, // so that the drawing is able to distinguish double spacing = majGroupSpacing; if( ttyp == QwtScaleDiv::MediumTick ){ spacing = midGroupSpacing; } if( ttyp == QwtScaleDiv::MinorTick ) { spacing = minGroupSpacing; } groupDividerPosList.push_back(currentBarPosition - 0.5 - 0.5*spacing); }; // clang-format on // Loop over entries, calculate tick positions and bar positions as we go for ( const BarEntry& barDef : filteredSortedBarEntries ) { bool hasAnyMajTics = !majTickTexts.empty(); auto majInsertResult = majTickTexts.insert( barDef.m_majTickText ); bool isStartingNewMajGroup = majInsertResult.second; bool isFinishingMajGroup = isStartingNewMajGroup && hasAnyMajTics; if ( isFinishingMajGroup ) { addGroupTickText( currentMajGroupStartPos, previousMajText, majTickPositions ); addGroupTickText( currentMidGroupStartPos, previousMidText, midTickPositions ); addGroupTickText( currentMinGroupStartPos, previousMinText, minTickPositions ); currentBarPosition += majGroupSpacing; addGroupDivider( currentBarPosition, majDividerPositions ); } if ( isStartingNewMajGroup ) { previousMajText = barDef.m_majTickText; previousMidText = ""; previousMinText = ""; midTickTexts.clear(); minTickTexts.clear(); currentMajGroupStartPos = currentBarPosition; currentMidGroupStartPos = currentBarPosition; currentMinGroupStartPos = currentBarPosition; } bool hasAnyMidTics = !midTickTexts.empty(); auto midInsertResult = midTickTexts.insert( barDef.m_midTickText ); bool isStartingNewMidGroup = midInsertResult.second; bool isFinishingMidGroup = isStartingNewMidGroup && hasAnyMidTics; if ( isFinishingMidGroup ) { addGroupTickText( currentMidGroupStartPos, previousMidText, midTickPositions ); addGroupTickText( currentMinGroupStartPos, previousMinText, minTickPositions ); currentBarPosition += midGroupSpacing; addGroupDivider( currentBarPosition, midDividerPositions ); } if ( isStartingNewMidGroup ) { previousMidText = barDef.m_midTickText; previousMinText = ""; minTickTexts.clear(); currentMidGroupStartPos = currentBarPosition; currentMinGroupStartPos = currentBarPosition; } bool hasAnyMinTics = !minTickTexts.empty(); auto minInsertResult = minTickTexts.insert( barDef.m_minTickText ); bool isStartingNewMinGroup = minInsertResult.second; bool isFinishingMinGroup = minInsertResult.second && hasAnyMinTics; if ( isFinishingMinGroup ) { addGroupTickText( currentMinGroupStartPos, previousMinText, minTickPositions ); currentBarPosition += minGroupSpacing; } if ( isStartingNewMinGroup ) { previousMinText = barDef.m_minTickText; currentMinGroupStartPos = currentBarPosition; } // Insert bar value in correct set of colored bars auto legendToBarPointsPair = legendToBarPointsMap.find( barDef.m_legendText ); QVector* barPoints = nullptr; if ( legendToBarPointsPair == legendToBarPointsMap.end() ) { barPoints = &( legendToBarPointsMap[barDef.m_legendText] ); } else { barPoints = &( legendToBarPointsPair->second ); } barPoints->push_back( { currentBarPosition, barDef.m_value } ); if ( !barDef.m_barText.isEmpty() ) { positionedBarLabels[currentBarPosition] = { QwtScaleDiv::MinorTick, barDef.m_barText }; } // Increment the bar position for the next bar currentBarPosition += 1.0; } // Add group tick texts for the last groups { if ( !previousMajText.isEmpty() ) { addGroupTickText( currentMajGroupStartPos, previousMajText, majTickPositions ); } if ( !previousMidText.isEmpty() ) { addGroupTickText( currentMidGroupStartPos, previousMidText, midTickPositions ); } if ( !previousMinText.isEmpty() ) { addGroupTickText( currentMinGroupStartPos, previousMinText, minTickPositions ); } } // Create QwtBarCharts for each of the legend groups int idx = 0; for ( const auto& legendToBarPointsPair : legendToBarPointsMap ) { QColor legendColor = RiaColorTables::categoryPaletteColors().cycledQColor( idx ); auto legendColorPairIt = m_legendColors.find( legendToBarPointsPair.first ); if ( legendColorPairIt != m_legendColors.end() ) { legendColor = legendColorPairIt->second; } addQwtBarChart( plot, legendToBarPointsPair.second, legendToBarPointsPair.first, legendColor, barOrientation ); idx++; } // Set up the axis to contain group texts and tick marks { QwtPlot::Axis axis = QwtPlot::xBottom; QwtPlot::Axis valueAxis = QwtPlot::yLeft; if ( barOrientation == Qt::Horizontal ) { axis = QwtPlot::yLeft; valueAxis = QwtPlot::xBottom; } QwtScaleDiv groupAxisScaleDiv( 0, currentBarPosition ); { if ( majTickPositions.size() ) groupAxisScaleDiv.setTicks( QwtScaleDiv::MajorTick, majTickPositions ); if ( midTickPositions.size() ) groupAxisScaleDiv.setTicks( QwtScaleDiv::MediumTick, midTickPositions ); if ( minTickPositions.size() ) groupAxisScaleDiv.setTicks( QwtScaleDiv::MinorTick, minTickPositions ); if ( barOrientation == Qt::Horizontal ) { groupAxisScaleDiv.invert(); } } RiuBarChartScaleDraw* scaleDrawer = new RiuBarChartScaleDraw( groupPositionedAxisTexts ); plot->setAxisScaleDraw( axis, scaleDrawer ); plot->setAxisScaleDiv( axis, groupAxisScaleDiv ); // Set up the value axis plot->setAxisAutoScale( valueAxis, true ); plot->setAxisScaleDraw( valueAxis, new QwtScaleDraw() ); } // Setup grids { QwtPlotGrid* plotGrid = nullptr; QwtPlotGrid* groupGrid = nullptr; // Get or create the two grids used in the plot { QwtPlotItemList gridList = plot->itemList( QwtPlotItem::Rtti_PlotGrid ); for ( QwtPlotItem* plItem : gridList ) { QwtPlotGrid* someGrid = dynamic_cast( plItem ); if ( someGrid ) { if ( someGrid->title() == QString( "GroupGrid" ) ) { groupGrid = someGrid; } else { plotGrid = someGrid; } } } if ( !groupGrid ) { groupGrid = new QwtPlotGrid; groupGrid->setTitle( QString( "GroupGrid" ) ); groupGrid->attach( plot ); QPen gridPen( Qt::SolidLine ); gridPen.setColor( Qt::lightGray ); // QColor( 240, 240, 240 ) ); groupGrid->setPen( gridPen ); groupGrid->enableYMin( true ); groupGrid->enableXMin( true ); } } if ( groupGrid ) { QwtScaleDiv gridDividerScaleDiv( 0, currentBarPosition ); if ( majDividerPositions.size() ) { gridDividerScaleDiv.setTicks( QwtScaleDiv::MajorTick, majDividerPositions ); } if ( midDividerPositions.size() ) { gridDividerScaleDiv.setTicks( QwtScaleDiv::MediumTick, midDividerPositions ); } if ( minDividerPositions.size() ) { gridDividerScaleDiv.setTicks( QwtScaleDiv::MinorTick, minDividerPositions ); } if ( barOrientation == Qt::Horizontal ) { gridDividerScaleDiv.invert(); groupGrid->setYDiv( gridDividerScaleDiv ); groupGrid->enableX( false ); groupGrid->enableY( true ); } else { groupGrid->setXDiv( gridDividerScaleDiv ); groupGrid->enableX( true ); groupGrid->enableY( false ); } groupGrid->setItemInterest( QwtPlotItem::ScaleInterest, false ); } if ( plotGrid ) { if ( barOrientation == Qt::Horizontal ) { plotGrid->enableX( true ); plotGrid->enableY( false ); } else { plotGrid->enableX( false ); plotGrid->enableY( true ); } } } // Add texts on the bars inside the plot { QwtScaleDraw::Alignment alignment = QwtScaleDraw::TopScale; double labelRotation = -90.0; Qt::Alignment labelAlignment = Qt::AlignVCenter | Qt::AlignRight; if ( barOrientation == Qt::Horizontal ) { alignment = QwtScaleDraw::RightScale; labelRotation = 0.0; } QwtScaleDiv barTextScaleDiv( 0, currentBarPosition ); { QList onBarTickPositions; for ( const auto& doubleStuffPair : positionedBarLabels ) { onBarTickPositions.push_back( doubleStuffPair.first ); } barTextScaleDiv.setTicks( QwtScaleDiv::MinorTick, onBarTickPositions ); if ( barOrientation == Qt::Horizontal ) { barTextScaleDiv.invert(); } } RiuBarChartScaleDraw* barTextScaleDrawer = new RiuBarChartScaleDraw( positionedBarLabels ); barTextScaleDrawer->setAlignment( alignment ); barTextScaleDrawer->setLabelRotation( labelRotation ); barTextScaleDrawer->setLabelAlignment( labelAlignment ); QwtPlotScaleItem* barTextScale = new QwtPlotScaleItem( alignment, 0.0 ); barTextScale->setScaleDraw( barTextScaleDrawer ); barTextScale->setScaleDiv( barTextScaleDiv ); barTextScale->attach( plot ); barTextScale->setZ( 1000 ); } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RiuGroupedBarChartBuilder::addQwtBarChart( QwtPlot* plot, const QVector& posAndValue, const QString& legendText, const QColor& barColor, Qt::Orientation orientation ) { QPalette palette; palette.setColor( QPalette::Window, barColor ); palette.setColor( QPalette::Dark, barColor ); RiuAvoidPixelOverlapColumnSymbol* barStyle = new RiuAvoidPixelOverlapColumnSymbol( QwtColumnSymbol::Box ); barStyle->setPalette( palette ); barStyle->setFrameStyle( QwtColumnSymbol::NoFrame ); barStyle->setLineWidth( 0 ); QwtPlotBarChart* barChart = new QwtPlotBarChart( legendText ); barChart->setSamples( posAndValue ); barChart->setLegendMode( QwtPlotBarChart::LegendChartTitle ); barChart->setLayoutPolicy( QwtPlotAbstractBarChart::ScaleSamplesToAxes ); barChart->setLayoutHint( 1.0 ); barChart->setSymbol( barStyle ); barChart->setOrientation( orientation ); barChart->attach( plot ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RiuGroupedBarChartBuilder::BarEntry::BarEntry() : m_sortValue( std::numeric_limits::infinity() ) , m_value( std::numeric_limits::infinity() ) { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RiuGroupedBarChartBuilder::BarEntry::BarEntry( const QString& majorTickText, const QString& midTickText, const QString& minTickText, const double sortValue, const QString& legendText, const QString& barText, const double value ) : m_majTickText( majorTickText ) , m_midTickText( midTickText ) , m_minTickText( minTickText ) , m_sortValue( sortValue ) , m_legendText( legendText ) , m_barText( barText ) , m_value( value ) { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RiuGroupedBarChartBuilder::BarEntry::operator<( const BarEntry& other ) const { if ( m_majorSortValue != other.m_majorSortValue ) return m_majorSortValue > other.m_majorSortValue; if ( m_majTickText != other.m_majTickText ) return m_majTickText < other.m_majTickText; if ( m_midSortValue != other.m_midSortValue ) return m_midSortValue > other.m_midSortValue; if ( m_midTickText != other.m_midTickText ) return m_midTickText < other.m_midTickText; if ( m_minTickText != other.m_minTickText ) return m_minTickText < other.m_minTickText; if ( m_sortValue != other.m_sortValue ) { return m_sortValue > other.m_sortValue; } if ( m_legendText != other.m_legendText ) return m_legendText < other.m_legendText; return false; }