diff --git a/client/src/util/centroid.js b/client/src/util/centroid.js index bbdd021e..cc7e9d8d 100644 --- a/client/src/util/centroid.js +++ b/client/src/util/centroid.js @@ -4,13 +4,92 @@ import { unassignedCategoryLabel } from "../globals"; /* Centroid coordinate calculation + Calculates centroids for displaying + In the case that a category is truncated, the truncated labels will not have + centroids calculated */ -/* - calcMedianCentroid goes through a given metadata category - fetches each cell's coordinates grouping by category value. +/* +Generates a mapping of categorical values to data needed to calculate centroids +categoricalValue -> { + length: int, + holdsFinite: Boolean, + xCoordinates: Float32Array, + yCoordinates: Float32Array +} +*/ +const getCoordinatesByCategoricalValues = ( + obsAnnotations, + obsLayout, + categoryName, + layoutDimNames, + categoricalSelection, + schemaObsByName +) => { + const coordsByCategoryLabel = new Map(); - It then calculates the median value and puts that in the array + const categoryArray = obsAnnotations.col(categoryName).asArray(); + + const layoutXArray = obsLayout.col(layoutDimNames[0]).asArray(); + const layoutYArray = obsLayout.col(layoutDimNames[1]).asArray(); + + const { categoryValueIndices, categoryValueCounts } = categoricalSelection[ + categoryName + ]; + + // Check to see if the current category is a user created annotation + const isUserAnno = schemaObsByName[categoryName].writable; + + // Iterate over all cells + for (let i = 0, len = categoryArray.length; i < len; i += 1) { + // Fetch the categorical value of the current cell + const categoryValue = categoryArray[i]; + + // Get the index of the categoryValue within the category + const categoryValueIndex = categoryValueIndices.get(categoryValue); + + // If the category is truncated and this value is removed, + // it will not be assigned a category value and will not be + // labeled on the graph + // If the user created this category, + // do not create a label for the `unassigned` value + if ( + categoryValueIndex !== undefined && + !(isUserAnno && categoryValue === unassignedCategoryLabel) + ) { + // Create/fetch the scratchpad value + let coords = coordsByCategoryLabel.get(categoryValue); + if (coords === undefined) { + // Get the number of cells which are in the categorical value + const numInCategoricalValue = categoryValueCounts[categoryValueIndex]; + coords = { + hasFinite: false, + xCoordinates: new Float32Array(numInCategoricalValue), + yCoordinates: new Float32Array(numInCategoricalValue), + length: 0 + }; + coordsByCategoryLabel.set(categoryValue, coords); + } + + coords.hasFinite = + coords.hasFinite || + (Number.isFinite(layoutXArray[i]) && Number.isFinite(layoutYArray[i])); + + const coordinatesLength = coords.length; + + coords.xCoordinates[coordinatesLength] = layoutXArray[i]; + coords.yCoordinates[coordinatesLength] = layoutYArray[i]; + + coords.length = coordinatesLength + 1; + } + } + return coordsByCategoryLabel; +}; + +/* + calcMedianCentroid calculates the median coordinates for categorical values in a given metadata field + + categoricalValue -> [x-Coordinate, y-Coordinate] */ const calcMedianCentroid = ( @@ -21,85 +100,39 @@ const calcMedianCentroid = ( categoricalSelection, schemaObsByName ) => { - const categoryArray = obsAnnotations.col(categoryName).asArray(); + // generate a map describing the coordinates for each value within the given category + const dataMap = getCoordinatesByCategoricalValues( + obsAnnotations, + obsLayout, + categoryName, + layoutDimNames, + categoricalSelection, + schemaObsByName + ); - const layoutXArray = obsLayout.col(layoutDimNames[0]).asArray(); - const layoutYArray = obsLayout.col(layoutDimNames[1]).asArray(); + // categoricalValue => [medianXCoordinate, medianYCoordinate] const coordinates = new Map(); - // Iterate over all the cells in the category - for (let i = 0, len = categoryArray.length; i < len; i += 1) { - const categoryValue = categoryArray[i]; - - // Get the index of the categoryValue within the category - // If the category is truncated and this value is removed, - // it will not be assigned a category value and will not be - // labeled on the graph - const categoryValueIndex = categoricalSelection[ - categoryName - ].categoryValueIndices.get(categoryValue); - - // Check to see if the current category is a user created annotation - // if the user created this category, do not create a label for the `unassigned` value - const isUserAnno = schemaObsByName[categoryName].writable; - - if ( - categoryValueIndex !== undefined && - !(isUserAnno && categoryValue === unassignedCategoryLabel) - ) { - // Get the number of cells which are in the category value - const numInCategoryValue = - categoricalSelection[categoryName].categoryValueCounts[ - categoryValueIndex - ]; - - // Create/fetch the valueArray, - // which is what the key points to in the `coordinates` hashmap - const valueArray = coordinates.get(categoryValue) || [ - false, // hasFinite - 0, // index - new Float32Array(numInCategoryValue), // x coordinates - new Float32Array(numInCategoryValue) // y coordinates - ]; - const index = valueArray[1]; - let hasFinite = valueArray[0]; - - hasFinite = - hasFinite || - (Number.isFinite(layoutXArray[i]) && Number.isFinite(layoutYArray[i])); - - valueArray[0] = hasFinite; - valueArray[1] = index + 1; - valueArray[2][index] = layoutXArray[i]; - valueArray[3][index] = layoutYArray[i]; - - coordinates.set(categoryValue, valueArray); - } - } - // Iterate over the recently created map - coordinates.forEach((value, key) => { - // If there are coordinates for this cateogrical value, + dataMap.forEach((value, key) => { + // If there are coordinates for this categorical value, // and there is a finite coordinate for the category value - if (value[2].length > 0 && value[3].length > 0 && value[0]) { - // Find the median x and y coordinate - // and insert them into the first two indices - value[0] = quantile([0.5], value[2])[0]; - value[1] = quantile([0.5], value[3])[0]; - // Remove the last two elements (where the arrays of coordinates were) - value.pop(); - value.pop(); - } else { - // remove the entry if not - coordinates.delete(key); + if (value.length > 0 && value.hasFinite) { + const calculatedCoordinates = []; + + // Find and store the median x and y coordinate + calculatedCoordinates[0] = quantile([0.5], value.xCoordinates)[0]; + calculatedCoordinates[1] = quantile([0.5], value.yCoordinates)[0]; + + coordinates.set(key, calculatedCoordinates); } }); + // return the map: categoricalValue -> [medianXCoordinate, medianYCoordinate] return coordinates; }; // A simple function to hash the parameters -// (not 100% on world hash, Bruce will have to check this one out) const hashMedianCentroid = ( obsAnnotations, obsLayout,