Skip to content
Snippets Groups Projects
  • Sreetam Das's avatar
    d032fce7
    fix: number format config not being respected for graphs (#2832) · d032fce7
    Sreetam Das authored
    * Add computed padding for handling clipped Net worth amounts
    
    * Add comment, early handle 5 character case
    
    * Add release note
    
    * Use currency decimal preference for Net Worth graph Y-axis ticks
    
    * use number format preference for `BarLineGraph` ticks
    
    * use number format preference for checking `NetWorthGraph` tick overflow
    
    * prevent `numberFormatConfig` overwrite
    
    `getNumberFormat` uses `numberFormatConfig` as the default argument;
    passing just `{ hideFraction: true }` caused `numberFormatConfig.format`
    to be ignored
    
    * add release note
    
    * use `amountToCurrencyNoDecimal` instead for `{BarLine,NetWorth}Graph` ticks
    fix: number format config not being respected for graphs (#2832)
    Sreetam Das authored
    * Add computed padding for handling clipped Net worth amounts
    
    * Add comment, early handle 5 character case
    
    * Add release note
    
    * Use currency decimal preference for Net Worth graph Y-axis ticks
    
    * use number format preference for `BarLineGraph` ticks
    
    * use number format preference for checking `NetWorthGraph` tick overflow
    
    * prevent `numberFormatConfig` overwrite
    
    `getNumberFormat` uses `numberFormatConfig` as the default argument;
    passing just `{ hideFraction: true }` caused `numberFormatConfig.format`
    to be ignored
    
    * add release note
    
    * use `amountToCurrencyNoDecimal` instead for `{BarLine,NetWorth}Graph` ticks
NetWorthGraph.tsx 5.83 KiB
// @ts-strict-ignore
import React from 'react';

import { css } from 'glamor';
import {
  AreaChart,
  Area,
  CartesianGrid,
  XAxis,
  YAxis,
  Tooltip,
  ResponsiveContainer,
} from 'recharts';

import { amountToCurrencyNoDecimal } from 'loot-core/shared/util';

import { usePrivacyMode } from '../../../hooks/usePrivacyMode';
import { useResponsive } from '../../../ResponsiveProvider';
import { theme } from '../../../style';
import { type CSSProperties } from '../../../style';
import { AlignedText } from '../../common/AlignedText';
import { Container } from '../Container';
import { numberFormatterTooltip } from '../numberFormatter';

type NetWorthGraphProps = {
  style?: CSSProperties;
  graphData;
  compact: boolean;
};

export function NetWorthGraph({
  style,
  graphData,
  compact,
}: NetWorthGraphProps) {
  const privacyMode = usePrivacyMode();
  const { isNarrowWidth } = useResponsive();

  const tickFormatter = tick => {
    const res = privacyMode
      ? '...'
      : `${amountToCurrencyNoDecimal(Math.round(tick))}`; // Formats the tick values as strings with commas

    return res;
  };

  const gradientOffset = () => {
    const dataMax = Math.max(...graphData.data.map(i => i.y));
    const dataMin = Math.min(...graphData.data.map(i => i.y));

    if (dataMax <= 0) {
      return 0;
    }
    if (dataMin >= 0) {
      return 1;
    }

    return dataMax / (dataMax - dataMin);
  };

  const off = gradientOffset();

  type PayloadItem = {
    payload: {
      date: string;
      assets: number | string;
      debt: number | string;
      networth: number | string;
      change: number | string;
    };
  };

  type CustomTooltipProps = {
    active?: boolean;
    payload?: PayloadItem[];
  };

  // eslint-disable-next-line react/no-unstable-nested-components
  const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
    if (active && payload && payload.length) {
      return (
        <div
          className={`${css(
            {
              zIndex: 1000,
              pointerEvents: 'none',
              borderRadius: 2,
              boxShadow: '0 1px 6px rgba(0, 0, 0, .20)',
              backgroundColor: theme.menuBackground,
              color: theme.menuItemText,
              padding: 10,
            },
            style,
          )}`}
        >
          <div>
            <div style={{ marginBottom: 10 }}>
              <strong>{payload[0].payload.date}</strong>
            </div>
            <div style={{ lineHeight: 1.5 }}>
              <AlignedText left="Assets:" right={payload[0].payload.assets} />
              <AlignedText left="Debt:" right={payload[0].payload.debt} />
              <AlignedText
                left="Net worth:"
                right={<strong>{payload[0].payload.networth}</strong>}
              />
              <AlignedText left="Change:" right={payload[0].payload.change} />
            </div>
          </div>
        </div>
      );
    }
  };

  return (
    <Container
      style={{
        ...style,
        ...(compact && { height: 'auto' }),
      }}
    >
      {(width, height) =>
        graphData && (
          <ResponsiveContainer>
            <div style={{ ...(!compact && { marginTop: '15px' }) }}>
              <AreaChart
                width={width}
                height={height}
                data={graphData.data}
                margin={{
                  top: 0,
                  right: 0,
                  left: compact ? 0 : computePadding(graphData.data),
                  bottom: 0,
                }}
              >
                {compact ? null : (
                  <CartesianGrid strokeDasharray="3 3" vertical={false} />
                )}
                <XAxis
                  dataKey="x"
                  hide={compact}
                  tick={{ fill: theme.pageText }}
                  tickLine={{ stroke: theme.pageText }}
                />
                <YAxis
                  dataKey="y"
                  domain={['auto', 'auto']}
                  hide={compact}
                  tickFormatter={tickFormatter}
                  tick={{ fill: theme.pageText }}
                  tickLine={{ stroke: theme.pageText }}
                />
                {(!isNarrowWidth || !compact) && (
                  <Tooltip
                    content={<CustomTooltip />}
                    formatter={numberFormatterTooltip}
                    isAnimationActive={false}
                  />
                )}
                <defs>
                  <linearGradient id="splitColor" x1="0" y1="0" x2="0" y2="1">
                    <stop
                      offset={off}
                      stopColor={theme.reportsBlue}
                      stopOpacity={0.2}
                    />
                    <stop
                      offset={off}
                      stopColor={theme.reportsRed}
                      stopOpacity={0.2}
                    />
                  </linearGradient>
                </defs>

                <Area
                  type="linear"
                  dot={false}
                  activeDot={false}
                  animationDuration={0}
                  dataKey="y"
                  stroke={theme.reportsBlue}
                  fill="url(#splitColor)"
                  fillOpacity={1}
                />
              </AreaChart>
            </div>
          </ResponsiveContainer>
        )
      }
    </Container>
  );
}

/**
 * Add left padding for Y-axis for when large amounts get clipped
 * @param netWorthData
 * @returns left padding for Net worth graph
 */
function computePadding(netWorthData: Array<{ y: number }>) {
  /**
   * Convert to string notation, get longest string length
   */
  const maxLength = Math.max(
    ...netWorthData.map(({ y }) => {
      return amountToCurrencyNoDecimal(Math.round(y)).length;
    }),
  );

  // No additional left padding is required for upto 5 characters
  return Math.max(0, (maxLength - 5) * 5);
}