Commit eae56c21 authored by tom's avatar tom

fix chart resize

parent 8f4609d7
import _debounce from 'lodash/debounce';
import type { LegacyRef } from 'react';
import React from 'react';
import useUpdateEffect from 'lib/hooks/useUpdateEffect';
export default function useClientRect<E extends Element>(): [ DOMRect | null, LegacyRef<E> | undefined ] {
const [ rect, setRect ] = React.useState<DOMRect | null>(null);
const nodeRef = React.useRef<E>();
const ref = React.useCallback((node: E) => {
if (node !== null) {
setRect(node.getBoundingClientRect());
}
nodeRef.current = node;
}, []);
useUpdateEffect(() => {
const content = window.document.querySelector('main');
if (!content) {
return;
}
const resizeHandler = _debounce(() => {
setRect(nodeRef.current?.getBoundingClientRect() ?? null);
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(content);
resizeObserver.observe(window.document.body);
return function cleanup() {
resizeObserver.unobserve(content);
resizeObserver.unobserve(window.document.body);
};
}, [ ]);
return [ rect, ref ];
}
import React from 'react';
// Returns true if component is just mounted (on first render) and false otherwise.
export function useFirstMountState(): boolean {
const isFirst = React.useRef(true);
if (isFirst.current) {
isFirst.current = false;
return true;
}
return isFirst.current;
}
import React from 'react';
import { useFirstMountState } from './useFirstMountState';
// React effect hook that ignores the first invocation (e.g. on mount). The signature is exactly the same as the useEffect hook.
const useUpdateEffect: typeof React.useEffect = (effect, deps) => {
const isFirstMount = useFirstMountState();
React.useEffect(() => {
if (!isFirstMount) {
return effect();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
};
export default useUpdateEffect;
......@@ -6,6 +6,7 @@ import type { TimeChartData } from 'ui/shared/chart/types';
import ethTokenTransferData from 'data/charts_eth_token_transfer.json';
import ethTxsData from 'data/charts_eth_txs.json';
import dayjs from 'lib/date/dayjs';
import useClientRect from 'lib/hooks/useClientRect';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis';
import ChartGridLine from 'ui/shared/chart/ChartGridLine';
......@@ -16,8 +17,8 @@ import ChartSelectionX from 'ui/shared/chart/ChartSelectionX';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
// import useBrushX from 'ui/shared/chart/useBrushX';
import useChartLegend from 'ui/shared/chart/useChartLegend';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 };
const CHART_OFFSET = {
......@@ -27,10 +28,10 @@ const RANGE_DEFAULT_START_DATE = dayjs.min(dayjs(ethTokenTransferData[0].date),
const RANGE_DEFAULT_LAST_DATE = dayjs.max(dayjs(ethTokenTransferData.at(-1)?.date), dayjs(ethTxsData.at(-1)?.date)).toDate();
const EthereumChart = () => {
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN, CHART_OFFSET);
const [ rect, ref ] = useClientRect<SVGSVGElement>();
const { innerWidth, innerHeight } = calculateInnerSize(rect, CHART_MARGIN, CHART_OFFSET);
const [ range, setRange ] = React.useState<[ Date, Date ]>([ RANGE_DEFAULT_START_DATE, RANGE_DEFAULT_LAST_DATE ]);
const data: TimeChartData = [
......@@ -75,8 +76,8 @@ const EthereumChart = () => {
return (
<Box display="inline-block" position="relative" width="100%" height="100%">
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref }>
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ width ? 1 : 0 }>
<svg width="100%" height="calc(100% - 26px)" ref={ ref }>
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ rect ? 1 : 0 }>
{ /* BASE GRID LINE */ }
<ChartGridLine
type="horizontal"
......
......@@ -2,17 +2,19 @@ import { useToken } from '@chakra-ui/react';
import React from 'react';
import ethTxsData from 'data/charts_eth_txs.json';
import useClientRect from 'lib/hooks/useClientRect';
import ChartLine from 'ui/shared/chart/ChartLine';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
import { BlueLineGradient } from 'ui/shared/chart/utils/gradients';
const CHART_MARGIN = { bottom: 0, left: 0, right: 0, top: 0 };
const DATA = ethTxsData.slice(-30).map((d) => ({ ...d, date: new Date(d.date) }));
const SplineChartExample = () => {
const ref = React.useRef<SVGSVGElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
const [ rect, ref ] = useClientRect<SVGSVGElement>();
const { innerWidth, innerHeight } = calculateInnerSize(rect, CHART_MARGIN);
const color = useToken('colors', 'blue.500');
const { xScale, yScale } = useTimeChartController({
data: [ { items: DATA, name: 'spline', color } ],
......@@ -21,7 +23,7 @@ const SplineChartExample = () => {
});
return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref }>
<svg width="100%" height="100%" ref={ ref }>
<defs>
<BlueLineGradient.defs/>
</defs>
......
......@@ -3,12 +3,13 @@ import React from 'react';
import type { TimeChartData } from 'ui/shared/chart/types';
import useClientRect from 'lib/hooks/useClientRect';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartLine from 'ui/shared/chart/ChartLine';
import ChartOverlay from 'ui/shared/chart/ChartOverlay';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
interface Props {
data: TimeChartData;
......@@ -18,11 +19,11 @@ interface Props {
const CHART_MARGIN = { bottom: 5, left: 10, right: 10, top: 0 };
const ChainIndicatorChart = ({ data }: Props) => {
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const lineColor = useToken('colors', 'blue.500');
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
const [ rect, ref ] = useClientRect<SVGSVGElement>();
const { innerWidth, innerHeight } = calculateInnerSize(rect, CHART_MARGIN);
const { xScale, yScale } = useTimeChartController({
data,
width: innerWidth,
......@@ -30,8 +31,8 @@ const ChainIndicatorChart = ({ data }: Props) => {
});
return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref } cursor="pointer">
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ width ? 1 : 0 }>
<svg width="100%" height="100%" ref={ ref } cursor="pointer">
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ rect ? 1 : 0 }>
<ChartArea
data={ data[0].items }
xScale={ xScale }
......
......@@ -5,6 +5,7 @@ import React, { useEffect, useMemo } from 'react';
import type { ChartMargin, TimeChartItem } from 'ui/shared/chart/types';
import dayjs from 'lib/date/dayjs';
import useClientRect from 'lib/hooks/useClientRect';
import useIsMobile from 'lib/hooks/useIsMobile';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis';
......@@ -13,8 +14,8 @@ import ChartLine from 'ui/shared/chart/ChartLine';
import ChartOverlay from 'ui/shared/chart/ChartOverlay';
import ChartSelectionX from 'ui/shared/chart/ChartSelectionX';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
interface Props {
isEnlarged?: boolean;
......@@ -31,10 +32,12 @@ const DEFAULT_CHART_MARGIN = { bottom: 20, left: 40, right: 20, top: 10 };
const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin }: Props) => {
const isMobile = useIsMobile();
const color = useToken('colors', 'blue.200');
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const [ rect, ref ] = useClientRect<SVGSVGElement>();
const chartMargin = { ...DEFAULT_CHART_MARGIN, ...margin };
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, chartMargin);
const { innerWidth, innerHeight } = calculateInnerSize(rect, chartMargin);
const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`;
const [ range, setRange ] = React.useState<[ Date, Date ]>([ items[0].date, items[items.length - 1].date ]);
......@@ -70,7 +73,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
}, [ isZoomResetInitial, items ]);
return (
<svg width="100%" height={ height || '100%' } ref={ ref } cursor="pointer" id={ chartId } opacity={ width ? 1 : 0 }>
<svg width="100%" height="100%" ref={ ref } cursor="pointer" id={ chartId } opacity={ rect ? 1 : 0 }>
<g transform={ `translate(${ chartMargin?.left || 0 },${ chartMargin?.top || 0 })` }>
<ChartGridLine
......
import _debounce from 'lodash/debounce';
import React, { useCallback } from 'react';
import type { ChartMargin, ChartOffset } from 'ui/shared/chart/types';
export default function useChartSize(svgEl: SVGSVGElement | null, margin?: ChartMargin, offsets?: ChartOffset) {
const [ rect, setRect ] = React.useState<{ width: number; height: number}>({ width: 0, height: 0 });
const calculateRect = useCallback((element: SVGSVGElement) => {
const rect = element?.getBoundingClientRect();
return { width: rect?.width || 0, height: rect?.height || 0 };
}, []);
React.useEffect(() => {
const content = window.document.querySelector('main');
if (!content) {
return;
}
let timeoutId: number;
const resizeHandler = _debounce(() => {
timeoutId = window.setTimeout(() => {
if (svgEl) {
setRect(calculateRect(svgEl));
}
}, 100);
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(content);
resizeObserver.observe(window.document.body);
return function cleanup() {
resizeObserver.unobserve(content);
resizeObserver.unobserve(window.document.body);
window.clearTimeout(timeoutId);
};
}, [ calculateRect, svgEl ]);
return React.useMemo(() => {
return {
width: Math.max(rect.width - (offsets?.x || 0), 0),
height: Math.max(rect.height - (offsets?.y || 0), 0),
innerWidth: Math.max(rect.width - (offsets?.x || 0) - (margin?.left || 0) - (margin?.right || 0), 0),
innerHeight: Math.max(rect.height - (offsets?.y || 0) - (margin?.bottom || 0) - (margin?.top || 0), 0),
};
}, [ margin?.bottom, margin?.left, margin?.right, margin?.top, offsets?.x, offsets?.y, rect.height, rect.width ]);
}
import type { ChartMargin, ChartOffset } from 'ui/shared/chart/types';
export default function calculateInnerSize(rect: DOMRect | null, margin?: ChartMargin, offsets?: ChartOffset) {
if (!rect) {
return { innerWidth: 0, innerHeight: 0 };
}
return {
innerWidth: Math.max(rect.width - (offsets?.x || 0) - (margin?.left || 0) - (margin?.right || 0), 0),
innerHeight: Math.max(rect.height - (offsets?.y || 0) - (margin?.bottom || 0) - (margin?.top || 0), 0),
};
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment