Skip to content

Commit

Permalink
[47] Introduce simple pivot table (#48)
Browse files Browse the repository at this point in the history
* Mocked view of pivot table

* Create endpoint for fetching pivot table data

* WIP redux for fetching pivot table data

* Working prototype of the pivot table for cases by country

* Refactoring and typing

* Cleanup and fix mocked total data

* Create cypress tests

* New tests for pivot table
  • Loading branch information
stanislaw-zakrzewski committed Jan 11, 2024
1 parent dddd7a8 commit 2aab131
Show file tree
Hide file tree
Showing 15 changed files with 891 additions and 2 deletions.
51 changes: 51 additions & 0 deletions data-serving/data-service/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,20 @@ paths:
$ref: '#/components/responses/422'
'500':
$ref: '#/components/responses/500'
/cases/countryData:
get:
summary: Data for cases by country table
operationId: listCountryData
tags: [ Suggest ]
responses:
'200':
$ref: '#/components/responses/200CasesByCountryObject'
'400':
$ref: '#/components/responses/400'
'403':
$ref: '#/components/responses/403'
'500':
$ref: '#/components/responses/500'
/cases/symptoms:
get:
summary: Lists most frequently used symptoms
Expand Down Expand Up @@ -901,6 +915,37 @@ components:
type: array
items:
type: string
CasesByCountryObject:
type: object
properties:
countries:
type: object
additionalProperties:
type: object
properties:
confirmed:
type: integer
suspected:
type: integer
death:
type: integer
recovered:
type: integer
total:
type: integer
globally:
type: object
properties:
confirmed:
type: integer
suspected:
type: integer
death:
type: integer
recovered:
type: integer
total:
type: integer
PlacesOfTransmissionArray:
type: object
properties:
Expand Down Expand Up @@ -1114,6 +1159,12 @@ components:
application/json:
schema:
$ref: '#/components/schemas/BatchUpdateResponse'
'200CasesByCountryObject':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/CasesByCountryObject'
'200SymptomArray':
description: OK
content:
Expand Down
217 changes: 216 additions & 1 deletion data-serving/data-service/src/controllers/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '../util/case';
import { logger } from '../util/logger';
import stringify from 'csv-stringify/lib/sync';
import _ from 'lodash';
import _, { forEach } from 'lodash';
import { AgeBucket } from '../model/age-bucket';
import { COUNTER_DOCUMENT_ID, IdCounter } from '../model/id-counter';
import { User } from '../model/user';
Expand Down Expand Up @@ -488,6 +488,221 @@ export class CasesController {
}
};

/**
* Get table data for Cases by Country.
*
* Handles HTTP GET /api/cases/countryData.
*/
countryData = async (req: Request, res: Response): Promise<void> => {
logger.info('Get cases by country method entrypoint');

try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const countriesData: any = {};
// Get total case cardinality
const grandTotalCount = await Day0Case.countDocuments({
caseStatus: ['confirmed', 'suspected'],
});
if (grandTotalCount === 0) {
res.status(200).json({});
return;
}

// Get cardinality of case statuses by country
const countriesStatusCounts = await Day0Case.aggregate([
{
$group: {
_id: {
country: '$location.country',
caseStatus: '$caseStatus',
},
totalCount: { $sum: 1 },
},
},
{
$group: {
_id: '$_id.country',
caseStatus: {
$push: {
k: '$_id.caseStatus',
v: '$totalCount',
},
},
total: { $sum: '$totalCount' },
},
},
{
$project: {
caseStatus: { $arrayToObject: '$caseStatus' },
total: 1,
},
},
]);

forEach(countriesStatusCounts, (countryStatusCounts) => {
countriesData[countryStatusCounts._id] = {};
forEach(
Object.keys(countryStatusCounts.caseStatus),
(statusName) => {
countriesData[countryStatusCounts._id][statusName] =
countryStatusCounts.caseStatus[statusName];
},
);
countriesData[countryStatusCounts._id].total =
countryStatusCounts.total;
});

// Get cardinality of outcomes by country
const countriesOutcomeCounts = await Day0Case.aggregate([
{
$match: {
'events.outcome': {
$exists: true,
$ne: null,
},
},
},
{
$group: {
_id: {
country: '$location.country',
outcome: '$events.outcome',
},
totalCount: { $sum: 1 },
},
},
{
$group: {
_id: '$_id.country',
outcome: {
$push: {
$cond: [
{ $not: ['$_id.outcome'] },
null,
{
k: '$_id.outcome',
v: '$totalCount',
},
],
},
},
},
},
{
$addFields: {
outcome: {
$filter: {
input: '$outcome',
as: 'd',
cond: {
$ne: ['$$d', null],
},
},
},
},
},
{
$project: {
outcome: { $arrayToObject: '$outcome' },
totalCount: 1,
},
},
]);

if (countriesOutcomeCounts.length > 0) {
forEach(countriesOutcomeCounts, (countryOutcomeCounts) => {
if (countryOutcomeCounts?.outcome) {
forEach(
Object.keys(countryOutcomeCounts.outcome),
(outcomeName) => {
countriesData[countryOutcomeCounts._id][
outcomeName
] = countryOutcomeCounts.outcome[outcomeName];
},
);
}
});
}

// Get cardinality of case statuses total
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const totalStatusCount: any = await Day0Case.aggregate([
{
$group: {
_id: '$caseStatus',
count: {
$sum: 1,
},
},
},
{
$group: {
_id: null,
root: { $push: { k: '$_id', v: '$count' } },
},
},
{
$replaceRoot: { newRoot: { $arrayToObject: '$root' } },
},
]);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const totalCounts: any = {};
forEach(Object.keys(totalStatusCount[0]), (statusName) => {
totalCounts[statusName] = totalStatusCount[0][statusName];
});

// Get cardinality of outcomes total
const totalOutcomeCount = await Day0Case.aggregate([
{
$match: {
'events.outcome': {
$exists: true,
$ne: null,
},
},
},
{
$group: {
_id: '$events.outcome',
count: {
$sum: 1,
},
},
},
{
$group: {
_id: null,
root: { $push: { k: '$_id', v: '$count' } },
},
},
{
$replaceRoot: { newRoot: { $arrayToObject: '$root' } },
},
]);

if (totalOutcomeCount.length > 0) {
forEach(Object.keys(totalOutcomeCount[0]), (outcomeName) => {
totalCounts[outcomeName] =
totalOutcomeCount[0][outcomeName];
});
}

res.status(200).json({
countries: countriesData,
globally: {
total: grandTotalCount,
...totalCounts,
},
});
} catch (e) {
if (e instanceof Error) logger.error(e);
console.log(e);
res.status(500).json(e);
return;
}
};

/**
* Create one or many identical cases.
*
Expand Down
1 change: 1 addition & 0 deletions data-serving/data-service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ if (remoteGeocodingLocation) {
const caseController = new cases.CasesController(geocoders);

apiRouter.get('/cases', caseController.list);
apiRouter.get('/cases/countryData', caseController.countryData);
apiRouter.get('/cases/symptoms', cases.listSymptoms);
apiRouter.get('/cases/placesOfTransmission', cases.listPlacesOfTransmission);
apiRouter.get('/cases/occupations', cases.listOccupations);
Expand Down
Loading

0 comments on commit 2aab131

Please sign in to comment.