diff --git a/src/main/data-managers/Reportable.js b/src/main/data-managers/Reportable.js
index 31eca31c4..0e304c99d 100644
--- a/src/main/data-managers/Reportable.js
+++ b/src/main/data-managers/Reportable.js
@@ -30,6 +30,7 @@ const flatten = shallowArrays => [].concat(...shallowArrays);
const entityKey = (entityName) => {
if (entityName === 'node') return 'nodes';
if (entityName === 'edge') return 'edges';
+ if (entityName === 'ego') return 'ego';
return null;
};
@@ -190,26 +191,57 @@ const Reportable = Super => class extends Super {
*
* @memberOf Reportable.prototype
*/
- optionValueBuckets(protocolId, variableNames, entityName = 'node') {
+ optionValueBuckets(protocolId, nodeNames, edgeNames, egoNames) {
+ let allBuckets;
+ return this.optionValueBucketsByEntity(protocolId, nodeNames, 'node')
+ .then((nodeBuckets) => {
+ allBuckets = { ...allBuckets, nodes: nodeBuckets };
+ return this.optionValueBucketsByEntity(protocolId, edgeNames, 'edge');
+ })
+ .then((edgeBuckets) => {
+ allBuckets = { ...allBuckets, edges: edgeBuckets };
+ return this.optionValueBucketsByEntity(protocolId, egoNames, 'ego');
+ })
+ .then(egoBuckets => ({ ...allBuckets, ego: egoBuckets }));
+ }
+
+ optionValueBucketsByEntity(protocolId, variableNames, entityName) {
return new Promise((resolve, reject) => {
const key = entityKey(entityName);
this.db.find({ protocolId, [`data.${key}`]: { $exists: true } }, resolveOrReject((docs) => {
const entities = flatten(docs.map(doc => doc.data[key]));
const buckets = entities.reduce((acc, entity) => {
- acc[entity.type] = acc[entity.type] || {};
- variableNames.forEach((variableName) => {
- acc[entity.type][variableName] = acc[entity.type][variableName] || {};
- const optionValue = entity[attributesProperty][variableName];
- if (optionValue !== undefined) {
- // Categorical values are expressed as arrays of multiple options
- const optionValues = (optionValue instanceof Array) ? optionValue : [optionValue];
- const counts = acc[entity.type][variableName];
- optionValues.forEach((value) => {
- counts[value] = counts[value] || 0;
- counts[value] += 1;
- });
- }
- });
+ if (entityName === 'ego') {
+ acc = acc || {};
+ (variableNames || []).forEach((variableName) => {
+ acc[variableName] = acc[variableName] || {};
+ const optionValue = entity[attributesProperty][variableName];
+ if (optionValue !== undefined) {
+ // Categorical values are expressed as arrays of multiple options
+ const optionValues = (optionValue instanceof Array) ? optionValue : [optionValue];
+ const counts = acc[variableName];
+ optionValues.forEach((value) => {
+ counts[value] = counts[value] || 0;
+ counts[value] += 1;
+ });
+ }
+ });
+ } else {
+ acc[entity.type] = acc[entity.type] || {};
+ (variableNames[entity.type] || []).forEach((variableName) => {
+ acc[entity.type][variableName] = acc[entity.type][variableName] || {};
+ const optionValue = entity[attributesProperty][variableName];
+ if (optionValue !== undefined) {
+ // Categorical values are expressed as arrays of multiple options
+ const optionValues = (optionValue instanceof Array) ? optionValue : [optionValue];
+ const counts = acc[entity.type][variableName];
+ optionValues.forEach((value) => {
+ counts[value] = counts[value] || 0;
+ counts[value] += 1;
+ });
+ }
+ });
+ }
return acc;
}, {});
resolve(buckets);
diff --git a/src/main/data-managers/__tests__/Reportable-test.js b/src/main/data-managers/__tests__/Reportable-test.js
index f65d5dd5d..fcc213ab9 100644
--- a/src/main/data-managers/__tests__/Reportable-test.js
+++ b/src/main/data-managers/__tests__/Reportable-test.js
@@ -109,27 +109,62 @@ describe('Reportable', () => {
});
});
- describe('with node variables', () => {
+ describe('with variables', () => {
beforeAll(() => { mockData = NodeDataSession; });
it('summarizes an ordinal variable', async () => {
- await expect(reportDB.optionValueBuckets(mockData.protocolId, ['frequencyOrdinal'])).resolves.toMatchObject({
- person: {
- frequencyOrdinal: {
- 1: 1,
- 2: 1,
+ await expect(reportDB.optionValueBuckets(mockData.protocolId, { person: ['frequencyOrdinal'] }, {}, [])).resolves.toMatchObject({
+ nodes: {
+ person: {
+ frequencyOrdinal: {
+ 1: 1,
+ 2: 1,
+ },
},
},
+ edges: {},
+ ego: {},
});
});
it('summarizes a categorical variable', async () => {
- await expect(reportDB.optionValueBuckets(mockData.protocolId, ['preferenceCategorical'])).resolves.toMatchObject({
- person: {
- preferenceCategorical: {
- a: 2,
- b: 1,
+ await expect(reportDB.optionValueBuckets(mockData.protocolId, { person: ['preferenceCategorical'] }, {}, [])).resolves.toMatchObject({
+ nodes: {
+ person: {
+ preferenceCategorical: {
+ a: 2,
+ b: 1,
+ },
},
},
+ edges: {},
+ ego: {},
+ });
+ });
+
+ it('summarizes an edge variable', async () => {
+ await expect(reportDB.optionValueBuckets(mockData.protocolId, {}, { friend: ['catVariable'] }, [])).resolves.toMatchObject({
+ edges: {
+ friend: {
+ catVariable: {
+ 1: 1,
+ 2: 1,
+ },
+ },
+ },
+ nodes: {},
+ ego: {},
+ });
+ });
+
+ it('summarizes an ego variable', async () => {
+ await expect(reportDB.optionValueBuckets(mockData.protocolId, {}, {}, ['ordVariable'])).resolves.toMatchObject({
+ ego: {
+ ordVariable: {
+ 2: 2,
+ },
+ },
+ edges: { friend: {} },
+ nodes: { person: {} },
});
});
@@ -144,7 +179,8 @@ describe('Reportable', () => {
const result = await reportDB.entityTimeSeries(mockData.protocolId);
expect(result.entities).toContainEqual({
time: expect.any(Number),
- edge: 0,
+ edge: 2,
+ edge_friend: 2,
node: 2,
node_person: 2,
});
diff --git a/src/main/data-managers/__tests__/data/node-data-session.json b/src/main/data-managers/__tests__/data/node-data-session.json
index 8243c9135..42db7c89c 100644
--- a/src/main/data-managers/__tests__/data/node-data-session.json
+++ b/src/main/data-managers/__tests__/data/node-data-session.json
@@ -1,6 +1,37 @@
{
"data": {
- "edges": [],
+ "ego": [
+ {
+ "_uid": "31980bde-1cc1-4681-b482-8a6ede07c2c8",
+ "type": "ego",
+ "attributes": {
+ "ordVariable": 2
+ }
+ },
+ {
+ "_uid": "35fb6341-91a0-4674-b615-039280c9c212",
+ "type": "person",
+ "attributes": {
+ "ordVariable": 2
+ }
+ }
+ ],
+ "edges": [
+ {
+ "_uid": "61980bde-1cc1-4681-b482-8a6ede07c2c8",
+ "type": "friend",
+ "attributes": {
+ "catVariable": 1
+ }
+ },
+ {
+ "_uid": "f5fb6341-91a0-4674-b615-039280c9c212",
+ "type": "friend",
+ "attributes": {
+ "catVariable": 2
+ }
+ }
+ ],
"nodes": [
{
"_uid": "91980bde-1cc1-4681-b482-8a6ede07c2c8",
diff --git a/src/main/server/AdminService.js b/src/main/server/AdminService.js
index 3b03aa36e..8eb1fcf10 100644
--- a/src/main/server/AdminService.js
+++ b/src/main/server/AdminService.js
@@ -193,11 +193,19 @@ class AdminService {
.then(() => next());
});
- // ?variableNames=var1,var2&entityName=node
- // "buckets": { "person": { "var1": { "val1": 0, "val2": 0 }, "var2": {} } }
- api.get('/protocols/:id/reports/option_buckets', (req, res, next) => {
- const { variableNames = '', entityName = 'node' } = req.query;
- this.reportDb.optionValueBuckets(req.params.id, variableNames.split(','), entityName)
+ // nodeNames: { type1: [var1, var2], type2: [var1, var3] },
+ // edgeNames: { type1: [var1] }, egoNames: [var1, var2],
+ // egoNames: [var1, var2]
+ // "buckets": {
+ // nodes: { "person": { "var1": { "val1": 0, "val2": 0 }, "var2": {} } }
+ // edges: { "friend": { "var1": { "val1": 0, "val2": 0} } }
+ // ego: { "var1": { "val1": 0 } }
+ // }
+ // We use post here, instead of get, because the data is more complicated than just a list
+ // of variables, it's organized by entity and type.
+ api.post('/protocols/:id/reports/option_buckets', (req, res, next) => {
+ const { nodeNames = '', edgeNames = '', egoNames = '' } = req.body;
+ this.reportDb.optionValueBuckets(req.params.id, nodeNames, edgeNames, egoNames)
.then(buckets => res.send({ status: 'ok', buckets }))
.then(() => next());
});
diff --git a/src/main/server/__tests__/AdminService-test.js b/src/main/server/__tests__/AdminService-test.js
index 8264e5102..bcf0238fa 100644
--- a/src/main/server/__tests__/AdminService-test.js
+++ b/src/main/server/__tests__/AdminService-test.js
@@ -294,7 +294,7 @@ describe('the AdminService', () => {
it('fetches bucketed categorical/ordinal data', async () => {
const endpoint = makeUrl('protocols/1/reports/option_buckets', apiBase);
- const res = await jsonClient.get(endpoint);
+ const res = await jsonClient.post(endpoint, { nodeNames: '', edgeNames: '', egoNames: '' });
expect(res.json.status).toBe('ok');
expect(res.json.buckets).toMatchObject(bucketsResult);
});
diff --git a/src/renderer/components/workspace/AnswerDistributionPanel.js b/src/renderer/components/workspace/AnswerDistributionPanel.js
index f397cbd4b..4e98c3964 100644
--- a/src/renderer/components/workspace/AnswerDistributionPanel.js
+++ b/src/renderer/components/workspace/AnswerDistributionPanel.js
@@ -10,6 +10,13 @@ const chartComponent = variableType => ((variableType === 'categorical') ? PieCh
const headerLabel = variableType => ((variableType === 'categorical') ? 'Categorical' : 'Ordinal');
+export const entityLabel = (entityKey, entityType) => {
+ if (entityKey === 'nodes') return `Node (${entityType})`;
+ if (entityKey === 'edges') return `Edge (${entityType})`;
+ if (entityKey === 'ego') return 'Ego';
+ return null;
+};
+
const content = (chartData, variableType) => {
const Chart = chartComponent(variableType);
if (chartData.length) {
@@ -24,12 +31,12 @@ const content = (chartData, variableType) => {
*/
class AnswerDistributionPanel extends PureComponent {
render() {
- const { chartData, variableDefinition } = this.props;
+ const { chartData, entityKey, entityType, variableDefinition } = this.props;
const totalObservations = sumValues(chartData);
return (
- {variableDefinition.name}
+ {entityLabel(entityKey, entityType)}: {variableDefinition.name}
{headerLabel(variableDefinition.type)} distribution
@@ -50,10 +57,14 @@ class AnswerDistributionPanel extends PureComponent {
AnswerDistributionPanel.defaultProps = {
chartData: [],
+ entityType: '',
+ entityKey: '',
};
AnswerDistributionPanel.propTypes = {
chartData: PropTypes.array,
+ entityKey: PropTypes.string,
+ entityType: PropTypes.string,
variableDefinition: Types.variableDefinition.isRequired,
};
diff --git a/src/renderer/containers/SettingsScreen.js b/src/renderer/containers/SettingsScreen.js
index 19a8de6d4..a3b9c5648 100644
--- a/src/renderer/containers/SettingsScreen.js
+++ b/src/renderer/containers/SettingsScreen.js
@@ -4,6 +4,8 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom';
import { compose } from 'recompose';
+
+import { entityLabel } from '../components/workspace/AnswerDistributionPanel';
import { actionCreators as dialogActions } from '../ducks/modules/dialogs';
import Types from '../types';
import CheckboxGroup from '../ui/components/Fields/CheckboxGroup';
@@ -34,21 +36,22 @@ class SettingsScreen extends Component {
{
distributionVariables &&
- Object.entries(distributionVariables).map(([section, vars]) => (
- {
- const newExcluded = vars.filter(v => !newValue.includes(v));
- setExcludedVariables(protocol.id, section, newExcluded);
- },
- }}
- options={vars.map(v => ({ value: v, label: v }))}
- />
- ))
+ Object.entries(distributionVariables).map(([entity, varsWithTypes]) => (
+ Object.entries(varsWithTypes).map(([section, vars]) => (
+ {
+ const newExcluded = vars.filter(v => !newValue.includes(v));
+ setExcludedVariables(protocol.id, entity, section, newExcluded);
+ },
+ }}
+ options={vars.map(v => ({ value: v, label: v }))}
+ />
+ ))))
}
@@ -69,10 +72,11 @@ class SettingsScreen extends Component {
}
}
- includedChartVariablesForSection = (section) => {
+ includedChartVariablesForSection = (entity, section) => {
const { excludedChartVariables, distributionVariables } = this.props;
- const excludeSection = excludedChartVariables[section];
- return distributionVariables[section].filter(
+ const excludeSection = excludedChartVariables[entity] &&
+ excludedChartVariables[entity][section];
+ return distributionVariables[entity][section].filter(
variable => !excludeSection || !excludeSection.includes(variable));
}
diff --git a/src/renderer/containers/__tests__/SettingsScreen-test.js b/src/renderer/containers/__tests__/SettingsScreen-test.js
index 5fcf6e3d0..d75402ae4 100644
--- a/src/renderer/containers/__tests__/SettingsScreen-test.js
+++ b/src/renderer/containers/__tests__/SettingsScreen-test.js
@@ -42,13 +42,17 @@ describe('', () => {
});
it('renders checkboxes for chart variable selection', () => {
- const distributionVariables = { person: ['catVar'] };
+ const distributionVariables = {
+ nodes: { person: ['catVar'] },
+ edges: { friend: ['catVar'] },
+ ego: { ego: ['catVar'] },
+ };
subject.setProps({ protocol: mockProtocol, distributionVariables });
- expect(subject.find('CheckboxGroup')).toHaveLength(1);
+ expect(subject.find('CheckboxGroup')).toHaveLength(3);
});
it('updates excluded variables from checkbox input', () => {
- const distributionVariables = { person: ['catVar'] };
+ const distributionVariables = { nodes: { person: ['catVar'] } };
subject.setProps({ protocol: mockProtocol, distributionVariables });
expect(setExcludedVariables).not.toHaveBeenCalled();
const checkboxes = subject.find('CheckboxGroup');
diff --git a/src/renderer/containers/workspace/WorkspaceScreen.js b/src/renderer/containers/workspace/WorkspaceScreen.js
index 5a620113f..e94321299 100644
--- a/src/renderer/containers/workspace/WorkspaceScreen.js
+++ b/src/renderer/containers/workspace/WorkspaceScreen.js
@@ -68,6 +68,8 @@ class WorkspaceScreen extends Component {
...answerDistributionCharts.map(chart => (
diff --git a/src/renderer/containers/workspace/__tests__/withAnswerDistributionCharts-test.js b/src/renderer/containers/workspace/__tests__/withAnswerDistributionCharts-test.js
index 9adf5f3d5..ffc42190b 100644
--- a/src/renderer/containers/workspace/__tests__/withAnswerDistributionCharts-test.js
+++ b/src/renderer/containers/workspace/__tests__/withAnswerDistributionCharts-test.js
@@ -9,8 +9,12 @@ import { mockProtocol } from '../../../../../config/jest/setupTestEnv';
jest.mock('../../../utils/adminApiClient', () => {
function MockApiClient() {}
- MockApiClient.prototype.get = jest.fn().mockResolvedValue({
- buckets: { person: { distributionVariable: { 1: 4, 2: 5 } } },
+ MockApiClient.prototype.post = jest.fn().mockResolvedValue({
+ buckets: {
+ nodes: { person: { distributionVariable: { 1: 4, 2: 5 } } },
+ edges: { friend: { catVariable: { 1: 3 } } },
+ ego: { ordVariable: { 1: 2 } },
+ },
});
return MockApiClient;
});
@@ -36,6 +40,34 @@ jest.mock('../../../ducks/modules/protocols', () => ({
},
},
},
+ edge: {
+ friend: {
+ variables: {
+ catVariable: {
+ name: '',
+ type: 'categorical',
+ options: [
+ { label: 'a', value: 1 },
+ { label: 'b', value: 2 },
+ { label: 'c', value: 3 },
+ ],
+ },
+ },
+ },
+ },
+ ego: {
+ variables: {
+ ordVariable: {
+ name: '',
+ type: 'ordinal',
+ options: [
+ { label: 'a', value: 1 },
+ { label: 'b', value: 2 },
+ { label: 'c', value: 3 },
+ ],
+ },
+ },
+ },
}),
},
}));
@@ -54,24 +86,24 @@ describe('AnswerDistributionPanels', () => {
});
it('loads data', () => {
- expect(mockApiClient.get).toHaveBeenCalled();
+ expect(mockApiClient.post).toHaveBeenCalled();
});
it('loads data from API when totalSessionsCount changes', () => {
wrapper.setProps({ totalSessionsCount: 1 });
- mockApiClient.get.mockClear();
- expect(mockApiClient.get).toHaveBeenCalledTimes(0);
+ mockApiClient.post.mockClear();
+ expect(mockApiClient.post).toHaveBeenCalledTimes(0);
wrapper.setProps({ totalSessionsCount: 2 });
- expect(mockApiClient.get).toHaveBeenCalledTimes(1);
+ expect(mockApiClient.post).toHaveBeenCalledTimes(1);
});
it('loads data when protocolId changes', () => {
// shallow to bypass mapStateToProps
const wrapped = shallow( state)} />).dive();
wrapped.setProps({ protocolId: null });
- mockApiClient.get.mockClear();
+ mockApiClient.post.mockClear();
wrapped.setProps({ protocolId: '2' });
- expect(mockApiClient.get).toHaveBeenCalled();
+ expect(mockApiClient.post).toHaveBeenCalled();
});
describe('API handler', () => {
@@ -81,17 +113,25 @@ describe('AnswerDistributionPanels', () => {
});
it('renders one chart per variable', () => {
- expect(wrapper.state('charts')).toHaveLength(1);
+ expect(wrapper.state('charts')).toHaveLength(3);
});
it('sets correct data format', async () => {
- const chart = wrapper.state('charts')[0];
- expect(chart.chartData).toContainEqual({ name: 'a', value: 4 });
- expect(chart.chartData).toContainEqual({ name: 'b', value: 5 });
+ const nodeChart = wrapper.state('charts')[0];
+ const edgeChart = wrapper.state('charts')[1];
+ const egoChart = wrapper.state('charts')[2];
+ expect(nodeChart.chartData).toContainEqual({ name: 'a', value: 4 });
+ expect(nodeChart.chartData).toContainEqual({ name: 'b', value: 5 });
+ expect(edgeChart.chartData).toContainEqual({ name: 'a', value: 3 });
+ expect(egoChart.chartData).toContainEqual({ name: 'a', value: 2 });
});
it('sets zeros for missing values', async () => {
expect(wrapper.state('charts')[0].chartData).toContainEqual({ name: 'c', value: 0 });
+ expect(wrapper.state('charts')[1].chartData).toContainEqual({ name: 'c', value: 0 });
+ expect(wrapper.state('charts')[1].chartData).toContainEqual({ name: 'b', value: 0 });
+ expect(wrapper.state('charts')[2].chartData).toContainEqual({ name: 'c', value: 0 });
+ expect(wrapper.state('charts')[2].chartData).toContainEqual({ name: 'b', value: 0 });
});
});
});
diff --git a/src/renderer/containers/workspace/withAnswerDistributionCharts.js b/src/renderer/containers/workspace/withAnswerDistributionCharts.js
index f3b32f834..9c33ebf0f 100644
--- a/src/renderer/containers/workspace/withAnswerDistributionCharts.js
+++ b/src/renderer/containers/workspace/withAnswerDistributionCharts.js
@@ -1,6 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
+import { get } from 'lodash';
import AdminApiClient from '../../utils/adminApiClient';
import { selectors as protocolSelectors } from '../../ducks/modules/protocols';
@@ -27,14 +28,17 @@ const hasData = bucket => bucket && Object.keys(bucket).length > 0;
* @param {Object} buckets The API response from `option_buckets`
* @return {Array} chartDefinitions
*/
-const shapeBucketData = (transposedNodeCodebook, buckets, excludedChartVariables) =>
+const shapeBucketDataByType = (
+ transposedNodeCodebook, buckets, excludedChartVariables, entityKey) =>
Object.entries(transposedNodeCodebook).reduce((acc, [entityType, { variables }]) => {
- const excludedSectionVariables = excludedChartVariables[entityType] || [];
- Object.entries(variables).forEach(([variableName, def]) => {
+ const excludedSectionVariables = (excludedChartVariables[entityKey] &&
+ excludedChartVariables[entityKey][entityType]) || [];
+ Object.entries(variables || []).forEach(([variableName, def]) => {
if (!isDistributionVariable(def) || excludedSectionVariables.includes(def.name)) {
return;
}
- const data = buckets[entityType] && buckets[entityType][variableName];
+ const dataPath = entityKey === 'ego' ? [variableName] : [entityType, variableName];
+ const data = get(buckets, dataPath);
const values = hasData(data) && def.options.map((option) => {
// Option defs are usually in the format { label, value }, however:
// - options may be strings or numerics instead of objects
@@ -48,6 +52,7 @@ const shapeBucketData = (transposedNodeCodebook, buckets, excludedChartVariables
};
});
acc.push({
+ entityKey,
entityType,
variableType: def.type,
variableDefinition: def,
@@ -57,6 +62,15 @@ const shapeBucketData = (transposedNodeCodebook, buckets, excludedChartVariables
return acc;
}, []);
+const shapeBucketData = (codebook, buckets, excludedChartVariables) =>
+ Object.entries(buckets).reduce((acc, [entityKey]) => {
+ const entityCodebook = entityKey === 'ego' ? { ego: codebook[entityKey] } : codebook[entityKey];
+ const bucketsByType = buckets[entityKey];
+ const shapeData = shapeBucketDataByType(
+ entityCodebook, bucketsByType, excludedChartVariables, entityKey);
+ return acc.concat(shapeData);
+ }, []);
+
/**
* HOC that provides chart definitions for the 'answer distribution' panels.
* Charts are exposed as a prop so that panel components can be managed directly
@@ -109,25 +123,39 @@ const withAnswerDistributionCharts = (WrappedComponent) => {
const {
excludedChartVariables,
protocolId,
- transposedCodebook: { node: nodeCodebook = {} },
+ transposedCodebook: {
+ node: nodeCodebook = {},
+ edge: edgeCodebook = {},
+ ego: egoCodebook = {},
+ },
} = this.props;
+ const nodeNames = Object.values(nodeCodebook).reduce((acc, nodeTypeDefinition) => (
+ {
+ ...acc,
+ [nodeTypeDefinition.name]: Object.keys(nodeTypeDefinition.variables || {}),
+ }
+ ), {});
+ const edgeNames = Object.values(edgeCodebook).reduce((acc, edgeTypeDefinition) => (
+ {
+ ...acc,
+ [edgeTypeDefinition.name]: Object.keys(edgeTypeDefinition.variables || {}),
+ }
+ ), {});
+ const egoNames = Object.keys(egoCodebook.variables || {});
+
if (!protocolId) {
return;
}
- const variableNames = Object.values(nodeCodebook).reduce((acc, nodeTypeDefinition) => {
- acc.push(...Object.keys(nodeTypeDefinition.variables || {}));
- return acc;
- }, []);
-
+ const variableNames = { nodes: nodeCodebook, edges: edgeCodebook, ego: egoCodebook };
const route = `/protocols/${this.props.protocolId}/reports/option_buckets`;
- const query = { variableNames };
+ const query = { nodeNames, edgeNames, egoNames };
- this.apiClient.get(route, query)
+ this.apiClient.post(route, query)
.then(({ buckets }) => {
this.setState({
- charts: shapeBucketData(nodeCodebook, buckets, excludedChartVariables),
+ charts: shapeBucketData(variableNames, buckets, excludedChartVariables),
});
});
}
diff --git a/src/renderer/ducks/modules/__tests__/excludedChartVariables-test.js b/src/renderer/ducks/modules/__tests__/excludedChartVariables-test.js
index 4d0070389..9b91a10ef 100644
--- a/src/renderer/ducks/modules/__tests__/excludedChartVariables-test.js
+++ b/src/renderer/ducks/modules/__tests__/excludedChartVariables-test.js
@@ -18,6 +18,7 @@ describe('excludedChartVariables', () => {
const action = {
type: actionTypes.SET_EXCLUDED_VARIABLES,
protocolId: 'protocol1',
+ entity: 'nodes',
section: 'person',
variables: ['someVar'],
};
@@ -27,6 +28,7 @@ describe('excludedChartVariables', () => {
it('returns current state if no protocol ID given', () => {
const action = {
type: actionTypes.SET_EXCLUDED_VARIABLES,
+ entity: 'nodes',
section: 'person',
variables: ['someVar'],
};
@@ -37,21 +39,23 @@ describe('excludedChartVariables', () => {
const action = {
type: actionTypes.SET_EXCLUDED_VARIABLES,
protocolId: '1',
+ entity: 'nodes',
section: 'person',
variables: ['someVar'],
};
expect(reducer({}, action)).toEqual({
- 1: { person: ['someVar'] },
+ 1: { nodes: { person: ['someVar'] } },
});
});
});
describe('setExcludedVariables action creator', () => {
it('produces an exclude action', () => {
- const action = actionCreators.setExcludedVariables('1', 'person', ['someVar']);
+ const action = actionCreators.setExcludedVariables('1', 'nodes', 'person', ['someVar']);
expect(action).toEqual({
type: actionTypes.SET_EXCLUDED_VARIABLES,
protocolId: '1',
+ entity: 'nodes',
section: 'person',
variables: ['someVar'],
});
@@ -61,10 +65,10 @@ describe('excludedChartVariables', () => {
describe('excludedVariablesForCurrentProtocol selector', () => {
const { excludedVariablesForCurrentProtocol } = selectors;
it('returns variables for the protocol', () => {
- const excludedChartVariables = { 1: { person: ['someVar'] } };
+ const excludedChartVariables = { 1: { nodes: { person: ['someVar'] } } };
const state = { protocols: [{ id: '1' }], excludedChartVariables };
const props = { match: { params: { id: '1' } } };
- expect(excludedVariablesForCurrentProtocol(state, props)).toEqual({ person: ['someVar'] });
+ expect(excludedVariablesForCurrentProtocol(state, props)).toEqual({ nodes: { person: ['someVar'] } });
});
});
});
diff --git a/src/renderer/ducks/modules/__tests__/protocols-test.js b/src/renderer/ducks/modules/__tests__/protocols-test.js
index ad824931c..b08c6f01c 100644
--- a/src/renderer/ducks/modules/__tests__/protocols-test.js
+++ b/src/renderer/ducks/modules/__tests__/protocols-test.js
@@ -128,13 +128,15 @@ describe('the protocols module', () => {
});
describe('ordinalAndCategoricalVariables', () => {
- it('returns node variable names sectioned by entity type', () => {
+ it('returns entity variable names sectioned by entity type', () => {
const codebook = {
node: { 'node-type-id': { name: 'person', variables: { 'var-id-1': { name: 'catVar', type: 'categorical' } } } },
+ edge: { 'edge-type-id': { name: 'friend', variables: { 'var-id-2': { name: 'ordVar', type: 'ordinal' } } } },
+ ego: { name: 'ego', variables: { 'var-id-3': { name: 'catVar', type: 'categorical' } } },
};
const state = { protocols: [{ id: '1', codebook }] };
const props = { match: { params: { id: '1' } } };
- expect(ordinalAndCategoricalVariables(state, props)).toEqual({ person: ['catVar'] });
+ expect(ordinalAndCategoricalVariables(state, props)).toEqual({ nodes: { person: ['catVar'] }, edges: { friend: ['ordVar'] }, ego: { ego: ['catVar'] } });
});
it('ignores sections without these variables', () => {
@@ -146,14 +148,15 @@ describe('the protocols module', () => {
expect(ordinalAndCategoricalVariables(state, props)).not.toHaveProperty('venue');
});
- it('returns an empty object if node codebook unavailable', () => {
+ it('returns an empty object if entity codebook unavailable', () => {
const state = { protocols: [{ id: '1', codebook: {} }] };
const props = { match: { params: { id: '1' } } };
- expect(ordinalAndCategoricalVariables(state, props)).toEqual({});
+ expect(ordinalAndCategoricalVariables(state, props)).toEqual(
+ { nodes: {}, edges: {}, ego: {} });
});
it('returns an empty object if protocol unavailable', () => {
- expect(ordinalAndCategoricalVariables({}, {})).toEqual({});
+ expect(ordinalAndCategoricalVariables({}, {})).toEqual({ nodes: {}, edges: {}, ego: {} });
});
});
@@ -166,12 +169,20 @@ describe('the protocols module', () => {
describe('transposedCodebook', () => {
it('returns a modified codebook', () => {
- const codebook = { node: { 'node-type-id': { name: 'person', variables: {} } } };
+ const codebook = {
+ node: { 'node-type-id': { name: 'person', variables: {} } },
+ edge: { 'edge-type-id': { name: 'friend', variables: {} } },
+ ego: { name: 'ego', variables: { 'var-id-1': { name: 'ordVar', type: 'ordinal' } } },
+ };
const state = { protocols: [{ id: '1', codebook }] };
const props = { match: { params: { id: '1' } } };
const transposed = transposedCodebook(state, props);
expect(transposed).toHaveProperty('node');
expect(transposed.node).toHaveProperty('person');
+ expect(transposed).toHaveProperty('edge');
+ expect(transposed.edge).toHaveProperty('friend');
+ expect(transposed).toHaveProperty('ego');
+ expect(transposed.ego.variables).toHaveProperty('ordVar');
});
it('does not require edge variables', () => {
diff --git a/src/renderer/ducks/modules/excludedChartVariables.js b/src/renderer/ducks/modules/excludedChartVariables.js
index 2aa45cb71..badcbee65 100644
--- a/src/renderer/ducks/modules/excludedChartVariables.js
+++ b/src/renderer/ducks/modules/excludedChartVariables.js
@@ -11,7 +11,9 @@ import { selectors as protocolSelectors } from './protocols';
* ```
* {
* [protocolId]: {
- * [entityType]: [variableName1, variableName2]
+ * [entity]: {
+ * [entityType]: [variableName1, variableName2]
+ * }
* }
* }
* ```
@@ -28,7 +30,12 @@ const reducer = (state = initialState, action = {}) => {
if (!protocolId) {
return state;
}
- const protocolState = { ...state[protocolId], [action.section]: action.variables };
+ const protocolState = { ...state[protocolId],
+ [action.entity]: {
+ ...(state[protocolId] || {})[action.entity],
+ [action.section]: action.variables,
+ },
+ };
return { ...state, [protocolId]: protocolState };
}
default:
@@ -38,13 +45,15 @@ const reducer = (state = initialState, action = {}) => {
/**
* @memberof module:excludedChartVariables
+ * @param {string} entity corresponds to an entity (e.g. node, edge, ego)
* @param {string} section corresponds to an entity (node) type
* @param {Array} variables list of variable names to exclude
* @return {Object} excluded variable names, sectioned by entity type
*/
-const setExcludedVariables = (protocolId, section, variables) => ({
+const setExcludedVariables = (protocolId, entity, section, variables) => ({
type: SET_EXCLUDED_VARIABLES,
protocolId,
+ entity,
section,
variables,
});
diff --git a/src/renderer/ducks/modules/protocols.js b/src/renderer/ducks/modules/protocols.js
index 613d168e2..56f86551b 100644
--- a/src/renderer/ducks/modules/protocols.js
+++ b/src/renderer/ducks/modules/protocols.js
@@ -1,7 +1,7 @@
import AdminApiClient from '../../utils/adminApiClient';
import viewModelMapper from '../../utils/baseViewModelMapper';
import { actionCreators as messageActionCreators } from './appMessages';
-import { transposedCodebookSection } from '../../../main/utils/formatters/network'; // TODO: move
+import { transposedCodebook as networkTransposedCodebook } from '../../../main/utils/formatters/network'; // TODO: move
const LOAD_PROTOCOLS = 'LOAD_PROTOCOLS';
const PROTOCOLS_LOADED = 'PROTOCOLS_LOADED';
@@ -56,15 +56,39 @@ const transposedCodebook = (state, props) => {
}
const codebook = protocol.codebook || {};
- return {
- edge: transposedCodebookSection(codebook.edge),
- node: transposedCodebookSection(codebook.node),
- };
+ return networkTransposedCodebook(codebook);
};
const distributionVariableTypes = ['ordinal', 'categorical'];
const isDistributionVariable = variable => distributionVariableTypes.includes(variable.type);
+const getDistributionVariableNames = variables => (
+ Object.entries(variables || {}).reduce((arr, [variableName, variable]) => {
+ if (isDistributionVariable(variable)) {
+ arr.push(variableName);
+ }
+ return arr;
+ }, [])
+);
+
+const ordinalAndCategoricalVariablesByEntity = (entities) => {
+ if ((entities || {}).name === 'ego') {
+ const variableNames = getDistributionVariableNames(entities.variables);
+ if (variableNames.length) {
+ return { ego: variableNames };
+ }
+ return {};
+ }
+
+ return Object.entries(entities || {}).reduce((acc, [entityType, { variables }]) => {
+ const variableNames = getDistributionVariableNames(variables);
+ if (variableNames.length) {
+ acc[entityType] = variableNames;
+ }
+ return acc;
+ }, {});
+};
+
/**
* @return {Object} all node ordinal & categorical variable names, sectioned by node type
*/
@@ -73,18 +97,10 @@ const ordinalAndCategoricalVariables = (state, props) => {
if (!codebook) {
return {};
}
- return Object.entries(codebook.node || {}).reduce((acc, [entityType, { variables }]) => {
- const variableNames = Object.entries(variables).reduce((arr, [variableName, variable]) => {
- if (isDistributionVariable(variable)) {
- arr.push(variableName);
- }
- return arr;
- }, []);
- if (variableNames.length) {
- acc[entityType] = variableNames;
- }
- return acc;
- }, {});
+
+ return { nodes: ordinalAndCategoricalVariablesByEntity(codebook.node),
+ edges: ordinalAndCategoricalVariablesByEntity(codebook.edge),
+ ego: ordinalAndCategoricalVariablesByEntity(codebook.ego) };
};
const protocolsHaveLoaded = state => state.protocols !== initialState;