Skip to content

Commit

Permalink
Merge pull request #1266 from complexdatacollective/fix/sort-order
Browse files Browse the repository at this point in the history
Fix/ sort by categorical variables
  • Loading branch information
buckhalt committed Apr 19, 2024
2 parents a15d1a6 + 2fbdd34 commit bfa31dc
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 11 deletions.
2 changes: 1 addition & 1 deletion config.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<widget android-versionCode="6415" id="org.codaco.NetworkCanvasInterviewer6" ios-CFBundleIdentifier="org.codaco.networkCanvasInterviewerBusiness" ios-CFBundleVersion="6415" version="6.5.2"
<widget android-versionCode="6415" id="org.codaco.NetworkCanvasInterviewer6" ios-CFBundleIdentifier="org.codaco.networkCanvasInterviewerBusiness" ios-CFBundleVersion="6415" version="6.5.3"
xmlns="http://www.w3.org/ns/widgets"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cdv="http://cordova.apache.org/ns/1.0">
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion 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",
Expand Down
2 changes: 1 addition & 1 deletion 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",
Expand Down
113 changes: 112 additions & 1 deletion src/utils/__tests__/createSorter.test.js
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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', () => {
Expand Down
43 changes: 38 additions & 5 deletions src/utils/createSorter.js
Expand Up @@ -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.
Expand All @@ -97,15 +127,13 @@ const hierarchyFunction = ({ property, direction = 'desc', hierarchy = [] }) =>
if (firstIndex > secondIndex) {
return -1;
}

if (firstIndex < secondIndex) {
return 1;
}
} else {
if (firstIndex < secondIndex) {
return -1;
}

if (firstIndex > secondIndex) {
return 1;
}
Expand Down Expand Up @@ -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
Expand All @@ -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);
};

Expand Down Expand Up @@ -195,6 +225,7 @@ const createSorter = (sortRules = []) => {
* - hierarchy
* - number
* - date
* - categorical
*
* Network Canvas Variables can be of type:
* - "boolean",
Expand All @@ -210,7 +241,6 @@ const createSorter = (sortRules = []) => {
export const mapNCType = (type) => {
switch (type) {
case 'text':
case 'categorical':
case 'layout':
return 'string';
case 'number':
Expand All @@ -221,6 +251,8 @@ export const mapNCType = (type) => {
return 'date';
case 'ordinal':
return 'hierarchy';
case 'categorical':
return 'categorical';
case 'scalar':
return 'number';
default:
Expand Down Expand Up @@ -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) },
};
};

Expand Down

0 comments on commit bfa31dc

Please sign in to comment.