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