#12276 Add RFT correlation report plot with cross-plot and tornado plot

* Fix defensive guards and TVD interpolation

Guard against out-of-bounds IJK and invalid cells in RifReaderRftInterface.
Guard against null eclCase in RimDataSourceForRftPlt::address.
Guard against null plot widgets in RiuMultiPlotPage.
Fix interpolateMdFromTvd corrupting valid entries after infinity TVD values
by skipping infinity entries in the first pass and tracking only the last
two valid MD values for the startMD adjustment.

* Add RiuQwtCurveSelectorFilter for click-to-select realization in Qwt plots

New shared event filter class that installs on a QwtPlot canvas and calls a
user-supplied callback with the closest data point on each click.

Migrate the local CurveSelectorFilter in RimParameterResultCrossPlot to use
the new shared class. Add Z_HIGHLIGHTED_CURVE to ZIndex enum and replace all
raw z-order integers in RiuQwtPlotCurveDefines with named constants.

* Add RFT correlation report plot with cross-plot and tornado plot

New classes:
- RimRftCorrelationReportPlot: composite plot showing an RFT well log track
  alongside a parameter cross-plot and a tornado plot sub-plot.
- RimParameterRftCrossPlot: cross-plot of ensemble parameters vs RFT-derived
  measured depth values, with click-to-select realization support.
- RimRftTornadoPlot: tornado plot with correlation-sorted parameter list and
  click-to-select realization support.
- RicCreateRftCorrelationReportFeature: context menu command on RimWellRftPlot
  that creates a RimRftCorrelationReportPlot in the correlation plot collection.

Wire up in RimCorrelationPlotCollection and RimContextCommandBuilder.

* Add click-to-select and highlight selected realization in RFT plots

Install RiuQwtCurveSelectorFilter on RimWellRftPlot canvas to support
click-to-select realization. Highlight the selected realization using a
contrast color (orange-red) and increased line thickness. React to both
click-to-select in the plot and selection changes in the Data Sources panel.

Add Z_HIGHLIGHTED_CURVE z-order and replace raw z-order integers with
named constants in RimWellRftPlot.

Highlight selected realization with XCROSS symbol in RimParameterRftCrossPlot
via RimSummaryEnsembleTools::findSummaryCase.

* RimWellRftPlot: add Create RFT Correlation Report to canvas right-click menu

Override appendMenuItems in RimWellRftPlot to add RicCreateRftCorrelationReportFeature.
Call appendMenuItems from RiuMultiPlotPage::contextMenuEvent so any plot type
can contribute canvas menu items without modifying RiuMultiPlotPage.

* RicShowPlotDataFeature: support RimRftCorrelationReportPlot

Add RimRftCorrelationReportPlot to the isCommandEnabled check and expand
it into its cross-plot and tornado sub-plots in onActionTriggered, matching
the pattern used for RimCorrelationReportPlot.
This commit is contained in:
Magne Sjaastad
2026-04-16 16:45:44 +02:00
committed by GitHub
parent 6d8092cd94
commit 387b19ef62
29 changed files with 2842 additions and 100 deletions
@@ -3,8 +3,11 @@ set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RimCorrelationPlot.h
${CMAKE_CURRENT_LIST_DIR}/RimCorrelationMatrixPlot.h
${CMAKE_CURRENT_LIST_DIR}/RimParameterResultCrossPlot.h
${CMAKE_CURRENT_LIST_DIR}/RimParameterRftCrossPlot.h
${CMAKE_CURRENT_LIST_DIR}/RimCorrelationPlotCollection.h
${CMAKE_CURRENT_LIST_DIR}/RimCorrelationReportPlot.h
${CMAKE_CURRENT_LIST_DIR}/RimRftCorrelationReportPlot.h
${CMAKE_CURRENT_LIST_DIR}/RimRftTornadoPlot.h
)
set(SOURCE_GROUP_SOURCE_FILES
@@ -12,8 +15,11 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RimCorrelationPlot.cpp
${CMAKE_CURRENT_LIST_DIR}/RimCorrelationMatrixPlot.cpp
${CMAKE_CURRENT_LIST_DIR}/RimParameterResultCrossPlot.cpp
${CMAKE_CURRENT_LIST_DIR}/RimParameterRftCrossPlot.cpp
${CMAKE_CURRENT_LIST_DIR}/RimCorrelationPlotCollection.cpp
${CMAKE_CURRENT_LIST_DIR}/RimCorrelationReportPlot.cpp
${CMAKE_CURRENT_LIST_DIR}/RimRftCorrelationReportPlot.cpp
${CMAKE_CURRENT_LIST_DIR}/RimRftTornadoPlot.cpp
)
list(APPEND CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES})
@@ -24,9 +24,12 @@
#include "RimCorrelationPlot.h"
#include "RimCorrelationReportPlot.h"
#include "RimParameterResultCrossPlot.h"
#include "RimParameterRftCrossPlot.h"
#include "RimProject.h"
#include "RimRftCorrelationReportPlot.h"
#include "RimSummaryEnsemble.h"
#include "RimSummaryEnsembleTools.h"
#include "RimWellRftPlot.h"
CAF_PDM_SOURCE_INIT( RimCorrelationPlotCollection, "CorrelationPlotCollection" );
@@ -39,6 +42,7 @@ RimCorrelationPlotCollection::RimCorrelationPlotCollection()
CAF_PDM_InitFieldNoDefault( &m_correlationPlots, "CorrelationPlots", "Correlation Plots" );
CAF_PDM_InitFieldNoDefault( &m_correlationReports, "CorrelationReports", "Correlation Reports" );
CAF_PDM_InitFieldNoDefault( &m_rftCorrelationReports, "RftCorrelationReports", "RFT Correlation Reports" );
}
//--------------------------------------------------------------------------------------------------
@@ -174,6 +178,52 @@ RimCorrelationReportPlot* RimCorrelationPlotCollection::createCorrelationReportP
return report;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimRftCorrelationReportPlot* RimCorrelationPlotCollection::createRftCorrelationReportPlot( RimWellRftPlot* source )
{
auto* report = new RimRftCorrelationReportPlot;
report->setAsPlotMdiWindow();
if ( source )
{
report->initializeFromSourcePlot( source );
const auto ensembles = source->selectedEnsembles();
if ( !ensembles.empty() )
{
auto* ensemble = ensembles.front();
report->crossPlot()->setEnsemble( ensemble );
// Pick the first numeric ensemble parameter as a default
for ( const auto& param : RimSummaryEnsembleTools::alphabeticEnsembleParameters( ensemble->allSummaryCases() ) )
{
if ( param.isNumeric() )
{
report->crossPlot()->setEnsembleParameter( param.name );
break;
}
}
}
report->crossPlot()->setWellName( source->simWellOrWellPathName() );
const auto timeSteps = source->selectedTimeSteps();
if ( !timeSteps.empty() ) report->crossPlot()->setTimeStep( timeSteps.front() );
}
m_rftCorrelationReports.push_back( report );
return report;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RimRftCorrelationReportPlot*> RimCorrelationPlotCollection::rftReports() const
{
return m_rftCorrelationReports.childrenByType();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -222,6 +272,7 @@ void RimCorrelationPlotCollection::deleteAllPlots()
{
RimTypedPlotCollection<RimAbstractCorrelationPlot>::deleteAllPlots();
m_correlationReports.deleteChildren();
m_rftCorrelationReports.deleteChildren();
}
//--------------------------------------------------------------------------------------------------
@@ -382,6 +433,9 @@ void RimCorrelationPlotCollection::loadDataAndUpdateAllPlots()
for ( const auto& corrPlot : m_correlationPlots )
corrPlot->loadDataAndUpdate();
for ( const auto& reports : m_correlationReports )
reports->loadDataAndUpdate();
for ( const auto& report : m_correlationReports )
report->loadDataAndUpdate();
for ( const auto& rftReport : m_rftCorrelationReports )
rftReport->loadDataAndUpdate();
}
@@ -31,7 +31,9 @@ class RimCorrelationPlot;
class RimCorrelationMatrixPlot;
class RimCorrelationReportPlot;
class RimParameterResultCrossPlot;
class RimRftCorrelationReportPlot;
class RimSummaryEnsemble;
class RimWellRftPlot;
//==================================================================================================
///
@@ -66,6 +68,8 @@ public:
void removeReport( RimCorrelationReportPlot* correlationReport );
RimRftCorrelationReportPlot* createRftCorrelationReportPlot( RimWellRftPlot* source );
std::vector<RimAbstractCorrelationPlot*> plots() const final;
size_t plotCount() const final;
void insertPlot( RimAbstractCorrelationPlot* plot, size_t index ) final;
@@ -73,7 +77,8 @@ public:
void deleteAllPlots() final;
void loadDataAndUpdateAllPlots() override;
std::vector<RimCorrelationReportPlot*> reports() const;
std::vector<RimCorrelationReportPlot*> reports() const;
std::vector<RimRftCorrelationReportPlot*> rftReports() const;
private:
void applyFirstEnsembleFieldAddressesToPlot( RimAbstractCorrelationPlot* plot, const std::vector<QString>& quantityNames = {} );
@@ -91,6 +96,7 @@ private:
std::time_t timeStep );
private:
caf::PdmChildArrayField<RimAbstractCorrelationPlot*> m_correlationPlots;
caf::PdmChildArrayField<RimCorrelationReportPlot*> m_correlationReports;
caf::PdmChildArrayField<RimAbstractCorrelationPlot*> m_correlationPlots;
caf::PdmChildArrayField<RimCorrelationReportPlot*> m_correlationReports;
caf::PdmChildArrayField<RimRftCorrelationReportPlot*> m_rftCorrelationReports;
};
@@ -33,11 +33,13 @@
#include "RiuContextMenuLauncher.h"
#include "RiuDockWidgetTools.h"
#include "RiuPlotCurve.h"
#include "RiuQwtCurveSelectorFilter.h"
#include "RiuQwtPlotCurve.h"
#include "RiuQwtPlotRectAnnotation.h"
#include "RiuQwtPlotWidget.h"
#include "RiuQwtSymbol.h"
#include "cafPdmPointer.h"
#include "cafPdmUiComboBoxEditor.h"
#include "cafPdmUiTextEditor.h"
#include "cafPdmUiValueRangeEditor.h"
@@ -424,6 +426,25 @@ void RimParameterResultCrossPlot::updateValueRanges()
m_yValueRange = { summaryMin - summaryRange * 0.1, summaryMax + summaryRange * 0.1 };
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimSummaryCase* RimParameterResultCrossPlot::findClosestCase( const QPoint& canvasPos )
{
auto* qwtWidget = dynamic_cast<RiuQwtPlotWidget*>( plotWidget() );
if ( !qwtWidget ) return nullptr;
auto caseData = createCaseData();
std::vector<std::pair<double, double>> points;
points.reserve( caseData.size() );
for ( const auto& d : caseData )
points.push_back( { d.parameterValue, d.summaryValue } );
int idx = RiuQwtCurveSelectorFilter::closestPointIndex( qwtWidget->qwtPlot(), canvasPos, points );
return idx >= 0 ? caseData[idx].summaryCase : nullptr;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -599,70 +620,6 @@ protected:
return QwtText();
}
};
class CurveSelectorFilter : public QObject
{
public:
CurveSelectorFilter( QwtPlot* plot, RimParameterResultCrossPlot* crossPlot )
: QObject( plot->canvas() )
, m_plot( plot )
, m_crossPlot( crossPlot )
{
plot->canvas()->installEventFilter( this );
}
protected:
bool eventFilter( QObject*, QEvent* event ) override
{
if ( event->type() == QEvent::MouseButtonPress )
{
auto* mouseEvent = static_cast<QMouseEvent*>( event );
if ( mouseEvent->button() == Qt::LeftButton )
{
selectClosestCase( mouseEvent->pos() );
}
}
return false;
}
private:
void selectClosestCase( const QPoint& pixelPos )
{
const auto xMap = m_plot->canvasMap( QwtAxis::XBottom );
const auto yMap = m_plot->canvasMap( QwtAxis::YLeft );
const double clickX = xMap.invTransform( pixelPos.x() );
const double clickY = yMap.invTransform( pixelPos.y() );
// Normalise by axis range to get a dimensionless distance
const double xRange = std::abs( xMap.s2() - xMap.s1() );
const double yRange = std::abs( yMap.s2() - yMap.s1() );
if ( xRange == 0.0 || yRange == 0.0 ) return;
double minDist = std::numeric_limits<double>::max();
RimSummaryCase* closestCase = nullptr;
for ( const auto& [paramValue, summaryValue, summaryCase] : m_crossPlot->createCaseData() )
{
const double dx = ( paramValue - clickX ) / xRange;
const double dy = ( summaryValue - clickY ) / yRange;
const double dist = dx * dx + dy * dy;
if ( dist < minDist )
{
minDist = dist;
closestCase = summaryCase;
}
}
// Accept if within 3% of the total axis range
if ( closestCase && minDist < 0.03 * 0.03 )
{
RiuDockWidgetTools::selectItemsInTreeView( RiuDockWidgetTools::plotMainWindowDataSourceTreeName(), { closestCase } );
}
}
QwtPlot* m_plot = nullptr;
RimParameterResultCrossPlot* m_crossPlot = nullptr;
};
} // namespace internal
//--------------------------------------------------------------------------------------------------
@@ -687,7 +644,10 @@ RiuPlotWidget* RimParameterResultCrossPlot::doCreatePlotViewWidget( QWidget* mai
new internal::CurveTracker( m_plotWidget->qwtPlot() );
// Add a click filter to select the realization in the tree view when a point is clicked
new internal::CurveSelectorFilter( m_plotWidget->qwtPlot(), this );
caf::PdmPointer<RimParameterResultCrossPlot> self( this );
new RiuQwtCurveSelectorFilter( m_plotWidget->qwtPlot(),
[self]( const QPoint& pos ) -> const caf::PdmUiItem*
{ return self ? self->findClosestCase( pos ) : nullptr; } );
}
return m_plotWidget;
@@ -67,8 +67,9 @@ private:
void updatePlotTitle() override;
void createPoints();
void updateValueRanges();
void updateFilterRanges();
void updateValueRanges();
void updateFilterRanges();
RimSummaryCase* findClosestCase( const QPoint& canvasPos );
QString excludedCasesText() const;
@@ -0,0 +1,796 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimParameterRftCrossPlot.h"
#include "RiaColorTables.h"
#include "RiaPreferences.h"
#include "RifEclipseRftAddress.h"
#include "RifReaderRftInterface.h"
#include "RigEnsembleParameter.h"
#include "RigStatisticsTools.h"
#include "RiaExtractionTools.h"
#include "Well/RigEclipseWellLogExtractor.h"
#include "RimEclipseCase.h"
#include "RimEclipseResultCase.h"
#include "RimProject.h"
#include "RimSummaryCase.h"
#include "RimSummaryEnsemble.h"
#include "RimSummaryEnsembleTools.h"
#include "RimWellPath.h"
#include "RiuContextMenuLauncher.h"
#include "RiuDockWidgetTools.h"
#include "RiuPlotCurve.h"
#include "RiuQwtCurveSelectorFilter.h"
#include "RiuQwtPlotCurve.h"
#include "RiuQwtPlotWidget.h"
#include "RiuQwtSymbol.h"
#include "cafPdmPointer.h"
#include "cafPdmUiComboBoxEditor.h"
#include "qwt_picker_machine.h"
#include "qwt_plot.h"
#include "qwt_plot_curve.h"
#include "qwt_plot_marker.h"
#include "qwt_plot_picker.h"
#include "qwt_scale_map.h"
#include "qwt_text.h"
#include <QMouseEvent>
#include <QPaintDevice>
#include <limits>
#include <numeric>
CAF_PDM_SOURCE_INIT( RimParameterRftCrossPlot, "ParameterRftCrossPlot" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimParameterRftCrossPlot::RimParameterRftCrossPlot()
{
CAF_PDM_InitObject( "Parameter RFT Cross Plot", ":/CorrelationCrossPlot16x16.png" );
CAF_PDM_InitFieldNoDefault( &m_ensemble, "Ensemble", "Ensemble" );
CAF_PDM_InitField( &m_wellName, "WellName", QString(), "Well Name" );
m_wellName.uiCapability()->setUiEditorTypeName( caf::PdmUiComboBoxEditor::uiEditorTypeName() );
CAF_PDM_InitFieldNoDefault( &m_selectedTimeStep, "TimeStep", "Time Step" );
m_selectedTimeStep.uiCapability()->setUiEditorTypeName( caf::PdmUiComboBoxEditor::uiEditorTypeName() );
CAF_PDM_InitFieldNoDefault( &m_eclipseCase, "EclipseCase", "Eclipse Case (MD fallback)" );
CAF_PDM_InitField( &m_useDepthRange, "UseDepthRange", false, "Filter by Depth Range" );
CAF_PDM_InitField( &m_depthRangeMin, "DepthRangeMin", 0.0, "Min Depth (MD)" );
CAF_PDM_InitField( &m_depthRangeMax, "DepthRangeMax", 5000.0, "Max Depth (MD)" );
CAF_PDM_InitField( &m_ensembleParameter, "EnsembleParameter", QString(), "Ensemble Parameter" );
m_ensembleParameter.uiCapability()->setUiEditorTypeName( caf::PdmUiComboBoxEditor::uiEditorTypeName() );
CAF_PDM_InitField( &m_useAutoPlotTitle, "UseAutoPlotTitle", true, "Auto Title" );
CAF_PDM_InitField( &m_description, "Description", QString( "RFT Cross Plot" ), "Title" );
CAF_PDM_InitFieldNoDefault( &m_axisTitleFontSize, "AxisTitleFontSize", "Axis Title Font Size" );
CAF_PDM_InitFieldNoDefault( &m_axisValueFontSize, "AxisValueFontSize", "Axis Value Font Size" );
m_axisTitleFontSize = caf::FontTools::RelativeSize::Small;
m_axisValueFontSize = caf::FontTools::RelativeSize::Small;
m_showPlotLegends = false;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimParameterRftCrossPlot::~RimParameterRftCrossPlot()
{
cleanupBeforeClose();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::setEnsemble( RimSummaryEnsemble* ensemble )
{
m_ensemble = ensemble;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::setWellName( const QString& wellName )
{
m_wellName = wellName;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::setTimeStep( const QDateTime& timeStep )
{
m_selectedTimeStep = timeStep;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::setDepthRange( double minMd, double maxMd )
{
m_depthRangeMin = minMd;
m_depthRangeMax = maxMd;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::setEnsembleParameter( const QString& paramName )
{
m_ensembleParameter = paramName;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimParameterRftCrossPlot::ensembleParameter() const
{
return m_ensembleParameter;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimParameterRftCrossPlot::wellName() const
{
return m_wellName;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QDateTime RimParameterRftCrossPlot::selectedTimeStep() const
{
return m_selectedTimeStep;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimSummaryEnsemble* RimParameterRftCrossPlot::ensemble() const
{
return m_ensemble();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiuQwtPlotWidget* RimParameterRftCrossPlot::viewer()
{
return m_plotWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<double> RimParameterRftCrossPlot::computeMeanPressurePerCase( RimSummaryEnsemble* ensemble,
const QString& wellName,
const QDateTime& timeStep,
RimEclipseResultCase* eclipseCase,
bool useDepthRange,
double depthRangeMin,
double depthRangeMax )
{
if ( !ensemble || wellName.isEmpty() || !timeStep.isValid() ) return {};
RigEclipseWellLogExtractor* extractor = nullptr;
if ( eclipseCase )
{
RimWellPath* wellPath = RimProject::current()->wellPathFromSimWellName( wellName );
extractor = RiaExtractionTools::findOrCreateWellLogExtractor( wellPath, eclipseCase );
if ( !extractor ) extractor = RiaExtractionTools::findOrCreateSimWellExtractor( eclipseCase, wellName, false, 0 );
}
const auto& allCases = ensemble->allSummaryCases();
std::vector<double> pressurePerCase;
pressurePerCase.reserve( allCases.size() );
for ( RimSummaryCase* summaryCase : allCases )
{
if ( !summaryCase )
{
pressurePerCase.push_back( std::numeric_limits<double>::infinity() );
continue;
}
RifReaderRftInterface* reader = summaryCase->rftReader();
if ( !reader )
{
pressurePerCase.push_back( std::numeric_limits<double>::infinity() );
continue;
}
auto pressureAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::PRESSURE );
std::vector<double> pressures;
reader->values( pressureAddress, &pressures );
if ( pressures.empty() )
{
pressurePerCase.push_back( std::numeric_limits<double>::infinity() );
continue;
}
auto mdAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::MD );
std::vector<double> depths;
reader->values( mdAddress, &depths );
if ( depths.empty() && extractor ) depths = reader->computeMeasuredDepth( wellName, timeStep, extractor );
// If depths are still empty after the fallback (no MD channel and no extractor), depth range
// filtering is not possible for this case; fall through to use all pressures unfiltered.
std::vector<double> samplesInRange;
if ( useDepthRange && depths.size() == pressures.size() )
{
for ( size_t i = 0; i < depths.size(); ++i )
if ( depths[i] >= depthRangeMin && depths[i] <= depthRangeMax ) samplesInRange.push_back( pressures[i] );
}
else
{
samplesInRange = pressures;
}
if ( samplesInRange.empty() )
pressurePerCase.push_back( std::numeric_limits<double>::infinity() );
else
pressurePerCase.push_back( std::accumulate( samplesInRange.begin(), samplesInRange.end(), 0.0 ) / samplesInRange.size() );
}
return pressurePerCase;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RimParameterRftCrossPlot::CaseData> RimParameterRftCrossPlot::createCaseData() const
{
if ( !m_ensemble() ) return {};
if ( m_wellName().isEmpty() ) return {};
if ( !m_selectedTimeStep().isValid() ) return {};
if ( m_ensembleParameter().isEmpty() ) return {};
RigEnsembleParameter parameter = m_ensemble->ensembleParameter( m_ensembleParameter );
if ( !parameter.isNumeric() || !parameter.isValid() ) return {};
const auto& allCases = m_ensemble->allSummaryCases();
const std::vector<double> pressurePerCase = computeMeanPressurePerCase( m_ensemble(),
m_wellName(),
m_selectedTimeStep(),
m_eclipseCase(),
m_useDepthRange(),
m_depthRangeMin(),
m_depthRangeMax() );
if ( pressurePerCase.size() != allCases.size() ) return {};
std::vector<CaseData> result;
result.reserve( allCases.size() );
for ( size_t caseIdx = 0; caseIdx < allCases.size(); ++caseIdx )
{
RimSummaryCase* summaryCase = allCases[caseIdx];
if ( !summaryCase ) continue;
if ( std::isinf( pressurePerCase[caseIdx] ) ) continue;
if ( caseIdx >= static_cast<size_t>( parameter.values.size() ) ) continue;
result.push_back(
{ .parameterValue = parameter.values[caseIdx].toDouble(), .pressureValue = pressurePerCase[caseIdx], .summaryCase = summaryCase } );
}
return result;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiuPlotWidget* RimParameterRftCrossPlot::plotWidget()
{
return m_plotWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::updateAxes()
{
if ( !m_plotWidget ) return;
const int axisTitleSize = caf::FontTools::absolutePointSize( RiaPreferences::current()->defaultPlotFontSize(), m_axisTitleFontSize() );
const int axisValueSize = caf::FontTools::absolutePointSize( RiaPreferences::current()->defaultPlotFontSize(), m_axisValueFontSize() );
const QString depthLabel = m_useDepthRange() ? QString( "Mean Pressure [MD %1 - %2]" ).arg( m_depthRangeMin() ).arg( m_depthRangeMax() )
: QString( "Mean Pressure" );
m_plotWidget->setAxisTitleText( RiuPlotAxis::defaultLeft(), depthLabel );
m_plotWidget->setAxisTitleEnabled( RiuPlotAxis::defaultLeft(), true );
m_plotWidget->setAxisFontsAndAlignment( RiuPlotAxis::defaultLeft(), axisTitleSize, axisValueSize, false, Qt::AlignCenter );
if ( m_yValueRange.has_value() )
{
m_plotWidget->setAxisRange( RiuPlotAxis::defaultLeft(), m_yValueRange->first, m_yValueRange->second );
}
m_plotWidget->setAxisTitleText( RiuPlotAxis::defaultBottom(), m_ensembleParameter() );
m_plotWidget->setAxisTitleEnabled( RiuPlotAxis::defaultBottom(), true );
m_plotWidget->setAxisFontsAndAlignment( RiuPlotAxis::defaultBottom(), axisTitleSize, axisValueSize, false, Qt::AlignCenter );
if ( m_xValueRange.has_value() )
{
m_plotWidget->setAxisRange( RiuPlotAxis::defaultBottom(), m_xValueRange->first, m_xValueRange->second );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimParameterRftCrossPlot::asciiDataForPlotExport() const
{
QString asciiData;
asciiData += "Realization\tParameter\tMean Pressure\n";
for ( const auto& [paramValue, pressureValue, summaryCase] : createCaseData() )
{
asciiData += QString( "%1\t%2\t%3\n" ).arg( summaryCase->displayCaseName() ).arg( paramValue ).arg( pressureValue );
}
return asciiData;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::detachAllCurves()
{
if ( m_plotWidget ) m_plotWidget->qwtPlot()->detachItems();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimParameterRftCrossPlot::description() const
{
return m_description();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QWidget* RimParameterRftCrossPlot::viewWidget()
{
return m_plotWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::deleteViewWidget()
{
cleanupBeforeClose();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::doRenderWindowContent( QPaintDevice* paintDevice )
{
if ( m_plotWidget ) m_plotWidget->render( paintDevice );
}
namespace
{
class CurveTracker : public QwtPlotPicker
{
public:
CurveTracker( QwtPlot* plot )
: QwtPlotPicker( plot->canvas() )
{
setStateMachine( new QwtPickerTrackerMachine() );
setRubberBand( QwtPicker::NoRubberBand );
setTrackerMode( QwtPicker::AlwaysOn );
}
protected:
QwtText trackerText( const QPoint& pos ) const override
{
double minDistance = std::numeric_limits<double>::max();
QString closestCurveLabel;
for ( QwtPlotItem* item : plot()->itemList() )
{
if ( item->rtti() == QwtPlotItem::Rtti_PlotCurve )
{
auto curve = static_cast<QwtPlotCurve*>( item );
double distance = std::numeric_limits<double>::max();
curve->closestPoint( pos, &distance );
if ( distance < minDistance )
{
minDistance = distance;
closestCurveLabel = curve->title().text();
}
}
}
if ( minDistance < 20.0 )
{
QwtText text( closestCurveLabel );
text.setBackgroundBrush( QBrush( Qt::white ) );
text.setColor( Qt::black );
return text;
}
return QwtText();
}
};
} // anonymous namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiuPlotWidget* RimParameterRftCrossPlot::doCreatePlotViewWidget( QWidget* parent )
{
if ( !m_plotWidget )
{
m_plotWidget = new RiuQwtPlotWidget( this, parent );
updatePlotTitle();
new RiuContextMenuLauncher( m_plotWidget, { "RicShowPlotDataFeature" } );
}
if ( m_plotWidget )
{
new CurveTracker( m_plotWidget->qwtPlot() );
caf::PdmPointer<RimParameterRftCrossPlot> self( this );
new RiuQwtCurveSelectorFilter( m_plotWidget->qwtPlot(),
[self]( const QPoint& pos ) -> const caf::PdmUiItem*
{ return self ? self->findClosestCase( pos ) : nullptr; } );
}
return m_plotWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::onLoadDataAndUpdate()
{
updateMdiWindowVisibility();
if ( m_plotWidget )
{
createPoints();
updateValueRanges();
updateAxes();
updatePlotTitle();
m_plotWidget->scheduleReplot();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
auto* dataGroup = uiOrdering.addNewGroup( "Data Source" );
dataGroup->add( &m_ensemble );
dataGroup->add( &m_wellName );
dataGroup->add( &m_selectedTimeStep );
dataGroup->add( &m_eclipseCase );
auto* depthGroup = uiOrdering.addNewGroup( "Depth Range" );
depthGroup->add( &m_useDepthRange );
depthGroup->add( &m_depthRangeMin );
depthGroup->add( &m_depthRangeMax );
m_depthRangeMin.uiCapability()->setUiReadOnly( !m_useDepthRange() );
m_depthRangeMax.uiCapability()->setUiReadOnly( !m_useDepthRange() );
auto* crossPlotGroup = uiOrdering.addNewGroup( "Cross Plot Parameter" );
crossPlotGroup->add( &m_ensembleParameter );
auto* plotGroup = uiOrdering.addNewGroup( "Plot Settings" );
plotGroup->setCollapsedByDefault();
plotGroup->add( &m_useAutoPlotTitle );
plotGroup->add( &m_description );
plotGroup->add( &m_axisTitleFontSize );
plotGroup->add( &m_axisValueFontSize );
m_description.uiCapability()->setUiReadOnly( m_useAutoPlotTitle() );
uiOrdering.skipRemainingFields( true );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue )
{
if ( changedField == &m_ensemble || changedField == &m_wellName )
{
// Reset time step to the first available one for the new ensemble/well combination
std::set<QDateTime> timeSteps;
if ( m_ensemble() && !m_wellName().isEmpty() )
{
for ( RimSummaryCase* summaryCase : m_ensemble->allSummaryCases() )
{
RifReaderRftInterface* reader = summaryCase->rftReader();
if ( reader )
{
for ( const QDateTime& dt : reader->availableTimeSteps( m_wellName() ) )
timeSteps.insert( dt );
}
}
}
m_selectedTimeStep = timeSteps.empty() ? QDateTime() : *timeSteps.begin();
}
RimPlot::fieldChangedByUi( changedField, oldValue, newValue );
loadDataAndUpdate();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QList<caf::PdmOptionItemInfo> RimParameterRftCrossPlot::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions )
{
QList<caf::PdmOptionItemInfo> options;
if ( fieldNeedingOptions == &m_ensemble )
{
for ( RimSummaryEnsemble* ensemble : RimProject::current()->summaryEnsembles() )
{
if ( ensemble->isEnsemble() ) options.push_back( caf::PdmOptionItemInfo( ensemble->name(), ensemble ) );
}
}
else if ( fieldNeedingOptions == &m_wellName )
{
std::set<QString> wellNames;
if ( m_ensemble() )
{
for ( RimSummaryCase* summaryCase : m_ensemble->allSummaryCases() )
{
RifReaderRftInterface* reader = summaryCase->rftReader();
if ( reader )
{
for ( const QString& name : reader->wellNames() )
wellNames.insert( name );
}
}
}
for ( const QString& name : wellNames )
options.push_back( caf::PdmOptionItemInfo( name, name ) );
}
else if ( fieldNeedingOptions == &m_selectedTimeStep )
{
std::set<QDateTime> timeSteps;
if ( m_ensemble() && !m_wellName().isEmpty() )
{
for ( RimSummaryCase* summaryCase : m_ensemble->allSummaryCases() )
{
RifReaderRftInterface* reader = summaryCase->rftReader();
if ( reader )
{
for ( const QDateTime& dt : reader->availableTimeSteps( m_wellName() ) )
timeSteps.insert( dt );
}
}
}
for ( const QDateTime& dt : timeSteps )
options.push_back( caf::PdmOptionItemInfo( dt.toString( "yyyy-MM-dd" ), dt ) );
}
else if ( fieldNeedingOptions == &m_eclipseCase )
{
options.push_back( caf::PdmOptionItemInfo( "None", static_cast<RimEclipseResultCase*>( nullptr ) ) );
for ( RimEclipseCase* c : RimProject::current()->eclipseCases() )
{
if ( auto* rc = dynamic_cast<RimEclipseResultCase*>( c ) )
options.push_back( caf::PdmOptionItemInfo( rc->caseUserDescription(), rc ) );
}
}
else if ( fieldNeedingOptions == &m_ensembleParameter )
{
if ( m_ensemble() )
{
const auto& allCases = m_ensemble->allSummaryCases();
// Build mean RFT pressure per case if enough context is available for correlation sorting
const bool canComputeCorrelation = !m_wellName().isEmpty() && m_selectedTimeStep().isValid();
std::vector<double> pressurePerCase;
if ( canComputeCorrelation )
{
pressurePerCase = computeMeanPressurePerCase( m_ensemble(),
m_wellName(),
m_selectedTimeStep(),
m_eclipseCase(),
m_useDepthRange(),
m_depthRangeMin(),
m_depthRangeMax() );
}
// Compute correlation for each numeric parameter, then sort by abs value descending
std::vector<std::pair<double, RigEnsembleParameter>> correlatedParams;
for ( const auto& param : RimSummaryEnsembleTools::alphabeticEnsembleParameters( allCases ) )
{
if ( !param.isNumeric() ) continue;
double absPearson = 0.0;
if ( canComputeCorrelation && static_cast<size_t>( param.values.size() ) == allCases.size() )
{
std::vector<double> paramValues, pressureValues;
for ( size_t i = 0; i < allCases.size(); ++i )
{
if ( std::isinf( pressurePerCase[i] ) ) continue;
paramValues.push_back( param.values[i].toDouble() );
pressureValues.push_back( pressurePerCase[i] );
}
if ( paramValues.size() >= 2 )
{
double r = RigStatisticsTools::pearsonCorrelation( paramValues, pressureValues );
if ( !std::isinf( r ) && !std::isnan( r ) ) absPearson = std::abs( r );
}
}
correlatedParams.emplace_back( absPearson, param );
}
std::stable_sort( correlatedParams.begin(),
correlatedParams.end(),
[]( const auto& a, const auto& b ) { return a.first > b.first; } );
for ( const auto& [corr, param] : correlatedParams )
options.push_back( caf::PdmOptionItemInfo( param.uiName(), param.name ) );
}
}
return options;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::createPoints()
{
detachAllCurves();
caf::ColorTable colorTable = RiaColorTables::categoryPaletteColors();
auto caseData = createCaseData();
if ( caseData.empty() ) return;
std::set<RimSummaryCase*> selectedSummaryCases;
auto selectedTreeViewItems = RiuDockWidgetTools::selectedItemsInTreeView( RiuDockWidgetTools::plotMainWindowDataSourceTreeName() );
for ( auto item : selectedTreeViewItems )
{
if ( auto summaryCase = dynamic_cast<RimSummaryCase*>( item ) )
{
selectedSummaryCases.insert( summaryCase );
}
}
int idx = 0;
for ( const auto& [paramValue, pressureValue, summaryCase] : caseData )
{
auto* plotCurve = new RiuQwtPlotCurve;
plotCurve->setSamplesValues( { paramValue }, { pressureValue } );
plotCurve->setStyle( QwtPlotCurve::NoCurve );
const bool isSelected = selectedSummaryCases.contains( summaryCase );
auto* symbol = new RiuQwtSymbol( isSelected ? RiuPlotCurveSymbol::SYMBOL_XCROSS : RiuPlotCurveSymbol::SYMBOL_ELLIPSE );
symbol->setSize( 8, 8 );
symbol->setColor( colorTable.cycledQColor( idx++ ) );
plotCurve->setSymbol( symbol );
plotCurve->setTitle( summaryCase->displayCaseName() );
plotCurve->attach( m_plotWidget->qwtPlot() );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::updatePlotTitle()
{
if ( !m_plotWidget ) return;
if ( m_useAutoPlotTitle && m_ensemble() )
{
if ( m_useDepthRange() )
{
m_description = QString( "%1 vs RFT Pressure [%2 - %3 m], %4" )
.arg( m_ensembleParameter() )
.arg( m_depthRangeMin() )
.arg( m_depthRangeMax() )
.arg( m_ensemble->name() );
}
else
{
m_description = QString( "%1 vs RFT Pressure, %2" ).arg( m_ensembleParameter() ).arg( m_ensemble->name() );
}
}
m_plotWidget->setPlotTitle( m_description() );
m_plotWidget->setPlotTitleEnabled( m_showPlotTitle() );
m_plotWidget->setPlotTitleFontSize( titleFontSize() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::updateValueRanges()
{
double xMin = std::numeric_limits<double>::infinity();
double xMax = -std::numeric_limits<double>::infinity();
double yMin = std::numeric_limits<double>::infinity();
double yMax = -std::numeric_limits<double>::infinity();
for ( const auto& [paramValue, pressureValue, summaryCase] : createCaseData() )
{
xMin = std::min( xMin, paramValue );
xMax = std::max( xMax, paramValue );
yMin = std::min( yMin, pressureValue );
yMax = std::max( yMax, pressureValue );
}
if ( xMin == std::numeric_limits<double>::infinity() )
{
m_xValueRange = std::nullopt;
m_yValueRange = std::nullopt;
return;
}
const double xRange = xMax - xMin;
const double yRange = yMax - yMin;
m_xValueRange = std::make_pair( xMin - xRange * 0.1, xMax + xRange * 0.1 );
m_yValueRange = std::make_pair( yMin - yRange * 0.1, yMax + yRange * 0.1 );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimSummaryCase* RimParameterRftCrossPlot::findClosestCase( const QPoint& canvasPos )
{
auto caseData = createCaseData();
std::vector<std::pair<double, double>> points;
points.reserve( caseData.size() );
for ( const auto& d : caseData )
points.push_back( { d.parameterValue, d.pressureValue } );
int idx = RiuQwtCurveSelectorFilter::closestPointIndex( m_plotWidget->qwtPlot(), canvasPos, points );
return idx >= 0 ? caseData[idx].summaryCase : nullptr;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimParameterRftCrossPlot::cleanupBeforeClose()
{
detachAllCurves();
if ( m_plotWidget )
{
m_plotWidget->setParent( nullptr );
delete m_plotWidget;
m_plotWidget = nullptr;
}
}
@@ -0,0 +1,136 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "Appearance/RimFontSizeField.h"
#include "RimPlot.h"
#include "cafPdmField.h"
#include "cafPdmPtrField.h"
#include <QDateTime>
#include <QPointer>
#include <optional>
#include <utility>
#include <vector>
class RimEclipseResultCase;
class RimSummaryEnsemble;
class RimSummaryCase;
class RiuQwtPlotWidget;
//==================================================================================================
///
/// Cross plot of ensemble parameter (X) vs mean RFT pressure (Y) within a MD depth range.
///
//==================================================================================================
class RimParameterRftCrossPlot : public RimPlot
{
Q_OBJECT;
CAF_PDM_HEADER_INIT;
public:
struct CaseData
{
double parameterValue;
double pressureValue; // mean pressure in depth range
RimSummaryCase* summaryCase;
};
public:
RimParameterRftCrossPlot();
~RimParameterRftCrossPlot() override;
void setEnsemble( RimSummaryEnsemble* ensemble );
void setWellName( const QString& wellName );
void setTimeStep( const QDateTime& timeStep );
void setDepthRange( double minMd, double maxMd );
void setEnsembleParameter( const QString& paramName );
QString ensembleParameter() const;
QString wellName() const;
QDateTime selectedTimeStep() const;
RimSummaryEnsemble* ensemble() const;
RiuQwtPlotWidget* viewer();
std::vector<CaseData> createCaseData() const;
// Computes mean RFT pressure per ensemble case (one entry per case, infinity = no data).
// Indices match ensemble->allSummaryCases().
static std::vector<double> computeMeanPressurePerCase( RimSummaryEnsemble* ensemble,
const QString& wellName,
const QDateTime& timeStep,
RimEclipseResultCase* eclipseCase,
bool useDepthRange,
double depthRangeMin,
double depthRangeMax );
// RimPlot pure virtual overrides
RiuPlotWidget* plotWidget() override;
void setAutoScaleXEnabled( bool ) override {}
void setAutoScaleYEnabled( bool ) override {}
void updateAxes() override;
void updateLegend() override {}
QString asciiDataForPlotExport() const override;
void reattachAllCurves() override {}
void detachAllCurves() override;
QString description() const override;
private:
// RimViewWindow / RimPlotWindow overrides
QWidget* viewWidget() override;
void zoomAll() override {}
void deleteViewWidget() override;
void doUpdateLayout() override {}
void doRenderWindowContent( QPaintDevice* paintDevice ) override;
RiuPlotWidget* doCreatePlotViewWidget( QWidget* parent = nullptr ) override;
void onLoadDataAndUpdate() override;
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
QList<caf::PdmOptionItemInfo> calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override;
void createPoints();
void updatePlotTitle();
void updateValueRanges();
void cleanupBeforeClose();
RimSummaryCase* findClosestCase( const QPoint& canvasPos );
private:
caf::PdmPtrField<RimSummaryEnsemble*> m_ensemble;
caf::PdmField<QString> m_wellName;
caf::PdmField<QDateTime> m_selectedTimeStep;
caf::PdmPtrField<RimEclipseResultCase*> m_eclipseCase;
caf::PdmField<bool> m_useDepthRange;
caf::PdmField<double> m_depthRangeMin;
caf::PdmField<double> m_depthRangeMax;
caf::PdmField<QString> m_ensembleParameter;
caf::PdmField<bool> m_useAutoPlotTitle;
caf::PdmField<QString> m_description;
RimFontSizeField m_axisTitleFontSize;
RimFontSizeField m_axisValueFontSize;
QPointer<RiuQwtPlotWidget> m_plotWidget;
std::optional<std::pair<double, double>> m_xValueRange;
std::optional<std::pair<double, double>> m_yValueRange;
};
@@ -0,0 +1,502 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimRftCorrelationReportPlot.h"
#include "RimParameterRftCrossPlot.h"
#include "RimRftTornadoPlot.h"
#include "RimWellLogTrack.h"
#include "RimWellRftPlot.h"
#include "RiuInterfaceToViewWindow.h"
#include "RiuPlotWidget.h"
#include "RiuQwtPlotWidget.h"
#include "DockAreaTitleBar.h"
#include "DockAreaWidget.h"
#include "DockManager.h"
#include "DockWidget.h"
#include "cafPdmUiCheckBoxEditor.h"
#include "cafPdmUiTreeOrdering.h"
#include "cafSelectionManager.h"
#include <QContextMenuEvent>
#include <QFrame>
#include <QSettings>
#include <QVBoxLayout>
static const char* RFT_DOCK_LAYOUT_REGISTRY_KEY = "RftCorrelationReportPlot/defaultDockLayout";
//--------------------------------------------------------------------------------------------------
/// Thin wrapper that implements RiuInterfaceToViewWindow for the dock manager frame.
//--------------------------------------------------------------------------------------------------
class RiuRftCorrelationReportPlotWidget : public QFrame, public RiuInterfaceToViewWindow
{
public:
RiuRftCorrelationReportPlotWidget( RimViewWindow* viewWindow, QWidget* parent = nullptr )
: QFrame( parent )
, m_viewWindow( viewWindow )
{
setLayout( new QVBoxLayout );
layout()->setContentsMargins( 0, 0, 0, 0 );
layout()->setSpacing( 0 );
}
RimViewWindow* ownerViewWindow() const override { return m_viewWindow; }
private:
RimViewWindow* m_viewWindow;
};
//--------------------------------------------------------------------------------------------------
/// Sets the report plot as the CAF-selected item whenever a context menu fires
/// anywhere inside the dock manager.
//--------------------------------------------------------------------------------------------------
class RftSelectionFixerOnContextMenu : public QObject
{
public:
RftSelectionFixerOnContextMenu( caf::PdmObject* item, QObject* parent )
: QObject( parent )
, m_item( item )
{
}
bool eventFilter( QObject*, QEvent* event ) override
{
if ( event->type() == QEvent::ContextMenu ) caf::SelectionManager::instance()->setSelectedItem( m_item );
return false;
}
private:
caf::PdmObject* m_item;
};
//==================================================================================================
//
//
//
//==================================================================================================
CAF_PDM_SOURCE_INIT( RimRftCorrelationReportPlot, "RftCorrelationReportPlot" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimRftCorrelationReportPlot::RimRftCorrelationReportPlot()
{
CAF_PDM_InitObject( "RFT Correlation Report Plot", ":/CorrelationReportPlot16x16.png" );
setDeletable( true );
CAF_PDM_InitFieldNoDefault( &m_name, "PlotWindowTitle", "Title" );
m_name.registerGetMethod( this, &RimRftCorrelationReportPlot::createDescription );
CAF_PDM_InitFieldNoDefault( &m_wellRftPlot, "WellRftPlot", "RFT Plot" );
CAF_PDM_InitFieldNoDefault( &m_parameterRftCrossPlot, "ParameterRftCrossPlot", "Cross Plot" );
CAF_PDM_InitFieldNoDefault( &m_correlationPlot, "CorrelationPlot", "Tornado Plot" );
CAF_PDM_InitField( &m_showDockTitleBars, "ShowDockTitleBars", false, "Show Title Bars" );
caf::PdmUiNativeCheckBoxEditor::configureFieldForEditor( &m_showDockTitleBars );
CAF_PDM_InitField( &m_dockState, "DockState", QString(), "Dock State" );
m_dockState.uiCapability()->setUiHidden( true );
setAsPlotMdiWindow();
m_showWindow = true;
m_showPlotLegends = false;
m_wellRftPlot = new RimWellRftPlot;
m_wellRftPlot->revokeMdiWindowStatus();
m_wellRftPlot->setShowWindow( true );
m_parameterRftCrossPlot = new RimParameterRftCrossPlot;
m_correlationPlot = new RimRftTornadoPlot;
m_correlationPlot->setParameterSelectedCallback( [this]( const QString& paramName ) { onTornadoParameterSelected( paramName ); } );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimRftCorrelationReportPlot::~RimRftCorrelationReportPlot()
{
removeMdiWindowFromMdiArea();
cleanupBeforeClose();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QWidget* RimRftCorrelationReportPlot::viewWidget()
{
return m_viewWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimRftCorrelationReportPlot::description() const
{
return createDescription();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
caf::PdmFieldHandle* RimRftCorrelationReportPlot::userDescriptionField()
{
return &m_name;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimWellRftPlot* RimRftCorrelationReportPlot::wellRftPlot() const
{
return m_wellRftPlot();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimParameterRftCrossPlot* RimRftCorrelationReportPlot::crossPlot() const
{
return m_parameterRftCrossPlot();
}
//--------------------------------------------------------------------------------------------------
/// Initialize the owned RimWellRftPlot from the source plot's selected data sources.
/// We keep the fresh (curve-free) child plot and call initializeDataSources(source) so
/// syncCurvesFromUiSelection builds curves from scratch without touching unresolved copies.
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::initializeFromSourcePlot( RimWellRftPlot* source )
{
if ( !source ) return;
m_wellRftPlot->setSimWellOrWellPathName( source->simWellOrWellPathName() );
// A fresh RimWellRftPlot has no tracks; syncCurvesFromUiSelection exits early without one.
// Guard against duplicate track creation if this is called more than once.
if ( m_wellRftPlot->plotCount() == 0 )
{
auto* track = new RimWellLogTrack();
m_wellRftPlot->addPlot( track );
track->setDescription( QString( "Track %1" ).arg( m_wellRftPlot->plotCount() ) );
}
m_wellRftPlot->initializeDataSources( source );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimRftCorrelationReportPlot::createDescription() const
{
if ( m_parameterRftCrossPlot() )
{
const QString wellName = m_parameterRftCrossPlot()->wellName();
const QString param = m_parameterRftCrossPlot()->ensembleParameter();
if ( !wellName.isEmpty() && !param.isEmpty() )
{
return QString( "RFT Correlation: %1 vs %2" ).arg( param ).arg( wellName );
}
}
return "RFT Correlation Report";
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::recreatePlotWidgets()
{
CAF_ASSERT( m_dockManager );
m_wellRftPlot->createPlotWidget( m_dockManager );
m_correlationPlot->createPlotWidget( m_dockManager );
m_parameterRftCrossPlot->createPlotWidget( m_dockManager );
// Context menu fixer — ensures this report is selected in CAF on any right-click
delete m_contextMenuFilter;
m_contextMenuFilter = new RftSelectionFixerOnContextMenu( this, this );
if ( auto* w = m_wellRftPlot->viewWidget() ) w->installEventFilter( m_contextMenuFilter );
if ( auto* w = m_correlationPlot->viewer() ) w->installEventFilter( m_contextMenuFilter );
if ( auto* w = m_parameterRftCrossPlot->viewer() ) w->installEventFilter( m_contextMenuFilter );
auto makeDockWidget = [&]( const QString& title, RimPlotWindow* plot, QWidget* widget ) -> ads::CDockWidget*
{
auto* dock = new ads::CDockWidget( title, m_dockManager );
dock->setWidget( widget, ads::CDockWidget::ForceNoScrollArea );
connect( dock,
&ads::CDockWidget::closed,
this,
[this, plot]()
{
plot->setShowWindow( false );
updateConnectedEditors();
} );
return dock;
};
m_rftDockWidget = makeDockWidget( "RFT Plot", m_wellRftPlot(), m_wellRftPlot->viewWidget() );
m_correlationDockWidget = makeDockWidget( "Tornado Plot", m_correlationPlot(), m_correlationPlot->viewer() );
m_crossPlotDockWidget = makeDockWidget( "Cross Plot", m_parameterRftCrossPlot(), m_parameterRftCrossPlot->viewer() );
// Restore saved dock state or apply hard-coded default layout
QByteArray stateToRestore;
if ( !m_dockState().isEmpty() )
{
stateToRestore = QByteArray::fromBase64( m_dockState().toLatin1() );
}
else
{
QSettings settings;
QVariant v = settings.value( RFT_DOCK_LAYOUT_REGISTRY_KEY );
if ( v.isValid() ) stateToRestore = v.toByteArray();
}
if ( !stateToRestore.isEmpty() )
{
m_dockManager->addDockWidget( ads::LeftDockWidgetArea, m_rftDockWidget );
auto* rightArea = m_dockManager->addDockWidget( ads::RightDockWidgetArea, m_correlationDockWidget );
m_dockManager->addDockWidget( ads::BottomDockWidgetArea, m_crossPlotDockWidget, rightArea );
m_dockManager->restoreState( stateToRestore, 1 );
}
else
{
// Default: RFT plot on the left, tornado top-right, cross plot bottom-right
m_dockManager->addDockWidget( ads::LeftDockWidgetArea, m_rftDockWidget );
auto* rightArea = m_dockManager->addDockWidget( ads::RightDockWidgetArea, m_correlationDockWidget );
m_dockManager->addDockWidget( ads::BottomDockWidgetArea, m_crossPlotDockWidget, rightArea );
}
m_rftDockWidget->toggleView( m_wellRftPlot->showWindow() );
m_correlationDockWidget->toggleView( m_correlationPlot->showWindow() );
m_crossPlotDockWidget->toggleView( m_parameterRftCrossPlot->showWindow() );
updateDockTitleBarsVisibility();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::cleanupBeforeClose()
{
// Detach and delete legend curves before the QwtPlot inside the track widget is destroyed.
// QwtPlot has autoDelete=true, so any curves still attached when it is deleted are freed by QWT
// — leaving m_legendPlotCurves with dangling pointers on the next loadDataAndUpdate().
if ( m_wellRftPlot() ) m_wellRftPlot->cleanupLegendCurves();
if ( m_correlationPlot() ) m_correlationPlot->detachAllCurves();
if ( m_parameterRftCrossPlot() ) m_parameterRftCrossPlot->detachAllCurves();
m_rftDockWidget = nullptr;
m_correlationDockWidget = nullptr;
m_crossPlotDockWidget = nullptr;
if ( m_dockManager )
{
delete m_dockManager;
m_dockManager = nullptr;
}
if ( m_viewWidget )
{
m_viewWidget->setParent( nullptr );
delete m_viewWidget;
m_viewWidget = nullptr;
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::setupBeforeSave()
{
if ( m_dockManager )
{
m_dockState = QString::fromLatin1( m_dockManager->saveState( 1 ).toBase64() );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::doRenderWindowContent( QPaintDevice* paintDevice )
{
if ( m_viewWidget ) m_viewWidget->render( paintDevice );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QWidget* RimRftCorrelationReportPlot::createViewWidget( QWidget* mainWindowParent )
{
auto* wrapper = new RiuRftCorrelationReportPlotWidget( this, mainWindowParent );
m_viewWidget = wrapper;
m_dockManager = new ads::CDockManager( wrapper );
m_dockManager->setStyleSheet( "ads--CDockSplitter::handle { width: 2px; height: 2px; background-color: #a0a0a0; }" );
wrapper->layout()->addWidget( m_dockManager );
recreatePlotWidgets();
return m_viewWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::deleteViewWidget()
{
cleanupBeforeClose();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::onLoadDataAndUpdate()
{
updateMdiWindowVisibility();
if ( m_showWindow )
{
m_wellRftPlot->loadDataAndUpdate();
syncTornadoInputsFromCrossPlot();
m_correlationPlot->loadDataAndUpdate();
m_parameterRftCrossPlot->loadDataAndUpdate();
}
updateLayout();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
// Delegate cross-plot settings (ensemble, well, depth range, parameter) to the cross plot
m_parameterRftCrossPlot->uiOrdering( uiConfigName, uiOrdering );
auto* layoutGroup = uiOrdering.addNewGroup( "Dock Layout" );
layoutGroup->setCollapsedByDefault();
layoutGroup->add( &m_showDockTitleBars );
uiOrdering.skipRemainingFields( true );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString /*uiConfigName*/ )
{
uiTreeOrdering.add( m_wellRftPlot() );
uiTreeOrdering.add( m_correlationPlot() );
uiTreeOrdering.add( m_parameterRftCrossPlot() );
uiTreeOrdering.skipRemainingChildren();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue )
{
if ( changedField == &m_showDockTitleBars )
{
updateDockTitleBarsVisibility();
return;
}
loadDataAndUpdate();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::childFieldChangedByUi( const caf::PdmFieldHandle* changedChildField )
{
if ( m_rftDockWidget && changedChildField == &m_wellRftPlot )
m_rftDockWidget->toggleView( m_wellRftPlot->showWindow() );
else if ( m_correlationDockWidget && changedChildField == &m_correlationPlot )
m_correlationDockWidget->toggleView( m_correlationPlot->showWindow() );
else if ( m_crossPlotDockWidget && changedChildField == &m_parameterRftCrossPlot )
{
m_crossPlotDockWidget->toggleView( m_parameterRftCrossPlot->showWindow() );
syncCrossPlotSelectionToRftPlot();
}
updateDockTitleBarsVisibility();
loadDataAndUpdate();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::syncCrossPlotSelectionToRftPlot()
{
if ( !m_wellRftPlot() || !m_parameterRftCrossPlot() ) return;
const QString wellName = m_parameterRftCrossPlot->wellName();
const QDateTime timeStep = m_parameterRftCrossPlot->selectedTimeStep();
if ( !wellName.isEmpty() ) m_wellRftPlot->setSimWellOrWellPathName( wellName );
if ( timeStep.isValid() ) m_wellRftPlot->setSelectedTimeSteps( { timeStep } );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimRftTornadoPlot* RimRftCorrelationReportPlot::correlationPlot() const
{
return m_correlationPlot();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::onTornadoParameterSelected( const QString& paramName )
{
if ( m_correlationPlot() )
{
m_correlationPlot->setSelectedParameter( paramName );
m_correlationPlot->loadDataAndUpdate();
}
if ( m_parameterRftCrossPlot() )
{
m_parameterRftCrossPlot->setEnsembleParameter( paramName );
m_parameterRftCrossPlot->loadDataAndUpdate();
}
updateConnectedEditors();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::syncTornadoInputsFromCrossPlot()
{
if ( !m_correlationPlot() || !m_parameterRftCrossPlot() ) return;
m_correlationPlot->setEnsemble( m_parameterRftCrossPlot->ensemble() );
m_correlationPlot->setWellName( m_parameterRftCrossPlot->wellName() );
m_correlationPlot->setTimeStep( m_parameterRftCrossPlot->selectedTimeStep() );
m_correlationPlot->setSelectedParameter( m_parameterRftCrossPlot->ensembleParameter() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftCorrelationReportPlot::updateDockTitleBarsVisibility()
{
if ( !m_dockManager ) return;
for ( auto* area : m_dockManager->openedDockAreas() )
area->titleBar()->setVisible( m_showDockTitleBars() );
}
@@ -0,0 +1,101 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimPlotWindow.h"
#include "cafPdmChildField.h"
#include "cafPdmField.h"
#include "cafPdmProxyValueField.h"
#include <QObject>
class RimParameterRftCrossPlot;
class RimRftTornadoPlot;
class RimWellRftPlot;
namespace ads
{
class CDockManager;
class CDockWidget;
} // namespace ads
//==================================================================================================
///
/// Composite report plot combining a RimWellRftPlot (pressure profile) and a
/// RimParameterRftCrossPlot (ensemble parameter vs mean pressure in a depth range).
///
//==================================================================================================
class RimRftCorrelationReportPlot : public QObject, public RimPlotWindow
{
CAF_PDM_HEADER_INIT;
public:
RimRftCorrelationReportPlot();
~RimRftCorrelationReportPlot() override;
QWidget* viewWidget() override;
QString description() const override;
void zoomAll() override {}
caf::PdmFieldHandle* userDescriptionField() override;
RimWellRftPlot* wellRftPlot() const;
RimParameterRftCrossPlot* crossPlot() const;
RimRftTornadoPlot* correlationPlot() const;
void initializeFromSourcePlot( RimWellRftPlot* source );
private:
QString createDescription() const;
void recreatePlotWidgets();
void cleanupBeforeClose();
void setupBeforeSave() override;
void doRenderWindowContent( QPaintDevice* paintDevice ) override;
QWidget* createViewWidget( QWidget* mainWindowParent = nullptr ) override;
void deleteViewWidget() override;
void onLoadDataAndUpdate() override;
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
void defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName = "" ) override;
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
void childFieldChangedByUi( const caf::PdmFieldHandle* changedChildField ) override;
void doUpdateLayout() override {}
void updateDockTitleBarsVisibility();
void syncCrossPlotSelectionToRftPlot();
void onTornadoParameterSelected( const QString& paramName );
void syncTornadoInputsFromCrossPlot();
private:
caf::PdmProxyValueField<QString> m_name;
caf::PdmChildField<RimWellRftPlot*> m_wellRftPlot;
caf::PdmChildField<RimParameterRftCrossPlot*> m_parameterRftCrossPlot;
caf::PdmChildField<RimRftTornadoPlot*> m_correlationPlot;
caf::PdmField<bool> m_showDockTitleBars;
caf::PdmField<QString> m_dockState;
QWidget* m_viewWidget = nullptr;
QObject* m_contextMenuFilter = nullptr;
ads::CDockManager* m_dockManager = nullptr;
ads::CDockWidget* m_rftDockWidget = nullptr;
ads::CDockWidget* m_correlationDockWidget = nullptr;
ads::CDockWidget* m_crossPlotDockWidget = nullptr;
};
@@ -0,0 +1,478 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimRftTornadoPlot.h"
#include "RiaColorTools.h"
#include "RiaPreferences.h"
#include "RigEnsembleParameter.h"
#include "RigStatisticsTools.h"
#include "RimEclipseResultCase.h"
#include "RimParameterRftCrossPlot.h"
#include "RimSummaryEnsemble.h"
#include "RimSummaryEnsembleTools.h"
#include "RiuContextMenuLauncher.h"
#include "RiuGroupedBarChartBuilder.h"
#include "RiuPlotItem.h"
#include "RiuQwtPlotItem.h"
#include "RiuQwtPlotWidget.h"
#include "cafPdmUiCheckBoxEditor.h"
#include "qwt_column_symbol.h"
#include "qwt_plot.h"
#include "qwt_plot_barchart.h"
#include "qwt_text.h"
#include <QPaintDevice>
#include <limits>
#include <map>
#include <numeric>
CAF_PDM_SOURCE_INIT( RimRftTornadoPlot, "RftTornadoPlot" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimRftTornadoPlot::RimRftTornadoPlot()
{
CAF_PDM_InitObject( "RFT Tornado Plot", ":/CorrelationTornadoPlot16x16.png" );
CAF_PDM_InitFieldNoDefault( &m_ensemble, "Ensemble", "Ensemble" );
CAF_PDM_InitField( &m_wellName, "WellName", QString(), "Well Name" );
CAF_PDM_InitFieldNoDefault( &m_selectedTimeStep, "TimeStep", "Time Step" );
CAF_PDM_InitFieldNoDefault( &m_eclipseCase, "EclipseCase", "Eclipse Case (MD fallback)" );
CAF_PDM_InitField( &m_useDepthRange, "UseDepthRange", false, "Filter by Depth Range" );
CAF_PDM_InitField( &m_depthRangeMin, "DepthRangeMin", 0.0, "Min Depth (MD)" );
CAF_PDM_InitField( &m_depthRangeMax, "DepthRangeMax", 5000.0, "Max Depth (MD)" );
CAF_PDM_InitField( &m_showAbsoluteValues, "ShowAbsoluteValues", false, "Show Absolute Values" );
CAF_PDM_InitField( &m_sortByAbsoluteValues, "SortByAbsoluteValues", true, "Sort by Absolute Values" );
CAF_PDM_InitField( &m_showOnlyTopNCorrelations, "ShowOnlyTopN", true, "Show Only Top Correlations" );
CAF_PDM_InitField( &m_topNFilterCount, "TopNFilterCount", 20, "Number of Rows" );
QColor qColor = QColor( "#3173b2" );
CAF_PDM_InitField( &m_barColor, "BarColor", RiaColorTools::fromQColorTo3f( qColor ), "Bar Color (Positive)" );
QColor highlightColor = QColor( "#f5a623" );
CAF_PDM_InitField( &m_highlightBarColor, "HighlightBarColor", RiaColorTools::fromQColorTo3f( highlightColor ), "Bar Color (Selected)" );
CAF_PDM_InitField( &m_useAutoPlotTitle, "UseAutoPlotTitle", true, "Auto Title" );
CAF_PDM_InitField( &m_description, "Description", QString( "RFT Tornado Plot" ), "Title" );
CAF_PDM_InitFieldNoDefault( &m_labelFontSize, "LabelFontSize", "Bar Label Font Size" );
CAF_PDM_InitFieldNoDefault( &m_axisTitleFontSize, "AxisTitleFontSize", "Axis Title Font Size" );
CAF_PDM_InitFieldNoDefault( &m_axisValueFontSize, "AxisValueFontSize", "Axis Value Font Size" );
m_labelFontSize = caf::FontTools::RelativeSize::Small;
m_axisTitleFontSize = caf::FontTools::RelativeSize::Small;
m_axisValueFontSize = caf::FontTools::RelativeSize::Small;
m_showPlotLegends = false;
setLegendsVisible( false );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimRftTornadoPlot::~RimRftTornadoPlot()
{
cleanupBeforeClose();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::setEnsemble( RimSummaryEnsemble* ensemble )
{
m_ensemble = ensemble;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::setWellName( const QString& wellName )
{
m_wellName = wellName;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::setTimeStep( const QDateTime& timeStep )
{
m_selectedTimeStep = timeStep;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::setEclipseCase( RimEclipseResultCase* eclipseCase )
{
m_eclipseCase = eclipseCase;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::setUseDepthRange( bool useDepthRange )
{
m_useDepthRange = useDepthRange;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::setDepthRange( double minMd, double maxMd )
{
m_depthRangeMin = minMd;
m_depthRangeMax = maxMd;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::setSelectedParameter( const QString& paramName )
{
m_selectedParameter = paramName;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::setParameterSelectedCallback( ParameterSelectedCallback callback )
{
m_parameterSelectedCallback = std::move( callback );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiuQwtPlotWidget* RimRftTornadoPlot::viewer()
{
return m_plotWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiuPlotWidget* RimRftTornadoPlot::plotWidget()
{
return m_plotWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::updateAxes()
{
if ( !m_plotWidget ) return;
const int titleSize = caf::FontTools::absolutePointSize( RiaPreferences::current()->defaultPlotFontSize(), m_axisTitleFontSize() );
const int valueSize = caf::FontTools::absolutePointSize( RiaPreferences::current()->defaultPlotFontSize(), m_axisValueFontSize() );
m_plotWidget->setAxisTitleText( RiuPlotAxis::defaultLeft(), "Parameter" );
m_plotWidget->setAxisTitleEnabled( RiuPlotAxis::defaultLeft(), true );
m_plotWidget->setAxisFontsAndAlignment( RiuPlotAxis::defaultLeft(), titleSize, valueSize, false, Qt::AlignCenter );
if ( m_showAbsoluteValues() )
{
m_plotWidget->setAxisTitleText( RiuPlotAxis::defaultBottom(), "Pearson Correlation Coefficient ABS" );
// Use a small negative margin so bar label text (anchored at x=0) remains visible
m_plotWidget->setAxisRange( RiuPlotAxis::defaultBottom(), -0.15, 1.0 );
}
else
{
m_plotWidget->setAxisTitleText( RiuPlotAxis::defaultBottom(), "Pearson Correlation Coefficient" );
m_plotWidget->setAxisRange( RiuPlotAxis::defaultBottom(), -1.0, 1.0 );
}
m_plotWidget->setAxisTitleEnabled( RiuPlotAxis::defaultBottom(), true );
m_plotWidget->setAxisFontsAndAlignment( RiuPlotAxis::defaultBottom(), titleSize, valueSize, false, Qt::AlignCenter );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimRftTornadoPlot::asciiDataForPlotExport() const
{
RiuGroupedBarChartBuilder chartBuilder;
addDataToChartBuilder( chartBuilder );
return chartBuilder.plotContentAsText();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::detachAllCurves()
{
if ( m_plotWidget )
{
m_plotWidget->qwtPlot()->detachItems( QwtPlotItem::Rtti_PlotBarChart );
m_plotWidget->qwtPlot()->detachItems( QwtPlotItem::Rtti_PlotScale );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimRftTornadoPlot::description() const
{
return m_description();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QWidget* RimRftTornadoPlot::viewWidget()
{
return m_plotWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::deleteViewWidget()
{
cleanupBeforeClose();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::doRenderWindowContent( QPaintDevice* paintDevice )
{
if ( m_plotWidget ) m_plotWidget->render( paintDevice );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiuPlotWidget* RimRftTornadoPlot::doCreatePlotViewWidget( QWidget* parent )
{
if ( !m_plotWidget )
{
m_plotWidget = new RiuQwtPlotWidget( this, parent );
updatePlotTitle();
new RiuContextMenuLauncher( m_plotWidget, { "RicShowPlotDataFeature" } );
}
return m_plotWidget;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::onLoadDataAndUpdate()
{
updateMdiWindowVisibility();
if ( m_plotWidget )
{
detachAllCurves();
RiuGroupedBarChartBuilder chartBuilder;
chartBuilder.setBarColor( RiaColorTools::toQColor( m_barColor() ) );
m_lastCorrelations = addDataToChartBuilder( chartBuilder );
const int labelSize = caf::FontTools::absolutePointSize( RiaPreferences::current()->defaultPlotFontSize(), m_labelFontSize() );
chartBuilder.setLabelFontSize( labelSize );
chartBuilder.addBarChartToPlot( m_plotWidget->qwtPlot(), Qt::Horizontal, m_showOnlyTopNCorrelations() ? m_topNFilterCount() : -1 );
highlightSelectedParameterBar();
m_plotWidget->qwtPlot()->insertLegend( nullptr );
updateAxes();
updatePlotTitle();
m_plotWidget->scheduleReplot();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
auto* settingsGroup = uiOrdering.addNewGroup( "Tornado Settings" );
settingsGroup->add( &m_showAbsoluteValues );
if ( !m_showAbsoluteValues() ) settingsGroup->add( &m_sortByAbsoluteValues );
settingsGroup->add( &m_showOnlyTopNCorrelations );
if ( m_showOnlyTopNCorrelations() ) settingsGroup->add( &m_topNFilterCount );
auto* colorGroup = uiOrdering.addNewGroup( "Colors" );
colorGroup->setCollapsedByDefault();
colorGroup->add( &m_barColor );
colorGroup->add( &m_highlightBarColor );
auto* plotGroup = uiOrdering.addNewGroup( "Plot Settings" );
plotGroup->setCollapsedByDefault();
plotGroup->add( &m_useAutoPlotTitle );
plotGroup->add( &m_description );
plotGroup->add( &m_labelFontSize );
plotGroup->add( &m_axisTitleFontSize );
plotGroup->add( &m_axisValueFontSize );
m_description.uiCapability()->setUiReadOnly( m_useAutoPlotTitle() );
uiOrdering.skipRemainingFields( true );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue )
{
RimPlot::fieldChangedByUi( changedField, oldValue, newValue );
loadDataAndUpdate();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::onPlotItemSelected( std::shared_ptr<RiuPlotItem> plotItem, bool /*toggle*/, int /*sampleIndex*/ )
{
auto* qwtPlotItem = dynamic_cast<RiuQwtPlotItem*>( plotItem.get() );
if ( !qwtPlotItem ) return;
auto* barChart = dynamic_cast<QwtPlotBarChart*>( qwtPlotItem->qwtPlotItem() );
if ( barChart && m_parameterSelectedCallback ) m_parameterSelectedCallback( barChart->title().text() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::map<QString, double> RimRftTornadoPlot::addDataToChartBuilder( RiuGroupedBarChartBuilder& chartBuilder ) const
{
std::map<QString, double> correlations;
if ( !m_ensemble() || m_wellName().isEmpty() || !m_selectedTimeStep().isValid() ) return correlations;
const auto& allCases = m_ensemble->allSummaryCases();
// Build pressure vector per case (indices match allCases order)
const std::vector<double> pressurePerCase = RimParameterRftCrossPlot::computeMeanPressurePerCase( m_ensemble(),
m_wellName(),
m_selectedTimeStep(),
m_eclipseCase(),
m_useDepthRange(),
m_depthRangeMin(),
m_depthRangeMax() );
// For each numeric parameter, compute Pearson correlation against pressurePerCase
for ( const auto& param : RimSummaryEnsembleTools::alphabeticEnsembleParameters( allCases ) )
{
if ( !param.isNumeric() ) continue;
if ( static_cast<size_t>( param.values.size() ) != allCases.size() ) continue;
std::vector<double> paramValues;
std::vector<double> pressureValues;
paramValues.reserve( allCases.size() );
pressureValues.reserve( allCases.size() );
for ( size_t i = 0; i < allCases.size(); ++i )
{
if ( std::isinf( pressurePerCase[i] ) ) continue;
paramValues.push_back( param.values[i].toDouble() );
pressureValues.push_back( pressurePerCase[i] );
}
if ( paramValues.size() < 2 ) continue;
double pearson = RigStatisticsTools::pearsonCorrelation( paramValues, pressureValues );
if ( std::isinf( pearson ) || std::isnan( pearson ) ) continue;
correlations[param.name] = pearson;
double value = m_showAbsoluteValues() ? std::abs( pearson ) : pearson;
double sortValue = m_sortByAbsoluteValues() ? std::abs( value ) : value;
// legendText becomes barChart->title() and is used for click-to-select; must equal param.name.
// barText is shown on the axis label and can include the correlation value.
QString axisLabel = QString( "%1 (%2)" ).arg( param.name ).arg( pearson, 5, 'f', 2 );
chartBuilder.addBarEntry( "", "", "", sortValue, param.name, axisLabel, value );
}
return correlations;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::highlightSelectedParameterBar()
{
if ( !m_plotWidget ) return;
const QColor highlightColor = RiaColorTools::toQColor( m_highlightBarColor() );
const QColor barColor = RiaColorTools::toQColor( m_barColor() );
for ( QwtPlotItem* item : m_plotWidget->qwtPlot()->itemList( QwtPlotItem::Rtti_PlotBarChart ) )
{
auto* barChart = static_cast<QwtPlotBarChart*>( item );
auto* symbol = const_cast<QwtColumnSymbol*>( barChart->symbol() );
if ( !symbol ) continue;
const QString paramName = barChart->title().text();
QColor color;
if ( paramName == m_selectedParameter )
{
color = highlightColor;
}
else
{
color = barColor;
}
QPalette palette = symbol->palette();
palette.setColor( QPalette::Window, color );
palette.setColor( QPalette::Dark, color );
symbol->setPalette( palette );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::updatePlotTitle()
{
if ( !m_plotWidget ) return;
if ( m_useAutoPlotTitle() && m_ensemble() )
{
const QString rangeStr = m_useDepthRange() ? QString( " [MD %1 - %2 m]" ).arg( m_depthRangeMin() ).arg( m_depthRangeMax() )
: QString();
m_description = QString( "Parameter Correlation vs RFT Pressure%1, %2" ).arg( rangeStr ).arg( m_ensemble->name() );
}
m_plotWidget->setPlotTitle( m_description() );
m_plotWidget->setPlotTitleEnabled( m_showPlotTitle() );
m_plotWidget->setPlotTitleFontSize( titleFontSize() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimRftTornadoPlot::cleanupBeforeClose()
{
detachAllCurves();
if ( m_plotWidget )
{
m_plotWidget->setParent( nullptr );
delete m_plotWidget;
m_plotWidget = nullptr;
}
}
@@ -0,0 +1,125 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimPlot.h"
#include "Appearance/RimFontSizeField.h"
#include "cafPdmField.h"
#include "cafPdmPtrField.h"
#include "cvfColor3.h"
#include <QDateTime>
#include <QPointer>
#include <functional>
#include <map>
class RimEclipseResultCase;
class RimSummaryEnsemble;
class RiuQwtPlotWidget;
//==================================================================================================
///
/// Tornado plot showing Pearson correlation between ensemble parameters and mean RFT pressure
/// within a depth range. Used as a child of RimRftCorrelationReportPlot.
///
//==================================================================================================
class RimRftTornadoPlot : public RimPlot
{
Q_OBJECT;
CAF_PDM_HEADER_INIT;
public:
RimRftTornadoPlot();
~RimRftTornadoPlot() override;
using ParameterSelectedCallback = std::function<void( const QString& )>;
void setParameterSelectedCallback( ParameterSelectedCallback callback );
// Inputs set by the parent report plot
void setEnsemble( RimSummaryEnsemble* ensemble );
void setWellName( const QString& wellName );
void setTimeStep( const QDateTime& timeStep );
void setEclipseCase( RimEclipseResultCase* eclipseCase );
void setUseDepthRange( bool useDepthRange );
void setDepthRange( double minMd, double maxMd );
void setSelectedParameter( const QString& paramName );
RiuQwtPlotWidget* viewer();
// RimPlot pure virtual overrides
RiuPlotWidget* plotWidget() override;
void setAutoScaleXEnabled( bool ) override {}
void setAutoScaleYEnabled( bool ) override {}
void updateAxes() override;
void updateLegend() override {}
QString asciiDataForPlotExport() const override;
void reattachAllCurves() override {}
void detachAllCurves() override;
QString description() const override;
private:
QWidget* viewWidget() override;
void zoomAll() override {}
void deleteViewWidget() override;
void doRenderWindowContent( QPaintDevice* paintDevice ) override;
RiuPlotWidget* doCreatePlotViewWidget( QWidget* parent = nullptr ) override;
void onLoadDataAndUpdate() override;
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
void onPlotItemSelected( std::shared_ptr<RiuPlotItem> plotItem, bool toggle, int sampleIndex ) override;
std::map<QString, double> addDataToChartBuilder( class RiuGroupedBarChartBuilder& chartBuilder ) const;
void highlightSelectedParameterBar();
void updatePlotTitle();
void cleanupBeforeClose();
private:
// Data source inputs
caf::PdmPtrField<RimSummaryEnsemble*> m_ensemble;
caf::PdmField<QString> m_wellName;
caf::PdmField<QDateTime> m_selectedTimeStep;
caf::PdmPtrField<RimEclipseResultCase*> m_eclipseCase;
caf::PdmField<bool> m_useDepthRange;
caf::PdmField<double> m_depthRangeMin;
caf::PdmField<double> m_depthRangeMax;
// Tornado settings
caf::PdmField<bool> m_showAbsoluteValues;
caf::PdmField<bool> m_sortByAbsoluteValues;
caf::PdmField<bool> m_showOnlyTopNCorrelations;
caf::PdmField<int> m_topNFilterCount;
caf::PdmField<cvf::Color3f> m_barColor;
caf::PdmField<cvf::Color3f> m_highlightBarColor;
caf::PdmField<bool> m_useAutoPlotTitle;
caf::PdmField<QString> m_description;
RimFontSizeField m_labelFontSize;
RimFontSizeField m_axisTitleFontSize;
RimFontSizeField m_axisValueFontSize;
ParameterSelectedCallback m_parameterSelectedCallback;
QString m_selectedParameter;
std::map<QString, double> m_lastCorrelations; // param.name -> pearson value, updated in onLoadDataAndUpdate
QPointer<RiuQwtPlotWidget> m_plotWidget;
};