Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ find_package(ManiVault COMPONENTS Core PointData ClusterData ColorData ImageData
set(PLUGIN
src/ScatterplotPlugin.h
src/ScatterplotPlugin.cpp
src/MappingUtils.h
src/MappingUtils.cpp
)

set(UI
Expand Down
109 changes: 109 additions & 0 deletions src/MappingUtils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#include "MappingUtils.h"

#include <Dataset.h>
#include <LinkedData.h>
#include <PointData/PointData.h>
#include <Set.h>

#include <algorithm>
#include <cstdint>
#include <functional>
#include <map>
#include <tuple>
#include <ranges>
#include <utility>
#include <vector>

std::pair<const mv::LinkedData*, unsigned int> getSelectionMapping(const mv::Dataset<Points>& source, const mv::Dataset<Points>& target, LinkedDataCondition checkMapping) {
const std::vector<mv::LinkedData>& linkedDatas = source->getLinkedData();

if (linkedDatas.empty())
return { nullptr, 0 } ;

// 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
if (const auto result = std::ranges::find_if(
linkedDatas,
[&target, &checkMapping](const mv::LinkedData& linkedData) -> bool {
return checkMapping(linkedData, target);
});
result != linkedDatas.end())
{
return {&(*result), target->getNumPoints() };
}

return { nullptr, 0 };
}

std::pair<const mv::LinkedData*, unsigned int> getSelectionMappingColorsToPositions(const mv::Dataset<Points>& colors, const mv::Dataset<Points>& positions) {
auto testTargetAndParent = [](const mv::LinkedData& linkedData, const mv::Dataset<Points>& positions) -> bool {
const mv::Dataset<mv::DatasetImpl> mapTargetData = linkedData.getTargetDataset();
return mapTargetData == positions || parentHasSameNumPoints(mapTargetData, positions);
};

return getSelectionMapping(colors, positions, testTargetAndParent);
}

std::pair<const mv::LinkedData*, unsigned int> getSelectionMappingPositionsToColors(const mv::Dataset<Points>& positions, const mv::Dataset<Points>& colors) {
auto testTarget = [](const mv::LinkedData& linkedData, const mv::Dataset<Points>& colors) -> bool {
return linkedData.getTargetDataset() == colors;
};

auto [mapping, numTargetPoints] = getSelectionMapping(positions, colors, testTarget);

if (mapping && parentHasSameNumPoints(positions, positions)) {
const auto positionsParent = positions->getParent<Points>();
std::tie(mapping, numTargetPoints) = getSelectionMapping(positionsParent, colors, testTarget);
}

return { mapping, numTargetPoints };
}

std::pair<const mv::LinkedData*, unsigned int> getSelectionMappingPositionSourceToColors(const mv::Dataset<Points>& positions, const mv::Dataset<Points>& colors) {
if (!positions->isDerivedData())
return { nullptr, 0 };

const auto fullSourceData = positions->getSourceDataset<Points>()->getFullDataset<Points>();

if(!fullSourceData.isValid())
return { nullptr, 0 };

return getSelectionMappingPositionsToColors(fullSourceData, colors);
}

bool checkSurjectiveMapping(const mv::LinkedData& linkedData, const std::uint32_t numPointsInTarget) {
const std::map<std::uint32_t, std::vector<std::uint32_t>>& linkedMap = linkedData.getMapping().getMap();

std::vector<bool> found(numPointsInTarget, false);
std::uint32_t count = 0;

for (const auto& [key, vec] : linkedMap) {
for (std::uint32_t val : vec) {
if (val >= numPointsInTarget) continue; // Skip values that are too large

if (!found[val]) {
found[val] = true;
if (++count == numPointsInTarget)
return true;
}
}
}

return false; // The previous loop would have returned early if the entire taget set was covered
}

bool checkSelectionMapping(const mv::Dataset<Points>& colors, const mv::Dataset<Points>& positions) {

// Check if there is a mapping
auto [mapping, numTargetPoints] = getSelectionMappingColorsToPositions(colors, positions);

if (!mapping)
std::tie(mapping, numTargetPoints) = getSelectionMappingPositionsToColors(positions, colors);

if (!mapping)
std::tie(mapping, numTargetPoints) = getSelectionMappingPositionSourceToColors(positions, colors);

if (!mapping)
return false;

return true;
}
59 changes: 59 additions & 0 deletions src/MappingUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#pragma once

#include <Dataset.h>
#include <LinkedData.h>
#include <PointData/PointData.h>
#include <Set.h>

#include <cstdint>
#include <functional>
#include <utility>

// This only checks the immedeate parent and is deliberately not recursive
// We might consider the latter in the future, but might need to cover edge cases
inline bool parentHasSameNumPoints(const mv::Dataset<mv::DatasetImpl> data, const mv::Dataset<Points>& other) {
if (!data->isDerivedData())
return false;

const auto parent = data->getParent();
if (parent->getDataType() != PointType)
return false;

const auto parentPoints = mv::Dataset<Points>(parent);
return parentPoints->getNumPoints() == other->getNumPoints();
}

// Is the data derived and does it's full source data have same number of points as the other data
inline bool fullSourceHasSameNumPoints(const mv::Dataset<mv::DatasetImpl> data, const mv::Dataset<Points>& other) {
if (!data->isDerivedData())
return false;

return data->getSourceDataset<Points>()->getFullDataset<Points>()->getNumPoints() == other->getNumPoints();
}

using LinkedDataCondition = std::function<bool(const mv::LinkedData& linkedData, const mv::Dataset<Points>& target)>;

/* Returns a mapping (linked data) from source that fulfils a given condition based on target, e.g.
auto checkMapping = [](const mv::LinkedData& linkedData, const mv::Dataset<Points>& target) -> bool {
return linkedData.getTargetDataset() == target;
};
This function will return the first match of the condition
*/
std::pair<const mv::LinkedData*, unsigned int> getSelectionMapping(const mv::Dataset<Points>& source, const mv::Dataset<Points>& 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<const mv::LinkedData*, unsigned int> getSelectionMappingColorsToPositions(const mv::Dataset<Points>& colors, const mv::Dataset<Points>& 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<const mv::LinkedData*, unsigned int> getSelectionMappingPositionsToColors(const mv::Dataset<Points>& positions, const mv::Dataset<Points>& colors);

// Returns a mapping (linked data) from positions' source data whose target is colors
std::pair<const mv::LinkedData*, unsigned int> getSelectionMappingPositionSourceToColors(const mv::Dataset<Points>& positions, const mv::Dataset<Points>& 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);

// returns whether there is a selection map from colors to positions or positions to colors (or respective parents)
// checks whether the mapping covers all elements in the target
bool checkSelectionMapping(const mv::Dataset<Points>& colors, const mv::Dataset<Points>& positions);
158 changes: 124 additions & 34 deletions src/ScatterplotPlugin.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "ScatterplotPlugin.h"

#include "MappingUtils.h"
#include "ScatterplotWidget.h"

#include <Application.h>
Expand Down Expand Up @@ -31,6 +32,9 @@

#include <algorithm>
#include <cassert>
#include <exception>
#include <map>
#include <stdexcept>
#include <vector>

#define VIEW_SAMPLING_HTML
Expand Down Expand Up @@ -172,25 +176,35 @@ ScatterplotPlugin::ScatterplotPlugin(const PluginFactory* factory) :
});
}

// Accept both data with the same number if points and data which is derived from
// a parent that has the same number of points (e.g. for HSNE embeddings)
// Accept for recoloring:
// 1. data with the same number of points
// 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
// 3. data which has a fully-covering selection mapping, that we can use for setting colors. Mapping in order of preference:
// a) from color (or it's parent) to position
// b) from color to position (or it's parent)
// c) from source of position to color

// [1. Same number of points]
const auto numPointsCandidate = candidateDataset->getNumPoints();
const auto numPointsPosition = _positionDataset->getNumPoints();
const bool sameNumPoints = numPointsPosition == numPointsCandidate;
const bool sameNumPointsAsFull =
/*if*/ _positionDataset->isDerivedData() ?
/*then*/ _positionDataset->getSourceDataset<Points>()->getFullDataset<Points>()->getNumPoints() == numPointsCandidate :
/*else*/ false;
const bool hasSameNumPoints = numPointsPosition == numPointsCandidate;

// [2. Derived from a parent]
const bool hasSameNumPointsAsFull = fullSourceHasSameNumPoints(_positionDataset, candidateDataset);

if (sameNumPoints || sameNumPointsAsFull) {
// [3. Full selection mapping]
const bool hasSelectionMapping = checkSelectionMapping(candidateDataset, _positionDataset);

if (hasSameNumPoints || hasSameNumPointsAsFull || hasSelectionMapping) {
// Offer the option to use the points dataset as source for points colors
dropRegions << new DropWidget::DropRegion(this, "Point color", QString("Colorize %1 points with %2").arg(_positionDataset->text(), candidateDataset->text()), "palette", true, [this, candidateDataset]() {
_settingsAction.getColoringAction().setCurrentColorDataset(candidateDataset); // calls addColorDataset internally
});

}

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

void ScatterplotPlugin::loadColors(const Dataset<Points>& points, const std::uint32_t& dimensionIndex)
void ScatterplotPlugin::loadColors(const Dataset<Points>& pointsColor, const std::uint32_t& dimensionIndex)
{
// Only proceed with valid points dataset
if (!points.isValid())
if (!pointsColor.isValid())
return;

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

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

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

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

if (numColorPoints != _numPoints) {
try {
const bool hasSameNumPointsAsFull = fullSourceHasSameNumPoints(_positionDataset, pointsColor);

const bool sameNumPointsAsFull =
/*if*/ _positionDataset->isDerivedData() ?
/*then*/ _positionSourceDataset->getFullDataset<Points>()->getNumPoints() == numColorPoints :
/*else*/ false;
if (hasSameNumPointsAsFull) {
std::vector<std::uint32_t> globalIndices = {};
_positionDataset->getGlobalIndices(globalIndices);

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

std::vector<float> localScalars(_numPoints, 0);
std::int32_t localColorIndex = 0;
}
else if ( // mapping from color data set to position data set
const auto [selectionMapping, numPointsTarget] = getSelectionMappingColorsToPositions(pointsColor, _positionDataset);
/* check if valid */
selectionMapping != nullptr &&
numPointsTarget == _numPoints &&
checkSurjectiveMapping(*selectionMapping, numPointsTarget)
)
{
// Map values like selection
const mv::SelectionMap::Map& mapColorsToPositions = selectionMapping->getMapping().getMap();

for (const auto& globalIndex : globalIndices)
localScalars[localColorIndex++] = scalars[globalIndex];
for (const auto& [fromColorID, vecOfPositionIDs] : mapColorsToPositions) {
for (std::uint32_t toPositionID : vecOfPositionIDs) {
mappedColorScalars[toPositionID] = colorScalars[fromColorID];
}
}

std::swap(localScalars, scalars);
}
else {
qWarning("Number of points used for coloring does not match number of points in data, aborting attempt to color plot");
}
else if ( // mapping from position data set to color data set
const auto [selectionMapping, numPointsTarget] = getSelectionMappingPositionsToColors(_positionDataset, pointsColor);
/* check if valid */
selectionMapping != nullptr &&
numPointsTarget == numColorPoints &&
checkSurjectiveMapping(*selectionMapping, numPointsTarget)
)
{
// Map values like selection (in reverse, use first value that occurs)
const mv::SelectionMap::Map& mapPositionsToColors = selectionMapping->getMapping().getMap();

for (const auto& [fromPositionID, vecOfColorIDs] : mapPositionsToColors) {
if (mappedColorScalars[fromPositionID] != std::numeric_limits<float>::lowest())
continue;
for (std::uint32_t toColorID : vecOfColorIDs) {
mappedColorScalars[fromPositionID] = colorScalars[toColorID];
}
}

}
else if ( // mapping from source of position data set to color data set
const auto [selectionMapping, numPointsTarget] = getSelectionMappingPositionSourceToColors(_positionDataset, pointsColor);
/* check if valid */
selectionMapping != nullptr &&
numPointsTarget == numColorPoints &&
checkSurjectiveMapping(*selectionMapping, numPointsTarget)
)
{
// the selection map is from full source data of positions data to pointsColor
// we need to use both the global indices of the positions (i.e. in the source) and the linked data mapping
const mv::SelectionMap::Map& mapGlobalToColors = selectionMapping->getMapping().getMap();
std::vector<std::uint32_t> globalIndices = {};
_positionDataset->getGlobalIndices(globalIndices);

for (std::int32_t localIndex = 0; localIndex < globalIndices.size(); localIndex++) {

if (mappedColorScalars[localIndex] != std::numeric_limits<float>::lowest())
continue;

const auto& indxColors = mapGlobalToColors.at(globalIndices[localIndex]); // from full source (parent) to colorDataset

for (const auto& indColors : indxColors) {
mappedColorScalars[localIndex] = colorScalars[indColors];
}
}

}
else {
throw std::runtime_error("Coloring data set does not match position data set in a known way, aborting attempt to color plot");
}

}
catch (const std::exception& e) {
qDebug() << "ScatterplotPlugin::loadColors: mapping failed -> " << e.what();
_settingsAction.getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant
return;
}
catch (...) {
qDebug() << "ScatterplotPlugin::loadColors: mapping failed for an unknown reason.";
_settingsAction.getColoringAction().getColorByAction().setCurrentIndex(0); // reset to color by constant
return;
}

std::swap(mappedColorScalars, colorScalars);
}

assert(scalars.size() == _numPoints);
assert(colorScalars.size() == _numPoints);

// Assign scalars and scalar effect
_scatterPlotWidget->setScalars(scalars);
// Assign colorScalars and scalar effect
_scatterPlotWidget->setScalars(colorScalars);
_scatterPlotWidget->setScalarEffect(PointEffect::Color);

_settingsAction.getColoringAction().updateColorMapActionScalarRange();
Expand Down