Skip to content

Commit 8a7f8ec

Browse files
alxvthSreeparna Deb
authored andcommitted
Color by selection mapping (#186)
* WIP: color by selection mapping * Add explanations * Delay check * Better be safe * Add Reverse mapping as well * Fix false parent * Move new functions to own file * Centralize & inline check * Simpler return types and checks * Single swap * WIP: map means to HSNE embedding * Fix mapping from source of position to color by linked data * Remove debug prints * Delay surjection check for faster UI response
1 parent 3f48e83 commit 8a7f8ec

File tree

4 files changed

+294
-34
lines changed

4 files changed

+294
-34
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ find_package(ManiVault COMPONENTS Core PointData ClusterData ColorData ImageData
4040
set(PLUGIN
4141
src/ScatterplotPlugin.h
4242
src/ScatterplotPlugin.cpp
43+
src/MappingUtils.h
44+
src/MappingUtils.cpp
4345
)
4446

4547
set(UI

src/MappingUtils.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#include "MappingUtils.h"
2+
3+
#include <Dataset.h>
4+
#include <LinkedData.h>
5+
#include <PointData/PointData.h>
6+
#include <Set.h>
7+
8+
#include <algorithm>
9+
#include <cstdint>
10+
#include <functional>
11+
#include <map>
12+
#include <tuple>
13+
#include <ranges>
14+
#include <utility>
15+
#include <vector>
16+
17+
std::pair<const mv::LinkedData*, unsigned int> getSelectionMapping(const mv::Dataset<Points>& source, const mv::Dataset<Points>& target, LinkedDataCondition checkMapping) {
18+
const std::vector<mv::LinkedData>& linkedDatas = source->getLinkedData();
19+
20+
if (linkedDatas.empty())
21+
return { nullptr, 0 } ;
22+
23+
// find linked data between source and target OR source and target's parent, if target is derived and they have the same number of points
24+
if (const auto result = std::ranges::find_if(
25+
linkedDatas,
26+
[&target, &checkMapping](const mv::LinkedData& linkedData) -> bool {
27+
return checkMapping(linkedData, target);
28+
});
29+
result != linkedDatas.end())
30+
{
31+
return {&(*result), target->getNumPoints() };
32+
}
33+
34+
return { nullptr, 0 };
35+
}
36+
37+
std::pair<const mv::LinkedData*, unsigned int> getSelectionMappingColorsToPositions(const mv::Dataset<Points>& colors, const mv::Dataset<Points>& positions) {
38+
auto testTargetAndParent = [](const mv::LinkedData& linkedData, const mv::Dataset<Points>& positions) -> bool {
39+
const mv::Dataset<mv::DatasetImpl> mapTargetData = linkedData.getTargetDataset();
40+
return mapTargetData == positions || parentHasSameNumPoints(mapTargetData, positions);
41+
};
42+
43+
return getSelectionMapping(colors, positions, testTargetAndParent);
44+
}
45+
46+
std::pair<const mv::LinkedData*, unsigned int> getSelectionMappingPositionsToColors(const mv::Dataset<Points>& positions, const mv::Dataset<Points>& colors) {
47+
auto testTarget = [](const mv::LinkedData& linkedData, const mv::Dataset<Points>& colors) -> bool {
48+
return linkedData.getTargetDataset() == colors;
49+
};
50+
51+
auto [mapping, numTargetPoints] = getSelectionMapping(positions, colors, testTarget);
52+
53+
if (mapping && parentHasSameNumPoints(positions, positions)) {
54+
const auto positionsParent = positions->getParent<Points>();
55+
std::tie(mapping, numTargetPoints) = getSelectionMapping(positionsParent, colors, testTarget);
56+
}
57+
58+
return { mapping, numTargetPoints };
59+
}
60+
61+
std::pair<const mv::LinkedData*, unsigned int> getSelectionMappingPositionSourceToColors(const mv::Dataset<Points>& positions, const mv::Dataset<Points>& colors) {
62+
if (!positions->isDerivedData())
63+
return { nullptr, 0 };
64+
65+
const auto fullSourceData = positions->getSourceDataset<Points>()->getFullDataset<Points>();
66+
67+
if(!fullSourceData.isValid())
68+
return { nullptr, 0 };
69+
70+
return getSelectionMappingPositionsToColors(fullSourceData, colors);
71+
}
72+
73+
bool checkSurjectiveMapping(const mv::LinkedData& linkedData, const std::uint32_t numPointsInTarget) {
74+
const std::map<std::uint32_t, std::vector<std::uint32_t>>& linkedMap = linkedData.getMapping().getMap();
75+
76+
std::vector<bool> found(numPointsInTarget, false);
77+
std::uint32_t count = 0;
78+
79+
for (const auto& [key, vec] : linkedMap) {
80+
for (std::uint32_t val : vec) {
81+
if (val >= numPointsInTarget) continue; // Skip values that are too large
82+
83+
if (!found[val]) {
84+
found[val] = true;
85+
if (++count == numPointsInTarget)
86+
return true;
87+
}
88+
}
89+
}
90+
91+
return false; // The previous loop would have returned early if the entire taget set was covered
92+
}
93+
94+
bool checkSelectionMapping(const mv::Dataset<Points>& colors, const mv::Dataset<Points>& positions) {
95+
96+
// Check if there is a mapping
97+
auto [mapping, numTargetPoints] = getSelectionMappingColorsToPositions(colors, positions);
98+
99+
if (!mapping)
100+
std::tie(mapping, numTargetPoints) = getSelectionMappingPositionsToColors(positions, colors);
101+
102+
if (!mapping)
103+
std::tie(mapping, numTargetPoints) = getSelectionMappingPositionSourceToColors(positions, colors);
104+
105+
if (!mapping)
106+
return false;
107+
108+
return true;
109+
}

src/MappingUtils.h

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#pragma once
2+
3+
#include <Dataset.h>
4+
#include <LinkedData.h>
5+
#include <PointData/PointData.h>
6+
#include <Set.h>
7+
8+
#include <cstdint>
9+
#include <functional>
10+
#include <utility>
11+
12+
// This only checks the immedeate parent and is deliberately not recursive
13+
// We might consider the latter in the future, but might need to cover edge cases
14+
inline bool parentHasSameNumPoints(const mv::Dataset<mv::DatasetImpl> data, const mv::Dataset<Points>& other) {
15+
if (!data->isDerivedData())
16+
return false;
17+
18+
const auto parent = data->getParent();
19+
if (parent->getDataType() != PointType)
20+
return false;
21+
22+
const auto parentPoints = mv::Dataset<Points>(parent);
23+
return parentPoints->getNumPoints() == other->getNumPoints();
24+
}
25+
26+
// Is the data derived and does it's full source data have same number of points as the other data
27+
inline bool fullSourceHasSameNumPoints(const mv::Dataset<mv::DatasetImpl> data, const mv::Dataset<Points>& other) {
28+
if (!data->isDerivedData())
29+
return false;
30+
31+
return data->getSourceDataset<Points>()->getFullDataset<Points>()->getNumPoints() == other->getNumPoints();
32+
}
33+
34+
using LinkedDataCondition = std::function<bool(const mv::LinkedData& linkedData, const mv::Dataset<Points>& target)>;
35+
36+
/* Returns a mapping (linked data) from source that fulfils a given condition based on target, e.g.
37+
auto checkMapping = [](const mv::LinkedData& linkedData, const mv::Dataset<Points>& target) -> bool {
38+
return linkedData.getTargetDataset() == target;
39+
};
40+
This function will return the first match of the condition
41+
*/
42+
std::pair<const mv::LinkedData*, unsigned int> getSelectionMapping(const mv::Dataset<Points>& source, const mv::Dataset<Points>& target, LinkedDataCondition checkMapping);
43+
44+
// Returns a mapping (linked data) from colors whose target is positions or whose target's parent has the same number of points as positions
45+
std::pair<const mv::LinkedData*, unsigned int> getSelectionMappingColorsToPositions(const mv::Dataset<Points>& colors, const mv::Dataset<Points>& positions);
46+
47+
// Returns a mapping (linked data) from positions whose target is colors or
48+
// a mapping from positions' parent whose target is colors if the number of data points match
49+
std::pair<const mv::LinkedData*, unsigned int> getSelectionMappingPositionsToColors(const mv::Dataset<Points>& positions, const mv::Dataset<Points>& colors);
50+
51+
// Returns a mapping (linked data) from positions' source data whose target is colors
52+
std::pair<const mv::LinkedData*, unsigned int> getSelectionMappingPositionSourceToColors(const mv::Dataset<Points>& positions, const mv::Dataset<Points>& colors);
53+
54+
// Check if the mapping is surjective, i.e. hits all elements in the target
55+
bool checkSurjectiveMapping(const mv::LinkedData& linkedData, const std::uint32_t numPointsInTarget);
56+
57+
// returns whether there is a selection map from colors to positions or positions to colors (or respective parents)
58+
// checks whether the mapping covers all elements in the target
59+
bool checkSelectionMapping(const mv::Dataset<Points>& colors, const mv::Dataset<Points>& positions);

src/ScatterplotPlugin.cpp

Lines changed: 124 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "ScatterplotPlugin.h"
22

3+
#include "MappingUtils.h"
34
#include "ScatterplotWidget.h"
45

56
#include <Application.h>
@@ -31,6 +32,9 @@
3132

3233
#include <algorithm>
3334
#include <cassert>
35+
#include <exception>
36+
#include <map>
37+
#include <stdexcept>
3438
#include <vector>
3539

3640
#define VIEW_SAMPLING_HTML
@@ -172,25 +176,35 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) :
172176
});
173177
}
174178

175-
// Accept both data with the same number if points and data which is derived from
176-
// a parent that has the same number of points (e.g. for HSNE embeddings)
179+
// Accept for recoloring:
180+
// 1. data with the same number of points
181+
// 2. data which is derived from a parent that has the same number of points (e.g. for HSNE embeddings), where we can use global indices for mapping
182+
// 3. data which has a fully-covering selection mapping, that we can use for setting colors. Mapping in order of preference:
183+
// a) from color (or it's parent) to position
184+
// b) from color to position (or it's parent)
185+
// c) from source of position to color
186+
187+
// [1. Same number of points]
177188
const auto numPointsCandidate = candidateDataset->getNumPoints();
178189
const auto numPointsPosition = _positionDataset->getNumPoints();
179-
const bool sameNumPoints = numPointsPosition == numPointsCandidate;
180-
const bool sameNumPointsAsFull =
181-
/*if*/ _positionDataset->isDerivedData() ?
182-
/*then*/ _positionDataset->getSourceDataset<Points>()->getFullDataset<Points>()->getNumPoints() == numPointsCandidate :
183-
/*else*/ false;
190+
const bool hasSameNumPoints = numPointsPosition == numPointsCandidate;
191+
192+
// [2. Derived from a parent]
193+
const bool hasSameNumPointsAsFull = fullSourceHasSameNumPoints(_positionDataset, candidateDataset);
184194

185-
if (sameNumPoints || sameNumPointsAsFull) {
195+
// [3. Full selection mapping]
196+
const bool hasSelectionMapping = checkSelectionMapping(candidateDataset, _positionDataset);
197+
198+
if (hasSameNumPoints || hasSameNumPointsAsFull || hasSelectionMapping) {
186199
// Offer the option to use the points dataset as source for points colors
187200
dropRegions << new DropWidget::DropRegion(this, "Point color", QString("Colorize %1 points with %2").arg(_positionDataset->text(), candidateDataset->text()), "palette", true, [this, candidateDataset]() {
188201
_settingsAction.getColoringAction().setCurrentColorDataset(candidateDataset); // calls addColorDataset internally
189202
});
190203

191204
}
192205

193-
if (sameNumPoints) {
206+
// Accept for resizing and opacity: Only data with the same number of points
207+
if (hasSameNumPoints) {
194208
// Offer the option to use the points dataset as source for points size
195209
dropRegions << new DropWidget::DropRegion(this, "Point size", QString("Size %1 points with %2").arg(_positionDataset->text(), candidateDataset->text()), "ruler-horizontal", true, [this, candidateDataset]() {
196210
_settingsAction.getPlotAction().getPointPlotAction().setCurrentPointSizeDataset(candidateDataset);
@@ -647,49 +661,125 @@ void ScatterplotPlugin::positionDatasetChanged()
647661
updateData();
648662
}
649663

650-
void ScatterplotPlugin::loadColors(const Dataset<Points>& points, const std::uint32_t& dimensionIndex)
664+
void ScatterplotPlugin::loadColors(const Dataset<Points>& pointsColor, const std::uint32_t& dimensionIndex)
651665
{
652666
// Only proceed with valid points dataset
653-
if (!points.isValid())
667+
if (!pointsColor.isValid())
654668
return;
655669

656-
// Generate point scalars for color mapping
657-
std::vector<float> scalars;
670+
const auto numColorPoints = pointsColor->getNumPoints();
658671

659-
points->extractDataForDimension(scalars, dimensionIndex);
672+
// Generate point colorScalars for color mapping
673+
std::vector<float> colorScalars = {};
674+
pointsColor->extractDataForDimension(colorScalars, dimensionIndex);
660675

661-
const auto numColorPoints = points->getNumPoints();
676+
// If number of points do not match, use a mapping
677+
// prefer global IDs (for derived data) over selection mapping
678+
// prefer color to position over position to color over source of position to color
679+
if (numColorPoints != _numPoints) {
662680

681+
std::vector<float> mappedColorScalars(_numPoints, std::numeric_limits<float>::lowest());
663682

664-
if (numColorPoints != _numPoints) {
683+
try {
684+
const bool hasSameNumPointsAsFull = fullSourceHasSameNumPoints(_positionDataset, pointsColor);
665685

666-
const bool sameNumPointsAsFull =
667-
/*if*/ _positionDataset->isDerivedData() ?
668-
/*then*/ _positionSourceDataset->getFullDataset<Points>()->getNumPoints() == numColorPoints :
669-
/*else*/ false;
686+
if (hasSameNumPointsAsFull) {
687+
std::vector<std::uint32_t> globalIndices = {};
688+
_positionDataset->getGlobalIndices(globalIndices);
670689

671-
if (sameNumPointsAsFull) {
672-
std::vector<std::uint32_t> globalIndices;
673-
_positionDataset->getGlobalIndices(globalIndices);
690+
for (std::int32_t localIndex = 0; localIndex < globalIndices.size(); localIndex++) {
691+
mappedColorScalars[localIndex] = colorScalars[globalIndices[localIndex]];
692+
}
674693

675-
std::vector<float> localScalars(_numPoints, 0);
676-
std::int32_t localColorIndex = 0;
694+
}
695+
else if ( // mapping from color data set to position data set
696+
const auto [selectionMapping, numPointsTarget] = getSelectionMappingColorsToPositions(pointsColor, _positionDataset);
697+
/* check if valid */
698+
selectionMapping != nullptr &&
699+
numPointsTarget == _numPoints &&
700+
checkSurjectiveMapping(*selectionMapping, numPointsTarget)
701+
)
702+
{
703+
// Map values like selection
704+
const mv::SelectionMap::Map& mapColorsToPositions = selectionMapping->getMapping().getMap();
677705

678-
for (const auto& globalIndex : globalIndices)
679-
localScalars[localColorIndex++] = scalars[globalIndex];
706+
for (const auto& [fromColorID, vecOfPositionIDs] : mapColorsToPositions) {
707+
for (std::uint32_t toPositionID : vecOfPositionIDs) {
708+
mappedColorScalars[toPositionID] = colorScalars[fromColorID];
709+
}
710+
}
680711

681-
std::swap(localScalars, scalars);
682-
}
683-
else {
684-
qWarning("Number of points used for coloring does not match number of points in data, aborting attempt to color plot");
712+
}
713+
else if ( // mapping from position data set to color data set
714+
const auto [selectionMapping, numPointsTarget] = getSelectionMappingPositionsToColors(_positionDataset, pointsColor);
715+
/* check if valid */
716+
selectionMapping != nullptr &&
717+
numPointsTarget == numColorPoints &&
718+
checkSurjectiveMapping(*selectionMapping, numPointsTarget)
719+
)
720+
{
721+
// Map values like selection (in reverse, use first value that occurs)
722+
const mv::SelectionMap::Map& mapPositionsToColors = selectionMapping->getMapping().getMap();
723+
724+
for (const auto& [fromPositionID, vecOfColorIDs] : mapPositionsToColors) {
725+
if (mappedColorScalars[fromPositionID] != std::numeric_limits<float>::lowest())
726+
continue;
727+
for (std::uint32_t toColorID : vecOfColorIDs) {
728+
mappedColorScalars[fromPositionID] = colorScalars[toColorID];
729+
}
730+
}
731+
732+
}
733+
else if ( // mapping from source of position data set to color data set
734+
const auto [selectionMapping, numPointsTarget] = getSelectionMappingPositionSourceToColors(_positionDataset, pointsColor);
735+
/* check if valid */
736+
selectionMapping != nullptr &&
737+
numPointsTarget == numColorPoints &&
738+
checkSurjectiveMapping(*selectionMapping, numPointsTarget)
739+
)
740+
{
741+
// the selection map is from full source data of positions data to pointsColor
742+
// we need to use both the global indices of the positions (i.e. in the source) and the linked data mapping
743+
const mv::SelectionMap::Map& mapGlobalToColors = selectionMapping->getMapping().getMap();
744+
std::vector<std::uint32_t> globalIndices = {};
745+
_positionDataset->getGlobalIndices(globalIndices);
746+
747+
for (std::int32_t localIndex = 0; localIndex < globalIndices.size(); localIndex++) {
748+
749+
if (mappedColorScalars[localIndex] != std::numeric_limits<float>::lowest())
750+
continue;
751+
752+
const auto& indxColors = mapGlobalToColors.at(globalIndices[localIndex]); // from full source (parent) to colorDataset
753+
754+
for (const auto& indColors : indxColors) {
755+
mappedColorScalars[localIndex] = colorScalars[indColors];
756+
}
757+
}
758+
759+
}
760+
else {
761+
throw std::runtime_error("Coloring data set does not match position data set in a known way, aborting attempt to color plot");
762+
}
763+
764+
}
765+
catch (const std::exception& e) {
766+
qDebug() << "ScatterplotPlugin::loadColors: mapping failed -> " << e.what();
767+
_settingsAction.getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant
685768
return;
686769
}
770+
catch (...) {
771+
qDebug() << "ScatterplotPlugin::loadColors: mapping failed for an unknown reason.";
772+
_settingsAction.getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant
773+
return;
774+
}
775+
776+
std::swap(mappedColorScalars, colorScalars);
687777
}
688778

689-
assert(scalars.size() == _numPoints);
779+
assert(colorScalars.size() == _numPoints);
690780

691-
// Assign scalars and scalar effect
692-
_scatterPlotWidget->setScalars(scalars);
781+
// Assign colorScalars and scalar effect
782+
_scatterPlotWidget->setScalars(colorScalars);
693783
_scatterPlotWidget->setScalarEffect(PointEffect::Color);
694784

695785
_settingsAction.getColoringAction().updateColorMapActionScalarRange();

0 commit comments

Comments
 (0)