Commit 902460a3 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #431 from blockscout/chart-touch-zoom

chart zoom for touch devices and selection issue fix
parents ad8b383c 489a4d28
......@@ -18,10 +18,9 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
const borderColor = useToken('colors', 'blue.200');
const ref = React.useRef(null);
const isPressed = React.useRef(false);
const isActive = React.useRef(false);
const startX = React.useRef<number>();
const endX = React.useRef<number>();
const startIndex = React.useRef<number>(0);
const getIndexByX = React.useCallback((x: number) => {
const xDate = scale.invert(x);
......@@ -51,20 +50,33 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
.attr('width', Math.abs(diffX));
}, []);
const handelMouseUp = React.useCallback(() => {
isPressed.current = false;
const handleSelect = React.useCallback((x0: number, x1: number) => {
const index0 = getIndexByX(x0);
const index1 = getIndexByX(x1);
if (Math.abs(index0 - index1) > SELECTION_THRESHOLD) {
onSelect([ Math.min(index0, index1), Math.max(index0, index1) ]);
}
}, [ getIndexByX, onSelect ]);
const cleanUp = React.useCallback(() => {
isActive.current = false;
startX.current = undefined;
endX.current = undefined;
d3.select(ref.current).attr('opacity', 0);
}, [ ]);
if (!endX.current) {
const handelMouseUp = React.useCallback(() => {
if (!isActive.current) {
return;
}
const index = getIndexByX(endX.current);
if (Math.abs(index - startIndex.current) > SELECTION_THRESHOLD) {
onSelect([ Math.min(index, startIndex.current), Math.max(index, startIndex.current) ]);
if (startX.current && endX.current) {
handleSelect(startX.current, endX.current);
}
}, [ getIndexByX, onSelect ]);
cleanUp();
}, [ cleanUp, handleSelect ]);
React.useEffect(() => {
if (!anchorEl) {
......@@ -76,20 +88,34 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
anchorD3
.on('mousedown.selectionX', (event: MouseEvent) => {
const [ x ] = d3.pointer(event, anchorEl);
isPressed.current = true;
isActive.current = true;
startX.current = x;
const index = getIndexByX(x);
startIndex.current = index;
})
.on('mouseup.selectionX', handelMouseUp)
.on('mousemove.selectionX', (event: MouseEvent) => {
if (isPressed.current) {
if (isActive.current) {
const [ x ] = d3.pointer(event, anchorEl);
startX.current && drawSelection(startX.current, x);
endX.current = x;
}
});
})
.on('mouseup.selectionX', handelMouseUp)
.on('touchstart.selectionX', (event: TouchEvent) => {
const pointers = d3.pointers(event, anchorEl);
isActive.current = pointers.length === 2;
})
.on('touchmove.selectionX', (event: TouchEvent) => {
if (isActive.current) {
const pointers = d3.pointers(event, anchorEl);
if (pointers.length === 2 && Math.abs(pointers[0][0] - pointers[1][0]) > 5) {
drawSelection(pointers[0][0], pointers[1][0]);
startX.current = pointers[0][0];
endX.current = pointers[1][0];
}
}
})
.on('touchend.selectionX', handelMouseUp);
d3.select('body').on('mouseup.selectionX', function(event) {
const isOutside = startX.current !== undefined && event.target !== anchorD3.node();
......@@ -99,10 +125,10 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
});
return () => {
anchorD3.on('mousedown.selectionX mouseup.selectionX mousemove.selectionX', null);
d3.select('body').on('mouseup.selectionX', null);
anchorD3.on('.selectionX', null);
d3.select('body').on('.selectionX', null);
};
}, [ anchorEl, drawSelection, getIndexByX, handelMouseUp ]);
}, [ anchorEl, cleanUp, drawSelection, getIndexByX, handelMouseUp, handleSelect ]);
return (
<g className="ChartSelectionX" ref={ ref } opacity={ 0 }>
......
......@@ -32,6 +32,8 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
const bgColor = useToken('colors', 'blackAlpha.900');
const ref = React.useRef(null);
const trackerId = React.useRef<number>();
const isVisible = React.useRef(false);
const drawLine = React.useCallback(
(x: number) => {
......@@ -129,67 +131,78 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
}, [ drawPoints, drawLine, drawContent ]);
const showContent = React.useCallback(() => {
if (!isVisible.current) {
d3.select(ref.current).attr('opacity', 1);
d3.select(ref.current)
.selectAll('.ChartTooltip__point')
.attr('opacity', 1);
isVisible.current = true;
}
}, []);
const hideContent = React.useCallback(() => {
d3.select(ref.current).attr('opacity', 0);
isVisible.current = false;
}, []);
const createPointerTracker = React.useCallback((event: PointerEvent, isSubsequentCall?: boolean) => {
let isShown = false;
let isPressed = event.pointerType === 'mouse' && event.type === 'pointerdown' && !isSubsequentCall;
if (isPressed) {
hideContent();
}
trackPointer(event, {
return trackPointer(event, {
move: (pointer) => {
if (!pointer.point || isPressed) {
return;
}
draw(pointer);
if (!isShown) {
showContent();
isShown = true;
}
},
out: () => {
hideContent();
isShown = false;
trackerId.current = undefined;
},
end: (tracker) => {
end: () => {
hideContent();
const isOutside = tracker.sourceEvent?.offsetX && width && (tracker.sourceEvent.offsetX > width || tracker.sourceEvent.offsetX < 0);
if (!isOutside && isPressed) {
window.setTimeout(() => {
createPointerTracker(event, true);
}, 0);
}
isShown = false;
trackerId.current = undefined;
isPressed = false;
},
});
}, [ draw, hideContent, showContent, width ]);
}, [ draw, hideContent, showContent ]);
React.useEffect(() => {
const anchorD3 = d3.select(anchorEl);
let isMultiTouch = false; // disabling creation of new tracker in multi touch mode
anchorD3
.on('touchmove.tooltip', (event: PointerEvent) => event.preventDefault()) // prevent scrolling
.on('touchmove.tooltip', (event: TouchEvent) => event.preventDefault()) // prevent scrolling
.on(`touchstart.tooltip`, (event: TouchEvent) => {
isMultiTouch = event.touches.length > 1;
})
.on(`touchend.tooltip`, (event: TouchEvent) => {
if (isMultiTouch && event.touches.length === 0) {
isMultiTouch = false;
}
})
.on('pointerenter.tooltip pointerdown.tooltip', (event: PointerEvent) => {
createPointerTracker(event);
if (!isMultiTouch) {
trackerId.current = createPointerTracker(event);
}
})
.on('pointermove.tooltip', (event: PointerEvent) => {
if (event.pointerType === 'mouse' && !isMultiTouch && trackerId.current === undefined) {
trackerId.current = createPointerTracker(event);
}
});
return () => {
anchorD3.on('touchmove.tooltip pointerenter.tooltip pointerdown.tooltip', null);
trackerId.current && anchorD3.on(
[ 'pointerup', 'pointercancel', 'lostpointercapture', 'pointermove', 'pointerout' ].map((event) => `${ event }.${ trackerId.current }`).join(' '),
null,
);
};
}, [ anchorEl, createPointerTracker, draw, hideContent, showContent ]);
......
......@@ -14,7 +14,7 @@ export interface PointerOptions {
end?: (tracker: Pointer) => void;
}
export function trackPointer(event: PointerEvent, { start, move, out, end }: PointerOptions) {
export function trackPointer(event: PointerEvent, { start, move, out, end }: PointerOptions): number {
const tracker: Pointer = {
id: event.pointerId,
point: null,
......@@ -26,16 +26,27 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi
tracker.point = d3.pointer(event, target);
target.setPointerCapture(id);
const untrack = (sourceEvent: PointerEvent) => {
tracker.sourceEvent = sourceEvent;
d3.select(target).on(`.${ id }`, null);
target.releasePointerCapture(id);
end?.(tracker);
};
d3.select(target)
.on(`touchstart.${ id }`, (sourceEvent: PointerEvent) => {
const target = sourceEvent.target as Element;
const touches = d3.pointers(sourceEvent, target);
// disable current tracker when entering multi touch mode
touches.length > 1 && untrack(sourceEvent);
})
.on(`pointerup.${ id } pointercancel.${ id } lostpointercapture.${ id }`, (sourceEvent: PointerEvent) => {
if (sourceEvent.pointerId !== id) {
return;
}
tracker.sourceEvent = sourceEvent;
d3.select(target).on(`.${ id }`, null);
target.releasePointerCapture(id);
end?.(tracker);
untrack(sourceEvent);
})
.on(`pointermove.${ id }`, (sourceEvent) => {
if (sourceEvent.pointerId !== id) {
......@@ -57,5 +68,5 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi
start?.(tracker);
return [ 'pointerup', 'pointercancel', 'lostpointercapture', 'pointermove', 'pointerout' ].map((event) => `${ event }.${ id }`);
return id;
}
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