Fix indexing bug in user-specified colors (#2051)

* repaint category value when color changes

* bug fix incorrect indexing of user colors

* add test for bug 2007

* lint
This commit is contained in:
Bruce Martin
2021-02-08 18:00:03 -08:00
committed by GitHub
parent e6281baa39
commit 036b5f8c0f
3 changed files with 236 additions and 31 deletions
@@ -174,7 +174,7 @@ class CategoryValue extends React.Component {
Checks to see if at least one of the following changed:
* world state
* the color accessor (what is currently being colored by)
* if this catagorical value's selection status has changed
* if this categorical value's selection status has changed
* the crossfilter (ie, global selection state)
If and only if true, update the component
@@ -201,6 +201,13 @@ class CategoryValue extends React.Component {
const newCount = newCategorySummary.categoryValueCounts[newCategoryIndex];
const countChanged = count !== newCount;
// If the user edits an annotation that is currently colored-by, colors may be re-assigned.
// This test is conservative - it may cause re-rendering of entire category (all labels)
// if any one changes, but only for the currently colored-by category.
const colorMightHaveChanged =
nextProps.colorAccessor === nextProps.metadataField &&
props.categorySummary !== nextProps.categorySummary;
return (
labelChanged ||
valueSelectionChange ||
@@ -208,7 +215,8 @@ class CategoryValue extends React.Component {
annotationsChange ||
editingLabel ||
dilationChange ||
countChanged
countChanged ||
colorMightHaveChanged
);
};
+28 -29
View File
@@ -55,8 +55,8 @@ create colors scale and RGB array and return as object. Parameters:
* userColors - optional user color table
Returns:
{
scale: color scale
rgb: cell to color mapping
scale: function, mapping label index to color scale
rgb: cell label to color mapping
}
*/
function _createColorTable(
@@ -70,7 +70,7 @@ function _createColorTable(
case "color by categorical metadata": {
const data = colorByData.col(colorByAccessor).asArray();
if (userColors && colorByAccessor in userColors) {
return createUserColors(data, colorByAccessor, userColors);
return createUserColors(data, colorByAccessor, schema, userColors);
}
return createColorsByCategoricalMetadata(data, colorByAccessor, schema);
}
@@ -91,42 +91,41 @@ function _createColorTable(
}
export const createColorTable = memoize(_createColorTable);
/**
* Create two category label-indexed objects:
* - colors: maps label to RGB triplet for that label (used by graph, etc)
* - scale: function which given label returns d3 color scale for label
* Order doesn't matter - everything is keyed by label value.
*/
export function loadUserColorConfig(userColors) {
const convertedUserColors = {};
Object.keys(userColors).forEach((category) => {
// We cannot iterate over keys without sorting
// because we handle categorical values in alphabetical order __ignoring case__
// while Object.keys() _usually_ is ordered alphabetically where all upper characters are less than lowercase (A, B, C, a, b, c)
const [colors, scaleMap] = Object.keys(userColors[category])
.sort((a, b) => {
a = a.toLowerCase();
b = b.toLowerCase();
if (a === b) return 0;
if (a > b) return 1;
return -1;
})
.reduce(
(acc, label) => {
const color = parseRGB(userColors[category][label]);
acc[0][label] = color;
acc[1][label] = d3.rgb(
255 * color[0],
255 * color[1],
255 * color[2]
);
return acc;
},
[{}, {}]
);
const [colors, scaleMap] = Object.keys(userColors[category]).reduce(
(acc, label) => {
const color = parseRGB(userColors[category][label]);
acc[0][label] = color;
acc[1][label] = d3.rgb(255 * color[0], 255 * color[1], 255 * color[2]);
return acc;
},
[{}, {}]
);
const scale = (label) => scaleMap[label];
convertedUserColors[category] = { colors, scale };
});
return convertedUserColors;
}
function _createUserColors(data, colorAccessor, userColors) {
const { colors, scale } = userColors[colorAccessor];
function _createUserColors(data, colorAccessor, schema, userColors) {
const { colors, scale: scaleByLabel } = userColors[colorAccessor];
const rgb = createRgbArray(data, colors);
// color scale function param is INDEX (offset) into schema categories. It is NOT label value.
// See createColorsByCategoricalMetadata() for another example.
const { categories } = schema.annotations.obsByName[colorAccessor];
const categoryMap = new Map();
categories.forEach((label, idx) => categoryMap.set(idx, label));
const scale = (idx) => scaleByLabel(categoryMap.get(idx));
return { rgb, scale };
}
const createUserColors = memoize(_createUserColors);