Skip to content

Commit

Permalink
Generate comparison reports
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Dec 20, 2023
1 parent acd0f81 commit 530e467
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 17 deletions.
69 changes: 68 additions & 1 deletion api/src/modules/impact/impact-report.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor';
import { ImpactReportService } from 'modules/impact/reports/impact.report';
import { Response } from 'express';
import { GetImpactTableDto } from 'modules/impact/dto/impact-table.dto';
import {
GetActualVsScenarioImpactTableDto,
GetImpactTableDto,
GetScenarioVsScenarioImpactTableDto,
} from 'modules/impact/dto/impact-table.dto';
import { CheckUserOwnsScenario } from 'modules/authorization/formodule/scenario-ownership.interceptor';

@Controller('/api/v1/impact')
@ApiTags('Impact')
Expand Down Expand Up @@ -47,4 +52,66 @@ export class ImpactReportController {
);
res.send(report);
}

@ApiOperation({
description:
'Get a Actual Vs Scenario Impact Table CSV Report for a given scenario',
})
@CheckUserOwnsScenario({
bypassIfScenarioIsPublic: true,
isComparisonMode: true,
})
@UseInterceptors(SetScenarioIdsInterceptor)
@Get('compare/scenario/vs/actual/report')
async getActualVsScenarioImpactTable(
@Query(ValidationPipe)
actualVsScenarioImpactTableDto: GetActualVsScenarioImpactTableDto,
@Res() res: Response,
): Promise<void> {
const { data } =
await this.actualVsScenarioImpactService.getActualVsScenarioImpactTable(
actualVsScenarioImpactTableDto,
{ disablePagination: true },
);
const report: string = await this.impactReports.generateImpactReport(
data.impactTable,
);
res.setHeader('Content-Type', 'text/csv');
res.setHeader(
'Content-Disposition',
'attachment; filename=impact_report.csv',
);
res.send(report);
}

@ApiOperation({
description:
'Get a Scenario Vs Scenario Impact Table CSV Report for 2 Scenarios',
})
@CheckUserOwnsScenario({
bypassIfScenarioIsPublic: true,
isComparisonMode: true,
})
@UseInterceptors(SetScenarioIdsInterceptor)
@Get('compare/scenario/vs/scenario/report')
async getTwoScenariosImpactTable(
@Query(ValidationPipe)
scenarioVsScenarioImpactTableDto: GetScenarioVsScenarioImpactTableDto,
@Res() res: Response,
): Promise<void> {
const { data } =
await this.scenarioVsScenarioService.getScenarioVsScenarioImpactTable(
scenarioVsScenarioImpactTableDto,
{ disablePagination: true },
);
const report: string = await this.impactReports.generateImpactReport(
data.impactTable,
);
res.setHeader('Content-Type', 'text/csv');
res.setHeader(
'Content-Disposition',
'attachment; filename=impact_report.csv',
);
res.send(report);
}
}
66 changes: 50 additions & 16 deletions api/src/modules/impact/reports/impact.report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,33 @@ import { Inject, Injectable } from '@nestjs/common';
import { ICSVReportService } from 'modules/reports/report-service.interface';
import { ReportServiceToken } from 'modules/reports/reports.module';
import {
AnyImpactTableRows,
AnyImpactTableRowsValues,
ImpactTableDataByIndicator,
ImpactTableRows,
ImpactTableRowsValues,
} from 'modules/impact/dto/response-impact-table.dto';
import { ImpactTableCSVReport } from 'modules/impact/reports/types';
import { ParserOptions } from '@json2csv/plainjs';
import { GROUP_BY_VALUES } from 'modules/impact/dto/impact-table.dto';
import { ScenarioVsScenarioImpactTableDataByIndicator } from 'modules/impact/dto/response-scenario-scenario.dto';
import { ActualVsScenarioImpactTableDataByIndicator } from 'modules/impact/dto/response-actual-scenario.dto';

const IndicatorColumnKey: string = 'Indicator';
const GroupByColumnKey: string = 'Group by';
const AbsoluteDifferenceColumnKey: string = 'Absolute Difference';
const PercentageDifferenceColumnKey: string = 'Percentage Difference';

type ImpactTableToCSVInput =
| ImpactTableDataByIndicator
| ScenarioVsScenarioImpactTableDataByIndicator
| ActualVsScenarioImpactTableDataByIndicator;

@Injectable()
export class ImpactReportService {
constructor(
@Inject(ReportServiceToken) private reportService: ICSVReportService,
) {}

async generateImpactReport(
data: ImpactTableDataByIndicator[],
): Promise<string> {
async generateImpactReport(data: ImpactTableToCSVInput[]): Promise<string> {
const parsedData: ImpactTableCSVReport[] = this.parseToCSVShape(data);
const columnOrder: Pick<ParserOptions, 'fields'> = {
fields: this.getColumnOrder(parsedData[0]),
Expand All @@ -28,13 +38,13 @@ export class ImpactReportService {
}

private parseToCSVShape(
data: ImpactTableDataByIndicator[],
data: ImpactTableToCSVInput[],
): ImpactTableCSVReport[] {
const results: ImpactTableCSVReport[] = [];

data.forEach((indicator: ImpactTableDataByIndicator) => {
data.forEach((indicator: ImpactTableToCSVInput) => {
const unit: string = indicator.metadata.unit;
indicator.rows.forEach((row: ImpactTableRows) => {
indicator.rows.forEach((row: AnyImpactTableRows) => {
this.processNode(
row,
indicator.indicatorShortName,
Expand All @@ -49,30 +59,49 @@ export class ImpactReportService {
}

private processNode(
node: ImpactTableRows,
node: AnyImpactTableRows,
indicatorName:
| Pick<ImpactTableDataByIndicator, 'indicatorShortName'>
| string,
groupBy: GROUP_BY_VALUES,
unit: string,
accumulator: ImpactTableCSVReport[],
): void {
const groupName: string = `Group by ${groupBy}`;
const groupName: string = `${GroupByColumnKey} ${groupBy}`;
const resultObject: ImpactTableCSVReport = {
[`Indicator`]: `${indicatorName} (${unit})`,
[groupName]: node.name,
};
node.values.forEach((nodeValue: ImpactTableRowsValues) => {
node.values.forEach((nodeValue: AnyImpactTableRowsValues) => {
let yearKey: string = nodeValue.year.toString();
if (nodeValue.isProjected) {
yearKey = yearKey.concat(' (projected)');
}
resultObject[yearKey] = nodeValue.value;
if ('baseScenarioValue' in nodeValue) {
resultObject[`${yearKey} (base Scenario)`] =
nodeValue.baseScenarioValue;
resultObject[`${yearKey} (compared Scenario)`] =
nodeValue.comparedScenarioValue;
} else if ('comparedScenarioValue' in nodeValue) {
resultObject[yearKey] = nodeValue.value;
resultObject[`${yearKey} (compared Scenario)`] =
nodeValue.comparedScenarioValue;
} else {
resultObject[yearKey] = nodeValue.value;
}
if ('absoluteDifference' in nodeValue) {
resultObject[AbsoluteDifferenceColumnKey] =
nodeValue.absoluteDifference;
}
if ('percentageDifference' in nodeValue) {
resultObject[PercentageDifferenceColumnKey] =
nodeValue.percentageDifference;
}
});
accumulator.push(resultObject);

if (node.children && node.children.length) {
node.children.forEach((children: ImpactTableRows) => {
node.children.forEach((children: AnyImpactTableRows) => {
this.processNode(children, indicatorName, groupBy, unit, accumulator);
});
}
Expand All @@ -82,15 +111,20 @@ export class ImpactReportService {
const indicatorKey: string[] = [];
const groupByKey: string[] = [];
const yearKeys: string[] = [];
const differenceKeys: string[] = [];
Object.keys(dataSample).forEach((key: string) => {
if (key.includes('Indicator')) {
if (key.includes(IndicatorColumnKey)) {
groupByKey.push(key);
} else if (key.includes('Group by')) {
} else if (key.includes(GroupByColumnKey)) {
groupByKey.push(key);
} else if (key.includes(AbsoluteDifferenceColumnKey)) {
differenceKeys.push(key);
} else if (key.includes(PercentageDifferenceColumnKey)) {
differenceKeys.push(key);
} else {
yearKeys.push(key);
}
});
return [...indicatorKey, ...groupByKey, ...yearKeys];
return [...indicatorKey, ...groupByKey, ...yearKeys, ...differenceKeys];
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Response DTO example for a new coefficients type of scenario intervention

export const newCoefficientsScenarioInterventionTable = {
impactTable: [
{
Expand Down

0 comments on commit 530e467

Please sign in to comment.