Recharts chart takes extra height and breaks grid

34 Views Asked by At

I have a page that should take 100vh and no more. The page has two columns in the left table in the right graph. Graph as well as table should occupy all possible height but not more. Also above the chart there is Select which when selected makes Selects appear above the chart and the height of the chart should be adapted to this and accordingly reduced. But as much as I tried the chart does not take all the available height, it seems to take all the height of the parent and thus moves down and breaks the whole grid and also does not adapt to new elements, that is, when new Selects appear it also just moves down instead of reducing its height.

The best I have achieved is to calculate the height manually with refs, but I think there is a smoother and more adequate solution. I also noticed that the chart can behave adequately if you put display flex on the parent and don't add columns but just put it down, but as soon as you wrap it in div it goes down again.

Image of problem

There is a code of page

/* eslint-disable react-perf/jsx-no-new-object-as-prop */
/* eslint-disable react-perf/jsx-no-new-function-as-prop */
import { useEffect, useMemo, useRef, useState } from 'react';
import Space from 'antd/es/space';
import Select from 'antd/es/select';
import Flex from 'antd/es/flex';
import Row from 'antd/es/row';
import Col from 'antd/es/col';
import { RangePickerProps } from 'antd/es/date-picker';
import { RadioChangeEvent } from 'antd/es/radio';
import Avatar from 'antd/es/avatar';
import Button from 'antd/es/button';
import { TableProps } from 'antd/es/table';
import { PageLayout } from '@layouts/PageLayout';
import { DashboardMetrics, TableCol } from '@pages/DashboardPage';
import { DashboardGraph } from '@pages/DashboardPage/components/DashboardGraph';
import { CurrencySelect } from '@components/Selects';
import { defaultAvatar } from '@constants/placeHolders';
import { Stat } from '@modules/Integrations_new';
import { Dayjs } from 'dayjs';
import { User } from 'types/user';
import { isArray } from 'lodash';
import { Currencies, MyDirectionState } from 'types/list';
import {
  AdvertiserTableItem,
  CampaignTableItem,
  CountryTableItem,
  PublisherTableItem,
} from 'types/dashboard';
import { RangeDateSelect } from '@components/Selects/RangeDateSelect';
import '@assets/sass/dashboardPage.scss';

type Props = {
  cardsDynamic: Stat;
  cardsYesterday: Stat;
  cardsThisMonth: Stat;

  onTableRadioChange: (e: RadioChangeEvent) => void;
  currentTable: string;

  tableData: Stat[];
  isTableLoading: boolean;
  onChange: TableProps<
    | PublisherTableItem
    | AdvertiserTableItem
    | CampaignTableItem
    | CountryTableItem
  >['onChange'];
  sortDirection: MyDirectionState | undefined;
  sortBy: string;

  onDateChange: RangePickerProps['onChange'];
  dates: [Dayjs, Dayjs];

  onCurrencyChange: (value: Currencies) => void;

  onGraphTagChange: (tag: string, checked: boolean) => void;
  selectedGraphTags: string[];
  graphData: Stat[];

  currentCurrency: Currencies;

  isLoading: boolean;
  loggedInUser: User;
};

const filterOptions = [
  { label: 'Publishers', value: 'Publishers' },
  { label: 'Advertisers', value: 'Advertisers' },
  { label: 'Countries', value: 'Countries' },
  { label: 'Campaigns', value: 'Campaigns' },
];

export const DashboardPage = ({
  cardsDynamic,
  cardsYesterday,
  cardsThisMonth,
  tableData,
  isTableLoading,
  onGraphTagChange,
  onTableRadioChange,
  currentTable,
  selectedGraphTags,
  onDateChange,
  isLoading,
  loggedInUser,
  onCurrencyChange,
  graphData,
  currentCurrency,
  onChange,
  sortBy,
  sortDirection,
  dates,
}: Props) => {
  const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
  const [isRangeOpen, setIsRangeOpen] = useState(false);
  const [graphHeight, setGraphHeight] = useState('');

  const controlsRef = useRef<HTMLDivElement>(null);
  const metricsRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const height =
      metricsRef.current && controlsRef.current
        ? `calc(100vh - ${
            metricsRef.current.clientHeight +
            controlsRef.current?.clientHeight +
            116
          }px)`
        : '100%';
    setGraphHeight(height);
  });

  const calculatedStyles: Record<string, React.CSSProperties> = useMemo(
    () => ({
      graph: {
        width: '100%',
        height: graphHeight,
        border: '1px solid lightgray',
        borderRadius: 8,
        padding: 12,
        marginTop: 12,
      },
      tableCol: {
        height: metricsRef.current?.clientHeight
          ? `calc(100vh - ${metricsRef.current.clientHeight + 104}px)`
          : '100%',
      },
    }),
    [graphHeight],
  );

  return (
    <PageLayout style={h100vh} innerStyle={h100}>
      <DashboardMetrics
        ref={metricsRef}
        onGraphTagChange={onGraphTagChange}
        selectedGraphTags={selectedGraphTags}
        currency={currentCurrency}
        profit={{
          dynamic: Number(cardsDynamic.profit),
          month: Number(cardsThisMonth.profit),
          yesterday: Number(cardsYesterday.profit),
        }}
        cost={{
          dynamic: Number(cardsDynamic.cost),
          month: Number(cardsThisMonth.cost),
          yesterday: Number(cardsYesterday.cost),
        }}
        revenue={{
          dynamic: Number(cardsDynamic.revenue),
          month: Number(cardsThisMonth.revenue),
          yesterday: Number(cardsYesterday.revenue),
        }}
        views={{
          dynamic: Number(cardsDynamic.views),
          month: Number(cardsThisMonth.views),
          yesterday: Number(cardsYesterday.views),
        }}
        clicks={{
          dynamic: Number(cardsDynamic.clicks),
          month: Number(cardsThisMonth.clicks),
          yesterday: Number(cardsYesterday.clicks),
        }}
        conversions={{
          dynamic: Number(cardsDynamic.conversions),
          month: Number(cardsThisMonth.conversions),
          yesterday: Number(cardsYesterday.conversions),
        }}
        margin={{
          dynamic: Number(cardsDynamic.margin),
          month: Number(cardsThisMonth.margin),
          yesterday: Number(cardsYesterday.margin),
        }}
      />
      <Row style={row} gutter={24}>
        <Col span={10}>
          <TableCol
            style={calculatedStyles.tableCol}
            currency={currentCurrency}
            currentTable={currentTable}
            isTableLoading={isTableLoading}
            onRadioChange={onTableRadioChange}
            tableData={tableData}
            onChange={onChange}
            sortBy={sortBy}
            sortDirection={sortDirection}
          />
        </Col>
        <Col span={14}>
          <Flex ref={controlsRef} gap="small" vertical={true}>
            <Flex justify="space-between">
              <Flex gap="small" vertical={true}>
                <Space>
                  <CurrencySelect
                    setOptions={(currency) =>
                      !isArray(currency) && onCurrencyChange(currency.value)
                    }
                    disabled={isLoading}
                    defaultValue="USD"
                  />
                  <RangeDateSelect
                    disabled={isLoading}
                    isRangeOpen={isRangeOpen}
                    onChange={onDateChange}
                    setIsRangeOpen={setIsRangeOpen}
                    value={dates}
                    defaultSelectValue="Last 7 days"
                  />
                  <Button disabled={true || isLoading} type="link">
                    Reset
                  </Button>
                </Space>
                <Select<string[]>
                  showSearch={false}
                  value={selectedFilters}
                  mode="multiple"
                  options={filterOptions}
                  onChange={(value) => setSelectedFilters(value)}
                  popupMatchSelectWidth={false}
                  disabled={isLoading}
                  style={filterSelect}
                  placeholder="Filters"
                />
              </Flex>
              {true && (
                <Space style={avatarText}>
                  <span>
                    your manager
                    <br />
                    {'loggedInUser.manager.UR_NAME'}
                  </span>
                  <Avatar
                    size={44}
                    src={loggedInUser.manager?.UR_AVATAR || defaultAvatar}
                  />
                </Space>
              )}
            </Flex>
            <Space>
              {selectedFilters.map((filter) => (
                <Select
                  disabled={true}
                  showSearch={false}
                  placeholder={filter}
                  options={filterOptions}
                  mode="multiple"
                  maxTagCount={1}
                  style={filterSelectOption}
                />
              ))}
            </Space>
          </Flex>
          <div style={calculatedStyles.graph}>
            <DashboardGraph
              currency={currentCurrency}
              isEmptyData={!selectedGraphTags.length}
              data={graphData as Required<Stat>[]}
            />
          </div>
        </Col>
      </Row>
    </PageLayout>
  );
};

const h100vh: React.CSSProperties = {
  height: '100vh',
};

const h100: React.CSSProperties = {
  height: '100%',
};

const row: React.CSSProperties = {
  marginTop: 24,
};

const filterSelect: React.CSSProperties = {
  minWidth: 90,
};

const filterSelectOption: React.CSSProperties = {
  width: 120,
};

const avatarText: React.CSSProperties = {
  textAlign: 'end',
};

and Graph

/* eslint-disable react-perf/jsx-no-new-function-as-prop */
import { useMemo } from 'react';
import Flex from 'antd/es/flex';
import Empty from 'antd/es/empty';
import {
  ComposedChart,
  Line,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
} from 'recharts';
import { MaverickTableIndicator } from '@components/MaverickSpin';
import { Colors } from '@constants/colors';
import { Stat } from '@modules/Integrations_new';
import {
  numberToEur,
  numberToUsd,
  numberToEurWithoutZeros,
  numberToUsdWithoutZeros,
} from '@utils/currencies';
import { Currencies } from 'types/list';

type Props = {
  data: Required<Stat>[];
  currency: Currencies;
  isEmptyData: boolean;
};

const margin = {
  top: 10,
  right: 0,
  bottom: 0,
  left: 15,
};

const viewsClicksLabel = {
  value: 'views/clicks',
  angle: -90,
  position: 'insideLeft',
};

const conversionsLabel = {
  value: 'conversions',
  angle: 90,
  position: 'insideRight',
};

const tickFormatter = (value: number, currency: Currencies) =>
  currency === 'USD'
    ? numberToUsdWithoutZeros(parseInt(String(value), 10))
    : numberToEurWithoutZeros(parseInt(String(value), 10));

const tooltipFormatter = (
  value: number,
  name: string,
  currency: Currencies,
) => {
  if (['cost', 'revenue', 'profit'].includes(name)) {
    return currency === 'USD' ? numberToUsd(value) : numberToEur(value);
  }
  return name === '' ? '' : value;
};

const percentage = (value: number, percentageValue: number) =>
  value * (percentageValue / 100);

export const DashboardGraph = ({ data, isEmptyData, currency }: Props) => {
  const lowestValue = Math.min(
    ...data.map((entry) => entry.profit),
    ...data.map((entry) => entry['revenue']),
    ...data.map((entry) => entry['cost']),
  );
  const biggestValue = Math.max(
    ...data.map((entry) => entry.profit),
    ...data.map((entry) => entry['revenue']),
    ...data.map((entry) => entry['cost']),
  );

  const domain = useMemo(
    () => [
      `dataMin - ${-Math.floor(percentage(lowestValue, 50))}`,
      `dataMax + ${Math.floor(percentage(biggestValue, 10))}`,
    ],
    [biggestValue, lowestValue],
  );

  if (isEmptyData) {
    return (
      <Flex justify="center" align="center" style={h100}>
        <Empty />
      </Flex>
    );
  }
  if (!data || !data.length) {
    return <MaverickTableIndicator size={50} />;
  }

  return (
    <ResponsiveContainer width="100%" height="100%">
      <ComposedChart data={data} maxBarSize={50} margin={margin}>
        <CartesianGrid stroke="#f5f5f5" />
        <XAxis angle={-20} dataKey="date" xAxisId={0} />
        <XAxis dataKey="date" xAxisId={1} hide={true} />
        <XAxis dataKey="date" xAxisId={2} hide={true} />
        <YAxis
          domain={domain}
          tickFormatter={(value) => tickFormatter(value, currency)}
          type="number"
          yAxisId={0}
          width={70}
        />
        <YAxis yAxisId={1} label={viewsClicksLabel} />
        <YAxis yAxisId={2} orientation="right" label={conversionsLabel} />
        <Tooltip<number, string>
          formatter={(value, name) => tooltipFormatter(value, name, currency)}
        />
        <Bar
          dataKey="cost"
          fill={Colors.error}
          stroke="red"
          strokeWidth={2}
          opacity={0.4}
          xAxisId={1}
          yAxisId={0}
        />
        <Bar
          dataKey="revenue"
          fill={Colors.warning}
          stroke="#8b5b01"
          strokeWidth={2}
          opacity={0.4}
          xAxisId={2}
          yAxisId={0}
        />
        <Bar
          dataKey="profit"
          fill={Colors.success}
          stroke="green"
          strokeWidth={2}
          opacity={0.4}
          xAxisId={0}
          yAxisId={0}
        />
        <Line
          yAxisId={1}
          type="monotone"
          dataKey="views"
          stroke={Colors.views}
        />
        <Line
          yAxisId={1}
          type="monotone"
          dataKey="clicks"
          stroke={Colors.clicks}
        />
        <Line
          yAxisId={2}
          type="monotone"
          dataKey="conversions"
          stroke={Colors.info}
        />
      </ComposedChart>
    </ResponsiveContainer>
  );
};

const h100: React.CSSProperties = {
  height: '100%',
};

0

There are 0 best solutions below