#14891: Add option to use a common legend across summary sub plots

This commit is contained in:
Magne Sjaastad committed 2026-10-22 13:39:05 +02:00
1 parent 6a15c84463
commit 7ed51e1fd5
11 files changed
+207 -5

No files matched your search

@@ -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();
}
@@ -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<ColumnCountEnum> m_columnCount;
caf::PdmField<RowCountEnum> m_rowsPerPage;
caf::PdmField<bool> m_showIndividualPlotTitles;
caf::PdmField<bool> m_useCommonLegend;
RimFontSizeField m_subTitleFontSize;
RimFontSizeField m_axisTitleFontSize;
RimFontSizeField m_axisValueFontSize;
@@ -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 );
@@ -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( {} ) );
}
@@ -641,6 +641,7 @@ const QList<QPointer<RiuMultiPlotPage>>& RiuMultiPlotBook::pages() const
RiuMultiPlotPage* RiuMultiPlotBook::createPage()
{
RiuMultiPlotPage* page = new RiuMultiPlotPage( m_plotDefinition, this );
page->setHostsCommonLegend( m_pages.isEmpty() );
applyPageSettings( page );
@@ -68,6 +68,15 @@
#include <cmath>
#include <optional>
namespace
{
bool useCommonLegend( const RimPlotWindow* plotWindow )
{
auto multiPlot = dynamic_cast<const RimMultiPlot*>( 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<QPointer<RiuQwtPlotLegend>> 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<QwtPlot*> qwtPlots;
for ( auto plotWidget : visiblePlotWidgets() )
{
if ( plotWidget->plotDefinition() && !plotWidget->plotDefinition()->legendsVisible() ) continue;
if ( auto qwtPlotWidget = dynamic_cast<RiuQwtPlotWidget*>( 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 );
@@ -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<RiuQwtPlotLegend> m_commonLegend;
int m_commonLegendItemCount = 0;
std::map<RiuQwtPlotLegend*, int> m_childCountForAdjustSizeOperation;
@@ -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 <QCryptographicHash>
#include <QImage>
#include <QSet>
#include <vector>
@@ -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<QList<QwtLegendData>> RiuQwtPlotTools::uniqueLegendData( const std::vector<QwtPlot*>& plots )
{
QList<QList<QwtLegendData>> uniqueData;
QSet<QString> keys;
for ( QwtPlot* plot : plots )
{
if ( !plot ) continue;
for ( QwtPlotItem* item : plot->itemList() )
{
if ( !item->testItemAttribute( QwtPlotItem::Legend ) ) continue;
QList<QwtLegendData> legendData = item->legendData();
if ( legendData.isEmpty() ) continue;
QString title = legendData.first().value( QwtLegendData::TitleRole ).value<QwtText>().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<QwtGraphic>();
if ( !icon.isNull() )
{
QImage image = icon.toImage( QSize( 32, 16 ).expandedTo( icon.defaultSize().toSize() ) );
QByteArray bytes( reinterpret_cast<const char*>( 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;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -65,9 +65,10 @@ public:
QColor color,
Qt::BrushStyle brushStyle = Qt::SolidPattern );
static void updateLegendData( RiuQwtPlotLegend* legend, const std::vector<RimPlotCurve*>& curves );
static QList<QwtLegendData> createLegendData( const std::vector<RimPlotCurve*>& curves );
static QwtAxis::Position toQwtPlotAxisEnum( RiaDefines::PlotAxis riaPlotAxis );
static void updateLegendData( RiuQwtPlotLegend* legend, const std::vector<RimPlotCurve*>& curves );
static QList<QwtLegendData> createLegendData( const std::vector<RimPlotCurve*>& curves );
static QList<QList<QwtLegendData>> uniqueLegendData( const std::vector<QwtPlot*>& plots );
static QwtAxis::Position toQwtPlotAxisEnum( RiaDefines::PlotAxis riaPlotAxis );
static RiaDefines::PlotAxis fromQwtPlotAxis( QwtAxis::Position );
@@ -96,6 +96,7 @@ RiuSummaryMultiPlotPage* RiuSummaryMultiPlotBook::createSummaryPage()
{
RimSummaryMultiPlot* sumMultPlot = dynamic_cast<RimSummaryMultiPlot*>( m_plotDefinition.p() );
RiuSummaryMultiPlotPage* page = new RiuSummaryMultiPlotPage( sumMultPlot, this );
page->setHostsCommonLegend( m_pages.isEmpty() );
applyPageSettings( page );
@@ -114,6 +114,8 @@ void RiuSummaryMultiPlotPage::reinsertPlotWidgets()
column += colSpan - 1;
}
}
updateCommonLegend();
}
//--------------------------------------------------------------------------------------------------