Refactoring to fix stacked graphs with NaNs.
[dygraphs.git] / auto_tests / tests / callback.js
1 /**
2 * @fileoverview Test cases for the callbacks.
3 *
4 * @author uemit.seren@gmail.com (Ümit Seren)
5 */
6
7 var CallbackTestCase = TestCase("callback");
8
9 CallbackTestCase.prototype.setUp = function() {
10 document.body.innerHTML = "<div id='graph'></div><div id='selection'></div>";
11 this.xhr = XMLHttpRequest;
12 this.styleSheet = document.createElement("style");
13 this.styleSheet.type = "text/css";
14 document.getElementsByTagName("head")[0].appendChild(this.styleSheet);
15 };
16
17 CallbackTestCase.prototype.tearDown = function() {
18 XMLHttpRequest = this.xhr;
19 };
20
21 var data = "X,a\,b,c\n" +
22 "10,-1,1,2\n" +
23 "11,0,3,1\n" +
24 "12,1,4,2\n" +
25 "13,0,2,3\n";
26
27
28 /**
29 * This tests that when the function idxToRow_ returns the proper row and the onHiglightCallback
30 * is properly called when the first series is hidden (setVisibility = false)
31 *
32 */
33 CallbackTestCase.prototype.testHighlightCallbackIsCalled = function() {
34 var h_row;
35 var h_pts;
36
37 var highlightCallback = function(e, x, pts, row) {
38 h_row = row;
39 h_pts = pts;
40 };
41
42 var graph = document.getElementById("graph");
43 var g = new Dygraph(graph, data,
44 {
45 width: 100,
46 height: 100,
47 visibility: [false, true, true],
48 highlightCallback: highlightCallback
49 });
50
51 DygraphOps.dispatchMouseMove(g, 13, 10);
52
53 //check correct row is returned
54 assertEquals(3, h_row);
55 //check there are only two points (because first series is hidden)
56 assertEquals(2, h_pts.length);
57 };
58
59
60 /**
61 * Test that drawPointCallback isn't called when drawPoints is false
62 */
63 CallbackTestCase.prototype.testDrawPointCallback_disabled = function() {
64 var called = false;
65
66 var callback = function() {
67 called = true;
68 };
69
70 var graph = document.getElementById("graph");
71 var g = new Dygraph(graph, data, {
72 drawPointCallback : callback,
73 });
74
75 assertFalse(called);
76 };
77
78 /**
79 * Test that drawPointCallback is called when drawPoints is true
80 */
81 CallbackTestCase.prototype.testDrawPointCallback_enabled = function() {
82 var called = false;
83
84 var callback = function() {
85 called = true;
86 };
87
88 var graph = document.getElementById("graph");
89 var g = new Dygraph(graph, data, {
90 drawPoints : true,
91 drawPointCallback : callback
92 });
93
94 assertTrue(called);
95 };
96
97 /**
98 * Test that drawPointCallback is called when drawPoints is true
99 */
100 CallbackTestCase.prototype.testDrawPointCallback_pointSize = function() {
101 var pointSize = 0;
102 var count = 0;
103
104 var callback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam) {
105 pointSize = pointSizeParam;
106 count++;
107 };
108
109 var graph = document.getElementById("graph");
110 var g = new Dygraph(graph, data, {
111 drawPoints : true,
112 drawPointCallback : callback
113 });
114
115 assertEquals(1.5, pointSize);
116 assertEquals(12, count); // one call per data point.
117
118 var g = new Dygraph(graph, data, {
119 drawPoints : true,
120 drawPointCallback : callback,
121 pointSize : 8
122 });
123
124 assertEquals(8, pointSize);
125 };
126
127 /**
128 * Test that drawPointCallback is called for isolated points when
129 * drawPoints is false, and also for gap points if that's enabled.
130 */
131 CallbackTestCase.prototype.testDrawPointCallback_isolated = function() {
132 var xvalues = [];
133
134 var g;
135 var callback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam) {
136 var dx = g.toDataXCoord(cx);
137 xvalues.push(dx);
138 Dygraph.Circles.DEFAULT.apply(this, arguments);
139 };
140
141 var graph = document.getElementById("graph");
142 var testdata = [[10, 2], [11, 3], [12, NaN], [13, 2], [14, NaN], [15, 3]];
143 var graphOpts = {
144 labels: ['X', 'Y'],
145 valueRange: [0, 4],
146 drawPoints : false,
147 drawPointCallback : callback,
148 pointSize : 8
149 };
150
151 // Test that isolated points get drawn
152 g = new Dygraph(graph, testdata, graphOpts);
153 assertEquals(2, xvalues.length);
154 assertEquals(13, xvalues[0]);
155 assertEquals(15, xvalues[1]);
156
157 // Test that isolated points + gap points get drawn when
158 // drawGapEdgePoints is set. This should add one point at the right
159 // edge of the segment at x=11, but not at the graph edge at x=10.
160 xvalues = []; // Reset for new test
161 graphOpts.drawGapEdgePoints = true;
162 g = new Dygraph(graph, testdata, graphOpts);
163 assertEquals(3, xvalues.length);
164 assertEquals(11, xvalues[0]);
165 assertEquals(13, xvalues[1]);
166 assertEquals(15, xvalues[2]);
167 };
168
169 /**
170 * This tests that when the function idxToRow_ returns the proper row and the onHiglightCallback
171 * is properly called when the first series is hidden (setVisibility = false)
172 *
173 */
174 CallbackTestCase.prototype.testDrawHighlightPointCallbackIsCalled = function() {
175 var called = false;
176
177 var drawHighlightPointCallback = function() {
178 called = true;
179 };
180
181 var graph = document.getElementById("graph");
182 var g = new Dygraph(graph, data,
183 {
184 width: 100,
185 height : 100,
186 drawHighlightPointCallback : drawHighlightPointCallback
187 });
188
189 assertFalse(called);
190 DygraphOps.dispatchMouseMove(g, 13, 10);
191 assertTrue(called);
192 };
193
194 /**
195 * Test the closest-series highlighting methods for normal and stacked modes.
196 * Also pass in line widths for plain and highlighted lines for easier visual
197 * confirmation that the highlighted line is drawn on top of the others.
198 */
199 var runClosestTest = function(isStacked, widthNormal, widthHighlighted) {
200 var h_row;
201 var h_pts;
202 var h_series;
203
204 var graph = document.getElementById("graph");
205 var g = new Dygraph(graph, data,
206 {
207 width: 600,
208 height: 400,
209 visibility: [false, true, true],
210 stackedGraph: isStacked,
211 strokeWidth: widthNormal,
212 strokeBorderWidth: 2,
213 highlightCircleSize: widthNormal * 2,
214 highlightSeriesBackgroundAlpha: 0.3,
215
216 highlightSeriesOpts: {
217 strokeWidth: widthHighlighted,
218 highlightCircleSize: widthHighlighted * 2
219 }
220 });
221
222 var highlightCallback = function(e, x, pts, row, set) {
223 h_row = row;
224 h_pts = pts;
225 h_series = set;
226 document.getElementById('selection').innerHTML='row=' + row + ', set=' + set;
227 };
228
229 g.updateOptions({highlightCallback: highlightCallback}, true);
230
231 if (isStacked) {
232 DygraphOps.dispatchMouseMove(g, 11.45, 1.4);
233 assertEquals(1, h_row);
234 assertEquals('c', h_series);
235
236 //now move up in the same row
237 DygraphOps.dispatchMouseMove(g, 11.45, 1.5);
238 assertEquals(1, h_row);
239 assertEquals('b', h_series);
240
241 //and a bit to the right
242 DygraphOps.dispatchMouseMove(g, 11.55, 1.5);
243 assertEquals(2, h_row);
244 assertEquals('c', h_series);
245 } else {
246 DygraphOps.dispatchMouseMove(g, 11, 1.5);
247 assertEquals(1, h_row);
248 assertEquals('c', h_series);
249
250 //now move up in the same row
251 DygraphOps.dispatchMouseMove(g, 11, 2.5);
252 assertEquals(1, h_row);
253 assertEquals('b', h_series);
254 }
255
256 return g;
257 };
258
259 /**
260 * Test basic closest-point highlighting.
261 */
262 CallbackTestCase.prototype.testClosestPointCallback = function() {
263 runClosestTest(false, 1, 3);
264 }
265
266 /**
267 * Test setSelection() with series name
268 */
269 CallbackTestCase.prototype.testSetSelection = function() {
270 var g = runClosestTest(false, 1, 3);
271 assertEquals(1, g.attr_('strokeWidth', 'c'));
272 g.setSelection(false, 'c');
273 assertEquals(3, g.attr_('strokeWidth', 'c'));
274 }
275
276 /**
277 * Test closest-point highlighting for stacked graph
278 */
279 CallbackTestCase.prototype.testClosestPointStackedCallback = function() {
280 runClosestTest(true, 1, 3);
281 }
282
283 /**
284 * Closest-point highlighting with legend CSS - border around active series.
285 */
286 CallbackTestCase.prototype.testClosestPointCallbackCss1 = function() {
287 var css = "div.dygraph-legend > span { display: block; }\n" +
288 "div.dygraph-legend > span.highlight { border: 1px solid grey; }\n";
289 this.styleSheet.innerHTML = css;
290 runClosestTest(false, 2, 4);
291 this.styleSheet.innerHTML = '';
292 }
293
294 /**
295 * Closest-point highlighting with legend CSS - show only closest series.
296 */
297 CallbackTestCase.prototype.testClosestPointCallbackCss2 = function() {
298 var css = "div.dygraph-legend > span { display: none; }\n" +
299 "div.dygraph-legend > span.highlight { display: inline; }\n";
300 this.styleSheet.innerHTML = css;
301 runClosestTest(false, 10, 15);
302 this.styleSheet.innerHTML = '';
303 // TODO(klausw): verify that the highlighted line is drawn on top?
304 }
305
306 /**
307 * Closest-point highlighting with locked series.
308 */
309 CallbackTestCase.prototype.testSetSelectionLocking = function() {
310 var g = runClosestTest(false, 2, 4);
311
312 // Default behavior, 'b' is closest
313 DygraphOps.dispatchMouseMove(g, 11, 4);
314 assertEquals('b', g.getHighlightSeries());
315
316 // Now lock selection to 'c'
317 g.setSelection(false, 'c', true);
318 DygraphOps.dispatchMouseMove(g, 11, 4);
319 assertEquals('c', g.getHighlightSeries());
320
321 // Unlock, should be back to 'b'
322 g.clearSelection();
323 DygraphOps.dispatchMouseMove(g, 11, 4);
324 assertEquals('b', g.getHighlightSeries());
325 }
326
327 /**
328 * This tests that closest point searches work for data containing NaNs.
329 *
330 * It's intended to catch a regression where a NaN Y value confuses the
331 * closest-point algorithm, treating it as closer as any previous point.
332 */
333 CallbackTestCase.prototype.testNaNData = function() {
334 var dataNaN = [
335 [9, -1, NaN, NaN],
336 [10, -1, 1, 2],
337 [11, 0, 3, 1],
338 [12, 1, 4, NaN],
339 [13, 0, 2, 3],
340 [14, -1, 1, 4]];
341
342 var h_row;
343 var h_pts;
344
345 var highlightCallback = function(e, x, pts, row) {
346 h_row = row;
347 h_pts = pts;
348 };
349
350 var graph = document.getElementById("graph");
351 var g = new Dygraph(graph, dataNaN,
352 {
353 width: 600,
354 height: 400,
355 labels: ['x', 'a', 'b', 'c'],
356 visibility: [false, true, true],
357 highlightCallback: highlightCallback
358 });
359
360 DygraphOps.dispatchMouseMove(g, 10.1, 0.9);
361 //check correct row is returned
362 assertEquals(1, h_row);
363
364 // Explicitly test closest point algorithms
365 var dom = g.toDomCoords(10.1, 0.9);
366 assertEquals(1, g.findClosestRow(dom[0]));
367
368 var res = g.findClosestPoint(dom[0], dom[1]);
369 assertEquals(1, res.row);
370 assertEquals('b', res.seriesName);
371
372 res = g.findStackedPoint(dom[0], dom[1]);
373 assertEquals(1, res.row);
374 assertEquals('c', res.seriesName);
375 };
376
377 /**
378 * This tests that stacked point searches work for data containing NaNs.
379 */
380 CallbackTestCase.prototype.testNaNDataStack = function() {
381 var dataNaN = [
382 [9, -1, NaN, NaN],
383 [10, -1, 1, 2],
384 [11, 0, 3, 1],
385 [12, 1, NaN, 2],
386 [13, 0, 2, 3],
387 [14, -1, 1, 4],
388 [15, 0, 2, NaN],
389 [16, 1, 1, 3],
390 [17, 1, NaN, 3],
391 [18, 0, 2, 5],
392 [19, 0, 1, 4]];
393
394 var h_row;
395 var h_pts;
396
397 var highlightCallback = function(e, x, pts, row) {
398 h_row = row;
399 h_pts = pts;
400 };
401
402 var graph = document.getElementById("graph");
403 var g = new Dygraph(graph, dataNaN,
404 {
405 width: 600,
406 height: 400,
407 labels: ['x', 'a', 'b', 'c'],
408 visibility: [false, true, true],
409 stackedGraph: true,
410 highlightCallback: highlightCallback
411 });
412
413 DygraphOps.dispatchMouseMove(g, 10.1, 0.9);
414 //check correct row is returned
415 assertEquals(1, h_row);
416
417 // Explicitly test stacked point algorithm.
418 var dom = g.toDomCoords(10.1, 0.9);
419 var res = g.findStackedPoint(dom[0], dom[1]);
420 assertEquals(1, res.row);
421 assertEquals('c', res.seriesName);
422
423 // All-NaN area at left, should get no points.
424 dom = g.toDomCoords(9.1, 0.9);
425 res = g.findStackedPoint(dom[0], dom[1]);
426 assertEquals(0, res.row);
427 assertEquals(undefined, res.seriesName);
428
429 // First gap, get 'c' since it's non-NaN.
430 dom = g.toDomCoords(12.1, 0.9);
431 res = g.findStackedPoint(dom[0], dom[1]);
432 assertEquals(3, res.row);
433 assertEquals('c', res.seriesName);
434
435 // Second gap, get 'b' since 'c' is NaN.
436 dom = g.toDomCoords(15.1, 0.9);
437 res = g.findStackedPoint(dom[0], dom[1]);
438 assertEquals(6, res.row);
439 assertEquals('b', res.seriesName);
440
441 // Isolated points should work, finding series b in this case.
442 dom = g.toDomCoords(15.9, 3.1);
443 res = g.findStackedPoint(dom[0], dom[1]);
444 assertEquals(7, res.row);
445 assertEquals('b', res.seriesName);
446 };
447
448 CallbackTestCase.prototype.testGapHighlight = function() {
449 var dataGap = [
450 [1, null, 3],
451 [2, 2, null],
452 [3, null, 5],
453 [4, 4, null],
454 [5, null, 7],
455 [6, NaN, null],
456 [8, 8, null],
457 [10, 10, null]];
458
459 var h_row;
460 var h_pts;
461
462 var highlightCallback = function(e, x, pts, row) {
463 h_row = row;
464 h_pts = pts;
465 };
466
467 var graph = document.getElementById("graph");
468 var g = new Dygraph(graph, dataGap, {
469 width: 400,
470 height: 300,
471 //stackedGraph: true,
472 connectSeparatedPoints: true,
473 drawPoints: true,
474 labels: ['x', 'A', 'B'],
475 highlightCallback : highlightCallback
476 });
477
478 DygraphOps.dispatchMouseMove(g, 1.1, 10);
479 //point from series B
480 assertEquals(0, h_row);
481 assertEquals(1, h_pts.length);
482 assertEquals(3, h_pts[0].yval);
483 assertEquals('B', h_pts[0].name);
484
485 DygraphOps.dispatchMouseMove(g, 6.1, 10);
486 // A is NaN at x=6
487 assertEquals(1, h_pts.length);
488 assert(isNaN(h_pts[0].yval));
489 assertEquals('A', h_pts[0].name);
490
491 DygraphOps.dispatchMouseMove(g, 8.1, 10);
492 //point from series A
493 assertEquals(6, h_row);
494 assertEquals(1, h_pts.length);
495 assertEquals(8, h_pts[0].yval);
496 assertEquals('A', h_pts[0].name);
497 };
498
499 CallbackTestCase.prototype.testFailedResponse = function() {
500
501 // Fake out the XMLHttpRequest so it doesn't do anything.
502 XMLHttpRequest = function () {};
503 XMLHttpRequest.prototype.open = function () {};
504 XMLHttpRequest.prototype.send = function () {};
505
506 var highlightCallback = function(e, x, pts, row) {
507 fail("should not reach here");
508 };
509
510 var graph = document.getElementById("graph");
511 graph.style.border = "2px solid black";
512 var g = new Dygraph(graph, "data.csv", { // fake name
513 width: 400,
514 height: 300,
515 highlightCallback : highlightCallback
516 });
517
518 DygraphOps.dispatchMouseOver_Point(g, 800, 800);
519 DygraphOps.dispatchMouseMove_Point(g, 100, 100);
520 DygraphOps.dispatchMouseMove_Point(g, 800, 800);
521
522 var oldOnerror = window.onerror;
523 var failed = false;
524 window.onerror = function() { failed = true; return false; }
525
526 DygraphOps.dispatchMouseOut_Point(g, 800, 800); // This call should not throw an exception.
527
528 assertFalse("exception thrown during mouseout", failed);
529 };
530
531
532 // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=355
533 CallbackTestCase.prototype.testHighlightCallbackRow = function() {
534 var highlightRow;
535 var highlightCallback = function(e, x, pts, row) {
536 highlightRow = row;
537 };
538
539 var graph = document.getElementById("graph");
540 var g = new Dygraph(graph,
541 "X,Y,Z\n" +
542 "0,1,2\n" + // 0
543 "1,2,3\n" + // 100
544 "2,3,4\n" + // 200
545 "3,4,5\n" + // 300
546 "4,5,6\n", // 400
547 { // fake name
548 width: 400,
549 height: 300,
550 highlightCallback : highlightCallback
551 });
552
553 // Mouse over each of the points
554 DygraphOps.dispatchMouseOver_Point(g, 0, 0);
555 DygraphOps.dispatchMouseMove_Point(g, 0, 0);
556 assertEquals(0, highlightRow);
557 DygraphOps.dispatchMouseMove_Point(g, 100, 0);
558 assertEquals(1, highlightRow);
559 DygraphOps.dispatchMouseMove_Point(g, 200, 0);
560 assertEquals(2, highlightRow);
561 DygraphOps.dispatchMouseMove_Point(g, 300, 0);
562 assertEquals(3, highlightRow);
563 DygraphOps.dispatchMouseMove_Point(g, 400, 0);
564 assertEquals(4, highlightRow);
565
566 // Now zoom and verify that the row numbers still refer to rows in the data
567 // array.
568 g.updateOptions({dateWindow: [2, 4]});
569 DygraphOps.dispatchMouseOver_Point(g, 0, 0);
570 DygraphOps.dispatchMouseMove_Point(g, 0, 0);
571 assertEquals(2, highlightRow);
572 assertEquals('2: Y: 3 Z: 4', Util.getLegend());
573 };
574
575 /**
576 * Test that underlay callback is called even when there are no series,
577 * and that the y axis ranges are not NaN.
578 */
579 CallbackTestCase.prototype.underlayCallback_noSeries = function() {
580 var called = false;
581 var yMin, yMax;
582
583 var callback = function(canvas, area, g) {
584 called = true;
585 yMin = g.yAxisRange(0)[0];
586 yMax = g.yAxisRange(0)[1];
587 };
588
589 var graph = document.getElementById("graph");
590 var g = new Dygraph(graph, "\n", {
591 underlayCallback: callback
592 });
593
594 assertTrue(called);
595 assertFalse(isNaN(yMin));
596 assertFalse(isNaN(yMax));
597 };
598
599 /**
600 * Test that underlay callback receives the correct y-axis range.
601 */
602 CallbackTestCase.prototype.underlayCallback_yAxisRange = function() {
603 var called = false;
604 var yMin, yMax;
605
606 var callback = function(canvas, area, g) {
607 yMin = g.yAxisRange(0)[0];
608 yMax = g.yAxisRange(0)[1];
609 };
610
611 var graph = document.getElementById("graph");
612 var g = new Dygraph(graph, "\n", {
613 valueRange: [0,10],
614 underlayCallback: callback
615 });
616
617 assertEquals(0, yMin);
618 assertEquals(10, yMax);
619 };
620
621 /**
622 * Test that drawPointCallback is called for isolated points and correct idx for the point is returned.
623 */
624 CallbackTestCase.prototype.testDrawPointCallback_idx = function() {
625 var indices = [];
626
627 var g;
628 var callback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam,idx) {
629 indices.push(idx);
630 Dygraph.Circles.DEFAULT.apply(this, arguments);
631 };
632
633 var graph = document.getElementById("graph");
634
635 var testdata = [[10, 2], [11, 3], [12, NaN], [13, 2], [14, NaN], [15, 3]];
636 var graphOpts = {
637 labels: ['X', 'Y'],
638 valueRange: [0, 4],
639 drawPoints : false,
640 drawPointCallback : callback,
641 pointSize : 8
642 };
643
644 // Test that correct idx for isolated points are passed to the callback.
645 g = new Dygraph(graph, testdata, graphOpts);
646 assertEquals(2, indices.length);
647 assertEquals([3, 5],indices);
648
649 // Test that correct indices for isolated points + gap points are passed to the callback when
650 // drawGapEdgePoints is set. This should add one point at the right
651 // edge of the segment at x=11, but not at the graph edge at x=10.
652 indices = []; // Reset for new test
653 graphOpts.drawGapEdgePoints = true;
654 g = new Dygraph(graph, testdata, graphOpts);
655 assertEquals(3, indices.length);
656 assertEquals([1, 3, 5],indices);
657
658
659 //Test that correct indices are passed to the callback when zoomed in.
660 indices = []; // Reset for new test
661 graphOpts.dateWindow = [12.5,13.5]
662 graphOpts.drawPoints = true;
663 testdata = [[10, 2], [11, 3], [12, 4], [13, 2], [14, 5], [15, 3]];
664 g = new Dygraph(graph, testdata, graphOpts);
665 assertEquals(3, indices.length);
666 assertEquals([2, 3, 4],indices);
667 };
668
669 /**
670 * Test that the correct idx is returned for the point in the onHiglightCallback.
671 */
672 CallbackTestCase.prototype.testDrawHighlightPointCallback_idx = function() {
673 var idxToCheck = null;
674
675 var drawHighlightPointCallback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam,idx) {
676 idxToCheck = idx;
677 };
678 var testdata = [[1, 2], [2, 3], [3, NaN], [4, 2], [5, NaN], [6, 3]];
679 var graph = document.getElementById("graph");
680 var g = new Dygraph(graph, testdata,
681 {
682 drawHighlightPointCallback : drawHighlightPointCallback
683 });
684
685 assertNull(idxToCheck);
686 DygraphOps.dispatchMouseMove(g, 3, 0);
687 // check that NaN point is not highlighted
688 assertNull(idxToCheck);
689 DygraphOps.dispatchMouseMove(g, 1, 2);
690 // check that correct index is returned
691 assertEquals(0,idxToCheck);
692 DygraphOps.dispatchMouseMove(g, 6, 3);
693 assertEquals(5,idxToCheck);
694 };