Commit bc2cb096 authored by tom's avatar tom

graph tooltip

parent 087575e8
...@@ -6,6 +6,8 @@ import Area from 'ui/shared/graphs/Area'; ...@@ -6,6 +6,8 @@ import Area from 'ui/shared/graphs/Area';
import Axis from 'ui/shared/graphs/Axis'; import Axis from 'ui/shared/graphs/Axis';
import GridLine from 'ui/shared/graphs/GridLine'; import GridLine from 'ui/shared/graphs/GridLine';
import Line from 'ui/shared/graphs/Line'; import Line from 'ui/shared/graphs/Line';
import Overlay from 'ui/shared/graphs/Overlay';
import Tooltip from 'ui/shared/graphs/Tooltip';
import useTimeGraphController from 'ui/shared/graphs/useTimeGraphController'; import useTimeGraphController from 'ui/shared/graphs/useTimeGraphController';
const data = { const data = {
...@@ -23,6 +25,8 @@ interface Props { ...@@ -23,6 +25,8 @@ interface Props {
const EthereumDailyTxsChart = ({ margin }: Props) => { const EthereumDailyTxsChart = ({ margin }: Props) => {
const ref = React.useRef<SVGSVGElement>(null); const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const [ rect, setRect ] = React.useState<{ width: number; height: number}>(); const [ rect, setRect ] = React.useState<{ width: number; height: number}>();
React.useEffect(() => { React.useEffect(() => {
...@@ -42,7 +46,6 @@ const EthereumDailyTxsChart = ({ margin }: Props) => { ...@@ -42,7 +46,6 @@ const EthereumDailyTxsChart = ({ margin }: Props) => {
return ( return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref }> <svg width={ width || '100%' } height={ height || '100%' } ref={ ref }>
{ width > 0 && (
<g transform={ `translate(${ margin?.left || 0 },${ margin?.top || 0 })` }> <g transform={ `translate(${ margin?.left || 0 },${ margin?.top || 0 })` }>
{ /* BASE GRID LINE */ } { /* BASE GRID LINE */ }
<GridLine <GridLine
...@@ -90,14 +93,25 @@ const EthereumDailyTxsChart = ({ margin }: Props) => { ...@@ -90,14 +93,25 @@ const EthereumDailyTxsChart = ({ margin }: Props) => {
ticks={ 5 } ticks={ 5 }
tickFormat={ yTickFormat } tickFormat={ yTickFormat }
/> />
<Overlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }>
<Axis <Axis
type="bottom" type="bottom"
scale={ xScale } scale={ xScale }
transform={ `translate(0, ${ innerHeight })` } transform={ `translate(0, ${ innerHeight })` }
ticks={ 5 } ticks={ 5 }
anchorEl={ overlayRef.current }
/>
<Tooltip
anchorEl={ overlayRef.current }
width={ innerWidth }
height={ innerHeight }
margin={ margin }
xScale={ xScale }
yScale={ yScale }
data={ data }
/> />
</Overlay>
</g> </g>
) }
</svg> </svg>
); );
}; };
......
...@@ -8,9 +8,10 @@ interface Props extends Omit<React.SVGProps<SVGGElement>, 'scale'> { ...@@ -8,9 +8,10 @@ interface Props extends Omit<React.SVGProps<SVGGElement>, 'scale'> {
disableAnimation?: boolean; disableAnimation?: boolean;
ticks: number; ticks: number;
tickFormat?: (domainValue: d3.AxisDomain, index: number) => string; tickFormat?: (domainValue: d3.AxisDomain, index: number) => string;
anchorEl?: SVGRectElement | null;
} }
const Axis = ({ type, scale, ticks, tickFormat, disableAnimation, ...props }: Props) => { const Axis = ({ type, scale, ticks, tickFormat, disableAnimation, anchorEl, ...props }: Props) => {
const ref = React.useRef<SVGGElement>(null); const ref = React.useRef<SVGGElement>(null);
const textColorToken = useColorModeValue('blackAlpha.500', 'whiteAlpha.500'); const textColorToken = useColorModeValue('blackAlpha.500', 'whiteAlpha.500');
...@@ -39,6 +40,28 @@ const Axis = ({ type, scale, ticks, tickFormat, disableAnimation, ...props }: Pr ...@@ -39,6 +40,28 @@ const Axis = ({ type, scale, ticks, tickFormat, disableAnimation, ...props }: Pr
.attr('font-size', '0.75rem'); .attr('font-size', '0.75rem');
}, [ scale, ticks, tickFormat, disableAnimation, type, textColor ]); }, [ scale, ticks, tickFormat, disableAnimation, type, textColor ]);
React.useEffect(() => {
if (!anchorEl) {
return;
}
d3.select(anchorEl)
.on('mouseout.axisX', () => {
d3.select(ref.current)
.selectAll('text')
.style('font-weight', 'normal');
})
.on('mousemove.axisX', (event) => {
const [ x ] = d3.pointer(event, anchorEl);
const xDate = scale.invert(x);
const textElements = d3.select(ref.current).selectAll('text');
const data = textElements.data();
const index = d3.bisector((d) => d).left(data, xDate);
textElements
.style('font-weight', (d, i) => i === index - 1 ? 'bold' : 'normal');
});
}, [ anchorEl, scale ]);
return <g ref={ ref } { ...props }/>; return <g ref={ ref } { ...props }/>;
}; };
......
import React from 'react';
interface Props {
width: number;
height: number;
children: React.ReactNode;
}
const Overlay = ({ width, height, children }: Props, ref: React.LegacyRef<SVGRectElement>) => (
<g>
{ children }
<rect ref={ ref } width={ width } height={ height } opacity={ 0 }/>
</g>
);
export default React.forwardRef(Overlay);
import { useToken, useColorModeValue } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
import type { TimeGraphItem } from 'ui/shared/graphs/types';
interface Props {
width?: number;
height?: number;
margin?: {
top?: number;
bottom?: number;
left?: number;
right?: number;
};
data: {
items: Array<TimeGraphItem>;
};
xScale: d3.ScaleTime<number, number>;
yScale: d3.ScaleLinear<number, number>;
anchorEl: SVGRectElement | null;
}
const Tooltip = ({ xScale, yScale, width, height, data, margin: _margin, anchorEl, ...props }: Props) => {
const margin = React.useMemo(() => ({
top: 0, bottom: 0, left: 0, right: 0,
..._margin,
}), [ _margin ]);
const lineColor = useToken('colors', 'red.500');
const textColor = useToken('colors', useColorModeValue('white', 'black'));
const bgColor = useToken('colors', useColorModeValue('gray.900', 'gray.400'));
const ref = React.useRef(null);
const drawLine = React.useCallback(
(x: number) => {
d3.select(ref.current)
.select('.Tooltip__line')
.attr('x1', x)
.attr('x2', x)
.attr('y1', -margin.top)
.attr('y2', height || 0);
},
[ ref, height, margin ],
);
const drawContent = React.useCallback(
(x: number) => {
const tooltipContent = d3.select(ref.current).select('.Tooltip__content');
tooltipContent.attr('transform', (cur, i, nodes) => {
const OFFSET = 8;
const node = nodes[i] as SVGGElement | null;
const nodeWidth = node?.getBoundingClientRect()?.width || 0;
const translateX = nodeWidth + x + OFFSET > (width || 0) ? x - nodeWidth - OFFSET : x + OFFSET;
return `translate(${ translateX }, ${ -margin.top })`;
});
tooltipContent
.select('.Tooltip__contentTitle')
.text(d3.timeFormat('%b %d, %Y')(xScale.invert(x)));
},
[ xScale, margin, width ],
);
const onChangePosition = React.useCallback((d: TimeGraphItem, isVisible: boolean) => {
d3.select('.Tooltip__value')
.text(isVisible ? d.value.toLocaleString() : '');
}, []);
const followPoints = React.useCallback((event: MouseEvent) => {
const [ x ] = d3.pointer(event, anchorEl);
const xDate = xScale.invert(x);
const bisectDate = d3.bisector<TimeGraphItem, unknown>((d) => d.date).left;
let baseXPos = 0;
// draw circles on line
d3.select(ref.current)
.select('.Tooltip__linePoint')
.attr('transform', (cur, i) => {
const index = bisectDate(data.items, xDate, 1);
const d0 = data.items[index - 1];
const d1 = data.items[index];
const d = xDate.getTime() - d0?.date.getTime() > d1?.date.getTime() - xDate.getTime() ? d1 : d0;
if (d.date === undefined && d.value === undefined) {
// move point out of container
return 'translate(-100,-100)';
}
const xPos = xScale(d.date);
if (i === 0) {
baseXPos = xPos;
}
let isVisible = true;
if (xPos !== baseXPos) {
isVisible = false;
}
const yPos = yScale(d.value);
onChangePosition(d, isVisible);
return isVisible ?
`translate(${ xPos }, ${ yPos })` :
'translate(-100,-100)';
});
drawLine(baseXPos);
drawContent(baseXPos);
}, [
anchorEl,
drawLine,
drawContent,
xScale,
yScale,
data,
onChangePosition,
]);
React.useEffect(() => {
d3.select(anchorEl)
.on('mouseout.tooltip', () => {
d3.select(ref.current).attr('opacity', 0);
})
.on('mouseover.tooltip', () => {
d3.select(ref.current).attr('opacity', 1);
})
.on('mousemove.tooltip', (event: MouseEvent) => {
d3.select(ref.current)
.select('.Tooltip__linePoint')
.attr('opacity', 1);
followPoints(event);
});
}, [ anchorEl, followPoints ]);
return (
<g ref={ ref } opacity={ 0 } { ...props }>
<line className="Tooltip__line" stroke={ lineColor }/>
<g className="Tooltip__content">
<rect className="Tooltip__contentBg" rx={ 8 } ry={ 8 } fill={ bgColor } width={ 125 } height={ 52 }/>
<text className="Tooltip__contentTitle" transform="translate(8,20)" fontSize="12px" fontWeight="bold" fill={ textColor }/>
<text
transform="translate(8,40)"
className="Tooltip__value"
fontSize="12px"
fill={ textColor }
/>
</g>
<circle className="Tooltip__linePoint" r={ 3 } opacity={ 0 } fill={ lineColor }/>
</g>
);
};
export default React.memo(Tooltip);
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