diff --git a/ApplicationLibCode/Application/Tools/CMakeLists_files.cmake b/ApplicationLibCode/Application/Tools/CMakeLists_files.cmake index 20f855efee..711f1a1a41 100644 --- a/ApplicationLibCode/Application/Tools/CMakeLists_files.cmake +++ b/ApplicationLibCode/Application/Tools/CMakeLists_files.cmake @@ -52,6 +52,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RiaNetworkTools.h ${CMAKE_CURRENT_LIST_DIR}/RiaOpenMPTools.h ${CMAKE_CURRENT_LIST_DIR}/RiaNumericalTools.h + ${CMAKE_CURRENT_LIST_DIR}/RiaRegressionTextTools.h ) set(SOURCE_GROUP_SOURCE_FILES @@ -101,6 +102,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RiaNetworkTools.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaOpenMPTools.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaNumericalTools.cpp + ${CMAKE_CURRENT_LIST_DIR}/RiaRegressionTextTools.cpp ) list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES}) diff --git a/ApplicationLibCode/Application/Tools/RiaRegressionTextTools.cpp b/ApplicationLibCode/Application/Tools/RiaRegressionTextTools.cpp new file mode 100644 index 0000000000..b0756300a9 --- /dev/null +++ b/ApplicationLibCode/Application/Tools/RiaRegressionTextTools.cpp @@ -0,0 +1,116 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2023- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RiaRegressionTextTools.h" + +#include "ExponentialRegression.hpp" +#include "LinearRegression.hpp" +#include "LogarithmicRegression.hpp" +#include "LogisticRegression.hpp" +#include "PolynomialRegression.hpp" +#include "PowerFitRegression.hpp" + +#include + +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiaRegressionTextTools::generateRegressionText( const regression::LinearRegression& reg ) +{ + QString sign = reg.intercept() < 0.0 ? "-" : "+"; + return QString( "y = %1x %2 %3" ).arg( formatDouble( reg.slope() ) ).arg( sign ).arg( formatDouble( std::fabs( reg.intercept() ) ) ) + + QString( "
R2 = %1" ).arg( reg.r2() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiaRegressionTextTools::generateRegressionText( const regression::PolynomialRegression& reg ) +{ + QString str = "y = "; + + bool isFirst = true; + std::vector coeffs = reg.coeffisients(); + QStringList parts; + for ( size_t i = 0; i < coeffs.size(); i++ ) + { + double coeff = coeffs[i]; + // Skip zero coeffs + if ( coeff != 0.0 ) + { + if ( coeff < 0.0 ) + parts.append( "-" ); + else if ( !isFirst ) + parts.append( "+" ); + + if ( i == 0 ) + { + parts.append( QString( "%1" ).arg( formatDouble( std::fabs( coeff ) ) ) ); + } + else if ( i == 1 ) + { + parts.append( QString( "%1x" ).arg( formatDouble( std::fabs( coeff ) ) ) ); + } + else + { + parts.append( QString( " %1x%2" ).arg( formatDouble( std::fabs( coeff ) ) ).arg( i ) ); + } + + isFirst = false; + } + } + + return str + parts.join( " " ) + QString( "
R2 = %1" ).arg( reg.r2() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiaRegressionTextTools::generateRegressionText( const regression::PowerFitRegression& reg ) +{ + return QString( "y = %1 + x%2" ).arg( formatDouble( reg.scale() ) ).arg( formatDouble( reg.exponent() ) ) + + QString( "
R2 = %1" ).arg( reg.r2() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiaRegressionTextTools::generateRegressionText( const regression::ExponentialRegression& reg ) +{ + return QString( "y = %1 * e%2x" ).arg( formatDouble( reg.a() ) ).arg( formatDouble( reg.b() ) ) + + QString( "
R2 = %1" ).arg( reg.r2() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiaRegressionTextTools::generateRegressionText( const regression::LogarithmicRegression& reg ) +{ + return QString( "y = %1 + %2 * ln(x)" ).arg( formatDouble( reg.a() ) ).arg( formatDouble( reg.b() ) ) + + QString( "
R2 = %1" ).arg( reg.r2() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiaRegressionTextTools::formatDouble( double v ) +{ + return QString::number( v, 'g', 2 ); +} diff --git a/ApplicationLibCode/Application/Tools/RiaRegressionTextTools.h b/ApplicationLibCode/Application/Tools/RiaRegressionTextTools.h new file mode 100644 index 0000000000..39c386fe13 --- /dev/null +++ b/ApplicationLibCode/Application/Tools/RiaRegressionTextTools.h @@ -0,0 +1,46 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2023- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace regression +{ +class ExponentialRegression; +class LinearRegression; +class LogarithmicRegression; +class PolynomialRegression; +class PowerFitRegression; +} // namespace regression + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +class RiaRegressionTextTools +{ +public: + static QString generateRegressionText( const regression::LinearRegression& reg ); + static QString generateRegressionText( const regression::PolynomialRegression& reg ); + static QString generateRegressionText( const regression::PowerFitRegression& reg ); + static QString generateRegressionText( const regression::LogarithmicRegression& reg ); + static QString generateRegressionText( const regression::ExponentialRegression& reg ); + +private: + static QString formatDouble( double v ); +}; // namespace RiaRegressionTextTools diff --git a/ApplicationLibCode/ProjectDataModel/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/CMakeLists_files.cmake index 47004c9370..ad087f3367 100644 --- a/ApplicationLibCode/ProjectDataModel/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/CMakeLists_files.cmake @@ -132,6 +132,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RimPressureDepthData.h ${CMAKE_CURRENT_LIST_DIR}/RimEclipseResultDefinitionTools.h ${CMAKE_CURRENT_LIST_DIR}/RimResultSelectionUi.h + ${CMAKE_CURRENT_LIST_DIR}/RimPlotRectAnnotation.h ) set(SOURCE_GROUP_SOURCE_FILES @@ -263,6 +264,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimPressureDepthData.cpp ${CMAKE_CURRENT_LIST_DIR}/RimEclipseResultDefinitionTools.cpp ${CMAKE_CURRENT_LIST_DIR}/RimResultSelectionUi.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimPlotRectAnnotation.cpp ) if(RESINSIGHT_USE_QT_CHARTS) diff --git a/ApplicationLibCode/ProjectDataModel/GridCrossPlots/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/CMakeLists_files.cmake index aff355e8c0..d6742c68b9 100644 --- a/ApplicationLibCode/ProjectDataModel/GridCrossPlots/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/CMakeLists_files.cmake @@ -2,6 +2,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RimGridCrossPlot.h ${CMAKE_CURRENT_LIST_DIR}/RimGridCrossPlotCollection.h ${CMAKE_CURRENT_LIST_DIR}/RimGridCrossPlotCurve.h + ${CMAKE_CURRENT_LIST_DIR}/RimGridCrossPlotRegressionCurve.h ${CMAKE_CURRENT_LIST_DIR}/RimGridCrossPlotDataSet.h ${CMAKE_CURRENT_LIST_DIR}/RimSaturationPressurePlot.h ${CMAKE_CURRENT_LIST_DIR}/RimSaturationPressurePlotCollection.h @@ -11,6 +12,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimGridCrossPlot.cpp ${CMAKE_CURRENT_LIST_DIR}/RimGridCrossPlotCollection.cpp ${CMAKE_CURRENT_LIST_DIR}/RimGridCrossPlotCurve.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimGridCrossPlotRegressionCurve.cpp ${CMAKE_CURRENT_LIST_DIR}/RimGridCrossPlotDataSet.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSaturationPressurePlot.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSaturationPressurePlotCollection.cpp diff --git a/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlot.cpp b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlot.cpp index 3cb5695302..482e061f58 100644 --- a/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlot.cpp +++ b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlot.cpp @@ -210,6 +210,7 @@ void RimGridCrossPlot::reattachAllCurves() for ( auto dataSet : m_crossPlotDataSets ) { dataSet->detachAllCurves(); + dataSet->detachAllRegressionCurves(); if ( dataSet->isChecked() ) { dataSet->setParentPlotNoReplot( m_plotWidget ); @@ -336,6 +337,7 @@ void RimGridCrossPlot::detachAllCurves() for ( auto dataSet : m_crossPlotDataSets() ) { dataSet->detachAllCurves(); + dataSet->detachAllRegressionCurves(); } } @@ -1026,6 +1028,7 @@ void RimGridCrossPlot::cleanupBeforeClose() for ( auto dataSet : m_crossPlotDataSets() ) { dataSet->detachAllCurves(); + dataSet->detachAllRegressionCurves(); } if ( m_plotWidget ) diff --git a/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotDataSet.cpp b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotDataSet.cpp index 6b7a934e44..b3c4ee6a2e 100644 --- a/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotDataSet.cpp +++ b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotDataSet.cpp @@ -19,6 +19,7 @@ #include "RimGridCrossPlotDataSet.h" #include "RiaColorTables.h" +#include "RiaColorTools.h" #include "RiaLogging.h" #include "RifTextDataTableFormatter.h" @@ -30,12 +31,11 @@ #include "RigEclipseCaseData.h" #include "RigEclipseCrossPlotDataExtractor.h" #include "RigEclipseResultAddress.h" - #include "RigFormationNames.h" #include "RigMainGrid.h" #include "RiuDraggableOverlayFrame.h" -#include "RiuGridCrossQwtPlot.h" +#include "RiuGuiTheme.h" #include "RiuPlotWidget.h" #include "RiuScalarMapperLegendFrame.h" @@ -48,6 +48,7 @@ #include "RimFormationNames.h" #include "RimGridCrossPlot.h" #include "RimGridCrossPlotCurve.h" +#include "RimGridCrossPlotRegressionCurve.h" #include "RimGridView.h" #include "RimProject.h" #include "RimRegularLegendConfig.h" @@ -63,9 +64,6 @@ #include "cafProgressInfo.h" #include "cafTitledOverlayFrame.h" #include "cvfScalarMapper.h" -#include "cvfqtUtils.h" - -#include "qwt_plot.h" #include @@ -129,6 +127,9 @@ RimGridCrossPlotDataSet::RimGridCrossPlotDataSet() CAF_PDM_InitFieldNoDefault( &m_crossPlotCurves, "CrossPlotCurves", "Curves" ); m_crossPlotCurves.uiCapability()->setUiTreeHidden( true ); + CAF_PDM_InitFieldNoDefault( &m_crossPlotRegressionCurves, "CrossPlotRegressionCurves", "Regression Curves" ); + // m_crossPlotRegressionCurves.uiCapability()->setUiTreeHidden( true ); + CAF_PDM_InitField( &m_useCustomColor, "UseCustomColor", false, "Use Custom Color" ); CAF_PDM_InitField( &m_customColor, "CustomColor", cvf::Color3f( cvf::Color3f::BLACK ), "Custom Color" ); @@ -159,43 +160,42 @@ RimGridCrossPlotDataSet::~RimGridCrossPlotDataSet() void RimGridCrossPlotDataSet::setCellFilterView( RimGridView* cellFilterView ) { m_cellFilterView = cellFilterView; + auto eclipseView = dynamic_cast( m_cellFilterView() ); + if ( !eclipseView ) return; - if ( eclipseView ) + m_groupingProperty->setReservoirView( eclipseView ); + RigEclipseResultAddress resAddr = eclipseView->cellResult()->eclipseResultAddress(); + if ( resAddr.isValid() ) { - m_groupingProperty->setReservoirView( eclipseView ); - RigEclipseResultAddress resAddr = eclipseView->cellResult()->eclipseResultAddress(); - if ( resAddr.isValid() ) + m_grouping = NO_GROUPING; + + RimEclipseCase* eclipseCase = eclipseView->eclipseCase(); + if ( eclipseCase ) { - m_grouping = NO_GROUPING; + m_case = eclipseCase; + m_xAxisProperty->setEclipseCase( eclipseCase ); + m_yAxisProperty->setEclipseCase( eclipseCase ); + m_groupingProperty->setEclipseCase( eclipseCase ); - RimEclipseCase* eclipseCase = eclipseView->eclipseCase(); - if ( eclipseCase ) + if ( eclipseCase->activeFormationNames() ) { - m_case = eclipseCase; - m_xAxisProperty->setEclipseCase( eclipseCase ); - m_yAxisProperty->setEclipseCase( eclipseCase ); - m_groupingProperty->setEclipseCase( eclipseCase ); - - if ( eclipseCase->activeFormationNames() ) - { - m_grouping = GROUP_BY_FORMATION; - m_groupingProperty->legendConfig()->setColorLegend( - RimRegularLegendConfig::mapToColorLegend( RimRegularLegendConfig::ColorRangesType::CATEGORY ) ); - } + m_grouping = GROUP_BY_FORMATION; + m_groupingProperty->legendConfig()->setColorLegend( + RimRegularLegendConfig::mapToColorLegend( RimRegularLegendConfig::ColorRangesType::CATEGORY ) ); } + } - m_xAxisProperty->setResultType( resAddr.resultCatType() ); - m_xAxisProperty->setResultVariable( resAddr.resultName() ); - m_yAxisProperty->setResultType( RiaDefines::ResultCatType::STATIC_NATIVE ); - m_yAxisProperty->setResultVariable( "DEPTH" ); - m_timeStep = eclipseView->currentTimeStep(); + m_xAxisProperty->setResultType( resAddr.resultCatType() ); + m_xAxisProperty->setResultVariable( resAddr.resultName() ); + m_yAxisProperty->setResultType( RiaDefines::ResultCatType::STATIC_NATIVE ); + m_yAxisProperty->setResultVariable( "DEPTH" ); + m_timeStep = eclipseView->currentTimeStep(); - auto parentPlot = firstAncestorOrThisOfType(); - if ( parentPlot ) - { - parentPlot->setYAxisInverted( true ); - } + auto parentPlot = firstAncestorOrThisOfType(); + if ( parentPlot ) + { + parentPlot->setYAxisInverted( true ); } } } @@ -217,6 +217,11 @@ void RimGridCrossPlotDataSet::setParentPlotNoReplot( RiuPlotWidget* parent ) { curve->setParentPlotNoReplot( m_isChecked() ? parent : nullptr ); } + + for ( auto& curve : m_crossPlotRegressionCurves() ) + { + curve->setParentPlotNoReplot( m_isChecked() ? parent : nullptr ); + } } //-------------------------------------------------------------------------------------------------- @@ -246,7 +251,7 @@ QString RimGridCrossPlotDataSet::infoText() const QStringList textLines; textLines += QString( "Case: %1" ).arg( m_case()->caseUserDescription() ); - textLines += QString( "Parameters:: %1 x %2" ) + textLines += QString( "Parameters: %1 x %2" ) .arg( m_xAxisProperty->resultVariableUiShortName() ) .arg( m_yAxisProperty->resultVariableUiShortName() ); if ( m_timeStep != -1 ) @@ -359,6 +364,17 @@ void RimGridCrossPlotDataSet::detachAllCurves() } } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotDataSet::detachAllRegressionCurves() +{ + for ( auto curve : m_crossPlotRegressionCurves() ) + { + curve->detach(); + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -525,6 +541,20 @@ void RimGridCrossPlotDataSet::onLoadDataAndUpdate( bool updateParentPlot ) fillCurveDataInExistingCurves( result ); } + if ( m_crossPlotRegressionCurves.size() != m_groupedResults.size() ) + { + destroyRegressionCurves(); + } + + if ( m_crossPlotRegressionCurves.empty() ) + { + createRegressionCurves( result ); + } + else + { + fillCurveDataInExistingRegressionCurves( result ); + } + updateLegendIcons(); if ( updateParentPlot ) @@ -591,6 +621,44 @@ void RimGridCrossPlotDataSet::assignCurveDataGroups( const RigEclipseCrossPlotRe } } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +cvf::Color3f RimGridCrossPlotDataSet::createCurveColor( bool useCustomColor, int colorIndex ) const +{ + if ( useCustomColor ) + { + return m_customColor(); + } + else + { + const caf::ColorTable& colors = RiaColorTables::contrastCategoryPaletteColors(); + return colors.cycledColor3f( colorIndex ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +cvf::Color3f RimGridCrossPlotDataSet::createCurveColor( const std::vector& tickValues, int groupIndex ) const +{ + if ( tickValues.empty() ) + { + return cvf::Color3f( legendConfig()->scalarMapper()->mapToColor( groupIndex ) ); + } + else + { + if ( groupIndex < static_cast( tickValues.size() ) ) + { + return cvf::Color3f( legendConfig()->scalarMapper()->mapToColor( tickValues[groupIndex] ) ); + } + else + { + return cvf::Color3f( legendConfig()->scalarMapper()->mapToColor( groupIndex ) ); + } + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -599,16 +667,7 @@ void RimGridCrossPlotDataSet::createCurves( const RigEclipseCrossPlotResult& res if ( !groupingEnabled() ) { RimGridCrossPlotCurve* curve = new RimGridCrossPlotCurve(); - if ( m_useCustomColor ) - { - curve->setColor( m_customColor() ); - } - else - { - const caf::ColorTable& colors = RiaColorTables::contrastCategoryPaletteColors(); - int colorIndex = indexInPlot(); - curve->setColor( colors.cycledColor3f( colorIndex ) ); - } + curve->setColor( createCurveColor( m_useCustomColor(), indexInPlot() ) ); curve->setSymbolEdgeColor( curve->color() ); curve->setGroupingInformation( indexInPlot(), 0 ); curve->setSamples( result.xValues, result.yValues ); @@ -628,25 +687,12 @@ void RimGridCrossPlotDataSet::createCurves( const RigEclipseCrossPlotResult& res // NB : Make sure iteration of curve and groups are syncronized with createCurves() for ( auto it = m_groupedResults.rbegin(); it != m_groupedResults.rend(); ++it ) { + auto [groupIndex, values] = *it; RimGridCrossPlotCurve* curve = new RimGridCrossPlotCurve(); - curve->setGroupingInformation( indexInPlot(), it->first ); - if ( groupingByCategoryResult() ) - { - curve->setColor( cvf::Color3f( legendConfig()->scalarMapper()->mapToColor( it->first ) ) ); - } - else - { - if ( it->first < static_cast( tickValues.size() ) ) - { - curve->setColor( cvf::Color3f( legendConfig()->scalarMapper()->mapToColor( tickValues[it->first] ) ) ); - } - else - { - curve->setColor( cvf::Color3f( legendConfig()->scalarMapper()->mapToColor( it->first ) ) ); - } - } + curve->setGroupingInformation( indexInPlot(), groupIndex ); + curve->setColor( createCurveColor( tickValues, groupIndex ) ); curve->setSymbolEdgeColor( curve->color() ); - curve->setSamples( it->second.xValues, it->second.yValues ); + curve->setSamples( values.xValues, values.yValues ); curve->setShowInLegend( m_crossPlotCurves.empty() ); curve->setLegendEntryText( createAutoName() ); curve->setCurveAutoAppearance(); @@ -688,6 +734,88 @@ void RimGridCrossPlotDataSet::fillCurveDataInExistingCurves( const RigEclipseCro } } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotDataSet::createRegressionCurves( const RigEclipseCrossPlotResult& result ) +{ + auto symbolEdgeColor = RiaColorTools::fromQColorTo3f( RiuGuiTheme::getColorByVariableName( "auxilliaryCurveColor" ) ); + + if ( !groupingEnabled() ) + { + RimGridCrossPlotRegressionCurve* curve = new RimGridCrossPlotRegressionCurve(); + m_crossPlotRegressionCurves.push_back( curve ); + curve->setColor( createCurveColor( m_useCustomColor(), indexInPlot() ) ); + curve->setSymbolEdgeColor( symbolEdgeColor ); + curve->setGroupingInformation( indexInPlot(), 0 ); + curve->setRangeDefaults( result.xValues, result.yValues ); + curve->setSamples( result.xValues, result.yValues ); + curve->setCurveAutoAppearance(); + curve->updateUiIconFromPlotSymbol(); + } + else + { + std::vector tickValues; + + if ( !groupingByCategoryResult() ) + { + legendConfig()->scalarMapper()->majorTickValues( &tickValues ); + } + + // NB : Make sure iteration of curve and groups are syncronized with createCurves() + for ( auto it = m_groupedResults.rbegin(); it != m_groupedResults.rend(); ++it ) + { + auto [groupIndex, values] = *it; + + bool isFirst = m_crossPlotRegressionCurves.empty(); + + RimGridCrossPlotRegressionCurve* curve = new RimGridCrossPlotRegressionCurve(); + m_crossPlotRegressionCurves.push_back( curve ); + curve->setGroupingInformation( indexInPlot(), groupIndex ); + curve->setColor( createCurveColor( tickValues, groupIndex ) ); + curve->setSymbolEdgeColor( symbolEdgeColor ); + curve->setRangeDefaults( values.xValues, values.yValues ); + curve->setSamples( values.xValues, values.yValues ); + curve->setShowInLegend( isFirst ); + curve->setLegendEntryText( createAutoName() ); + curve->setCurveAutoAppearance(); + curve->updateUiIconFromPlotSymbol(); + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotDataSet::fillCurveDataInExistingRegressionCurves( const RigEclipseCrossPlotResult& result ) +{ + if ( !groupingEnabled() ) + { + CVF_ASSERT( m_crossPlotRegressionCurves.size() == 1u ); + RimGridCrossPlotRegressionCurve* curve = m_crossPlotRegressionCurves[0]; + curve->setGroupingInformation( indexInPlot(), 0 ); + curve->updateCurveVisibility(); + curve->setSamples( result.xValues, result.yValues ); + curve->updateCurveAppearance(); + curve->updateUiIconFromPlotSymbol(); + } + else + { + // NB : Make sure iteration of curve and groups are syncronized with fillCurveDataInExistingCurves() + auto curveIt = m_crossPlotRegressionCurves.begin(); + auto groupIt = m_groupedResults.rbegin(); + for ( ; curveIt != m_crossPlotRegressionCurves.end() && groupIt != m_groupedResults.rend(); ++curveIt, ++groupIt ) + { + RimGridCrossPlotRegressionCurve* curve = *curveIt; + curve->setGroupingInformation( indexInPlot(), groupIt->first ); + curve->updateCurveVisibility(); + curve->setSamples( groupIt->second.xValues, groupIt->second.yValues ); + curve->updateCurveAppearance(); + curve->updateUiIconFromPlotSymbol(); + } + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -697,6 +825,15 @@ void RimGridCrossPlotDataSet::destroyCurves() m_crossPlotCurves.deleteChildren(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotDataSet::destroyRegressionCurves() +{ + detachAllRegressionCurves(); + m_crossPlotRegressionCurves.deleteChildren(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -882,6 +1019,7 @@ void RimGridCrossPlotDataSet::fieldChangedByUi( const caf::PdmFieldHandle* chang } destroyCurves(); + destroyRegressionCurves(); loadDataAndUpdate( true ); } } @@ -893,6 +1031,7 @@ void RimGridCrossPlotDataSet::fieldChangedByUi( const caf::PdmFieldHandle* chang } destroyCurves(); + destroyRegressionCurves(); loadDataAndUpdate( true ); } else if ( changedField == &m_grouping ) @@ -914,6 +1053,7 @@ void RimGridCrossPlotDataSet::fieldChangedByUi( const caf::PdmFieldHandle* chang } destroyCurves(); + destroyRegressionCurves(); loadDataAndUpdate( true ); } else if ( changedField == &m_cellFilterView ) @@ -936,12 +1076,17 @@ void RimGridCrossPlotDataSet::childFieldChangedByUi( const caf::PdmFieldHandle* { if ( changedChildField == &m_yAxisProperty ) { - if ( m_yAxisProperty->resultVariable() == "DEPTH" ) - { - auto plot = firstAncestorOrThisOfTypeAsserted(); - plot->setYAxisInverted( true ); - triggerPlotNameUpdateAndReplot(); - } + bool useInvertedYAxis = m_yAxisProperty->resultVariable() == "DEPTH"; + auto plot = firstAncestorOrThisOfTypeAsserted(); + plot->setYAxisInverted( useInvertedYAxis ); + triggerPlotNameUpdateAndReplot(); + } + + if ( changedChildField == &m_xAxisProperty || changedChildField == &m_yAxisProperty ) + { + destroyCurves(); + destroyRegressionCurves(); + loadDataAndUpdate( true ); } else if ( changedChildField == &m_crossPlotCurves ) { @@ -1125,6 +1270,12 @@ void RimGridCrossPlotDataSet::swapAxisProperties( bool updatePlot ) m_xAxisProperty = yAxisProperties; updateConnectedEditors(); + + for ( auto curve : m_crossPlotRegressionCurves ) + { + curve->swapAxis(); + } + loadDataAndUpdate( updatePlot ); } @@ -1288,6 +1439,32 @@ void RimGridCrossPlotDataSet::updateCurveNames( size_t dataSetIndex, size_t data } curve->updateCurveNameAndUpdatePlotLegendAndTitle(); } + + for ( size_t i = 0; i < m_crossPlotRegressionCurves.size(); ++i ) + { + QString dataSetName = createAutoName(); + if ( dataSetName.isEmpty() ) + { + if ( dataSetCount > 1u ) + dataSetName = QString( "DataSet #%1" ).arg( dataSetIndex + 1 ); + else + dataSetName = "Data Set"; + } + + auto curve = m_crossPlotRegressionCurves[i]; + if ( groupingEnabled() ) + { + QString curveGroupName = createGroupName( curve->groupIndex() ); + curve->setCustomName( curveGroupName + " " + curve->getRegressionTypeString() ); + curve->setLegendEntryText( dataSetName ); + } + else + { + curve->setCustomName( dataSetName + " " + curve->getRegressionTypeString() ); + } + + curve->updateCurveNameAndUpdatePlotLegendAndTitle(); + } } //-------------------------------------------------------------------------------------------------- @@ -1313,29 +1490,26 @@ void RimGridCrossPlotDataSet::performAutoNameUpdate() void RimGridCrossPlotDataSet::setDefaults() { RimProject* project = RimProject::current(); - if ( project ) + if ( project && !project->eclipseCases().empty() ) { - if ( !project->eclipseCases().empty() ) + RimEclipseCase* eclipseCase = project->eclipseCases().front(); + m_case = eclipseCase; + m_xAxisProperty->setEclipseCase( eclipseCase ); + m_yAxisProperty->setEclipseCase( eclipseCase ); + m_groupingProperty->setEclipseCase( eclipseCase ); + + m_xAxisProperty->setResultType( RiaDefines::ResultCatType::STATIC_NATIVE ); + m_xAxisProperty->setResultVariable( "PORO" ); + + m_yAxisProperty->setResultType( RiaDefines::ResultCatType::STATIC_NATIVE ); + m_yAxisProperty->setResultVariable( "PERMX" ); + + m_grouping = NO_GROUPING; + if ( eclipseCase->activeFormationNames() ) { - RimEclipseCase* eclipseCase = project->eclipseCases().front(); - m_case = eclipseCase; - m_xAxisProperty->setEclipseCase( eclipseCase ); - m_yAxisProperty->setEclipseCase( eclipseCase ); - m_groupingProperty->setEclipseCase( eclipseCase ); - - m_xAxisProperty->setResultType( RiaDefines::ResultCatType::STATIC_NATIVE ); - m_xAxisProperty->setResultVariable( "PORO" ); - - m_yAxisProperty->setResultType( RiaDefines::ResultCatType::STATIC_NATIVE ); - m_yAxisProperty->setResultVariable( "PERMX" ); - - m_grouping = NO_GROUPING; - if ( eclipseCase->activeFormationNames() ) - { - m_grouping = GROUP_BY_FORMATION; - m_groupingProperty->legendConfig()->setColorLegend( - RimRegularLegendConfig::mapToColorLegend( RimRegularLegendConfig::ColorRangesType::CATEGORY ) ); - } + m_grouping = GROUP_BY_FORMATION; + m_groupingProperty->legendConfig()->setColorLegend( + RimRegularLegendConfig::mapToColorLegend( RimRegularLegendConfig::ColorRangesType::CATEGORY ) ); } } } @@ -1362,6 +1536,8 @@ void RimGridCrossPlotDataSet::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTr uiTreeOrdering.add( curve ); } + uiTreeOrdering.add( &m_crossPlotRegressionCurves ); + uiTreeOrdering.add( &m_plotCellFilterCollection ); uiTreeOrdering.skipRemainingChildren( true ); diff --git a/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotDataSet.h b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotDataSet.h index 9c96b2eafd..a0cb226a6c 100644 --- a/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotDataSet.h +++ b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotDataSet.h @@ -42,6 +42,7 @@ class RifTextDataTableFormatter; class RimCase; class RimGridCrossPlotCurve; +class RimGridCrossPlotRegressionCurve; class RimGridView; class RimEclipseCase; class RimEclipseResultCase; @@ -110,6 +111,7 @@ public: QString groupTitle() const; QString groupParameter() const; void detachAllCurves(); + void detachAllRegressionCurves(); void cellFilterViewUpdated(); RimRegularLegendConfig* legendConfig() const; @@ -138,6 +140,8 @@ public: void setCustomColor( const cvf::Color3f color ); void destroyCurves(); + void destroyRegressionCurves(); + size_t visibleCurveCount() const; size_t sampleCount() const; @@ -149,6 +153,8 @@ protected: void createCurves( const RigEclipseCrossPlotResult& result ); void fillCurveDataInExistingCurves( const RigEclipseCrossPlotResult& result ); QString createGroupName( size_t curveIndex ) const; + void createRegressionCurves( const RigEclipseCrossPlotResult& result ); + void fillCurveDataInExistingRegressionCurves( const RigEclipseCrossPlotResult& result ); std::map calculateCellVisibility( RimEclipseCase* eclipseCase ) const; @@ -168,6 +174,9 @@ protected: bool hasMultipleTimeSteps() const; void filterInvalidCurveValues( RigEclipseCrossPlotResult* result ); + cvf::Color3f createCurveColor( bool useCustomColor, int colorIndex ) const; + cvf::Color3f createCurveColor( const std::vector& tickValues, int groupIndex ) const; + private: caf::PdmPtrField m_case; caf::PdmField m_timeStep; @@ -179,7 +188,8 @@ private: caf::PdmChildField m_nameConfig; - caf::PdmChildArrayField m_crossPlotCurves; + caf::PdmChildArrayField m_crossPlotCurves; + caf::PdmChildArrayField m_crossPlotRegressionCurves; std::map m_groupedResults; diff --git a/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotRegressionCurve.cpp b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotRegressionCurve.cpp new file mode 100644 index 0000000000..4a25a78e3d --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotRegressionCurve.cpp @@ -0,0 +1,546 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2023- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimGridCrossPlotRegressionCurve.h" + +#include "RiaNumericalTools.h" +#include "RiaRegressionTextTools.h" + +#include "RimGridCrossPlot.h" +#include "RimGridCrossPlotDataSet.h" +#include "RimPlotRectAnnotation.h" + +#include "RimPlotRectAnnotation.h" +#include "RiuPlotCurve.h" +#include "RiuPlotWidget.h" + +#include "ExponentialRegression.hpp" +#include "LinearRegression.hpp" +#include "LogarithmicRegression.hpp" +#include "PolynomialRegression.hpp" +#include "PowerFitRegression.hpp" + +#include "cafPdmUiDoubleSliderEditor.h" +#include "cafPdmUiDoubleValueEditor.h" +#include "cafPdmUiLineEditor.h" +#include "cafPdmUiTextEditor.h" + +#include "cvfMath.h" + +CAF_PDM_SOURCE_INIT( RimGridCrossPlotRegressionCurve, "GridCrossPlotRegressionCurve" ); + +namespace caf +{ +template <> +void caf::AppEnum::setUp() +{ + addItem( RimGridCrossPlotRegressionCurve::RegressionType::LINEAR, "LINEAR", "Linear" ); + addItem( RimGridCrossPlotRegressionCurve::RegressionType::POLYNOMIAL, "POLYNOMIAL", "Polynomial" ); + addItem( RimGridCrossPlotRegressionCurve::RegressionType::POWER_FIT, "POWER_FIT", "Power Fit" ); + addItem( RimGridCrossPlotRegressionCurve::RegressionType::EXPONENTIAL, "EXPONENTIAL", "Exponential" ); + addItem( RimGridCrossPlotRegressionCurve::RegressionType::LOGARITHMIC, "LOGARITHMIC", "Logarithmic" ); + setDefault( RimGridCrossPlotRegressionCurve::RegressionType::LINEAR ); +} +}; // namespace caf + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimGridCrossPlotRegressionCurve::RimGridCrossPlotRegressionCurve() + : m_dataSetIndex( 0 ) + , m_groupIndex( 0 ) +{ + CAF_PDM_InitObject( "Cross Plot Regression Curve", ":/WellLogCurve16x16.png" ); + + CAF_PDM_InitFieldNoDefault( &m_regressionType, "RegressionType", "Type" ); + + CAF_PDM_InitFieldNoDefault( &m_minExtrapolationRangeX, "MinExtrapolationRangeX", "Min" ); + m_minExtrapolationRangeX.uiCapability()->setUiEditorTypeName( caf::PdmUiDoubleValueEditor::uiEditorTypeName() ); + + CAF_PDM_InitFieldNoDefault( &m_maxExtrapolationRangeX, "MaxExtrapolationRangeX", "Max" ); + m_maxExtrapolationRangeX.uiCapability()->setUiEditorTypeName( caf::PdmUiDoubleValueEditor::uiEditorTypeName() ); + + CAF_PDM_InitField( &m_polynomialDegree, "PolynomialDegree", 3, "Degree" ); + + CAF_PDM_InitFieldNoDefault( &m_minRangeX, "MinRangeX", "Min X" ); + m_minRangeX.uiCapability()->setUiEditorTypeName( caf::PdmUiDoubleSliderEditor::uiEditorTypeName() ); + + CAF_PDM_InitFieldNoDefault( &m_maxRangeX, "MaxRangeX", "Max X" ); + m_maxRangeX.uiCapability()->setUiEditorTypeName( caf::PdmUiDoubleSliderEditor::uiEditorTypeName() ); + + CAF_PDM_InitFieldNoDefault( &m_minRangeY, "MinRangeY", "Min Y" ); + m_minRangeY.uiCapability()->setUiEditorTypeName( caf::PdmUiDoubleSliderEditor::uiEditorTypeName() ); + + CAF_PDM_InitFieldNoDefault( &m_maxRangeY, "MaxRangeY", "Max Y" ); + m_maxRangeY.uiCapability()->setUiEditorTypeName( caf::PdmUiDoubleSliderEditor::uiEditorTypeName() ); + + CAF_PDM_InitField( &m_showDataSelectionInPlot, "ShowDataSelectionInPlot", false, "Show In Plot" ); + + CAF_PDM_InitFieldNoDefault( &m_expressionText, "ExpressionText", "Expression" ); + m_expressionText.uiCapability()->setUiEditorTypeName( caf::PdmUiTextEditor::uiEditorTypeName() ); + m_expressionText.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::HIDDEN ); + m_expressionText.uiCapability()->setUiReadOnly( true ); + m_expressionText.xmlCapability()->disableIO(); + + setLineStyle( RiuQwtPlotCurveDefines::LineStyleEnum::STYLE_SOLID ); + setSymbol( RiuPlotCurveSymbol::SYMBOL_RECT ); + setSymbolSize( 6 ); + setZOrder( RiuQwtPlotCurveDefines::zDepthForIndex( RiuQwtPlotCurveDefines::ZIndex::Z_REGRESSION_CURVE ) ); + + m_dataRangeX = { cvf::UNDEFINED_DOUBLE, cvf::UNDEFINED_DOUBLE }; + m_dataRangeY = { cvf::UNDEFINED_DOUBLE, cvf::UNDEFINED_DOUBLE }; + + auto rectAnnotation = new RimPlotRectAnnotation; + rectAnnotation->setName( "Data Selection" ); + m_rectAnnotations.push_back( rectAnnotation ); + m_rectAnnotations.uiCapability()->setUiTreeHidden( true ); + m_rectAnnotations.uiCapability()->setUiTreeChildrenHidden( true ); + + setCheckState( false ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::setGroupingInformation( int dataSetIndex, int groupIndex ) +{ + m_dataSetIndex = dataSetIndex; + m_groupIndex = groupIndex; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::setSamples( const std::vector& xValues, const std::vector& yValues ) +{ + CVF_ASSERT( xValues.size() == yValues.size() ); + + if ( xValues.empty() || yValues.empty() || !m_plotCurve ) return; + + auto [minX, maxX] = minmax_element( xValues.begin(), xValues.end() ); + auto [minY, maxY] = minmax_element( yValues.begin(), yValues.end() ); + + m_dataRangeX = { *minX, *maxX }; + m_dataRangeY = { *minY, *maxY }; + + auto filterValues = []( const std::vector& x, const std::vector& y, double minX, double maxX, double minY, double maxY ) + { + std::vector filteredX; + std::vector filteredY; + + for ( size_t i = 0; i < x.size(); i++ ) + { + if ( x[i] >= minX && x[i] <= maxX && y[i] >= minY && y[i] <= maxY ) + { + filteredX.push_back( x[i] ); + filteredY.push_back( y[i] ); + } + } + + return std::make_pair( filteredX, filteredY ); + }; + + auto [filteredX, filteredY] = filterValues( xValues, yValues, m_minRangeX, m_maxRangeX, m_minRangeY, m_maxRangeY ); + if ( filteredX.empty() || filteredX.size() != filteredY.size() ) return; + + auto subsampleValues = []( double min, double max, int numSamples ) + { + double step = ( max - min ) / numSamples; + + std::vector subSampledRange( numSamples ); + for ( int i = 0; i < numSamples; i++ ) + subSampledRange[i] = min + step * i; + subSampledRange.push_back( max ); + + return subSampledRange; + }; + + std::vector subsampledXValues = subsampleValues( m_minExtrapolationRangeX, m_maxExtrapolationRangeX, 50 ); + + auto [outputXValues, outputYValues, regressionText] = calculateRegression( m_regressionType(), filteredX, filteredY, subsampledXValues ); + + m_expressionText = regressionText; + + bool useLogarithmicScale = false; + m_plotCurve->setSamplesFromXValuesAndYValues( outputXValues, outputYValues, useLogarithmicScale ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::setRangeDefaults( const std::vector& xValues, const std::vector& yValues ) +{ + CVF_ASSERT( xValues.size() == yValues.size() ); + + if ( xValues.empty() || yValues.empty() ) return; + + auto [minX, maxX] = minmax_element( xValues.begin(), xValues.end() ); + auto [minY, maxY] = minmax_element( yValues.begin(), yValues.end() ); + + m_minRangeX = *minX; + m_maxRangeX = *maxX; + + m_minRangeY = *minY; + m_maxRangeY = *maxY; + + m_maxExtrapolationRangeX = *maxX; + m_minExtrapolationRangeX = *minX; + + m_dataRangeX = { *minX, *maxX }; + m_dataRangeY = { *minY, *maxY }; + + updateRectAnnotation(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::swapAxis() +{ + std::swap( m_minRangeX, m_minRangeY ); + std::swap( m_maxRangeX, m_maxRangeY ); + + m_maxExtrapolationRangeX = m_maxRangeX; + m_minExtrapolationRangeX = m_minRangeX; + + std::swap( m_dataRangeX, m_dataRangeY ); + + updateRectAnnotation(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::setCurveAutoAppearance() +{ + updateCurveAppearance(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +int RimGridCrossPlotRegressionCurve::groupIndex() const +{ + return m_groupIndex; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +size_t RimGridCrossPlotRegressionCurve::sampleCount() const +{ + return m_plotCurve ? m_plotCurve->numSamples() : 0; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::determineLegendIcon() +{ + if ( !m_plotCurve ) return; + + auto plot = firstAncestorOrThisOfTypeAsserted(); + int fontSize = plot->legendFontSize(); + m_plotCurve->setLegendIconSize( QSize( fontSize, fontSize ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::setBlackAndWhiteLegendIcons( bool blackAndWhite ) +{ + if ( m_plotCurve ) + { + m_plotCurve->setBlackAndWhiteLegendIcon( blackAndWhite ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::updateZoomInParentPlot() +{ + auto plot = firstAncestorOrThisOfTypeAsserted(); + plot->calculateZoomRangeAndUpdateQwt(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimGridCrossPlotRegressionCurve::createCurveAutoName() +{ + return m_curveName; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimGridCrossPlotRegressionCurve::getRegressionTypeString() const +{ + return m_regressionType().uiText() + " Regression"; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::onLoadDataAndUpdate( bool updateParentPlot ) +{ + if ( updateParentPlot ) + { + m_parentPlot->replot(); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + caf::PdmUiGroup* regressionCurveGroup = uiOrdering.addNewGroup( "Regression Analysis" ); + regressionCurveGroup->add( &m_regressionType ); + + if ( m_regressionType == RegressionType::POLYNOMIAL ) + { + regressionCurveGroup->add( &m_polynomialDegree ); + } + + regressionCurveGroup->add( &m_expressionText ); + + caf::PdmUiGroup* dataSelectionGroup = uiOrdering.addNewGroup( "Data Selection" ); + dataSelectionGroup->add( &m_minRangeX ); + dataSelectionGroup->add( &m_maxRangeX ); + dataSelectionGroup->add( &m_minRangeY ); + dataSelectionGroup->add( &m_maxRangeY ); + dataSelectionGroup->add( &m_showDataSelectionInPlot ); + + caf::PdmUiGroup* forecastingGroup = uiOrdering.addNewGroup( "Extrapolation" ); + forecastingGroup->add( &m_minExtrapolationRangeX ); + forecastingGroup->add( &m_maxExtrapolationRangeX ); + + caf::PdmUiGroup* appearanceGroup = uiOrdering.addNewGroup( "Appearance" ); + RimPlotCurve::appearanceUiOrdering( *appearanceGroup ); + + uiOrdering.skipRemainingFields( true ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) +{ + // Implement an empty method to avoid the base class implementation in RimPlotCurve + // The color tag is not used for Grid Cross Plot Curves +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::defineEditorAttribute( const caf::PdmFieldHandle* field, + QString uiConfigName, + caf::PdmUiEditorAttribute* attribute ) +{ + if ( field == &m_polynomialDegree ) + { + if ( auto* lineEditorAttr = dynamic_cast( attribute ) ) + { + // Polynomial degree should be a positive number. + lineEditorAttr->validator = new QIntValidator( 1, 50, nullptr ); + } + } + else if ( field == &m_minRangeX || field == &m_maxRangeX ) + { + if ( auto* myAttr = dynamic_cast( attribute ) ) + { + auto [min, max] = m_dataRangeX; + myAttr->m_minimum = RiaNumericalTools::roundToNumSignificantDigits( min, 2 ); + myAttr->m_maximum = RiaNumericalTools::roundToNumSignificantDigits( max, 2 ); + myAttr->m_decimals = 3; + } + } + else if ( field == &m_minRangeY || field == &m_maxRangeY ) + { + if ( auto* myAttr = dynamic_cast( attribute ) ) + { + auto [min, max] = m_dataRangeY; + myAttr->m_minimum = RiaNumericalTools::roundToNumSignificantDigits( min, 2 ); + myAttr->m_maximum = RiaNumericalTools::roundToNumSignificantDigits( max, 2 ); + myAttr->m_decimals = 3; + } + } + else if ( field == &m_minExtrapolationRangeX || field == &m_maxExtrapolationRangeX ) + { + auto doubleAttr = dynamic_cast( attribute ); + if ( doubleAttr ) + { + doubleAttr->m_decimals = 2; + doubleAttr->m_numberFormat = caf::PdmUiDoubleValueEditorAttribute::NumberFormat::FIXED; + } + } + + else if ( field == &m_expressionText ) + { + auto myAttr = dynamic_cast( attribute ); + if ( myAttr ) + { + myAttr->wrapMode = caf::PdmUiTextEditorAttribute::NoWrap; + myAttr->textMode = caf::PdmUiTextEditorAttribute::HTML; + + QFont font; + auto pointSize = font.pointSize(); + font.setPointSize( pointSize + 2 ); + myAttr->font = font; + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) +{ + if ( &m_showCurve == changedField ) + { + // RimPlotCurve::fieldChangedByUi always replot, and this is usually unnecessary except for visibility changes + RimPlotCurve::fieldChangedByUi( changedField, oldValue, newValue ); + } + + auto enforceRange = []( const caf::PdmFieldHandle* changedField, caf::PdmField& minRange, caf::PdmField& maxRange ) + { + if ( &minRange == changedField && minRange > maxRange ) maxRange = minRange; + if ( &maxRange == changedField && maxRange < minRange ) minRange = maxRange; + }; + + enforceRange( changedField, m_minRangeX, m_maxRangeX ); + enforceRange( changedField, m_minRangeY, m_maxRangeY ); + + if ( &m_minRangeX == changedField || &m_maxRangeX == changedField || &m_minRangeY == changedField || &m_maxRangeY == changedField || + &m_showDataSelectionInPlot == changedField ) + { + updateRectAnnotation(); + } + + if ( &m_minRangeX == changedField || &m_maxRangeX == changedField || &m_minRangeY == changedField || &m_maxRangeY == changedField || + &m_minExtrapolationRangeX == changedField || &m_maxExtrapolationRangeX == changedField || &m_regressionType == changedField || + &m_polynomialDegree == changedField || &m_showDataSelectionInPlot == changedField ) + { + auto dataSet = firstAncestorOrThisOfTypeAsserted(); + dataSet->loadDataAndUpdate( true ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::tuple, std::vector, QString> + RimGridCrossPlotRegressionCurve::calculateRegression( RimGridCrossPlotRegressionCurve::RegressionType regressionType, + const std::vector& xValues, + const std::vector& yValues, + const std::vector& outputXValues ) const +{ + if ( regressionType == RegressionType::LINEAR ) + { + regression::LinearRegression linearRegression; + linearRegression.fit( xValues, yValues ); + std::vector predictedValues = linearRegression.predict( outputXValues ); + return { outputXValues, predictedValues, RiaRegressionTextTools::generateRegressionText( linearRegression ) }; + } + else if ( m_regressionType == RegressionType::POLYNOMIAL ) + { + regression::PolynomialRegression polynomialRegression; + polynomialRegression.fit( xValues, yValues, m_polynomialDegree ); + std::vector predictedValues = polynomialRegression.predict( outputXValues ); + return { outputXValues, predictedValues, RiaRegressionTextTools::generateRegressionText( polynomialRegression ) }; + } + else if ( m_regressionType == RegressionType::POWER_FIT ) + { + auto [filteredTimeSteps, filteredValues] = getPositiveValues( xValues, yValues ); + if ( filteredTimeSteps.empty() || filteredValues.empty() ) return {}; + + regression::PowerFitRegression powerFitRegression; + powerFitRegression.fit( filteredTimeSteps, filteredValues ); + std::vector predictedValues = powerFitRegression.predict( outputXValues ); + return { outputXValues, predictedValues, RiaRegressionTextTools::generateRegressionText( powerFitRegression ) }; + } + else if ( m_regressionType == RegressionType::EXPONENTIAL ) + { + auto [filteredTimeSteps, filteredValues] = getPositiveValues( xValues, yValues ); + if ( filteredTimeSteps.empty() || filteredValues.empty() ) return {}; + + regression::ExponentialRegression exponentialRegression; + exponentialRegression.fit( filteredTimeSteps, filteredValues ); + std::vector predictedValues = exponentialRegression.predict( outputXValues ); + return { outputXValues, predictedValues, RiaRegressionTextTools::generateRegressionText( exponentialRegression ) }; + } + else if ( m_regressionType == RegressionType::LOGARITHMIC ) + { + auto [filteredTimeSteps, filteredValues] = getPositiveValues( xValues, yValues ); + if ( filteredTimeSteps.empty() || filteredValues.empty() ) return {}; + + regression::LogarithmicRegression logarithmicRegression; + logarithmicRegression.fit( filteredTimeSteps, filteredValues ); + std::vector predictedValues = logarithmicRegression.predict( outputXValues ); + return { outputXValues, predictedValues, RiaRegressionTextTools::generateRegressionText( logarithmicRegression ) }; + } + + return { {}, {}, "" }; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::pair, std::vector> RimGridCrossPlotRegressionCurve::getPositiveValues( const std::vector& xValues, + const std::vector& yValues ) +{ + std::vector filteredXValues; + std::vector filteredYValues; + for ( size_t i = 0; i < xValues.size(); i++ ) + { + if ( xValues[i] > 0.0 && yValues[i] > 0.0 ) + { + filteredXValues.push_back( xValues[i] ); + filteredYValues.push_back( yValues[i] ); + } + } + + return std::make_pair( filteredXValues, filteredYValues ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCrossPlotRegressionCurve::updateRectAnnotation() +{ + if ( !m_rectAnnotations.empty() ) + { + RimPlotRectAnnotation* annotation = m_rectAnnotations[0]; + annotation->setRangeX( m_minRangeX, m_maxRangeX ); + annotation->setRangeY( m_minRangeY, m_maxRangeY ); + annotation->setColor( m_curveAppearance->color() ); + annotation->setCheckState( m_showDataSelectionInPlot() ); + + auto dataSet = firstAncestorOrThisOfType(); + if ( dataSet ) + { + QString textLines; + textLines += QString( "Case: %1
" ).arg( dataSet->caseNameString() ); + textLines += QString( "%1: %2 - %3
" ).arg( dataSet->xAxisName() ).arg( m_minRangeX ).arg( m_maxRangeX ); + textLines += QString( "%1: %2 - %3
" ).arg( dataSet->yAxisName() ).arg( m_minRangeY ).arg( m_maxRangeY ); + annotation->setText( textLines ); + } + } +} diff --git a/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotRegressionCurve.h b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotRegressionCurve.h new file mode 100644 index 0000000000..17ee83f439 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/GridCrossPlots/RimGridCrossPlotRegressionCurve.h @@ -0,0 +1,93 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2023- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "RimPlotCurve.h" + +//================================================================================================== +/// +/// +//================================================================================================== +class RimGridCrossPlotRegressionCurve : public RimPlotCurve +{ + CAF_PDM_HEADER_INIT; + +public: + enum class RegressionType + { + LINEAR, + POLYNOMIAL, + POWER_FIT, + EXPONENTIAL, + LOGARITHMIC, + LOGISTIC + }; + + RimGridCrossPlotRegressionCurve(); + ~RimGridCrossPlotRegressionCurve() override = default; + void setGroupingInformation( int dataSetIndex, int groupIndex ); + void setSamples( const std::vector& xValues, const std::vector& yValues ); + void setRangeDefaults( const std::vector& xValues, const std::vector& yValues ); + + void setCurveAutoAppearance(); + int groupIndex() const; + size_t sampleCount() const; + void determineLegendIcon(); + void setBlackAndWhiteLegendIcons( bool blackAndWhite ); + + QString getRegressionTypeString() const; + + void swapAxis(); + +protected: + void updateZoomInParentPlot() override; + QString createCurveAutoName() override; + void onLoadDataAndUpdate( bool updateParentPlot ) override; + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + void defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override; + void defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override; + void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; + void updateRectAnnotation(); + + std::tuple, std::vector, QString> + calculateRegression( RimGridCrossPlotRegressionCurve::RegressionType regressionType, + const std::vector& xValues, + const std::vector& yValues, + const std::vector& outputXValues ) const; + + static std::pair, std::vector> getPositiveValues( const std::vector& xValues, + const std::vector& yValues ); + +private: + caf::PdmField> m_regressionType; + caf::PdmField m_minRangeX; + caf::PdmField m_maxRangeX; + caf::PdmField m_minRangeY; + caf::PdmField m_maxRangeY; + caf::PdmField m_showDataSelectionInPlot; + caf::PdmField m_polynomialDegree; + caf::PdmField m_expressionText; + caf::PdmField m_minExtrapolationRangeX; + caf::PdmField m_maxExtrapolationRangeX; + + std::pair m_dataRangeX; + std::pair m_dataRangeY; + + int m_dataSetIndex; + int m_groupIndex; +}; diff --git a/ApplicationLibCode/ProjectDataModel/RimPlotCurve.cpp b/ApplicationLibCode/ProjectDataModel/RimPlotCurve.cpp index 454c2a22b5..90daf3e28c 100644 --- a/ApplicationLibCode/ProjectDataModel/RimPlotCurve.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimPlotCurve.cpp @@ -26,6 +26,7 @@ #include "RimEnsembleCurveSet.h" #include "RimEnsembleCurveSetCollection.h" #include "RimNameConfig.h" +#include "RimPlotRectAnnotation.h" #include "RimProject.h" #include "RimSummaryCrossPlot.h" #include "RimSummaryCurve.h" @@ -135,6 +136,8 @@ RimPlotCurve::RimPlotCurve() m_additionalDataSources.uiCapability()->setUiEditorTypeName( caf::PdmUiTreeSelectionEditor::uiEditorTypeName() ); m_additionalDataSources.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::HIDDEN ); + CAF_PDM_InitFieldNoDefault( &m_rectAnnotations, "RectAnnotation", "Plot Rect Annotations" ); + m_plotCurve = nullptr; m_parentPlot = nullptr; } @@ -1137,6 +1140,14 @@ std::vector RimPlotCurve::additionalDataSources() const return m_additionalDataSources.ptrReferencedObjectsByType(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimPlotCurve::rectAnnotations() const +{ + return m_rectAnnotations.childrenByType(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/RimPlotCurve.h b/ApplicationLibCode/ProjectDataModel/RimPlotCurve.h index 89dccc540f..0fea8e20f3 100644 --- a/ApplicationLibCode/ProjectDataModel/RimPlotCurve.h +++ b/ApplicationLibCode/ProjectDataModel/RimPlotCurve.h @@ -28,6 +28,7 @@ #include "RiuQwtPlotCurveDefines.h" #include "RiuQwtSymbol.h" +#include "cafPdmChildArrayField.h" #include "cafPdmChildField.h" #include "cafPdmField.h" #include "cafPdmFieldCvfColor.h" @@ -39,6 +40,7 @@ class RiuPlotCurve; class RiuPlotWidget; +class RimPlotRectAnnotation; //================================================================================================== /// @@ -139,6 +141,8 @@ public: bool isSameCurve( const RiuPlotCurve* plotCurve ) const; void deletePlotCurve(); + std::vector rectAnnotations() const; + protected: virtual QString createCurveAutoName() = 0; @@ -208,7 +212,8 @@ protected: caf::PdmChildField m_curveAppearance; - caf::PdmPtrArrayField m_additionalDataSources; + caf::PdmPtrArrayField m_additionalDataSources; + caf::PdmChildArrayField m_rectAnnotations; QPointer m_parentPlot; RiuPlotCurve* m_plotCurve; diff --git a/ApplicationLibCode/ProjectDataModel/RimPlotRectAnnotation.cpp b/ApplicationLibCode/ProjectDataModel/RimPlotRectAnnotation.cpp new file mode 100644 index 0000000000..7a445f32c9 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/RimPlotRectAnnotation.cpp @@ -0,0 +1,129 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2023- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimPlotRectAnnotation.h" + +#include "RimPlot.h" +#include "RimTools.h" + +#include "cafPdmUiDoubleSliderEditor.h" + +#include + +CAF_PDM_SOURCE_INIT( RimPlotRectAnnotation, "RimPlotRectAnnotation" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimPlotRectAnnotation::RimPlotRectAnnotation() +{ + CAF_PDM_InitObject( "Plot Rect Annotation", ":/LeftAxis16x16.png" ); + + CAF_PDM_InitFieldNoDefault( &m_minX, "MinX", "Min X" ); + CAF_PDM_InitFieldNoDefault( &m_maxX, "MaxX", "Max X" ); + CAF_PDM_InitFieldNoDefault( &m_minY, "MinY", "Min Y" ); + CAF_PDM_InitFieldNoDefault( &m_maxY, "MaxY", "Max Y" ); + + CAF_PDM_InitField( &m_color, "Color", cvf::Color3f( cvf::Color3f::LIGHT_GRAY ), "Color" ); + CAF_PDM_InitField( &m_transparency, "Transparency", 0.1, "Transparency" ); + m_transparency.uiCapability()->setUiEditorTypeName( caf::PdmUiDoubleSliderEditor::uiEditorTypeName() ); + + CAF_PDM_InitFieldNoDefault( &m_text, "Text", "Text" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPlotRectAnnotation::setRangeX( double minX, double maxX ) +{ + m_minX = minX; + m_maxX = maxX; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPlotRectAnnotation::setRangeY( double minY, double maxY ) +{ + m_minY = minY; + m_maxY = maxY; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::pair RimPlotRectAnnotation::rangeX() const +{ + return { m_minX, m_maxX }; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::pair RimPlotRectAnnotation::rangeY() const +{ + return { m_minY, m_maxY }; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPlotRectAnnotation::setColor( const cvf::Color3f& color ) +{ + m_color = color; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +cvf::Color3f RimPlotRectAnnotation::color() const +{ + return m_color; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPlotRectAnnotation::setTransparency( double transparency ) +{ + m_transparency = transparency; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RimPlotRectAnnotation::transparency() const +{ + return m_transparency; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPlotRectAnnotation::setText( const QString& text ) +{ + m_text = text; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimPlotRectAnnotation::text() const +{ + return m_text; +} diff --git a/ApplicationLibCode/ProjectDataModel/RimPlotRectAnnotation.h b/ApplicationLibCode/ProjectDataModel/RimPlotRectAnnotation.h new file mode 100644 index 0000000000..6d50e5ba73 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/RimPlotRectAnnotation.h @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2023- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "RimCheckableNamedObject.h" + +#include "cafPdmField.h" +#include "cafPdmFieldCvfColor.h" + +#include + +//================================================================================================== +/// +/// +//================================================================================================== +class RimPlotRectAnnotation : public RimCheckableNamedObject +{ + CAF_PDM_HEADER_INIT; + +public: + RimPlotRectAnnotation(); + + void setRangeX( double minX, double maxX ); + void setRangeY( double minY, double maxY ); + + std::pair rangeX() const; + std::pair rangeY() const; + + void setColor( const cvf::Color3f& color ); + cvf::Color3f color() const; + + void setText( const QString& text ); + QString text() const; + + void setTransparency( double transparency ); + double transparency() const; + +protected: + caf::PdmField m_minX; + caf::PdmField m_maxX; + caf::PdmField m_minY; + caf::PdmField m_maxY; + caf::PdmField m_color; + caf::PdmField m_transparency; + caf::PdmField m_text; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.cpp index e5ebac0187..e57629b500 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.cpp @@ -19,6 +19,7 @@ #include "RimSummaryRegressionAnalysisCurve.h" #include "RiaQDateTimeTools.h" +#include "RiaRegressionTextTools.h" #include "RiaTimeTTools.h" #include "RimSummaryPlot.h" @@ -366,22 +367,12 @@ QString RimSummaryRegressionAnalysisCurve::curveExportDescription( const RifEcli return RimSummaryCurve::curveExportDescription() + "." + m_regressionType().uiText() + "_Regression"; } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -QString RimSummaryRegressionAnalysisCurve::formatDouble( double v ) -{ - return QString::number( v, 'g', 2 ); -} - //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::LinearRegression& reg ) { - QString sign = reg.intercept() < 0.0 ? "-" : "+"; - return QString( "y = %1x %2 %3" ).arg( formatDouble( reg.slope() ) ).arg( sign ).arg( formatDouble( std::fabs( reg.intercept() ) ) ) + - QString( "
R2 = %1" ).arg( reg.r2() ) + getXAxisUnitText(); + return RiaRegressionTextTools::generateRegressionText( reg ) + getXAxisUnitText(); } //-------------------------------------------------------------------------------------------------- @@ -389,40 +380,7 @@ QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regress //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::PolynomialRegression& reg ) { - QString str = "y = "; - - bool isFirst = true; - std::vector coeffs = reg.coeffisients(); - QStringList parts; - for ( size_t i = 0; i < coeffs.size(); i++ ) - { - double coeff = coeffs[i]; - // Skip zero coeffs - if ( coeff != 0.0 ) - { - if ( coeff < 0.0 ) - parts.append( "-" ); - else if ( !isFirst ) - parts.append( "+" ); - - if ( i == 0 ) - { - parts.append( QString( "%1" ).arg( formatDouble( std::fabs( coeff ) ) ) ); - } - else if ( i == 1 ) - { - parts.append( QString( "%1x" ).arg( formatDouble( std::fabs( coeff ) ) ) ); - } - else - { - parts.append( QString( " %1x%2" ).arg( formatDouble( std::fabs( coeff ) ) ).arg( i ) ); - } - - isFirst = false; - } - } - - return str + parts.join( " " ) + QString( "
R2 = %1" ).arg( reg.r2() ) + getXAxisUnitText(); + return RiaRegressionTextTools::generateRegressionText( reg ) + getXAxisUnitText(); } //-------------------------------------------------------------------------------------------------- @@ -430,8 +388,7 @@ QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regress //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::PowerFitRegression& reg ) { - return QString( "y = %1 + x%2" ).arg( formatDouble( reg.scale() ) ).arg( formatDouble( reg.exponent() ) ) + - QString( "
R2 = %1" ).arg( reg.r2() ) + getXAxisUnitText(); + return RiaRegressionTextTools::generateRegressionText( reg ) + getXAxisUnitText(); } //-------------------------------------------------------------------------------------------------- @@ -439,8 +396,7 @@ QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regress //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::ExponentialRegression& reg ) { - return QString( "y = %1 * e%2x" ).arg( formatDouble( reg.a() ) ).arg( formatDouble( reg.b() ) ) + - QString( "
R2 = %1" ).arg( reg.r2() ) + getXAxisUnitText(); + return RiaRegressionTextTools::generateRegressionText( reg ) + getXAxisUnitText(); } //-------------------------------------------------------------------------------------------------- @@ -448,8 +404,7 @@ QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regress //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::LogarithmicRegression& reg ) { - return QString( "y = %1 + %2 * ln(x)" ).arg( formatDouble( reg.a() ) ).arg( formatDouble( reg.b() ) ) + - QString( "
R2 = %1" ).arg( reg.r2() ) + getXAxisUnitText(); + return RiaRegressionTextTools::generateRegressionText( reg ) + getXAxisUnitText(); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.h b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.h index 387d9e42ff..6739656bf0 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.h @@ -110,7 +110,6 @@ private: static QString generateRegressionText( const regression::LogarithmicRegression& reg ); static QString generateRegressionText( const regression::ExponentialRegression& reg ); - static QString formatDouble( double v ); static QString getXAxisUnitText(); static void appendTimeSteps( std::vector& destinationTimeSteps, const std::set& sourceTimeSteps ); diff --git a/ApplicationLibCode/UserInterface/CMakeLists_files.cmake b/ApplicationLibCode/UserInterface/CMakeLists_files.cmake index eebde5bc4a..6a3bba7a94 100644 --- a/ApplicationLibCode/UserInterface/CMakeLists_files.cmake +++ b/ApplicationLibCode/UserInterface/CMakeLists_files.cmake @@ -108,6 +108,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RiuQwtDateScaleWrapper.h ${CMAKE_CURRENT_LIST_DIR}/RiuMatrixPlotWidget.h ${CMAKE_CURRENT_LIST_DIR}/RiuMenuBarBuildTools.h + ${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotRectAnnotation.h ) set(SOURCE_GROUP_SOURCE_FILES @@ -217,6 +218,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RiuQwtDateScaleWrapper.cpp ${CMAKE_CURRENT_LIST_DIR}/RiuMatrixPlotWidget.cpp ${CMAKE_CURRENT_LIST_DIR}/RiuMenuBarBuildTools.cpp + ${CMAKE_CURRENT_LIST_DIR}/RiuQwtPlotRectAnnotation.cpp ) if(RESINSIGHT_USE_QT_CHARTS) diff --git a/ApplicationLibCode/UserInterface/RiuQwtPlotCurve.cpp b/ApplicationLibCode/UserInterface/RiuQwtPlotCurve.cpp index 4edd4c7943..f2fbdfdfd5 100644 --- a/ApplicationLibCode/UserInterface/RiuQwtPlotCurve.cpp +++ b/ApplicationLibCode/UserInterface/RiuQwtPlotCurve.cpp @@ -19,9 +19,14 @@ #include "RiuQwtPlotCurve.h" +#include "RiaColorTools.h" #include "RiaCurveDataTools.h" #include "RiaImageTools.h" +#include "RimPlotCurve.h" +#include "RimPlotRectAnnotation.h" + +#include "RiuQwtPlotRectAnnotation.h" #include "RiuQwtPlotTools.h" #include "RiuQwtPlotWidget.h" #include "RiuQwtSymbol.h" @@ -32,9 +37,11 @@ #include "qwt_painter.h" #include "qwt_plot_curve.h" #include "qwt_plot_intervalcurve.h" +#include "qwt_plot_marker.h" #include "qwt_point_mapper.h" #include "qwt_scale_map.h" #include "qwt_symbol.h" +#include "qwt_text.h" #include "qwt_weeding_curve_fitter.h" #include @@ -59,6 +66,8 @@ RiuQwtPlotCurve::RiuQwtPlotCurve( RimPlotCurve* ownerRimCurve, const QString& ti m_qwtCurveErrorBars->setSymbol( new QwtIntervalSymbol( QwtIntervalSymbol::Bar ) ); m_qwtCurveErrorBars->setItemAttribute( QwtPlotItem::Legend, false ); m_qwtCurveErrorBars->setZ( RiuQwtPlotCurveDefines::zDepthForIndex( RiuQwtPlotCurveDefines::ZIndex::Z_ERROR_BARS ) ); + + m_qwtPlotRectAnnotation = new RiuQwtPlotRectAnnotation; } //-------------------------------------------------------------------------------------------------- @@ -70,6 +79,9 @@ RiuQwtPlotCurve::~RiuQwtPlotCurve() delete m_qwtCurveErrorBars; m_qwtCurveErrorBars = nullptr; + + delete m_qwtPlotRectAnnotation; + m_qwtPlotRectAnnotation = nullptr; } //-------------------------------------------------------------------------------------------------- @@ -322,6 +334,30 @@ void RiuQwtPlotCurve::attachToPlot( RiuPlotWidget* plotWidget ) { m_qwtCurveErrorBars->attach( m_plotWidget->qwtPlot() ); } + + if ( m_ownerRimCurve ) + { + auto rectAnnotations = m_ownerRimCurve->rectAnnotations(); + if ( !rectAnnotations.empty() ) + { + auto [minX, maxX] = rectAnnotations[0]->rangeX(); + auto [minY, maxY] = rectAnnotations[0]->rangeY(); + m_qwtPlotRectAnnotation->setInterval( minX, maxX, minY, maxY ); + QColor brushColor( RiaColorTools::toQColor( rectAnnotations[0]->color() ) ); + brushColor.setAlphaF( rectAnnotations[0]->transparency() ); + QBrush brush( brushColor ); + m_qwtPlotRectAnnotation->setBrush( brush ); + QColor penColor( RiaColorTools::toQColor( rectAnnotations[0]->color() ) ); + + QPen pen( penColor ); + m_qwtPlotRectAnnotation->setPen( pen ); + + m_qwtPlotRectAnnotation->setText( rectAnnotations[0]->text() ); + m_qwtPlotRectAnnotation->attach( m_plotWidget->qwtPlot() ); + + m_qwtPlotRectAnnotation->setVisible( rectAnnotations[0]->isChecked() ); + } + } } //-------------------------------------------------------------------------------------------------- @@ -331,6 +367,7 @@ void RiuQwtPlotCurve::detach() { QwtPlotCurve::detach(); m_qwtCurveErrorBars->detach(); + m_qwtPlotRectAnnotation->detach(); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/UserInterface/RiuQwtPlotCurve.h b/ApplicationLibCode/UserInterface/RiuQwtPlotCurve.h index bd0727ccad..cf4a252c5d 100644 --- a/ApplicationLibCode/UserInterface/RiuQwtPlotCurve.h +++ b/ApplicationLibCode/UserInterface/RiuQwtPlotCurve.h @@ -26,6 +26,7 @@ class QwtPlotIntervalCurve; class RiuQwtPlotWidget; +class RiuQwtPlotRectAnnotation; //================================================================================================== // @@ -99,5 +100,7 @@ protected: QwtPlotIntervalCurve* m_qwtCurveErrorBars; bool m_showErrorBars; + RiuQwtPlotRectAnnotation* m_qwtPlotRectAnnotation; + QPointer m_plotWidget; }; diff --git a/ApplicationLibCode/UserInterface/RiuQwtPlotCurveDefines.cpp b/ApplicationLibCode/UserInterface/RiuQwtPlotCurveDefines.cpp index b690e38d94..46973d509d 100644 --- a/ApplicationLibCode/UserInterface/RiuQwtPlotCurveDefines.cpp +++ b/ApplicationLibCode/UserInterface/RiuQwtPlotCurveDefines.cpp @@ -67,6 +67,13 @@ int RiuQwtPlotCurveDefines::zDepthForIndex( ZIndex index ) case RiuQwtPlotCurveDefines::ZIndex::Z_SINGLE_CURVE_OBSERVED: return 500; break; + case RiuQwtPlotCurveDefines::ZIndex::Z_PLOT_RECT_ANNOTATION: + return 600; + break; + case RiuQwtPlotCurveDefines::ZIndex::Z_REGRESSION_CURVE: + return 700; + break; + default: break; } diff --git a/ApplicationLibCode/UserInterface/RiuQwtPlotCurveDefines.h b/ApplicationLibCode/UserInterface/RiuQwtPlotCurveDefines.h index a5e4579de8..2a92f2acea 100644 --- a/ApplicationLibCode/UserInterface/RiuQwtPlotCurveDefines.h +++ b/ApplicationLibCode/UserInterface/RiuQwtPlotCurveDefines.h @@ -45,7 +45,9 @@ enum class ZIndex Z_ENSEMBLE_STAT_CURVE, Z_SINGLE_CURVE_NON_OBSERVED, Z_ERROR_BARS, - Z_SINGLE_CURVE_OBSERVED + Z_SINGLE_CURVE_OBSERVED, + Z_PLOT_RECT_ANNOTATION, + Z_REGRESSION_CURVE }; int zDepthForIndex( ZIndex index ); diff --git a/ApplicationLibCode/UserInterface/RiuQwtPlotRectAnnotation.cpp b/ApplicationLibCode/UserInterface/RiuQwtPlotRectAnnotation.cpp new file mode 100644 index 0000000000..e0b322a234 --- /dev/null +++ b/ApplicationLibCode/UserInterface/RiuQwtPlotRectAnnotation.cpp @@ -0,0 +1,214 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2023- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RiuQwtPlotRectAnnotation.h" + +#include "RiuGuiTheme.h" +#include "RiuQwtPlotCurveDefines.h" + +#include "qwt_painter.h" +#include "qwt_plot_marker.h" +#include "qwt_scale_map.h" +#include "qwt_symbol.h" +#include "qwt_text.h" + +#include +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiuQwtPlotRectAnnotation::RiuQwtPlotRectAnnotation() + : QwtPlotItem( QwtText( "PlotRectAnnotation" ) ) +{ + setItemAttribute( QwtPlotItem::AutoScale, false ); + setItemAttribute( QwtPlotItem::Legend, false ); + + QColor c( Qt::darkGray ); + c.setAlpha( 50 ); + m_brush = QBrush( c ); + + m_pen = QPen(); + m_pen.setColor( Qt::black ); + m_pen.setWidth( 2 ); + + setZ( RiuQwtPlotCurveDefines::zDepthForIndex( RiuQwtPlotCurveDefines::ZIndex::Z_PLOT_RECT_ANNOTATION ) ); + + m_textLabel = std::make_unique(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuQwtPlotRectAnnotation::setPen( const QPen& pen ) +{ + m_pen = pen; + itemChanged(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +const QPen& RiuQwtPlotRectAnnotation::pen() const +{ + return m_pen; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuQwtPlotRectAnnotation::setBrush( const QBrush& brush ) +{ + m_brush = brush; + itemChanged(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +const QBrush& RiuQwtPlotRectAnnotation::brush() const +{ + return m_brush; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuQwtPlotRectAnnotation::setInterval( double minX, double maxX, double minY, double maxY ) +{ + m_intervalX.setMinValue( minX ); + m_intervalX.setMaxValue( maxX ); + m_intervalY.setMinValue( minY ); + m_intervalY.setMaxValue( maxY ); + + itemChanged(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuQwtPlotRectAnnotation::draw( QPainter* painter, const QwtScaleMap& xMap, const QwtScaleMap& yMap, const QRectF& canvasRect ) const +{ + if ( !m_intervalX.isValid() || !m_intervalY.isValid() ) return; + + QPen pen = m_pen; + pen.setCapStyle( Qt::FlatCap ); + + double x1 = xMap.transform( m_intervalX.minValue() ); + double x2 = xMap.transform( m_intervalX.maxValue() ); + + double y1 = yMap.transform( m_intervalY.minValue() ); + double y2 = yMap.transform( m_intervalY.maxValue() ); + + const bool doAlign = QwtPainter::roundingAlignment( painter ); + if ( doAlign ) + { + x1 = qRound( x1 ); + x2 = qRound( x2 ); + + y1 = qRound( y1 ); + y2 = qRound( y2 ); + } + + QRectF r( x1, y1, x2 - x1, y2 - y1 ); + r = r.normalized(); + + if ( m_brush.style() != Qt::NoBrush && x1 != x2 && y1 != y2 ) + { + QwtPainter::fillRect( painter, r, m_brush ); + } + + if ( m_pen.style() != Qt::NoPen ) + { + painter->setPen( m_pen ); + + QwtPainter::drawRect( painter, r ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QRectF RiuQwtPlotRectAnnotation::boundingRect() const +{ + QRectF br = QwtPlotItem::boundingRect(); + + if ( m_intervalX.isValid() && m_intervalY.isValid() ) + { + br.setTop( m_intervalY.minValue() ); + br.setBottom( m_intervalY.maxValue() ); + br.setLeft( m_intervalX.minValue() ); + br.setRight( m_intervalX.maxValue() ); + } + + return br; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +int RiuQwtPlotRectAnnotation::rtti() const +{ + return 5001; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuQwtPlotRectAnnotation::setText( const QString& text ) +{ + QwtText label( text ); + auto textColor = RiuGuiTheme::getColorByVariableName( "textColor" ); + label.setColor( textColor ); + label.setRenderFlags( Qt::AlignLeft ); + + m_textLabel->setLabel( label ); + m_textLabel->setLabelAlignment( Qt::AlignRight | Qt::AlignBottom ); + + m_textLabel->setLineStyle( QwtPlotMarker::NoLine ); + m_textLabel->setSymbol( new QwtSymbol( QwtSymbol::NoSymbol ) ); + + m_textLabel->setValue( m_intervalX.minValue(), m_intervalY.maxValue() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuQwtPlotRectAnnotation::attach( QwtPlot* plot ) +{ + QwtPlotItem::attach( plot ); + m_textLabel->attach( plot ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuQwtPlotRectAnnotation::detach() +{ + QwtPlotItem::detach(); + m_textLabel->detach(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuQwtPlotRectAnnotation::setVisible( bool isVisible ) +{ + QwtPlotItem::setVisible( isVisible ); + m_textLabel->setVisible( isVisible ); +} diff --git a/ApplicationLibCode/UserInterface/RiuQwtPlotRectAnnotation.h b/ApplicationLibCode/UserInterface/RiuQwtPlotRectAnnotation.h new file mode 100644 index 0000000000..d5431d5d46 --- /dev/null +++ b/ApplicationLibCode/UserInterface/RiuQwtPlotRectAnnotation.h @@ -0,0 +1,69 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2023- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "qwt_interval.h" +#include "qwt_plot_item.h" + +#include +#include + +#include + +class QRectF; +class QPainter; +class QwtPlotMarker; + +//================================================================================================== +/// Rectangular plot item for annotation areas of a plot. +/// +//================================================================================================== +class RiuQwtPlotRectAnnotation : public QwtPlotItem +{ +public: + RiuQwtPlotRectAnnotation(); + ~RiuQwtPlotRectAnnotation() override = default; + + void setInterval( double minX, double maxX, double minY, double maxY ); + + void setPen( const QPen& ); + const QPen& pen() const; + + void setBrush( const QBrush& ); + const QBrush& brush() const; + + void setText( const QString& text ); + + int rtti() const override; + void draw( QPainter* painter, const QwtScaleMap& xMap, const QwtScaleMap& yMap, const QRectF& canvasRect ) const override; + QRectF boundingRect() const override; + + void attach( QwtPlot* plot ); + void detach(); + void setVisible( bool isVisible ) override; + +protected: + QwtInterval m_intervalX; + QwtInterval m_intervalY; + QPen m_pen; + QBrush m_brush; + QString m_text; + + std::unique_ptr m_textLabel; +};