Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DC-710]Add mocked visualizations and total cohort count #5114

Open
wants to merge 17 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 139 additions & 38 deletions src/dataset-builder/CohortEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Spinner, useLoadedData } from '@terra-ui-packages/components';
import _ from 'lodash/fp';
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { div, h, h2, h3, strong } from 'react-hyperscript-helpers';
import Chart from 'src/components/Chart';
import { ButtonOutline, ButtonPrimary, GroupedSelect, Link, Select } from 'src/components/common';
import Slider from 'src/components/common/Slider';
import { icon } from 'src/components/icons';
Expand All @@ -17,6 +18,13 @@ import {
ProgramDataListCriteria,
ProgramDataRangeCriteria,
} from 'src/dataset-builder/DatasetBuilderUtils';
import {
cohortAgeData,
cohortDemographicData,
CohortDemographics,
generateAgeSeries,
generateDemographicSeries,
} from 'src/dataset-builder/TestConstants';
import {
DataRepo,
SnapshotBuilderCountResponse,
Expand Down Expand Up @@ -592,48 +600,141 @@ export const CohortEditor: React.FC<CohortEditorProps> = (props) => {
getNextCriteriaIndex,
} = props;
const [cohort, setCohort] = useState<Cohort>(originalCohort);
const [snapshotRequestParticipantCount, setSnapshotRequestParticipantCount] =
useLoadedData<SnapshotBuilderCountResponse>();
const [cohortAges, setCohortAges] = useState<CohortDemographics>(cohortAgeData);
const [cohortDemographics, setCohortDemographics] = useState<CohortDemographics>(cohortDemographicData);

function chartOptions(cohortDemographics: CohortDemographics) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The charts are meaty enough they may be worth an intermediate component, so that we can isolate the chart configuration. What are your thoughts on that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I think that's a good idea

return {
chart: {
spacingLeft: 20,
spacingRight: 30,
height: cohortDemographics.height,
style: { fontFamily: 'inherit' },
type: 'bar',
},
legend: { enabled: cohortDemographics.legendEnabled },
plotOptions: { series: { stacking: 'normal' } },
series: cohortDemographics.series,
title: {
align: 'left',
style: { fontSize: '16px', fontWeight: 'bold', color: '#333f52' },
text: cohortDemographics.title,
},
tooltip: {
followPointer: true,
formatter() {
// @ts-ignore
// eslint-disable-next-line react/no-this-in-sfc
const currCategory = _.find((category) => category.short === this.x, cohortDemographics.categories);
const categoryDescription = currCategory.long || currCategory.short;
if (cohortDemographics.showSeriesName) {
// @ts-ignore
// eslint-disable-next-line react/no-this-in-sfc
return `${categoryDescription} <br/><span style="color:${this.color}">\u25CF</span> ${this.series.name}<br/> ${this.y}`;
}
// @ts-ignore
// eslint-disable-next-line react/no-this-in-sfc
return `${categoryDescription} <br/> ${this.y}`;
},
},
xAxis: {
categories: _.map('short', cohortDemographics.categories),
crosshair: true,
},
yAxis: {
crosshair: true,
title: { text: cohortDemographics.yTitle },
},
accessibility: {
point: {
descriptionFormatter: (point) => {
return `${point.index + 1}. Category ${point.category}, ${point.series.name}: ${point.y}.`;
},
},
},
exporting: { buttons: { contextButton: { x: -15 } } },
};
}

const updateCohort = (updateCohort: (Cohort) => Cohort) => setCohort(updateCohort);

return h(Fragment, [
h(CohortEditorContents, {
updateCohort,
cohort,
snapshotId,
snapshotBuilderSettings,
onStateChange,
getNextCriteriaIndex,
}),
// add div to cover page to footer
div(
{
style: {
display: 'flex',
backgroundColor: editorBackgroundColor,
alignItems: 'end',
flexDirection: 'row-reverse',
padding: wideMargin,
useEffect(() => {
setSnapshotRequestParticipantCount(
withErrorReporting(`Error fetching snapshot builder count for snapshot ${snapshotId}`)(async () =>
DataRepo()
.snapshot(snapshotId)
.getSnapshotBuilderCount(createSnapshotBuilderCountRequest([cohort]))
)
);
}, [snapshotId, setSnapshotRequestParticipantCount, cohort]);

useEffect(() => {
setTimeout(() => {
cohortAgeData.series = generateAgeSeries(3, 90);
setCohortAges(cohortAgeData);
}, 2000);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this timeout is doing everything I want it to do. It does stop a double re-render (like without it, the graphs re-render when the async count method is called and again when it is returned), but I wanted to add a timeout so that it would return at a similar time as the async count call and it isn't doing that even if I increase the time. What am I missing here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I wouldn't expect you to need both the timeout and the useEffect subscribed to snapshotRequestParticipantCount. This effect should be called whenever snapshotRequestParticipantCount is updated. The trick might be that snapshotRequestParticipantCount is a useLoadedData hook, so you probably need to check the status of useLoadedData and only display/update when it is 'Ready'

}, [snapshotRequestParticipantCount]);

useEffect(() => {
setTimeout(() => {
cohortDemographicData.series = generateDemographicSeries();
setCohortDemographics(cohortDemographicData);
}, 2000);
}, [snapshotRequestParticipantCount]);

return div({ style: { display: 'flex' } }, [
div([
h(CohortEditorContents, {
updateCohort,
cohort,
snapshotId,
snapshotBuilderSettings,
onStateChange,
getNextCriteriaIndex,
}),
// add div to cover page to footer
div(
{
style: {
display: 'flex',
backgroundColor: editorBackgroundColor,
alignItems: 'end',
flexDirection: 'row-reverse',
padding: '0 3rem',
},
},
},
[
h(
ButtonPrimary,
{
onClick: () => {
updateCohorts((cohorts) => {
const index = _.findIndex((c) => _.equals(c.name, cohort.name), cohorts);
if (index === -1) {
// Only add to selectedCohorts on creation of new cohort
addSelectedCohort(cohort);
}
return _.set(`[${index === -1 ? cohorts.length : index}]`, cohort, cohorts);
});
onStateChange(homepageState.new());
[
h(
ButtonPrimary,
{
onClick: () => {
updateCohorts((cohorts) => {
const index = _.findIndex((c) => _.equals(c.name, cohort.name), cohorts);
if (index === -1) {
// Only add to selectedCohorts on creation of new cohort
addSelectedCohort(cohort);
}
return _.set(`[${index === -1 ? cohorts.length : index}]`, cohort, cohorts);
});
onStateChange(homepageState.new());
},
},
},
['Save cohort']
),
]
),
['Save cohort']
),
]
),
]),
div({ style: { backgroundColor: 'white', width: '42rem' } }, [
h2({ style: { marginLeft: '1rem' } }, [
'Total participant count: ',
snapshotRequestParticipantCount.status === 'Ready'
s-rubenstein marked this conversation as resolved.
Show resolved Hide resolved
? formatCount(snapshotRequestParticipantCount.state.result.total)
: h(Spinner),
]),
h(Chart, { options: chartOptions(cohortAges) }),
h(Chart, { options: chartOptions(cohortDemographics) }),
]),
]);
};
110 changes: 110 additions & 0 deletions src/dataset-builder/TestConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,113 @@ const dummyConcepts = [
export const dummyGetConceptForId = (id: number): Concept => {
return _.find({ id }, dummyConcepts)!;
};

interface ChartLabel {
short: string;
long?: string;
}
interface ChartSeries {
name?: string;
data: number[];
}

export interface CohortDemographics {
categories: ChartLabel[];
series: ChartSeries[];
title: string;
yTitle: string;
height: string;
legendEnabled: boolean;
showSeriesName: boolean;
}

export const cohortAgeData = {
categories: [
{ short: 'Female' },
{ short: 'Male' },
{ short: 'Other', long: 'Nonbinary, 2 Spirit, Genderqueer, etc.' },
],
series: generateAgeSeries(3, 90),
title: 'Gender identity',
yTitle: 'AVERAGE AGE',
height: '250rem',
legendEnabled: false,
showSeriesName: false,
};

export const cohortDemographicData = {
categories: [
{ short: 'Female' },
{ short: 'Female 18-44' },
{ short: 'Female 45-64' },
{ short: 'Female 65+' },
{ short: 'Male' },
{ short: 'Male 18-44' },
{ short: 'Male 45-64' },
{ short: 'Male 65+' },
{ short: 'Other', long: 'Nonbinary, 2 Spirit, Genderqueer, etc.' },
{ short: 'Other 18-44', long: 'Nonbinary, 2 Spirit, Genderqueer, etc. 18-44' },
{ short: 'Other 45-64', long: 'Nonbinary, 2 Spirit, Genderqueer, etc. 45-64' },
{ short: 'Other 65+', long: 'Nonbinary, 2 Spirit, Genderqueer, etc. 65+' },
],
series: generateDemographicSeries(),
title: 'Gender identity, current age, race',
yTitle: 'OVERALL PERCENTAGE',
height: '500rem',
legendEnabled: true,
showSeriesName: true,
};

export function generateAgeSeries(numNumbers: number, max: number) {
const randomNumbers: number[] = [];
for (let i = 0; i < numNumbers; i++) {
const randomNumber = Math.floor(Math.random() * max) + 1;
randomNumbers.push(randomNumber);
}
return [{ name: 'Overall', data: randomNumbers }];
}

function generateRandomNumbersThatAddUpTo(total: number, numNumbers: number): number[] {
const randomNumbers: number[] = [];
let remaining = total;
for (let i = 0; i < numNumbers - 1; i++) {
const randomNumber = Math.floor(Math.random() * remaining);
remaining -= randomNumber;
randomNumbers.push(randomNumber);
}
randomNumbers.push(remaining);
return _.shuffle(randomNumbers);
}

// series and data will always be the same length
function addToSeriesData(series: ChartSeries[], data: number[]) {
for (let i = 0; i < series.length; i++) {
series[i].data.push(data[i]);
}
}

export function generateDemographicSeries(): ChartSeries[] {
// order of data points is Female, Male, Other, Female 18-44, Female 45-64, Female 65+, Male 18-44, Male
const series: ChartSeries[] = [
{ name: 'Asian', data: [] },
{ name: 'Black', data: [] },
{ name: 'White', data: [] },
{ name: 'Native American', data: [] },
{ name: 'Pacific Islander', data: [] },
];
// for each of the three gender identity totals,
const genderTotals = generateRandomNumbersThatAddUpTo(100, 3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as a note - and its dummy data so not a big deal, but I think we would want it to add up to the number of participants, not to 100?

That said, I think trying to get the dummy data perfect is not worth the effort, and just letting it render charts seems fine to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the graph is based on the overall percentage of participants not the number of participants. that's how it was in the mock

for (const genderTotal of genderTotals) {
// generate the race breakdown for the gender totals
const genderTotalRaceBreakDown = generateRandomNumbersThatAddUpTo(genderTotal, 5);
addToSeriesData(series, genderTotalRaceBreakDown);
// get the three age group breakdowns
const ageGroupGenderTotal = generateRandomNumbersThatAddUpTo(genderTotal, 3);
// get race breakdowns for each of the gender age groups
for (const genderAgeGroupTotal of ageGroupGenderTotal) {
const genderAgeGroupRaceBreakdown = generateRandomNumbersThatAddUpTo(genderAgeGroupTotal, 5);
addToSeriesData(series, genderAgeGroupRaceBreakdown);
}
}
return series;
}
Loading