diff --git a/ApplicationLibCode/ProjectDataModel/RimMultiPlot.cpp b/ApplicationLibCode/ProjectDataModel/RimMultiPlot.cpp index 142811de7f..8fd8502ad2 100644 --- a/ApplicationLibCode/ProjectDataModel/RimMultiPlot.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimMultiPlot.cpp @@ -71,6 +71,14 @@ RimMultiPlot::RimMultiPlot() CAF_PDM_InitField( &m_showIndividualPlotTitles, "ShowPlotTitles", true, "Show Sub Plot Titles" ); CAF_PDM_InitFieldNoDefault( &m_majorTickmarkCount, "MajorTickmarkCount", "Major Tickmark Count" ); + CAF_PDM_InitField( &m_useCommonLegend, + "UseCommonLegend", + false, + "Use Common Legend", + "", + "Show one legend above the sub plots, merging identical entries. Useful when all sub plots share the same legend " + "entries." ); + CAF_PDM_InitFieldNoDefault( &m_subTitleFontSize, "SubTitleFontSize", "Sub Plot Title Font Size" ); m_subTitleFontSize = caf::FontTools::RelativeSize::Large; @@ -119,6 +127,7 @@ RimMultiPlot& RimMultiPlot::operator=( RimMultiPlot&& rhs ) m_rowsPerPage = rhs.m_rowsPerPage; m_showIndividualPlotTitles = rhs.m_showIndividualPlotTitles; m_subTitleFontSize = rhs.m_subTitleFontSize; + m_useCommonLegend = rhs.m_useCommonLegend; m_pagePreviewMode = rhs.m_pagePreviewMode; return *this; @@ -527,6 +536,14 @@ bool RimMultiPlot::showPlotTitles() const return m_showIndividualPlotTitles; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimMultiPlot::useCommonLegend() const +{ + return m_useCommonLegend(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -706,7 +723,7 @@ void RimMultiPlot::fieldChangedByUi( const caf::PdmFieldHandle* changedField, co { RimPlotWindow::fieldChangedByUi( changedField, oldValue, newValue ); - if ( changedField == &m_showIndividualPlotTitles ) + if ( changedField == &m_showIndividualPlotTitles || changedField == &m_useCommonLegend ) { updateLayout(); } diff --git a/ApplicationLibCode/ProjectDataModel/RimMultiPlot.h b/ApplicationLibCode/ProjectDataModel/RimMultiPlot.h index 61dbb463bb..2b3bbc13c8 100644 --- a/ApplicationLibCode/ProjectDataModel/RimMultiPlot.h +++ b/ApplicationLibCode/ProjectDataModel/RimMultiPlot.h @@ -100,6 +100,7 @@ public: void setShowPlotTitles( bool enable ); bool showPlotTitles() const; + bool useCommonLegend() const; void zoomAll() override; void zoomAllYAxes(); @@ -166,6 +167,7 @@ protected: caf::PdmField m_columnCount; caf::PdmField m_rowsPerPage; caf::PdmField m_showIndividualPlotTitles; + caf::PdmField m_useCommonLegend; RimFontSizeField m_subTitleFontSize; RimFontSizeField m_axisTitleFontSize; RimFontSizeField m_axisValueFontSize; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryMultiPlot.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryMultiPlot.cpp index 835e5b85d9..8b6623aa1d 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryMultiPlot.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryMultiPlot.cpp @@ -447,6 +447,7 @@ void RimSummaryMultiPlot::defineUiOrdering( QString uiConfigName, caf::PdmUiOrde legendsGroup->add( &m_showPlotLegends ); legendsGroup->add( &m_plotLegendsHorizontal ); legendsGroup->add( &m_legendPosition ); + legendsGroup->add( &m_useCommonLegend ); legendsGroup->add( &m_legendFontSize ); uiOrdering.skipRemainingFields( true ); diff --git a/ApplicationLibCode/UnitTests/RicWellPathExportCompletionDataFeatureImpl-Test.cpp b/ApplicationLibCode/UnitTests/RicWellPathExportCompletionDataFeatureImpl-Test.cpp index 3cdc0378ae..cd9e288f72 100644 --- a/ApplicationLibCode/UnitTests/RicWellPathExportCompletionDataFeatureImpl-Test.cpp +++ b/ApplicationLibCode/UnitTests/RicWellPathExportCompletionDataFeatureImpl-Test.cpp @@ -98,3 +98,11 @@ TEST( RicWellPathExportCompletionDataFeatureImpl, CompletionNumberIgnoresContrib EXPECT_EQ( 4, numberForAllOrders( { makeCompletion( 100.0, 110.0, std::nullopt ), makeCompletion( 110.0, 111.0, 4 ) } ) ); EXPECT_EQ( std::nullopt, numberForAllOrders( { makeCompletion( 100.0, 110.0, std::nullopt ) } ) ); } + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RicWellPathExportCompletionDataFeatureImpl, CompletionNumberEmptyCompletionsGivesNoNumber ) +{ + EXPECT_EQ( std::nullopt, RicWellPathExportCompletionDataFeatureImpl::completionNumberForCell( {} ) ); +} diff --git a/ApplicationLibCode/UserInterface/RiuMultiPlotBook.cpp b/ApplicationLibCode/UserInterface/RiuMultiPlotBook.cpp index ca66735d5c..a2d5a3a35c 100644 --- a/ApplicationLibCode/UserInterface/RiuMultiPlotBook.cpp +++ b/ApplicationLibCode/UserInterface/RiuMultiPlotBook.cpp @@ -641,6 +641,7 @@ const QList>& RiuMultiPlotBook::pages() const RiuMultiPlotPage* RiuMultiPlotBook::createPage() { RiuMultiPlotPage* page = new RiuMultiPlotPage( m_plotDefinition, this ); + page->setHostsCommonLegend( m_pages.isEmpty() ); applyPageSettings( page ); diff --git a/ApplicationLibCode/UserInterface/RiuMultiPlotPage.cpp b/ApplicationLibCode/UserInterface/RiuMultiPlotPage.cpp index 3fd5b22e4b..6768bf734a 100644 --- a/ApplicationLibCode/UserInterface/RiuMultiPlotPage.cpp +++ b/ApplicationLibCode/UserInterface/RiuMultiPlotPage.cpp @@ -68,6 +68,15 @@ #include #include +namespace +{ +bool useCommonLegend( const RimPlotWindow* plotWindow ) +{ + auto multiPlot = dynamic_cast( plotWindow ); + return multiPlot && multiPlot->useCommonLegend(); +} +} // namespace + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -357,6 +366,22 @@ void RiuMultiPlotPage::scheduleReplotOfAllPlots() } } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuMultiPlotPage::setHostsCommonLegend( bool hostsCommonLegend ) +{ + m_hostsCommonLegend = hostsCommonLegend; +} + +//-------------------------------------------------------------------------------------------------- +/// With a common legend, the legends of the individual sub plots are not displayed +//-------------------------------------------------------------------------------------------------- +bool RiuMultiPlotPage::showSubPlotLegends() const +{ + return !useCommonLegend( m_plotDefinition ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -404,13 +429,20 @@ void RiuMultiPlotPage::renderTo( QPainter* painter, double scalingFactor ) for ( auto legend : legendsForVisiblePlots() ) { - if ( legend ) + if ( legend && !legend->isHidden() ) { QPoint renderOffset = m_plotWidgetFrame->mapToParent( legend->frameGeometry().topLeft() ) - marginOffset; legend->render( painter, renderOffset ); } } + if ( m_commonLegend && !m_commonLegend->isHidden() ) + { + // The common legend is a direct child of the page, not of the plot widget frame + QPoint renderOffset = m_commonLegend->frameGeometry().topLeft() - marginOffset; + m_commonLegend->render( painter, renderOffset ); + } + for ( auto plotWidget : visiblePlotWidgets() ) { QRect plotWidgetGeometry = plotWidget->frameGeometry(); @@ -611,9 +643,19 @@ void RiuMultiPlotPage::onLegendUpdated() //-------------------------------------------------------------------------------------------------- void RiuMultiPlotPage::refreshLegends() { + // The common legend is rebuilt from the sub plot legend data. A full layout rebuild here could trigger new legend + // data changes in the plots, and thereby an endless update loop + if ( useCommonLegend( m_plotDefinition ) ) updateCommonLegend(); + QList> legends = legendsForVisiblePlots(); for ( const auto& l : legends ) { + if ( !l ) continue; + if ( !showSubPlotLegends() ) + { + l->hide(); + continue; + } l->setVisible( !l->isEmpty() ); updateLegendFont( l ); } @@ -668,6 +710,67 @@ void RiuMultiPlotPage::reinsertPlotWidgets() reinsertPlotWidget( plotWidget, legend, legendFrame, subTitle, row, column, rowSpan, colSpan ); } } + + updateCommonLegend(); +} + +//-------------------------------------------------------------------------------------------------- +/// Build one legend from the legend data of all sub plots. Entries with identical title and icon are merged. +//-------------------------------------------------------------------------------------------------- +void RiuMultiPlotPage::updateCommonLegend() +{ + if ( m_commonLegend ) m_layout->removeWidget( m_commonLegend ); + + bool isActive = useCommonLegend( m_plotDefinition ) && m_hostsCommonLegend && m_plotDefinition->legendsVisible(); + if ( !isActive ) + { + if ( m_commonLegend ) m_commonLegend->hide(); + return; + } + + if ( !m_commonLegend ) + { + m_commonLegend = new RiuQwtPlotLegend( this ); + m_commonLegend->horizontalScrollBar()->setVisible( false ); + m_commonLegend->verticalScrollBar()->setVisible( false ); + m_commonLegend->setDefaultItemMode( QwtLegendData::ReadOnly ); + m_commonLegend->contentsWidget()->layout()->setAlignment( Qt::AlignBottom | Qt::AlignHCenter ); + m_commonLegend->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Maximum ); + } + + for ( int i = 0; i < m_commonLegendItemCount; i++ ) + { + m_commonLegend->updateLegend( QVariant( i ), {} ); + } + m_commonLegendItemCount = 0; + + std::vector qwtPlots; + for ( auto plotWidget : visiblePlotWidgets() ) + { + if ( plotWidget->plotDefinition() && !plotWidget->plotDefinition()->legendsVisible() ) continue; + + if ( auto qwtPlotWidget = dynamic_cast( plotWidget.data() ) ) + { + qwtPlots.push_back( qwtPlotWidget->qwtPlot() ); + } + } + + for ( const auto& legendData : RiuQwtPlotTools::uniqueLegendData( qwtPlots ) ) + { + m_commonLegend->updateLegend( QVariant( m_commonLegendItemCount++ ), legendData ); + } + + if ( m_commonLegendItemCount == 0 ) + { + m_commonLegend->hide(); + return; + } + + // The common legend is placed below the title, centered above all sub plots + m_layout->insertWidget( 1, m_commonLegend, 0, Qt::AlignHCenter ); + updateLegendColumns( m_commonLegend ); + updateLegendFont( m_commonLegend ); + m_commonLegend->show(); } //-------------------------------------------------------------------------------------------------- @@ -682,6 +785,17 @@ void RiuMultiPlotPage::reinsertPlotWidget( RiuPlotWidget* plotWidget, int rowSpan, int colSpan ) { + if ( !showSubPlotLegends() ) + { + if ( legend ) legend->hide(); + if ( legendFrame ) + { + legendFrame->hide(); + plotWidget->removeOverlayFrame( legendFrame ); + } + legend = nullptr; + } + m_gridLayout->addWidget( subTitle, 3 * row, column, 1, colSpan ); addLegendWidget( plotWidget, legend, legendFrame, 3 * row + 1, column, colSpan ); diff --git a/ApplicationLibCode/UserInterface/RiuMultiPlotPage.h b/ApplicationLibCode/UserInterface/RiuMultiPlotPage.h index 37763d1ee3..0b90a02e33 100644 --- a/ApplicationLibCode/UserInterface/RiuMultiPlotPage.h +++ b/ApplicationLibCode/UserInterface/RiuMultiPlotPage.h @@ -82,6 +82,7 @@ public: void setPagePreviewModeEnabled( bool previewMode ); void setAutoAlignAxes( bool autoAlignAxes ); + void setHostsCommonLegend( bool hostsCommonLegend ); void scheduleUpdate( RiaDefines::MultiPlotPageUpdateType whatToUpdate = RiaDefines::MultiPlotPageUpdateType::ALL ); void scheduleReplotOfAllPlots(); @@ -181,6 +182,14 @@ protected: bool m_previewMode; bool m_showSubTitles; bool m_autoAlignAxes; + bool m_hostsCommonLegend = true; + + bool showSubPlotLegends() const; + void updateCommonLegend(); + + // Owned by Qt as a child widget of this page; QPointer only observes it + QPointer m_commonLegend; + int m_commonLegendItemCount = 0; std::map m_childCountForAdjustSizeOperation; diff --git a/ApplicationLibCode/UserInterface/RiuQwtPlotTools.cpp b/ApplicationLibCode/UserInterface/RiuQwtPlotTools.cpp index 929403268b..db4928bcf0 100644 --- a/ApplicationLibCode/UserInterface/RiuQwtPlotTools.cpp +++ b/ApplicationLibCode/UserInterface/RiuQwtPlotTools.cpp @@ -36,9 +36,15 @@ #include "qwt_painter.h" #include "qwt_plot.h" #include "qwt_plot_grid.h" +#include "qwt_plot_item.h" #include "qwt_plot_layout.h" #include "qwt_plot_shapeitem.h" #include "qwt_scale_widget.h" +#include "qwt_text.h" + +#include +#include +#include #include @@ -264,6 +270,46 @@ void RiuQwtPlotTools::updateLegendData( RiuQwtPlotLegend* legend, const std::vec legend->updateLegend( QVariant(), legendDataList ); } +//-------------------------------------------------------------------------------------------------- +/// Collect legend data, one entry per legend item, from the plots. Items with identical title and icon are merged. +//-------------------------------------------------------------------------------------------------- +QList> RiuQwtPlotTools::uniqueLegendData( const std::vector& plots ) +{ + QList> uniqueData; + QSet keys; + + for ( QwtPlot* plot : plots ) + { + if ( !plot ) continue; + + for ( QwtPlotItem* item : plot->itemList() ) + { + if ( !item->testItemAttribute( QwtPlotItem::Legend ) ) continue; + + QList legendData = item->legendData(); + if ( legendData.isEmpty() ) continue; + + QString title = legendData.first().value( QwtLegendData::TitleRole ).value().text(); + + // Include the icon in the key, so entries with the same title but different color or style are all kept + QString key = title; + auto icon = legendData.first().value( QwtLegendData::IconRole ).value(); + if ( !icon.isNull() ) + { + QImage image = icon.toImage( QSize( 32, 16 ).expandedTo( icon.defaultSize().toSize() ) ); + QByteArray bytes( reinterpret_cast( image.constBits() ), image.sizeInBytes() ); + key += "|" + QString::fromLatin1( QCryptographicHash::hash( bytes, QCryptographicHash::Md5 ).toHex() ); + } + if ( keys.contains( key ) ) continue; + + keys.insert( key ); + uniqueData.append( legendData ); + } + } + + return uniqueData; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/UserInterface/RiuQwtPlotTools.h b/ApplicationLibCode/UserInterface/RiuQwtPlotTools.h index a554a75851..f963427b58 100644 --- a/ApplicationLibCode/UserInterface/RiuQwtPlotTools.h +++ b/ApplicationLibCode/UserInterface/RiuQwtPlotTools.h @@ -65,9 +65,10 @@ public: QColor color, Qt::BrushStyle brushStyle = Qt::SolidPattern ); - static void updateLegendData( RiuQwtPlotLegend* legend, const std::vector& curves ); - static QList createLegendData( const std::vector& curves ); - static QwtAxis::Position toQwtPlotAxisEnum( RiaDefines::PlotAxis riaPlotAxis ); + static void updateLegendData( RiuQwtPlotLegend* legend, const std::vector& curves ); + static QList createLegendData( const std::vector& curves ); + static QList> uniqueLegendData( const std::vector& plots ); + static QwtAxis::Position toQwtPlotAxisEnum( RiaDefines::PlotAxis riaPlotAxis ); static RiaDefines::PlotAxis fromQwtPlotAxis( QwtAxis::Position ); diff --git a/ApplicationLibCode/UserInterface/RiuSummaryMultiPlotBook.cpp b/ApplicationLibCode/UserInterface/RiuSummaryMultiPlotBook.cpp index 4570b3aa0b..ecfd58dd14 100644 --- a/ApplicationLibCode/UserInterface/RiuSummaryMultiPlotBook.cpp +++ b/ApplicationLibCode/UserInterface/RiuSummaryMultiPlotBook.cpp @@ -96,6 +96,7 @@ RiuSummaryMultiPlotPage* RiuSummaryMultiPlotBook::createSummaryPage() { RimSummaryMultiPlot* sumMultPlot = dynamic_cast( m_plotDefinition.p() ); RiuSummaryMultiPlotPage* page = new RiuSummaryMultiPlotPage( sumMultPlot, this ); + page->setHostsCommonLegend( m_pages.isEmpty() ); applyPageSettings( page ); diff --git a/ApplicationLibCode/UserInterface/RiuSummaryMultiPlotPage.cpp b/ApplicationLibCode/UserInterface/RiuSummaryMultiPlotPage.cpp index 2eb4504ec8..94e2a2a304 100644 --- a/ApplicationLibCode/UserInterface/RiuSummaryMultiPlotPage.cpp +++ b/ApplicationLibCode/UserInterface/RiuSummaryMultiPlotPage.cpp @@ -114,6 +114,8 @@ void RiuSummaryMultiPlotPage::reinsertPlotWidgets() column += colSpan - 1; } } + + updateCommonLegend(); } //--------------------------------------------------------------------------------------------------