Skip to content

Commit

Permalink
Handle sorting of paths data cards without infinite loops
Browse files Browse the repository at this point in the history
  • Loading branch information
terotik committed Oct 29, 2024
1 parent b91b49c commit 6e4168e
Show file tree
Hide file tree
Showing 10 changed files with 1,809 additions and 1,444 deletions.
1,435 changes: 1,151 additions & 284 deletions common/__generated__/graphql.ts

Large diffs are not rendered by default.

909 changes: 187 additions & 722 deletions common/__generated__/paths/graphql.ts

Large diffs are not rendered by default.

223 changes: 144 additions & 79 deletions components/paths/CategoryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import React, { useEffect, useState } from 'react';

import {
Category,
CategoryFragmentFragment,
} from 'common/__generated__/graphql';
import {
ActionNode,
CausalGridNodeFragment,
} from 'common/__generated__/paths/graphql';
import { beautifyValue } from 'common/data/format';
import { Link } from 'common/links';
import ActionParameters from 'components/paths/ActionParameters';
Expand All @@ -12,15 +22,18 @@ import {
import styled, { useTheme } from 'styled-components';

import PopoverTip from '@/components/common/PopoverTip';
import HighlightValue from '@/components/paths/HighlightValue';
import { activeGoalVar, yearRangeVar } from '@/context/paths/cache';
import { GET_NODE_CONTENT } from '@/queries/paths/get-paths-node';
import { getScopeLabel, getScopeTotal } from '@/utils/paths/emissions';
import { DimensionalMetric } from '@/utils/paths/metric';
import { getHttpHeaders } from '@/utils/paths/paths.utils';
import PathsActionNode from '@/utils/paths/PathsActionNode';
import { useQuery, useReactiveVar } from '@apollo/client';
import { NetworkStatus, useQuery, useReactiveVar } from '@apollo/client';

const GroupIdentifierHeader = styled.div`
const GroupIdentifierHeader = styled.div<{
$color?: string | null | undefined;
}>`
background-color: ${(props) => props.$color};
color: ${(props) => readableColor(props.$color || '#ffffff')};
padding: 6px;
Expand Down Expand Up @@ -106,97 +119,138 @@ const IndicatorSparkline = (props) => {
</IndicatorSparklineContainer>
);
};
const PathsBasicNodeContent = (props) => {
const { categoryId, node, pathsInstance } = props;
const yearRange = useReactiveVar(yearRangeVar);
const activeGoal = useReactiveVar(activeGoalVar);
// const t = useTranslations();

if (node.metricDim) {
const nodeMetric = new DimensionalMetric(node.metricDim!);
type PathsBasicNodeContentProps = {
categoryId: string;
node: CausalGridNodeFragment;
onLoaded: (id: string, impact: number) => void;
};

const indirectEmissions = getScopeTotal(
nodeMetric,
'indirect',
yearRange[1]
);
const directEmissions = getScopeTotal(nodeMetric, 'direct', yearRange[1]);
type Emissions = {
total: { value: number | null; label: string | null };
indirect: { value: number | null; label: string | null };
direct: { value: number | null; label: string | null };
};

const indirectEmissionsLabel = getScopeLabel(nodeMetric, 'indirect');
const directEmissionsLabel = getScopeLabel(nodeMetric, 'direct');
const PathsBasicNodeContent = (props: PathsBasicNodeContentProps) => {
const { categoryId, node, onLoaded } = props;
const yearRange = useReactiveVar(yearRangeVar);

/*
console.log('default config', defaultConfig);
console.log('metric', nodeMetric);
console.log('this year', thisYear);
*/
// TODO: Just get any label for now
const [emissions, setEmissions] = useState<Emissions>({
total: { value: null, label: null },
indirect: { value: null, label: null },
direct: { value: null, label: null },
});

const unit = nodeMetric.getUnit();
const [unit, setUnit] = useState<string | null>(null);

return (
<CardContentBlock>
<div>
{directEmissions || indirectEmissions ? (
<div>
<h5>
{nodeMetric.getName()} ({yearRange[1]})
</h5>
<h4>
{(directEmissions + indirectEmissions).toPrecision(3)} {unit}
</h4>
</div>
) : null}
{directEmissions ? (
<div>
{directEmissionsLabel}
<h5>
{directEmissions && directEmissions.toPrecision(3)} {unit}
</h5>
</div>
) : (
<div />
)}
{indirectEmissions ? (
<div>
{indirectEmissionsLabel}
<h5>
{indirectEmissions.toPrecision(3)} {unit}
</h5>
</div>
) : (
<div />
)}
</div>
</CardContentBlock>
);
} else {
return <div>{node.__typename} not supported</div>;
}
useEffect(() => {
const nodeMetric = new DimensionalMetric(node.metricDim!);
const indirect = getScopeTotal(nodeMetric, 'indirect', yearRange[1]);
const direct = getScopeTotal(nodeMetric, 'direct', yearRange[1]);
setEmissions({
total: { value: indirect + direct, label: nodeMetric.getName() },
indirect: {
value: indirect,
label: getScopeLabel(nodeMetric, 'indirect'),
},
direct: { value: direct, label: getScopeLabel(nodeMetric, 'direct') },
});
setUnit(nodeMetric.getUnit());
onLoaded(categoryId, indirect + direct);
// using exhausive deps here causes an infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [yearRange[1]]);

return (
<CardContentBlock>
<div>
{emissions.total.value ? (
<div>
<HighlightValue
displayValue={emissions.total.value.toPrecision(3) || ''}
header={`${emissions.total.label} (${yearRange[1]})`}
unit={unit || ''}
size="md"
/>
</div>
) : null}
{emissions.direct.value ? (
<div>
<HighlightValue
displayValue={emissions.direct.value.toPrecision(3)}
header={emissions.direct.label || ''}
unit={unit || ''}
size="sm"
/>
</div>
) : (
<div />
)}
{emissions.indirect.value ? (
<div>
<HighlightValue
displayValue={emissions.indirect.value.toPrecision(3)}
header={emissions.direct.label || ''}
unit={unit || ''}
size="sm"
/>
</div>
) : (
<div />
)}
</div>
</CardContentBlock>
);
};

const PathsActionNodeContent = (props) => {
const { categoryId, node, pathsInstance, onLoaded } = props;
const yearRange = useReactiveVar(yearRangeVar);
type PathsActionNodeContentProps = {
categoryId: string;
node: ActionNode;
refetching: boolean;
onLoaded: (id: string, impact: number) => void;
};

const PathsActionNodeContent = (props: PathsActionNodeContentProps) => {
const { categoryId, node, refetching = false, onLoaded } = props;
const t = useTranslations();

const yearRange = useReactiveVar(yearRangeVar);
const pathsAction = new PathsActionNode(node);
const impact = pathsAction.getYearlyImpact(yearRange[1]) || 0;

useEffect(() => {
onLoaded(categoryId, impact);
// Using exhaustive deps here causes an infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [yearRange[1]]);

return (
<CardContentBlock>
{t('impact')} {yearRange[1]}
<h4>
{yearRange ? beautifyValue(impact) : <span>---</span>}
{pathsAction.getUnit()}
</h4>
<HighlightValue
displayValue={pathsAction.isEnabled() ? beautifyValue(impact) : '-'}
header={`${t('impact')} ${yearRange[1]}`}
unit={pathsAction.getUnit() || ''}
size="md"
muted={refetching || !pathsAction.isEnabled()}
mutedReason={!pathsAction.isEnabled() ? 'Not included in scenario' : ''}
/>
<ActionParameters parameters={node.parameters} />
</CardContentBlock>
);
};

const PathsNodeContent = (props) => {
type PathsNodeContentProps = {
categoryId: string;
node: string;
paths: string;
onLoaded: (id: string, impact: number) => void;
};

const PathsNodeContent = React.memo((props: PathsNodeContentProps) => {
const { categoryId, node, paths, onLoaded } = props;
const activeGoal = useReactiveVar(activeGoalVar);

const { data, loading, error, networkStatus } = useQuery(GET_NODE_CONTENT, {
fetchPolicy: 'no-cache',
variables: { node: node, goal: activeGoal?.id },
Expand All @@ -207,7 +261,9 @@ const PathsNodeContent = (props) => {
},
});

if (loading) {
const refetching = networkStatus === NetworkStatus.refetch;

if (loading && !refetching) {
return <PathsContentLoader />;
}
if (error) {
Expand All @@ -219,26 +275,35 @@ const PathsNodeContent = (props) => {
<PathsActionNodeContent
categoryId={categoryId}
node={data.node}
pathsInstance={paths}
onLoaded={onLoaded}
refetching={refetching}
/>
);
} else if (data.node.__typename) {
return (
<PathsBasicNodeContent
categoryId={categoryId}
node={data.node}
pathsInstance={paths}
onLoaded={onLoaded}
/>
);
}
return null;
}
});

PathsNodeContent.displayName = 'PathsNodeContent';

type CategoryCardProps = {
category: Category;
group?: CategoryFragmentFragment;
pathsInstance?: string;
onLoaded: (id: string, impact: number) => void;
};

const CategoryCard = (props) => {
const CategoryCard = (props: CategoryCardProps) => {
const { category, group, pathsInstance, onLoaded } = props;

return (
<Card>
{group && (
Expand All @@ -248,7 +313,7 @@ const CategoryCard = (props) => {
)}
<div>
{' '}
<Link href={category.categoryPage.urlPath} legacyBehavior>
<Link href={category?.categoryPage?.urlPath || ''} legacyBehavior>
<a className="card-wrapper">
<CardHeader className="card-title">
{!category?.type.hideCategoryIdentifiers && (
Expand All @@ -261,15 +326,15 @@ const CategoryCard = (props) => {
{category.leadParagraph && (
<CardContentBlock>{category.leadParagraph}</CardContentBlock>
)}
{category.kausalPathsNodeUuid && (
{category.kausalPathsNodeUuid && pathsInstance && (
<PathsNodeContent
categoryId={category.id}
node={category.kausalPathsNodeUuid}
paths={pathsInstance}
onLoaded={onLoaded}
/>
)}
{category.indicators.length > 0 && (
{category.indicators?.length > 0 && (
<CardContentBlock>
<IndicatorSparkline indicator={category.indicators[0]} />
</CardContentBlock>
Expand Down
Loading

0 comments on commit 6e4168e

Please sign in to comment.