From 0347aedcb650e4af0f37594b96e1e726ffb2379f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Ho=CC=88llt?= Date: Wed, 22 Jul 2026 20:03:49 +0200 Subject: [PATCH 1/7] Initial attemp at 2D and 3D data driven coloring --- src/ColoringAction.cpp | 137 +++++++++++++++++++++++++++++++++++--- src/ColoringAction.h | 11 ++- src/ScatterplotPlugin.cpp | 71 +++++++++++++++++--- src/ScatterplotPlugin.h | 28 ++++++++ src/ScatterplotWidget.cpp | 16 ++++- src/ScatterplotWidget.h | 6 ++ 6 files changed, 249 insertions(+), 20 deletions(-) diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index c3fca1f..cb16685 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(&_colorSpaceAction); addAction(&_colorMap2DAction); addAction(&_colorMap1DAction); 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,37 @@ 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); } 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 +159,22 @@ 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) { + 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 +192,7 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : updateScatterplotWidgetColorMap(); updateColorMapActionScalarRange(); + updateChannelActionsReadOnly(); _scatterplotPlugin->getScatterplotWidget().setColoringMode(ScatterplotWidget::ColoringMode::Constant); } @@ -251,10 +284,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; + } - if (currentDimensionIndex >= 0) - _scatterplotPlugin->loadColors(currentColorDataset.get(), _dimensionAction.getCurrentDimensionIndex()); + case 2: // RGB + { + const auto dimension2 = _dimensionAction2.getCurrentDimensionIndex(); + const auto dimension3 = _dimensionAction3.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 +358,15 @@ void ColoringAction::updateScatterplotWidgetColorMap() scatterplotWidget.setColoringMode(ScatterplotWidget::ColoringMode::Scatter); } else { - scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); + // Data-driven coloring: the color space determines which color map to use. + // Duo uses the 2D color map; Scalar (and RGB, which ignores the color map) uses the 1D color map. + const auto currentColorDataset = getCurrentColorDataset(); + const bool isDuo = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType && _colorSpaceAction.getCurrentIndex() == 1; + + if (isDuo) + scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage()); + else + scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); } break; @@ -351,9 +420,53 @@ 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) && !isDuo && !isRGB); + _colorMap2DAction.setEnabled(shouldEnableColorMap() && (currentIndex == 1 || isDuo)); +} + +void ColoringAction::updateChannelActionsReadOnly() +{ + const auto currentIndex = _colorByAction.getCurrentIndex(); + const auto colorSpace = _colorSpaceAction.getCurrentIndex(); - _colorMap1DAction.setEnabled(shouldEnableColorMap() && (currentIndex >= 2)); - _colorMap2DAction.setEnabled(shouldEnableColorMap() && (currentIndex == 1)); + 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); + + // Dimension picker labels reflect their role in the current color space + if (isRGB) { + _dimensionAction.setText("Red"); + _dimensionAction2.setText("Green"); + _dimensionAction3.setText("Blue"); + } + else if (isDuo) { + _dimensionAction.setText("Dimension 1 (x)"); + _dimensionAction2.setText("Dimension 2 (y)"); + _dimensionAction3.setText("Dimension 3"); + } + else { + _dimensionAction.setText("Dimension"); + _dimensionAction2.setText("Dimension 2"); + _dimensionAction3.setText("Dimension 3"); + } } void ColoringAction::connectToPublicAction(WidgetAction* publicAction, bool recursive) @@ -368,7 +481,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 +500,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); } diff --git a/src/ColoringAction.h b/src/ColoringAction.h index 4a1d480..71b4d23 100644 --- a/src/ColoringAction.h +++ b/src/ColoringAction.h @@ -66,6 +66,9 @@ 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(); + /** Update the colors of the points in the scatter plot widget */ void updateScatterPlotWidgetColors(); @@ -119,7 +122,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 +137,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 */ diff --git a/src/ScatterplotPlugin.cpp b/src/ScatterplotPlugin.cpp index 531eb45..2f18488 100644 --- a/src/ScatterplotPlugin.cpp +++ b/src/ScatterplotPlugin.cpp @@ -713,16 +713,16 @@ void ScatterplotPlugin::positionDatasetChanged() updateData(); } -void ScatterplotPlugin::loadColors(const Dataset& pointsColor, const std::uint32_t& dimensionIndex) +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 @@ -815,14 +815,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); @@ -830,6 +828,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); @@ -840,6 +850,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 diff --git a/src/ScatterplotPlugin.h b/src/ScatterplotPlugin.h index 437876b..ef32af8 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,6 +130,17 @@ 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); + private: mv::gui::DropWidget* _dropWidget; /** Widget for dropping datasets */ ScatterplotWidget* _scatterPlotWidget; /** The visualization widget */ 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) From b182bb8b6807ff5d353aa1b3c58964c6194f15f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Ho=CC=88llt?= Date: Wed, 22 Jul 2026 20:12:30 +0200 Subject: [PATCH 2/7] use the same mirrored colormap for 2D as for the original layout-based one --- src/ColoringAction.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index cb16685..72826d8 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -358,15 +358,7 @@ void ColoringAction::updateScatterplotWidgetColorMap() scatterplotWidget.setColoringMode(ScatterplotWidget::ColoringMode::Scatter); } else { - // Data-driven coloring: the color space determines which color map to use. - // Duo uses the 2D color map; Scalar (and RGB, which ignores the color map) uses the 1D color map. - const auto currentColorDataset = getCurrentColorDataset(); - const bool isDuo = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType && _colorSpaceAction.getCurrentIndex() == 1; - - if (isDuo) - scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage()); - else - scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); + scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); } break; From d3f9e3f47b6dca818463a6dab23b69e3c9a07dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Ho=CC=88llt?= Date: Wed, 22 Jul 2026 20:46:25 +0200 Subject: [PATCH 3/7] Fixes normalization issues --- src/ColoringAction.cpp | 75 ++++++++++++++++++++++++++++++++++++++++-- src/ColoringAction.h | 4 +++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index 72826d8..12e8139 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -103,6 +103,20 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : _dimensionAction.setPointsDataset(_currentColorPointsDataset); _dimensionAction2.setPointsDataset(_currentColorPointsDataset); _dimensionAction3.setPointsDataset(_currentColorPointsDataset); + + // Auto-select the color space for datasets with exactly two or three channels + if (!_restoringState) { + 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()); @@ -169,6 +183,9 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : }); connect(&_colorSpaceAction, &OptionAction::currentIndexChanged, this, [this](const std::int32_t& currentIndex) { + if (!_restoringState) + applyDefaultChannels(); + updateChannelActionsReadOnly(); updateScatterPlotWidgetColors(); updateScatterplotWidgetColorMap(); @@ -358,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().mirrored(false, true)); + else + scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); } break; @@ -385,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 @@ -461,6 +497,41 @@ void ColoringAction::updateChannelActionsReadOnly() } } +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) { auto publicColoringAction = dynamic_cast(publicAction); diff --git a/src/ColoringAction.h b/src/ColoringAction.h index 71b4d23..1e08e4a 100644 --- a/src/ColoringAction.h +++ b/src/ColoringAction.h @@ -69,6 +69,9 @@ class ColoringAction : public VerticalGroupAction /** 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(); @@ -144,6 +147,7 @@ class ColoringAction : public VerticalGroupAction ColorMap1DAction _colorMap1DAction; /** One-dimensional color map action */ ColorMap2DAction _colorMap2DAction; /** Two-dimensional color map action */ Dataset _currentColorPointsDataset; /** Current color dataset */ + bool _restoringState = false; /** Guards auto color-space selection and default channels during state restore */ /** Default constant color */ static const QColor DEFAULT_CONSTANT_COLOR; From ce4d38f5fceb4d2ee7ddcb23a59100d756a067e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Ho=CC=88llt?= Date: Wed, 22 Jul 2026 21:00:09 +0200 Subject: [PATCH 4/7] Adds serialization for new parameters --- src/ColoringAction.cpp | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index 12e8139..e4dfba9 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -379,9 +379,11 @@ void ColoringAction::updateScatterplotWidgetColorMap() const bool isDuo = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType && _colorSpaceAction.getCurrentIndex() == 1; if (isDuo) - scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().mirrored(false, true)); + //scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().mirrored(false, true)); + scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().flipped(Qt::Vertical)); else - scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); + //scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); + scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().flipped(Qt::Vertical)); } break; @@ -575,13 +577,29 @@ void ColoringAction::disconnectFromPublicAction(bool recursive) void ColoringAction::fromVariantMap(const QVariantMap& variantMap) { + // Suppress auto color-space selection and default channels so the saved values are honored + _restoringState = true; + 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); + _dimensionAction3.fromParentVariantMap(variantMap); + _colorSpaceAction.fromParentVariantMap(variantMap); _colorMap1DAction.fromParentVariantMap(variantMap); _colorMap2DAction.fromParentVariantMap(variantMap); + + _restoringState = false; + + // Apply the fully-restored coloring state + updateChannelActionsReadOnly(); + updateScatterPlotWidgetColors(); + updateScatterplotWidgetColorMap(); + updateColorMapActionsReadOnly(); } QVariantMap ColoringAction::toVariantMap() const @@ -590,7 +608,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); From 4813d37c08f98fa5f2b0358634b14557b8d076a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Ho=CC=88llt?= Date: Wed, 22 Jul 2026 21:55:59 +0200 Subject: [PATCH 5/7] Restores Qt 6.8 compatibility --- src/ColoringAction.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index e4dfba9..21d95ac 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -379,11 +379,12 @@ void ColoringAction::updateScatterplotWidgetColorMap() const bool isDuo = currentColorDataset.isValid() && currentColorDataset->getDataType() == PointType && _colorSpaceAction.getCurrentIndex() == 1; if (isDuo) - //scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().mirrored(false, true)); - scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().flipped(Qt::Vertical)); + // mirrored is deprecated in Qt 6.9, flipped can replace it + scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().mirrored(false, true)); + //scatterplotWidget.setColorMap(_colorMap2DAction.getColorMapImage().flipped(Qt::Vertical)); else - //scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); - scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().flipped(Qt::Vertical)); + scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().mirrored(false, true)); + //scatterplotWidget.setColorMap(_colorMap1DAction.getColorMapImage().flipped(Qt::Vertical)); } break; From 6f7848cb28b56d45c5a6fdd45e06de7e9a63c163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Ho=CC=88llt?= Date: Thu, 23 Jul 2026 14:52:39 +0200 Subject: [PATCH 6/7] UI Fixes Orders colormap pickers as 1D, 2D Removes relabeling of dimension picker actions and consistently uses Dimension 1, Dimension 2, and Dimensions 3 --- src/ColoringAction.cpp | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index 21d95ac..383f546 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -17,7 +17,7 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : _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"), + _dimensionAction(this, "Dimension 1"), _dimensionAction2(this, "Dimension 2"), _dimensionAction3(this, "Dimension 3"), _colorMap1DAction(this, "1D Color map"), @@ -30,8 +30,8 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : addAction(&_colorByAction); addAction(&_constantColorAction); addAction(&_colorSpaceAction); - addAction(&_colorMap2DAction); addAction(&_colorMap1DAction); + addAction(&_colorMap2DAction); addAction(&_dimensionAction); addAction(&_dimensionAction2); addAction(&_dimensionAction3); @@ -481,23 +481,6 @@ void ColoringAction::updateChannelActionsReadOnly() _dimensionAction.setEnabled(isPointsSource); _dimensionAction2.setEnabled(isDuo || isRGB); _dimensionAction3.setEnabled(isRGB); - - // Dimension picker labels reflect their role in the current color space - if (isRGB) { - _dimensionAction.setText("Red"); - _dimensionAction2.setText("Green"); - _dimensionAction3.setText("Blue"); - } - else if (isDuo) { - _dimensionAction.setText("Dimension 1 (x)"); - _dimensionAction2.setText("Dimension 2 (y)"); - _dimensionAction3.setText("Dimension 3"); - } - else { - _dimensionAction.setText("Dimension"); - _dimensionAction2.setText("Dimension 2"); - _dimensionAction3.setText("Dimension 3"); - } } void ColoringAction::applyDefaultChannels() From de2722fc0c6b97f8f3796244ac1c021ad5bc4392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Ho=CC=88llt?= Date: Thu, 23 Jul 2026 15:45:01 +0200 Subject: [PATCH 7/7] removes homemade shielding during de-serialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit removes the homemade ‘_restoringState’ bool in favor of `mv::projects().isOpeningProject()`. Bracing `fromVariantMap` with the class bool would break when `fromVariantMap` encounters errors. --- src/ColoringAction.cpp | 9 ++------- src/ColoringAction.h | 3 +-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/ColoringAction.cpp b/src/ColoringAction.cpp index 383f546..71db211 100644 --- a/src/ColoringAction.cpp +++ b/src/ColoringAction.cpp @@ -105,7 +105,7 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : _dimensionAction3.setPointsDataset(_currentColorPointsDataset); // Auto-select the color space for datasets with exactly two or three channels - if (!_restoringState) { + if (!mv::projects().isOpeningProject()) { const auto numDimensions = static_cast(_currentColorPointsDataset->getNumDimensions()); if (numDimensions == 2) { @@ -183,7 +183,7 @@ ColoringAction::ColoringAction(QObject* parent, const QString& title) : }); connect(&_colorSpaceAction, &OptionAction::currentIndexChanged, this, [this](const std::int32_t& currentIndex) { - if (!_restoringState) + if (!mv::projects().isOpeningProject()) applyDefaultChannels(); updateChannelActionsReadOnly(); @@ -561,9 +561,6 @@ void ColoringAction::disconnectFromPublicAction(bool recursive) void ColoringAction::fromVariantMap(const QVariantMap& variantMap) { - // Suppress auto color-space selection and default channels so the saved values are honored - _restoringState = true; - GroupAction::fromVariantMap(variantMap); // Restore the color source first so the dimension pickers are targeted at the right dataset, @@ -577,8 +574,6 @@ void ColoringAction::fromVariantMap(const QVariantMap& variantMap) _colorMap1DAction.fromParentVariantMap(variantMap); _colorMap2DAction.fromParentVariantMap(variantMap); - _restoringState = false; - // Apply the fully-restored coloring state updateChannelActionsReadOnly(); updateScatterPlotWidgetColors(); diff --git a/src/ColoringAction.h b/src/ColoringAction.h index 1e08e4a..a1de452 100644 --- a/src/ColoringAction.h +++ b/src/ColoringAction.h @@ -147,7 +147,6 @@ class ColoringAction : public VerticalGroupAction ColorMap1DAction _colorMap1DAction; /** One-dimensional color map action */ ColorMap2DAction _colorMap2DAction; /** Two-dimensional color map action */ Dataset _currentColorPointsDataset; /** Current color dataset */ - bool _restoringState = false; /** Guards auto color-space selection and default channels during state restore */ /** Default constant color */ static const QColor DEFAULT_CONSTANT_COLOR; @@ -158,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");