diff --git a/docs/access.asciidoc b/docs/access.asciidoc new file mode 100644 index 000000000000..373ed248a7a5 --- /dev/null +++ b/docs/access.asciidoc @@ -0,0 +1,2 @@ +[[access]] +== Accessing Kibana \ No newline at end of file diff --git a/docs/dashboard.asciidoc b/docs/dashboard.asciidoc new file mode 100644 index 000000000000..206115b8f452 --- /dev/null +++ b/docs/dashboard.asciidoc @@ -0,0 +1,2 @@ +[[dashboard]] +== Working with Dashboards \ No newline at end of file diff --git a/docs/discover.asciidoc b/docs/discover.asciidoc new file mode 100644 index 000000000000..28b326c456f3 --- /dev/null +++ b/docs/discover.asciidoc @@ -0,0 +1,2 @@ +[[discover]] +== Discovering your Data \ No newline at end of file diff --git a/docs/index.asciidoc b/docs/index.asciidoc new file mode 100644 index 000000000000..82636a717632 --- /dev/null +++ b/docs/index.asciidoc @@ -0,0 +1,22 @@ +[[kibana-guide]] += Kibana User Guide + + +include::introduction.asciidoc[] + +include::setup.asciidoc[] + +include::access.asciidoc[] + +include::discover.asciidoc[] + +include::visualize.asciidoc[] + +include::dashboard.asciidoc[] + +include::settings.asciidoc[] + + + + + diff --git a/docs/introduction.asciidoc b/docs/introduction.asciidoc new file mode 100644 index 000000000000..be12182f8351 --- /dev/null +++ b/docs/introduction.asciidoc @@ -0,0 +1,2 @@ +[[introduction]] +== Introduction \ No newline at end of file diff --git a/docs/settings.asciidoc b/docs/settings.asciidoc new file mode 100644 index 000000000000..01a38831b93a --- /dev/null +++ b/docs/settings.asciidoc @@ -0,0 +1,2 @@ +[[settings]] +== Configuring Kibana \ No newline at end of file diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc new file mode 100644 index 000000000000..df6b008b4c62 --- /dev/null +++ b/docs/setup.asciidoc @@ -0,0 +1,2 @@ +[[setup]] +== Getting Kibana Up and Running \ No newline at end of file diff --git a/docs/visualize.asciidoc b/docs/visualize.asciidoc new file mode 100644 index 000000000000..f43dc9e31202 --- /dev/null +++ b/docs/visualize.asciidoc @@ -0,0 +1,2 @@ +[[visualize]] +== Visualizing your Data \ No newline at end of file diff --git a/package.json b/package.json index 87ff5be776fa..f8950bb88391 100644 --- a/package.json +++ b/package.json @@ -47,14 +47,15 @@ "elasticsearch": "^3.1.1", "express": "~4.10.6", "glob": "^4.3.2", - "http-proxy": "^1.8.1", "jade": "~1.8.2", "js-yaml": "^3.2.5", "less-middleware": "1.0.x", "lodash": "^2.4.1", "morgan": "~1.5.1", "semver": "^4.2.0", - "serve-favicon": "~2.2.0" + "request": "^2.40.0", + "serve-favicon": "~2.2.0", + "ssl-root-cas": "^1.1.7" }, "devDependencies": { "connect": "~2.19.5", @@ -91,7 +92,6 @@ "opn": "~1.0.0", "path-browserify": "0.0.0", "progress": "^1.1.8", - "request": "^2.40.0", "requirejs": "~2.1.14", "rjs-build-analysis": "0.0.3", "simple-git": "^0.11.0", diff --git a/src/kibana/components/agg_types/controls/interval.html b/src/kibana/components/agg_types/controls/interval.html index dcd9d5a387ef..d7f56dfb7195 100644 --- a/src/kibana/components/agg_types/controls/interval.html +++ b/src/kibana/components/agg_types/controls/interval.html @@ -17,5 +17,6 @@ class="form-control" name="interval" min="0" + input-whole-number > diff --git a/src/kibana/components/config/_delayed_updater.js b/src/kibana/components/config/_delayed_updater.js index b8568975aee5..033a9a24583b 100644 --- a/src/kibana/components/config/_delayed_updater.js +++ b/src/kibana/components/config/_delayed_updater.js @@ -19,7 +19,6 @@ define(function (require) { if (updater.fired) return; updater.fired = true; - var method; var body; var updated = []; @@ -44,6 +43,9 @@ define(function (require) { queue.forEach(function (q) { q.resolve(resp); }); }, function (err) { queue.forEach(function (q) { q.reject(err); }); + }) + .finally(function () { + $rootScope.$emit('change:config', updated.concat(deleted)); }); }; @@ -68,7 +70,7 @@ define(function (require) { var defer = Promise.defer(); queue.push(defer); notify.log('config change: ' + key + ': ' + oldVal + ' -> ' + newVal); - $rootScope.$broadcast('change:config.' + key, newVal, oldVal); + $rootScope.$emit('change:config.' + key, newVal, oldVal); // reset the fire timer clearTimeout(timer); diff --git a/src/kibana/components/timepicker/quick_ranges.js b/src/kibana/components/timepicker/quick_ranges.js index 0baf75b30325..9e1f0641996b 100644 --- a/src/kibana/components/timepicker/quick_ranges.js +++ b/src/kibana/components/timepicker/quick_ranges.js @@ -25,7 +25,15 @@ define(function (require) { { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 2 }, { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 2 }, { from: 'now-7d', to: 'now', display: 'Last 7 days', section: 2 }, - { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 2 }, + + { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 3 }, + { from: 'now-60d', to: 'now', display: 'Last 60 days', section: 3 }, + { from: 'now-90d', to: 'now', display: 'Last 90 days', section: 3 }, + { from: 'now-6M', to: 'now', display: 'Last 6 months', section: 3 }, + { from: 'now-1y', to: 'now', display: 'Last 1 year', section: 3 }, + { from: 'now-2y', to: 'now', display: 'Last 2 years', section: 3 }, + { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 3 }, + ]); }); diff --git a/src/kibana/components/vislib/lib/layout/layout.js b/src/kibana/components/vislib/lib/layout/layout.js index 539640524c89..8a359eb21bb9 100644 --- a/src/kibana/components/vislib/lib/layout/layout.js +++ b/src/kibana/components/vislib/lib/layout/layout.js @@ -91,7 +91,7 @@ define(function (require) { } if (obj.splits) { - d3.select(this.el).select('.' + obj.class).call(obj.splits); + d3.select(this.el).select('.' + obj.class).call(obj.splits, obj.parent); } if (obj.children) { diff --git a/src/kibana/components/vislib/lib/layout/splits/pie_chart/chart_title_split.js b/src/kibana/components/vislib/lib/layout/splits/pie_chart/chart_title_split.js index fd260b530aad..a538ac2ce7de 100644 --- a/src/kibana/components/vislib/lib/layout/splits/pie_chart/chart_title_split.js +++ b/src/kibana/components/vislib/lib/layout/splits/pie_chart/chart_title_split.js @@ -9,7 +9,7 @@ define(function () { * if not data.rows or data.columns, return no chart titles */ - return function (selection) { + return function (selection, parent) { selection.each(function (data) { var div = d3.select(this); @@ -24,9 +24,9 @@ define(function () { .attr('class', 'chart-title'); if (data.rows) { - d3.select('.x-axis-chart-title').remove(); + d3.select(parent).select('.x-axis-chart-title').remove(); } else { - d3.select('.y-axis-chart-title').remove(); + d3.select(parent).select('.y-axis-chart-title').remove(); } return div; diff --git a/src/kibana/components/vislib/lib/x_axis.js b/src/kibana/components/vislib/lib/x_axis.js index fabd00c4d3e1..c5935a5d8332 100644 --- a/src/kibana/components/vislib/lib/x_axis.js +++ b/src/kibana/components/vislib/lib/x_axis.js @@ -208,10 +208,10 @@ define(function (require) { selection.each(function () { axis = d3.select(this); labels = axis.selectAll('.tick text'); - if (!ordered || ordered === undefined) { - axis.call(self.rotateAxisLabels()); - } else { + if (ordered && ordered.date) { axis.call(self.filterAxisLabels()); + } else { + axis.call(self.rotateAxisLabels()); } }); diff --git a/src/kibana/components/vislib/styles/_layout.less b/src/kibana/components/vislib/styles/_layout.less index e30bcc61327c..de885e421523 100644 --- a/src/kibana/components/vislib/styles/_layout.less +++ b/src/kibana/components/vislib/styles/_layout.less @@ -164,6 +164,7 @@ min-height: 15px; max-height: 15px; min-width: 20px; + overflow: hidden; } .x-axis-div { diff --git a/src/kibana/components/vislib/visualizations/area_chart.js b/src/kibana/components/vislib/visualizations/area_chart.js index 0d3a2b8268f2..76189b8df6fd 100644 --- a/src/kibana/components/vislib/visualizations/area_chart.js +++ b/src/kibana/components/vislib/visualizations/area_chart.js @@ -187,8 +187,10 @@ define(function (require) { // Append the bars circles = layer .selectAll('rect') - .data(function appendData(d) { - return d; + .data(function appendData(data) { + return data.filter(function isNotZero(d) { + return d.y !== 0; + }); }); // exit diff --git a/src/kibana/directives/input_whole_number.js b/src/kibana/directives/input_whole_number.js new file mode 100644 index 000000000000..ac5835d1b039 --- /dev/null +++ b/src/kibana/directives/input_whole_number.js @@ -0,0 +1,19 @@ +define(function (require) { + var module = require('modules').get('kibana'); + + module.directive('inputWholeNumber', function () { + return { + restrict: 'A', + require: 'ngModel', + link: function ($scope, $elem, attrs, ngModel) { + ngModel.$parsers.push(checkWholeNumber); + ngModel.$formatters.push(checkWholeNumber); + + function checkWholeNumber(value) { + ngModel.$setValidity('whole', value % 1 === 0); + return value; + } + } + }; + }); +}); \ No newline at end of file diff --git a/src/kibana/images/initial_load.gif b/src/kibana/images/initial_load.gif new file mode 100644 index 000000000000..2bde128cc5db Binary files /dev/null and b/src/kibana/images/initial_load.gif differ diff --git a/src/kibana/index.html b/src/kibana/index.html index 19c224b4cda1..aa89363b2988 100644 --- a/src/kibana/index.html +++ b/src/kibana/index.html @@ -7,6 +7,22 @@ Kibana 4 + + + + + +
+ +
- - - + + diff --git a/src/kibana/partials/saved_object_finder.html b/src/kibana/partials/saved_object_finder.html index 9a6f9a92351b..8c24ffd72b57 100644 --- a/src/kibana/partials/saved_object_finder.html +++ b/src/kibana/partials/saved_object_finder.html @@ -5,6 +5,7 @@
-
+

No results found

diff --git a/src/kibana/styles/main.less b/src/kibana/styles/main.less index bcc705550c5b..3ce7c1fde1a4 100644 --- a/src/kibana/styles/main.less +++ b/src/kibana/styles/main.less @@ -434,6 +434,10 @@ textarea { resize: vertical; } +.initial-load { + margin-top: 60px; +} + .field-collapse-toggle { color: #999; margin-left: 10px !important; diff --git a/src/kibana/utils/interval.js b/src/kibana/utils/interval.js index ef0d51f593ea..e78b3ce01348 100644 --- a/src/kibana/utils/interval.js +++ b/src/kibana/utils/interval.js @@ -3,6 +3,8 @@ define(function (require) { var moment = require('moment'); var datemath = require('utils/datemath'); + require('directives/input_whole_number'); + /** * Calculate a graph interval * diff --git a/src/server/config/kibana.yml b/src/server/config/kibana.yml index 10ab0bf10581..b651def2cd11 100644 --- a/src/server/config/kibana.yml +++ b/src/server/config/kibana.yml @@ -34,3 +34,7 @@ shard_timeout: 0 # certificate. verify_ssl: true +# If you need to provide a CA certificate for your Elasticsarech instance, put +# the path of the pem file here. +# ca: /path/to/your/CA.pem + diff --git a/src/server/routes/proxy.js b/src/server/routes/proxy.js index ddef350ac147..6c7508dd2f22 100644 --- a/src/server/routes/proxy.js +++ b/src/server/routes/proxy.js @@ -1,52 +1,104 @@ -var logger = require('../lib/logger'); -var express = require('express'); -var router = module.exports = express.Router(); -var httpProxy = require('http-proxy'); var config = require('../config'); +var request = require('request'); +var buffer = require('buffer'); +var querystring = require('querystring'); +var express = require('express'); +var _ = require('lodash'); +var fs = require('fs'); var url = require('url'); var target = url.parse(config.elasticsearch); -var proxy = new httpProxy.createProxyServer({}); -var buffer = require('buffer'); +var join = require('path').join; -proxy.on('proxyReq', function (proxyReq, req, res, options) { - // To support the elasticsearch_preserve_host feature we need to change the - // host header to the target host header. - if (config.kibana.elasticsearch_preserve_host) { - proxyReq.setHeader('host', target.host); + +// If the target is backed by an SSL and a CA is provided via the config +// then we need to inject the CA +var hasCustomCA = false; +if (/^https/.test(target.protocol) && config.kibana.ca) { + var sslRootCAs = require('ssl-root-cas/latest'); + sslRootCAs.inject(); + var ca = fs.readFileSync(config.kibana.ca, 'utf8'); + var https = require('https'); + https.globalAgent.options.ca.push(ca); + hasCustomCA = true; +} + +// Create the router +var router = module.exports = express.Router(); + +// We need to capture the raw body before moving on +router.use(function (req, res, next) { + req.rawBody = ''; + req.setEncoding('utf8'); + req.on('data', function (chunk) { + req.rawBody += chunk; + }); + req.on('end', next); +}); + +function getPort(req) { + var matches = req.headers.host.match(/:(\d+)/); + if (matches[1]) return matches[1]; + return req.connection.pair ? '443' : '80'; +} + +// Create the proxy middleware +router.use(function (req, res, next) { + + var uri = _.defaults({}, target); + var options = { + url: uri.protocol + '//' + uri.host + req.url, + method: req.method, + headers: _.defaults({ host: target.hostname }, req.headers), + strictSSL: config.kibana.verify_ssl, + timeout: config.kibana.request_timeout + }; + + + options.headers['x-forward-for'] = req.connection.remoteAddress || req.socket.remoteAddress; + options.headers['x-forward-port'] = getPort(req); + options.headers['x-forward-proto'] = req.connection.pair ? 'https' : 'http'; + + // If the server has a custom CA we need to add it to the agent options + if (hasCustomCA) { + options.agentOptions = { ca: https.globalAgent.options.ca }; + } + + // Only send the body if it's a PATCH, PUT, or POST + if (req.rawBody) { + options.body = req.rawBody; } // Support for handling basic auth if (config.kibana.elasticsearch_username && config.kibana.elasticsearch_password) { var code = new buffer.Buffer(config.kibana.elasticsearch_username + ':' + config.kibana.elasticsearch_password); var auth = 'Basic ' + code.toString('base64'); - proxyReq.setHeader('authorization', auth); - } -}); - -// Error handling for the proxy -proxy.on('error', function (err, req, res) { - var code = 502; - var body = { message: 'Bad Gateway' }; - - if (err.code === 'ECONNREFUSED') { - body.message = 'Unable to connect to Elasticsearch'; + options.headers.authorization = auth; } - if (err.message === 'DEPTH_ZERO_SELF_SIGNED_CERT') { - body.message = 'SSL handshake with Elasticsearch failed'; + // To support the elasticsearch_preserve_host feature we need to change the + // host header to the target host header. I don't quite understand the value + // of this... but it's a feature we had before so I guess we are keeping it. + if (config.kibana.elasticsearch_preserve_host) { + options.headers.host = target.host; } - res.writeHead(502, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(body)); -}); - -router.use(function (req, res, next) { - var options = { - target: config.elasticsearch, - secure: config.kibana.verify_ssl, - xfwd: true, - timeout: (config.kibana.request_timeout) - }; - proxy.web(req, res, options); + // Create the request and pipe the response + var esRequest = request(options); + esRequest.on('error', function (err) { + var code = 502; + var body = { message: 'Bad Gateway' }; + + if (err.code === 'ECONNREFUSED') { + body.message = 'Unable to connect to Elasticsearch'; + } + + if (err.message === 'DEPTH_ZERO_SELF_SIGNED_CERT') { + body.message = 'SSL handshake with Elasticsearch failed'; + } + + body.err = err.message; + res.status(code).json(body); + }); + esRequest.pipe(res); }); diff --git a/tasks/npm_install_kibana.js b/tasks/npm_install_kibana.js index 1d70b8997fed..d55380c4836f 100644 --- a/tasks/npm_install_kibana.js +++ b/tasks/npm_install_kibana.js @@ -4,7 +4,7 @@ module.exports = function (grunt) { grunt.registerTask('npm_install_kibana', 'NPM isntall kibana server into dist', function () { var done = this.async(); var cwd = join(grunt.config.get('build'), 'dist', 'kibana', 'src'); - var command = 'npm install --production'; + var command = 'npm install --production --no-optional'; var options = { cwd: cwd }; child_process.exec(command, options, function (err, stdout, stderr) { if (err) { diff --git a/tasks/test.js b/tasks/test.js index efe881034c77..941b5803a2cc 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -32,7 +32,6 @@ module.exports = function (grunt) { ]; addTestTask(tasks); if (process.env.TRAVIS) tasks.unshift('esvm:dev'); - addTestTask(tasks); grunt.task.run(tasks); }); diff --git a/test/unit/specs/directives/input_whole_number.js b/test/unit/specs/directives/input_whole_number.js new file mode 100644 index 000000000000..07c0226ec4df --- /dev/null +++ b/test/unit/specs/directives/input_whole_number.js @@ -0,0 +1,48 @@ +define(function (require) { + var angular = require('angular'); + require('directives/input_whole_number'); + + describe('Whole number input directive', function () { + var $compile, $rootScope; + var html = ''; + + beforeEach(module('kibana')); + + beforeEach(inject(function (_$compile_, _$rootScope_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + })); + + it('should allow whole numbers', function () { + var element = $compile(html)($rootScope); + + $rootScope.value = '123'; + $rootScope.$digest(); + expect(element.hasClass('ng-valid')).to.be.ok(); + + $rootScope.value = '1.0'; + $rootScope.$digest(); + expect(element.hasClass('ng-valid')).to.be.ok(); + + $rootScope.value = '-5.0'; + $rootScope.$digest(); + expect(element.hasClass('ng-valid')).to.be.ok(); + }); + + it('should disallow numbers with decimals', function () { + var element = $compile(html)($rootScope); + + $rootScope.value = '123.0'; + $rootScope.$digest(); + expect(element.hasClass('ng-valid')).to.be.ok(); + + $rootScope.value = '1.2'; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid')).to.be.ok(); + + $rootScope.value = '-5.5'; + $rootScope.$digest(); + expect(element.hasClass('ng-invalid')).to.be.ok(); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/specs/directives/timepicker.js b/test/unit/specs/directives/timepicker.js index 205a78385973..d72ee753909d 100644 --- a/test/unit/specs/directives/timepicker.js +++ b/test/unit/specs/directives/timepicker.js @@ -153,8 +153,8 @@ define(function (require) { $scope.$digest(); }); - it('should contain 3 lists of options', function (done) { - expect($elem.find('.kbn-timepicker-section .list-unstyled').length).to.be(3); + it('should contain 4 lists of options', function (done) { + expect($elem.find('.kbn-timepicker-section .list-unstyled').length).to.be(4); done(); }); diff --git a/test/unit/specs/vislib/fixture/_vis_fixture.js b/test/unit/specs/vislib/fixture/_vis_fixture.js index e0045309d928..d6b7a7620606 100644 --- a/test/unit/specs/vislib/fixture/_vis_fixture.js +++ b/test/unit/specs/vislib/fixture/_vis_fixture.js @@ -7,7 +7,7 @@ define(function (require) { $('body').append('
'); - var $el = $('.visualize-chart'); + var $el = $('.visualize-chart:last'); $el.width(1024); $el.height(300); diff --git a/test/unit/specs/vislib/visualizations/area_chart.js b/test/unit/specs/vislib/visualizations/area_chart.js index 4c094df94994..810009b1bd30 100644 --- a/test/unit/specs/vislib/visualizations/area_chart.js +++ b/test/unit/specs/vislib/visualizations/area_chart.js @@ -183,6 +183,23 @@ define(function (require) { expect($(chart.chartEl).find('circle').length).to.be.greaterThan(0); }); }); + + it('should not draw circles where d.y === 0', function () { + vis.handler.charts.forEach(function (chart) { + var series = chart.chartData.series; + var isZero = series.some(function (d) { + return d.y === 0; + }); + var circles = $.makeArray($(chart.chartEl).find('circle')); + var isNotDrawn = circles.some(function (d) { + return d.__data__.y === 0; + }); + + if (isZero) { + expect(isNotDrawn).to.be(false); + } + }); + }); }); // Cannot seem to get these tests to work on the box diff --git a/test/unit/specs/vislib/visualizations/column_chart.js b/test/unit/specs/vislib/visualizations/column_chart.js index 1e462c02e989..924e99bfb6b6 100644 --- a/test/unit/specs/vislib/visualizations/column_chart.js +++ b/test/unit/specs/vislib/visualizations/column_chart.js @@ -7,24 +7,24 @@ define(function (require) { // Data var series = require('vislib_fixtures/mock_data/date_histogram/_series'); var termsColumns = require('vislib_fixtures/mock_data/terms/_columns'); - var histogramRows = require('vislib_fixtures/mock_data/histogram/_rows'); + //var histogramRows = require('vislib_fixtures/mock_data/histogram/_rows'); var stackedSeries = require('vislib_fixtures/mock_data/date_histogram/_stacked_series'); var dataArray = [ series, termsColumns, - histogramRows, + //histogramRows, stackedSeries ]; var names = [ 'series', 'terms columns', - 'histogram rows', + //'histogram rows', 'stackedSeries' ]; var modes = [ 'stacked', 'grouped', - 'percentage', + //'percentage', 'stacked' ]; diff --git a/test/unit/specs/vislib/visualizations/pie_chart.js b/test/unit/specs/vislib/visualizations/pie_chart.js index eaee3b72b8c1..ab264773f136 100644 --- a/test/unit/specs/vislib/visualizations/pie_chart.js +++ b/test/unit/specs/vislib/visualizations/pie_chart.js @@ -47,6 +47,79 @@ define(function (require) { 120 ]; + describe('No global chart settings', function () { + var visLibParams1 = { + el: '
', + type: 'pie', + addLegend: true, + addTooltip: true + }; + var visLibParams2 = { + el: '
', + type: 'pie', + addLegend: true, + addTooltip: true + }; + var chart1; + var chart2; + var Vis; + var indexPattern; + var buildHierarchicalData; + var data1; + var data2; + + beforeEach(function () { + module('PieChartFactory'); + }); + + beforeEach(function () { + inject(function (d3, Private) { + chart1 = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams1); + chart2 = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams2); + Vis = Private(require('components/vis/vis')); + indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + buildHierarchicalData = Private(require('components/agg_response/hierarchical/build_hierarchical_data')); + require('css!components/vislib/styles/main'); + + var id_1 = 1; + var id_2 = 1; + var stubVis1 = new Vis(indexPattern, { + type: 'pie', + aggs: rowAgg + }); + var stubVis2 = new Vis(indexPattern, { + type: 'pie', + aggs: colAgg + }); + + // We need to set the aggs to a known value. + _.each(stubVis1.aggs, function (agg) { + agg.id = 'agg_' + id_1++; + }); + _.each(stubVis2.aggs, function (agg) { + agg.id = 'agg_' + id_2++; + }); + + data1 = buildHierarchicalData(stubVis1, fixtures.threeTermBuckets); + data2 = buildHierarchicalData(stubVis2, fixtures.threeTermBuckets); + + chart1.render(data1); + chart2.render(data2); + }); + }); + + afterEach(function () { + $('.visualize-chart').remove(); + chart1 = null; + chart2 = null; + }); + + it('should render chart titles for all charts', function () { + expect($(chart1.el).find('.y-axis-chart-title').length).to.be(1); + expect($(chart2.el).find('.x-axis-chart-title').length).to.be(1); + }); + }); + aggArray.forEach(function (dataAgg, i) { describe('Vislib PieChart Class Test Suite for ' + names[i] + ' data', function () { var visLibParams = {