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

[WIP] Full grants table #52

Merged
merged 8 commits into from
Mar 30, 2021
Merged
5 changes: 5 additions & 0 deletions src/components/GrantRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { slugify, dollarsFormatter } from '../utils';
const GrantRow = ({ amount, description, org, orgUuid, summary, years, uuid }) => {
let name = description;

// Keep grant descriptions to roughly one line; handle better using text-oveflow in future?
if (name.length > 110) {
name = name.slice(0,110) + '...';
}

if (summary) {
name = <strong><a href={`/organizations/${slugify(name)}/${orgUuid}`}>{org}</a></strong>;
} else if (uuid) {
Expand Down
35 changes: 26 additions & 9 deletions src/components/YearlySumsBarchart.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,55 @@ const YearlySumsBarchart = ({ sums }) => {
(acc, cur) => (cur > acc ? cur : acc),
0
);

const startYear = Object.keys(sums).reduce(
(acc, cur) => (cur < acc ? cur : acc),
Infinity
);

const endYear = Object.keys(sums).reduce(
(acc, cur) => (cur > acc ? cur : acc),
0
);

const baseHeight = 35;

return (
<figure>
<figcaption>
Graph of totals by year, {startYear} - {endYear}
<figure style={{ margin: `5px 0 5px 0` }}>
<figcaption style={{ fontWeight: 700, color: `#04ae36` }}>
Annual grant totals, {startYear} - {endYear}
</figcaption>
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height={data.length * 25}
height={data.length * baseHeight}
role="img"
>
{data.map((d, i) => (
<g key={d.label}>
<rect x="0" y={i * 25} fill="#ccc" width="100%" height="25"></rect>
<rect
x="0"
y={i * baseHeight}
width="100%"
height={`${baseHeight}`}
fill="#ebf9ef"
></rect>
<rect
x="0"
y={i * 25}
fill="#0088cc"
y={i * baseHeight}
width={`${(d.value / max || 0) * 100}%`}
height="25"
height={`${baseHeight}`}
fill="#04ae36"
stroke="#ebf9ef"
strokeWidth="3px"
></rect>
<text x="6" y={i * 25 + 12} fill="#fff" dy=".35em">
<text
x="6"
y={i * baseHeight + (baseHeight / 2)}
dy=".35em"
fill="#000"
>
{d.label}: {dollarsFormatter.format(d.value)}
</text>
</g>
Expand Down
225 changes: 225 additions & 0 deletions src/containers/GrantsTableWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import React from 'react';

import { uniq, map, filter, findIndex, sortBy } from 'lodash';

import { useQuery, gql } from '@apollo/client';

import { stripHtml, extractYear } from '../utils';

import GrantTable from '../components/GrantTable';

const GET_ORGANIZATION_GRANTS = gql`
query getOrg($organizationId: String!, $grantsFundedOffset: Int, $grantsReceivedOffset: Int, $limit: Int) {
organization(uuid: $organizationId) {
grantsFunded (offset: $grantsFundedOffset, limit: $limit, orderBy: uuid) {
uuid
dateFrom
dateTo
to {
name
uuid
}
amount
description
}
grantsReceived (offset: $grantsReceivedOffset, limit: $limit, orderBy: uuid) {
uuid
dateFrom
dateTo
from {
name
uuid
}
amount
description
}
}
}
`;

const GrantsTableWrapper = (props) => {
const { loading, error, data, fetchMore } = useQuery(GET_ORGANIZATION_GRANTS, {
variables: {
organizationId: props.organizationId,
grantsFundedOffset: 0,
grantsReceivedOffset: 0,
limit: 100
},
notifyOnNetworkStatusChange: true, // Update 'loading' prop after fetchMore
});

if (loading) return 'Loading...';
if (error) return `Error! ${error}`;

if (!data.organization) return `Failed to load org data!`;

const loadMoreGrantsFunded = data.organization.grantsFunded.length < props.countGrantsFrom;
const loadMoreGrantsReceived = data.organization.grantsReceived.length < props.countGrantsTo;

// If our initial response is fewer records than the total grant count for either side,
// fetchMore records and merge the arrays using apollo's offset-based pagination
if (loadMoreGrantsFunded || loadMoreGrantsReceived) {
fetchMore({
variables: {
grantsFundedOffset: data.organization.grantsFunded.length,
grantsReceivedOffset: data.organization.grantsReceived.length,
},
});
}

console.log(
`Fetched ${data.organization.grantsFunded.length}/${props.countGrantsFrom} grants funded\
and ${data.organization.grantsReceived.length}/${props.countGrantsTo} grants received.`
);

// Do this after offset-basd fetching because arrays lengthen during cleansing
const {
Copy link
Member

@hampelm hampelm Mar 29, 2021

Choose a reason for hiding this comment

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

Moving this block up around line 55 could shorten some of the code above, if I'm reading it correctly

Copy link
Member Author

@jessicamcinchak jessicamcinchak Mar 29, 2021

Choose a reason for hiding this comment

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

Ah so this is a bit tricky -- we need the orginal length of the grantsFunded/grantsReceived response pre-cleanse in order to properly set offset. Because the cleanse will create and insert the "org header rows" directly into the array, we'd be trying to fetch something like 182/171 possible grants if this is defined upfront.

Maybe a phase 2 refactor though, would be nice to have shorter variable names here.

grantsFunded,
grantsReceived,
fundedYearlySums,
receivedYearlySums,
} = cleanse(data.organization);

return (
<GrantTable
verb={props.showGrantSide}
grants={props.showGrantSide === 'funded' ? grantsFunded : grantsReceived}
sums={
props.showGrantSide === 'funded' ? fundedYearlySums : receivedYearlySums
}
/>
);
};

/**
* Flatten, format, and sort our grant attributes
* and calculate organization- and year-level totals
*/
const cleanse = (organization) => {
Copy link
Member

Choose a reason for hiding this comment

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

Could this get a short comment about its role?

// Flatten grantsReceived & grantsFunded nested objects
const flattenedGrantsReceived = organization.grantsReceived.map((grant) => {
const dateFrom = extractYear(grant.dateFrom);
const dateTo = extractYear(grant.dateTo);
const years = `${dateFrom} - ${dateTo}`;
const desc = grant.description ? stripHtml(grant.description) : 'No description available';

return {
org: grant.from.name,
orgUuid: grant.from.uuid,
description: desc,
amount: grant.amount,
uuid: grant.uuid,
dateFrom,
dateTo,
years,
summary: false,
};
});

const flattenedGrantsFunded = organization.grantsFunded.map((grant) => {
const dateFrom = extractYear(grant.dateFrom);
const dateTo = extractYear(grant.dateTo);
const years = `${dateFrom} - ${dateTo}`;
const desc = grant.description ? stripHtml(grant.description) : 'No description available';

return {
org: grant.to.name,
orgUuid: grant.to.uuid,
description: desc,
amount: grant.amount,
uuid: grant.uuid,
dateFrom,
dateTo,
years,
summary: false,
};
});

// Sort flattened lists by org id (boring) and then the inverse of the start year.
const sortedFlatGrantsReceived = sortBy(
flattenedGrantsReceived,
(grant) => grant.orgUuid + 1 / grant.dateFrom
);

const sortedFlatGrantsFunded = sortBy(
flattenedGrantsFunded,
(grant) => grant.orgUuid + 1 / grant.dateFrom
);

// Insert summary rows with org-level totals into the lists
const { grants: grantsFunded, yearlySums: fundedYearlySums } = addSummaryRows(
sortedFlatGrantsFunded
);

const { grants: grantsReceived, yearlySums: receivedYearlySums } = addSummaryRows(
sortedFlatGrantsReceived
);

// Create a map containing a union of years in funded & received sums with zero values
// This is used to ensure that the bar charts for funded/received have the same y axis categories
const allYears = Object.keys({
...fundedYearlySums,
...receivedYearlySums,
}).reduce((acc, cur) => ({ ...acc, [cur]: 0 }), {});
Copy link
Member

Choose a reason for hiding this comment

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

could this get longer variable names?


return {
grantsFunded,
fundedYearlySums: { ...allYears, ...fundedYearlySums },
grantsReceived,
receivedYearlySums: { ...allYears, ...receivedYearlySums },
};
};

/**
* Add summary rows to table data,
* and provide yearly totals.
*/
function addSummaryRows(grantsOrig) {
// Copy the provided array.
const grants = grantsOrig;

// Get orgs
const uniqOrgs = uniq(map(grants, 'orgUuid'));

// Get stats per org, and stick em right in the array of grants as summary rows!
const yearlySums = {};
let insertAt = 0;

uniqOrgs.forEach((orgUuid) => {
let org = '';

const sum = filter(grants, { orgUuid }).reduce((memo, grant) => {
// along the way, build our yearly sums!
if (yearlySums[grant.dateFrom] > 0) {
yearlySums[grant.dateFrom] += grant.amount;
} else {
yearlySums[grant.dateFrom] = grant.amount;
}

// This'll happen over and over but that is just fine. We just want the right name.
org = grant.org;

return memo + grant.amount;
}, 0);

// Find first row of this org's grant
insertAt = findIndex(grants, { orgUuid }, insertAt);

// Splice in their stats row there
// Add 1 to search index since we inserted another row
grants.splice(insertAt, 0, {
org,
description: `${org}:`,
orgUuid,
amount: sum,
uuid: `summaryrow-${orgUuid}`,
start: null,
end: null,
summary: true,
});
});

return { grants, yearlySums };
};

export default GrantsTableWrapper;
Loading