diff --git a/config.xml b/config.xml index 499ee606f..7c293af40 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - diff --git a/package-lock.json b/package-lock.json index 72bd56ae4..b5b70b84b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "network-canvas-interviewer", - "version": "6.5.2", + "version": "6.5.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "network-canvas-interviewer", - "version": "6.5.2", + "version": "6.5.3", "dependencies": { "@babel/runtime": "7.10.1", "@xmldom/xmldom": "~0.8.10", diff --git a/package.json b/package.json index 0e7613adf..4f20528fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "network-canvas-interviewer", - "version": "6.5.2", + "version": "6.5.3", "productName": "Network Canvas Interviewer", "description": "A tool for conducting Network Canvas Interviews.", "author": "Complex Data Collective", diff --git a/public/package.json b/public/package.json index bef634fc5..d9bac8f52 100644 --- a/public/package.json +++ b/public/package.json @@ -1,6 +1,6 @@ { "name": "network-canvas-interviewer", - "version": "6.5.2", + "version": "6.5.3", "productName": "Network Canvas Interviewer", "description": "A tool for conducting Network Canvas Interviews.", "author": "Complex Data Collective", diff --git a/src/utils/__tests__/createSorter.test.js b/src/utils/__tests__/createSorter.test.js index e1c30e02b..9865de34a 100644 --- a/src/utils/__tests__/createSorter.test.js +++ b/src/utils/__tests__/createSorter.test.js @@ -320,6 +320,117 @@ describe('Types', () => { }); }); +describe('Categorical sorting', () => { + it('sorts items based on categorical values', () => { + const mockItems = [ + { + category: ['cow'], + name: 'alice', + }, + { + category: ['duck'], + name: 'bob', + }, + { + category: ['lizard'], + name: 'charlie', + }, + { + category: ['cow'], + name: 'david', + }, + ]; + + const sorter = createSorter([ + { + property: 'category', + type: 'categorical', + hierarchy: ['duck', 'lizard', 'cow'], + }, + { + property: 'name', + type: 'string', + direction: 'asc', + }, + ]); + + const result = sorter(mockItems).map((item) => item.name); + expect(result).toEqual(['alice', 'david', 'charlie', 'bob']); + }); + + it('handles items with multiple categories', () => { + const mockItems = [ + { + category: ['duck', 'lizard'], + name: 'alice', + }, + { + category: ['cow', 'duck'], + name: 'bob', + }, + { + category: ['cow'], + name: 'charlie', + }, + { + category: ['lizard'], + name: 'david', + }, + ]; + + const sorter = createSorter([ + { + property: 'category', + type: 'categorical', + hierarchy: ['cow', 'duck', 'lizard'], + }, + { + property: 'name', + type: 'string', + direction: 'asc', + }, + ]); + + const result = sorter(mockItems).map((item) => item.name); + expect(result).toEqual(['david', 'alice', 'bob', 'charlie']); + }); + + it('handles missing categories', () => { + const mockItems = [ + { + name: 'alice', + }, + { + category: ['duck'], + name: 'bob', + }, + { + category: ['lizard'], + name: 'charlie', + }, + { + name: 'david', + }, + ]; + + const sorter = createSorter([ + { + property: 'category', + type: 'categorical', + hierarchy: ['lizard', 'duck', 'cow'], + }, + { + property: 'name', + type: 'string', + direction: 'asc', + }, + ]); + + const result = sorter(mockItems).map((item) => item.name); + expect(result).toEqual(['bob', 'charlie', 'alice', 'david']); + }); +}); + describe('Order direction', () => { it('orders ascending with "asc"', () => { const mockItems = [ @@ -994,7 +1105,7 @@ describe('processProtocolSortRule', () => { property: 'category', direction: 'asc', }; - expect(processProtocolSortRule(codebookVariables)(rule).type).toEqual('string'); + expect(processProtocolSortRule(codebookVariables)(rule).type).toEqual('categorical'); }); it('ordinal', () => { diff --git a/src/utils/createSorter.js b/src/utils/createSorter.js index 163e4e4ae..f71ba2583 100644 --- a/src/utils/createSorter.js +++ b/src/utils/createSorter.js @@ -74,6 +74,36 @@ const stringFunction = ({ property, direction }) => (a, b) => { return collator.compare(secondValue, firstValue); }; +const categoricalFunction = ({ property, direction, hierarchy = [] }) => (a, b) => { + // hierarchy is whatever order the variables were specified in the variable definition + const firstValues = get(a, property, []); + const secondValues = get(b, property, []); + + for (let i = 0; i < Math.max(firstValues.length, secondValues.length); i += 1) { + const firstValue = i < firstValues.length ? firstValues[i] : null; + const secondValue = i < secondValues.length ? secondValues[i] : null; + + if (firstValue !== secondValue) { + // If one of the values is not in the hierarchy, it is sorted to the end of the list + const firstIndex = hierarchy.indexOf(firstValue); + const secondIndex = hierarchy.indexOf(secondValue); + + if (firstIndex === -1) { + return 1; + } + if (secondIndex === -1) { + return -1; + } + + if (direction === 'asc') { + return firstIndex - secondIndex; + } return secondIndex - firstIndex; // desc + } + } + + return 0; +}; + /** * Creates a sort function that sorts items according to the index of their * property value in a hierarchy array. @@ -97,7 +127,6 @@ const hierarchyFunction = ({ property, direction = 'desc', hierarchy = [] }) => if (firstIndex > secondIndex) { return -1; } - if (firstIndex < secondIndex) { return 1; } @@ -105,7 +134,6 @@ const hierarchyFunction = ({ property, direction = 'desc', hierarchy = [] }) => if (firstIndex < secondIndex) { return -1; } - if (firstIndex > secondIndex) { return 1; } @@ -147,7 +175,7 @@ const getSortFunction = (rule) => { const { property, direction = 'asc', - type, // REQUIRED! number, boolean, string, date, hierarchy + type, // REQUIRED! number, boolean, string, date, hierarchy, categorical } = rule; // LIFO/FIFO rule sorted by _createdIndex @@ -165,8 +193,10 @@ const getSortFunction = (rule) => { if (type === 'hierarchy') { return hierarchyFunction(rule); } + if (type === 'categorical') { return categoricalFunction(rule); } + // eslint-disable-next-line no-console - console.warn('🤔 Sort rule missing required property \'type\', or type was not recognized. Sorting as a string, which may cause incorrect results. Supported types are: number, boolean, string, date, hierarchy.'); + console.warn('🤔 Sort rule missing required property \'type\', or type was not recognized. Sorting as a string, which may cause incorrect results. Supported types are: number, boolean, string, date, hierarchy, categorical'); return stringFunction(rule); }; @@ -195,6 +225,7 @@ const createSorter = (sortRules = []) => { * - hierarchy * - number * - date + * - categorical * * Network Canvas Variables can be of type: * - "boolean", @@ -210,7 +241,6 @@ const createSorter = (sortRules = []) => { export const mapNCType = (type) => { switch (type) { case 'text': - case 'categorical': case 'layout': return 'string'; case 'number': @@ -221,6 +251,8 @@ export const mapNCType = (type) => { return 'date'; case 'ordinal': return 'hierarchy'; + case 'categorical': + return 'categorical'; case 'scalar': return 'number'; default: @@ -271,6 +303,7 @@ export const processProtocolSortRule = (codebookVariables) => (sortRule) => { type: mapNCType(type), // Generate a hierarchy if the variable is ordinal based on the ordinal options ...type === 'ordinal' && { hierarchy: variableDefinition.options.map((option) => option.value) }, + ...type === 'categorical' && { hierarchy: variableDefinition.options.map((option) => option.value) }, }; };