From 93130484fed349bdcc66d32997cb49cc18c81351 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Thu, 18 Apr 2024 10:30:10 -0700 Subject: [PATCH 1/5] fix: ordinal sort direction asc was sorted in desc and vice versa. this fixes those so they are sorted in the correct direction --- src/utils/createSorter.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/utils/createSorter.js b/src/utils/createSorter.js index 163e4e4ae..7afcfe959 100644 --- a/src/utils/createSorter.js +++ b/src/utils/createSorter.js @@ -95,19 +95,17 @@ const hierarchyFunction = ({ property, direction = 'desc', hierarchy = [] }) => if (direction === 'asc') { if (firstIndex > secondIndex) { - return -1; + return 1; } - if (firstIndex < secondIndex) { - return 1; + return -1; } } else { if (firstIndex < secondIndex) { - return -1; + return 1; } - if (firstIndex > secondIndex) { - return 1; + return -1; } } return 0; From be22e8d492df27c389a33da318d9e25cb8655c0a Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Thu, 18 Apr 2024 11:01:43 -0700 Subject: [PATCH 2/5] revert ordinal sort direction. correct as is. was not the cause of the bug --- src/utils/createSorter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/createSorter.js b/src/utils/createSorter.js index 7afcfe959..ca90ded8e 100644 --- a/src/utils/createSorter.js +++ b/src/utils/createSorter.js @@ -95,17 +95,17 @@ const hierarchyFunction = ({ property, direction = 'desc', hierarchy = [] }) => if (direction === 'asc') { if (firstIndex > secondIndex) { - return 1; + return -1; } if (firstIndex < secondIndex) { - return -1; + return 1; } } else { if (firstIndex < secondIndex) { - return 1; + return -1; } if (firstIndex > secondIndex) { - return -1; + return 1; } } return 0; From 8548a8c1886631d428c57bafd822cb9219fabe40 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Thu, 18 Apr 2024 15:11:42 -0700 Subject: [PATCH 3/5] feat: sort categorical variables with hierarchy being order created --- src/utils/__tests__/createSorter.test.js | 113 ++++++++++++++++++++++- src/utils/createSorter.js | 39 +++++++- 2 files changed, 149 insertions(+), 3 deletions(-) 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 ca90ded8e..87d8a94f4 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. @@ -163,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); }; @@ -193,6 +225,7 @@ const createSorter = (sortRules = []) => { * - hierarchy * - number * - date + * - categorical * * Network Canvas Variables can be of type: * - "boolean", @@ -208,7 +241,6 @@ const createSorter = (sortRules = []) => { export const mapNCType = (type) => { switch (type) { case 'text': - case 'categorical': case 'layout': return 'string'; case 'number': @@ -219,6 +251,8 @@ export const mapNCType = (type) => { return 'date'; case 'ordinal': return 'hierarchy'; + case 'categorical': + return 'categorical'; case 'scalar': return 'number'; default: @@ -269,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) }, }; }; From 123793144324de342d992bc612c6f4bedfd36b26 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Fri, 19 Apr 2024 07:29:15 -0700 Subject: [PATCH 4/5] add categorical type to comments about supported types --- src/utils/createSorter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/createSorter.js b/src/utils/createSorter.js index 87d8a94f4..f71ba2583 100644 --- a/src/utils/createSorter.js +++ b/src/utils/createSorter.js @@ -175,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 From 2fbdd34c4837208a726589da16ac62ade32c779e Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Fri, 19 Apr 2024 07:58:18 -0700 Subject: [PATCH 5/5] bump version to 6.5.3 --- config.xml | 2 +- package-lock.json | 4 ++-- package.json | 2 +- public/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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",