Skip to content
This repository was archived by the owner on Apr 18, 2024. It is now read-only.

Commit 46cf260

Browse files
chore: DEV-4026: E2E for TimeSeries with huge data and overview manipulations (#1099)
Fix problems with NaN and Infinity values in svg attrs Co-authored-by: Nick Skriabin <767890+nicholasrq@users.noreply.github.com>
1 parent 73f96a3 commit 46cf260

File tree

4 files changed

+252
-34
lines changed

4 files changed

+252
-34
lines changed

e2e/fragments/AtTimeSeries.js

Lines changed: 115 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,115 @@
1-
/* global inject */
2-
const { I } = inject();
3-
4-
module.exports = {
5-
_rootSelector: '.htx-timeseries',
6-
get _channelStageSelector() {
7-
return `${this._rootSelector} .htx-timeseries-channel .new_brush`;
8-
},
9-
_stageBBox: { x: 0, y: 0, width: 0, height: 0 },
10-
11-
async lookForStage() {
12-
I.scrollPageToTop();
13-
const bbox = await I.grabElementBoundingRect(this._channelStageSelector);
14-
15-
this._stageBBox = bbox;
16-
},
17-
18-
/**
19-
* Mousedown - mousemove - mouseup drawing a region on the first Channel. Works in conjunction with lookForStage.
20-
* @example
21-
* await AtTimeSeries.lookForStage();
22-
* AtTimeseries.drawByDrag(50, 200);
23-
* @param x
24-
* @param shiftX
25-
*/
26-
drawByDrag(x,shiftX) {
27-
I.scrollPageToTop();
28-
I.moveMouse(this._stageBBox.x + x, this._stageBBox.y + this._stageBBox.height / 2);
29-
I.pressMouseDown();
30-
I.moveMouse(this._stageBBox.x + x + shiftX, this._stageBBox.y + this._stageBBox.height / 2, 3);
31-
I.pressMouseUp();
32-
},
33-
};
1+
/* global inject */
2+
const { I } = inject();
3+
4+
module.exports = {
5+
_rootSelector: '.htx-timeseries',
6+
_channelSelector: '.htx-timeseries-channel .overlay',
7+
_overviewSelector: '.htx-timeseries-overview .overlay',
8+
_westHandleSelector: '.htx-timeseries-overview .handle--w',
9+
_eastHandleSelector: '.htx-timeseries-overview .handle--e',
10+
get _channelStageSelector() {
11+
return `${this._rootSelector} .htx-timeseries-channel .new_brush`;
12+
},
13+
_stageBBox: { x: 0, y: 0, width: 0, height: 0 },
14+
15+
WEST: 'west',
16+
EAST: 'east',
17+
18+
async lookForStage() {
19+
I.scrollPageToTop();
20+
const bbox = await I.grabElementBoundingRect(this._channelStageSelector);
21+
22+
this._stageBBox = bbox;
23+
},
24+
25+
/**
26+
* Select range on overview to zoom in
27+
* @param {number} from - relative position of start between 0 and 1
28+
* @param {number} to - relative position of finish between 0 and 1
29+
* @returns {Promise<void>}
30+
*
31+
* @example
32+
* await AtTimeSeries.selectOverviewRange(.25, .75);
33+
*/
34+
async selectOverviewRange(from, to) {
35+
I.scrollPageToTop();
36+
const overviewBBox = await I.grabElementBoundingRect(this._overviewSelector);
37+
38+
I.moveMouse(overviewBBox.x + overviewBBox.width * from, overviewBBox.y + overviewBBox.height / 2);
39+
I.pressMouseDown();
40+
I.moveMouse(overviewBBox.x + overviewBBox.width * to, overviewBBox.y + overviewBBox.height / 2, 3);
41+
I.pressMouseUp();
42+
},
43+
44+
/**
45+
* Move range on overview to another position
46+
* @param {number} where - position between 0 and 1
47+
* @returns {Promise<void>}
48+
*/
49+
async clickOverview(where) {
50+
I.scrollPageToTop();
51+
const overviewBBox = await I.grabElementBoundingRect(this._overviewSelector);
52+
53+
I.clickAt(overviewBBox.x + overviewBBox.width * where, overviewBBox.y + overviewBBox.height / 2);
54+
},
55+
56+
/**
57+
* Move overview handle by mouse drag
58+
* @param {number} where - position between 0 and 1
59+
* @param {"west"|"east"} [which="west"] - handler name
60+
* @returns {Promise<void>}
61+
*
62+
* @example
63+
* await AtTimeSeries.moveHandle(.5, AtTimeSeries.WEST);
64+
*/
65+
async moveHandle(where, which = this.WEST) {
66+
I.scrollPageToTop();
67+
const handlerBBox = await I.grabElementBoundingRect(this[`_${which}HandleSelector`]);
68+
const overviewBBox = await I.grabElementBoundingRect(this._overviewSelector);
69+
70+
I.moveMouse(handlerBBox.x + handlerBBox.width / 2, handlerBBox.y + handlerBBox.height / 2);
71+
I.pressMouseDown();
72+
I.moveMouse(overviewBBox.x + overviewBBox.width * where, overviewBBox.y + overviewBBox.height / 2, 3);
73+
I.pressMouseUp();
74+
},
75+
76+
/**
77+
* Zoom by mouse wheel over the channel
78+
* @param {number} deltaY
79+
* @param {Object} [atPoint] - Point where will be called wheel action
80+
* @param {number} [atPoint.x=0.5] - relative X coordinate
81+
* @param {number} [atPoint.y=0.5] - relative Y coordinate
82+
* @returns {Promise<void>}
83+
*
84+
* @example
85+
* // zoom in
86+
* await AtTimeSeries.zoomByMouse(-100, { x: .01 });
87+
* // zoom out
88+
* await AtTimeSeries.zoomByMouse(100);
89+
*/
90+
async zoomByMouse(deltaY, atPoint) {
91+
const { x = 0.5, y = 0.5 } = atPoint;
92+
93+
I.scrollPageToTop();
94+
const channelBBox = await I.grabElementBoundingRect(this._channelSelector);
95+
96+
I.moveMouse(channelBBox.x + channelBBox.width * x, channelBBox.y + channelBBox.height* y);
97+
I.mouseWheel({ deltaY });
98+
},
99+
100+
/**
101+
* Mousedown - mousemove - mouseup drawing a region on the first Channel. Works in conjunction with lookForStage.
102+
* @example
103+
* await AtTimeSeries.lookForStage();
104+
* AtTimeseries.drawByDrag(50, 200);
105+
* @param x
106+
* @param shiftX
107+
*/
108+
drawByDrag(x,shiftX) {
109+
I.scrollPageToTop();
110+
I.moveMouse(this._stageBBox.x + x, this._stageBBox.y + this._stageBBox.height / 2);
111+
I.pressMouseDown();
112+
I.moveMouse(this._stageBBox.x + x + shiftX, this._stageBBox.y + this._stageBBox.height / 2, 3);
113+
I.pressMouseUp();
114+
},
115+
};

e2e/helpers/MouseActions.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ class MouseActions extends Helper {
2626
return page.mouse.move(x, y, { steps });
2727
}
2828

29+
/**
30+
* Mouse wheel action
31+
* @param {{deltaY: number, deltaX: number}} deltas
32+
*/
33+
mouseWheel({ deltaX = 0, deltaY = 0 }) {
34+
const page = getPage(this.helpers);
35+
36+
return page.mouse.wheel(deltaX, deltaY);
37+
}
38+
2939
/**
3040
* Drag action from point to point
3141
* @param {object} from

e2e/tests/timeseries.test.js

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* global Feature, Scenario, locate */
22
const { initLabelStudio } = require('./helpers');
3-
// const Utils = require("../examples/utils");
3+
const assert = require('assert');
44

55
const config = ({ timeformat }) => `
66
<View>
@@ -169,6 +169,22 @@ const scenarios = {
169169
},
170170
};
171171

172+
function generateData(stepsNumber) {
173+
const timeseries = {
174+
time: [],
175+
one: [],
176+
two: [],
177+
};
178+
179+
for (let i = 0; i < stepsNumber; i++) {
180+
timeseries.time[i] = i;
181+
timeseries.one[i] = Math.sin(Math.sqrt(i));
182+
timeseries.two[i] = Math.cos(Math.sqrt(i));
183+
}
184+
185+
return timeseries;
186+
}
187+
172188
Feature('TimeSeries datasets');
173189
Object.entries(scenarios).forEach(([title, scenario]) =>
174190
Scenario(title, async function({ I }) {
@@ -181,3 +197,81 @@ Object.entries(scenarios).forEach(([title, scenario]) =>
181197

182198
scenario.assert(I);
183199
}));
200+
201+
Scenario('TimeSeries with optimized data', async ({ I, LabelStudio, ErrorsCollector, AtTimeSeries }) => {
202+
async function doNotSeeProblems() {
203+
await I.wait(2);
204+
I.seeElement('.htx-timeseries');
205+
const errors = await ErrorsCollector.grabErrors();
206+
207+
if (errors.length) {
208+
assert.fail(`Got an error: ${errors[0]}`);
209+
}
210+
211+
const counters = await I.executeScript(() => {
212+
return {
213+
NaN: document.querySelectorAll('[d*=\'NaN\']').length +
214+
document.querySelectorAll('[cy*=\'NaN\']').length +
215+
document.querySelectorAll('[transform*=\'NaN\']').length,
216+
Infinity: document.querySelectorAll('[transform*=\'Infinity\']').length,
217+
};
218+
});
219+
220+
if (counters.NaN) {
221+
assert.fail('Found element with NaN in attribute');
222+
}
223+
if (counters.Infinity) {
224+
assert.fail('Found element with Infinity in attribute');
225+
}
226+
}
227+
228+
I.amOnPage('/');
229+
await ErrorsCollector.run();
230+
231+
const SLICES_COUNT = 10;
232+
const BAD_MULTIPLIER = 1.9;
233+
const screenWidth = await I.executeScript(()=>{
234+
return window.screen.width * window.devicePixelRatio;
235+
});
236+
const stepsToGenerate = screenWidth * SLICES_COUNT * BAD_MULTIPLIER;
237+
const params = {
238+
annotations: [{
239+
id: 'test',
240+
result: [],
241+
}],
242+
config: config({}),
243+
data: {
244+
timeseries: generateData(stepsToGenerate),
245+
},
246+
};
247+
248+
LabelStudio.init(params);
249+
I.waitForVisible('.htx-timeseries');
250+
251+
I.say('try to get errors by selecting overview range');
252+
await AtTimeSeries.selectOverviewRange(.98, 1);
253+
await doNotSeeProblems();
254+
255+
I.say('try to get errors by zooming in by mouse wheel');
256+
I.pressKeyDown('Control');
257+
for (let i = 0; i < 10; i++) {
258+
await AtTimeSeries.zoomByMouse(-100, { x: .98 });
259+
}
260+
I.pressKeyUp('Control');
261+
await doNotSeeProblems();
262+
263+
I.say('try to get errors by moving handle to the extreme position');
264+
await AtTimeSeries.moveHandle(1.1);
265+
await AtTimeSeries.moveHandle(1.1);
266+
await doNotSeeProblems();
267+
268+
I.say('try to get errors by moving overview range by click');
269+
await AtTimeSeries.clickOverview(0.15);
270+
await doNotSeeProblems();
271+
272+
I.say('try to get errors by creating micro ranges at overview');
273+
await AtTimeSeries.selectOverviewRange(.9, .9001);
274+
await doNotSeeProblems();
275+
await AtTimeSeries.selectOverviewRange(.9, .8999);
276+
await doNotSeeProblems();
277+
});

src/tags/object/TimeSeries.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,17 @@ const Overview = observer(({ item, data, series }) => {
614614
const defaultSelection = [0, width >> 2];
615615
const prevBrush = React.useRef(defaultSelection);
616616
const MIN_OVERVIEW = 10;
617+
let startX;
618+
619+
function brushstarted() {
620+
const [x1, x2] = d3.event.selection;
621+
622+
if (x1 === x2) {
623+
startX = x1;
624+
} else {
625+
startX = null;
626+
}
627+
}
617628

618629
function brushed() {
619630
if (d3.event.selection && !checkD3EventLoop('brush') && !checkD3EventLoop('wheel')) {
@@ -636,11 +647,31 @@ const Overview = observer(({ item, data, series }) => {
636647
end = mid + item.zoomedRange / 2;
637648
// if overview was resized
638649
} else if (overviewWidth < MIN_OVERVIEW) {
650+
if (prev[0] !== x1 && prev[1] !== x2) {
651+
if (prev[0] === x2 || prev[1] === x1) {
652+
// This may happen after sides swap
653+
// so we swap prev as well
654+
[prev[0], prev[1]] = [prev[1], prev[0]];
655+
} else {
656+
// This may happen at begining when range was not enough wide yet
657+
if (x1 === startX) {
658+
x2 = Math.min(width, x1 + MIN_OVERVIEW);
659+
x1 = Math.max(0, x2 - MIN_OVERVIEW);
660+
} else {
661+
x1 = Math.max(0, x2 - MIN_OVERVIEW);
662+
x2 = Math.min(width, x1 + MIN_OVERVIEW);
663+
}
664+
}
665+
}
639666
if (prev[0] === x1) {
640667
x2 = Math.min(width, x1 + MIN_OVERVIEW);
668+
x1 = Math.max(0, x2 - MIN_OVERVIEW);
641669
} else if (prev[1] === x2) {
642670
x1 = Math.max(0, x2 - MIN_OVERVIEW);
671+
x2 = Math.min(width, x1 + MIN_OVERVIEW);
643672
}
673+
start = +x.invert(x1);
674+
end = +x.invert(x2);
644675
// change the data range, but keep min-width for overview
645676
gb.current.call(brush.move, [x1, x2]);
646677
}
@@ -669,6 +700,7 @@ const Overview = observer(({ item, data, series }) => {
669700
[0, 0],
670701
[width, focusHeight],
671702
])
703+
.on('start', brushstarted)
672704
.on('brush', brushed)
673705
.on('end', brushended);
674706

0 commit comments

Comments
 (0)