Skip to content
This repository has been archived by the owner on Apr 4, 2024. It is now read-only.

Commit

Permalink
Merge pull request #306 from complexdatacollective/fix/cat-and-ord-gr…
Browse files Browse the repository at this point in the history
…aphs

categorical and ordinal graphs need non-transposed data
  • Loading branch information
rebeccamadsen authored Nov 14, 2020
2 parents e80334e + b561395 commit 6f05ff2
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 83 deletions.
2 changes: 1 addition & 1 deletion src/main/data-managers/ProtocolManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ class ProtocolManager {
if (invalidFileList.length > 0) {
processFilePromises.push(
Promise.reject(invalidFileList.map(invalidFile => constructErrorObject(
ErrorMessages.InvalidFileExtension,
ErrorMessages.InvalidSessionFileExtension,
null,
invalidFile,
))),
Expand Down
33 changes: 0 additions & 33 deletions src/main/utils/formatters/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,37 +93,6 @@ const insertEgoInNetworks = networks => (
networks.map(network => insertNetworkEgo(network))
);

const transposedCodebookVariables = (sectionCodebook, definition) => {
if (!definition.variables) { // not required for edges
sectionCodebook[definition.name] = definition; // eslint-disable-line no-param-reassign
return sectionCodebook;
}

const displayVariable = definition.variables[definition.displayVariable];

const variables = Object.values(definition.variables).reduce((acc, variable) => {
acc[variable.name] = variable;
return acc;
}, {});
sectionCodebook[definition.name] = { // eslint-disable-line no-param-reassign
...definition,
displayVariable: displayVariable && displayVariable.name,
variables,
};
return sectionCodebook;
};

const transposedCodebookSection = (section = {}) =>
Object.values(section).reduce((sectionCodebook, definition) => (
transposedCodebookVariables(sectionCodebook, definition)
), {});

const transposedCodebook = (codebook = {}) => ({
edge: transposedCodebookSection(codebook.edge),
node: transposedCodebookSection(codebook.node),
ego: transposedCodebookVariables({}, { ...codebook.ego, name: 'ego' }).ego,
});

module.exports = {
convertUuidToDecimal,
filterNetworkEntities,
Expand All @@ -136,7 +105,5 @@ module.exports = {
caseProperty,
nodePrimaryKeyProperty,
processEntityVariables,
transposedCodebook,
transposedCodebookSection,
unionOfNetworks,
};
33 changes: 30 additions & 3 deletions src/renderer/containers/SettingsScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ import Types from '../types';
import { actionCreators, selectors as protocolSelectors } from '../ducks/modules/protocols';
import { actionCreators as chartActionCreators, selectors as chartSelectors } from '../ducks/modules/excludedChartVariables';

const entityName = (entityKey) => {
if (entityKey === 'nodes') return 'node';
if (entityKey === 'edges') return 'edge';
if (entityKey === 'ego') return 'ego';
return null;
};

const entityVariableType = (codebook, entity, section) => (
codebook && codebook[entityName(entity)] && codebook[entityName(entity)][section] &&
codebook[entityName(entity)][section].name);

const entityVariableName = (codebook, entity, section, variable) => {
if (entity === 'ego') {
return (codebook && codebook[entityName(entity)] && codebook[entityName(entity)].variables &&
codebook[entityName(entity)].variables[variable] &&
codebook[entityName(entity)].variables[variable].name) || variable;
}

return (codebook && codebook[entityName(entity)] && codebook[entityName(entity)][section] &&
codebook[entityName(entity)][section].variables[variable] &&
codebook[entityName(entity)][section].variables[variable].name) || variable;
};

class SettingsScreen extends Component {
constructor(props) {
super(props);
Expand All @@ -21,7 +44,7 @@ class SettingsScreen extends Component {
}

get chartConfigSection() {
const { distributionVariables, protocol, setExcludedVariables } = this.props;
const { distributionVariables, protocol, codebook, setExcludedVariables } = this.props;
if (!Object.keys(distributionVariables).length) {
return null;
}
Expand All @@ -40,15 +63,16 @@ class SettingsScreen extends Component {
<CheckboxGroup
key={section}
className="settings__checkbox-group"
label={entityLabel(entity, section)}
label={entityLabel(entity, entityVariableType(codebook, entity, section))}
input={{
value: this.includedChartVariablesForSection(entity, section),
onChange: (newValue) => {
const newExcluded = vars.filter(v => !newValue.includes(v));
setExcludedVariables(protocol.id, entity, section, newExcluded);
},
}}
options={vars.map(v => ({ value: v, label: v }))}
options={vars.map(v =>
({ value: v, label: entityVariableName(codebook, entity, section, v) }))}
/>
))))
}
Expand Down Expand Up @@ -120,6 +144,7 @@ const mapStateToProps = (state, ownProps) => ({
excludedChartVariables: chartSelectors.excludedVariablesForCurrentProtocol(state, ownProps),
protocolsHaveLoaded: protocolSelectors.protocolsHaveLoaded(state),
protocol: protocolSelectors.currentProtocol(state, ownProps),
codebook: protocolSelectors.currentCodebook(state, ownProps),
distributionVariables: protocolSelectors.ordinalAndCategoricalVariables(state, ownProps),
});

Expand All @@ -133,6 +158,7 @@ SettingsScreen.defaultProps = {
apiClient: null,
distributionVariables: {},
excludedChartVariables: {},
codebook: {},
protocol: null,
};

Expand All @@ -141,6 +167,7 @@ SettingsScreen.propTypes = {
distributionVariables: PropTypes.object,
match: PropTypes.object.isRequired,
excludedChartVariables: PropTypes.object,
codebook: PropTypes.object,
protocol: Types.protocol,
protocolsHaveLoaded: PropTypes.bool.isRequired,
setExcludedVariables: PropTypes.func.isRequired,
Expand Down
33 changes: 26 additions & 7 deletions src/renderer/containers/workspace/EntityTimeSeriesPanel.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import { selectors as protocolSelectors } from '../../ducks/modules/protocols';
import { EmptyData, TimeSeriesChart } from '../../components';
import withApiClient from '../../components/withApiClient';

const { currentCodebook } = protocolSelectors;

// Data series are keyed with node_[subtype] and edge_[subtype]; we can assume subtypes are
// meaningfully unique and label with just the subtype
const subtypeLabel = subtype => ({ key: subtype, label: `${subtype.split('_')[1]}` });
const subtypeLabel = subtype => subtype.split('_')[1];
const codebookSubtypeLabel = (codebook, entityType, subtype) => (
(codebook && codebook[entityType][subtypeLabel(subtype)] &&
codebook[entityType][subtypeLabel(subtype)].name) || subtypeLabel(subtype)
);

// Based on the API response, determine which series to render.
// If there's only one node subtype (e.g., 'person'), don't render it.
const dataSeries = (timeSeriesKeys = []) => {
const dataSeries = (timeSeriesKeys = [], codebook) => {
const series = [];
const nodeSubtypes = timeSeriesKeys.filter(key => (/node_/).test(key));
const edgeSubtypes = timeSeriesKeys.filter(key => (/edge_/).test(key));
if (timeSeriesKeys.includes('node')) {
series.push({ key: 'node', label: 'node' });
}
if (nodeSubtypes.length > 1) {
series.push(...nodeSubtypes.map(subtypeLabel));
series.push(...nodeSubtypes.map(subtype =>
({ key: subtype, label: codebookSubtypeLabel(codebook, 'node', subtype) })));
}
if (timeSeriesKeys.includes('edge')) {
series.push({ key: 'edge', label: 'edge' });
}
if (edgeSubtypes.length > 1) {
series.push(...edgeSubtypes.map(subtypeLabel));
series.push(...edgeSubtypes.map(subtype =>
({ key: subtype, label: codebookSubtypeLabel(codebook, 'edge', subtype) })));
}
return series;
};
Expand Down Expand Up @@ -67,9 +77,10 @@ class EntityTimeSeriesPanel extends PureComponent {

render() {
const { timeSeriesData, timeSeriesKeys } = this.state;
const { codebook } = this.props;
let content;
if (timeSeriesData.length > 0) {
const series = dataSeries(timeSeriesKeys);
const series = dataSeries(timeSeriesKeys, codebook);
content = <TimeSeriesChart data={this.state.timeSeriesData} series={series} />;
} else {
content = <EmptyData />;
Expand All @@ -90,16 +101,24 @@ class EntityTimeSeriesPanel extends PureComponent {
EntityTimeSeriesPanel.defaultProps = {
apiClient: null,
sessionCount: null,
codebook: {},
};

EntityTimeSeriesPanel.propTypes = {
apiClient: PropTypes.object,
protocolId: PropTypes.string.isRequired,
sessionCount: PropTypes.number,
codebook: PropTypes.object,
};

export default withApiClient(EntityTimeSeriesPanel);
const mapStateToProps = (state, ownProps) => ({
codebook: currentCodebook(state, ownProps),
});

const UnconnectedEntityTimeSeriesPanel = (withApiClient(EntityTimeSeriesPanel));

export default connect(mapStateToProps)(withApiClient(EntityTimeSeriesPanel));

export {
EntityTimeSeriesPanel as UnconnectedEntityTimeSeriesPanel,
UnconnectedEntityTimeSeriesPanel,
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import React from 'react';
import { mount } from 'enzyme';

import EntityTimeSeriesPanel from '../EntityTimeSeriesPanel';
import { UnconnectedEntityTimeSeriesPanel } from '../EntityTimeSeriesPanel';
import AdminApiClient from '../../../utils/adminApiClient';

jest.mock('recharts');
Expand All @@ -24,7 +24,7 @@ describe('ProtocolCountsPanel', () => {

beforeEach(() => {
mockApiClient = new AdminApiClient();
subject = mount(<EntityTimeSeriesPanel {...props} />);
subject = mount(<UnconnectedEntityTimeSeriesPanel {...props} />);
});

it('renders a dashboard panel', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jest.mock('../../../ducks/modules/protocols', () => ({
currentProtocol: jest.fn(),
currentProtocolId: jest.fn().mockReturnValue('1'),
isDistributionVariable: jest.fn().mockReturnValue(true),
transposedCodebook: jest.fn().mockReturnValue({
currentCodebook: jest.fn().mockReturnValue({
node: {
person: {
variables: {
Expand Down
29 changes: 15 additions & 14 deletions src/renderer/containers/workspace/withAnswerDistributionCharts.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { selectors as protocolSelectors } from '../../ducks/modules/protocols';
import { selectors as variableSelectors } from '../../ducks/modules/excludedChartVariables';
import Types from '../../types';

const { currentProtocolId, isDistributionVariable, transposedCodebook } = protocolSelectors;
const { currentProtocolId, isDistributionVariable, currentCodebook } = protocolSelectors;
const { excludedVariablesForCurrentProtocol } = variableSelectors;

const hasData = bucket => bucket && Object.keys(bucket).length > 0;
Expand All @@ -24,17 +24,18 @@ const hasData = bucket => bucket && Object.keys(bucket).length > 0;
*
* @private
*
* @param {Object} transposedNodeCodebook `transposedCodebook.node`, with transposed names
* @param {Object} nodeCodebook `codebook.node`
* @param {Object} buckets The API response from `option_buckets`
* @return {Array} chartDefinitions
*/
const shapeBucketDataByType = (
transposedNodeCodebook, buckets, excludedChartVariables, entityKey) =>
Object.entries(transposedNodeCodebook).reduce((acc, [entityType, { variables }]) => {
nodeCodebook, buckets, excludedChartVariables, entityKey) =>
Object.entries(nodeCodebook).reduce((acc, [entityType, { variables }]) => {
const excludedSectionVariables = (excludedChartVariables[entityKey] &&
excludedChartVariables[entityKey][entityType]) || [];
Object.entries(variables || []).forEach(([variableName, def]) => {
if (!isDistributionVariable(def) || excludedSectionVariables.includes(def.name)) {
Object.keys(variables || []).forEach((variableName) => {
const def = variables[variableName];
if (!isDistributionVariable(def) || excludedSectionVariables.includes(variableName)) {
return;
}
const dataPath = entityKey === 'ego' ? [variableName] : [entityType, variableName];
Expand All @@ -53,7 +54,7 @@ const shapeBucketDataByType = (
});
acc.push({
entityKey,
entityType,
entityType: nodeCodebook[entityType].name,
variableType: def.type,
variableDefinition: def,
chartData: values || [],
Expand Down Expand Up @@ -85,7 +86,7 @@ const withAnswerDistributionCharts = (WrappedComponent) => {
excludedChartVariables: PropTypes.object,
protocolId: PropTypes.string,
totalSessionsCount: PropTypes.number,
transposedCodebook: Types.codebook.isRequired,
codebook: Types.codebook.isRequired,
}

static defaultProps = {
Expand Down Expand Up @@ -123,23 +124,23 @@ const withAnswerDistributionCharts = (WrappedComponent) => {
const {
excludedChartVariables,
protocolId,
transposedCodebook: {
codebook: {
node: nodeCodebook = {},
edge: edgeCodebook = {},
ego: egoCodebook = {},
},
} = this.props;

const nodeNames = Object.values(nodeCodebook).reduce((acc, nodeTypeDefinition) => (
const nodeNames = Object.keys(nodeCodebook).reduce((acc, nodeType) => (
{
...acc,
[nodeTypeDefinition.name]: Object.keys(nodeTypeDefinition.variables || {}),
[nodeType]: Object.keys(nodeCodebook[nodeType].variables || {}),
}
), {});
const edgeNames = Object.values(edgeCodebook).reduce((acc, edgeTypeDefinition) => (
const edgeNames = Object.keys(edgeCodebook).reduce((acc, edgeType) => (
{
...acc,
[edgeTypeDefinition.name]: Object.keys(edgeTypeDefinition.variables || {}),
[edgeType]: Object.keys(edgeCodebook[edgeType].variables || {}),
}
), {});
const egoNames = Object.keys(egoCodebook.variables || {});
Expand Down Expand Up @@ -168,7 +169,7 @@ const withAnswerDistributionCharts = (WrappedComponent) => {
const mapStateToProps = (state, ownProps) => ({
excludedChartVariables: excludedVariablesForCurrentProtocol(state, ownProps),
protocolId: currentProtocolId(state, ownProps),
transposedCodebook: transposedCodebook(state, ownProps),
codebook: currentCodebook(state, ownProps),
});

return connect(mapStateToProps)(AnswerDistributionPanels);
Expand Down
26 changes: 13 additions & 13 deletions src/renderer/ducks/modules/__tests__/protocols-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe('the protocols module', () => {
isDistributionVariable,
ordinalAndCategoricalVariables,
protocolsHaveLoaded,
transposedCodebook,
currentCodebook,
} = selectors;

describe('currentProtocol', () => {
Expand Down Expand Up @@ -136,7 +136,7 @@ describe('the protocols module', () => {
};
const state = { protocols: [{ id: '1', codebook }] };
const props = { match: { params: { id: '1' } } };
expect(ordinalAndCategoricalVariables(state, props)).toEqual({ nodes: { person: ['catVar'] }, edges: { friend: ['ordVar'] }, ego: { ego: ['catVar'] } });
expect(ordinalAndCategoricalVariables(state, props)).toEqual({ nodes: { 'node-type-id': ['var-id-1'] }, edges: { 'edge-type-id': ['var-id-2'] }, ego: { ego: ['var-id-3'] } });
});

it('ignores sections without these variables', () => {
Expand Down Expand Up @@ -167,22 +167,22 @@ describe('the protocols module', () => {
});
});

describe('transposedCodebook', () => {
it('returns a modified codebook', () => {
describe('currentCodebook', () => {
it('returns a codebook', () => {
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');
const codebook2 = currentCodebook(state, props);
expect(codebook2).toHaveProperty('node');
expect(codebook2.node).toHaveProperty('node-type-id');
expect(codebook2).toHaveProperty('edge');
expect(codebook2.edge).toHaveProperty('edge-type-id');
expect(codebook2).toHaveProperty('ego');
expect(codebook2.ego.variables).toHaveProperty('var-id-1');
});

it('does not require edge variables', () => {
Expand All @@ -192,8 +192,8 @@ describe('the protocols module', () => {
};
const state = { protocols: [{ id: '1', codebook }] };
const props = { match: { params: { id: '1' } } };
const transposed = transposedCodebook(state, props);
expect(transposed.edge).toEqual({ 'edge-name': { name: 'edge-name' } });
const codebook2 = currentCodebook(state, props);
expect(codebook2.edge).toEqual({ 'edge-type-id': { name: 'edge-name' } });
});
});
});
Expand Down
Loading

0 comments on commit 6f05ff2

Please sign in to comment.