From b67003a8fedfa39c6e30c4950b61c7631acac29e Mon Sep 17 00:00:00 2001 From: Tim Buckley Date: Mon, 7 Mar 2016 19:26:56 -0700 Subject: [PATCH] Add TooltipService to generate custom nvd3 tooltips This adds a new TooltipService with a method for generating custom tooltip functions for nvd3 charts. These functions can include arbitary information about the current datapoint rather than the plain numeric value that nvd3 displays by default. Change-Id: Ib769b76d6aaad0bb080116c55b5b8a8e0c30aa92 --- app/js/services/tooltip.js | 132 +++++++++++++++++++++++++++++ test/unit/services/tooltip_spec.js | 79 +++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 app/js/services/tooltip.js create mode 100644 test/unit/services/tooltip_spec.js diff --git a/app/js/services/tooltip.js b/app/js/services/tooltip.js new file mode 100644 index 00000000..0819ecad --- /dev/null +++ b/app/js/services/tooltip.js @@ -0,0 +1,132 @@ +'use strict'; + +var angular = require('angular'); + +var servicesModule = require('./_index.js'); + +function TooltipService() { + var service = {}; + + var render = function(dest, value, d) { + if (angular.isFunction(value)) { + value = value(d); + } + + if (angular.isElement(value)) { + dest.append(value); + } else { + dest.text(value); + } + + return dest; + }; + + var fill = function(dest, element, count) { + for (var i = 0; i < count; i++) { + dest.append(element.clone()); + } + }; + + service.generator = function(content, options) { + options = options || {}; + + return function(d) { + // partially render content first so we can determine the column count + var columns = 0; + var columnOffset = 0; + var partialContent = []; + + var table = angular.element(''); + table.addClass('osh-tooltip'); + if (options.addonClass) { + table.addClass(options.addonClass); + } + + angular.forEach(content, function(row) { + // row can be a function to output colum values (or more functions) + if (angular.isFunction(row)) { + row = row(d); + } + + var values = []; + angular.forEach(row, function(col, i) { + values.push(col); + if (i + 1 > columns) { + columns = i + 1; + } + }); + + partialContent.push(values); + }); + + // build the header, if any + if (options.title || options.header) { + var thead = angular.element(''); + + if (options.colors) { + columnOffset++; + } + + if (options.title) { + var tr = angular.element(''); + + var th = render( + angular.element(''); + fill(tr, angular.element(''); + angular.forEach(partialContent, function(row, rowIndex) { + var tr = angular.element(''); + + if (options.colors && options.colors[rowIndex]) { + var td = angular.element('
').attr('colspan', columns + columnOffset), + options.title, d); + tr.append(th); + thead.append(tr); + } + + if (options.header) { + var tr = angular.element('
'), columnOffset); + angular.forEach(options.header, function(title) { + tr.append(render(angular.element(''), title, d)); + }); + thead.append(tr); + } + + table.append(thead); + } + + // build the body + var tbody = angular.element('
'); + td.addClass('legend-color-guide'); + + var div = angular.element('
'); + div.css('background-color', options.colors[rowIndex]); + td.append(div); + tr.append(td); + + fill(tr, angular.element('
'), columnOffset - 1); + } else { + fill(tr, angular.element(''), columnOffset); + } + + angular.forEach(row, function(col, i) { + var td = render(angular.element(''), col, d); + if (row.length < columns && i == row.length - 1) { + // auto-set colspan for last entry in row + td.attr('colspan', columns - i); + } + + tr.append(td); + }); + + tbody.append(tr); + }); + table.append(tbody); + + return table[0].outerHTML; + }; + }; + + return service; +} + +servicesModule.service('tooltipService', TooltipService); diff --git a/test/unit/services/tooltip_spec.js b/test/unit/services/tooltip_spec.js new file mode 100644 index 00000000..f4946b64 --- /dev/null +++ b/test/unit/services/tooltip_spec.js @@ -0,0 +1,79 @@ +describe('TooltipService', function() { + var $compile; + var tooltipService; + var sampleData; + + beforeEach(function() { + module('app'); + module('app.services'); + + inject(function($injector) { + $compile = $injector.get('$compile'); + tooltipService = $injector.get('tooltipService'); + }); + + sampleData = { + index: 0, + value: 0, + color: 'green', + data: { custom: 123 } + }; + }); + + it('should generate a simple tooltip', function() { + var generator = tooltipService.generator([['Value']]); + var element = angular.element(generator(sampleData))[0]; + + expect(element.classList).toContain('osh-tooltip'); + expect(element.querySelectorAll('tr').length).toEqual(1); + expect(element.querySelectorAll('td').length).toEqual(1); + expect(element.querySelectorAll('th').length).toEqual(0); + }); + + it('should generate a tooltip with a header', function() { + var generator = tooltipService.generator([ + ['Value'] + ], { title: 'test', header: ['column'] }); + + var element = angular.element(generator(sampleData))[0]; + + expect(element.querySelectorAll('tr').length).toEqual(3); + expect(element.querySelectorAll('thead tr').length).toEqual(2); + expect(element.querySelectorAll('thead tr')[0].innerText).toEqual('test'); + expect(element.querySelectorAll('thead tr')[1].innerText).toEqual('column'); + expect(element.querySelector('tbody tr td').innerText).toEqual('Value'); + }); + + it('should generate a tooltip with custom columns', function() { + var generator = tooltipService.generator([ + ['Value', function(d) { return d.value; }], + ['Custom', function(d) { return d.data.custom; }] + ]); + var element = angular.element(generator(sampleData))[0]; + + expect(element.querySelectorAll('tr').length).toEqual(2); + expect(element.querySelectorAll('td').length).toEqual(4); + + var rows = element.querySelectorAll('tbody tr'); + expect(rows[0].children[0].innerText).toEqual('Value'); + expect(rows[0].children[1].innerText).toEqual('0'); + expect(rows[1].children[0].innerText).toEqual('Custom'); + expect(rows[1].children[1].innerText).toEqual('123'); + }); + + it('should generate a tooltip with colors', function() { + var generator = tooltipService.generator([ + ['Value', function(d) { return d.value; }], + ['Custom', function(d) { return d.data.custom; }] + ], { colors: ['red', 'blue'] }); + var element = angular.element(generator(sampleData))[0]; + + expect(element.querySelectorAll('tr').length).toEqual(2); + expect(element.querySelectorAll('td').length).toEqual(6); + + var guides = element.querySelectorAll('td:first-child.legend-color-guide'); + expect(guides.length).toEqual(2); + expect(guides[0].children[0].getAttribute('style')).toEqual('background-color: red;'); + expect(guides[1].children[0].getAttribute('style')).toEqual('background-color: blue;'); + }); +});