diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61ed56b..1023720 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,14 +5,10 @@ on: pull_request: workflow_dispatch: -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release - # for matrix check https://docs.github.com/en/actions/reference/specifications-for-github-hosted-runners jobs: prepare_matrix: - runs-on: ubuntu-latest + runs-on: ubuntu-slim outputs: matrix: ${{ steps.matrix_setup.outputs.matrix }} steps: @@ -34,7 +30,7 @@ jobs: steps: - name: Checkout the source - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive ref: ${{ github.event.pull_request.head.ref }} @@ -45,9 +41,9 @@ jobs: sudo xcode-select -switch /Applications/Xcode_${{matrix.build-xcode-version}}.app - name: Setup python version - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: - python-version: "3.11" + python-version: "3.12" - name: Start ssh key agent uses: webfactory/ssh-agent@v0.9.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index b9674d0..f3fc5aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,17 +15,7 @@ PROJECT(${PROJECT} # ----------------------------------------------------------------------------- # CMake Options # ----------------------------------------------------------------------------- -set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_AUTORCC ON) -set(CMAKE_AUTOMOC ON) - -if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DWIN32 /EHsc /MP /permissive- /Zc:__cplusplus") - set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd") - set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /MD") - set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD") -endif() # ----------------------------------------------------------------------------- # Dependencies @@ -33,6 +23,11 @@ endif() find_package(Qt6 COMPONENTS Widgets WebEngineWidgets OpenGL OpenGLWidgets REQUIRED) find_package(ManiVault COMPONENTS Core PointData ClusterData ColorData ImageData CONFIG QUIET) +mv_project_defaults() + +if(UNIX AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + find_package(TBB REQUIRED) +endif() # ----------------------------------------------------------------------------- # Source files @@ -116,9 +111,10 @@ target_include_directories(${PROJECT} PRIVATE "${ManiVault_INCLUDE_DIR}") # ----------------------------------------------------------------------------- target_compile_features(${PROJECT} PRIVATE cxx_std_20) -if(MV_UNITY_BUILD) - set_target_properties(${PROJECT} PROPERTIES UNITY_BUILD ON) -endif() +set_target_properties(${PROJECT} PROPERTIES + AUTOMOC ON + UNITY_BUILD ${MV_UNITY_BUILD} +) # ----------------------------------------------------------------------------- # Target library linking @@ -134,6 +130,10 @@ target_link_libraries(${PROJECT} PRIVATE ManiVault::ClusterData) target_link_libraries(${PROJECT} PRIVATE ManiVault::ImageData) target_link_libraries(${PROJECT} PRIVATE ManiVault::ColorData) +if(UNIX AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_link_libraries(${PROJECT} PRIVATE TBB::tbb) +endif() + # ----------------------------------------------------------------------------- # Target installation # ----------------------------------------------------------------------------- diff --git a/PluginInfo.json b/PluginInfo.json index a09ee50..9dc9e34 100644 --- a/PluginInfo.json +++ b/PluginInfo.json @@ -2,7 +2,7 @@ "name" : "Scatterplot View", "version" : { "plugin" : "1.0.0", - "core" : ["1.3"] + "core" : ["1.5"] }, "type" : "View", "dependencies" : ["Points"] diff --git a/conanfile.py b/conanfile.py index 3b84dde..1980dd0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,6 +1,7 @@ from conans import ConanFile from conan.tools.cmake import CMakeDeps, CMake, CMakeToolchain from conans.tools import save, load +from conans.tools import os_info from conans import tools import os import pathlib @@ -72,8 +73,8 @@ def configure(self): pass def system_requirements(self): - # May be needed for macOS or Linux - pass + if os_info.is_linux: + self.run("sudo apt update && sudo apt install -y libtbb-dev") def config_options(self): if self.settings.os == "Windows": @@ -105,7 +106,7 @@ def generate(self): tc.variables["ManiVault_DIR"] = manivault_dir # Set some build options - tc.variables["MV_UNITY_BUILD"] = "ON" + tc.cache_variables["MV_UNITY_BUILD"] = True tc.generate() @@ -120,7 +121,6 @@ def build(self): cmake = self._configure_cmake() cmake.build(build_type="RelWithDebInfo") - cmake.build(build_type="Release") def package(self): package_dir = pathlib.Path(self.build_folder, "package") @@ -138,23 +138,9 @@ def package(self): relWithDebInfo_dir, ] ) - subprocess.run( - [ - "cmake", - "--install", - self.build_folder, - "--config", - "Release", - "--prefix", - release_dir, - ] - ) self.copy(pattern="*", src=package_dir) def package_info(self): self.cpp_info.relwithdebinfo.libdirs = ["RelWithDebInfo/lib"] self.cpp_info.relwithdebinfo.bindirs = ["RelWithDebInfo/Plugins", "RelWithDebInfo"] self.cpp_info.relwithdebinfo.includedirs = ["RelWithDebInfo/include", "RelWithDebInfo"] - self.cpp_info.release.libdirs = ["Release/lib"] - self.cpp_info.release.bindirs = ["Release/Plugins", "Release"] - self.cpp_info.release.includedirs = ["Release/include", "Release"] diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index c3fca1f..a0859e1 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -16,7 +16,10 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : _colorByModel(this), _colorByAction(this, "Color by"), _constantColorAction(this, "Constant color", DEFAULT_CONSTANT_COLOR), + _colorSpaceAction(this, "Color space", { "Scalar (1D)", "Duo (2D)", "RGB" }, "Scalar (1D)"), _dimensionAction(this, "Dimension"), + _dimensionAction2(this, "Dimension 2"), + _dimensionAction3(this, "Dimension 3"), _colorMap1DAction(this, "1D Color map"), _colorMap2DAction(this, "2D Color map") { @@ -26,9 +29,14 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : addAction(&_colorByAction); addAction(&_constantColorAction); - addAction(&_colorMap2DAction); + addAction(&_colorSpaceAction); addAction(&_colorMap1DAction); + addAction(&_colorMap2DAction); addAction(&_dimensionAction); + addAction(&_dimensionAction2); + addAction(&_dimensionAction3); + + _colorSpaceAction.setToolTip("Color space for data-driven coloring"); _scatterplotPlugin->getWidget().addAction(&_colorByAction); _scatterplotPlugin->getWidget().addAction(&_dimensionAction); @@ -86,28 +94,51 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : connect(&_currentColorPointsDataset, &Dataset::dataDimensionsChanged, this, [this]() { if (_currentColorPointsDataset.isValid()) { _dimensionAction.setPointsDataset(_currentColorPointsDataset); + _dimensionAction2.setPointsDataset(_currentColorPointsDataset); + _dimensionAction3.setPointsDataset(_currentColorPointsDataset); updateScatterPlotWidgetColors(); } }); _dimensionAction.setPointsDataset(_currentColorPointsDataset); + _dimensionAction2.setPointsDataset(_currentColorPointsDataset); + _dimensionAction3.setPointsDataset(_currentColorPointsDataset); + + // Auto-select the color space for datasets with exactly two or three channels + if (!mv::projects().isOpeningProject()) { + const auto numDimensions = static_cast(_currentColorPointsDataset->getNumDimensions()); + + if (numDimensions == 2) { + _colorSpaceAction.setCurrentIndex(1); // Duo (2D) + applyDefaultChannels(); // also apply defaults when the index was already Duo + } + else if (numDimensions == 3) { + _colorSpaceAction.setCurrentIndex(2); // RGB + applyDefaultChannels(); // also apply defaults when the index was already RGB + } + } } else { _dimensionAction.setPointsDataset(Dataset()); + _dimensionAction2.setPointsDataset(Dataset()); + _dimensionAction3.setPointsDataset(Dataset()); } } else { _dimensionAction.setPointsDataset(Dataset()); + _dimensionAction2.setPointsDataset(Dataset()); + _dimensionAction3.setPointsDataset(Dataset()); } - //_dimensionAction.setVisible(currentColorDatasetTypeIsPointType); emit currentColorDatasetChanged(currentColorDataset); } else { _dimensionAction.setPointsDataset(Dataset()); - //_dimensionAction.setVisible(false); + _dimensionAction2.setPointsDataset(Dataset()); + _dimensionAction3.setPointsDataset(Dataset()); } + updateChannelActionsReadOnly(); updateScatterPlotWidgetColors(); updateScatterplotWidgetColorMap(); updateColorMapActionScalarRange(); @@ -142,7 +173,25 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : updateColorMapActionsReadOnly(); updateColorMapActionScalarRange(); }); - + + connect(&_dimensionAction2, &DimensionPickerAction::currentDimensionIndexChanged, this, [this](const int32_t& currentDimensionIndex) { + updateScatterPlotWidgetColors(); + }); + + connect(&_dimensionAction3, &DimensionPickerAction::currentDimensionIndexChanged, this, [this](const int32_t& currentDimensionIndex) { + updateScatterPlotWidgetColors(); + }); + + connect(&_colorSpaceAction, &OptionAction::currentIndexChanged, this, [this](const std::int32_t& currentIndex) { + if (!mv::projects().isOpeningProject()) + applyDefaultChannels(); + + updateChannelActionsReadOnly(); + updateScatterPlotWidgetColors(); + updateScatterplotWidgetColorMap(); + updateColorMapActionsReadOnly(); + }); + connect(&_constantColorAction, &ColorAction::colorChanged, this, &ColoringAction::updateScatterplotWidgetColorMap); connect(&_colorMap1DAction, &ColorMapAction::imageChanged, this, &ColoringAction::updateScatterplotWidgetColorMap); connect(&_colorMap2DAction, &ColorMapAction::imageChanged, this, &ColoringAction::updateScatterplotWidgetColorMap); @@ -160,6 +209,7 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : updateScatterplotWidgetColorMap(); updateColorMapActionScalarRange(); + updateChannelActionsReadOnly(); _scatterplotPlugin->getScatterplotWidget().setColoringMode(ScatterplotWidget::ColoringMode::Constant); } @@ -251,10 +301,38 @@ void ColoringAction::updateScatterPlotWidgetColors() if (currentColorDataset->getDataType() == ClusterType) _scatterplotPlugin->loadColors(currentColorDataset.get()); else { - const auto currentDimensionIndex = _dimensionAction.getCurrentDimensionIndex(); + const auto dimension1 = _dimensionAction.getCurrentDimensionIndex(); + + if (dimension1 < 0) + return; + + switch (_colorSpaceAction.getCurrentIndex()) + { + case 1: // Duo (2D) + { + const auto dimension2 = _dimensionAction2.getCurrentDimensionIndex(); + + if (dimension2 >= 0) + _scatterplotPlugin->loadColors2D(currentColorDataset.get(), dimension1, dimension2); + + break; + } + + case 2: // RGB + { + const auto dimension2 = _dimensionAction2.getCurrentDimensionIndex(); + const auto dimension3 = _dimensionAction3.getCurrentDimensionIndex(); - if (currentDimensionIndex >= 0) - _scatterplotPlugin->loadColors(currentColorDataset.get(), _dimensionAction.getCurrentDimensionIndex()); + if (dimension2 >= 0 && dimension3 >= 0) + _scatterplotPlugin->loadColorsRGB(currentColorDataset.get(), dimension1, dimension2, dimension3); + + break; + } + + default: // Scalar (1D) + _scatterplotPlugin->loadColors(currentColorDataset.get(), dimension1); + break; + } } updateScatterplotWidgetColorMap(); @@ -297,7 +375,13 @@ void ColoringAction::updateScatterplotWidgetColorMap() scatterplotWidget.setColoringMode(ScatterplotWidget::ColoringMode::Scatter); } else { - scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); + const auto currentColorDataset = getCurrentColorDataset(); + const bool isDuo = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType && _colorSpaceAction.getCurrentIndex() == 1; + + if (isDuo) + scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().flipped(Qt::Vertical)); + else + scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().flipped(Qt::Vertical)); } break; @@ -324,9 +408,22 @@ void ColoringAction::updateScatterplotWidgetColorMap() void ColoringAction::updateScatterPlotWidgetColorMapRange() { + auto& scatterplotWidget = _scatterplotPlugin->getScatterplotWidget(); + + // The adjustable 1D color-map range only drives the (channel 1) scalar range for 1D scalar coloring. + // In Duo/RGB the color channels each use their own automatically-computed range, so leave channel 1 + // untouched here (otherwise identical channels would normalize differently and produce a color tint). + if (scatterplotWidget.getRenderMode() == ScatterplotWidget::SCATTERPLOT) { + const auto currentColorDataset = getCurrentColorDataset(); + const bool isPointsSource = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType; + + if (isPointsSource && _colorSpaceAction.getCurrentIndex() != 0) // Duo (1) or RGB (2) + return; + } + const auto& rangeAction = _colorMap1DAction.getRangeAction(ColorMapAction::Axis::X); - _scatterplotPlugin->getScatterplotWidget().setColorMapRange(rangeAction.getMinimum(), rangeAction.getMaximum()); + scatterplotWidget.setColorMapRange(rangeAction.getMinimum(), rangeAction.getMaximum()); } bool ColoringAction::shouldEnableColorMap() const @@ -351,9 +448,71 @@ bool ColoringAction::shouldEnableColorMap() const void ColoringAction::updateColorMapActionsReadOnly() { const auto currentIndex = _colorByAction.getCurrentIndex(); + const bool isPointsSource = currentIndex >= 2 && _currentColorPointsDataset.isValid(); + const bool isDuo = isPointsSource && _colorSpaceAction.getCurrentIndex() == 1; + const bool isRGB = isPointsSource && _colorSpaceAction.getCurrentIndex() == 2; - _colorMap1DAction.setEnabled(shouldEnableColorMap() && (currentIndex >= 2)); - _colorMap2DAction.setEnabled(shouldEnableColorMap() && (currentIndex == 1)); + _colorMap1DAction.setEnabled(shouldEnableColorMap() && (currentIndex >= 2) && !isDuo && !isRGB); + _colorMap2DAction.setEnabled(shouldEnableColorMap() && (currentIndex == 1 || isDuo)); +} + +void ColoringAction::updateChannelActionsReadOnly() +{ + const auto currentIndex = _colorByAction.getCurrentIndex(); + const auto colorSpace = _colorSpaceAction.getCurrentIndex(); + + const bool isPointsSource = currentIndex >= 2 && _currentColorPointsDataset.isValid(); + + const bool isDuo = isPointsSource && colorSpace == 1; // Duo (2D) + const bool isRGB = isPointsSource && colorSpace == 2; // RGB + + // All actions remain visible; only their enabled state reflects the current coloring mode. + + // Constant color: only usable in constant mode + _constantColorAction.setEnabled(currentIndex == 0); + + // Color space selector: only usable for a points color source + _colorSpaceAction.setEnabled(isPointsSource); + + // Dimension pickers: channel 1 for any points source, channel 2 for Duo/RGB, channel 3 for RGB only + _dimensionAction.setEnabled(isPointsSource); + _dimensionAction2.setEnabled(isDuo || isRGB); + _dimensionAction3.setEnabled(isRGB); +} + +void ColoringAction::applyDefaultChannels() +{ + if (!_currentColorPointsDataset.isValid()) + return; + + const auto numDimensions = static_cast(_currentColorPointsDataset->getNumDimensions()); + + switch (_colorSpaceAction.getCurrentIndex()) + { + case 1: // Duo (2D): default to the first two channels + { + if (numDimensions >= 2) { + _dimensionAction.setCurrentDimensionIndex(0); + _dimensionAction2.setCurrentDimensionIndex(1); + } + + break; + } + + case 2: // RGB: default to the first three channels + { + if (numDimensions >= 3) { + _dimensionAction.setCurrentDimensionIndex(0); + _dimensionAction2.setCurrentDimensionIndex(1); + _dimensionAction3.setCurrentDimensionIndex(2); + } + + break; + } + + default: + break; + } } void ColoringAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) @@ -368,7 +527,10 @@ void ColoringAction::connectToPublicAction(WidgetAction* publicAction, bool recu if (recursive) { actions().connectPrivateActionToPublicAction(&_colorByAction, &publicColoringAction->getColorByAction(), recursive); actions().connectPrivateActionToPublicAction(&_constantColorAction, &publicColoringAction->getConstantColorAction(), recursive); + actions().connectPrivateActionToPublicAction(&_colorSpaceAction, &publicColoringAction->getColorSpaceAction(), recursive); actions().connectPrivateActionToPublicAction(&_dimensionAction, &publicColoringAction->getDimensionAction(), recursive); + actions().connectPrivateActionToPublicAction(&_dimensionAction2, &publicColoringAction->getDimensionAction2(), recursive); + actions().connectPrivateActionToPublicAction(&_dimensionAction3, &publicColoringAction->getDimensionAction3(), recursive); actions().connectPrivateActionToPublicAction(&_colorMap1DAction, &publicColoringAction->getColorMap1DAction(), recursive); actions().connectPrivateActionToPublicAction(&_colorMap2DAction, &publicColoringAction->getColorMap2DAction(), recursive); } @@ -384,7 +546,10 @@ void ColoringAction::disconnectFromPublicAction(bool recursive) if (recursive) { actions().disconnectPrivateActionFromPublicAction(&_colorByAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_constantColorAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_colorSpaceAction, recursive); actions().disconnectPrivateActionFromPublicAction(&_dimensionAction, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionAction2, recursive); + actions().disconnectPrivateActionFromPublicAction(&_dimensionAction3, recursive); actions().disconnectPrivateActionFromPublicAction(&_colorMap2DAction, recursive); } @@ -395,11 +560,22 @@ void ColoringAction::fromVariantMap(const QVariantMap& variantMap) { GroupAction::fromVariantMap(variantMap); + // Restore the color source first so the dimension pickers are targeted at the right dataset, + // then restore the color space and channels, and finally the color maps. _colorByAction.fromParentVariantMap(variantMap); _constantColorAction.fromParentVariantMap(variantMap); _dimensionAction.fromParentVariantMap(variantMap); + _dimensionAction2.fromParentVariantMap(variantMap, true); + _dimensionAction3.fromParentVariantMap(variantMap, true); + _colorSpaceAction.fromParentVariantMap(variantMap, true); _colorMap1DAction.fromParentVariantMap(variantMap); _colorMap2DAction.fromParentVariantMap(variantMap); + + // Apply the fully-restored coloring state + updateChannelActionsReadOnly(); + updateScatterPlotWidgetColors(); + updateScatterplotWidgetColorMap(); + updateColorMapActionsReadOnly(); } QVariantMap ColoringAction::toVariantMap() const @@ -408,7 +584,10 @@ QVariantMap ColoringAction::toVariantMap() const _colorByAction.insertIntoVariantMap(variantMap); _constantColorAction.insertIntoVariantMap(variantMap); + _colorSpaceAction.insertIntoVariantMap(variantMap); _dimensionAction.insertIntoVariantMap(variantMap); + _dimensionAction2.insertIntoVariantMap(variantMap); + _dimensionAction3.insertIntoVariantMap(variantMap); _colorMap1DAction.insertIntoVariantMap(variantMap); _colorMap2DAction.insertIntoVariantMap(variantMap); diff --git a/src/ColoringAction.h b/src/ColoringAction.h index 4a1d480..a1de452 100644 --- a/src/ColoringAction.h +++ b/src/ColoringAction.h @@ -66,6 +66,12 @@ class ColoringAction : public VerticalGroupAction /** Update the color by action options */ void updateColorByActionOptions(); + /** Enable/disable the color space and channel picker actions for the current coloring mode */ + void updateChannelActionsReadOnly(); + + /** Set the dimension pickers to sensible defaults (the first channels) for the current color space */ + void applyDefaultChannels(); + /** Update the colors of the points in the scatter plot widget */ void updateScatterPlotWidgetColors(); @@ -119,7 +125,10 @@ class ColoringAction : public VerticalGroupAction OptionAction& getColorByAction() { return _colorByAction; } ColorAction& getConstantColorAction() { return _constantColorAction; } + OptionAction& getColorSpaceAction() { return _colorSpaceAction; } DimensionPickerAction& getDimensionAction() { return _dimensionAction; } + DimensionPickerAction& getDimensionAction2() { return _dimensionAction2; } + DimensionPickerAction& getDimensionAction3() { return _dimensionAction3; } ColorMapAction& getColorMap1DAction() { return _colorMap1DAction; } ColorMapAction& getColorMap2DAction() { return _colorMap2DAction; } @@ -131,7 +140,10 @@ class ColoringAction : public VerticalGroupAction ColorSourceModel _colorByModel; /** Color by model (model input for the color by action) */ OptionAction _colorByAction; /** Action for picking the coloring type */ ColorAction _constantColorAction; /** Action for picking the constant color */ - DimensionPickerAction _dimensionAction; /** Dimension picker action */ + OptionAction _colorSpaceAction; /** Color space for data coloring (Scalar 1D / Duo 2D / RGB) */ + DimensionPickerAction _dimensionAction; /** Dimension picker action (color channel 1) */ + DimensionPickerAction _dimensionAction2; /** Dimension picker action (color channel 2, for Duo/RGB) */ + DimensionPickerAction _dimensionAction3; /** Dimension picker action (color channel 3, for RGB) */ ColorMap1DAction _colorMap1DAction; /** One-dimensional color map action */ ColorMap2DAction _colorMap2DAction; /** Two-dimensional color map action */ Dataset _currentColorPointsDataset; /** Current color dataset */ @@ -145,4 +157,4 @@ class ColoringAction : public VerticalGroupAction Q_DECLARE_METATYPE(ColoringAction) -inline const auto coloringActionMetaTypeId = qRegisterMetaType("ColoringAction"); \ No newline at end of file +inline const auto coloringActionMetaTypeId = qRegisterMetaType("ColoringAction"); diff --git a/src/MappingUtils.cpp b/src/MappingUtils.cpp index e238064..5483a24 100644 --- a/src/MappingUtils.cpp +++ b/src/MappingUtils.cpp @@ -14,7 +14,7 @@ #include #include -std::pair getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping) { +std::pair getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping) { const std::vector& linkedDatas = source->getLinkedData(); if (linkedDatas.empty()) @@ -34,18 +34,18 @@ std::pair getSelectionMapping(const mv::Dat return { nullptr, 0 }; } -std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions) { - auto testTargetAndParent = [](const mv::LinkedData& linkedData, const mv::Dataset& positions) -> bool { +std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions) { + auto testTargetAndParent = [](const mv::LinkedData& linkedData, const mv::Dataset& positions_) -> bool { const mv::Dataset mapTargetData = linkedData.getTargetDataset(); - return mapTargetData == positions || parentHasSameNumPoints(mapTargetData, positions); + return mapTargetData == positions_ || parentHasSameNumPoints(mapTargetData, positions_); }; return getSelectionMapping(colors, positions, testTargetAndParent); } -std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors) { - auto testTarget = [](const mv::LinkedData& linkedData, const mv::Dataset& colors) -> bool { - return linkedData.getTargetDataset() == colors; +std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors) { + auto testTarget = [](const mv::LinkedData& linkedData, const mv::Dataset& colors_) -> bool { + return linkedData.getTargetDataset() == colors_; }; auto [mapping, numTargetPoints] = getSelectionMapping(positions, colors, testTarget); @@ -58,7 +58,7 @@ std::pair getSelectionMappingPositionsToCol return { mapping, numTargetPoints }; } -std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors) { +std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors) { if (!positions->isDerivedData()) return { nullptr, 0 }; @@ -77,7 +77,7 @@ bool checkSurjectiveMapping(const mv::LinkedData& linkedData, const std::uint32_ std::uint32_t count = 0; for (const auto& [key, vec] : linkedMap) { - for (std::uint32_t val : vec) { + for (const std::uint32_t val : vec) { if (val >= numPointsInTarget) continue; // Skip values that are too large if (!found[val]) { diff --git a/src/MappingUtils.h b/src/MappingUtils.h index 62a4cba..6229dcb 100644 --- a/src/MappingUtils.h +++ b/src/MappingUtils.h @@ -39,17 +39,17 @@ using LinkedDataCondition = std::function getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping); +std::pair getSelectionMapping(const mv::Dataset& source, const mv::Dataset& target, LinkedDataCondition checkMapping); // Returns a mapping (linked data) from colors whose target is positions or whose target's parent has the same number of points as positions -std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions); +std::pair getSelectionMappingColorsToPositions(const mv::Dataset& colors, const mv::Dataset& positions); // Returns a mapping (linked data) from positions whose target is colors or // a mapping from positions' parent whose target is colors if the number of data points match -std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors); +std::pair getSelectionMappingPositionsToColors(const mv::Dataset& positions, const mv::Dataset& colors); // Returns a mapping (linked data) from positions' source data whose target is colors -std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors); +std::pair getSelectionMappingPositionSourceToColors(const mv::Dataset& positions, const mv::Dataset& colors); // Check if the mapping is surjective, i.e. hits all elements in the target bool checkSurjectiveMapping(const mv::LinkedData& linkedData, const std::uint32_t numPointsInTarget); diff --git a/src/PointPlotAction.cpp b/src/PointPlotAction.cpp index e5c33ab..2425ab6 100644 --- a/src/PointPlotAction.cpp +++ b/src/PointPlotAction.cpp @@ -162,6 +162,10 @@ void PointPlotAction::initialize(ScatterplotPlugin* scatterplotPlugin) connect(&_opacityAction, &ScalarAction::magnitudeChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); connect(&_opacityAction, &ScalarAction::offsetChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); connect(&_opacityAction, &ScalarAction::sourceSelectionChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); + connect(&_opacityAction.getSourceDatasetPickerAction(), &DatasetPickerAction::datasetPicked, this, [this](Dataset<> picked) -> void { + setCurrentPointOpacityDataset(Dataset(picked)); + }); + connect(&_opacityAction, &ScalarAction::sourceDataChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); connect(&_opacityAction, &ScalarAction::scalarRangeChanged, this, &PointPlotAction::updateScatterPlotWidgetPointOpacityScalars); } diff --git a/src/ScalarAction.cpp b/src/ScalarAction.cpp index f500bac..24f86b6 100644 --- a/src/ScalarAction.cpp +++ b/src/ScalarAction.cpp @@ -29,13 +29,16 @@ ScalarAction::ScalarAction(QObject* parent, const QString& title, const float& m if (auto scatterplotPlugin = dynamic_cast(findPluginAncestor())) { auto positionDataset = scatterplotPlugin->getPositionDataset(); auto scalarSourcePointsDataset = Dataset(getCurrentDataset()); - const auto numScalars = scalarSourcePointsDataset->getNumPoints(); - const auto numPositions = positionDataset->getNumPoints(); - if (numScalars != numPositions) { - emitSourceSelectionChanged = false; + if (scalarSourcePointsDataset.isValid() && positionDataset.isValid()) { + const auto numScalars = scalarSourcePointsDataset->getNumPoints(); + const auto numPositions = positionDataset->getNumPoints(); - scatterplotPlugin->addNotification(QString("The number of points in the scalar source dataset does not match the number of points in the position dataset. (numPositions=%1, numScalars:%2)").arg(QString::number(numPositions), QString::number(numScalars))); + if (numScalars != numPositions) { + emitSourceSelectionChanged = false; + + scatterplotPlugin->addNotification(QString("The number of points in the scalar source dataset does not match the number of points in the position dataset. (numPositions=%1, numScalars:%2)").arg(QString::number(numPositions), QString::number(numScalars))); + } } } } else { @@ -184,7 +187,7 @@ void ScalarAction::fromVariantMap(const QVariantMap& variantMap) _magnitudeAction.fromParentVariantMap(variantMap); _sourceAction.fromParentVariantMap(variantMap); - _sourceDatasetPickerAction.fromParentVariantMap(variantMap); + //_sourceDatasetPickerAction.fromParentVariantMap(variantMap); } QVariantMap ScalarAction::toVariantMap() const @@ -193,7 +196,7 @@ QVariantMap ScalarAction::toVariantMap() const _magnitudeAction.insertIntoVariantMap(variantMap); _sourceAction.insertIntoVariantMap(variantMap); - _sourceDatasetPickerAction.insertIntoVariantMap(variantMap); + //_sourceDatasetPickerAction.insertIntoVariantMap(variantMap); return variantMap; } diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 64b494b..a6808a6 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -34,10 +35,28 @@ #include #include #include +#include #include #include #include +#ifdef __cpp_lib_execution +#ifdef __GNUC__ // both TBB and Qt define emit keyword: undef +#undef emit +#endif +#include +#ifdef __GNUC__ // both TBB and Qt define emit keyword: def again +#define emit +#endif +#ifdef NDEBUG +#define MV_SCATTER_PARALLEL_EXECUTION std::execution::par, +#else +#define MV_SCATTER_PARALLEL_EXECUTION std::execution::seq, +#endif +#else +#define MV_SCATTER_PARALLEL_EXECUTION +#endif + #define VIEW_SAMPLING_HTML //#define VIEW_SAMPLING_WIDGET @@ -150,10 +169,10 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : if (datasetsMimeData == nullptr) return dropRegions; - if (datasetsMimeData->getDatasets().count() > 1) + if (datasetsMimeData->getDatasetsCount() != 1) return dropRegions; - const auto dataset = datasetsMimeData->getDatasets().first(); + const auto& dataset = datasetsMimeData->getDatasetsRef().first(); const auto datasetGuiName = dataset->text(); const auto datasetId = dataset->getId(); const auto dataType = dataset->getDataType(); @@ -253,30 +272,34 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) : else { if (candidateDataset.isValid()) { - // Check to set whether the number of data points comprised throughout all clusters is the same number - // as the number of data points in the dataset we are trying to color - int totalNumIndices = 0; - for (const Cluster& cluster : candidateDataset->getClusters()) - { - totalNumIndices += cluster.getIndices().size(); - } - - int totalNumPoints = 0; - if (_positionDataset->isDerivedData()) - totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); - else - totalNumPoints = _positionDataset->getFullDataset()->getNumPoints(); - - // First check if cross-dataset metadata coloring is possible - if (events().areDatasetsPartOfSelectionGroup(getTopDataset(_positionDataset), getTopDataset(candidateDataset))) - { - // Use the clusters set for points color - dropRegions << new DropWidget::DropRegion(this, "Color", description, "palette", true, [this, candidateDataset]() { - _settingsAction->getColoringAction().addColorDataset(candidateDataset); - _settingsAction->getColoringAction().setCurrentColorDataset(candidateDataset); - }); - } - else if (totalNumIndices == totalNumPoints) + // Check that the max index in the cluster data does not exceed the max index of the shown point data + auto getMaxIndex = [](const QVector& clusters) -> std::uint32_t + { + if (clusters.empty()) + return std::numeric_limits::lowest(); + + std::vector clusterIndicesMax(clusters.size()); + + std::transform( + MV_SCATTER_PARALLEL_EXECUTION + clusters.cbegin(), clusters.cend(), + clusterIndicesMax.begin(), + [](const Cluster& cluster) -> std::uint32_t { + const std::vector& indices = cluster.getIndices(); + if (indices.empty()) + return std::numeric_limits::lowest(); + + return *std::ranges::max_element(indices); + }); + + return *std::max_element( + MV_SCATTER_PARALLEL_EXECUTION + clusterIndicesMax.cbegin(), clusterIndicesMax.cend()); + }; + + const auto maxIndex = getMaxIndex(candidateDataset->getClusters()); + + if (maxIndex < numTotalPoints()) { // Use the clusters set for points color dropRegions << new DropWidget::DropRegion(this, "Color", description, "palette", true, [this, candidateDataset]() { @@ -589,7 +612,7 @@ void ScatterplotPlugin::selectPoints() auto& navigationAction = navigator.getNavigationAction(); - navigationAction.getZoomSelectionAction().setEnabled(!targetSelectionIndices.empty() && !navigationAction.getFreezeNavigation().isChecked()); + navigationAction.getZoomSelectionAction().setEnabled(!targetSelectionIndices.empty() && navigationAction.isNavigationActive()); _positionDataset->setSelectionIndices(targetSelectionIndices); @@ -717,13 +740,8 @@ void ScatterplotPlugin::positionDatasetChanged() if (!_positionDataset.isValid()) return; - // Reset dataset references - //_positionSourceDataset.reset(); - - // Set position source dataset reference when the position dataset is derived - //if (_positionDataset->isDerivedData()) _positionSourceDataset = _positionDataset->getSourceDataset(); - + _numPoints = _positionDataset->getNumPoints(); _scatterPlotWidget->getPointRendererNavigator().resetView(true); @@ -732,16 +750,26 @@ void ScatterplotPlugin::positionDatasetChanged() updateData(); } -void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std::uint32_t& dimensionIndex) +std::uint64_t ScatterplotPlugin::numTotalPoints() const +{ + if (!_positionDataset.isValid()) + return 0; + + return _positionDataset->isDerivedData() + ? _positionSourceDataset->getFullDataset()->getNumPoints() + : _positionDataset->getFullDataset()->getNumPoints(); +} + +bool ScatterplotPlugin::mapColorScalars(const Dataset& pointsColor, const std::uint32_t& dimensionIndex, std::vector& colorScalars) { // Only proceed with valid points dataset if (!pointsColor.isValid()) - return; + return false; const auto numColorPoints = pointsColor->getNumPoints(); // Generate point colorScalars for color mapping - std::vector colorScalars = {}; + colorScalars.clear(); pointsColor->extractDataForDimension(colorScalars, dimensionIndex); // If number of points do not match, use a mapping @@ -775,7 +803,7 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std const mv::SelectionMap::Map& mapColorsToPositions = selectionMapping->getMapping().getMap(); for (const auto& [fromColorID, vecOfPositionIDs] : mapColorsToPositions) { - for (std::uint32_t toPositionID : vecOfPositionIDs) { + for (const std::uint32_t toPositionID : vecOfPositionIDs) { mappedColorScalars[toPositionID] = colorScalars[fromColorID]; } } @@ -795,7 +823,7 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std for (const auto& [fromPositionID, vecOfColorIDs] : mapPositionsToColors) { if (mappedColorScalars[fromPositionID] != std::numeric_limits::lowest()) continue; - for (std::uint32_t toColorID : vecOfColorIDs) { + for (const std::uint32_t toColorID : vecOfColorIDs) { mappedColorScalars[fromPositionID] = colorScalars[toColorID]; } } @@ -834,14 +862,12 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std } catch (const std::exception& e) { - qDebug() << "ScatterplotPlugin::loadColors: mapping failed -> " << e.what(); - _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant - return; + qDebug() << "ScatterplotPlugin::mapColorScalars: mapping failed -> " << e.what(); + return false; } catch (...) { - qDebug() << "ScatterplotPlugin::loadColors: mapping failed for an unknown reason."; - _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant - return; + qDebug() << "ScatterplotPlugin::mapColorScalars: mapping failed for an unknown reason."; + return false; } std::swap(mappedColorScalars, colorScalars); @@ -849,6 +875,18 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std assert(colorScalars.size() == _numPoints); + return true; +} + +void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std::uint32_t& dimensionIndex) +{ + std::vector colorScalars = {}; + + if (!mapColorScalars(pointsColor, dimensionIndex, colorScalars)) { + _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant + return; + } + // Assign colorScalars and scalar effect _scatterPlotWidget->setScalars(colorScalars); _scatterPlotWidget->setScalarEffect(PointEffect::Color); @@ -859,6 +897,49 @@ void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std getWidget().update(); } +void ScatterplotPlugin::loadColors2D(const Dataset& pointsColor, const std::uint32_t& dimensionIndexX, const std::uint32_t& dimensionIndexY) +{ + std::vector colorScalarsX = {}; + std::vector colorScalarsY = {}; + + if (!mapColorScalars(pointsColor, dimensionIndexX, colorScalarsX) || + !mapColorScalars(pointsColor, dimensionIndexY, colorScalarsY)) { + _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant + return; + } + + // Assign both channels and the two-channel 2D coloring effect + _scatterPlotWidget->setScalars(colorScalarsX); + _scatterPlotWidget->setScalars2(colorScalarsY); + _scatterPlotWidget->setScalarEffect(PointEffect::Color2DChannels); + + // Render + getWidget().update(); +} + +void ScatterplotPlugin::loadColorsRGB(const Dataset& pointsColor, const std::uint32_t& dimensionIndexR, const std::uint32_t& dimensionIndexG, const std::uint32_t& dimensionIndexB) +{ + std::vector colorScalarsR = {}; + std::vector colorScalarsG = {}; + std::vector colorScalarsB = {}; + + if (!mapColorScalars(pointsColor, dimensionIndexR, colorScalarsR) || + !mapColorScalars(pointsColor, dimensionIndexG, colorScalarsG) || + !mapColorScalars(pointsColor, dimensionIndexB, colorScalarsB)) { + _settingsAction->getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant + return; + } + + // Assign the three channels and the RGB coloring effect + _scatterPlotWidget->setScalars(colorScalarsR); + _scatterPlotWidget->setScalars2(colorScalarsG); + _scatterPlotWidget->setScalars3(colorScalarsB); + _scatterPlotWidget->setScalarEffect(PointEffect::ColorRGB); + + // Render + getWidget().update(); +} + void ScatterplotPlugin::loadColors(const Dataset& clusters) { // Only proceed with valid clusters and position dataset @@ -904,11 +985,7 @@ void ScatterplotPlugin::loadColors(const Dataset& clusters) } // Get global indices from the position dataset - int totalNumPoints = 0; - if (_positionDataset->isDerivedData()) - totalNumPoints = _positionSourceDataset->getFullDataset()->getNumPoints(); - else - totalNumPoints = _positionDataset->getFullDataset()->getNumPoints(); + const std::uint64_t totalNumPoints = numTotalPoints(); // Mapping from local to global indices std::vector globalIndices; @@ -920,18 +997,17 @@ void ScatterplotPlugin::loadColors(const Dataset& clusters) const auto& clusterVec = clusters->getClusters(); - if (totalNumPoints == _numPoints && clusterVec.size() == totalNumPoints) + if (totalNumPoints == _numPoints && static_cast(clusterVec.size()) == totalNumPoints) { - for (size_t i = 0; i < static_cast(clusterVec.size()); i++) + // Each cluster corresponds to one point + for (const auto& cluster : clusterVec) { - const auto& cluster = clusterVec[i]; const auto color = cluster.getColor(); - localColors[cluster.getIndices()[0]] = Vector3f(color.redF(), color.greenF(), color.blueF()); } } - else if(globalIndices.size() == _numPoints) + else { // Loop over all clusters and populate global colors for (const auto& cluster : clusterVec) diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index 8649f82..c04e6a8 100644 --- a/src/ScatterplotPlugin.h +++ b/src/ScatterplotPlugin.h @@ -63,6 +63,23 @@ class ScatterplotPlugin : public ViewPlugin */ void loadColors(const Dataset& points, const std::uint32_t& dimensionIndex); + /** + * Load 2D color from two dimensions of a points dataset (mapped through the 2D color map) + * @param points Smart pointer to points dataset + * @param dimensionIndexX Index of the dimension mapped to the color map x-axis + * @param dimensionIndexY Index of the dimension mapped to the color map y-axis + */ + void loadColors2D(const Dataset& points, const std::uint32_t& dimensionIndexX, const std::uint32_t& dimensionIndexY); + + /** + * Load RGB color from three dimensions of a points dataset + * @param points Smart pointer to points dataset + * @param dimensionIndexR Index of the dimension mapped to red + * @param dimensionIndexG Index of the dimension mapped to green + * @param dimensionIndexB Index of the dimension mapped to blue + */ + void loadColorsRGB(const Dataset& points, const std::uint32_t& dimensionIndexR, const std::uint32_t& dimensionIndexG, const std::uint32_t& dimensionIndexB); + /** * Load color from clusters dataset * @param clusters Smart pointer to clusters dataset @@ -113,13 +130,30 @@ class ScatterplotPlugin : public ViewPlugin */ QVariantMap toVariantMap() const override; +private: + + /** + * Extract dimension \p dimensionIndex from \p pointsColor and map it into the position dataset's point space + * @param pointsColor Smart pointer to the color points dataset + * @param dimensionIndex Index of the dimension to extract + * @param colorScalars Output vector of scalars, sized to the number of position points on success + * @return Boolean determining whether the mapping succeeded + */ + bool mapColorScalars(const Dataset& pointsColor, const std::uint32_t& dimensionIndex, std::vector& colorScalars); + + /** + * Number of points in positions data set (might be more than _numPoints) + * @return Number of points in positions data set (might be more than _numPoints) + */ + std::uint64_t numTotalPoints() const; + private: mv::gui::DropWidget* _dropWidget; /** Widget for dropping datasets */ ScatterplotWidget* _scatterPlotWidget; /** The visualization widget */ Dataset _positionDataset; /** Smart pointer to points dataset for point position */ Dataset _positionSourceDataset; /** Smart pointer to source of the points dataset for point position (if any) */ std::vector _positions; /** Point positions */ - unsigned int _numPoints; /** Number of point positions */ + std::uint64_t _numPoints; /** Number of point positions */ QPointer _settingsAction; /** Group action for all settings */ QPointer _primaryToolbarAction; /** Horizontal toolbar for primary content */ QRectF _selectionBoundaries; /** Boundaries of the selection */ diff --git a/src/ScatterplotWidget.cpp b/src/ScatterplotWidget.cpp index ff6f6f8..837ed7c 100644 --- a/src/ScatterplotWidget.cpp +++ b/src/ScatterplotWidget.cpp @@ -328,7 +328,21 @@ void ScatterplotWidget::setHighlights(const std::vector& highlights, const void ScatterplotWidget::setScalars(const std::vector& scalars) { _pointRenderer.setColorChannelScalars(scalars); - + + update(); +} + +void ScatterplotWidget::setScalars2(const std::vector& scalars) +{ + _pointRenderer.setColorChannel2Scalars(scalars); + + update(); +} + +void ScatterplotWidget::setScalars3(const std::vector& scalars) +{ + _pointRenderer.setColorChannel3Scalars(scalars); + update(); } diff --git a/src/ScatterplotWidget.h b/src/ScatterplotWidget.h index 9e73b49..4441bd6 100644 --- a/src/ScatterplotWidget.h +++ b/src/ScatterplotWidget.h @@ -80,6 +80,12 @@ class ScatterplotWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_C void setHighlights(const std::vector& highlights, const std::int32_t& numSelectedPoints); void setScalars(const std::vector& scalars); + /** Set the second color scalar channel (used for 2D and RGB coloring) */ + void setScalars2(const std::vector& scalars); + + /** Set the third color scalar channel (used for RGB coloring) */ + void setScalars3(const std::vector& scalars); + /** * Set colors for each individual data point * @param colors Vector of colors (size must match that of the loaded points dataset) diff --git a/src/SettingsAction.cpp b/src/SettingsAction.cpp index 03d0f82..f0739dc 100644 --- a/src/SettingsAction.cpp +++ b/src/SettingsAction.cpp @@ -70,8 +70,8 @@ void SettingsAction::fromVariantMap(const QVariantMap& variantMap) _plotAction.fromParentVariantMap(variantMap); _positionAction.fromParentVariantMap(variantMap); _coloringAction.fromParentVariantMap(variantMap); - _subsetAction.fromParentVariantMap(variantMap); - _clusteringAction.fromParentVariantMap(variantMap); + _subsetAction.fromParentVariantMap(variantMap, true); + _clusteringAction.fromParentVariantMap(variantMap, true); _renderModeAction.fromParentVariantMap(variantMap); _selectionAction.fromParentVariantMap(variantMap); _miscellaneousAction.fromParentVariantMap(variantMap);