Skip to content

Commit

Permalink
Merge pull request #606 from HHS/js-394-add-topics-reasons-graph
Browse files Browse the repository at this point in the history
Add topic/reasons graph widget
  • Loading branch information
jasalisbury authored Nov 9, 2021
2 parents 47d77ef + b6d4218 commit f9d28ac
Show file tree
Hide file tree
Showing 22 changed files with 3,614 additions and 110 deletions.
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"lodash": "^4.17.20",
"moment": "^2.29.1",
"moment-timezone": "^0.5.33",
"plotly.js": "^2.5.1",
"plotly.js-basic-dist": "^2.2.1",
"prop-types": "^15.7.2",
"query-string": "^7.0.0",
Expand All @@ -34,6 +35,7 @@
"react-idle-timer": "^4.4.2",
"react-input-autosize": "^3.0.0",
"react-js-pagination": "^3.0.3",
"react-plotly.js": "^2.5.1",
"react-responsive": "^8.1.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/ButtonSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function ButtonSelect(props) {
endDatePickerId,
dateRange,
disabled,
className,
} = props;

const [checked, setChecked] = useState(applied);
Expand Down Expand Up @@ -160,7 +161,7 @@ function ButtonSelect(props) {
const ariaLabel = `${menuIsOpen ? 'press escape to close ' : 'Open '} ${ariaName}`;

return (
<div className="margin-left-1" onBlur={onBlur} data-testid="data-sort">
<div className={className} onBlur={onBlur} data-testid="data-sort">
<button
onClick={setMenuIsOpen}
onKeyDown={onKeyDown}
Expand Down Expand Up @@ -301,6 +302,7 @@ ButtonSelect.propTypes = {
applied: PropTypes.number.isRequired,
ariaName: PropTypes.string.isRequired,
disabled: PropTypes.bool,
className: PropTypes.string,

// style as a select box
styleAsSelect: PropTypes.bool,
Expand All @@ -321,6 +323,7 @@ ButtonSelect.defaultProps = {
startDatePickerId: '',
endDatePickerId: '',
disabled: false,
className: 'margin-left-1',
};

export default ButtonSelect;
4 changes: 3 additions & 1 deletion frontend/src/pages/ActivityReport/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Note that if this topic list is changed, it needs also to be changed in
// - src/constants.js
export const reasons = [
'Below Competitive Threshold (CLASS)',
'Below Quality Threshold (CLASS)',
Expand Down Expand Up @@ -69,7 +71,7 @@ export const programTypes = [
];

// Note that if this topic list is changed, it needs also to be changed in
// - src/widgets/topicFrequencyGraph.js
// - src/constants.js
export const topics = [
'Behavioral / Mental Health / Trauma',
'Child Assessment, Development, Screening',
Expand Down
37 changes: 18 additions & 19 deletions frontend/src/pages/GranteeRecord/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,26 @@ export default function GranteeRecord({ match, location }) {
'grants.number': '',
granteeId,
});
const [filters, setFilters] = useState([]);
const [error, setError] = useState();

useEffect(() => {
const filtersToApply = [
{
id: uuidv4(),
topic: 'region',
condition: 'Contains',
query: regionId,
},
{
id: uuidv4(),
topic: 'granteeId',
condition: 'Contains',
query: granteeId,
},
];
const defaultFilters = [
{
id: uuidv4(),
topic: 'region',
condition: 'Contains',
query: regionId,
},
{
id: uuidv4(),
topic: 'granteeId',
condition: 'Contains',
query: granteeId,
},
];

setFilters(filtersToApply);
}, [granteeId, regionId]);
// set filters will be used very soon, disabling warning
// eslint-disable-next-line no-unused-vars
const [filters, setFilters] = useState(defaultFilters);
const [error, setError] = useState();

useEffect(() => {
async function fetchGrantee(id, region) {
Expand Down
30 changes: 19 additions & 11 deletions frontend/src/pages/GranteeRecord/pages/TTAHistory.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Grid } from '@trussworks/react-uswds';
import Overview from '../../../widgets/DashboardOverview';
import FrequencyGraph from '../../../widgets/FrequencyGraph';

export default function TTAHistory({ filters }) {
return (
<div className="margin-right-3">
<Overview
fields={[
'Activity reports',
'Hours of TTA',
'Participants',
'In-person activities',
]}
showTooltips
filters={filters}
/>

<Grid>
<Grid col={12}>
<Overview
fields={[
'Activity reports',
'Hours of TTA',
'Participants',
'In-person activities',
]}
showTooltips
filters={filters}
/>
</Grid>
<Grid desktop={{ col: 8 }} tablet={{ col: 12 }}>
<FrequencyGraph filters={filters} />
</Grid>
</Grid>
</div>
);
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import 'react-dates/initialize';
// 'MutationObserver shim removed'
import MutationObserver from '@sheerun/mutationobserver-shim';

// See https://github.com/plotly/react-plotly.js/issues/115
window.URL.createObjectURL = () => {};

window.MutationObserver = MutationObserver;
jest.setTimeout(50000);

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/widgets/BarGraph.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.smart-hub--vertical-text {
writing-mode: vertical-lr;
transform: rotate(180deg);
}
138 changes: 138 additions & 0 deletions frontend/src/widgets/BarGraph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Plot from 'react-plotly.js';
import './BarGraph.css';

const WIDGET_PER_CATEGORY = 180;

/**
*
* Takes a string, a reason (or topic, if you prefer)
* provided for an activity report and intersperses it with line breaks
* depending on the length
*
* @param {string} topic
* @returns string with line breaks
*/
function topicsWithLineBreaks(reason) {
const arrayOfTopics = reason.split(' ');

return arrayOfTopics.reduce((accumulator, currentValue) => {
const lineBreaks = accumulator.match(/<br>/g);
const allowedLength = lineBreaks ? lineBreaks.length * 6 : 6;

// we don't want slashes on their own lines
if (currentValue === '/' || currentValue === '|' || currentValue === '&') {
return `${accumulator} ${currentValue}`;
}

if (accumulator.length > allowedLength) {
return `${accumulator}<br>${currentValue}`;
}

return `${accumulator} ${currentValue}`;
}, '');
}

function BarGraph({ data, yAxisLabel, xAxisLabel }) {
const [plot, updatePlot] = useState({});

useEffect(() => {
if (!data || !Array.isArray(data)) {
return;
}

const categories = [];
const counts = [];

data.forEach((dataPoint) => {
categories.push(dataPoint.category);
counts.push(dataPoint.count);
});

const trace = {
type: 'bar',
x: categories.map((category) => topicsWithLineBreaks(category)),
y: counts,
hoverinfo: 'y',
};

const width = categories.length * WIDGET_PER_CATEGORY;

const layout = {
bargap: 0.5,
height: 300,
hoverlabel: {
bgcolor: '#000',
bordercolor: '#000',
font: {
color: '#fff',
size: 16,
},
},
width,
margin: {
l: 80,
pad: 20,
t: 24,
},
xaxis: {
automargin: true,
fixedrange: true,
tickangle: 0,
},
yaxis: {
tickformat: ',.0d',
fixedrange: true,
},
hovermode: 'none',
};

updatePlot({
data: [trace],
layout,
config: {
responsive: true, displayModeBar: false, hovermode: 'none',
},
});
}, [data]);

return (
<>
<div className="display-flex flex-align-center">
<div className="margin-right-1 smart-hub--vertical-text">
{ yAxisLabel }
</div>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
<div className="overflow-x-scroll" tabIndex={0}>
<caption className="sr-only">Use the arrow keys to scroll graph</caption>
<Plot
data={plot.data}
layout={plot.layout}
config={plot.config}
/>
</div>
</div>
<div className="display-flex flex-justify-center margin-top-1">
{ xAxisLabel }
</div>
</>
);
}

BarGraph.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
category: PropTypes.string,
count: PropTypes.number,
}),
),
yAxisLabel: PropTypes.string.isRequired,
xAxisLabel: PropTypes.string.isRequired,
};

BarGraph.defaultProps = {
data: [],
};

export default BarGraph;
10 changes: 5 additions & 5 deletions frontend/src/widgets/DashboardOverview.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,23 @@ Field.defaultProps = {
const DASHBOARD_FIELDS = [
{
key: 'Activity reports',
render: (data, showTooltip) => <Field showTooltip={showTooltip} tooltipText="The total number of approved activity reports." icon={faChartBar} iconColor="#148439" backgroundColor="#F0FCF4" label="Activity reports" data={data.numReports} />,
render: (data, showTooltip) => <Field key="activity-reports" showTooltip={showTooltip} tooltipText="The total number of approved activity reports." icon={faChartBar} iconColor="#148439" backgroundColor="#F0FCF4" label="Activity reports" data={data.numReports} />,
},
{
key: 'Grants served',
render: (data) => <Field showTooltip={false} icon={faBuilding} iconColor="#2B7FB9" backgroundColor="#E2EFF7" label="Grants served" data={data.numGrants} />,
render: (data) => <Field key="grants-served" showTooltip={false} icon={faBuilding} iconColor="#2B7FB9" backgroundColor="#E2EFF7" label="Grants served" data={data.numGrants} />,
},
{
key: 'Participants',
render: (data, showTooltip) => <Field showTooltip={showTooltip} tooltipText="The total number of people involved in all activities." icon={faUserFriends} iconColor="#264A64" backgroundColor="#ECEEF1" label="Participants" data={data.numParticipants} />,
render: (data, showTooltip) => <Field key="participants" showTooltip={showTooltip} tooltipText="The total number of people involved in all activities." icon={faUserFriends} iconColor="#264A64" backgroundColor="#ECEEF1" label="Participants" data={data.numParticipants} />,
},
{
key: 'Hours of TTA',
render: (data, showTooltip) => <Field showTooltip={showTooltip} tooltipText="The total number of hours spent on all TTA activities." icon={faClock} iconColor="#E29F4D" backgroundColor="#FFF1E0" label="Hours of TTA" data={data.sumDuration} decimalPlaces={1} />,
render: (data, showTooltip) => <Field key="hours-of-tta" showTooltip={showTooltip} tooltipText="The total number of hours spent on all TTA activities." icon={faClock} iconColor="#E29F4D" backgroundColor="#FFF1E0" label="Hours of TTA" data={data.sumDuration} decimalPlaces={1} />,
},
{
key: 'In-person activities',
render: (data, showTooltip) => <Field icon={faUser} showTooltip={showTooltip} tooltipText="Number of activities that were conducted in-person vs. virtual." iconColor="#A12854" backgroundColor="#FFE8F0" label="In-person activities" data={data.inPerson} />,
render: (data, showTooltip) => <Field key="in-person-activities" icon={faUser} showTooltip={showTooltip} tooltipText="Number of activities that were conducted in-person vs. virtual." iconColor="#A12854" backgroundColor="#FFE8F0" label="In-person activities" data={data.inPerson} />,
},
];

Expand Down
17 changes: 17 additions & 0 deletions frontend/src/widgets/FrequencyGraph.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.ttahub--frequency-graph {
width: 100%;
}

@media(min-width: 1090px){
.ttahub--show-accessible-data-button {
flex: 1;
text-align: right;
}
}

@media(max-width: 768px) {
.ttahub--frequency-graph-control-row {
display: block;
width: 100%;
}
}
Loading

0 comments on commit f9d28ac

Please sign in to comment.