New sampling algorithm shows very few points when zoomed out
[WebKit-https.git] / Websites / perf.webkit.org / browser-tests / time-series-chart-tests.js
1 const scripts = [
2     '../shared/statistics.js',
3     'instrumentation.js',
4     'models/data-model.js',
5     'models/metric.js',
6     'models/time-series.js',
7     'models/measurement-set.js',
8     'models/measurement-cluster.js',
9     'models/measurement-adaptor.js',
10     'components/base.js',
11     'components/time-series-chart.js',
12     'components/interactive-time-series-chart.js'];
13
14 function posixTime(string) { return +new Date(string); }
15
16 const dayInMilliseconds = 24 * 3600 * 1000;
17
18 const sampleCluster = {
19     "clusterStart": posixTime('2016-01-01T00:00:00Z'),
20     "clusterSize": 7 * dayInMilliseconds,
21     "startTime": posixTime('2016-01-01T00:00:00Z'),
22     "endTime": posixTime('2016-01-08T00:00:00Z'),
23     "lastModified": posixTime('2016-01-18T00:00:00Z'),
24     "clusterCount": 1,
25     "status": "OK",
26     "formatMap": [
27         "id", "mean", "iterationCount", "sum", "squareSum", "markedOutlier",
28         "revisions",
29         "commitTime", "build", "buildTime", "buildNumber", "builder"
30     ],
31     "configurations": {
32         "current": [
33             [
34                 1000, 100, 1, 100, 100, false,
35                 [ [ 2000, 1, "4000", posixTime('2016-01-05T17:35:00Z')] ],
36                 posixTime('2016-01-05T17:35:00Z'), 5000, posixTime('2016-01-05T19:23:00Z'), "10", 7
37             ],
38             [
39                 1001, 131, 1, 131, 131, true,
40                 [ [ 2001, 1, "4001", posixTime('2016-01-05T18:43:01Z')] ],
41                 posixTime('2016-01-05T18:43:01Z'), 5001, posixTime('2016-01-05T20:58:01Z'), "11", 7
42             ],
43             [
44                 1002, 122, 1, 122, 122, false,
45                 [ [ 2002, 1, "4002", posixTime('2016-01-05T20:01:02Z') ] ],
46                 posixTime('2016-01-05T20:01:02Z'), 5002, posixTime('2016-01-05T22:37:02Z'), "12", 7
47             ],
48             [
49                 1003, 113, 1, 113, 113, false,
50                 [ [ 2003, 1, "4003", posixTime('2016-01-05T23:19:03Z') ] ],
51                 posixTime('2016-01-05T23:19:03Z'), 5003, posixTime('2016-01-06T23:19:03Z'), "13", 7
52             ],
53             [
54                 1004, 124, 1, 124, 124, false,
55                 [ [ 2004, 1, "4004", posixTime('2016-01-06T01:52:04Z') ] ],
56                 posixTime('2016-01-06T01:52:04Z'), 5004, posixTime('2016-01-06T02:42:04Z'), "14", 7
57             ],
58             [
59                 1005, 115, 1, 115, 115, true,
60                 [ [ 2005, 1, "4005", posixTime('2016-01-06T03:22:05Z') ] ],
61                 posixTime('2016-01-06T03:22:05Z'), 5005, posixTime('2016-01-06T06:01:05Z'), "15", 7
62             ],
63             [
64                 1006, 116, 1, 116, 116, false,
65                 [ [ 2006, 1, "4006", posixTime('2016-01-06T05:59:06Z') ] ],
66                 posixTime('2016-01-06T05:59:06Z'), 5006, posixTime('2016-01-06T08:34:06Z'), "16", 7
67             ]
68         ]
69     },
70 };
71
72 function createChartWithSampleCluster(context, chartOptions = {}, options = {})
73 {
74     const TimeSeriesChart = context.symbols[options.interactiveChart ? 'InteractiveTimeSeriesChart' : 'TimeSeriesChart'];
75     const MeasurementSet = context.symbols.MeasurementSet;
76
77     const chart = new TimeSeriesChart([
78         {
79             type: 'current',
80             lineStyle: options.lineStyle || '#666',
81             measurementSet: MeasurementSet.findSet(1, 1, 0),
82             interactive: options.interactive || false,
83             includeOutliers: options.includeOutliers || false,
84             sampleData: options.sampleData || false,
85         }], chartOptions);
86     const element = chart.element();
87     element.style.width = options.width || '300px';
88     element.style.height = options.height || '100px';
89     context.document.body.appendChild(element);
90
91     return chart;
92 }
93
94 function respondWithSampleCluster(request)
95 {
96     expect(request.url).to.be('../data/measurement-set-1-1.json');
97     expect(request.method).to.be('GET');
98     request.resolve(sampleCluster);
99 }
100
101 describe('TimeSeriesChart', () => {
102
103     it('should be constructible with an empty sourec list and an empty options', () => {
104         return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
105             new TimeSeriesChart([], {});
106         });
107     });
108
109     describe('computeTimeGrid', () => {
110         it('should return an empty array when the start and the end times are identical', () => {
111             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
112                 const someTime = Date.now();
113                 const labels = TimeSeriesChart.computeTimeGrid(someTime, someTime, 0);
114                 expect(labels).to.be.a('array');
115                 expect(labels.length).to.be(0);
116             });
117         });
118
119         const millisecondsPerHour = 3600 * 1000;
120         const millisecondsPerDay = 24 * millisecondsPerHour;
121
122         it('should return an empty array when maxLabels is 0', () => {
123             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
124                 const endTime = Date.now();
125                 const labels = TimeSeriesChart.computeTimeGrid(endTime - millisecondsPerDay, endTime, 0);
126                 expect(labels).to.be.a('array');
127                 expect(labels.length).to.be(0);
128             });
129         });
130
131         it('should return an empty array when maxLabels is 0 even when the interval spans multiple months', () => {
132             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
133                 const endTime = Date.now();
134                 const labels = TimeSeriesChart.computeTimeGrid(endTime - 120 * millisecondsPerDay, endTime, 0);
135                 expect(labels).to.be.a('array');
136                 expect(labels.length).to.be(0);
137             });
138         });
139
140         function checkGridItem(item, label, expectedDate)
141         {
142             expect(item.label).to.be(label);
143             expect(item.time.__proto__.constructor.name).to.be('Date');
144             expect(+item.time).to.be(+new Date(expectedDate));
145         }
146
147         it('should generate one hour label with just day for two hour interval when maxLabels is 1', () => {
148             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
149                 const endTime = new Date('2017-01-15T07:53:00Z');
150                 const labels = TimeSeriesChart.computeTimeGrid(endTime - 2 * millisecondsPerHour, +endTime, 1);
151                 expect(labels).to.be.a('array');
152                 expect(labels.length).to.be(1);
153                 checkGridItem(labels[0], '6AM', '2017-01-15T06:00:00Z');
154             });
155         });
156
157         it('should generate two two-hour labels for four hour interval when maxLabels is 2', () => {
158             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
159                 const endTime = new Date('2017-01-15T07:53:00Z');
160                 const labels = TimeSeriesChart.computeTimeGrid(endTime - 4 * millisecondsPerHour, +endTime, 2);
161                 expect(labels).to.be.a('array');
162                 expect(labels.length).to.be(2);
163                 checkGridItem(labels[0], '4AM', '2017-01-15T04:00:00Z');
164                 checkGridItem(labels[1], '6AM', '2017-01-15T06:00:00Z');
165             });
166         });
167
168         it('should generate six two-hour labels for twelve hour interval when maxLabels is 6', () => {
169             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
170                 const endTime = new Date('2017-01-15T07:53:00Z');
171                 const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 12 * millisecondsPerHour, 6);
172                 expect(labels).to.be.a('array');
173                 expect(labels.length).to.be(6);
174                 checkGridItem(labels[0], '8AM', '2017-01-15T08:00:00Z');
175                 checkGridItem(labels[1], '10AM', '2017-01-15T10:00:00Z');
176                 checkGridItem(labels[2], '12PM', '2017-01-15T12:00:00Z');
177                 checkGridItem(labels[3], '2PM', '2017-01-15T14:00:00Z');
178                 checkGridItem(labels[4], '4PM', '2017-01-15T16:00:00Z');
179                 checkGridItem(labels[5], '6PM', '2017-01-15T18:00:00Z');
180             });
181         });
182
183         it('should generate six two-hour labels with one date label for twelve hour interval that cross a day when maxLabels is 6', () => {
184             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
185                 const endTime = new Date('2017-01-15T16:12:00Z');
186                 const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 12 * millisecondsPerHour, 6);
187                 expect(labels).to.be.a('array');
188                 expect(labels.length).to.be(6);
189                 checkGridItem(labels[0], '6PM', '2017-01-15T18:00:00Z');
190                 checkGridItem(labels[1], '8PM', '2017-01-15T20:00:00Z');
191                 checkGridItem(labels[2], '10PM', '2017-01-15T22:00:00Z');
192                 checkGridItem(labels[3], '1/16', '2017-01-16T00:00:00Z');
193                 checkGridItem(labels[4], '2AM', '2017-01-16T02:00:00Z');
194                 checkGridItem(labels[5], '4AM', '2017-01-16T04:00:00Z');
195             });
196         });
197
198         it('should generate three two-hour labels for six hour interval that cross a year when maxLabels is 5', () => {
199             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
200                 const endTime = new Date('2016-12-31T21:37:00Z');
201                 const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 6 * millisecondsPerHour, 5);
202                 expect(labels).to.be.a('array');
203                 expect(labels.length).to.be(3);
204                 checkGridItem(labels[0], '10PM', '2016-12-31T22:00:00Z');
205                 checkGridItem(labels[1], '1/1', '2017-01-01T00:00:00Z');
206                 checkGridItem(labels[2], '2AM', '2017-01-01T02:00:00Z');
207             });
208         });
209
210         it('should generate one one-day label for one day interval when maxLabels is 1', () => {
211             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
212                 const endTime = new Date('2017-01-15T07:53:00Z');
213                 const labels = TimeSeriesChart.computeTimeGrid(endTime - millisecondsPerDay, +endTime, 1);
214                 expect(labels).to.be.a('array');
215                 expect(labels.length).to.be(1);
216                 checkGridItem(labels[0], '1/15', '2017-01-15T00:00:00Z');
217             });
218         });
219
220         it('should generate two one-day labels for one day interval when maxLabels is 2', () => {
221             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
222                 const endTime = new Date('2017-01-15T07:53:00Z');
223                 const labels = TimeSeriesChart.computeTimeGrid(endTime - millisecondsPerDay, +endTime, 2);
224                 expect(labels).to.be.a('array');
225                 expect(labels.length).to.be(2);
226                 checkGridItem(labels[0], '1/14 12PM', '2017-01-14T12:00:00Z');
227                 checkGridItem(labels[1], '1/15', '2017-01-15T00:00:00Z');
228             });
229         });
230
231         it('should generate four half-day labels for two day interval when maxLabels is 5', () => {
232             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
233                 const endTime = new Date('2017-01-15T16:12:00Z');
234                 const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 2 * millisecondsPerDay, 5);
235                 expect(labels).to.be.a('array');
236                 expect(labels.length).to.be(4);
237                 checkGridItem(labels[0], '1/16', '2017-01-16T00:00:00Z');
238                 checkGridItem(labels[1], '12PM', '2017-01-16T12:00:00Z');
239                 checkGridItem(labels[2], '1/17', '2017-01-17T00:00:00Z');
240                 checkGridItem(labels[3], '12PM', '2017-01-17T12:00:00Z');
241             });
242         });
243
244         it('should generate four half-day labels for two day interval that cross a year when maxLabels is 5', () => {
245             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
246                 const endTime = new Date('2016-12-31T09:12:00Z');
247                 const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 2 * millisecondsPerDay, 5);
248                 expect(labels).to.be.a('array');
249                 expect(labels.length).to.be(4);
250                 checkGridItem(labels[0], '12/31 12PM', '2016-12-31T12:00:00Z');
251                 checkGridItem(labels[1], '1/1', '2017-01-01T00:00:00Z');
252                 checkGridItem(labels[2], '12PM', '2017-01-01T12:00:00Z');
253                 checkGridItem(labels[3], '1/2', '2017-01-02T00:00:00Z');
254             });
255         });
256
257         it('should generate seven per-day labels for one week interval when maxLabels is 10', () => {
258             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
259                 const endTime = new Date('2017-01-15T07:53:00Z');
260                 const labels = TimeSeriesChart.computeTimeGrid(endTime - 7 * millisecondsPerDay, endTime, 10);
261                 expect(labels).to.be.a('array');
262                 expect(labels.length).to.be(7);
263                 checkGridItem(labels[0], '1/9', '2017-01-09T00:00:00Z');
264                 checkGridItem(labels[1], '1/10', '2017-01-10T00:00:00Z');
265                 checkGridItem(labels[2], '1/11', '2017-01-11T00:00:00Z');
266                 checkGridItem(labels[3], '1/12', '2017-01-12T00:00:00Z');
267                 checkGridItem(labels[4], '1/13', '2017-01-13T00:00:00Z');
268                 checkGridItem(labels[5], '1/14', '2017-01-14T00:00:00Z');
269                 checkGridItem(labels[6], '1/15', '2017-01-15T00:00:00Z');
270             });
271         });
272
273         it('should generate three two-day labels for one week interval when maxLabels is 4', () => {
274             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
275                 const endTime = new Date('2017-01-15T07:53:00Z');
276                 const labels = TimeSeriesChart.computeTimeGrid(endTime - 7 * millisecondsPerDay, endTime, 4);
277                 expect(labels).to.be.a('array');
278                 expect(labels.length).to.be(3);
279                 checkGridItem(labels[0], '1/10', '2017-01-10T00:00:00Z');
280                 checkGridItem(labels[1], '1/12', '2017-01-12T00:00:00Z');
281                 checkGridItem(labels[2], '1/14', '2017-01-14T00:00:00Z');
282             });
283         });
284
285         it('should generate seven one-day labels for two week interval when maxLabels is 8', () => {
286             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
287                 const endTime = new Date('2017-01-15T18:53:00Z');
288                 const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 14 * millisecondsPerDay, 8);
289                 expect(labels).to.be.a('array');
290                 expect(labels.length).to.be(7);
291                 checkGridItem(labels[0], '1/17', '2017-01-17T00:00:00Z');
292                 checkGridItem(labels[1], '1/19', '2017-01-19T00:00:00Z');
293                 checkGridItem(labels[2], '1/21', '2017-01-21T00:00:00Z');
294                 checkGridItem(labels[3], '1/23', '2017-01-23T00:00:00Z');
295                 checkGridItem(labels[4], '1/25', '2017-01-25T00:00:00Z');
296                 checkGridItem(labels[5], '1/27', '2017-01-27T00:00:00Z');
297                 checkGridItem(labels[6], '1/29', '2017-01-29T00:00:00Z');
298             });
299         });
300
301         it('should generate two one-week labels for two week interval when maxLabels is 3', () => {
302             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
303                 const endTime = new Date('2017-01-15T18:53:00Z');
304                 const labels = TimeSeriesChart.computeTimeGrid(+endTime, +endTime + 14 * millisecondsPerDay, 3);
305                 expect(labels).to.be.a('array');
306                 expect(labels.length).to.be(2);
307                 checkGridItem(labels[0], '1/22', '2017-01-22T00:00:00Z');
308                 checkGridItem(labels[1], '1/29', '2017-01-29T00:00:00Z');
309             });
310         });
311
312         it('should generate seven one-month labels for six and half months interval starting before 15th when maxLabels is 7', () => {
313             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
314                 const endTime = new Date('2017-01-15T18:53:00Z');
315                 const labels = TimeSeriesChart.computeTimeGrid(new Date('2016-07-12T18:53:00Z'), new Date('2017-01-18T08:17:53Z'), 7);
316                 expect(labels).to.be.a('array');
317                 expect(labels.length).to.be(7);
318                 checkGridItem(labels[0], '7/15', '2016-07-15T00:00:00Z');
319                 checkGridItem(labels[1], '8/15', '2016-08-15T00:00:00Z');
320                 checkGridItem(labels[2], '9/15', '2016-09-15T00:00:00Z');
321                 checkGridItem(labels[3], '10/15', '2016-10-15T00:00:00Z');
322                 checkGridItem(labels[4], '11/15', '2016-11-15T00:00:00Z');
323                 checkGridItem(labels[5], '12/15', '2016-12-15T00:00:00Z');
324                 checkGridItem(labels[6], '1/15', '2017-01-15T00:00:00Z');
325             });
326         });
327
328         it('should generate seven one-month labels for six months interval staring after 15th when maxLabels is 7', () => {
329             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
330                 const endTime = new Date('2017-01-15T18:53:00Z');
331                 const labels = TimeSeriesChart.computeTimeGrid(new Date('2016-07-18T18:53:00Z'), new Date('2017-01-18T08:17:53Z'), 7);
332                 expect(labels).to.be.a('array');
333                 expect(labels.length).to.be(6);
334                 checkGridItem(labels[0], '8/1', '2016-08-01T00:00:00Z');
335                 checkGridItem(labels[1], '9/1', '2016-09-01T00:00:00Z');
336                 checkGridItem(labels[2], '10/1', '2016-10-01T00:00:00Z');
337                 checkGridItem(labels[3], '11/1', '2016-11-01T00:00:00Z');
338                 checkGridItem(labels[4], '12/1', '2016-12-01T00:00:00Z');
339             });
340         });
341
342         it('should generate six two-months labels for one year interval when maxLabels is 7', () => {
343             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
344                 const endTime = new Date('2017-01-15T18:53:00Z');
345                 const labels = TimeSeriesChart.computeTimeGrid(new Date('2016-07-11T18:53:00Z'), new Date('2017-07-27T08:17:53Z'), 7);
346                 expect(labels).to.be.a('array');
347                 expect(labels.length).to.be(6);
348                 checkGridItem(labels[0], '9/1', '2016-09-01T00:00:00Z');
349                 checkGridItem(labels[1], '11/1', '2016-11-01T00:00:00Z');
350                 checkGridItem(labels[2], '1/1', '2017-01-01T00:00:00Z');
351                 checkGridItem(labels[3], '3/1', '2017-03-01T00:00:00Z');
352                 checkGridItem(labels[4], '5/1', '2017-05-01T00:00:00Z');
353                 checkGridItem(labels[5], '7/1', '2017-07-01T00:00:00Z');
354             });
355         });
356
357         it('should generate four three-months labels for one year interval when maxLabels is 5', () => {
358             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart').then((TimeSeriesChart) => {
359                 const endTime = new Date('2017-01-15T18:53:00Z');
360                 const labels = TimeSeriesChart.computeTimeGrid(new Date('2016-07-11T18:53:00Z'), new Date('2017-07-27T08:17:53Z'), 5);
361                 expect(labels).to.be.a('array');
362                 expect(labels.length).to.be(4);
363                 checkGridItem(labels[0], '10/1', '2016-10-01T00:00:00Z');
364                 checkGridItem(labels[1], '1/1', '2017-01-01T00:00:00Z');
365                 checkGridItem(labels[2], '4/1', '2017-04-01T00:00:00Z');
366                 checkGridItem(labels[3], '7/1', '2017-07-01T00:00:00Z');
367             });
368         });
369     });
370
371     describe('computeValueGrid', () => {
372
373         function checkValueGrid(actual, expected) {
374             expect(actual).to.be.a('array');
375             expect(JSON.stringify(actual)).to.be(JSON.stringify(expected));
376         }
377
378         function approximate(number)
379         {
380             return Math.round(number * 100000000) / 100000000;
381         }
382
383         it('should generate [0.5, 1.0, 1.5, 2.0] for [0.3, 2.3] when maxLabels is 5', () => {
384             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
385                 const [TimeSeriesChart, Metric] = symbols;
386                 const grid = TimeSeriesChart.computeValueGrid(0.3, 2.3, 5, Metric.makeFormatter('pt', 2));
387                 expect(grid.map((item) => approximate(item.value))).to.eql([0.5, 1.0, 1.5, 2.0]);
388                 expect(grid.map((item) => item.label)).to.eql(['0.5 pt', '1.0 pt', '1.5 pt', '2.0 pt']);
389             });
390         });
391
392         it('should generate [0.4, 0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8, 2, 2.2] for [0.3, 2.3] when maxLabels is 10', () => {
393             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
394                 const [TimeSeriesChart, Metric] = symbols;
395                 const grid = TimeSeriesChart.computeValueGrid(0.3, 2.3, 10, Metric.makeFormatter('pt', 2));
396                 expect(grid.map((item) => approximate(item.value))).to.eql([0.4, 0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8, 2, 2.2]);
397                 expect(grid.map((item) => item.label)).to.eql(['0.4 pt', '0.6 pt', '0.8 pt', '1.0 pt', '1.2 pt', '1.4 pt', '1.6 pt', '1.8 pt', '2.0 pt', '2.2 pt']);
398             });
399         });
400
401         it('should generate [1, 2] for [0.3, 2.3] when maxLabels is 2', () => {
402             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
403                 const [TimeSeriesChart, Metric] = symbols;
404                 const grid = TimeSeriesChart.computeValueGrid(0.3, 2.3, 2, Metric.makeFormatter('pt', 2));
405                 expect(grid.map((item) => item.value)).to.eql([1, 2]);
406                 expect(grid.map((item) => item.label)).to.eql(['1.0 pt', '2.0 pt']);
407             });
408         });
409
410         it('should generate [0.4, 0.6, 0.8, 1.0, 1.2] for [0.3, 1.3] when maxLabels is 5', () => {
411             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
412                 const [TimeSeriesChart, Metric] = symbols;
413                 const grid = TimeSeriesChart.computeValueGrid(0.3, 1.3, 5, Metric.makeFormatter('pt', 2));
414                 expect(grid.map((item) => approximate(item.value))).to.eql([0.4, 0.6, 0.8, 1.0, 1.2]);
415                 expect(grid.map((item) => item.label)).to.eql(['0.4 pt', '0.6 pt', '0.8 pt', '1.0 pt', '1.2 pt']);
416             });
417         });
418
419         it('should generate [0.2, 0.4, 0.6, 0.8, 1, 1.2] for [0.2, 1.3] when maxLabels is 10', () => {
420             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
421                 const [TimeSeriesChart, Metric] = symbols;
422                 const grid = TimeSeriesChart.computeValueGrid(0.2, 1.3, 10, Metric.makeFormatter('pt', 2));
423                 expect(grid.map((item) => approximate(item.value))).to.eql([0.2, 0.4, 0.6, 0.8, 1, 1.2]);
424                 expect(grid.map((item) => item.label)).to.eql(['0.2 pt', '0.4 pt', '0.6 pt', '0.8 pt', '1.0 pt', '1.2 pt']);
425             });
426         });
427
428         it('should generate [0.5, 1.0] for [0.3, 1.3] when maxLabels is 4', () => {
429             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
430                 const [TimeSeriesChart, Metric] = symbols;
431                 const grid = TimeSeriesChart.computeValueGrid(0.3, 1.3, 4, Metric.makeFormatter('pt', 2));
432                 expect(grid.map((item) => approximate(item.value))).to.eql([0.5, 1.0]);
433                 expect(grid.map((item) => item.label)).to.eql(['0.5 pt', '1.0 pt']);
434             });
435         });
436
437         it('should generate [10, 20, 30] for [4, 35] when maxLabels is 4', () => {
438             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
439                 const [TimeSeriesChart, Metric] = symbols;
440                 const grid = TimeSeriesChart.computeValueGrid(4, 35, 4, Metric.makeFormatter('pt', 2));
441                 expect(grid.map((item) => item.value)).to.eql([10, 20, 30]);
442                 expect(grid.map((item) => item.label)).to.eql(['10 pt', '20 pt', '30 pt']);
443             });
444         });
445
446         it('should generate [10, 20, 30] for [4, 35] when maxLabels is 6', () => {
447             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
448                 const [TimeSeriesChart, Metric] = symbols;
449                 const grid = TimeSeriesChart.computeValueGrid(4, 35, 6, Metric.makeFormatter('pt', 2));
450                 expect(grid.map((item) => item.value)).to.eql([10, 20, 30]);
451                 expect(grid.map((item) => item.label)).to.eql(['10 pt', '20 pt', '30 pt']);
452             });
453         });
454
455         it('should generate [10, 15, 20, 25, 30, 35] for [6, 35] when maxLabels is 6', () => {
456             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
457                 const [TimeSeriesChart, Metric] = symbols;
458                 const grid = TimeSeriesChart.computeValueGrid(6, 35, 6, Metric.makeFormatter('pt', 2));
459                 expect(grid.map((item) => item.value)).to.eql([10, 15, 20, 25, 30, 35]);
460                 expect(grid.map((item) => item.label)).to.eql(['10 pt', '15 pt', '20 pt', '25 pt', '30 pt', '35 pt']);
461             });
462         });
463
464         it('should generate [110, 115, 120, 125, 130] for [107, 134] when maxLabels is 6', () => {
465             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
466                 const [TimeSeriesChart, Metric] = symbols;
467                 const grid = TimeSeriesChart.computeValueGrid(107, 134, 6, Metric.makeFormatter('pt', 3));
468                 expect(grid.map((item) => item.value)).to.eql([110, 115, 120, 125, 130]);
469                 expect(grid.map((item) => item.label)).to.eql(['110 pt', '115 pt', '120 pt', '125 pt', '130 pt']);
470             });
471         });
472
473         it('should generate [5e7, 10e7] for [1e7, 1e8] when maxLabels is 4', () => {
474             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
475                 const [TimeSeriesChart, Metric] = symbols;
476                 const grid = TimeSeriesChart.computeValueGrid(1e7, 1e8, 4, Metric.makeFormatter('pt', 3));
477                 expect(grid.map((item) => item.value)).to.eql([5e7, 10e7]);
478                 expect(grid.map((item) => item.label)).to.eql(['50.0 Mpt', '100 Mpt']);
479             });
480         });
481
482         it('should generate [2e7, 4e7, 6e7, 8e7, 10e7] for [1e7, 1e8] when maxLabels is 5', () => {
483             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
484                 const [TimeSeriesChart, Metric] = symbols;
485                 const grid = TimeSeriesChart.computeValueGrid(1e7, 1e8, 5, Metric.makeFormatter('pt', 3));
486                 expect(grid.map((item) => item.value)).to.eql([2e7, 4e7, 6e7, 8e7, 10e7]);
487                 expect(grid.map((item) => item.label)).to.eql(['20.0 Mpt', '40.0 Mpt', '60.0 Mpt', '80.0 Mpt', '100 Mpt']);
488             });
489         });
490
491         it('should generate [-1.5, -1.0, -0.5, 0.0, 0.5] for [-1.8, 0.7] when maxLabels is 5', () => {
492             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
493                 const [TimeSeriesChart, Metric] = symbols;
494                 const grid = TimeSeriesChart.computeValueGrid(-1.8, 0.7, 5, Metric.makeFormatter('pt', 2));
495                 expect(grid.map((item) => approximate(item.value))).to.eql([-1.5, -1.0, -0.5, 0.0, 0.5]);
496                 expect(grid.map((item) => item.label)).to.eql(['-1.5 pt', '-1.0 pt', '-0.5 pt', '0.0 pt', '0.5 pt']);
497             });
498         });
499
500         it('should generate [200ms, 400ms, 600ms, 800ms, 1.00s, 1.20s] for [0.2, 1.3] when maxLabels is 10 and unit is seconds', () => {
501             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
502                 const [TimeSeriesChart, Metric] = symbols;
503                 const grid = TimeSeriesChart.computeValueGrid(0.2, 1.3, 10, Metric.makeFormatter('s', 3));
504                 expect(grid.map((item) => approximate(item.value))).to.eql([0.2, 0.4, 0.6, 0.8, 1, 1.2]);
505                 expect(grid.map((item) => item.label)).to.eql(['200 ms', '400 ms', '600 ms', '800 ms', '1.00 s', '1.20 s']);
506             });
507         });
508
509         it('should generate [2.0GB, 4.0GB, 6.0GB] for [1.2GB, 7.2GB] when maxLabels is 4 and unit is bytes', () => {
510             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
511                 const [TimeSeriesChart, Metric] = symbols;
512                 const gigabytes = Math.pow(1024, 3);
513                 const grid = TimeSeriesChart.computeValueGrid(1.2 * gigabytes, 7.2 * gigabytes, 4, Metric.makeFormatter('B', 2));
514                 expect(grid.map((item) => approximate(item.value))).to.eql([2 * gigabytes, 4 * gigabytes, 6 * gigabytes]);
515                 expect(grid.map((item) => item.label)).to.eql(['2.0 GB', '4.0 GB', '6.0 GB']);
516             });
517         });
518
519         it('should generate [0.6GB, 0.8GB, 1.0GB, 1.2GB] for [0.53GB, 1.23GB] when maxLabels is 4 and unit is bytes', () => {
520             return new BrowsingContext().importScripts(scripts, 'TimeSeriesChart', 'Metric').then((symbols) => {
521                 const [TimeSeriesChart, Metric] = symbols;
522                 const gigabytes = Math.pow(1024, 3);
523                 const grid = TimeSeriesChart.computeValueGrid(0.53 * gigabytes, 1.23 * gigabytes, 4, Metric.makeFormatter('B', 2));
524                 expect(grid.map((item) => item.label)).to.eql(['0.6 GB', '0.8 GB', '1.0 GB', '1.2 GB']);
525             });
526         });
527
528     });
529
530     describe('fetchMeasurementSets', () => {
531
532         it('should fetch the measurement set and create a canvas element upon receiving the data', () => {
533             const context = new BrowsingContext();
534             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
535                 const chart = createChartWithSampleCluster(context);
536
537                 chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
538                 chart.fetchMeasurementSets();
539
540                 const requests = context.symbols.MockRemoteAPI.requests;
541                 expect(requests.length).to.be(1);
542                 respondWithSampleCluster(requests[0]);
543
544                 expect(chart.content().querySelector('canvas')).to.be(null);
545                 return waitForComponentsToRender(context).then(() => {
546                     expect(chart.content().querySelector('canvas')).to.not.be(null);
547                 });
548             });
549         });
550
551         it('should immediately enqueue to render when the measurement set had already been fetched', () => {
552             const context = new BrowsingContext();
553             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
554                 const chart = createChartWithSampleCluster(context);
555
556                 let set = context.symbols.MeasurementSet.findSet(1, 1, 0);
557                 let promise = set.fetchBetween(sampleCluster.startTime, sampleCluster.endTime);
558
559                 const requests = context.symbols.MockRemoteAPI.requests;
560                 expect(requests.length).to.be(1);
561                 respondWithSampleCluster(requests[0]);
562
563                 return promise.then(() => {
564                     expect(chart.content().querySelector('canvas')).to.be(null);
565                     chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
566                     chart.fetchMeasurementSets();
567                     return waitForComponentsToRender(context);
568                 }).then(() => {
569                     expect(requests.length).to.be(1);
570                     expect(chart.content().querySelector('canvas')).to.not.be(null);
571                 });
572             });
573         });
574
575         it('should dispatch "dataChange" action once the fetched data becomes available', () => {
576             const context = new BrowsingContext();
577             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
578                 const chart = createChartWithSampleCluster(context);
579
580                 let dataChangeCount = 0;
581                 chart.listenToAction('dataChange', () => dataChangeCount++);
582
583                 chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
584                 chart.fetchMeasurementSets();
585
586                 const requests = context.symbols.MockRemoteAPI.requests;
587                 expect(requests.length).to.be(1);
588                 respondWithSampleCluster(requests[0]);
589
590                 expect(dataChangeCount).to.be(0);
591                 expect(chart.sampledTimeSeriesData('current')).to.be(null);
592                 expect(chart.content().querySelector('canvas')).to.be(null);
593                 return waitForComponentsToRender(context).then(() => {
594                     expect(dataChangeCount).to.be(1);
595                     expect(chart.sampledTimeSeriesData('current')).to.not.be(null);
596                     expect(chart.content().querySelector('canvas')).to.not.be(null);
597                 });
598             });
599         });
600     });
601
602     describe('sampledTimeSeriesData', () => {
603         it('should not contain an outlier when includeOutliers is false', () => {
604             const context = new BrowsingContext();
605             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
606                 const chart = createChartWithSampleCluster(context, {}, {includeOutliers: false});
607
608                 chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
609                 chart.fetchMeasurementSets();
610                 respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
611
612                 return waitForComponentsToRender(context).then(() => {
613                     const view = chart.sampledTimeSeriesData('current');
614                     expect(view.length()).to.be(5);
615                     for (let point of view)
616                         expect(point.markedOutlier).to.be(false);
617                 });
618             });
619         });
620
621         it('should contain every outlier when includeOutliers is true', () => {
622             const context = new BrowsingContext();
623             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
624                 const chart = createChartWithSampleCluster(context, {}, {includeOutliers: true});
625
626                 chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
627                 chart.fetchMeasurementSets();
628                 respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
629
630                 return waitForComponentsToRender(context).then(() => {
631                     const view = chart.sampledTimeSeriesData('current');
632                     expect(view.length()).to.be(7);
633                     expect(view.findPointByIndex(1).markedOutlier).to.be(true);
634                     expect(view.findPointByIndex(5).markedOutlier).to.be(true);
635                 });
636             });
637         });
638
639         it('should only contain data points in the domain and one preceding point when there are no succeeding points', () => {
640             const context = new BrowsingContext();
641             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
642                 const chart = createChartWithSampleCluster(context, {}, {includeOutliers: true});
643
644                 chart.setDomain(posixTime('2016-01-06T00:00:00Z'), posixTime('2016-01-07T00:00:00Z'));
645                 chart.fetchMeasurementSets();
646                 respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
647
648                 return waitForComponentsToRender(context).then(() => {
649                     const view = chart.sampledTimeSeriesData('current');
650                     expect([...view].map((point) => point.id)).to.be.eql([1003, 1004, 1005, 1006]);
651                 });
652             });
653         });
654
655         it('should only contain data points in the domain and one succeeding point when there are no preceding points', () => {
656             const context = new BrowsingContext();
657             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
658                 const chart = createChartWithSampleCluster(context, {}, {includeOutliers: true});
659
660                 chart.setDomain(posixTime('2016-01-05T00:00:00Z'), posixTime('2016-01-06T00:00:00Z'));
661                 chart.fetchMeasurementSets();
662                 chart.fetchMeasurementSets();
663                 respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
664
665                 return waitForComponentsToRender(context).then(() => {
666                     const view = chart.sampledTimeSeriesData('current');
667                     expect([...view].map((point) => point.id)).to.be.eql([1000, 1001, 1002, 1003, 1004]);
668                 });
669             });
670         });
671
672         it('should only contain data points in the domain and one preceding point and one succeeding point', () => {
673             const context = new BrowsingContext();
674             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
675                 const chart = createChartWithSampleCluster(context, {}, {includeOutliers: true});
676
677                 chart.setDomain(posixTime('2016-01-05T21:00:00Z'), posixTime('2016-01-06T02:00:00Z'));
678                 chart.fetchMeasurementSets();
679                 respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
680
681                 return waitForComponentsToRender(context).then(() => {
682                     const view = chart.sampledTimeSeriesData('current');
683                     expect([...view].map((point) => point.id)).to.be.eql([1002, 1003, 1004, 1005]);
684                 });
685             });
686         });
687     });
688
689     describe('render', () => {
690         it('should update the canvas size and its content after the window has been resized', () => {
691             const context = new BrowsingContext();
692             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
693                 const chart = createChartWithSampleCluster(context, {}, {width: '100%', height: '100%'});
694
695                 let dataChangeCount = 0;
696                 chart.listenToAction('dataChange', () => dataChangeCount++);
697
698                 chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
699                 chart.fetchMeasurementSets();
700
701                 const requests = context.symbols.MockRemoteAPI.requests;
702                 expect(requests.length).to.be(1);
703                 respondWithSampleCluster(requests[0]);
704
705                 expect(dataChangeCount).to.be(0);
706                 expect(chart.sampledTimeSeriesData('current')).to.be(null);
707                 expect(chart.content().querySelector('canvas')).to.be(null);
708                 let canvas;
709                 let originalWidth;
710                 let originalHeight;
711                 return waitForComponentsToRender(context).then(() => {
712                     expect(dataChangeCount).to.be(1);
713                     expect(chart.sampledTimeSeriesData('current')).to.not.be(null);
714                     canvas = chart.content().querySelector('canvas');
715                     expect(canvas).to.not.be(null);
716
717                     originalWidth = canvas.offsetWidth;
718                     originalHeight = canvas.offsetHeight;
719                     expect(originalWidth).to.be(context.document.body.offsetWidth);
720                     expect(originalHeight).to.be(context.document.body.offsetHeight);
721
722                     CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
723                     context.iframe.style.width = context.iframe.offsetWidth * 2 + 'px';
724                     context.global.dispatchEvent(new Event('resize'));
725
726                     expect(canvas.offsetWidth).to.be(originalWidth);
727                     expect(canvas.offsetHeight).to.be(originalHeight);
728
729                     return waitForComponentsToRender(context);
730                 }).then(() => {
731                     expect(dataChangeCount).to.be(2);
732                     expect(canvas.offsetWidth).to.be.greaterThan(originalWidth);
733                     expect(canvas.offsetWidth).to.be(context.document.body.offsetWidth);
734                     expect(canvas.offsetHeight).to.be(originalHeight);
735                     expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
736                 });
737             });
738         });
739
740         it('should not update update the canvas when the window has been resized but its dimensions stays the same', () => {
741             const context = new BrowsingContext();
742             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
743                 const chart = createChartWithSampleCluster(context, {}, {width: '100px', height: '100px'});
744
745                 let dataChangeCount = 0;
746                 chart.listenToAction('dataChange', () => dataChangeCount++);
747
748                 chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
749                 chart.fetchMeasurementSets();
750
751                 const requests = context.symbols.MockRemoteAPI.requests;
752                 expect(requests.length).to.be(1);
753                 respondWithSampleCluster(requests[0]);
754                 expect(dataChangeCount).to.be(0);
755
756                 let canvas;
757                 let data;
758                 return waitForComponentsToRender(context).then(() => {
759                     expect(dataChangeCount).to.be(1);
760                     data = chart.sampledTimeSeriesData('current');
761                     expect(data).to.not.be(null);
762                     canvas = chart.content().querySelector('canvas');
763                     expect(canvas).to.not.be(null);
764
765                     expect(canvas.offsetWidth).to.be(100);
766                     expect(canvas.offsetHeight).to.be(100);
767
768                     CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
769                     context.iframe.style.width = context.iframe.offsetWidth * 2 + 'px';
770                     context.global.dispatchEvent(new Event('resize'));
771
772                     expect(canvas.offsetWidth).to.be(100);
773                     expect(canvas.offsetHeight).to.be(100);
774
775                     return waitForComponentsToRender(context);
776                 }).then(() => {
777                     expect(dataChangeCount).to.be(1);
778                     expect(chart.sampledTimeSeriesData('current')).to.be(data);
779                     expect(canvas.offsetWidth).to.be(100);
780                     expect(canvas.offsetHeight).to.be(100);
781                     expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(false);
782                 });
783             });
784         });
785
786         it('should render Y-axis', () => {
787             const context = new BrowsingContext();
788             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI', 'Metric').then(() => {
789                 const chartWithoutYAxis = createChartWithSampleCluster(context, {axis:
790                     {
791                         gridStyle: '#ccc',
792                         fontSize: 1,
793                         valueFormatter: context.symbols.Metric.makeFormatter('ms', 3),
794                     }
795                 });
796                 const chartWithYAxis1 = createChartWithSampleCluster(context, {axis:
797                     {
798                         yAxisWidth: 4,
799                         gridStyle: '#ccc',
800                         fontSize: 1,
801                         valueFormatter: context.symbols.Metric.makeFormatter('ms', 3),
802                     }
803                 });
804                 const chartWithYAxis2 = createChartWithSampleCluster(context, {axis:
805                     {
806                         yAxisWidth: 4,
807                         gridStyle: '#ccc',
808                         fontSize: 1,
809                         valueFormatter: context.symbols.Metric.makeFormatter('B', 3),
810                     }
811                 });
812
813                 chartWithoutYAxis.setDomain(sampleCluster.startTime, sampleCluster.endTime);
814                 chartWithoutYAxis.fetchMeasurementSets();
815                 chartWithYAxis1.setDomain(sampleCluster.startTime, sampleCluster.endTime);
816                 chartWithYAxis1.fetchMeasurementSets();
817                 chartWithYAxis2.setDomain(sampleCluster.startTime, sampleCluster.endTime);
818                 chartWithYAxis2.fetchMeasurementSets();
819
820                 const requests = context.symbols.MockRemoteAPI.requests;
821                 expect(requests.length).to.be(1);
822                 respondWithSampleCluster(requests[0]);
823
824                 return waitForComponentsToRender(context).then(() => {
825                     let canvasWithoutYAxis = chartWithoutYAxis.content().querySelector('canvas');
826                     let canvasWithYAxis1 = chartWithYAxis1.content().querySelector('canvas');
827                     let canvasWithYAxis2 = chartWithYAxis2.content().querySelector('canvas');
828                     CanvasTest.expectCanvasesMismatch(canvasWithoutYAxis, canvasWithYAxis1);
829                     CanvasTest.expectCanvasesMismatch(canvasWithoutYAxis, canvasWithYAxis1);
830                     CanvasTest.expectCanvasesMismatch(canvasWithYAxis1, canvasWithYAxis2);
831
832                     expect(CanvasTest.canvasContainsColor(canvasWithYAxis1, {r: 204, g: 204, b: 204},
833                         {x: canvasWithYAxis1.width - 1, width: 1, y: 0, height: canvasWithYAxis1.height})).to.be(true);
834                 });
835             });
836         });
837
838         it('should render the sampled time series', () => {
839             const context = new BrowsingContext();
840             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
841                 const chartWithoutSampling = createChartWithSampleCluster(context, {}, {lineStyle: 'rgb(0, 128, 255)', width: '100px', height: '100px', sampleData: false});
842                 const chartWithSampling = createChartWithSampleCluster(context, {}, {lineStyle: 'rgb(0, 128, 255)', width: '100px', height: '100px', sampleData: true});
843
844                 chartWithoutSampling.setDomain(sampleCluster.startTime, sampleCluster.endTime);
845                 chartWithoutSampling.fetchMeasurementSets();
846                 respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
847
848                 chartWithSampling.setDomain(sampleCluster.startTime, sampleCluster.endTime);
849                 chartWithSampling.fetchMeasurementSets();
850
851                 let canvasWithSampling;
852                 let canvasWithoutSampling;
853                 return waitForComponentsToRender(context).then(() => {
854                     canvasWithoutSampling = chartWithoutSampling.content().querySelector('canvas');
855                     canvasWithSampling = chartWithSampling.content().querySelector('canvas');
856
857                     CanvasTest.expectCanvasesMatch(canvasWithSampling, canvasWithoutSampling);
858                     expect(CanvasTest.canvasContainsColor(canvasWithoutSampling, {r: 0, g: 128, b: 255})).to.be(true);
859                     expect(CanvasTest.canvasContainsColor(canvasWithSampling, {r: 0, g: 128, b: 255})).to.be(true);
860
861                     const diff = sampleCluster.endTime - sampleCluster.startTime;
862                     chartWithoutSampling.setDomain(sampleCluster.startTime - 2 * diff, sampleCluster.endTime);
863                     chartWithSampling.setDomain(sampleCluster.startTime - 2 * diff, sampleCluster.endTime);
864
865                     CanvasTest.fillCanvasBeforeRedrawCheck(canvasWithoutSampling);
866                     CanvasTest.fillCanvasBeforeRedrawCheck(canvasWithSampling);
867                     return waitForComponentsToRender(context);
868                 }).then(() => {
869                     expect(CanvasTest.hasCanvasBeenRedrawn(canvasWithoutSampling)).to.be(true);
870                     expect(CanvasTest.hasCanvasBeenRedrawn(canvasWithSampling)).to.be(true);
871
872                     expect(CanvasTest.canvasContainsColor(canvasWithoutSampling, {r: 0, g: 128, b: 255})).to.be(true);
873                     expect(CanvasTest.canvasContainsColor(canvasWithSampling, {r: 0, g: 128, b: 255})).to.be(true);
874
875                     CanvasTest.expectCanvasesMismatch(canvasWithSampling, canvasWithoutSampling);
876                 });
877             });
878         });
879
880         it('should render annotations', () => {
881             const context = new BrowsingContext();
882             return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
883                 const options = {annotations: {
884                     textStyle: '#000',
885                     textBackground: '#fff',
886                     minWidth: 3,
887                     barHeight: 7,
888                     barSpacing: 2}};
889                 const chartWithoutAnnotations = createChartWithSampleCluster(context, options);
890                 const chartWithAnnotations = createChartWithSampleCluster(context, options);
891
892                 chartWithoutAnnotations.setDomain(sampleCluster.startTime, sampleCluster.endTime);
893                 chartWithoutAnnotations.fetchMeasurementSets();
894                 respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
895
896                 chartWithAnnotations.setDomain(sampleCluster.startTime, sampleCluster.endTime);
897                 chartWithAnnotations.fetchMeasurementSets();
898
899                 let canvasWithAnnotations;
900                 return waitForComponentsToRender(context).then(() => {
901                     const diff = sampleCluster.endTime - sampleCluster.startTime;
902                     chartWithAnnotations.setAnnotations([{
903                         startTime: sampleCluster.startTime + diff / 4,
904                         endTime: sampleCluster.startTime + diff / 2,
905                         label: 'hello, world',
906                         fillStyle: 'rgb(0, 0, 255)',
907                     }]);
908
909                     canvasWithAnnotations = chartWithAnnotations.content().querySelector('canvas');
910                     CanvasTest.fillCanvasBeforeRedrawCheck(canvasWithAnnotations);
911                     return waitForComponentsToRender(context);
912                 }).then(() => {
913                     expect(CanvasTest.hasCanvasBeenRedrawn(canvasWithAnnotations)).to.be(true);
914                 
915                     const canvasWithoutAnnotations = chartWithoutAnnotations.content().querySelector('canvas');
916                     CanvasTest.expectCanvasesMismatch(canvasWithAnnotations, canvasWithoutAnnotations);
917                 });
918             });
919         });
920     });
921
922 });
923
924 describe('InteractiveTimeSeriesChart', () => {
925
926     it('should change the indicator to the point closest to the last mouse move position', () => {
927         const context = new BrowsingContext();
928         return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
929             const chart = createChartWithSampleCluster(context, {}, {interactiveChart: true, interactive: true});
930
931             chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
932             chart.fetchMeasurementSets();
933             respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
934
935             const indicatorChangeCalls = [];
936             chart.listenToAction('indicatorChange', (...args) => indicatorChangeCalls.push(args));
937
938             let selectionChangeCount = 0;
939             chart.listenToAction('selectionChange', () => selectionChangeCount++);
940
941             let canvas;
942             return waitForComponentsToRender(context).then(() => {
943                 expect(chart.currentSelection()).to.be(null);
944                 expect(chart.currentPoint()).to.be(null);
945                 expect(chart.lockedIndicator()).to.be(null);
946                 expect(indicatorChangeCalls).to.be.eql([]);
947
948                 canvas = chart.content().querySelector('canvas');
949                 const rect = canvas.getBoundingClientRect();
950                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right - 1, clientY: rect.top + rect.height / 2, composed: true, bubbles: true}));
951
952                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
953                 return waitForComponentsToRender(context);
954             }).then(() => {
955                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
956
957                 expect(chart.currentSelection()).to.be(null);
958                 expect(chart.currentPoint()).to.not.be(null);
959                 expect(chart.lockedIndicator()).to.be(null);
960                 const lastPoint = chart.sampledTimeSeriesData('current').lastPoint();
961                 expect(chart.currentPoint()).to.be(lastPoint);
962                 expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, false]]);
963
964                 expect(selectionChangeCount).to.be(0);
965             });
966         });
967     });
968
969     it('should lock the indicator to the point closest to the clicked position', () => {
970         const context = new BrowsingContext();
971         return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
972             const chart = createChartWithSampleCluster(context, {}, {interactiveChart: true, interactive: true});
973
974             chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
975             chart.fetchMeasurementSets();
976             respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
977
978             const indicatorChangeCalls = [];
979             chart.listenToAction('indicatorChange', (...args) => indicatorChangeCalls.push(args));
980
981             let selectionChangeCount = 0;
982             chart.listenToAction('selectionChange', () => selectionChangeCount++);
983
984             let canvas;
985             return waitForComponentsToRender(context).then(() => {
986                 expect(chart.currentSelection()).to.be(null);
987                 expect(chart.currentPoint()).to.be(null);
988                 expect(chart.lockedIndicator()).to.be(null);
989                 expect(indicatorChangeCalls).to.be.eql([]);
990                 canvas = chart.content().querySelector('canvas');
991                 const rect = canvas.getBoundingClientRect();
992
993                 const x = rect.right - 1;
994                 const y = rect.top + rect.height / 2;
995                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: x, clientY: y, composed: true, bubbles: true}));
996                 canvas.dispatchEvent(new MouseEvent('mousedown', {target: canvas, clientX: x, clientY: y, composed: true, bubbles: true}));
997                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: x - 0.5, clientY: y + 0.5, composed: true, bubbles: true}));
998                 canvas.dispatchEvent(new MouseEvent('mouseup', {target: canvas, clientX: x - 0.5, clientY: y + 0.5, composed: true, bubbles: true}));
999                 canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: x - 0.5, clientY: y + 0.5, composed: true, bubbles: true}));
1000
1001                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1002                 return waitForComponentsToRender(context);
1003             }).then(() => {
1004                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1005
1006                 const lastPoint = chart.sampledTimeSeriesData('current').lastPoint();
1007                 expect(chart.currentSelection()).to.be(null);
1008                 expect(chart.currentPoint()).to.be(lastPoint);
1009                 expect(chart.lockedIndicator()).to.be(lastPoint);
1010                 expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, false], [lastPoint.id, true]]);
1011
1012                 expect(selectionChangeCount).to.be(0);
1013             });
1014         });
1015     });
1016
1017     it('should clear the unlocked indicator when the mouse cursor exits the chart', () => {
1018         const context = new BrowsingContext();
1019         return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
1020             const chart = createChartWithSampleCluster(context, {}, {interactiveChart: true, interactive: true});
1021
1022             chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
1023             chart.fetchMeasurementSets();
1024             respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
1025
1026             const indicatorChangeCalls = [];
1027             chart.listenToAction('indicatorChange', (...args) => indicatorChangeCalls.push(args));
1028
1029             let selectionChangeCount = 0;
1030             chart.listenToAction('selectionChange', () => selectionChangeCount++);
1031
1032             let canvas;
1033             let rect;
1034             let lastPoint;
1035             return waitForComponentsToRender(context).then(() => {
1036                 expect(chart.currentSelection()).to.be(null);
1037                 expect(chart.currentPoint()).to.be(null);
1038                 expect(chart.lockedIndicator()).to.be(null);
1039                 expect(indicatorChangeCalls).to.be.eql([]);
1040
1041                 canvas = chart.content().querySelector('canvas');
1042                 rect = canvas.getBoundingClientRect();
1043                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right - 1, clientY: rect.top + rect.height / 2, composed: true, bubbles: true}));
1044
1045                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1046                 return waitForComponentsToRender(context);
1047             }).then(() => {
1048                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1049
1050                 lastPoint = chart.sampledTimeSeriesData('current').lastPoint();
1051                 expect(chart.currentSelection()).to.be(null);
1052                 expect(chart.currentPoint()).to.be(lastPoint);
1053                 expect(chart.lockedIndicator()).to.be(null);
1054                 expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, false]]);
1055
1056                 canvas.parentNode.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right + 50, clientY: rect.bottom + 50, composed: true, bubbles: true}));
1057                 canvas.dispatchEvent(new MouseEvent('mouseleave', {target: canvas, clientX: rect.right + 50, clientY: rect.bottom + 50, composed: true, bubbles: true}));
1058
1059                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1060                 return waitForComponentsToRender(context);
1061             }).then(() => {
1062                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1063
1064                 expect(chart.currentSelection()).to.be(null);
1065                 expect(chart.currentPoint()).to.be(null);
1066                 expect(chart.lockedIndicator()).to.be(null);
1067                 expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, false], [null, false]]);
1068
1069                 expect(selectionChangeCount).to.be(0);
1070             });
1071         });
1072     });
1073
1074     it('should not clear the locked indicator when the mouse cursor exits the chart', () => {
1075         const context = new BrowsingContext();
1076         return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
1077             const chart = createChartWithSampleCluster(context, {}, {interactiveChart: true, interactive: true});
1078
1079             chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
1080             chart.fetchMeasurementSets();
1081             respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
1082
1083             const indicatorChangeCalls = [];
1084             chart.listenToAction('indicatorChange', (...args) => indicatorChangeCalls.push(args));
1085
1086             let selectionChangeCount = 0;
1087             chart.listenToAction('selectionChange', () => selectionChangeCount++);
1088
1089             let canvas;
1090             let rect;
1091             let lastPoint;
1092             return waitForComponentsToRender(context).then(() => {
1093                 expect(chart.currentSelection()).to.be(null);
1094                 expect(chart.currentPoint()).to.be(null);
1095                 expect(chart.lockedIndicator()).to.be(null);
1096                 expect(indicatorChangeCalls).to.be.eql([]);
1097
1098                 canvas = chart.content().querySelector('canvas');
1099                 rect = canvas.getBoundingClientRect();
1100                 canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.right - 1, clientY: rect.top + rect.height / 2, composed: true, bubbles: true}));
1101
1102                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1103                 return waitForComponentsToRender(context);
1104             }).then(() => {
1105                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1106
1107                 lastPoint = chart.sampledTimeSeriesData('current').lastPoint();
1108                 expect(chart.currentSelection()).to.be(null);
1109                 expect(chart.currentPoint()).to.be(lastPoint);
1110                 expect(chart.lockedIndicator()).to.be(lastPoint);
1111                 expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, true]]);
1112
1113                 canvas.parentNode.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right + 50, clientY: rect.bottom + 50, composed: true, bubbles: true}));
1114                 canvas.dispatchEvent(new MouseEvent('mouseleave', {target: canvas, clientX: rect.right + 50, clientY: rect.bottom + 50, composed: true, bubbles: true}));
1115
1116                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1117                 return waitForComponentsToRender(context);
1118             }).then(() => {
1119                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(false);
1120
1121                 expect(chart.currentSelection()).to.be(null);
1122                 expect(chart.currentPoint()).to.be(lastPoint);
1123                 expect(chart.lockedIndicator()).to.be(lastPoint);
1124                 expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, true]]);
1125
1126                 expect(selectionChangeCount).to.be(0);
1127             })
1128         });
1129     });
1130
1131     it('should clear the locked indicator when clicked', () => {
1132         const context = new BrowsingContext();
1133         return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
1134             const chart = createChartWithSampleCluster(context, {}, {interactiveChart: true, interactive: true});
1135
1136             chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
1137             chart.fetchMeasurementSets();
1138             respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
1139
1140             const indicatorChangeCalls = [];
1141             chart.listenToAction('indicatorChange', (...args) => indicatorChangeCalls.push(args));
1142
1143             let selectionChangeCount = 0;
1144             chart.listenToAction('selectionChange', () => selectionChangeCount++);
1145
1146             let canvas;
1147             let rect;
1148             let y;
1149             let lastPoint;
1150             return waitForComponentsToRender(context).then(() => {
1151                 expect(chart.currentSelection()).to.be(null);
1152                 expect(chart.currentPoint()).to.be(null);
1153                 expect(chart.lockedIndicator()).to.be(null);
1154                 expect(indicatorChangeCalls).to.be.eql([]);
1155
1156                 canvas = chart.content().querySelector('canvas');
1157                 rect = canvas.getBoundingClientRect();
1158                 y = rect.top + rect.height / 2;
1159                 canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.right - 1, clientY: y, composed: true, bubbles: true}));
1160
1161                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1162                 return waitForComponentsToRender(context);
1163             }).then(() => {
1164                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1165
1166                 lastPoint = chart.sampledTimeSeriesData('current').lastPoint();
1167                 expect(chart.currentSelection()).to.be(null);
1168                 expect(chart.currentPoint()).to.be(lastPoint);
1169                 expect(chart.lockedIndicator()).to.be(lastPoint);
1170                 expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, true]]);
1171
1172                 canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.left + 1, clientY: y, composed: true, bubbles: true}));
1173
1174                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1175                 return waitForComponentsToRender(context);
1176             }).then(() => {
1177                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1178
1179                 expect(chart.currentSelection()).to.be(null);
1180                 const firstPoint = chart.sampledTimeSeriesData('current').firstPoint();
1181                 expect(chart.currentPoint()).to.be(firstPoint);
1182                 expect(chart.lockedIndicator()).to.be(null);
1183                 expect(indicatorChangeCalls).to.be.eql([[lastPoint.id, true], [firstPoint.id, false]]);
1184
1185                 expect(selectionChangeCount).to.be(0);
1186             })
1187         });
1188     });
1189
1190     it('should change the selection when the mouse cursor is dragged', () => {
1191         const context = new BrowsingContext();
1192         return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
1193             const chart = createChartWithSampleCluster(context,
1194                 {selection: {lineStyle: '#f93', lineWidth: 2, fillStyle: '#ccc'}},
1195                 {interactiveChart: true, interactive: true});
1196
1197             chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
1198             chart.fetchMeasurementSets();
1199             respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
1200
1201             const indicatorChangeCalls = [];
1202             chart.listenToAction('indicatorChange', (...args) => indicatorChangeCalls.push(args));
1203
1204             const selectionChangeCalls = [];
1205             chart.listenToAction('selectionChange', (...args) => selectionChangeCalls.push(args));
1206
1207             const zoomButton = chart.content('zoom-button');
1208
1209             let canvas;
1210             let rect;
1211             let y;
1212             let firstPoint;
1213             let oldRange;
1214             let newRange;
1215             return waitForComponentsToRender(context).then(() => {
1216                 expect(chart.currentSelection()).to.be(null);
1217                 expect(chart.currentPoint()).to.be(null);
1218                 expect(chart.lockedIndicator()).to.be(null);
1219                 expect(selectionChangeCalls).to.be.eql([]);
1220
1221                 canvas = chart.content().querySelector('canvas');
1222                 rect = canvas.getBoundingClientRect();
1223                 y = rect.top + rect.height / 2;
1224                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
1225
1226                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1227                 return waitForComponentsToRender(context);
1228             }).then(() => {
1229                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1230
1231                 firstPoint = chart.sampledTimeSeriesData('current').firstPoint();
1232                 expect(chart.currentSelection()).to.be(null);
1233                 expect(chart.currentPoint()).to.be(firstPoint);
1234                 expect(chart.lockedIndicator()).to.be(null);
1235                 expect(indicatorChangeCalls).to.be.eql([[firstPoint.id, false]]);
1236                 expect(zoomButton.offsetHeight).to.be(0);
1237
1238                 canvas.dispatchEvent(new MouseEvent('mousedown', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
1239
1240                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1241                 return waitForComponentsToRender(context);
1242             }).then(() => {
1243                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(false);
1244
1245                 expect(chart.currentSelection()).to.be(null);
1246                 expect(chart.currentPoint()).to.be(firstPoint);
1247                 expect(chart.lockedIndicator()).to.be(null);
1248                 expect(selectionChangeCalls).to.be.eql([]);
1249                 expect(indicatorChangeCalls).to.be.eql([[firstPoint.id, false]]);
1250                 expect(zoomButton.offsetHeight).to.be(0);
1251
1252                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.left + 15, clientY: y + 5, composed: true, bubbles: true}));
1253
1254                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1255                 return waitForComponentsToRender(context);
1256             }).then(() => {
1257                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1258
1259                 expect(chart.currentSelection()).to.not.be(null);
1260                 expect(chart.currentPoint()).to.be(null);
1261                 expect(chart.lockedIndicator()).to.be(null);
1262                 expect(selectionChangeCalls.length).to.be(1);
1263                 oldRange = selectionChangeCalls[0][0];
1264                 expect(oldRange).to.be.eql(chart.currentSelection());
1265                 expect(selectionChangeCalls[0][1]).to.be(false);
1266                 expect(indicatorChangeCalls).to.be.eql([[firstPoint.id, false], [null, false]]);
1267                 expect(zoomButton.offsetHeight).to.be(0);
1268
1269                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right - 5, clientY: y + 5, composed: true, bubbles: true}));
1270
1271                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1272                 return waitForComponentsToRender(context);
1273             }).then(() => {
1274                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1275
1276                 expect(chart.currentSelection()).to.not.be(null);
1277                 expect(chart.currentPoint()).to.be(null);
1278                 expect(chart.lockedIndicator()).to.be(null);
1279                 expect(selectionChangeCalls.length).to.be(2);
1280                 newRange = selectionChangeCalls[1][0];
1281                 expect(newRange).to.be.eql(chart.currentSelection());
1282                 expect(newRange[0]).to.be(oldRange[0]);
1283                 expect(newRange[1]).to.be.greaterThan(oldRange[1]);
1284                 expect(selectionChangeCalls[1][1]).to.be(false);
1285                 expect(zoomButton.offsetHeight).to.be(0);
1286
1287                 canvas.dispatchEvent(new MouseEvent('mouseup', {target: canvas, clientX: rect.right - 5, clientY: y + 5, composed: true, bubbles: true}));
1288                 canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.right - 5, clientY: y + 5, composed: true, bubbles: true}));
1289
1290                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1291                 return waitForComponentsToRender(context);
1292             }).then(() => {
1293                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1294
1295                 expect(chart.currentSelection()).to.be.eql(newRange);
1296                 expect(chart.currentPoint()).to.be(null);
1297                 expect(chart.lockedIndicator()).to.be(null);
1298                 expect(selectionChangeCalls.length).to.be(3);
1299                 expect(selectionChangeCalls[2][0]).to.be.eql(newRange);
1300                 expect(selectionChangeCalls[2][1]).to.be(true);
1301                 expect(zoomButton.offsetHeight).to.be(0);
1302             });
1303         });
1304     });
1305
1306     it('should dispatch the "zoom" action when the zoom button is clicked', () => {
1307         const context = new BrowsingContext();
1308         return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
1309             const chart = createChartWithSampleCluster(context,
1310                 {selection: {lineStyle: '#f93', lineWidth: 2, fillStyle: '#ccc'}, zoomButton: true},
1311                 {interactiveChart: true, interactive: true});
1312
1313             chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
1314             chart.fetchMeasurementSets();
1315             respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
1316
1317             const zoomCalls = [];
1318             chart.listenToAction('zoom', (...args) => zoomCalls.push(args));
1319             const zoomButton = chart.content('zoom-button');
1320
1321             let selection;
1322             let canvas;
1323             return waitForComponentsToRender(context).then(() => {
1324                 expect(zoomButton.offsetHeight).to.be(0);
1325                 canvas = chart.content().querySelector('canvas');
1326                 const rect = canvas.getBoundingClientRect();
1327                 const y = rect.top + rect.height / 2;
1328                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
1329                 canvas.dispatchEvent(new MouseEvent('mousedown', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
1330                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right - 10, clientY: y + 5, composed: true, bubbles: true}));
1331                 canvas.dispatchEvent(new MouseEvent('mouseup', {target: canvas, clientX: rect.right - 10, clientY: y + 5, composed: true, bubbles: true}));
1332
1333                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1334                 return waitForComponentsToRender(context);
1335             }).then(() => {
1336                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1337
1338                 selection = chart.currentSelection();
1339                 expect(selection).to.not.be(null);
1340                 expect(chart.currentPoint()).to.be(null);
1341                 expect(chart.lockedIndicator()).to.be(null);
1342                 expect(zoomButton.offsetHeight).to.not.be(0);
1343                 expect(zoomCalls).to.be.eql([]);
1344                 zoomButton.click();
1345             }).then(() => {
1346                 expect(zoomCalls).to.be.eql([[selection]]);
1347
1348                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1349                 return waitForComponentsToRender(context);
1350             }).then(() => {
1351                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(false);
1352             });
1353         });
1354     });
1355
1356     it('should clear the selection when clicked', () => {
1357         const context = new BrowsingContext();
1358         return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
1359             const chart = createChartWithSampleCluster(context,
1360                 {selection: {lineStyle: '#f93', lineWidth: 2, fillStyle: '#ccc'}},
1361                 {interactiveChart: true, interactive: true});
1362
1363             chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
1364             chart.fetchMeasurementSets();
1365             respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
1366
1367             let canvas;
1368             let rect;
1369             let y;
1370             return waitForComponentsToRender(context).then(() => {
1371                 canvas = chart.content().querySelector('canvas');
1372                 rect = canvas.getBoundingClientRect();
1373                 y = rect.top + rect.height / 2;
1374                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
1375                 canvas.dispatchEvent(new MouseEvent('mousedown', {target: canvas, clientX: rect.left + 5, clientY: y, composed: true, bubbles: true}));
1376                 canvas.dispatchEvent(new MouseEvent('mousemove', {target: canvas, clientX: rect.right - 10, clientY: y + 5, composed: true, bubbles: true}));
1377                 canvas.dispatchEvent(new MouseEvent('mouseup', {target: canvas, clientX: rect.right - 10, clientY: y + 5, composed: true, bubbles: true}));
1378                 canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.right - 10, clientY: y + 5, composed: true, bubbles: true}));
1379
1380                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1381                 return waitForComponentsToRender(context);
1382             }).then(() => {
1383                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1384
1385                 expect(chart.currentSelection()).to.not.be(null);
1386                 expect(chart.currentPoint()).to.be(null);
1387                 expect(chart.lockedIndicator()).to.be(null);
1388
1389                 canvas.dispatchEvent(new MouseEvent('click', {target: canvas, clientX: rect.left + 1, clientY: y + 5, composed: true, bubbles: true}));
1390
1391                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1392                 return waitForComponentsToRender(context);
1393             }).then(() => {
1394                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1395
1396                 expect(chart.currentSelection()).to.be(null);
1397                 expect(chart.currentPoint()).to.be(chart.sampledTimeSeriesData('current').firstPoint());
1398                 expect(chart.lockedIndicator()).to.be(null);
1399             });
1400         });
1401     });
1402
1403     it('should dispatch "annotationClick" action when an annotation is clicked', () => {
1404         const context = new BrowsingContext();
1405         return context.importScripts(scripts, 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart', 'MeasurementSet', 'MockRemoteAPI').then(() => {
1406             const options = {annotations: {
1407                 textStyle: '#000',
1408                 textBackground: '#fff',
1409                 minWidth: 3,
1410                 barHeight: 10,
1411                 barSpacing: 1}};
1412             const chart = createChartWithSampleCluster(context, options, {interactiveChart: true});
1413
1414             chart.setDomain(sampleCluster.startTime, sampleCluster.endTime);
1415             chart.fetchMeasurementSets();
1416             respondWithSampleCluster(context.symbols.MockRemoteAPI.requests[0]);
1417
1418             const diff = sampleCluster.endTime - sampleCluster.startTime;
1419             const annotations = [{
1420                 startTime: sampleCluster.startTime + diff / 2,
1421                 endTime: sampleCluster.endTime - diff / 4,
1422                 label: 'hello, world',
1423                 fillStyle: 'rgb(0, 0, 255)',
1424             }]
1425             chart.setAnnotations(annotations);
1426
1427             const annotationClickCalls = [];
1428             chart.listenToAction('annotationClick', (...args) => annotationClickCalls.push(args));
1429
1430             let canvas;
1431             let init;
1432             return waitForComponentsToRender(context).then(() => {
1433                 expect(annotationClickCalls).to.be.eql([]);
1434                 expect(chart.content('annotation-label').textContent).to.not.contain('hello, world');
1435
1436                 canvas = chart.content().querySelector('canvas');
1437                 const rect = canvas.getBoundingClientRect();
1438                 init = {target: canvas, clientX: rect.right - rect.width / 4, clientY: rect.bottom - 5, composed: true, bubbles: true};
1439                 canvas.dispatchEvent(new MouseEvent('mousemove', init));
1440
1441                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1442                 return waitForComponentsToRender(context);
1443             }).then(() => {
1444                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(true);
1445
1446                 expect(chart.content('annotation-label').textContent).to.contain('hello, world');
1447                 expect(annotationClickCalls).to.be.eql([]);
1448                 canvas.dispatchEvent(new MouseEvent('mousedown', init));
1449                 canvas.dispatchEvent(new MouseEvent('mouseup', init));
1450                 canvas.dispatchEvent(new MouseEvent('click', init));
1451
1452                 expect(annotationClickCalls).to.be.eql([[annotations[0]]]);
1453
1454                 CanvasTest.fillCanvasBeforeRedrawCheck(canvas);
1455                 return waitForComponentsToRender(context);
1456             }).then(() => {
1457                 expect(CanvasTest.hasCanvasBeenRedrawn(canvas)).to.be(false);
1458             });
1459         });
1460     });
1461
1462 });