diff --git a/demo/examples/tooltip-with-datarefs.html b/demo/examples/tooltip-with-datarefs.html index e300a6f1..e8d933df 100644 --- a/demo/examples/tooltip-with-datarefs.html +++ b/demo/examples/tooltip-with-datarefs.html @@ -42,10 +42,10 @@

Tooltip Plugin

title: { text: 'By scale, constant', }, - timeline: new Array(5).fill().map((_, i) => i * 1000), + timeline: new Array(100).fill().map((_, i) => i * 1000), series: [ - {data: new Array(5).fill().map((_, i) => Math.random() * 6), color: 'red'}, - {data: new Array(5).fill().map((_, i) => Math.random() * 6), color: 'green'}, + {data: new Array(100).fill().map((_, i) => Math.random() * 6), color: 'red'}, + {data: new Array(100).fill().map((_, i) => Math.random() * 6), color: 'green'}, ], chart: { select: {zoom: false}, @@ -112,23 +112,23 @@

Tooltip Plugin

return ` ${id} - ${ref.min.toFixed(2) ?? '-'} - ${ref.max.toFixed(2) ?? '-'} - ${ref.avg.toFixed(2) ?? '-'} - ${ref.sum.toFixed(2) ?? '-'} - ${ref.count.toFixed(2) ?? '-'} - ${ref.integral.toFixed(2) ?? '-'} - ${ref.last ?? '-'} + ${ref.min.toFixed(1) ?? '-'} + ${ref.max.toFixed(1) ?? '-'} + ${ref.avg.toFixed(1) ?? '-'} + ${ref.sum.toFixed(1) ?? '-'} + ${ref.count.toFixed(1) ?? '-'} + ${ref.integral.toFixed(1) ?? '-'} + ${ref.last?.toFixed(1) ?? '-'} `; })} Total - ${refs.y.total.min.toFixed(2)} - ${refs.y.total.max.toFixed(2)} - ${refs.y.total.avg.toFixed(2)} - ${refs.y.total.sum.toFixed(2)} - ${refs.y.total.count.toFixed(2)} - ${refs.y.total.integral.toFixed(2)} + ${refs.y.total.min.toFixed(1)} + ${refs.y.total.max.toFixed(1)} + ${refs.y.total.avg.toFixed(1)} + ${refs.y.total.sum.toFixed(1)} + ${refs.y.total.count.toFixed(1)} + ${refs.y.total.integral.toFixed(1)} ${refs.y.total.last ?? '-'} `; diff --git a/src/YagrCore/plugins/tooltip/tooltip.ts b/src/YagrCore/plugins/tooltip/tooltip.ts index cc67920a..7b77b352 100644 --- a/src/YagrCore/plugins/tooltip/tooltip.ts +++ b/src/YagrCore/plugins/tooltip/tooltip.ts @@ -119,6 +119,7 @@ class YagrTooltip { private bLeft: number; private bTop: number; + private bWidth: number; constructor(yagr: Yagr, options: Partial = {}) { this.yagr = yagr; @@ -153,6 +154,7 @@ class YagrTooltip { this.bLeft = 0; this.bTop = 0; + this.bWidth = 0; if (this.opts.virtual) { this.placement = () => {}; @@ -295,7 +297,7 @@ class YagrTooltip { return; } - if ((left < 0 || top < 0) && !state.pinned) { + if ((left < 0 || top < 0) && !state.pinned && this.isNotInDrag) { this.hide(); } @@ -435,7 +437,7 @@ class YagrTooltip { if (hasOneRow) { this.onMouseEnter(); } else { - this.onMouseLeave(); + this.hide(); return; } @@ -443,6 +445,7 @@ class YagrTooltip { this.bLeft = bbox.left; this.bTop = bbox.top; + this.bWidth = bbox.width; const anchor = { left: left + this.bLeft, @@ -491,10 +494,11 @@ class YagrTooltip { this.over = u.root.querySelector('.u-over') as HTMLDivElement; this.over.addEventListener('mousedown', this.onMouseDown); - this.over.addEventListener('mouseup', this.onMouseUp); this.over.addEventListener('mousemove', this.onMouseMove); this.over.addEventListener('mouseenter', this.onMouseEnter); this.over.addEventListener('mouseleave', this.onMouseLeave); + + document.addEventListener('mouseup', this.onMouseUp); }; setSize = () => { @@ -507,11 +511,11 @@ class YagrTooltip { dispose = () => { /** Free overlay listeners */ this.over.removeEventListener('mousedown', this.onMouseDown); - this.over.removeEventListener('mouseup', this.onMouseUp); this.over.removeEventListener('mousemove', this.onMouseMove); this.over.removeEventListener('mouseenter', this.onMouseEnter); this.over.removeEventListener('mouseleave', this.onMouseLeave); + document.removeEventListener('mouseup', this.onMouseUp); document.removeEventListener('mousemove', this.checkFocus); document.removeEventListener('mousedown', this.detectClickOutside); @@ -569,9 +573,58 @@ class YagrTooltip { } }; - private onMouseUp = () => { + /** + * Calculates where exactly cursor leaved the chart + * and sets range[1] to this position + */ + private setCursorLeaved = (e: MouseEvent) => { + const rect = this.over.getBoundingClientRect(); + const x = e.clientX; + const range = this.state.range!; + const startPoint = range[0]!; + const xInOver = x - rect.left; + const end = xInOver > startPoint.clientX; + const timeline = this.yagr.config.timeline; + + let result; + if (end) { + range[1] = { + clientX: this.bWidth, + value: this.yagr.uplot.posToVal(this.bWidth, 'x'), + idx: timeline.length - 1, + }; + result = range[1]; + } else { + /** Swap range[1] and range[0] in case if tooltip leaved chart in begining of element */ + range[1] = range[0]; + range[0] = { + clientX: 0, + value: this.yagr.uplot.posToVal(0, 'x'), + idx: 0, + }; + + result = range[0]; + } + + this.yagr.uplot.setCursor({ + left: result.clientX, + top: e.clientY - rect.top, + }); + }; + + private onMouseUp = (e: MouseEvent) => { + if (this.state.range === null) { + return; + } + const [from] = this.state.range || []; - const cursor = this.getCursorPosition(); + let cursor: SelectionRange[number]; + + if (e.target === this.over) { + cursor = this.getCursorPosition(); + } else { + cursor = this.state.range[1]; + } if (this.opts.strategy === 'none') { return; @@ -599,8 +652,14 @@ class YagrTooltip { this.show(); }; - private onMouseLeave = () => { - if (!this.state.pinned) { + private onMouseLeave = (e: MouseEvent) => { + const isPinned = this.state.pinned; + + if (this.state.range?.[0]) { + this.setCursorLeaved(e); + } + + if (!isPinned && this.isNotInDrag) { this.hide(); } }; @@ -650,6 +709,13 @@ class YagrTooltip { get stripValue() { return this.interpolation ? this.interpolation.value : undefined; } + get isNotInDrag() { + if (this.opts.strategy === 'none' || this.opts.strategy === 'pin') { + return true; + } + + return !this.state.range?.[1]; + } } /* diff --git a/tests/plugins/tooltip.test.ts b/tests/plugins/tooltip.test.ts index 501eeeb3..c5ec192f 100644 --- a/tests/plugins/tooltip.test.ts +++ b/tests/plugins/tooltip.test.ts @@ -21,6 +21,8 @@ describe('tooltip', () => { }); expect(window.document.querySelector(`#${yagr.id}_tooltip`)).toBeTruthy(); + + yagr.dispose(); }); it('should render tooltip sum', async () => { @@ -42,6 +44,8 @@ describe('tooltip', () => { yagr.plugins.tooltip?.on('show', () => { expect(window.document.querySelector(`.__section_sum`)).not.toBeNull(); }); + + yagr.dispose(); }); }); @@ -79,6 +83,11 @@ describe('tooltip', () => { it('should render tooltip sum', () => { expect(tElem.querySelectorAll('.__section_sum').length).toBe(1); }); + + afterAll(() => { + y.dispose(); + y.root.remove(); + }); }); describe('on/off', () => { @@ -110,6 +119,11 @@ describe('tooltip', () => { expect(handler).toBeCalledTimes(1); }); + + afterAll(() => { + yagr.dispose(); + window.document.body.innerHTML = ''; + }); }); describe('pinning', () => { @@ -124,7 +138,7 @@ describe('tooltip', () => { yagr.uplot.over.dispatchEvent(new MouseEvent('mousedown', {clientX: 30, clientY: 30})); await new Promise((resolve) => setTimeout(resolve, 100)); - yagr.uplot.over.dispatchEvent(new MouseEvent('mouseup', {clientX: 30, clientY: 30})); + yagr.uplot.over.dispatchEvent(new MouseEvent('mouseup', {clientX: 30, clientY: 30, bubbles: true})); expect(yagr.plugins.tooltip?.state.pinned).toBe(false); }); @@ -139,12 +153,18 @@ describe('tooltip', () => { yagr.uplot.over.dispatchEvent(new MouseEvent('mousedown', {clientX: 30, clientY: 30})); await new Promise((resolve) => setTimeout(resolve, 100)); - yagr.uplot.over.dispatchEvent(new MouseEvent('mouseup', {clientX: 30, clientY: 30})); + yagr.uplot.over.dispatchEvent(new MouseEvent('mouseup', {clientX: 30, clientY: 30, bubbles: true})); expect(yagr.plugins.tooltip?.state.pinned).toBe(false); }); it('should pin tooltip if strategy=pin', async () => { const yagr = gen({ + chart: { + size: { + width: 600, + height: 400, + }, + }, timeline: [1, 2, 3, 4], series: [{data: [1, 2, 3, 4]}], tooltip: { @@ -152,9 +172,9 @@ describe('tooltip', () => { }, }); - yagr.uplot.over.dispatchEvent(new MouseEvent('mousedown', {clientX: 30, clientY: 30})); + yagr.uplot.over.dispatchEvent(new MouseEvent('mousedown', {clientX: 100, clientY: 30})); await new Promise((resolve) => setTimeout(resolve, 100)); - yagr.uplot.over.dispatchEvent(new MouseEvent('mouseup', {clientX: 30, clientY: 30})); + yagr.uplot.over.dispatchEvent(new MouseEvent('mouseup', {clientX: 100, clientY: 30, bubbles: true})); expect(yagr.plugins.tooltip?.state.pinned).toBe(true); }); @@ -172,7 +192,7 @@ describe('tooltip', () => { yagr.uplot.over.dispatchEvent(new MouseEvent('mousemove', {clientX: 40, clientY: 30})); await new Promise((resolve) => setTimeout(resolve, 100)); expect(yagr.plugins.tooltip?.state.range).toHaveLength(2); - yagr.uplot.over.dispatchEvent(new MouseEvent('mouseup', {clientX: 40, clientY: 30})); + yagr.uplot.over.dispatchEvent(new MouseEvent('mouseup', {clientX: 40, clientY: 30, bubbles: true})); expect(yagr.plugins.tooltip?.state.pinned).toBe(false); expect(yagr.plugins.tooltip?.state.range).toBeNull(); }); @@ -191,7 +211,7 @@ describe('tooltip', () => { yagr.uplot.over.dispatchEvent(new MouseEvent('mousemove', {clientX: 40, clientY: 30})); await new Promise((resolve) => setTimeout(resolve, 100)); expect(yagr.plugins.tooltip?.state.range).toHaveLength(2); - yagr.uplot.over.dispatchEvent(new MouseEvent('mouseup', {clientX: 40, clientY: 30})); + yagr.uplot.over.dispatchEvent(new MouseEvent('mouseup', {clientX: 40, clientY: 30, bubbles: true})); expect(yagr.plugins.tooltip?.state.pinned).toBe(true); expect(yagr.plugins.tooltip?.state.range).toBeNull(); });