Skip to content

Commit

Permalink
Downloadable table
Browse files Browse the repository at this point in the history
  • Loading branch information
terotik committed Dec 17, 2024
1 parent 00404a7 commit 9be7e47
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 174 deletions.
300 changes: 190 additions & 110 deletions components/paths/graphs/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
import { useFormatter, useTranslations } from 'next-intl';
import { Table } from 'reactstrap';
import Styled from 'styled-components';
import {
DropdownItem,
DropdownMenu,
DropdownToggle,
UncontrolledDropdown,
Table,
} from 'reactstrap';
import Icon from '@/components/common/Icon';
import styled from 'styled-components';

import type { OutcomeNodeFieldsFragment } from '@/common/__generated__/paths/graphql';
import type {
DimensionalNodeMetricFragment,
OutcomeNodeFieldsFragment,
} from '@/common/__generated__/paths/graphql';
import {
DimensionalMetric,
MetricSlice,
SliceConfig,
} from '@/utils/paths/metric';
import { useReactiveVar } from '@apollo/client';
import { useEffect, useMemo, useState } from 'react';
import { isEqual } from 'lodash';
import { activeGoalVar, activeScenarioVar } from '@/context/paths/cache';

// import { useFeatures } from '@/common/instance';
interface DataTableProps {
node: OutcomeNodeFieldsFragment;
subNodes?: Node[];
startYear: number;
endYear: number;
separateYears?: number[] | null;
goalName?: string;
disclaimer?: string;
}

const TableWrapper = Styled.div`
const Tools = styled.div`
padding: 0 1rem 0.5rem;
text-align: right;
.btn-link {
text-decoration: none;
}
.icon {
width: 1.25rem !important;
height: 1.25rem !important;
vertical-align: -0.2rem;
}
`;

const TableWrapper = styled.div`
margin: 0 auto;
max-width: 100%;
overflow-x: auto;
Expand All @@ -28,141 +51,198 @@ const TableWrapper = Styled.div`
font-size: 70%;
`;

interface DataTableProps {
metric: NonNullable<DimensionalNodeMetricFragment['metricDim']>;
startYear: number;
endYear: number;
separateYears?: number[] | null;
goalName?: string;
disclaimer?: string;
}

const DataTable = (props: DataTableProps) => {
const {
node,
subNodes,
startYear,
endYear,
separateYears,
goalName,
disclaimer,
} = props;
const { metric, startYear, endYear, separateYears, goalName, disclaimer } =
props;

const t = useTranslations();
const format = useFormatter();
const totalHistoricalValues = node.metric.historicalValues.filter((value) =>
separateYears
? separateYears.includes(value.year)
: value.year >= startYear && value.year <= endYear
const activeGoal = useReactiveVar(activeGoalVar);
const activeScenario = useReactiveVar(activeScenarioVar);

const cube = useMemo(() => new DimensionalMetric(metric), [metric]);

const lastMetricYear = metric.years.slice(-1)[0];
const usableEndYear =
lastMetricYear && endYear > lastMetricYear ? lastMetricYear : endYear;

const defaultConfig = cube.getDefaultSliceConfig(activeGoal);
const [sliceConfig, setSliceConfig] = useState<SliceConfig>(defaultConfig);

useEffect(() => {
/**
* If the active goal changes, we will reset the grouping + filtering
* to be compatible with the new choices (if the new goal has common
* dimensions with our metric).
*/
if (!activeGoal) return;
const newDefault = cube.getDefaultSliceConfig(activeGoal);
if (!newDefault || isEqual(sliceConfig, newDefault)) return;
setSliceConfig(newDefault);
}, [activeGoal, cube, sliceConfig]);

const sliceableDims = cube.dimensions.filter(
(dim) => !sliceConfig.categories[dim.id]
);
const totalForecastValues = node.metric.forecastValues.filter((value) =>
separateYears
? separateYears.includes(value.year)
: value.year >= startYear && value.year <= endYear
const slicedDim = cube.dimensions.find(
(dim) => dim.id === sliceConfig.dimensionId
);
//const maximumFractionDigits = useFeatures().maximumFractionDigits ?? undefined;
const maximumFractionDigits = 3;

const hasTotalValues =
totalHistoricalValues.some((val) => val.value !== null) ||
totalForecastValues.some((val) => val.value !== null);

// Add this function to check if a subNode has any data
const hasData = (subNode) => {
const hasHistoricalData = subNode.metric.historicalValues
.filter((value) =>
separateYears
? separateYears.includes(value.year)
: value.year >= startYear && value.year <= endYear
)
.some((value) => value.value !== null);

const hasForecastData = subNode.metric.forecastValues
.filter((value) =>
separateYears
? separateYears.includes(value.year)
: value.year >= startYear && value.year <= endYear
)
.some((value) => value.value !== null);

return hasHistoricalData || hasForecastData;
};

// Filter subNodes to only include those with data
const validSubNodes = subNodes?.filter(hasData);

const years =
separateYears ??
Array.from(
{ length: usableEndYear - startYear + 1 },
(_, i) => startYear + i
);

let slice: MetricSlice;
if (slicedDim) {
slice = cube.sliceBy(
slicedDim.id,
true,
sliceConfig.categories,
undefined,
years
);
} else {
slice = cube.flatten(sliceConfig.categories, years);
}

const forecastLabel =
activeScenario && !separateYears
? `${t('table-scenario-forecast')} (${activeScenario.name})`
: t('table-scenario-forecast');

const tableTitle = `${metric.name}, ${goalName}`;

return (
<TableWrapper>
<h5>
{node.name}, {goalName}
{separateYears ? '' : ` (${startYear} - ${endYear})`}
</h5>
<h5 className="my-4">{tableTitle}</h5>
<Table bordered size="sm" responsive>
{disclaimer && <caption>{disclaimer}</caption>}
<thead>
<tr>
<th>{t('table-year')}</th>
<th>{t('table-measure-type')}</th>
{validSubNodes?.map((subNode) => (
<th key={subNode.id}>{subNode.name}</th>
{slice.categoryValues.map((cat) => (
<th key={cat.category.id}>{cat.category.label}</th>
))}
{hasTotalValues && <th>{node.metric.name}</th>}
<th>{t('plot-total')}</th>
<th>{t('table-unit')}</th>
</tr>
</thead>
<tbody>
{totalHistoricalValues.map((metric) => (
<tr key={`h-${metric.year}`}>
<td>{metric.year}</td>
{slice.historicalYears.map((year, idx) => (
<tr key={`h-${year}`}>
<td>{year}</td>
<td>{t('table-historical')}</td>
{validSubNodes?.map((subNode) => (
<td key={`${subNode.id}-${metric.year}`}>
{subNode.metric.historicalValues.find(
(value) => value.year === metric.year
)
? format.number(
subNode.metric.historicalValues.find(
(value) => value.year === metric.year
).value,
{ maximumSignificantDigits: 2 }
)
: '-'}
{slice.categoryValues.map((cat) => (
<td key={`${cat.category.id}-h-${year}`}>
{cat.historicalValues[idx]
? format.number(cat.historicalValues[idx], {
maximumSignificantDigits: 2,
})
: ''}
</td>
))}
{hasTotalValues && (
<td>
{format.number(metric.value, { maximumSignificantDigits: 2 })}
</td>
)}
<td>
{slice.totalValues?.historicalValues[idx]
? format.number(slice.totalValues?.historicalValues[idx], {
maximumSignificantDigits: 2,
})
: ''}
</td>
<td
dangerouslySetInnerHTML={{
__html: node.metric?.unit?.htmlShort,
__html: slice.unit,
}}
/>
</tr>
))}
{totalForecastValues.map((metric) => (
<tr key={`f-${metric.year}`}>
<td>{metric.year}</td>
<td>{t('table-scenario-forecast')}</td>
{validSubNodes?.map((subNode) => (
<td key={`${subNode.id}-${metric.year}`}>
{subNode.metric.forecastValues.find(
(value) => value.year === metric.year
)
? format.number(
subNode.metric.forecastValues.find(
(value) => value.year === metric.year
).value,
{ maximumSignificantDigits: 2 }
)
: '-'}
{slice.forecastYears.map((year, idx) => (
<tr key={`h-${year}`}>
<td>{year}</td>
<td>{forecastLabel}</td>
{slice.categoryValues.map((cat) => (
<td key={`${cat.category.id}-f-${year}`}>
{cat.forecastValues[idx]
? format.number(cat.forecastValues[idx], {
maximumSignificantDigits: 2,
})
: ''}
</td>
))}
{hasTotalValues && (
<td>
{format.number(metric.value, { maximumSignificantDigits: 2 })}
</td>
)}
<td>
{slice.totalValues?.forecastValues[idx]
? format.number(slice.totalValues?.forecastValues[idx], {
maximumSignificantDigits: 2,
})
: ''}
</td>
<td
dangerouslySetInnerHTML={{
__html: node.metric?.unit?.htmlShort,
__html: slice.unit,
}}
/>
</tr>
))}
</tbody>
</Table>
<Tools>
<UncontrolledDropdown size="sm">
<DropdownToggle caret color="link">
<Icon name="download" />
{` ${t('download-data')}`}
</DropdownToggle>
<DropdownMenu>
<DropdownItem
onClick={async (ev) =>
await cube.downloadData(
sliceConfig,
'xlsx',
years,
tableTitle,
t('plot-total'),
t('table-measure-type'),
t('table-year'),
t('table-unit'),
t('table-historical'),
forecastLabel
)
}
>
<Icon name="file" /> XLS
</DropdownItem>
<DropdownItem
onClick={async (ev) =>
await cube.downloadData(
sliceConfig,
'csv',
years,
tableTitle,
t('plot-total'),
t('table-measure-type'),
t('table-year'),
t('table-unit'),
t('table-historical'),
forecastLabel
)
}
>
<Icon name="file" /> CSV
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
</Tools>
</TableWrapper>
);
};
Expand Down
4 changes: 2 additions & 2 deletions components/paths/outcome/OutcomeNodeContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -361,10 +361,10 @@ const OutcomeNodeContent = ({
<OutcomeNodeDetails node={node} t={t} />
</ContentWrapper>
)}
{activeTabId === 'table' && (
{activeTabId === 'table' && node.metricDim && (
<ContentWrapper tabIndex={0}>
<DataTable
node={node}
metric={node.metricDim}
goalName={activeGoal?.label}
separateYears={separateYears}
subNodes={subNodes}
Expand Down
Loading

0 comments on commit 9be7e47

Please sign in to comment.