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.
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%',
};