From 083d564a7c56d2c67ce7bd8f55c923f6216d8824 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Fri, 2 Dec 2016 12:01:59 -0500 Subject: [PATCH 1/8] Modifying the dashboard grid to not unnecessarily set appStatus.dirty (#9307) --- .../public/dashboard/directives/grid.js | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/directives/grid.js b/src/core_plugins/kibana/public/dashboard/directives/grid.js index 7b744ba9a1572..2204ff6d2779e 100644 --- a/src/core_plugins/kibana/public/dashboard/directives/grid.js +++ b/src/core_plugins/kibana/public/dashboard/directives/grid.js @@ -36,20 +36,6 @@ app.directive('dashboardGrid', function ($compile, Notifier) { function init() { $el.addClass('gridster'); - // See issue https://github.com/elastic/kibana/issues/2138 and the - // subsequent fix for why we need to sort here. Short story is that - // gridster can fail to render widgets in the correct order, depending - // on the specific order of the panels. - // See https://github.com/ducksboard/gridster.js/issues/147 - // for some additional back story. - $state.panels.sort((a, b) => { - if (a.row === b.row) { - return a.col - b.col; - } else { - return a.row - b.row; - } - }); - gridster = $el.gridster({ max_cols: COLS, min_cols: COLS, @@ -85,7 +71,23 @@ app.directive('dashboardGrid', function ($compile, Notifier) { const added = _.difference(panels, currentPanels); if (removed.length) removed.forEach(removePanel); - if (added.length) added.forEach(addPanel); + if (added.length) { + // See issue https://github.com/elastic/kibana/issues/2138 and the + // subsequent fix for why we need to sort here. Short story is that + // gridster can fail to render widgets in the correct order, depending + // on the specific order of the panels. + // See https://github.com/ducksboard/gridster.js/issues/147 + // for some additional back story. + added.sort((a, b) => { + if (a.row === b.row) { + return a.col - b.col; + } else { + return a.row - b.row; + } + }); + + added.forEach(addPanel); + }; // ensure that every panel can be serialized now that we are done $state.panels.forEach(makePanelSerializeable); From d635c11c618c33afe0164e964bc7ab584b6e5dd1 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Fri, 2 Dec 2016 10:54:47 -0800 Subject: [PATCH 2/8] Update mappings for kibana index (#9280) * Update buildNum mapping from string/not_analyzed to keyword * Change index-pattern type mappings from string -> keyword * Updating unit test * Updating string mappings for various saved object types * Fixing mapping for index pattern title field This needs to be text because other types in the Kibana index also have a field named "title", whose mapping needs to be text. We can't have the same field have different mappings in different types within the same index. * Removing unused files * Removing test file because corresponding source file is gone now --- .../lib/__tests__/create_kibana_index.js | 4 +- .../lib/kibana_index_mappings.js | 3 +- .../dashboard/services/_saved_dashboard.js | 16 +-- .../discover/saved_searches/_saved_search.js | 8 +- .../saved_visualizations/_saved_vis.js | 8 +- .../lib/__tests__/init_default_field_props.js | 100 ------------------ .../server/lib/init_default_field_props.js | 50 --------- .../kibana/server/lib/mapping_overrides.js | 38 ------- .../timelion/public/services/_saved_sheet.js | 10 +- .../courier/saved_object/saved_object.js | 2 +- .../public/index_patterns/_index_pattern.js | 8 +- .../public/utils/__tests__/mapping_setup.js | 4 +- src/ui/public/utils/mapping_setup.js | 2 +- 13 files changed, 31 insertions(+), 222 deletions(-) delete mode 100644 src/core_plugins/kibana/server/lib/__tests__/init_default_field_props.js delete mode 100644 src/core_plugins/kibana/server/lib/init_default_field_props.js delete mode 100644 src/core_plugins/kibana/server/lib/mapping_overrides.js diff --git a/src/core_plugins/elasticsearch/lib/__tests__/create_kibana_index.js b/src/core_plugins/elasticsearch/lib/__tests__/create_kibana_index.js index d004186594f69..09b89472972a3 100644 --- a/src/core_plugins/elasticsearch/lib/__tests__/create_kibana_index.js +++ b/src/core_plugins/elasticsearch/lib/__tests__/create_kibana_index.js @@ -53,9 +53,7 @@ describe('plugins/elasticsearch', function () { expect(params.body.mappings.config.properties) .to.have.property('buildNum'); expect(params.body.mappings.config.properties.buildNum) - .to.have.property('type', 'string'); - expect(params.body.mappings.config.properties.buildNum) - .to.have.property('index', 'not_analyzed'); + .to.have.property('type', 'keyword'); }); }); diff --git a/src/core_plugins/elasticsearch/lib/kibana_index_mappings.js b/src/core_plugins/elasticsearch/lib/kibana_index_mappings.js index b9a8021686936..959cf2c1e8243 100644 --- a/src/core_plugins/elasticsearch/lib/kibana_index_mappings.js +++ b/src/core_plugins/elasticsearch/lib/kibana_index_mappings.js @@ -2,8 +2,7 @@ export const mappings = { config: { properties: { buildNum: { - type: 'string', - index: 'not_analyzed' + type: 'keyword' } } }, diff --git a/src/core_plugins/kibana/public/dashboard/services/_saved_dashboard.js b/src/core_plugins/kibana/public/dashboard/services/_saved_dashboard.js index e049b6995e90d..8712720e65cf0 100644 --- a/src/core_plugins/kibana/public/dashboard/services/_saved_dashboard.js +++ b/src/core_plugins/kibana/public/dashboard/services/_saved_dashboard.js @@ -46,20 +46,20 @@ module.factory('SavedDashboard', function (courier, config) { // if type:dashboard has no mapping, we push this mapping into ES SavedDashboard.mapping = { - title: 'string', + title: 'text', hits: 'integer', - description: 'string', - panelsJSON: 'string', - optionsJSON: 'string', - uiStateJSON: 'string', + description: 'text', + panelsJSON: 'keyword', + optionsJSON: 'keyword', + uiStateJSON: 'keyword', version: 'integer', timeRestore: 'boolean', - timeTo: 'string', - timeFrom: 'string', + timeTo: 'keyword', + timeFrom: 'keyword', refreshInterval: { type: 'object', properties: { - display: {type: 'string'}, + display: {type: 'keyword'}, pause: { type: 'boolean'}, section: { type: 'integer'}, value: { type: 'integer'} diff --git a/src/core_plugins/kibana/public/discover/saved_searches/_saved_search.js b/src/core_plugins/kibana/public/discover/saved_searches/_saved_search.js index 7ad9d46bfb54a..f0da5c5d06782 100644 --- a/src/core_plugins/kibana/public/discover/saved_searches/_saved_search.js +++ b/src/core_plugins/kibana/public/discover/saved_searches/_saved_search.js @@ -31,11 +31,11 @@ module.factory('SavedSearch', function (courier) { SavedSearch.type = 'search'; SavedSearch.mapping = { - title: 'string', - description: 'string', + title: 'text', + description: 'text', hits: 'integer', - columns: 'string', - sort: 'string', + columns: 'keyword', + sort: 'keyword', version: 'integer' }; diff --git a/src/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js index 0bf171cd90333..205a53e862931 100644 --- a/src/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ b/src/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js @@ -53,11 +53,11 @@ uiModules SavedVis.type = 'visualization'; SavedVis.mapping = { - title: 'string', + title: 'text', visState: 'json', - uiStateJSON: 'string', - description: 'string', - savedSearchId: 'string', + uiStateJSON: 'keyword', + description: 'text', + savedSearchId: 'keyword', version: 'integer' }; diff --git a/src/core_plugins/kibana/server/lib/__tests__/init_default_field_props.js b/src/core_plugins/kibana/server/lib/__tests__/init_default_field_props.js deleted file mode 100644 index 3a1d0d309407e..0000000000000 --- a/src/core_plugins/kibana/server/lib/__tests__/init_default_field_props.js +++ /dev/null @@ -1,100 +0,0 @@ -import initDefaultFieldProps from '../init_default_field_props'; -import expect from 'expect.js'; -import _ from 'lodash'; -let fields; - -const testData = [ - { - 'name': 'ip', - 'type': 'ip' - }, { - 'name': '@timestamp', - 'type': 'date' - }, { - 'name': 'agent', - 'type': 'string' - }, { - 'name': 'bytes', - 'type': 'number' - }, - { - 'name': 'geo.coordinates', - 'type': 'geo_point' - } -]; - -describe('initDefaultFieldProps', function () { - - beforeEach(function () { - fields = _.cloneDeep(testData); - }); - - it('should throw an error if no argument is passed or the argument is not an array', function () { - expect(initDefaultFieldProps).to.throwException(/requires an array argument/); - expect(initDefaultFieldProps).withArgs({}).to.throwException(/requires an array argument/); - }); - - it('should set the same defaults for everything but strings', function () { - const results = initDefaultFieldProps(fields); - _.forEach(results, function (field) { - if (field.type !== 'string') { - expect(field).to.have.property('indexed', true); - expect(field).to.have.property('analyzed', false); - expect(field).to.have.property('doc_values', true); - expect(field).to.have.property('scripted', false); - expect(field).to.have.property('count', 0); - } - }); - }); - - it('should make string fields analyzed', function () { - const results = initDefaultFieldProps(fields); - _.forEach(results, function (field) { - if (field.type === 'string' && !_.contains(field.name, 'keyword')) { - expect(field).to.have.property('indexed', true); - expect(field).to.have.property('analyzed', true); - expect(field).to.have.property('doc_values', false); - expect(field).to.have.property('scripted', false); - expect(field).to.have.property('count', 0); - } - }); - }); - - it('should create an extra raw non-analyzed field for strings', function () { - const results = initDefaultFieldProps(fields); - const rawField = _.find(results, function (field) { - return _.contains(field.name, 'keyword'); - }); - expect(rawField).to.have.property('indexed', true); - expect(rawField).to.have.property('analyzed', false); - expect(rawField).to.have.property('doc_values', true); - expect(rawField).to.have.property('scripted', false); - expect(rawField).to.have.property('count', 0); - }); - - it('should apply some overrides to metafields', function () { - const results = initDefaultFieldProps([{name: '_source'}, {name: '_timestamp'}]); - const expected = [ - { - name: '_source', - indexed: false, - analyzed: false, - doc_values: false, - count: 0, - scripted: false, - type: '_source' - }, - { - name: '_timestamp', - indexed: true, - analyzed: false, - doc_values: false, - count: 0, - scripted: false, - type: 'date' - } - ]; - - expect(_.isEqual(expected, results)).to.be.ok(); - }); -}); diff --git a/src/core_plugins/kibana/server/lib/init_default_field_props.js b/src/core_plugins/kibana/server/lib/init_default_field_props.js deleted file mode 100644 index a5b0486f5d32d..0000000000000 --- a/src/core_plugins/kibana/server/lib/init_default_field_props.js +++ /dev/null @@ -1,50 +0,0 @@ -import _ from 'lodash'; -import mappingOverrides from './mapping_overrides'; - -module.exports = function initDefaultFieldProps(fields) { - if (fields === undefined || !_.isArray(fields)) { - throw new Error('requires an array argument'); - } - - const results = []; - - _.forEach(fields, function (field) { - const newField = _.cloneDeep(field); - results.push(newField); - - if (newField.type === 'string') { - _.defaults(newField, { - indexed: true, - analyzed: true, - doc_values: false, - scripted: false, - count: 0 - }); - - results.push({ - name: newField.name + '.keyword', - type: 'string', - indexed: true, - analyzed: false, - doc_values: true, - scripted: false, - count: 0 - }); - } - else { - _.defaults(newField, { - indexed: true, - analyzed: false, - doc_values: true, - scripted: false, - count: 0 - }); - } - - if (mappingOverrides[newField.name]) { - _.assign(newField, mappingOverrides[newField.name]); - } - }); - - return results; -}; diff --git a/src/core_plugins/kibana/server/lib/mapping_overrides.js b/src/core_plugins/kibana/server/lib/mapping_overrides.js deleted file mode 100644 index ad944c0ed42af..0000000000000 --- a/src/core_plugins/kibana/server/lib/mapping_overrides.js +++ /dev/null @@ -1,38 +0,0 @@ -export default { - _source: { - type: '_source', - indexed: false, - analyzed: false, - doc_values: false - }, - _index: { - type: 'string', - indexed: false, - analyzed: false, - doc_values: false - }, - _type: { - type: 'string', - indexed: false, - analyzed: false, - doc_values: false - }, - _id: { - type: 'string', - indexed: false, - analyzed: false, - doc_values: false - }, - _timestamp: { - type: 'date', - indexed: true, - analyzed: false, - doc_values: false - }, - _score: { - type: 'number', - indexed: false, - analyzed: false, - doc_values: false - } -}; diff --git a/src/core_plugins/timelion/public/services/_saved_sheet.js b/src/core_plugins/timelion/public/services/_saved_sheet.js index bdede15570705..a59e3f4c8c403 100644 --- a/src/core_plugins/timelion/public/services/_saved_sheet.js +++ b/src/core_plugins/timelion/public/services/_saved_sheet.js @@ -38,12 +38,12 @@ module.factory('SavedSheet', function (courier, config) { // if type:sheet has no mapping, we push this mapping into ES SavedSheet.mapping = { - title: 'string', + title: 'text', hits: 'integer', - description: 'string', - timelion_sheet: 'string', - timelion_interval: 'string', - timelion_other_interval: 'string', + description: 'text', + timelion_sheet: 'keyword', + timelion_interval: 'keyword', + timelion_other_interval: 'keyword', timelion_chart_height: 'integer', timelion_columns: 'integer', timelion_rows: 'integer', diff --git a/src/ui/public/courier/saved_object/saved_object.js b/src/ui/public/courier/saved_object/saved_object.js index 38d53eb12e625..56e9f765dceae 100644 --- a/src/ui/public/courier/saved_object/saved_object.js +++ b/src/ui/public/courier/saved_object/saved_object.js @@ -100,7 +100,7 @@ export default function SavedObjectFactory(es, kbnIndex, Promise, Private, Notif properties: { // setup the searchSource mapping, even if it is not used but this type yet searchSourceJSON: { - type: 'string' + type: 'keyword' } } }; diff --git a/src/ui/public/index_patterns/_index_pattern.js b/src/ui/public/index_patterns/_index_pattern.js index d0bb4a21cd5d6..1fa06d4cf6eeb 100644 --- a/src/ui/public/index_patterns/_index_pattern.js +++ b/src/ui/public/index_patterns/_index_pattern.js @@ -38,14 +38,14 @@ export default function IndexPatternFactory(Private, Notifier, config, kbnIndex, }); const mapping = mappingSetup.expandShorthand({ - title: 'string', - timeFieldName: 'string', + title: 'text', + timeFieldName: 'keyword', notExpandable: 'boolean', - intervalName: 'string', + intervalName: 'keyword', fields: 'json', sourceFilters: 'json', fieldFormatMap: { - type: 'string', + type: 'keyword', _serialize(map = {}) { const serialized = _.transform(map, serialize); return _.isEmpty(serialized) ? undefined : angular.toJson(serialized); diff --git a/src/ui/public/utils/__tests__/mapping_setup.js b/src/ui/public/utils/__tests__/mapping_setup.js index 347c6c6c0b089..fbb23bdd3435a 100644 --- a/src/ui/public/utils/__tests__/mapping_setup.js +++ b/src/ui/public/utils/__tests__/mapping_setup.js @@ -23,9 +23,9 @@ describe('ui/utils/mapping_setup', function () { }); context('when type is json', function () { - it('returned object is type string', function () { + it('returned object is type keyword', function () { const mapping = mappingSetup.expandShorthand({ foo: 'json' }); - expect(mapping.foo.type).to.be('string'); + expect(mapping.foo.type).to.be('keyword'); }); it('returned object has _serialize function', function () { diff --git a/src/ui/public/utils/mapping_setup.js b/src/ui/public/utils/mapping_setup.js index fdca52ff7afc9..79078224c0ca3 100644 --- a/src/ui/public/utils/mapping_setup.js +++ b/src/ui/public/utils/mapping_setup.js @@ -45,7 +45,7 @@ define(function () { if (typeof val === 'string') val = { type: val }; if (val.type === 'json') { - val.type = 'string'; + val.type = 'keyword'; val._serialize = json._serialize; val._deserialize = json._deserialize; } From ac6c4cd3efe70401d04081cb668b2d1db921d15b Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Fri, 2 Dec 2016 18:56:22 -0500 Subject: [PATCH 3/8] improve tag cloud doc (#9356) --- docs/visualize.asciidoc | 1 + docs/visualize/tagcloud.asciidoc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/visualize.asciidoc b/docs/visualize.asciidoc index 5412591685701..d560c8f54a44f 100644 --- a/docs/visualize.asciidoc +++ b/docs/visualize.asciidoc @@ -33,6 +33,7 @@ different series. instructions. <>:: Display a single number. <>:: Display each source's contribution to a total. +<>:: Display words as a cloud in which the size of the word correspond to its importance <>:: Associate the results of an aggregation with geographic locations. Timeseries:: Compute and combine data from multiple time series diff --git a/docs/visualize/tagcloud.asciidoc b/docs/visualize/tagcloud.asciidoc index b06e57a722e47..f001485c6e0e6 100644 --- a/docs/visualize/tagcloud.asciidoc +++ b/docs/visualize/tagcloud.asciidoc @@ -1,5 +1,5 @@ [[tagcloud-chart]] -== Cloud Tag Charts +== Tag Clouds A tag cloud visualization is a visual representation of text data, typically used to visualize free form text. Tags are usually single words, and the importance of each tag is shown with font size or color. From 5b61fc346bd77d924128a28bf5da798708236979 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 5 Dec 2016 09:38:47 -0500 Subject: [PATCH 4/8] Tag Cloud should deal with empty responses correctly (#9354) tag cloud did not work with empty responses correctly. One of the side-effects is that the back button did not work properly. For example, pressing the back button would not empty out the screen and show stale clouds. This also caused a type-error. Empty configurations meant that we could not access any aggregation-configs to produce a label. --- src/core_plugins/tagcloud/public/tag_cloud.js | 17 ++++++++++++++++- .../tagcloud/public/tag_cloud_controller.js | 17 +++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/core_plugins/tagcloud/public/tag_cloud.js b/src/core_plugins/tagcloud/public/tag_cloud.js index e1e34691b8e0b..96a59108a8c3f 100644 --- a/src/core_plugins/tagcloud/public/tag_cloud.js +++ b/src/core_plugins/tagcloud/public/tag_cloud.js @@ -129,8 +129,23 @@ class TagCloud extends EventEmitter { const job = this._queue.pop(); this._inFlight = true; - this._onLayoutEnd(job); + if (job.words.length) { + this._onLayoutEnd(job); + } else { + this._emptyCloud(job); + } + + } + + _emptyCloud(job) { + this._svgGroup.selectAll('text').remove(); + this._cloudWidth = 0; + this._cloudHeight = 0; + this._allInViewBox = true; + this._inFlight = false; + this._currentJob = job; + this._processQueue(); } _onLayoutEnd(job) { diff --git a/src/core_plugins/tagcloud/public/tag_cloud_controller.js b/src/core_plugins/tagcloud/public/tag_cloud_controller.js index 4b5ac87773c69..1f9f50e72bb6d 100644 --- a/src/core_plugins/tagcloud/public/tag_cloud_controller.js +++ b/src/core_plugins/tagcloud/public/tag_cloud_controller.js @@ -21,13 +21,23 @@ module.controller('KbnTagCloudController', function ($scope, $element, Private, clickHandler({point: {aggConfigResult: aggConfigResult}}); }); tagCloud.on('renderComplete', () => { + + const truncatedMessage = containerNode.querySelector('.tagcloud-truncated-message'); + const incompleteMessage = containerNode.querySelector('.tagcloud-incomplete-message'); + + if (!$scope.vis.aggs[0] || !$scope.vis.aggs[1]) { + incompleteMessage.style.display = 'none'; + truncatedMessage.style.display = 'none'; + return; + } + const bucketName = containerNode.querySelector('.tagcloud-custom-label'); bucketName.innerHTML = `${$scope.vis.aggs[0].makeLabel()} - ${$scope.vis.aggs[1].makeLabel()}`; - const truncatedMessage = containerNode.querySelector('.tagcloud-truncated-message'); + truncatedMessage.style.display = truncated ? 'block' : 'none'; - const incompleteMessage = containerNode.querySelector('.tagcloud-incomplete-message'); + const status = tagCloud.getStatus(); if (TagCloud.STATUS.COMPLETE === status) { @@ -36,17 +46,20 @@ module.controller('KbnTagCloudController', function ($scope, $element, Private, incompleteMessage.style.display = 'block'; } + $element.trigger('renderComplete'); }); $scope.$watch('esResponse', async function (response) { if (!response) { + tagCloud.setData([]); return; } const tagsAggId = _.first(_.pluck($scope.vis.aggs.bySchemaName.segment, 'id')); if (!tagsAggId || !response.aggregations) { + tagCloud.setData([]); return; } From b19794585b3de29b801033b9f6ec4bfa9e1c1150 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Tue, 6 Dec 2016 12:55:17 +0100 Subject: [PATCH 5/8] Vislib Point Series updates (#9044) Multiple changes to the Vislib point series charts. Before each of the chart (bar/area/line) would be an independent unit responsible for drawing all of its parts (axes, titles). This change splits things up so we can have greater control and better code reuse. converting to ES6 moving axes out of each chart joining both axes types (x and y) into a common axis type allowing multiple axes allowing top/bottom/left/right axis positioning introducing new chart type 'point_series' which can combine bar/line/area series making each of the series (bar/line/area) direction independent (vertical/horizontal charts) --- .../kbn_vislib_vis_types/public/area.js | 4 +- .../kbn_vislib_vis_types/public/histogram.js | 4 +- .../kbn_vislib_vis_types/public/line.js | 4 +- .../kbn_vislib_vis_types/public/pie.js | 1 - src/fixtures/vislib/_vis_fixture.js | 1 - src/ui/public/vis/__tests__/_vis.js | 1 - src/ui/public/vislib/VISLIB.md | 24 + .../__tests__/components/zero_injection.js | 405 +++++--------- .../public/vislib/__tests__/lib/axis_title.js | 52 +- .../vislib/__tests__/lib/chart_title.js | 14 +- src/ui/public/vislib/__tests__/lib/data.js | 92 +--- .../public/vislib/__tests__/lib/dispatch.js | 2 +- .../vislib/__tests__/lib/handler/handler.js | 2 +- .../vislib/__tests__/lib/layout/layout.js | 26 +- .../__tests__/lib/layout/layout_types.js | 2 +- .../lib/layout/types/column_layout.js | 4 +- .../public/vislib/__tests__/lib/vis_config.js | 115 ++++ src/ui/public/vislib/__tests__/lib/x_axis.js | 86 +-- src/ui/public/vislib/__tests__/lib/y_axis.js | 148 ++--- src/ui/public/vislib/__tests__/vis.js | 2 +- .../__tests__/visualizations/area_chart.js | 57 +- .../vislib/__tests__/visualizations/chart.js | 16 +- .../__tests__/visualizations/column_chart.js | 45 +- .../__tests__/visualizations/line_chart.js | 23 +- .../__tests__/visualizations/pie_chart.js | 7 +- .../__tests__/visualizations/tile_maps/map.js | 2 +- .../visualizations/tile_maps/tile_map.js | 9 +- .../__tests__/visualizations/vis_types.js | 2 +- .../public/vislib/components/color/color.js | 2 +- .../vislib/components/color/color_palette.js | 2 +- .../vislib/components/color/mapped_colors.js | 2 +- .../vislib/components/labels/data_array.js | 2 +- .../public/vislib/components/labels/labels.js | 6 +- .../components/labels/pie/get_pie_names.js | 2 +- .../components/labels/pie/pie_labels.js | 4 +- .../components/zero_injection/inject_zeros.js | 34 +- .../zero_injection/ordered_x_keys.js | 2 +- .../components/zero_injection/uniq_keys.js | 2 +- .../zero_injection/zero_filled_array.js | 5 +- src/ui/public/vislib/lib/axis/axis.js | 324 +++++++++++ src/ui/public/vislib/lib/axis/axis_config.js | 187 +++++++ src/ui/public/vislib/lib/axis/axis_labels.js | 133 +++++ src/ui/public/vislib/lib/axis/axis_scale.js | 203 +++++++ src/ui/public/vislib/lib/axis/axis_title.js | 53 ++ src/ui/public/vislib/lib/axis/scale_modes.js | 10 + src/ui/public/vislib/lib/axis_title.js | 73 --- src/ui/public/vislib/lib/chart_title.js | 51 +- src/ui/public/vislib/lib/data.js | 393 +++----------- src/ui/public/vislib/lib/dispatch.js | 78 +-- .../vislib/lib/{handler => }/handler.js | 56 +- .../vislib/lib/handler/handler_types.js | 20 - src/ui/public/vislib/lib/handler/types/pie.js | 17 - .../vislib/lib/handler/types/point_series.js | 99 ---- .../vislib/lib/handler/types/tile_map.js | 24 - src/ui/public/vislib/lib/layout/layout.js | 51 +- .../public/vislib/lib/layout/layout_types.js | 12 +- .../layout/splits/column_chart/chart_split.js | 27 +- .../splits/column_chart/x_axis_split.js | 14 +- .../splits/column_chart/y_axis_split.js | 35 +- .../vislib/lib/layout/types/column_layout.js | 66 ++- .../vislib/lib/layout/types/map_layout.js | 2 +- .../vislib/lib/layout/types/pie_layout.js | 4 +- src/ui/public/vislib/lib/types/index.js | 21 + src/ui/public/vislib/lib/types/pie.js | 13 + .../public/vislib/lib/types/point_series.js | 142 +++++ src/ui/public/vislib/lib/types/tile_map.js | 19 + src/ui/public/vislib/lib/vis_config.js | 46 ++ src/ui/public/vislib/lib/x_axis.js | 513 ------------------ src/ui/public/vislib/lib/y_axis.js | 236 -------- src/ui/public/vislib/styles/_layout.less | 51 +- src/ui/public/vislib/styles/_svg.less | 4 - src/ui/public/vislib/vis.js | 36 +- src/ui/public/vislib/vislib.js | 20 +- src/ui/public/vislib/visualizations/_chart.js | 7 +- .../visualizations/_point_series_chart.js | 176 ------ .../vislib/visualizations/area_chart.js | 379 ------------- .../vislib/visualizations/column_chart.js | 329 ----------- .../vislib/visualizations/line_chart.js | 353 ------------ .../marker_types/geohash_grid.js | 2 +- .../visualizations/marker_types/heatmap.js | 2 +- .../marker_types/scaled_circles.js | 2 +- .../marker_types/shaded_circles.js | 2 +- .../public/vislib/visualizations/pie_chart.js | 15 +- .../vislib/visualizations/point_series.js | 248 +++++++++ .../point_series/_point_series.js | 102 ++++ .../visualizations/point_series/area_chart.js | 238 ++++++++ .../point_series/column_chart.js | 245 +++++++++ .../visualizations/point_series/line_chart.js | 216 ++++++++ .../point_series/series_types.js | 12 + .../public/vislib/visualizations/tile_map.js | 8 +- .../public/vislib/visualizations/vis_types.js | 14 +- .../__tests__/_vislib_renderbot.js | 7 +- src/ui/public/visualize/visualize_legend.js | 8 +- test/functional/apps/discover/_discover.js | 47 +- test/support/page_objects/discover_page.js | 60 +- test/support/page_objects/visualize_page.js | 4 +- 96 files changed, 3279 insertions(+), 3443 deletions(-) create mode 100644 src/ui/public/vislib/VISLIB.md create mode 100644 src/ui/public/vislib/__tests__/lib/vis_config.js create mode 100644 src/ui/public/vislib/lib/axis/axis.js create mode 100644 src/ui/public/vislib/lib/axis/axis_config.js create mode 100644 src/ui/public/vislib/lib/axis/axis_labels.js create mode 100644 src/ui/public/vislib/lib/axis/axis_scale.js create mode 100644 src/ui/public/vislib/lib/axis/axis_title.js create mode 100644 src/ui/public/vislib/lib/axis/scale_modes.js delete mode 100644 src/ui/public/vislib/lib/axis_title.js rename src/ui/public/vislib/lib/{handler => }/handler.js (79%) delete mode 100644 src/ui/public/vislib/lib/handler/handler_types.js delete mode 100644 src/ui/public/vislib/lib/handler/types/pie.js delete mode 100644 src/ui/public/vislib/lib/handler/types/point_series.js delete mode 100644 src/ui/public/vislib/lib/handler/types/tile_map.js create mode 100644 src/ui/public/vislib/lib/types/index.js create mode 100644 src/ui/public/vislib/lib/types/pie.js create mode 100644 src/ui/public/vislib/lib/types/point_series.js create mode 100644 src/ui/public/vislib/lib/types/tile_map.js create mode 100644 src/ui/public/vislib/lib/vis_config.js delete mode 100644 src/ui/public/vislib/lib/x_axis.js delete mode 100644 src/ui/public/vislib/lib/y_axis.js delete mode 100644 src/ui/public/vislib/visualizations/_point_series_chart.js delete mode 100644 src/ui/public/vislib/visualizations/area_chart.js delete mode 100644 src/ui/public/vislib/visualizations/column_chart.js delete mode 100644 src/ui/public/vislib/visualizations/line_chart.js create mode 100644 src/ui/public/vislib/visualizations/point_series.js create mode 100644 src/ui/public/vislib/visualizations/point_series/_point_series.js create mode 100644 src/ui/public/vislib/visualizations/point_series/area_chart.js create mode 100644 src/ui/public/vislib/visualizations/point_series/column_chart.js create mode 100644 src/ui/public/vislib/visualizations/point_series/line_chart.js create mode 100644 src/ui/public/vislib/visualizations/point_series/series_types.js diff --git a/src/core_plugins/kbn_vislib_vis_types/public/area.js b/src/core_plugins/kbn_vislib_vis_types/public/area.js index cf33aaef09a04..b2fb930eb613a 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/area.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/area.js @@ -16,7 +16,6 @@ export default function HistogramVisType(Private) { 'effect on the series above it.', params: { defaults: { - shareYAxis: true, addTooltip: true, addLegend: true, legendPosition: 'right', @@ -27,8 +26,7 @@ export default function HistogramVisType(Private) { times: [], addTimeMarker: false, defaultYExtents: false, - setYExtents: false, - yAxis: {} + setYExtents: false }, legendPositions: [{ value: 'left', diff --git a/src/core_plugins/kbn_vislib_vis_types/public/histogram.js b/src/core_plugins/kbn_vislib_vis_types/public/histogram.js index 3b3db63fddcdc..a91c510fe72d5 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/histogram.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/histogram.js @@ -14,7 +14,6 @@ export default function HistogramVisType(Private) { 'exact numbers or percentages. If you are not sure which chart you need, you could do worse than to start here.', params: { defaults: { - shareYAxis: true, addTooltip: true, addLegend: true, legendPosition: 'right', @@ -23,8 +22,7 @@ export default function HistogramVisType(Private) { times: [], addTimeMarker: false, defaultYExtents: false, - setYExtents: false, - yAxis: {} + setYExtents: false }, legendPositions: [{ value: 'left', diff --git a/src/core_plugins/kbn_vislib_vis_types/public/line.js b/src/core_plugins/kbn_vislib_vis_types/public/line.js index c061d6183237d..6d52d4ea9421b 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/line.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/line.js @@ -14,7 +14,6 @@ export default function HistogramVisType(Private) { 'Be careful with sparse sets as the connection between points can be misleading.', params: { defaults: { - shareYAxis: true, addTooltip: true, addLegend: true, legendPosition: 'right', @@ -27,8 +26,7 @@ export default function HistogramVisType(Private) { times: [], addTimeMarker: false, defaultYExtents: false, - setYExtents: false, - yAxis: {} + setYExtents: false }, legendPositions: [{ value: 'left', diff --git a/src/core_plugins/kbn_vislib_vis_types/public/pie.js b/src/core_plugins/kbn_vislib_vis_types/public/pie.js index 9a66fd9924ccb..670b6b290b869 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/pie.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/pie.js @@ -14,7 +14,6 @@ export default function HistogramVisType(Private) { 'Pro Tip: Pie charts are best used sparingly, and with no more than 7 slices per pie.', params: { defaults: { - shareYAxis: true, addTooltip: true, addLegend: true, legendPosition: 'right', diff --git a/src/fixtures/vislib/_vis_fixture.js b/src/fixtures/vislib/_vis_fixture.js index 79a82dfe52414..611bec8d7d35a 100644 --- a/src/fixtures/vislib/_vis_fixture.js +++ b/src/fixtures/vislib/_vis_fixture.js @@ -35,7 +35,6 @@ module.exports = function VislibFixtures(Private) { return function (visLibParams) { let Vis = Private(VislibVisProvider); return new Vis($visCanvas.new(), _.defaults({}, visLibParams || {}, { - shareYAxis: true, addTooltip: true, addLegend: true, defaultYExtents: false, diff --git a/src/ui/public/vis/__tests__/_vis.js b/src/ui/public/vis/__tests__/_vis.js index ad4aa0ad837e5..882f48f0b4ea0 100644 --- a/src/ui/public/vis/__tests__/_vis.js +++ b/src/ui/public/vis/__tests__/_vis.js @@ -93,7 +93,6 @@ describe('Vis Class', function () { expect(vis.params).to.have.property('addLegend', true); expect(vis.params).to.have.property('addTooltip', true); expect(vis.params).to.have.property('mode', 'stacked'); - expect(vis.params).to.have.property('shareYAxis', true); }); }); diff --git a/src/ui/public/vislib/VISLIB.md b/src/ui/public/vislib/VISLIB.md new file mode 100644 index 0000000000000..96dcc2ab8242d --- /dev/null +++ b/src/ui/public/vislib/VISLIB.md @@ -0,0 +1,24 @@ +# Vislib general overview + +`vis.js` constructor accepts vis parameters and render method accepts data. it exposes event emitter interface so we can listen to certain events like 'renderComplete'. + +`vis.render` will create 'lib/vis_config' to handle configuration (applying defaults etc) and then create 'lib/handler' which will take the work over. + +`vis/handler` will init all parts of the chart (based on visualization type) and call render method on each of the building blocks. + +## Visualizations + +Each base vis type (`lib/types`) can have a different layout defined (`lib/layout`) and different building blocks (pie charts dont have axes for example) + +All base visualizations extend from `visualizations/_chart` + +### Pie chart + +### Map + +### Point series chart + +`visualizations/point_series` takes care of drawing the point series chart (no axes or titles, just the chart itself). It creates all the series defined and calls render method on them. + +currently there are 3 series types available (line, area, bars), they all extend from `vislualizations/point_series/_point_series`. + diff --git a/src/ui/public/vislib/__tests__/components/zero_injection.js b/src/ui/public/vislib/__tests__/components/zero_injection.js index aefb1ab70452a..10ea2601e3784 100644 --- a/src/ui/public/vislib/__tests__/components/zero_injection.js +++ b/src/ui/public/vislib/__tests__/components/zero_injection.js @@ -11,157 +11,73 @@ import VislibComponentsZeroInjectionZeroFilledArrayProvider from 'ui/vislib/comp import VislibComponentsZeroInjectionZeroFillDataArrayProvider from 'ui/vislib/components/zero_injection/zero_fill_data_array'; describe('Vislib Zero Injection Module Test Suite', function () { - const dateHistogramRows = { - 'rows': [ - { - 'label': 'Top 5 @tags: success', - 'ordered': { - 'date': true, - 'interval': 60000, - 'min': 1418410540548, - 'max': 1418410936568 - }, - 'series': [ - { - 'label': 'jpg', - 'values': [ - { 'x': 1418410560000, 'y': 2 }, - { 'x': 1418410620000, 'y': 4 }, - { 'x': 1418410680000, 'y': 1 }, - { 'x': 1418410740000, 'y': 5 }, - { 'x': 1418410800000, 'y': 2 }, - { 'x': 1418410860000, 'y': 3 }, - { 'x': 1418410920000, 'y': 2 } - ] - }, - { - 'label': 'css', - 'values': [ - { 'x': 1418410560000, 'y': 1 }, - { 'x': 1418410620000, 'y': 3 }, - { 'x': 1418410680000, 'y': 1 }, - { 'x': 1418410740000, 'y': 4 }, - { 'x': 1418410800000, 'y': 2 } - ] - }, - { - 'label': 'gif', - 'values': [ - { 'x': 1418410500000, 'y': 1 }, - { 'x': 1418410680000, 'y': 3 }, - { 'x': 1418410740000, 'y': 2 } - ] - } - ] - }, - { - 'label': 'Top 5 @tags: info', - 'ordered': { - 'date': true, - 'interval': 60000, - 'min': 1418410540548, - 'max': 1418410936568 - }, - 'series': [ - { - 'label': 'jpg', - 'values': [ - { 'x': 1418410560000, 'y': 4 }, - { 'x': 1418410620000, 'y': 2 }, - { 'x': 1418410680000, 'y': 1 }, - { 'x': 1418410740000, 'y': 5 }, - { 'x': 1418410800000, 'y': 2 }, - { 'x': 1418410860000, 'y': 3 }, - { 'x': 1418410920000, 'y': 2 } - ] - }, - { - 'label': 'css', - 'values': [ - { 'x': 1418410620000, 'y': 3 }, - { 'x': 1418410680000, 'y': 1 }, - { 'x': 1418410740000, 'y': 4 }, - { 'x': 1418410800000, 'y': 2 } - ] - }, - { - 'label': 'gif', - 'values': [ - { 'x': 1418410500000, 'y': 1 } - ] - } - ] - }, - { - 'label': 'Top 5 @tags: security', - 'ordered': { - 'date': true, - 'interval': 60000, - 'min': 1418410540548, - 'max': 1418410936568 - }, - 'series': [ - { - 'label': 'jpg', - 'values': [ - { 'x': 1418410560000, 'y': 1 }, - { 'x': 1418410620000, 'y': 3 }, - { 'x': 1418410920000, 'y': 2 } - ] - }, - { - 'label': 'gif', - 'values': [ - { 'x': 1418410680000, 'y': 3 }, - { 'x': 1418410740000, 'y': 1 } - ] - } - ] - }, + const dateHistogramRows = [ + { + 'label': 'html', + 'values': [ + { 'x': 1418410560000, 'y': 2 }, + { 'x': 1418410620000, 'y': 4 }, + { 'x': 1418410680000, 'y': 1 }, + { 'x': 1418410740000, 'y': 5 }, + { 'x': 1418410800000, 'y': 2 }, + { 'x': 1418410860000, 'y': 3 }, + { 'x': 1418410920000, 'y': 2 } + ] + }, + { + 'label': 'css', + 'values': [ + { 'x': 1418410560000, 'y': 1 }, + { 'x': 1418410620000, 'y': 3 }, + { 'x': 1418410680000, 'y': 1 }, + { 'x': 1418410740000, 'y': 4 }, + { 'x': 1418410800000, 'y': 2 } + ] + } + ]; + + const dateHistogramRowsObj = { + series: [ { - 'label': 'Top 5 @tags: login', - 'ordered': { - 'date': true, - 'interval': 60000, - 'min': 1418410540548, - 'max': 1418410936568 - }, - 'series': [ - { - 'label': 'jpg', - 'values': [ - { 'x': 1418410740000, 'y': 1 } - ] - }, - { - 'label': 'css', - 'values': [ - { 'x': 1418410560000, 'y': 1 } - ] - } + 'label': 'html', + 'values': [ + {'x': 1418410560000, 'y': 2}, + {'x': 1418410620000, 'y': 4}, + {'x': 1418410680000, 'y': 1}, + {'x': 1418410740000, 'y': 5}, + {'x': 1418410800000, 'y': 2}, + {'x': 1418410860000, 'y': 3}, + {'x': 1418410920000, 'y': 2} ] }, { - 'label': 'Top 5 @tags: warning', - 'ordered': { - 'date': true, - 'interval': 60000, - 'min': 1418410540548, - 'max': 1418410936568 - }, - 'series': [ - { - 'label': 'jpg', - 'values': [ - { 'x': 1418410860000, 'y': 2 } - ] - } + 'label': 'css', + 'values': [ + {'x': 1418410560000, 'y': 1}, + {'x': 1418410620000, 'y': 3}, + {'x': 1418410680000, 'y': 1}, + {'x': 1418410740000, 'y': 4}, + {'x': 1418410800000, 'y': 2} ] } ] }; - const seriesData = { + + const seriesData = [ + { + label: '200', + values: [ + {x: 'v1', y: 234}, + {x: 'v2', y: 34}, + {x: 'v3', y: 834}, + {x: 'v4', y: 1234}, + {x: 'v5', y: 4} + ] + } + ]; + + const seriesDataObj = { series: [ { label: '200', @@ -176,7 +92,34 @@ describe('Vislib Zero Injection Module Test Suite', function () { ] }; - const multiSeriesData = { + const multiSeriesData = [ + { + label: '200', + values: [ + {x: '1', y: 234}, + {x: '2', y: 34}, + {x: '3', y: 834}, + {x: '4', y: 1234}, + {x: '5', y: 4} + ] + }, + { + label: '404', + values: [ + {x: '1', y: 1234}, + {x: '3', y: 234}, + {x: '5', y: 34} + ] + }, + { + label: '503', + values: [ + {x: '3', y: 834} + ] + } + ]; + + const multiSeriesDataObj = { series: [ { label: '200', @@ -205,7 +148,34 @@ describe('Vislib Zero Injection Module Test Suite', function () { ] }; - const multiSeriesNumberedData = { + const multiSeriesNumberedData = [ + { + label: '200', + values: [ + {x: 1, y: 234}, + {x: 2, y: 34}, + {x: 3, y: 834}, + {x: 4, y: 1234}, + {x: 5, y: 4} + ] + }, + { + label: '404', + values: [ + {x: 1, y: 1234}, + {x: 3, y: 234}, + {x: 5, y: 34} + ] + }, + { + label: '503', + values: [ + {x: 3, y: 834} + ] + } + ]; + + const multiSeriesNumberedDataObj = { series: [ { label: '200', @@ -263,101 +233,51 @@ describe('Vislib Zero Injection Module Test Suite', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { injectZeros = Private(VislibComponentsZeroInjectionInjectZerosProvider); - sample1 = injectZeros(seriesData); - sample2 = injectZeros(multiSeriesData); - sample3 = injectZeros(multiSeriesNumberedData); + sample1 = injectZeros(seriesData, seriesDataObj); + sample2 = injectZeros(multiSeriesData, multiSeriesDataObj); + sample3 = injectZeros(multiSeriesNumberedData, multiSeriesNumberedDataObj); })); - it('should throw an error if the input is not an object', function () { - expect(function () { - injectZeros(str); - }).to.throwError(); - - expect(function () { - injectZeros(number); - }).to.throwError(); - - expect(function () { - injectZeros(boolean); - }).to.throwError(); - - expect(function () { - injectZeros(emptyArray); - }).to.throwError(); - - expect(function () { - injectZeros(nullValue); - }).to.throwError(); - - expect(function () { - injectZeros(notAValue); - }).to.throwError(); - }); - - it('should throw an error if property series, rows, or columns is not ' + - 'present', function () { - - expect(function () { - injectZeros(childrenObject); - }).to.throwError(); - }); - - it('should not throw an error if object has property series, rows, or ' + - 'columns', function () { - - expect(function () { - injectZeros(seriesObject); - }).to.not.throwError(); - - expect(function () { - injectZeros(rowsObject); - }).to.not.throwError(); - - expect(function () { - injectZeros(columnsObject); - }).to.not.throwError(); - }); - it('should be a function', function () { expect(_.isFunction(injectZeros)).to.be(true); }); it('should return an object with series[0].values', function () { expect(_.isObject(sample1)).to.be(true); - expect(_.isObject(sample1.series[0].values)).to.be(true); + expect(_.isObject(sample1[0].values)).to.be(true); }); it('should return the same array of objects when the length of the series array is 1', function () { - expect(sample1.series[0].values[0].x).to.be(seriesData.series[0].values[0].x); - expect(sample1.series[0].values[1].x).to.be(seriesData.series[0].values[1].x); - expect(sample1.series[0].values[2].x).to.be(seriesData.series[0].values[2].x); - expect(sample1.series[0].values[3].x).to.be(seriesData.series[0].values[3].x); - expect(sample1.series[0].values[4].x).to.be(seriesData.series[0].values[4].x); + expect(sample1[0].values[0].x).to.be(seriesData[0].values[0].x); + expect(sample1[0].values[1].x).to.be(seriesData[0].values[1].x); + expect(sample1[0].values[2].x).to.be(seriesData[0].values[2].x); + expect(sample1[0].values[3].x).to.be(seriesData[0].values[3].x); + expect(sample1[0].values[4].x).to.be(seriesData[0].values[4].x); }); it('should inject zeros in the input array', function () { - expect(sample2.series[1].values[1].y).to.be(0); - expect(sample2.series[2].values[0].y).to.be(0); - expect(sample2.series[2].values[1].y).to.be(0); - expect(sample2.series[2].values[4].y).to.be(0); - expect(sample3.series[1].values[1].y).to.be(0); - expect(sample3.series[2].values[0].y).to.be(0); - expect(sample3.series[2].values[1].y).to.be(0); - expect(sample3.series[2].values[4].y).to.be(0); + expect(sample2[1].values[1].y).to.be(0); + expect(sample2[2].values[0].y).to.be(0); + expect(sample2[2].values[1].y).to.be(0); + expect(sample2[2].values[4].y).to.be(0); + expect(sample3[1].values[1].y).to.be(0); + expect(sample3[2].values[0].y).to.be(0); + expect(sample3[2].values[1].y).to.be(0); + expect(sample3[2].values[4].y).to.be(0); }); it('should return values arrays with the same x values', function () { - expect(sample2.series[1].values[0].x).to.be(sample2.series[2].values[0].x); - expect(sample2.series[1].values[1].x).to.be(sample2.series[2].values[1].x); - expect(sample2.series[1].values[2].x).to.be(sample2.series[2].values[2].x); - expect(sample2.series[1].values[3].x).to.be(sample2.series[2].values[3].x); - expect(sample2.series[1].values[4].x).to.be(sample2.series[2].values[4].x); + expect(sample2[1].values[0].x).to.be(sample2[2].values[0].x); + expect(sample2[1].values[1].x).to.be(sample2[2].values[1].x); + expect(sample2[1].values[2].x).to.be(sample2[2].values[2].x); + expect(sample2[1].values[3].x).to.be(sample2[2].values[3].x); + expect(sample2[1].values[4].x).to.be(sample2[2].values[4].x); }); it('should return values arrays of the same length', function () { - expect(sample2.series[0].values.length).to.be(sample2.series[1].values.length); - expect(sample2.series[0].values.length).to.be(sample2.series[2].values.length); - expect(sample2.series[1].values.length).to.be(sample2.series[2].values.length); + expect(sample2[0].values.length).to.be(sample2[1].values.length); + expect(sample2[0].values.length).to.be(sample2[2].values.length); + expect(sample2[1].values.length).to.be(sample2[2].values.length); }); }); @@ -369,36 +289,10 @@ describe('Vislib Zero Injection Module Test Suite', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { orderXValues = Private(VislibComponentsZeroInjectionOrderedXKeysProvider); - results = orderXValues(multiSeriesData); - numberedResults = orderXValues(multiSeriesNumberedData); + results = orderXValues(multiSeriesDataObj); + numberedResults = orderXValues(multiSeriesNumberedDataObj); })); - it('should throw an error if input is not an object', function () { - expect(function () { - orderXValues(str); - }).to.throwError(); - - expect(function () { - orderXValues(number); - }).to.throwError(); - - expect(function () { - orderXValues(boolean); - }).to.throwError(); - - expect(function () { - orderXValues(nullValue); - }).to.throwError(); - - expect(function () { - orderXValues(emptyArray); - }).to.throwError(); - - expect(function () { - orderXValues(notAValue); - }).to.throwError(); - }); - it('should return a function', function () { expect(_.isFunction(orderXValues)).to.be(true); }); @@ -422,8 +316,8 @@ describe('Vislib Zero Injection Module Test Suite', function () { it('should return an array of values ordered by their sum when orderBucketsBySum is true', function () { const orderBucketsBySum = true; - results = orderXValues(multiSeriesData, orderBucketsBySum); - numberedResults = orderXValues(multiSeriesNumberedData, orderBucketsBySum); + results = orderXValues(multiSeriesDataObj, orderBucketsBySum); + numberedResults = orderXValues(multiSeriesNumberedDataObj, orderBucketsBySum); expect(results[0]).to.be('3'); expect(results[1]).to.be('1'); @@ -445,7 +339,7 @@ describe('Vislib Zero Injection Module Test Suite', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { uniqueKeys = Private(VislibComponentsZeroInjectionUniqKeysProvider); - results = uniqueKeys(multiSeriesData); + results = uniqueKeys(multiSeriesDataObj); })); it('should throw an error if input is not an object', function () { @@ -494,7 +388,7 @@ describe('Vislib Zero Injection Module Test Suite', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { flattenData = Private(VislibComponentsZeroInjectionFlattenDataProvider); - results = flattenData(multiSeriesData); + results = flattenData(multiSeriesDataObj); })); it('should return a function', function () { @@ -666,24 +560,23 @@ describe('Vislib Zero Injection Module Test Suite', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { injectZeros = Private(VislibComponentsZeroInjectionInjectZerosProvider); - results = injectZeros(dateHistogramRows); + results = injectZeros(dateHistogramRows, dateHistogramRowsObj); })); it('should return an array of objects', function () { - results.rows.forEach(function (row) { - expect(_.isArray(row.series[0].values)).to.be(true); + results.forEach(function (row) { + expect(_.isArray(row.values)).to.be(true); }); }); it('should return ordered x values', function () { - const values = results.rows[0].series[0].values; + const values = results[0].values; expect(values[0].x).to.be.lessThan(values[1].x); expect(values[1].x).to.be.lessThan(values[2].x); expect(values[2].x).to.be.lessThan(values[3].x); expect(values[3].x).to.be.lessThan(values[4].x); expect(values[4].x).to.be.lessThan(values[5].x); expect(values[5].x).to.be.lessThan(values[6].x); - expect(values[6].x).to.be.lessThan(values[7].x); }); }); }); diff --git a/src/ui/public/vislib/__tests__/lib/axis_title.js b/src/ui/public/vislib/__tests__/lib/axis_title.js index e542392cef20a..37fb41aa87605 100644 --- a/src/ui/public/vislib/__tests__/lib/axis_title.js +++ b/src/ui/public/vislib/__tests__/lib/axis_title.js @@ -4,12 +4,16 @@ import _ from 'lodash'; import ngMock from 'ng_mock'; import expect from 'expect.js'; import $ from 'jquery'; -import VislibLibAxisTitleProvider from 'ui/vislib/lib/axis_title'; +import VislibLibAxisTitleProvider from 'ui/vislib/lib/axis/axis_title'; +import VislibLibAxisConfigProvider from 'ui/vislib/lib/axis/axis_config'; +import VislibLibVisConfigProvider from 'ui/vislib/lib/vis_config'; import VislibLibDataProvider from 'ui/vislib/lib/data'; import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state'; describe('Vislib AxisTitle Class Test Suite', function () { let AxisTitle; + let AxisConfig; + let VisConfig; let Data; let PersistedState; let axisTitle; @@ -79,6 +83,8 @@ describe('Vislib AxisTitle Class Test Suite', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { AxisTitle = Private(VislibLibAxisTitleProvider); + AxisConfig = Private(VislibLibAxisConfigProvider); + VisConfig = Private(VislibLibVisConfigProvider); Data = Private(VislibLibDataProvider); PersistedState = Private(PersistedStatePersistedStateProvider); @@ -86,20 +92,39 @@ describe('Vislib AxisTitle Class Test Suite', function () { .attr('class', 'vis-wrapper'); el.append('div') - .attr('class', 'y-axis-title') - .style('height', '20px') - .style('width', '20px'); + .attr('class', 'axis-wrapper-bottom') + .append('div') + .attr('class', 'axis-title y-axis-title') + .style('height', '20px') + .style('width', '20px'); el.append('div') - .attr('class', 'x-axis-title') - .style('height', '20px') - .style('width', '20px'); + .attr('class', 'axis-wrapper-left') + .append('div') + .attr('class', 'axis-title x-axis-title') + .style('height', '20px') + .style('width', '20px'); - dataObj = new Data(data, {}, new PersistedState()); - xTitle = dataObj.get('xAxisLabel'); - yTitle = dataObj.get('yAxisLabel'); - axisTitle = new AxisTitle($('.vis-wrapper')[0], xTitle, yTitle); + dataObj = new Data(data, new PersistedState()); + const visConfig = new VisConfig({ + type: 'histogram', + el: el.node() + }, data, new PersistedState()); + const xAxisConfig = new AxisConfig(visConfig, { + position: 'bottom', + title: { + text: dataObj.get('xAxisLabel') + } + }); + const yAxisConfig = new AxisConfig(visConfig, { + position: 'left', + title: { + text: dataObj.get('yAxisLabel') + } + }); + xTitle = new AxisTitle(xAxisConfig); + yTitle = new AxisTitle(yAxisConfig); })); afterEach(function () { @@ -108,7 +133,8 @@ describe('Vislib AxisTitle Class Test Suite', function () { describe('render Method', function () { beforeEach(function () { - axisTitle.render(); + xTitle.render(); + yTitle.render(); }); it('should append an svg to div', function () { @@ -129,7 +155,7 @@ describe('Vislib AxisTitle Class Test Suite', function () { describe('draw Method', function () { it('should be a function', function () { - expect(_.isFunction(axisTitle.draw())).to.be(true); + expect(_.isFunction(xTitle.draw())).to.be(true); }); }); diff --git a/src/ui/public/vislib/__tests__/lib/chart_title.js b/src/ui/public/vislib/__tests__/lib/chart_title.js index ef8a34b35641a..b309820541d36 100644 --- a/src/ui/public/vislib/__tests__/lib/chart_title.js +++ b/src/ui/public/vislib/__tests__/lib/chart_title.js @@ -6,11 +6,13 @@ import expect from 'expect.js'; import $ from 'jquery'; import VislibLibChartTitleProvider from 'ui/vislib/lib/chart_title'; import VislibLibDataProvider from 'ui/vislib/lib/data'; +import VislibLibVisConfigProvider from 'ui/vislib/lib/vis_config'; import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state'; describe('Vislib ChartTitle Class Test Suite', function () { let ChartTitle; let Data; + let VisConfig; let persistedState; let chartTitle; let el; @@ -78,6 +80,7 @@ describe('Vislib ChartTitle Class Test Suite', function () { beforeEach(ngMock.inject(function (Private) { ChartTitle = Private(VislibLibChartTitleProvider); Data = Private(VislibLibDataProvider); + VisConfig = Private(VislibLibVisConfigProvider); persistedState = new (Private(PersistedStatePersistedStateProvider))(); el = d3.select('body').append('div') @@ -88,8 +91,15 @@ describe('Vislib ChartTitle Class Test Suite', function () { .attr('class', 'chart-title') .style('height', '20px'); - dataObj = new Data(data, {}, persistedState); - chartTitle = new ChartTitle($('.vis-wrapper')[0], 'rows'); + dataObj = new Data(data, persistedState); + const visConfig = new VisConfig({ + type: 'histogram', + title: { + 'text': 'rows' + }, + el: el.node() + }, data, persistedState); + chartTitle = new ChartTitle(visConfig); })); afterEach(function () { diff --git a/src/ui/public/vislib/__tests__/lib/data.js b/src/ui/public/vislib/__tests__/lib/data.js index 8e108a06080f3..a1e5e51fa4d94 100644 --- a/src/ui/public/vislib/__tests__/lib/data.js +++ b/src/ui/public/vislib/__tests__/lib/data.js @@ -117,7 +117,7 @@ describe('Vislib Data Class Test Suite', function () { }); it('should return an object', function () { - const rowIn = new Data(rowsData, {}, persistedState); + const rowIn = new Data(rowsData, persistedState); expect(_.isObject(rowIn)).to.be(true); }); }); @@ -136,7 +136,7 @@ describe('Vislib Data Class Test Suite', function () { }; beforeEach(function () { - data = new Data(pieData, {}, persistedState); + data = new Data(pieData, persistedState); }); it('should remove zero values', function () { @@ -154,9 +154,9 @@ describe('Vislib Data Class Test Suite', function () { let colOut; beforeEach(function () { - serIn = new Data(seriesData, {}, persistedState); - rowIn = new Data(rowsData, {}, persistedState); - colIn = new Data(colsData, {}, persistedState); + serIn = new Data(seriesData, persistedState); + rowIn = new Data(rowsData, persistedState); + colIn = new Data(colsData, persistedState); serOut = serIn.flatten(); rowOut = rowIn.flatten(); colOut = colIn.flatten(); @@ -172,7 +172,7 @@ describe('Vislib Data Class Test Suite', function () { function testLength(inputData) { return function () { - const data = new Data(inputData, {}, persistedState); + const data = new Data(inputData, persistedState); const len = _.reduce(data.chartData(), function (sum, chart) { return sum + chart.series.reduce(function (sum, series) { return sum + series.values.length; @@ -184,80 +184,6 @@ describe('Vislib Data Class Test Suite', function () { } }); - describe('getYMin method', function () { - let visData; - let visDataNeg; - let visDataStacked; - const minValue = 4; - const minValueNeg = -41; - const minValueStacked = 15; - - beforeEach(function () { - visData = new Data(dataSeries, {}, persistedState); - visDataNeg = new Data(dataSeriesNeg, {}, persistedState); - visDataStacked = new Data(dataStacked, { type: 'histogram' }, persistedState); - }); - - // The first value in the time series is less than the min date in the - // date range. It also has the largest y value. This value should be excluded - // when calculating the Y max value since it falls outside of the range. - it('should return the Y domain min value', function () { - expect(visData.getYMin()).to.be(minValue); - expect(visDataNeg.getYMin()).to.be(minValueNeg); - expect(visDataStacked.getYMin()).to.be(minValueStacked); - }); - - it('should have a minimum date value that is greater than the max value within the date range', function () { - const series = _.pluck(visData.chartData(), 'series'); - const stackedSeries = _.pluck(visDataStacked.chartData(), 'series'); - expect(_.min(series.values, function (d) { return d.x; })).to.be.greaterThan(minValue); - expect(_.min(stackedSeries.values, function (d) { return d.x; })).to.be.greaterThan(minValueStacked); - }); - - it('allows passing a value getter for manipulating the values considered', function () { - const realMin = visData.getYMin(); - const multiplier = 13.2; - expect(visData.getYMin(function (d) { return d.y * multiplier; })).to.be(realMin * multiplier); - }); - }); - - describe('getYMax method', function () { - let visData; - let visDataNeg; - let visDataStacked; - const maxValue = 41; - const maxValueNeg = -4; - const maxValueStacked = 115; - - beforeEach(function () { - visData = new Data(dataSeries, {}, persistedState); - visDataNeg = new Data(dataSeriesNeg, {}, persistedState); - visDataStacked = new Data(dataStacked, { type: 'histogram' }, persistedState); - }); - - // The first value in the time series is less than the min date in the - // date range. It also has the largest y value. This value should be excluded - // when calculating the Y max value since it falls outside of the range. - it('should return the Y domain min value', function () { - expect(visData.getYMax()).to.be(maxValue); - expect(visDataNeg.getYMax()).to.be(maxValueNeg); - expect(visDataStacked.getYMax()).to.be(maxValueStacked); - }); - - it('should have a minimum date value that is greater than the max value within the date range', function () { - const series = _.pluck(visData.chartData(), 'series'); - const stackedSeries = _.pluck(visDataStacked.chartData(), 'series'); - expect(_.min(series, function (d) { return d.x; })).to.be.greaterThan(maxValue); - expect(_.min(stackedSeries, function (d) { return d.x; })).to.be.greaterThan(maxValueStacked); - }); - - it('allows passing a value getter for manipulating the values considered', function () { - const realMax = visData.getYMax(); - const multiplier = 13.2; - expect(visData.getYMax(function (d) { return d.y * multiplier; })).to.be(realMax * multiplier); - }); - }); - describe('geohashGrid methods', function () { let data; const geohashGridData = { @@ -298,7 +224,7 @@ describe('Vislib Data Class Test Suite', function () { }; beforeEach(function () { - data = new Data(geohashGridData, {}, persistedState); + data = new Data(geohashGridData, persistedState); }); describe('getVisData', function () { @@ -319,7 +245,7 @@ describe('Vislib Data Class Test Suite', function () { describe('null value check', function () { it('should return false', function () { - const data = new Data(rowsData, {}, persistedState); + const data = new Data(rowsData, persistedState); expect(data.hasNullValues()).to.be(false); }); @@ -335,7 +261,7 @@ describe('Vislib Data Class Test Suite', function () { ] }); - const data = new Data(nullRowData, {}, persistedState); + const data = new Data(nullRowData, persistedState); expect(data.hasNullValues()).to.be(true); }); }); diff --git a/src/ui/public/vislib/__tests__/lib/dispatch.js b/src/ui/public/vislib/__tests__/lib/dispatch.js index 5cd43aad46aa3..e5ba351dd3ddb 100644 --- a/src/ui/public/vislib/__tests__/lib/dispatch.js +++ b/src/ui/public/vislib/__tests__/lib/dispatch.js @@ -88,7 +88,7 @@ describe('Vislib Dispatch Class Test Suite', function () { it('returns a function that binds ' + event + ' events to a selection', function () { const chart = _.first(vis.handler.charts); - const apply = chart.events[name](d3.select(document.createElement('svg'))); + const apply = chart.events[name](chart.series[0].chartEl); expect(apply).to.be.a('function'); const els = getEls(vis.el, 3, 'div'); diff --git a/src/ui/public/vislib/__tests__/lib/handler/handler.js b/src/ui/public/vislib/__tests__/lib/handler/handler.js index b143ead5589e2..de24cf4349d6b 100644 --- a/src/ui/public/vislib/__tests__/lib/handler/handler.js +++ b/src/ui/public/vislib/__tests__/lib/handler/handler.js @@ -7,7 +7,7 @@ import columns from 'fixtures/vislib/mock_data/date_histogram/_columns'; import rows from 'fixtures/vislib/mock_data/date_histogram/_rows'; import stackedSeries from 'fixtures/vislib/mock_data/date_histogram/_stacked_series'; import $ from 'jquery'; -import VislibLibHandlerHandlerProvider from 'ui/vislib/lib/handler/handler'; +import VislibLibHandlerHandlerProvider from 'ui/vislib/lib/handler'; import FixturesVislibVisFixtureProvider from 'fixtures/vislib/_vis_fixture'; import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state'; const dateHistogramArray = [ diff --git a/src/ui/public/vislib/__tests__/lib/layout/layout.js b/src/ui/public/vislib/__tests__/lib/layout/layout.js index fa89a1ff34b1f..39c4d4e83294f 100644 --- a/src/ui/public/vislib/__tests__/lib/layout/layout.js +++ b/src/ui/public/vislib/__tests__/lib/layout/layout.js @@ -12,6 +12,8 @@ import $ from 'jquery'; import VislibLibLayoutLayoutProvider from 'ui/vislib/lib/layout/layout'; import FixturesVislibVisFixtureProvider from 'fixtures/vislib/_vis_fixture'; import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state'; +import VislibVisConfig from 'ui/vislib/lib/vis_config'; + const dateHistogramArray = [ series, columns, @@ -32,6 +34,7 @@ dateHistogramArray.forEach(function (data, i) { let persistedState; let numberOfCharts; let testLayout; + let VisConfig; beforeEach(ngMock.module('kibana')); @@ -40,6 +43,7 @@ dateHistogramArray.forEach(function (data, i) { Layout = Private(VislibLibLayoutLayoutProvider); vis = Private(FixturesVislibVisFixtureProvider)(); persistedState = new (Private(PersistedStatePersistedStateProvider))(); + VisConfig = Private(VislibVisConfig); vis.render(data, persistedState); numberOfCharts = vis.handler.charts.length; }); @@ -52,22 +56,26 @@ dateHistogramArray.forEach(function (data, i) { describe('createLayout Method', function () { it('should append all the divs', function () { expect($(vis.el).find('.vis-wrapper').length).to.be(1); - expect($(vis.el).find('.y-axis-col-wrapper').length).to.be(1); + expect($(vis.el).find('.y-axis-col-wrapper').length).to.be(2); expect($(vis.el).find('.vis-col-wrapper').length).to.be(1); - expect($(vis.el).find('.y-axis-col').length).to.be(1); - expect($(vis.el).find('.y-axis-title').length).to.be(1); - expect($(vis.el).find('.y-axis-div-wrapper').length).to.be(1); - expect($(vis.el).find('.y-axis-spacer-block').length).to.be(1); + expect($(vis.el).find('.y-axis-col').length).to.be(2); + expect($(vis.el).find('.y-axis-title').length).to.be(2); + expect($(vis.el).find('.y-axis-div-wrapper').length).to.be(2); + expect($(vis.el).find('.y-axis-spacer-block').length).to.be(4); expect($(vis.el).find('.chart-wrapper').length).to.be(numberOfCharts); - expect($(vis.el).find('.x-axis-wrapper').length).to.be(1); - expect($(vis.el).find('.x-axis-div-wrapper').length).to.be(1); - expect($(vis.el).find('.x-axis-title').length).to.be(1); + expect($(vis.el).find('.x-axis-wrapper').length).to.be(2); + expect($(vis.el).find('.x-axis-div-wrapper').length).to.be(2); + expect($(vis.el).find('.x-axis-title').length).to.be(2); }); }); describe('layout Method', function () { beforeEach(function () { - testLayout = new Layout(vis.el, vis.data, 'histogram'); + let visConfig = new VisConfig({ + el: vis.el, + type: 'histogram' + }, data, persistedState); + testLayout = new Layout(visConfig); }); it('should append a div with the correct class name', function () { diff --git a/src/ui/public/vislib/__tests__/lib/layout/layout_types.js b/src/ui/public/vislib/__tests__/lib/layout/layout_types.js index 9d0b10c6d0986..6be0e20534c78 100644 --- a/src/ui/public/vislib/__tests__/lib/layout/layout_types.js +++ b/src/ui/public/vislib/__tests__/lib/layout/layout_types.js @@ -11,7 +11,7 @@ describe('Vislib Layout Types Test Suite', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { layoutType = Private(VislibLibLayoutLayoutTypesProvider); - layoutFunc = layoutType.histogram; + layoutFunc = layoutType.point_series; })); it('should be an object', function () { diff --git a/src/ui/public/vislib/__tests__/lib/layout/types/column_layout.js b/src/ui/public/vislib/__tests__/lib/layout/types/column_layout.js index 578cd854cdcfc..3bd8fd9ac496f 100644 --- a/src/ui/public/vislib/__tests__/lib/layout/types/column_layout.js +++ b/src/ui/public/vislib/__tests__/lib/layout/types/column_layout.js @@ -72,7 +72,7 @@ describe('Vislib Column Layout Test Suite', function () { beforeEach(ngMock.inject(function (Private) { layoutType = Private(VislibLibLayoutLayoutTypesProvider); el = d3.select('body').append('div').attr('class', 'visualization'); - columnLayout = layoutType.histogram(el, data); + columnLayout = layoutType.point_series(el, data); })); afterEach(function () { @@ -85,6 +85,6 @@ describe('Vislib Column Layout Test Suite', function () { }); it('should throw an error when the wrong number or no arguments provided', function () { - expect(function () { layoutType.histogram(el); }).to.throwError(); + expect(function () { layoutType.point_series(el); }).to.throwError(); }); }); diff --git a/src/ui/public/vislib/__tests__/lib/vis_config.js b/src/ui/public/vislib/__tests__/lib/vis_config.js new file mode 100644 index 0000000000000..19497f6de03a6 --- /dev/null +++ b/src/ui/public/vislib/__tests__/lib/vis_config.js @@ -0,0 +1,115 @@ +import d3 from 'd3'; +import _ from 'lodash'; +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import VislibLibVisConfigProvider from 'ui/vislib/lib/vis_config'; +import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state'; + +describe('Vislib VisConfig Class Test Suite', function () { + let visConfig; + let el; + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458 + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8 + }, + { + x: 1408734090000, + y: 23 + }, + { + x: 1408734120000, + y: 30 + }, + { + x: 1408734150000, + y: 28 + }, + { + x: 1408734180000, + y: 36 + }, + { + x: 1408734210000, + y: 30 + }, + { + x: 1408734240000, + y: 26 + }, + { + x: 1408734270000, + y: 22 + }, + { + x: 1408734300000, + y: 29 + }, + { + x: 1408734330000, + y: 24 + } + ] + } + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count' + }; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + const VisConfig = Private(VislibLibVisConfigProvider); + const PersistedState = Private(PersistedStatePersistedStateProvider); + el = d3.select('body') + .attr('class', 'vis-wrapper') + .node(); + + visConfig = new VisConfig({ + type: 'point_series', + el: el + }, data, new PersistedState()); + })); + + describe('get Method', function () { + it('should be a function', function () { + expect(typeof visConfig.get).to.be('function'); + }); + + it('should get the property', function () { + expect(visConfig.get('el')).to.be(el); + expect(visConfig.get('type')).to.be('point_series'); + }); + + it('should return defaults if property does not exist', function () { + expect(visConfig.get('this.does.not.exist', 'defaults')).to.be('defaults'); + }); + + it('should throw an error if property does not exist and defaults were not provided', function () { + expect(function () { + visConfig.get('this.does.not.exist'); + }).to.throwError(); + }); + }); + + describe('set Method', function () { + it('should be a function', function () { + expect(typeof visConfig.set).to.be('function'); + }); + + it('should set a property', function () { + visConfig.set('this.does.not.exist', 'it.does.now'); + expect(visConfig.get('this.does.not.exist')).to.be('it.does.now'); + }); + }); +}); diff --git a/src/ui/public/vislib/__tests__/lib/x_axis.js b/src/ui/public/vislib/__tests__/lib/x_axis.js index 7b76513cf15a9..f5a37207c3ff2 100644 --- a/src/ui/public/vislib/__tests__/lib/x_axis.js +++ b/src/ui/public/vislib/__tests__/lib/x_axis.js @@ -6,16 +6,18 @@ import expect from 'expect.js'; import $ from 'jquery'; import VislibLibDataProvider from 'ui/vislib/lib/data'; import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state'; -import VislibLibXAxisProvider from 'ui/vislib/lib/x_axis'; +import VislibLibAxisProvider from 'ui/vislib/lib/axis'; +import VislibVisConfig from 'ui/vislib/lib/vis_config'; describe('Vislib xAxis Class Test Suite', function () { - let XAxis; + let Axis; let Data; let persistedState; let xAxis; let el; let fixture; let dataObj; + let VisConfig; const data = { hits: 621, ordered: { @@ -82,7 +84,8 @@ describe('Vislib xAxis Class Test Suite', function () { beforeEach(ngMock.inject(function (Private) { Data = Private(VislibLibDataProvider); persistedState = new (Private(PersistedStatePersistedStateProvider))(); - XAxis = Private(VislibLibXAxisProvider); + Axis = Private(VislibLibAxisProvider); + VisConfig = Private(VislibVisConfig); el = d3.select('body').append('div') .attr('class', 'x-axis-wrapper') @@ -91,15 +94,13 @@ describe('Vislib xAxis Class Test Suite', function () { fixture = el.append('div') .attr('class', 'x-axis-div'); - dataObj = new Data(data, {}, persistedState); - xAxis = new XAxis({ + let visConfig = new VisConfig({ el: $('.x-axis-div')[0], - xValues: dataObj.xValues(), - ordered: dataObj.get('ordered'), - xAxisFormatter: dataObj.get('xAxisFormatter'), - _attr: { - margin: { top: 0, right: 0, bottom: 0, left: 0 } - } + type: 'histogram' + }, data, persistedState); + xAxis = new Axis(visConfig, { + type: 'category', + id: 'CategoryAxis-1' }); })); @@ -126,7 +127,7 @@ describe('Vislib xAxis Class Test Suite', function () { }); }); - describe('getScale, getDomain, getTimeDomain, getOrdinalDomain, and getRange Methods', function () { + describe('getScale, getDomain, getTimeDomain, and getRange Methods', function () { let ordered; let timeScale; let timeDomain; @@ -136,28 +137,45 @@ describe('Vislib xAxis Class Test Suite', function () { let range; beforeEach(function () { - timeScale = xAxis.getScale(); - timeDomain = xAxis.getDomain(timeScale); - range = xAxis.getRange(timeDomain, width); - xAxis.ordered = {}; - ordinalScale = xAxis.getScale(); - ordinalDomain = ordinalScale.domain(['this', 'should', 'be', 'an', 'array']); width = $('.x-axis-div').width(); + xAxis.getAxis(width); + timeScale = xAxis.getScale(); + timeDomain = xAxis.axisScale.getExtents(); + range = xAxis.axisScale.getRange(width); }); it('should return a function', function () { expect(_.isFunction(timeScale)).to.be(true); - expect(_.isFunction(ordinalScale)).to.be(true); }); it('should return the correct domain', function () { - expect(_.isDate(timeDomain.domain()[0])).to.be(true); - expect(_.isDate(timeDomain.domain()[1])).to.be(true); + expect(_.isDate(timeScale.domain()[0])).to.be(true); + expect(_.isDate(timeScale.domain()[1])).to.be(true); }); it('should return the min and max dates', function () { - expect(timeDomain.domain()[0].toDateString()).to.be(new Date(1408734060000).toDateString()); - expect(timeDomain.domain()[1].toDateString()).to.be(new Date(1408734330000).toDateString()); + expect(timeScale.domain()[0].toDateString()).to.be(new Date(1408734060000).toDateString()); + expect(timeScale.domain()[1].toDateString()).to.be(new Date(1408734330000).toDateString()); + }); + + it('should return the correct range', function () { + expect(range[0]).to.be(0); + expect(range[1]).to.be(width); + }); + }); + + describe('getOrdinalDomain Method', function () { + let ordinalScale; + let ordinalDomain; + let width; + + beforeEach(function () { + width = $('.x-axis-div').width(); + xAxis.ordered = null; + xAxis.axisConfig.ordered = null; + xAxis.getAxis(width); + ordinalScale = xAxis.getScale(); + ordinalDomain = ordinalScale.domain(['this', 'should', 'be', 'an', 'array']); }); it('should return an ordinal scale', function () { @@ -168,11 +186,6 @@ describe('Vislib xAxis Class Test Suite', function () { it('should return an array of values', function () { expect(_.isArray(ordinalDomain.domain())).to.be(true); }); - - it('should return the correct range', function () { - expect(range.range()[0]).to.be(0); - expect(range.range()[1]).to.be(width); - }); }); describe('getXScale Method', function () { @@ -181,7 +194,8 @@ describe('Vislib xAxis Class Test Suite', function () { beforeEach(function () { width = $('.x-axis-div').width(); - xScale = xAxis.getXScale(width); + xAxis.getAxis(width); + xScale = xAxis.getScale(); }); it('should return a function', function () { @@ -205,19 +219,11 @@ describe('Vislib xAxis Class Test Suite', function () { beforeEach(function () { width = $('.x-axis-div').width(); - xAxis.getXAxis(width); - }); - - it('should create an xAxis function on the xAxis class', function () { - expect(_.isFunction(xAxis.xAxis)).to.be(true); - }); - - it('should create an xScale function on the xAxis class', function () { - expect(_.isFunction(xAxis.xScale)).to.be(true); + xAxis.getAxis(width); }); - it('should create an xAxisFormatter function on the xAxis class', function () { - expect(_.isFunction(xAxis.xAxisFormatter)).to.be(true); + it('should create an getScale function on the xAxis class', function () { + expect(_.isFunction(xAxis.getScale())).to.be(true); }); }); diff --git a/src/ui/public/vislib/__tests__/lib/y_axis.js b/src/ui/public/vislib/__tests__/lib/y_axis.js index 19e5ab0883146..90e177baf4508 100644 --- a/src/ui/public/vislib/__tests__/lib/y_axis.js +++ b/src/ui/public/vislib/__tests__/lib/y_axis.js @@ -5,7 +5,8 @@ import expect from 'expect.js'; import $ from 'jquery'; import VislibLibDataProvider from 'ui/vislib/lib/data'; import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state'; -import VislibLibYAxisProvider from 'ui/vislib/lib/y_axis'; +import VislibLibYAxisProvider from 'ui/vislib/lib/axis'; +import VislibVisConfig from 'ui/vislib/lib/vis_config'; let YAxis; let Data; @@ -14,6 +15,7 @@ let el; let buildYAxis; let yAxis; let yAxisDiv; +let VisConfig; const timeSeries = [ 1408734060000, @@ -72,22 +74,19 @@ function createData(seriesData) { yAxisDiv = el.append('div') .attr('class', 'y-axis-div'); - const dataObj = new Data(data, { - defaultYMin: true - }, persistedState); - buildYAxis = function (params) { - return new YAxis(_.merge({}, params, { + let visConfig = new VisConfig({ el: node, - yMin: dataObj.getYMin(), - yMax: dataObj.getYMax(), - _attr: { - margin: { top: 0, right: 0, bottom: 0, left: 0 }, + type: 'histogram' + }, data, persistedState); + return new YAxis(visConfig, _.merge({}, { + id: 'ValueAxis-1', + type: 'value', + scale: { defaultYMin: true, setYExtents: false, - yAxis: {} } - })); + }, params)); }; yAxis = buildYAxis(); @@ -100,19 +99,21 @@ describe('Vislib yAxis Class Test Suite', function () { Data = Private(VislibLibDataProvider); persistedState = new (Private(PersistedStatePersistedStateProvider))(); YAxis = Private(VislibLibYAxisProvider); + VisConfig = Private(VislibVisConfig); expect($('.y-axis-wrapper')).to.have.length(0); })); afterEach(function () { - el.remove(); - yAxisDiv.remove(); + if (el) { + el.remove(); + yAxisDiv.remove(); + } }); describe('render Method', function () { beforeEach(function () { createData(defaultGraphData); - expect(d3.select(yAxis.el).selectAll('.y-axis-div')).to.have.length(1); yAxis.render(); }); @@ -150,7 +151,8 @@ describe('Vislib yAxis Class Test Suite', function () { describe('API', function () { beforeEach(function () { createData(defaultGraphData); - yScale = yAxis.getYScale(height); + yAxis.getAxis(height); + yScale = yAxis.getScale(); }); it('should return a function', function () { @@ -158,25 +160,12 @@ describe('Vislib yAxis Class Test Suite', function () { }); }); - describe('should return log values', function () { - let domain; - let extents; - - it('should return 1', function () { - yAxis._attr.scale = 'log'; - extents = [0, 400]; - domain = yAxis._getExtents(extents); - - // Log scales have a yMin value of 1 - expect(domain[0]).to.be(1); - }); - }); - describe('positive values', function () { beforeEach(function () { graphData = defaultGraphData; createData(graphData); - yScale = yAxis.getYScale(height); + yAxis.getAxis(height); + yScale = yAxis.getScale(); }); @@ -196,7 +185,8 @@ describe('Vislib yAxis Class Test Suite', function () { [ -22, -8, -30, -4, 0, 0, -3, -22, -14, -24 ] ]; createData(graphData); - yScale = yAxis.getYScale(height); + yAxis.getAxis(height); + yScale = yAxis.getScale(); }); it('should have domain between min value and 0', function () { @@ -215,7 +205,8 @@ describe('Vislib yAxis Class Test Suite', function () { [ 22, 8, -30, -4, 0, 0, 3, -22, 14, 24 ] ]; createData(graphData); - yScale = yAxis.getYScale(height); + yAxis.getAxis(height); + yScale = yAxis.getScale(); }); it('should have domain between min and max values', function () { @@ -230,9 +221,11 @@ describe('Vislib yAxis Class Test Suite', function () { describe('validate user defined values', function () { beforeEach(function () { - yAxis._attr.mode = 'stacked'; - yAxis._attr.setYExtents = false; - yAxis._attr.yAxis = {}; + createData(defaultGraphData); + yAxis.axisConfig.set('scale.stacked', true); + yAxis.axisConfig.set('scale.setYExtents', false); + yAxis.getAxis(height); + yScale = yAxis.getScale(); }); it('should throw a NaN error', function () { @@ -240,17 +233,18 @@ describe('Vislib yAxis Class Test Suite', function () { const max = 12; expect(function () { - yAxis._validateUserExtents(min, max); + yAxis.axisScale.validateUserExtents(min, max); }).to.throwError(); }); it('should return a decimal value', function () { - yAxis._attr.mode = 'percentage'; - yAxis._attr.setYExtents = true; + yAxis.axisConfig.set('scale.mode', 'percentage'); + yAxis.axisConfig.set('scale.setYExtents', true); + yAxis.getAxis(height); domain = []; - domain[0] = yAxis._attr.yAxis.min = 20; - domain[1] = yAxis._attr.yAxis.max = 80; - const newDomain = yAxis._validateUserExtents(domain); + domain[0] = 20; + domain[1] = 80; + const newDomain = yAxis.axisScale.validateUserExtents(domain); expect(newDomain[0]).to.be(domain[0] / 100); expect(newDomain[1]).to.be(domain[1] / 100); @@ -258,7 +252,7 @@ describe('Vislib yAxis Class Test Suite', function () { it('should return the user defined value', function () { domain = [20, 50]; - const newDomain = yAxis._validateUserExtents(domain); + const newDomain = yAxis.axisScale.validateUserExtents(domain); expect(newDomain[0]).to.be(domain[0]); expect(newDomain[1]).to.be(domain[1]); @@ -271,7 +265,7 @@ describe('Vislib yAxis Class Test Suite', function () { const max = 12; expect(function () { - yAxis._validateAxisExtents(min, max); + yAxis.axisScale.validateAxisExtents(min, max); }).to.throwError(); }); @@ -280,7 +274,7 @@ describe('Vislib yAxis Class Test Suite', function () { const max = 10; expect(function () { - yAxis._validateAxisExtents(min, max); + yAxis.axisScale.validateAxisExtents(min, max); }).to.throwError(); }); }); @@ -291,16 +285,16 @@ describe('Vislib yAxis Class Test Suite', function () { it('should return a function', function () { fnNames.forEach(function (fnName) { - expect(yAxis._getScaleType(fnName)).to.be.a(Function); + expect(yAxis.axisScale.getD3Scale(fnName)).to.be.a(Function); }); // if no value is provided to the function, scale should default to a linear scale - expect(yAxis._getScaleType()).to.be.a(Function); + expect(yAxis.axisScale.getD3Scale()).to.be.a(Function); }); it('should throw an error if function name is undefined', function () { expect(function () { - yAxis._getScaleType('square'); + yAxis.axisScale.getD3Scale('square'); }).to.throwError(); }); }); @@ -308,18 +302,18 @@ describe('Vislib yAxis Class Test Suite', function () { describe('_logDomain method', function () { it('should throw an error', function () { expect(function () { - yAxis._logDomain(-10, -5); + yAxis.axisScale.logDomain(-10, -5); }).to.throwError(); expect(function () { - yAxis._logDomain(-10, 5); + yAxis.axisScale.logDomain(-10, 5); }).to.throwError(); expect(function () { - yAxis._logDomain(0, -5); + yAxis.axisScale.logDomain(0, -5); }).to.throwError(); }); it('should return a yMin value of 1', function () { - const yMin = yAxis._logDomain(0, 200)[0]; + const yMin = yAxis.axisScale.logDomain(0, 200)[0]; expect(yMin).to.be(1); }); }); @@ -330,35 +324,30 @@ describe('Vislib yAxis Class Test Suite', function () { let yScale; beforeEach(function () { createData(defaultGraphData); - mode = yAxis._attr.mode; yMax = yAxis.yMax; - yScale = yAxis.getYScale; }); afterEach(function () { - yAxis._attr.mode = mode; yAxis.yMax = yMax; - yAxis.getYScale = yScale; + yAxis = buildYAxis(); }); it('should use percentage format for percentages', function () { - yAxis._attr.mode = 'percentage'; - const tickFormat = yAxis.getYAxis().tickFormat(); + yAxis = buildYAxis({ + scale: { + mode: 'percentage' + } + }); + const tickFormat = yAxis.getAxis().tickFormat(); expect(tickFormat(1)).to.be('100%'); }); it('should use decimal format for small values', function () { yAxis.yMax = 1; - const tickFormat = yAxis.getYAxis().tickFormat(); + const tickFormat = yAxis.getAxis().tickFormat(); expect(tickFormat(0.8)).to.be('0.8'); }); - it('should throw an error if yScale is NaN', function () { - yAxis.getYScale = function () { return NaN; }; - expect(function () { - yAxis.getYAxis(); - }).to.throwError(); - }); }); describe('draw Method', function () { @@ -382,33 +371,4 @@ describe('Vislib yAxis Class Test Suite', function () { expect(yAxis.tickScale(20)).to.be(0); }); }); - - describe('#tickFormat()', function () { - const formatter = function () {}; - - it('returns a basic number formatter by default', function () { - const yAxis = buildYAxis(); - expect(yAxis.tickFormat()).to.not.be(formatter); - expect(yAxis.tickFormat()(1)).to.be('1'); - }); - - it('returns the yAxisFormatter when passed', function () { - const yAxis = buildYAxis({ - yAxisFormatter: formatter - }); - expect(yAxis.tickFormat()).to.be(formatter); - }); - - it('returns a percentage formatter when the vis is in percentage mode', function () { - const yAxis = buildYAxis({ - yAxisFormatter: formatter, - _attr: { - mode: 'percentage' - } - }); - - expect(yAxis.tickFormat()).to.not.be(formatter); - expect(yAxis.tickFormat()(1)).to.be('100%'); - }); - }); }); diff --git a/src/ui/public/vislib/__tests__/vis.js b/src/ui/public/vislib/__tests__/vis.js index 46a5d5b9903c9..e12a1ba3d1160 100644 --- a/src/ui/public/vislib/__tests__/vis.js +++ b/src/ui/public/vislib/__tests__/vis.js @@ -122,7 +122,7 @@ dataArray.forEach(function (data, i) { it('should get attribue values', function () { expect(vis.get('addLegend')).to.be(true); expect(vis.get('addTooltip')).to.be(true); - expect(vis.get('type')).to.be('histogram'); + expect(vis.get('type')).to.be('point_series'); }); }); diff --git a/src/ui/public/vislib/__tests__/visualizations/area_chart.js b/src/ui/public/vislib/__tests__/visualizations/area_chart.js index c99fd0af82efc..310df97bcdc9c 100644 --- a/src/ui/public/vislib/__tests__/visualizations/area_chart.js +++ b/src/ui/public/vislib/__tests__/visualizations/area_chart.js @@ -8,7 +8,7 @@ import notQuiteEnoughVariables from 'fixtures/vislib/mock_data/not_enough_data/_ import $ from 'jquery'; import FixturesVislibVisFixtureProvider from 'fixtures/vislib/_vis_fixture'; import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state'; -const someOtherVariables = { +const dataTypesArray = { 'series pos': require('fixtures/vislib/mock_data/date_histogram/_series'), 'series pos neg': require('fixtures/vislib/mock_data/date_histogram/_series_pos_neg'), 'series neg': require('fixtures/vislib/mock_data/date_histogram/_series_neg'), @@ -20,12 +20,13 @@ const someOtherVariables = { const visLibParams = { type: 'area', addLegend: true, - addTooltip: true + addTooltip: true, + mode: 'stacked' }; -_.forOwn(someOtherVariables, function (variablesAreCool, imaVariable) { - describe('Vislib Area Chart Test Suite for ' + imaVariable + ' Data', function () { +_.forOwn(dataTypesArray, function (dataType, dataTypeName) { + describe('Vislib Area Chart Test Suite for ' + dataTypeName + ' Data', function () { let vis; let persistedState; @@ -34,7 +35,7 @@ _.forOwn(someOtherVariables, function (variablesAreCool, imaVariable) { vis = Private(FixturesVislibVisFixtureProvider)(visLibParams); persistedState = new (Private(PersistedStatePersistedStateProvider))(); vis.on('brush', _.noop); - vis.render(variablesAreCool, persistedState); + vis.render(dataType, persistedState); })); afterEach(function () { @@ -50,9 +51,11 @@ _.forOwn(someOtherVariables, function (variablesAreCool, imaVariable) { it('should throw a Not Enough Data Error', function () { vis.handler.charts.forEach(function (chart) { - expect(function () { - chart.checkIfEnoughData(); - }).to.throwError(); + chart.series.forEach(function (series) { + expect(function () { + series.checkIfEnoughData(); + }).to.throwError(); + }); }); }); }); @@ -66,9 +69,11 @@ _.forOwn(someOtherVariables, function (variablesAreCool, imaVariable) { it('should not throw a Not Enough Data Error', function () { vis.handler.charts.forEach(function (chart) { - expect(function () { - chart.checkIfEnoughData(); - }).to.not.throwError(); + chart.series.forEach(function (series) { + expect(function () { + series.checkIfEnoughData(); + }).to.not.throwError(); + }); }); }); }); @@ -79,10 +84,10 @@ _.forOwn(someOtherVariables, function (variablesAreCool, imaVariable) { beforeEach(function () { vis.handler.charts.forEach(function (chart) { - stackedData = chart.stackData(chart.chartData); + stackedData = chart.chartData; - isStacked = stackedData.every(function (arr) { - return arr.every(function (d) { + isStacked = stackedData.series.every(function (arr) { + return arr.values.every(function (d) { return _.isNumber(d.y0); }); }); @@ -181,16 +186,17 @@ _.forOwn(someOtherVariables, function (variablesAreCool, imaVariable) { it('should return a yMin and yMax', function () { vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.yAxis; + const yAxis = chart.handler.valueAxes[0]; + const domain = yAxis.getScale().domain(); - expect(yAxis.domain[0]).to.not.be(undefined); - expect(yAxis.domain[1]).to.not.be(undefined); + expect(domain[0]).to.not.be(undefined); + expect(domain[1]).to.not.be(undefined); }); }); it('should render a zero axis line', function () { vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.yAxis; + const yAxis = chart.handler.valueAxes[0]; if (yAxis.yMin < 0 && yAxis.yMax > 0) { expect($(chart.chartEl).find('line.zero-line').length).to.be(1); @@ -216,17 +222,18 @@ _.forOwn(someOtherVariables, function (variablesAreCool, imaVariable) { describe('defaultYExtents is true', function () { beforeEach(function () { - vis._attr.defaultYExtents = true; - vis.render(variablesAreCool, persistedState); + vis.visConfigArgs.defaultYExtents = true; + vis.render(dataType, persistedState); }); it('should return yAxis extents equal to data extents', function () { vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.yAxis; - const yVals = [vis.handler.data.getYMin(), vis.handler.data.getYMax()]; - - expect(yAxis.domain[0]).to.equal(yVals[0]); - expect(yAxis.domain[1]).to.equal(yVals[1]); + const yAxis = chart.handler.valueAxes[0]; + const min = vis.handler.valueAxes[0].axisScale.getYMin(); + const max = vis.handler.valueAxes[0].axisScale.getYMax(); + const domain = yAxis.getScale().domain(); + expect(domain[0]).to.equal(min); + expect(domain[1]).to.equal(max); }); }); }); diff --git a/src/ui/public/vislib/__tests__/visualizations/chart.js b/src/ui/public/vislib/__tests__/visualizations/chart.js index 5cfcb0ae62596..32dcb76af4a57 100644 --- a/src/ui/public/vislib/__tests__/visualizations/chart.js +++ b/src/ui/public/vislib/__tests__/visualizations/chart.js @@ -4,11 +4,11 @@ import ngMock from 'ng_mock'; import VislibVisProvider from 'ui/vislib/vis'; import VislibLibDataProvider from 'ui/vislib/lib/data'; import PersistedStatePersistedStateProvider from 'ui/persisted_state/persisted_state'; -import VislibVisualizationsColumnChartProvider from 'ui/vislib/visualizations/column_chart'; +import VislibVisualizationsPieChartProvider from 'ui/vislib/visualizations/pie_chart'; import VislibVisualizationsChartProvider from 'ui/vislib/visualizations/_chart'; describe('Vislib _chart Test Suite', function () { - let ColumnChart; + let PieChart; let Chart; let Data; let persistedState; @@ -88,23 +88,23 @@ describe('Vislib _chart Test Suite', function () { Vis = Private(VislibVisProvider); Data = Private(VislibLibDataProvider); persistedState = new (Private(PersistedStatePersistedStateProvider))(); - ColumnChart = Private(VislibVisualizationsColumnChartProvider); + PieChart = Private(VislibVisualizationsPieChartProvider); Chart = Private(VislibVisualizationsChartProvider); el = d3.select('body').append('div').attr('class', 'column-chart'); config = { type: 'histogram', - shareYAxis: true, addTooltip: true, addLegend: true, - stack: d3.layout.stack(), + hasTimeField: true, + zeroFill: true }; vis = new Vis(el[0][0], config); - vis.data = new Data(data, config, persistedState); + vis.render(data, persistedState); - myChart = new ColumnChart(vis, el, chartData); + myChart = vis.handler.charts[0]; })); afterEach(function () { @@ -125,7 +125,7 @@ describe('Vislib _chart Test Suite', function () { myChart.destroy(); expect(function () { - myChart.draw(); + myChart.render(); }).to.throwError(); }); diff --git a/src/ui/public/vislib/__tests__/visualizations/column_chart.js b/src/ui/public/vislib/__tests__/visualizations/column_chart.js index dc45e737cbde9..35466967c8df9 100644 --- a/src/ui/public/vislib/__tests__/visualizations/column_chart.js +++ b/src/ui/public/vislib/__tests__/visualizations/column_chart.js @@ -37,7 +37,8 @@ dataTypesArray.forEach(function (dataType, i) { hasTimeField: true, addLegend: true, addTooltip: true, - mode: mode + mode: mode, + zeroFill: true }; beforeEach(ngMock.module('kibana')); @@ -58,17 +59,17 @@ dataTypesArray.forEach(function (dataType, i) { beforeEach(function () { vis.handler.charts.forEach(function (chart) { - stackedData = chart.stackData(chart.chartData); + stackedData = chart.chartData; - isStacked = stackedData.every(function (arr) { - return arr.every(function (d) { + isStacked = stackedData.series.every(function (arr) { + return arr.values.every(function (d) { return _.isNumber(d.y0); }); }); }); }); - it('should append a d.y0 key to the data object', function () { + it('should stack values', function () { expect(isStacked).to.be(true); }); }); @@ -88,17 +89,6 @@ dataTypesArray.forEach(function (dataType, i) { }); }); - describe('updateBars method', function () { - beforeEach(function () { - vis.handler._attr.mode = 'grouped'; - vis.render(vis.data, persistedState); - }); - - it('should returned grouped bars', function () { - vis.handler.charts.forEach(function (chart) {}); - }); - }); - describe('addBarEvents method', function () { function checkChart(chart) { const rect = $(chart.chartEl).find('.series rect').get(0); @@ -149,16 +139,17 @@ dataTypesArray.forEach(function (dataType, i) { it('should return a yMin and yMax', function () { vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.yAxis; + const yAxis = chart.handler.valueAxes[0]; + const domain = yAxis.getScale().domain(); - expect(yAxis.domain[0]).to.not.be(undefined); - expect(yAxis.domain[1]).to.not.be(undefined); + expect(domain[0]).to.not.be(undefined); + expect(domain[1]).to.not.be(undefined); }); }); it('should render a zero axis line', function () { vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.yAxis; + const yAxis = chart.handler.valueAxes[0]; if (yAxis.yMin < 0 && yAxis.yMax > 0) { expect($(chart.chartEl).find('line.zero-line').length).to.be(1); @@ -184,18 +175,18 @@ dataTypesArray.forEach(function (dataType, i) { describe('defaultYExtents is true', function () { beforeEach(function () { - vis._attr.defaultYExtents = true; + vis.visConfigArgs.defaultYExtents = true; vis.render(data, persistedState); }); it('should return yAxis extents equal to data extents', function () { vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.yAxis; - const min = vis.handler.data.getYMin(); - const max = vis.handler.data.getYMax(); - - expect(yAxis.domain[0]).to.equal(min); - expect(yAxis.domain[1]).to.equal(max); + const yAxis = chart.handler.valueAxes[0]; + const min = vis.handler.valueAxes[0].axisScale.getYMin(); + const max = vis.handler.valueAxes[0].axisScale.getYMax(); + const domain = yAxis.getScale().domain(); + expect(domain[0]).to.equal(min); + expect(domain[1]).to.equal(max); }); }); }); diff --git a/src/ui/public/vislib/__tests__/visualizations/line_chart.js b/src/ui/public/vislib/__tests__/visualizations/line_chart.js index 0b4049850e1b6..66ee574611fbf 100644 --- a/src/ui/public/vislib/__tests__/visualizations/line_chart.js +++ b/src/ui/public/vislib/__tests__/visualizations/line_chart.js @@ -132,16 +132,16 @@ describe('Vislib Line Chart', function () { it('should return a yMin and yMax', function () { vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.yAxis; - - expect(yAxis.domain[0]).to.not.be(undefined); - expect(yAxis.domain[1]).to.not.be(undefined); + const yAxis = chart.handler.valueAxes[0]; + const domain = yAxis.getScale().domain(); + expect(domain[0]).to.not.be(undefined); + expect(domain[1]).to.not.be(undefined); }); }); it('should render a zero axis line', function () { vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.yAxis; + const yAxis = chart.handler.valueAxes[0]; if (yAxis.yMin < 0 && yAxis.yMax > 0) { expect($(chart.chartEl).find('line.zero-line').length).to.be(1); @@ -167,17 +167,18 @@ describe('Vislib Line Chart', function () { describe('defaultYExtents is true', function () { beforeEach(function () { - vis._attr.defaultYExtents = true; + vis.visConfigArgs.defaultYExtents = true; vis.render(data, persistedState); }); it('should return yAxis extents equal to data extents', function () { vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.yAxis; - const yVals = [vis.handler.data.getYMin(), vis.handler.data.getYMax()]; - - expect(yAxis.domain[0]).to.equal(yVals[0]); - expect(yAxis.domain[1]).to.equal(yVals[1]); + const yAxis = chart.handler.valueAxes[0]; + const min = vis.handler.valueAxes[0].axisScale.getYMin(); + const max = vis.handler.valueAxes[0].axisScale.getYMax(); + const domain = yAxis.getScale().domain(); + expect(domain[0]).to.equal(min); + expect(domain[1]).to.equal(max); }); }); }); diff --git a/src/ui/public/vislib/__tests__/visualizations/pie_chart.js b/src/ui/public/vislib/__tests__/visualizations/pie_chart.js index ad4ece255a76f..4ddf502f744b9 100644 --- a/src/ui/public/vislib/__tests__/visualizations/pie_chart.js +++ b/src/ui/public/vislib/__tests__/visualizations/pie_chart.js @@ -1,5 +1,4 @@ import d3 from 'd3'; -import angular from 'angular'; import expect from 'expect.js'; import ngMock from 'ng_mock'; import _ from 'lodash'; @@ -140,16 +139,16 @@ describe('No global chart settings', function () { it('should throw an error when all charts contain zeros', function () { expect(function () { - chart1.ChartClass.prototype._validatePieData(allZeros); + chart1.handler.ChartClass.prototype._validatePieData(allZeros); }).to.throwError(); }); it('should not throw an error when only some or no charts contain zeros', function () { expect(function () { - chart1.ChartClass.prototype._validatePieData(someZeros); + chart1.handler.ChartClass.prototype._validatePieData(someZeros); }).to.not.throwError(); expect(function () { - chart1.ChartClass.prototype._validatePieData(noZeros); + chart1.handler.ChartClass.prototype._validatePieData(noZeros); }).to.not.throwError(); }); }); diff --git a/src/ui/public/vislib/__tests__/visualizations/tile_maps/map.js b/src/ui/public/vislib/__tests__/visualizations/tile_maps/map.js index 3366a4a21200e..777296976d58a 100644 --- a/src/ui/public/vislib/__tests__/visualizations/tile_maps/map.js +++ b/src/ui/public/vislib/__tests__/visualizations/tile_maps/map.js @@ -16,7 +16,7 @@ import VislibVisualizationsMapProvider from 'ui/vislib/visualizations/_map'; // ['rows', require('fixtures/vislib/mock_data/geohash/_rows')], // ]; -// // TODO: Test the specific behavior of each these +// TODO: Test the specific behavior of each these // const mapTypes = [ // 'Scaled Circle Markers', // 'Shaded Circle Markers', diff --git a/src/ui/public/vislib/__tests__/visualizations/tile_maps/tile_map.js b/src/ui/public/vislib/__tests__/visualizations/tile_maps/tile_map.js index 0803ad328bebd..8500ca9cd790f 100644 --- a/src/ui/public/vislib/__tests__/visualizations/tile_maps/tile_map.js +++ b/src/ui/public/vislib/__tests__/visualizations/tile_maps/tile_map.js @@ -1,4 +1,3 @@ -import angular from 'angular'; import expect from 'expect.js'; import ngMock from 'ng_mock'; import _ from 'lodash'; @@ -14,7 +13,13 @@ let TileMap; let extentsStub; function createTileMap(handler, chartEl, chartData) { - handler = handler || {}; + handler = handler || { + visConfig: { + get: function () { + return ''; + } + } + }; chartEl = chartEl || mockChartEl; chartData = chartData || geoJsonData; diff --git a/src/ui/public/vislib/__tests__/visualizations/vis_types.js b/src/ui/public/vislib/__tests__/visualizations/vis_types.js index a025fe13a7285..761e5a8aaf99d 100644 --- a/src/ui/public/vislib/__tests__/visualizations/vis_types.js +++ b/src/ui/public/vislib/__tests__/visualizations/vis_types.js @@ -11,7 +11,7 @@ describe('Vislib Vis Types Test Suite', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { visTypes = Private(VislibVisualizationsVisTypesProvider); - visFunc = visTypes.histogram; + visFunc = visTypes.point_series; })); it('should be an object', function () { diff --git a/src/ui/public/vislib/components/color/color.js b/src/ui/public/vislib/components/color/color.js index 3a7d4543fb7f9..70e53a33c7439 100644 --- a/src/ui/public/vislib/components/color/color.js +++ b/src/ui/public/vislib/components/color/color.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import VislibComponentsColorMappedColorsProvider from 'ui/vislib/components/color/mapped_colors'; +import VislibComponentsColorMappedColorsProvider from './mapped_colors'; export default function ColorUtilService(Private) { const mappedColors = Private(VislibComponentsColorMappedColorsProvider); diff --git a/src/ui/public/vislib/components/color/color_palette.js b/src/ui/public/vislib/components/color/color_palette.js index 5b4586a2642d8..3b8a95f6d29f2 100644 --- a/src/ui/public/vislib/components/color/color_palette.js +++ b/src/ui/public/vislib/components/color/color_palette.js @@ -1,6 +1,6 @@ import d3 from 'd3'; import _ from 'lodash'; -import VislibComponentsColorSeedColorsProvider from 'ui/vislib/components/color/seed_colors'; +import VislibComponentsColorSeedColorsProvider from './seed_colors'; export default function ColorPaletteUtilService(Private) { const seedColors = Private(VislibComponentsColorSeedColorsProvider); diff --git a/src/ui/public/vislib/components/color/mapped_colors.js b/src/ui/public/vislib/components/color/mapped_colors.js index d665dd5cae5a0..67644163bedb5 100644 --- a/src/ui/public/vislib/components/color/mapped_colors.js +++ b/src/ui/public/vislib/components/color/mapped_colors.js @@ -1,6 +1,6 @@ import _ from 'lodash'; import d3 from 'd3'; -import VislibComponentsColorColorPaletteProvider from 'ui/vislib/components/color/color_palette'; +import VislibComponentsColorColorPaletteProvider from './color_palette'; define((require) => (Private, config, $rootScope) => { const createColorPalette = Private(VislibComponentsColorColorPaletteProvider); diff --git a/src/ui/public/vislib/components/labels/data_array.js b/src/ui/public/vislib/components/labels/data_array.js index 1aae7dbe9816a..8872bd8993a30 100644 --- a/src/ui/public/vislib/components/labels/data_array.js +++ b/src/ui/public/vislib/components/labels/data_array.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import VislibComponentsLabelsFlattenSeriesProvider from 'ui/vislib/components/labels/flatten_series'; +import VislibComponentsLabelsFlattenSeriesProvider from './flatten_series'; export default function GetArrayUtilService(Private) { const flattenSeries = Private(VislibComponentsLabelsFlattenSeriesProvider); diff --git a/src/ui/public/vislib/components/labels/labels.js b/src/ui/public/vislib/components/labels/labels.js index dab5a5e1486a0..761f1ecfdbc7a 100644 --- a/src/ui/public/vislib/components/labels/labels.js +++ b/src/ui/public/vislib/components/labels/labels.js @@ -1,7 +1,7 @@ import _ from 'lodash'; -import VislibComponentsLabelsDataArrayProvider from 'ui/vislib/components/labels/data_array'; -import VislibComponentsLabelsUniqLabelsProvider from 'ui/vislib/components/labels/uniq_labels'; -import VislibComponentsLabelsPiePieLabelsProvider from 'ui/vislib/components/labels/pie/pie_labels'; +import VislibComponentsLabelsDataArrayProvider from './data_array'; +import VislibComponentsLabelsUniqLabelsProvider from './uniq_labels'; +import VislibComponentsLabelsPiePieLabelsProvider from './pie/pie_labels'; export default function LabelUtilService(Private) { const createArr = Private(VislibComponentsLabelsDataArrayProvider); diff --git a/src/ui/public/vislib/components/labels/pie/get_pie_names.js b/src/ui/public/vislib/components/labels/pie/get_pie_names.js index 22ac25a686fcf..c667f6822966d 100644 --- a/src/ui/public/vislib/components/labels/pie/get_pie_names.js +++ b/src/ui/public/vislib/components/labels/pie/get_pie_names.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import VislibComponentsLabelsPieReturnPieNamesProvider from 'ui/vislib/components/labels/pie/return_pie_names'; +import VislibComponentsLabelsPieReturnPieNamesProvider from './return_pie_names'; export default function GetPieNames(Private) { const returnNames = Private(VislibComponentsLabelsPieReturnPieNamesProvider); diff --git a/src/ui/public/vislib/components/labels/pie/pie_labels.js b/src/ui/public/vislib/components/labels/pie/pie_labels.js index 250563c6586e5..956882e40c652 100644 --- a/src/ui/public/vislib/components/labels/pie/pie_labels.js +++ b/src/ui/public/vislib/components/labels/pie/pie_labels.js @@ -1,6 +1,6 @@ import _ from 'lodash'; -import VislibComponentsLabelsPieRemoveZeroSlicesProvider from 'ui/vislib/components/labels/pie/remove_zero_slices'; -import VislibComponentsLabelsPieGetPieNamesProvider from 'ui/vislib/components/labels/pie/get_pie_names'; +import VislibComponentsLabelsPieRemoveZeroSlicesProvider from './remove_zero_slices'; +import VislibComponentsLabelsPieGetPieNamesProvider from './get_pie_names'; export default function PieLabels(Private) { const removeZeroSlices = Private(VislibComponentsLabelsPieRemoveZeroSlicesProvider); diff --git a/src/ui/public/vislib/components/zero_injection/inject_zeros.js b/src/ui/public/vislib/components/zero_injection/inject_zeros.js index 5c475de3a13cc..36a80d4b17fad 100644 --- a/src/ui/public/vislib/components/zero_injection/inject_zeros.js +++ b/src/ui/public/vislib/components/zero_injection/inject_zeros.js @@ -1,7 +1,7 @@ import _ from 'lodash'; -import VislibComponentsZeroInjectionOrderedXKeysProvider from 'ui/vislib/components/zero_injection/ordered_x_keys'; -import VislibComponentsZeroInjectionZeroFilledArrayProvider from 'ui/vislib/components/zero_injection/zero_filled_array'; -import VislibComponentsZeroInjectionZeroFillDataArrayProvider from 'ui/vislib/components/zero_injection/zero_fill_data_array'; +import VislibComponentsZeroInjectionOrderedXKeysProvider from './ordered_x_keys'; +import VislibComponentsZeroInjectionZeroFilledArrayProvider from './zero_filled_array'; +import VislibComponentsZeroInjectionZeroFillDataArrayProvider from './zero_fill_data_array'; export default function ZeroInjectionUtilService(Private) { const orderXValues = Private(VislibComponentsZeroInjectionOrderedXKeysProvider); @@ -19,30 +19,12 @@ export default function ZeroInjectionUtilService(Private) { * and injects zeros where needed. */ - function getDataArray(obj) { - if (obj.rows) { - return obj.rows; - } else if (obj.columns) { - return obj.columns; - } else if (obj.series) { - return [obj]; - } - } + return function (obj, data, orderBucketsBySum = false) { + const keys = orderXValues(data, orderBucketsBySum); - return function (obj, orderBucketsBySum = false) { - if (!_.isObject(obj) || !obj.rows && !obj.columns && !obj.series) { - throw new TypeError('ZeroInjectionUtilService expects an object with a series, rows, or columns key'); - } - - const keys = orderXValues(obj, orderBucketsBySum); - const arr = getDataArray(obj); - - arr.forEach(function (object) { - object.series.forEach(function (series) { - const zeroArray = createZeroFilledArray(keys); - - series.values = zeroFillDataArray(zeroArray, series.values); - }); + obj.forEach(function (series) { + const zeroArray = createZeroFilledArray(keys, series.label); + series.values = zeroFillDataArray(zeroArray, series.values); }); return obj; diff --git a/src/ui/public/vislib/components/zero_injection/ordered_x_keys.js b/src/ui/public/vislib/components/zero_injection/ordered_x_keys.js index 5754e8a07043f..c77cec73f2260 100644 --- a/src/ui/public/vislib/components/zero_injection/ordered_x_keys.js +++ b/src/ui/public/vislib/components/zero_injection/ordered_x_keys.js @@ -1,6 +1,6 @@ import _ from 'lodash'; import moment from 'moment'; -import VislibComponentsZeroInjectionUniqKeysProvider from 'ui/vislib/components/zero_injection/uniq_keys'; +import VislibComponentsZeroInjectionUniqKeysProvider from './uniq_keys'; export default function OrderedXKeysUtilService(Private) { const getUniqKeys = Private(VislibComponentsZeroInjectionUniqKeysProvider); diff --git a/src/ui/public/vislib/components/zero_injection/uniq_keys.js b/src/ui/public/vislib/components/zero_injection/uniq_keys.js index 1f6bf9187b36b..7a4e17a47c99c 100644 --- a/src/ui/public/vislib/components/zero_injection/uniq_keys.js +++ b/src/ui/public/vislib/components/zero_injection/uniq_keys.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import VislibComponentsZeroInjectionFlattenDataProvider from 'ui/vislib/components/zero_injection/flatten_data'; +import VislibComponentsZeroInjectionFlattenDataProvider from './flatten_data'; export default function UniqueXValuesUtilService(Private) { const flattenDataArray = Private(VislibComponentsZeroInjectionFlattenDataProvider); diff --git a/src/ui/public/vislib/components/zero_injection/zero_filled_array.js b/src/ui/public/vislib/components/zero_injection/zero_filled_array.js index ceeb4afc0c2f1..c8edf68912036 100644 --- a/src/ui/public/vislib/components/zero_injection/zero_filled_array.js +++ b/src/ui/public/vislib/components/zero_injection/zero_filled_array.js @@ -7,7 +7,7 @@ define(function () { * Returns a zero filled array. */ - return function (arr) { + return function (arr, label) { if (!_.isArray(arr)) { throw new Error('ZeroFilledArrayUtilService expects an array of strings or numbers'); } @@ -18,7 +18,8 @@ define(function () { zeroFilledArray.push({ x: val, xi: Infinity, - y: 0 + y: 0, + series: label }); }); diff --git a/src/ui/public/vislib/lib/axis/axis.js b/src/ui/public/vislib/lib/axis/axis.js new file mode 100644 index 0000000000000..19ec6bd6d3e5d --- /dev/null +++ b/src/ui/public/vislib/lib/axis/axis.js @@ -0,0 +1,324 @@ +import d3 from 'd3'; +import _ from 'lodash'; +import $ from 'jquery'; +import ErrorHandlerProvider from '../_error_handler'; +import AxisTitleProvider from './axis_title'; +import AxisLabelsProvider from './axis_labels'; +import AxisScaleProvider from './axis_scale'; +import AxisConfigProvider from './axis_config'; +import errors from 'ui/errors'; + +export default function AxisFactory(Private) { + const ErrorHandler = Private(ErrorHandlerProvider); + const AxisTitle = Private(AxisTitleProvider); + const AxisLabels = Private(AxisLabelsProvider); + const AxisScale = Private(AxisScaleProvider); + const AxisConfig = Private(AxisConfigProvider); + + class Axis extends ErrorHandler { + constructor(visConfig, axisConfigArgs) { + super(); + this.visConfig = visConfig; + + this.axisConfig = new AxisConfig(this.visConfig, axisConfigArgs); + if (this.axisConfig.get('type') === 'category') { + this.values = this.axisConfig.values; + this.ordered = this.axisConfig.ordered; + } + this.axisScale = new AxisScale(this.axisConfig, visConfig); + this.axisTitle = new AxisTitle(this.axisConfig); + this.axisLabels = new AxisLabels(this.axisConfig, this.axisScale); + + this.stack = d3.layout.stack() + .x(d => { + return d.x; + }) + .y(d => { + if (this.axisConfig.get('scale.offset') === 'expand') { + return Math.abs(d.y); + } + return d.y; + }) + .offset(this.axisConfig.get('scale.offset', 'zero')); + + const stackedMode = ['normal', 'grouped'].includes(this.axisConfig.get('scale.mode')); + if (stackedMode) { + this.stack.out((d, y0, y) => { + return this._stackNegAndPosVals(d, y0, y); + }); + } + } + + /** + * Returns true for positive numbers + */ + _isPositive(num) { + return num >= 0; + }; + + /** + * Returns true for negative numbers + */ + _isNegative(num) { + return num < 0; + }; + + /** + * Adds two input values + */ + _addVals(a, b) { + return a + b; + }; + + /** + * Returns the results of the addition of numbers in a filtered array. + */ + _sumYs(arr, callback) { + const filteredArray = arr.filter(callback); + + return (filteredArray.length) ? filteredArray.reduce(this._addVals) : 0; + }; + + /** + * Calculates the d.y0 value for stacked data in D3. + */ + _calcYZero(y, arr) { + if (y === 0 && this._lastY0) return this._sumYs(arr, this._lastY0 > 0 ? this._isPositive : this._isNegative); + if (y >= 0) return this._sumYs(arr, this._isPositive); + return this._sumYs(arr, this._isNegative); + }; + + _getCounts(i, j) { + const data = this.visConfig.data.chartData(); + const dataLengths = {}; + + dataLengths.charts = data.length; + dataLengths.stacks = dataLengths.charts ? data[i].series.length : 0; + dataLengths.values = dataLengths.stacks ? data[i].series[j].values.length : 0; + + return dataLengths; + }; + + _createCache() { + const cache = { + index: { + chart: 0, + stack: 0, + value: 0 + }, + yValsArr: [] + }; + + cache.count = this._getCounts(cache.index.chart, cache.index.stack); + + return cache; + }; + /** + * Stacking function passed to the D3 Stack Layout `.out` API. + * See: https://github.com/mbostock/d3/wiki/Stack-Layout + * It is responsible for calculating the correct d.y0 value for + * mixed datasets containing both positive and negative values. + */ + _stackNegAndPosVals(d, y0, y) { + const data = this.visConfig.data.chartData(); + + // Storing counters and data characteristics needed to stack values properly + if (!this._cache) { + this._cache = this._createCache(); + } + + d.y0 = this._calcYZero(y, this._cache.yValsArr); + if (d.y0 > 0) this._lastY0 = 1; + if (d.y0 < 0) this._lastY0 = -1; + ++this._cache.index.stack; + + + // last stack, or last value, reset the stack count and y value array + const lastStack = (this._cache.index.stack >= this._cache.count.stacks); + if (lastStack) { + this._cache.index.stack = 0; + ++this._cache.index.value; + this._cache.yValsArr = []; + // still building the stack collection, push v value to array + } else if (y !== 0) { + this._cache.yValsArr.push(y); + } + + // last value, prepare for the next chart, if one exists + const lastValue = (this._cache.index.value >= this._cache.count.values); + if (lastValue) { + this._cache.index.value = 0; + ++this._cache.index.chart; + + // no more charts, reset the queue and finish + if (this._cache.index.chart >= this._cache.count.charts) { + this._cache = this._createCache(); + return; + } + + // get stack and value count for next chart + const chartSeries = data[this._cache.index.chart].series; + this._cache.count.stacks = chartSeries.length; + this._cache.count.values = chartSeries.length ? chartSeries[this._cache.index.stack].values.length : 0; + } + }; + + render() { + const elSelector = this.axisConfig.get('elSelector'); + const rootEl = this.axisConfig.get('rootEl'); + d3.select(rootEl).selectAll(elSelector).call(this.draw()); + } + + destroy() { + const elSelector = this.axisConfig.get('elSelector'); + const rootEl = this.axisConfig.get('rootEl'); + $(rootEl).find(elSelector).find('svg').remove(); + } + + getAxis(length) { + const scale = this.axisScale.getScale(length); + const position = this.axisConfig.get('position'); + const axisFormatter = this.axisConfig.get('labels.axisFormatter'); + + return d3.svg.axis() + .scale(scale) + .tickFormat(axisFormatter) + .ticks(this.tickScale(length)) + .orient(position); + } + + getScale() { + return this.axisScale.scale; + } + + addInterval(interval) { + return this.axisScale.addInterval(interval); + } + + substractInterval(interval) { + return this.axisScale.substractInterval(interval); + } + + tickScale(length) { + const yTickScale = d3.scale.linear() + .clamp(true) + .domain([20, 40, 1000]) + .range([0, 3, 11]); + + return Math.ceil(yTickScale(length)); + } + + getLength(el) { + if (this.axisConfig.isHorizontal()) { + return $(el).width(); + } else { + return $(el).height(); + } + } + + adjustSize() { + const config = this.axisConfig; + const style = config.get('style'); + const margin = this.visConfig.get('style.margin'); + const chartEl = this.visConfig.get('el'); + const position = config.get('position'); + const axisPadding = 5; + + return function (selection) { + const text = selection.selectAll('.tick text'); + const lengths = []; + + text.each(function textWidths() { + lengths.push((() => { + if (config.isHorizontal()) { + return d3.select(this.parentNode).node().getBBox().height; + } else { + return d3.select(this.parentNode).node().getBBox().width; + } + })()); + }); + let length = lengths.length > 0 ? _.max(lengths) : 0; + length += axisPadding; + + if (config.isHorizontal()) { + selection.attr('height', Math.ceil(length)); + if (position === 'top') { + selection.select('g') + .attr('transform', `translate(0, ${length - parseInt(style.lineWidth)})`); + selection.select('path') + .attr('transform', 'translate(1,0)'); + } + if (config.get('type') === 'value') { + const spacerNodes = $(chartEl).find(`.y-axis-spacer-block-${position}`); + const elHeight = $(chartEl).find(`.axis-wrapper-${position}`).height(); + spacerNodes.height(elHeight); + } + } else { + const axisWidth = Math.ceil(length); + selection.attr('width', axisWidth); + if (position === 'left') { + selection.select('g') + .attr('transform', `translate(${axisWidth},0)`); + } + } + }; + } + + validate() { + if (this.axisConfig.isLogScale() && this.axisConfig.isPercentage()) { + throw new errors.VislibError(`Can't mix percentage mode with log scale.`); + } + } + + draw() { + const self = this; + const config = this.axisConfig; + const style = config.get('style'); + + return function (selection) { + const n = selection[0].length; + if (self.axisTitle) { + self.axisTitle.render(selection); + } + selection.each(function () { + const el = this; + const div = d3.select(el); + const width = $(el).width(); + const height = $(el).height(); + const length = self.getLength(el, n); + + self.validate(); + + const axis = self.getAxis(length); + + if (config.get('show')) { + const svg = div.append('svg') + .attr('width', width) + .attr('height', height); + + const axisClass = self.axisConfig.isHorizontal() ? 'x' : 'y'; + svg.append('g') + .attr('class', `${axisClass} axis ${config.get('id')}`) + .call(axis); + + const container = svg.select('g.axis').node(); + if (container) { + svg.select('path') + .style('stroke', style.color) + .style('stroke-width', style.lineWidth) + .style('stroke-opacity', style.opacity); + svg.selectAll('line') + .style('stroke', style.tickColor) + .style('stroke-width', style.tickWidth) + .style('stroke-opacity', style.opacity); + } + if (self.axisLabels) self.axisLabels.render(svg); + svg.call(self.adjustSize()); + } + }); + }; + } + } + + return Axis; +}; diff --git a/src/ui/public/vislib/lib/axis/axis_config.js b/src/ui/public/vislib/lib/axis/axis_config.js new file mode 100644 index 0000000000000..8c02f45211752 --- /dev/null +++ b/src/ui/public/vislib/lib/axis/axis_config.js @@ -0,0 +1,187 @@ +import _ from 'lodash'; +import d3 from 'd3'; +import SCALE_MODES from './scale_modes'; + +export default function AxisConfigFactory() { + + const defaults = { + show: true, + type: 'value', + elSelector: '.axis-wrapper-{pos} .axis-div', + position: 'left', + scale: { + type: 'linear', + expandLastBucket: true, + inverted: false, + setYExtents: null, + defaultYExtents: null, + min: null, + max: null, + mode: SCALE_MODES.NORMAL + }, + style: { + color: '#ddd', + lineWidth: '1px', + opacity: 1, + tickColor: '#ddd', + tickWidth: '1px', + tickLength: '6px' + }, + labels: { + axisFormatter: null, + show: true, + rotate: 0, + rotateAnchor: 'center', + filter: false, + color: '#ddd', + font: '"Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif', + fontSize: '8pt', + truncate: 100 + }, + title: { + text: '', + elSelector: '.axis-wrapper-{pos} .axis-title' + } + }; + + const categoryDefaults = { + type: 'category', + position: 'bottom', + labels: { + rotate: 0, + rotateAnchor: 'end', + filter: true, + truncate: 0, + } + }; + + const valueDefaults = { + labels: { + axisFormatter: d3.format('n') + } + }; + + class AxisConfig { + constructor(chartConfig, axisConfigArgs) { + const typeDefaults = axisConfigArgs.type === 'category' ? categoryDefaults : valueDefaults; + // _.defaultsDeep mutates axisConfigArgs nested values so we clone it first + const axisConfigArgsClone = _.cloneDeep(axisConfigArgs); + this._values = _.defaultsDeep({}, axisConfigArgsClone, typeDefaults, defaults); + + this._values.elSelector = this._values.elSelector.replace('{pos}', this._values.position); + this._values.rootEl = chartConfig.get('el'); + + this.data = chartConfig.data; + if (this._values.type === 'category') { + if (!this._values.values) { + this.values = this.data.xValues(chartConfig.get('orderBucketsBySum', false)); + this.ordered = this.data.get('ordered'); + } else { + this.values = this._values.values; + } + if (!this._values.labels.axisFormatter) { + this._values.labels.axisFormatter = this.data.data.xAxisFormatter || this.data.get('xAxisFormatter'); + } + } + + if (this.get('type') === 'value') { + const isWiggleOrSilhouette = + this.get('scale.mode') === SCALE_MODES.WIGGLE || + this.get('scale.mode') === SCALE_MODES.SILHOUETTE; + // if show was not explicitly set and wiggle or silhouette option was checked + if (isWiggleOrSilhouette) { + this._values.scale.defaultYExtents = false; + + if (!axisConfigArgs.show) { + this._values.show = false; + this._values.title.show = true; + } + } + + // override axisFormatter (to replicate current behaviour) + if (this.isPercentage()) { + this._values.labels.axisFormatter = d3.format('%'); + this._values.scale.defaultYExtents = true; + } + + if (this.isLogScale()) { + this._values.labels.filter = true; + } + } + + // horizontal axis with ordinal scale should have labels rotated (so we can fit more) + // unless explicitly overriden by user + if (this.isHorizontal() && this.isOrdinal()) { + this._values.labels.filter = _.get(axisConfigArgs, 'labels.filter', false); + this._values.labels.rotate = _.get(axisConfigArgs, 'labels.rotate', 90); + } + + let offset; + let stacked = true; + switch (this.get('scale.mode')) { + case SCALE_MODES.NORMAL: + offset = 'zero'; + stacked = false; + break; + case SCALE_MODES.GROUPED: + offset = 'group'; + break; + case SCALE_MODES.PERCENTAGE: + offset = 'expand'; + break; + default: + offset = this.get('scale.mode'); + } + this.set('scale.offset', _.get(axisConfigArgs, 'scale.offset', offset)); + /* axis.scale.stacked means that axis stacking function should be run */ + this.set('scale.stacked', stacked); + }; + + get(property, defaults) { + if (typeof defaults !== 'undefined' || _.has(this._values, property)) { + return _.get(this._values, property, defaults); + } else { + throw new Error(`Accessing invalid config property: ${property}`); + return defaults; + } + }; + + set(property, value) { + return _.set(this._values, property, value); + }; + + isHorizontal() { + return (this._values.position === 'top' || this._values.position === 'bottom'); + }; + + isOrdinal() { + return !!this.values && (!this.isTimeDomain()); + }; + + isTimeDomain() { + return this.ordered && this.ordered.date; + }; + + isPercentage() { + return this._values.scale.mode === SCALE_MODES.PERCENTAGE; + }; + + isUserDefined() { + return this._values.scale.setYExtents; + }; + + isYExtents() { + return this._values.scale.defaultYExtents; + }; + + isLogScale() { + return this.getScaleType() === 'log'; + }; + + getScaleType() { + return this._values.scale.type; + }; + } + + return AxisConfig; +} diff --git a/src/ui/public/vislib/lib/axis/axis_labels.js b/src/ui/public/vislib/lib/axis/axis_labels.js new file mode 100644 index 0000000000000..3d666ee05b637 --- /dev/null +++ b/src/ui/public/vislib/lib/axis/axis_labels.js @@ -0,0 +1,133 @@ +import d3 from 'd3'; +import $ from 'jquery'; +import _ from 'lodash'; +export default function AxisLabelsFactory(Private) { + class AxisLabels { + constructor(axisConfig, scale) { + this.axisConfig = axisConfig; + this.axisScale = scale; + } + + render(selection) { + selection.call(this.draw()); + }; + + rotateAxisLabels() { + const config = this.axisConfig; + return function (selection) { + const text = selection.selectAll('.tick text'); + + if (config.get('labels.rotate')) { + text + .style('text-anchor', function () { + return config.get('labels.rotateAnchor') === 'center' ? 'center' : 'end'; + }) + .attr('dy', function () { + if (config.isHorizontal()) { + if (config.get('position') === 'top') return '-0.9em'; + else return '0.3em'; + } + return '0'; + }) + .attr('dx', function () { + return config.isHorizontal() ? '-0.9em' : '0'; + }) + .attr('transform', function rotate(d, j) { + let rotateDeg = config.get('labels.rotate'); + if (config.get('labels.rotateAnchor') === 'center') { + const coord = text[0][j].getBBox(); + const transX = ((coord.x) + (coord.width / 2)); + const transY = ((coord.y) + (coord.height / 2)); + return `rotate(${rotateDeg}, ${transX}, ${transY})`; + } else { + rotateDeg = config.get('position') === 'top' ? rotateDeg : -rotateDeg; + return `rotate(${rotateDeg})`; + } + }); + } + }; + }; + + truncateLabel(text, size) { + const node = d3.select(text).node(); + let str = $(node).text(); + const width = node.getBBox().width; + const chars = str.length; + const pxPerChar = width / chars; + let endChar = 0; + const ellipsesPad = 4; + + if (width > size) { + endChar = Math.floor((size / pxPerChar) - ellipsesPad); + while (str[endChar - 1] === ' ' || str[endChar - 1] === '-' || str[endChar - 1] === ',') { + endChar = endChar - 1; + } + str = str.substr(0, endChar) + '...'; + } + return str; + }; + + truncateLabels() { + const self = this; + const config = this.axisConfig; + return function (selection) { + if (!config.get('labels.truncate')) return; + + selection.selectAll('.tick text') + .text(function () { + return self.truncateLabel(this, config.get('labels.truncate')); + }); + }; + }; + + filterAxisLabels() { + const self = this; + const config = this.axisConfig; + let startPos = 0; + let padding = 1.1; + + return function (selection) { + if (!config.get('labels.filter')) return; + + selection.selectAll('.tick text') + .text(function (d) { + const par = d3.select(this.parentNode).node(); + const el = $(config.get('rootEl')).find(config.get('elSelector')); + const maxSize = config.isHorizontal() ? el.width() : el.height(); + const myPos = config.isHorizontal() ? self.axisScale.scale(d) : maxSize - self.axisScale.scale(d); + const mySize = (config.isHorizontal() ? par.getBBox().width : par.getBBox().height) * padding; + const halfSize = mySize / 2; + + if ((startPos + halfSize) < myPos && maxSize > (myPos + halfSize)) { + startPos = myPos + halfSize; + return this.innerHTML; + } else { + d3.select(this.parentNode).remove(); + } + }); + }; + }; + + draw() { + const self = this; + const config = this.axisConfig; + + return function (selection) { + selection.each(function () { + selection.selectAll('text') + .attr('style', function () { + const currentStyle = d3.select(this).attr('style'); + return `${currentStyle} font-size: ${config.get('labels.fontSize')};`; + }); + if (!config.get('labels.show')) selection.selectAll('text').attr('style', 'display: none;'); + + selection.call(self.truncateLabels()); + selection.call(self.rotateAxisLabels()); + selection.call(self.filterAxisLabels()); + }); + }; + }; + } + + return AxisLabels; +}; diff --git a/src/ui/public/vislib/lib/axis/axis_scale.js b/src/ui/public/vislib/lib/axis/axis_scale.js new file mode 100644 index 0000000000000..60fd583450d5e --- /dev/null +++ b/src/ui/public/vislib/lib/axis/axis_scale.js @@ -0,0 +1,203 @@ +import d3 from 'd3'; +import _ from 'lodash'; +import moment from 'moment'; +import errors from 'ui/errors'; + +export default function AxisScaleFactory(Private) { + class AxisScale { + constructor(axisConfig, visConfig) { + this.axisConfig = axisConfig; + this.visConfig = visConfig; + + if (this.axisConfig.get('type') === 'category') { + this.values = this.axisConfig.values; + this.ordered = this.axisConfig.ordered; + } + }; + + getScaleType() { + return this.axisConfig.getScaleType(); + }; + + validateUserExtents(domain) { + const config = this.axisConfig; + return domain.map((val) => { + val = parseInt(val, 10); + if (isNaN(val)) throw new Error(val + ' is not a valid number'); + if (config.isPercentage() && config.isUserDefined()) return val / 100; + return val; + }); + }; + + getTimeDomain(data) { + return [this.minExtent(data), this.maxExtent(data)]; + }; + + minExtent(data) { + return this.calculateExtent(data || this.values, 'min'); + }; + + maxExtent(data) { + return this.calculateExtent(data || this.values, 'max'); + }; + + calculateExtent(data, extent) { + const ordered = this.ordered; + const opts = [ordered[extent]]; + + let point = d3[extent](data); + if (this.axisConfig.get('scale.expandLastBucket') && extent === 'max') { + point = this.addInterval(point); + } + opts.push(point); + + return d3[extent](opts.reduce(function (opts, v) { + if (!_.isNumber(v)) v = +v; + if (!isNaN(v)) opts.push(v); + return opts; + }, [])); + }; + + addInterval(x) { + return this.modByInterval(x, +1); + }; + + subtractInterval(x) { + return this.modByInterval(x, -1); + }; + + modByInterval(x, n) { + const ordered = this.ordered; + if (!ordered) return x; + const interval = ordered.interval; + if (!interval) return x; + + if (!ordered.date) { + return x += (ordered.interval * n); + } + + const y = moment(x); + const method = n > 0 ? 'add' : 'subtract'; + + _.times(Math.abs(n), function () { + y[method](interval); + }); + + return y.valueOf(); + }; + + getAllPoints() { + const config = this.axisConfig; + const data = this.visConfig.data.chartData(); + const chartPoints = _.reduce(data, (chartPoints, chart, chartIndex) => { + const points = chart.series.reduce((points, seri, seriIndex) => { + const seriConfig = this.visConfig.get(`charts[${chartIndex}].series[${seriIndex}]`); + const matchingValueAxis = !!seriConfig.valueAxis && seriConfig.valueAxis === config.get('id'); + const isFirstAxis = config.get('id') === this.visConfig.get('valueAxes[0].id'); + + if (matchingValueAxis || (!seriConfig.valueAxis && isFirstAxis)) { + const axisPoints = seri.values.map(val => { + if (val.y0) { + return val.y0 + val.y; + } + return val.y; + }); + return points.concat(axisPoints); + } + return points; + }, []); + return chartPoints.concat(points); + }, []); + + return chartPoints; + }; + + getYMin() { + return d3.min(this.getAllPoints()); + }; + + getYMax() { + return d3.max(this.getAllPoints()); + }; + + getExtents() { + if (this.axisConfig.get('type') === 'category') { + if (this.axisConfig.isTimeDomain()) return this.getTimeDomain(this.values); + if (this.axisConfig.isOrdinal()) return this.values; + } + + const min = this.axisConfig.get('scale.min') || this.getYMin(); + const max = this.axisConfig.get('scale.max') || this.getYMax(); + const domain = [min, max]; + if (this.axisConfig.isUserDefined()) return this.validateUserExtents(domain); + if (this.axisConfig.isYExtents()) return domain; + if (this.axisConfig.isLogScale()) return this.logDomain(min, max); + return [Math.min(0, min), Math.max(0, max)]; + }; + + getRange(length) { + if (this.axisConfig.isHorizontal()) { + return !this.axisConfig.get('scale.inverted') ? [0, length] : [length, 0]; + } else { + return this.axisConfig.get('scale.inverted') ? [0, length] : [length, 0]; + } + }; + + throwCustomError(message) { + throw new Error(message); + }; + + throwLogScaleValuesError() { + throw new errors.InvalidLogScaleValues(); + }; + + logDomain(min, max) { + if (min < 0 || max < 0) return this.throwLogScaleValuesError(); + return [1, max]; + }; + + getD3Scale(scaleTypeArg) { + let scaleType = scaleTypeArg || 'linear'; + if (scaleType === 'square root') scaleType = 'sqrt'; + + if (this.axisConfig.isTimeDomain()) return d3.time.scale.utc(); // allow time scale + if (this.axisConfig.isOrdinal()) return d3.scale.ordinal(); + if (typeof d3.scale[scaleType] !== 'function') { + return this.throwCustomError(`Axis.getScaleType: ${scaleType} is not a function`); + } + + return d3.scale[scaleType](); + }; + + canApplyNice() { + const config = this.axisConfig; + return (!config.isUserDefined() && !config.isYExtents() && !config.isOrdinal() && !config.isTimeDomain()); + } + + getScale(length) { + const config = this.axisConfig; + const scale = this.getD3Scale(config.getScaleType()); + const domain = this.getExtents(); + const range = this.getRange(length); + this.scale = scale.domain(domain); + if (config.isOrdinal()) { + this.scale.rangeBands(range, 0.1); + } else { + this.scale.range(range); + } + + if (this.canApplyNice()) this.scale.nice(); + // Prevents bars from going off the chart when the y extents are within the domain range + if (this.visConfig.get('type') === 'histogram' && this.scale.clamp) this.scale.clamp(true); + + this.validateScale(this.scale); + + return this.scale; + }; + + validateScale(scale) { + if (!scale || _.isNaN(scale)) throw new Error('scale is ' + scale); + }; + } + return AxisScale; +}; diff --git a/src/ui/public/vislib/lib/axis/axis_title.js b/src/ui/public/vislib/lib/axis/axis_title.js new file mode 100644 index 0000000000000..22f0784fa05d9 --- /dev/null +++ b/src/ui/public/vislib/lib/axis/axis_title.js @@ -0,0 +1,53 @@ +import d3 from 'd3'; +import $ from 'jquery'; +export default function AxisTitleFactory(Private) { + + class AxisTitle { + constructor(axisConfig) { + this.axisConfig = axisConfig; + this.elSelector = this.axisConfig.get('title.elSelector').replace('{pos}', this.axisConfig.get('position')); + } + + render() { + d3.select(this.axisConfig.get('rootEl')).selectAll(this.elSelector).call(this.draw()); + }; + + draw() { + const config = this.axisConfig; + + return function (selection) { + selection.each(function () { + if (!config.get('show') && !config.get('title.show', false)) return; + + const el = this; + const div = d3.select(el); + const width = $(el).width(); + const height = $(el).height(); + + const svg = div.append('svg') + .attr('width', width) + .attr('height', height); + + const bbox = svg.append('text') + .attr('transform', function () { + if (config.isHorizontal()) { + return 'translate(' + width / 2 + ',11)'; + } + return 'translate(11,' + height / 2 + ') rotate(270)'; + }) + .attr('text-anchor', 'middle') + .text(config.get('title.text')) + .node() + .getBBox(); + + if (config.isHorizontal()) { + svg.attr('height', bbox.height); + } else { + svg.attr('width', bbox.height); + } + }); + }; + }; + } + return AxisTitle; +}; diff --git a/src/ui/public/vislib/lib/axis/scale_modes.js b/src/ui/public/vislib/lib/axis/scale_modes.js new file mode 100644 index 0000000000000..ce3aede11c67f --- /dev/null +++ b/src/ui/public/vislib/lib/axis/scale_modes.js @@ -0,0 +1,10 @@ +const SCALE_MODES = { + NORMAL: 'normal', + PERCENTAGE: 'percentage', + WIGGLE: 'wiggle', + SILHOUETTE: 'silhouette', + GROUPED: 'grouped', // this should not be a scale mode but it is at this point to make it compatible with old charts + ALL: ['normal', 'percentage', 'wiggle', 'silhouette'] +}; + +export default SCALE_MODES; diff --git a/src/ui/public/vislib/lib/axis_title.js b/src/ui/public/vislib/lib/axis_title.js deleted file mode 100644 index 7fa2f54230d23..0000000000000 --- a/src/ui/public/vislib/lib/axis_title.js +++ /dev/null @@ -1,73 +0,0 @@ -import d3 from 'd3'; -import $ from 'jquery'; -import VislibLibErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; -export default function AxisTitleFactory(Private) { - - const ErrorHandler = Private(VislibLibErrorHandlerProvider); - - /** - * Appends axis title(s) to the visualization - * - * @class AxisTitle - * @constructor - * @param el {HTMLElement} DOM element - * @param xTitle {String} X-axis title - * @param yTitle {String} Y-axis title - */ - class AxisTitle extends ErrorHandler { - constructor(el, xTitle, yTitle) { - super(); - this.el = el; - this.xTitle = xTitle; - this.yTitle = yTitle; - } - - /** - * Renders both x and y axis titles - * - * @method render - * @returns {HTMLElement} DOM Element with axis titles - */ - render() { - d3.select(this.el).select('.x-axis-title').call(this.draw(this.xTitle)); - d3.select(this.el).select('.y-axis-title').call(this.draw(this.yTitle)); - }; - - /** - * Appends an SVG with title text - * - * @method draw - * @param title {String} Axis title - * @returns {Function} Appends axis title to a D3 selection - */ - draw(title) { - const self = this; - - return function (selection) { - selection.each(function () { - const el = this; - const div = d3.select(el); - const width = $(el).width(); - const height = $(el).height(); - - self.validateWidthandHeight(width, height); - - div.append('svg') - .attr('width', width) - .attr('height', height) - .append('text') - .attr('transform', function () { - if (div.attr('class') === 'x-axis-title') { - return 'translate(' + width / 2 + ',11)'; - } - return 'translate(11,' + height / 2 + ')rotate(270)'; - }) - .attr('text-anchor', 'middle') - .text(title); - }); - }; - }; - } - - return AxisTitle; -}; diff --git a/src/ui/public/vislib/lib/chart_title.js b/src/ui/public/vislib/lib/chart_title.js index 1159f148eeb29..53ca787c8d5cd 100644 --- a/src/ui/public/vislib/lib/chart_title.js +++ b/src/ui/public/vislib/lib/chart_title.js @@ -1,34 +1,20 @@ import d3 from 'd3'; import _ from 'lodash'; -import VislibLibErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; -import VislibComponentsTooltipProvider from 'ui/vislib/components/tooltip'; +import ErrorHandlerProvider from './_error_handler'; +import TooltipProvider from '../components/tooltip'; export default function ChartTitleFactory(Private) { + const ErrorHandler = Private(ErrorHandlerProvider); + const Tooltip = Private(TooltipProvider); - const ErrorHandler = Private(VislibLibErrorHandlerProvider); - const Tooltip = Private(VislibComponentsTooltipProvider); - - /** - * Appends chart titles to the visualization - * - * @class ChartTitle - * @constructor - * @param el {HTMLElement} Reference to DOM element - */ class ChartTitle extends ErrorHandler { - constructor(el) { + constructor(visConfig) { super(); - this.el = el; - this.tooltip = new Tooltip('chart-title', el, function (d) { + this.el = visConfig.get('el'); + this.tooltip = new Tooltip('chart-title', this.el, function (d) { return '

' + _.escape(d.label) + '

'; }); } - /** - * Renders chart titles - * - * @method render - * @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles - */ render() { const el = d3.select(this.el).select('.chart-title').node(); const width = el ? el.clientWidth : 0; @@ -37,13 +23,6 @@ export default function ChartTitleFactory(Private) { return d3.select(this.el).selectAll('.chart-title').call(this.draw(width, height)); }; - /** - * Truncates chart title text - * - * @method truncate - * @param size {Number} Height or width of the HTML Element - * @returns {Function} Truncates text - */ truncate(size) { const self = this; @@ -72,25 +51,12 @@ export default function ChartTitleFactory(Private) { }; }; - /** - * Adds tooltip events on truncated chart titles - * - * @method addMouseEvents - * @param target {HTMLElement} DOM element to attach event listeners - * @returns {*} DOM element with event listeners attached - */ addMouseEvents(target) { if (this.tooltip) { return target.call(this.tooltip.render()); } }; - /** - * Appends chart titles to the visualization - * - * @method draw - * @returns {Function} Appends chart titles to a D3 selection - */ draw(width, height) { const self = this; @@ -119,8 +85,7 @@ export default function ChartTitleFactory(Private) { }); // truncate long chart titles - div.selectAll('text') - .call(self.truncate(size)); + div.selectAll('text').call(self.truncate(size)); }); }; }; diff --git a/src/ui/public/vislib/lib/data.js b/src/ui/public/vislib/lib/data.js index 9e23c01aa0cd7..d459573439daa 100644 --- a/src/ui/public/vislib/lib/data.js +++ b/src/ui/public/vislib/lib/data.js @@ -1,9 +1,9 @@ import d3 from 'd3'; import _ from 'lodash'; -import VislibComponentsZeroInjectionInjectZerosProvider from 'ui/vislib/components/zero_injection/inject_zeros'; -import VislibComponentsZeroInjectionOrderedXKeysProvider from 'ui/vislib/components/zero_injection/ordered_x_keys'; -import VislibComponentsLabelsLabelsProvider from 'ui/vislib/components/labels/labels'; -import VislibComponentsColorColorProvider from 'ui/vislib/components/color/color'; +import VislibComponentsZeroInjectionInjectZerosProvider from '../components/zero_injection/inject_zeros'; +import VislibComponentsZeroInjectionOrderedXKeysProvider from '../components/zero_injection/ordered_x_keys'; +import VislibComponentsLabelsLabelsProvider from '../components/labels/labels'; +import VislibComponentsColorColorProvider from '../components/color/color'; export default function DataFactory(Private) { const injectZeros = Private(VislibComponentsZeroInjectionInjectZerosProvider); @@ -21,172 +21,61 @@ export default function DataFactory(Private) { * @param attr {Object|*} Visualization options */ class Data { - constructor(data, attr, uiState) { + constructor(data, uiState) { this.uiState = uiState; - - const self = this; - let offset; - - if (attr.mode === 'stacked') { - offset = 'zero'; - } else if (attr.mode === 'percentage') { - offset = 'expand'; - } else if (attr.mode === 'grouped') { - offset = 'group'; - } else { - offset = attr.mode; - } - - this.data = data; + this.data = this.copyDataObj(data); this.type = this.getDataType(); this.labels = this._getLabels(this.data); this.color = this.labels ? color(this.labels, uiState.get('vis.colors')) : undefined; this._normalizeOrdered(); + } - this._attr = _.defaults(attr || {}, { - stack: d3.layout.stack() - .x(function (d) { - return d.x; - }) - .y(function (d) { - if (offset === 'expand') { - return Math.abs(d.y); + copyDataObj(data) { + const copyChart = data => { + const newData = {}; + Object.keys(data).forEach(key => { + if (key !== 'series') { + newData[key] = data[key]; + } else { + newData[key] = data[key].map(seri => { + return { + label: seri.label, + values: seri.values.map(val => { + const newVal = _.clone(val); + newVal.aggConfig = val.aggConfig; + newVal.aggConfigResult = val.aggConfigResult; + newVal.extraMetrics = val.extraMetrics; + return newVal; + }) + }; + }); } - return d.y; - }) - .offset(offset || 'zero') - }); + }); + return newData; + }; - if (attr.mode === 'stacked' && attr.type === 'histogram') { - this._attr.stack.out(function (d, y0, y) { - return self._stackNegAndPosVals(d, y0, y); + if (!data.series) { + const newData = {}; + Object.keys(data).forEach(key => { + if (!['rows', 'columns'].includes(key)) { + newData[key] = data[key]; + } + else { + newData[key] = data[key].map(chart => { + return copyChart(chart); + }); + } }); + return newData; } + return copyChart(data); } _getLabels(data) { return this.type === 'series' ? getLabels(data) : this.pieNames(); }; - /** - * Returns true for positive numbers - */ - _isPositive(num) { - return num >= 0; - }; - - /** - * Returns true for negative numbers - */ - _isNegative(num) { - return num < 0; - }; - - /** - * Adds two input values - */ - _addVals(a, b) { - return a + b; - }; - - /** - * Returns the results of the addition of numbers in a filtered array. - */ - _sumYs(arr, callback) { - const filteredArray = arr.filter(callback); - - return (filteredArray.length) ? filteredArray.reduce(this._addVals) : 0; - }; - - /** - * Calculates the d.y0 value for stacked data in D3. - */ - _calcYZero(y, arr) { - if (y >= 0) return this._sumYs(arr, this._isPositive); - return this._sumYs(arr, this._isNegative); - }; - - /** - * - */ - _getCounts(i, j) { - const data = this.chartData(); - const dataLengths = {}; - - dataLengths.charts = data.length; - dataLengths.stacks = dataLengths.charts ? data[i].series.length : 0; - dataLengths.values = dataLengths.stacks ? data[i].series[j].values.length : 0; - - return dataLengths; - }; - - /** - * - */ - _createCache() { - const cache = { - index: { - chart: 0, - stack: 0, - value: 0 - }, - yValsArr: [] - }; - - cache.count = this._getCounts(cache.index.chart, cache.index.stack); - - return cache; - }; - - /** - * Stacking function passed to the D3 Stack Layout `.out` API. - * See: https://github.com/mbostock/d3/wiki/Stack-Layout - * It is responsible for calculating the correct d.y0 value for - * mixed datasets containing both positive and negative values. - */ - _stackNegAndPosVals(d, y0, y) { - const data = this.chartData(); - - // Storing counters and data characteristics needed to stack values properly - if (!this._cache) { - this._cache = this._createCache(); - } - - d.y0 = this._calcYZero(y, this._cache.yValsArr); - ++this._cache.index.stack; - - - // last stack, or last value, reset the stack count and y value array - const lastStack = (this._cache.index.stack >= this._cache.count.stacks); - if (lastStack) { - this._cache.index.stack = 0; - ++this._cache.index.value; - this._cache.yValsArr = []; - // still building the stack collection, push v value to array - } else if (y !== 0) { - this._cache.yValsArr.push(y); - } - - // last value, prepare for the next chart, if one exists - const lastValue = (this._cache.index.value >= this._cache.count.values); - if (lastValue) { - this._cache.index.value = 0; - ++this._cache.index.chart; - - // no more charts, reset the queue and finish - if (this._cache.index.chart >= this._cache.count.charts) { - this._cache = this._createCache(); - return; - } - - // get stack and value count for next chart - const chartSeries = data[this._cache.index.chart].series; - this._cache.count.stacks = chartSeries.length; - this._cache.count.values = chartSeries.length ? chartSeries[this._cache.index.stack].values.length : 0; - } - }; - getDataType() { const data = this.getVisData(); let type; @@ -219,6 +108,48 @@ export default function DataFactory(Private) { return [this.data]; }; + shouldBeStacked(seriesConfig) { + const isHistogram = (seriesConfig.type === 'histogram'); + const isArea = (seriesConfig.type === 'area'); + const stacked = (seriesConfig.mode === 'stacked'); + + return (isHistogram || isArea) && stacked; + }; + + getStackedSeries(chartConfig, axis, series, first = false) { + const matchingSeries = []; + chartConfig.series.forEach((seriArgs, i) => { + const matchingAxis = seriArgs.valueAxis === axis.axisConfig.get('id') || (!seriArgs.valueAxis && first); + if (matchingAxis && (this.shouldBeStacked(seriArgs) || axis.axisConfig.get('scale.stacked'))) { + matchingSeries.push(series[i]); + } + }); + return matchingSeries; + }; + + stackChartData(handler, data, chartConfig) { + const stackedData = {}; + handler.valueAxes.forEach((axis, i) => { + const id = axis.axisConfig.get('id'); + stackedData[id] = this.getStackedSeries(chartConfig, axis, data, i === 0); + stackedData[id] = this.injectZeros(stackedData[id], handler.visConfig.get('orderBucketsBySum', false)); + axis.stack(_.map(stackedData[id], 'values')); + }); + return stackedData; + }; + + stackData(handler) { + const data = this.data; + if (data.rows || data.columns) { + const charts = data.rows ? data.rows : data.columns; + charts.forEach((chart, i) => { + this.stackChartData(handler, chart.series, handler.visConfig.get(`charts[${i}]`)); + }); + } else { + this.stackChartData(handler, data.series, handler.visConfig.get('charts[0]')); + } + } + /** * Returns an array of chart data objects * @@ -317,25 +248,6 @@ export default function DataFactory(Private) { .value(); }; - /** - * Determines whether histogram charts should be stacked - * TODO: need to make this more generic - * - * @method shouldBeStacked - * @returns {boolean} - */ - shouldBeStacked() { - const isHistogram = (this._attr.type === 'histogram'); - const isArea = (this._attr.type === 'area'); - const isOverlapping = (this._attr.mode === 'overlap'); - const grouped = (this._attr.mode === 'grouped'); - - const stackedHisto = isHistogram && !grouped; - const stackedArea = isArea && !isOverlapping; - - return stackedHisto || stackedArea; - }; - /** * Validates that the Y axis min value defined by user input * is a number. @@ -350,135 +262,6 @@ export default function DataFactory(Private) { return val; }; - /** - * Calculates the lowest Y value across all charts, taking - * stacking into consideration. - * - * @method getYMin - * @param {function} [getValue] - optional getter that will receive a - * point and should return the value that should - * be considered - * @returns {Number} Min y axis value - */ - getYMin(getValue) { - const self = this; - - if (this._attr.mode === 'percentage' || this._attr.mode === 'wiggle' || - this._attr.mode === 'silhouette') { - return 0; - } - - const flat = this.flatten(); - // if there is only one data point and its less than zero, - // return 0 as the yMax value. - if (!flat.length || flat.length === 1 && flat[0].y > 0) { - return 0; - } - - let min = Infinity; - - // for each object in the dataArray, - // push the calculated y value to the initialized array (arr) - _.each(this.chartData(), function (chart) { - const calculatedMin = self._getYExtent(chart, 'min', getValue); - if (!_.isUndefined(calculatedMin)) { - min = Math.min(min, calculatedMin); - } - }); - - return min; - }; - - /** - * Calculates the highest Y value across all charts, taking - * stacking into consideration. - * - * @method getYMax - * @param {function} [getValue] - optional getter that will receive a - * point and should return the value that should - * be considered - * @returns {Number} Max y axis value - */ - getYMax(getValue) { - const self = this; - - if (self._attr.mode === 'percentage') { - return 1; - } - - const flat = this.flatten(); - // if there is only one data point and its less than zero, - // return 0 as the yMax value. - if (!flat.length || flat.length === 1 && flat[0].y < 0) { - return 0; - } - - let max = -Infinity; - - // for each object in the dataArray, - // push the calculated y value to the initialized array (arr) - _.each(this.chartData(), function (chart) { - const calculatedMax = self._getYExtent(chart, 'max', getValue); - if (!_.isUndefined(calculatedMax)) { - max = Math.max(max, calculatedMax); - } - }); - - return max; - }; - - /** - * Calculates the stacked values for each data object - * - * @method stackData - * @param series {Array} Array of data objects - * @returns {*} Array of data objects with x, y, y0 keys - */ - stackData(series) { - // Should not stack values on line chart - if (this._attr.type === 'line') return series; - return this._attr.stack(series); - }; - - /** - * Returns the max Y axis value for a `series` array based on - * a specified callback function (calculation). - * @param {function} [getValue] - Optional getter that will be used to read - * values from points when calculating the extent. - * default is either this._getYStack or this.getY - * based on this.shouldBeStacked(). - */ - _getYExtent(chart, extent, getValue) { - if (this.shouldBeStacked()) { - this.stackData(_.pluck(chart.series, 'values')); - getValue = getValue || this._getYStack; - } else { - getValue = getValue || this._getY; - } - - const points = chart.series - .reduce(function (points, series) { - return points.concat(series.values); - }, []) - .map(getValue); - - return d3[extent](points); - }; - - /** - * Calculates the y stack value for each data object - */ - _getYStack(d) { - return d.y0 + d.y; - }; - - /** - * Calculates the Y max value - */ - _getY(d) { - return d.y; - }; - /** * Helper function for getNames * Returns an array of objects with a name (key) value and an index value. @@ -590,8 +373,8 @@ export default function DataFactory(Private) { * @method injectZeros * @returns {Object} Data object with zeros injected */ - injectZeros() { - return injectZeros(this.data); + injectZeros(data, orderBucketsBySum = false) { + return injectZeros(data, this.data, orderBucketsBySum); }; /** @@ -600,8 +383,8 @@ export default function DataFactory(Private) { * @method xValues * @returns {Array} Array of x axis values */ - xValues() { - return orderKeys(this.data, this._attr.orderBucketsBySum); + xValues(orderBucketsBySum = false) { + return orderKeys(this.data, orderBucketsBySum); }; /** diff --git a/src/ui/public/vislib/lib/dispatch.js b/src/ui/public/vislib/lib/dispatch.js index 55da4ef9ff2f8..555899713358d 100644 --- a/src/ui/public/vislib/lib/dispatch.js +++ b/src/ui/public/vislib/lib/dispatch.js @@ -26,21 +26,21 @@ export default function DispatchClass(Private, config) { * @param d {Object} Data point * @param i {Number} Index number of data point * @returns {{value: *, point: *, label: *, color: *, pointIndex: *, - * series: *, config: *, data: (Object|*), - * e: (d3.event|*), handler: (Object|*)}} Event response object + * series: *, config: *, data: (Object|*), + * e: (d3.event|*), handler: (Object|*)}} Event response object */ eventResponse(d, i) { const datum = d._input || d; const data = d3.event.target.nearestViewportElement ? d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__; - const label = d.label ? d.label : d.name; + const label = d.label ? d.label : (d.series || 'Count'); const isSeries = !!(data && data.series); const isSlices = !!(data && data.slices); const series = isSeries ? data.series : undefined; const slices = isSlices ? data.slices : undefined; const handler = this.handler; const color = _.get(handler, 'data.color'); - const isPercentage = (handler && handler._attr.mode === 'percentage'); + const isPercentage = (handler && handler.visConfig.get('mode') === 'percentage'); const eventData = { value: d.y, @@ -51,7 +51,7 @@ export default function DispatchClass(Private, config) { pointIndex: i, series: series, slices: slices, - config: handler && handler._attr, + config: handler && handler.visConfig, data: data, e: d3.event, handler: handler @@ -59,12 +59,14 @@ export default function DispatchClass(Private, config) { if (isSeries) { // Find object with the actual d value and add it to the point object - const object = _.find(series, {'label': d.label}); - eventData.value = +object.values[i].y; + const object = _.find(series, {'label': label}); + if (object) { + eventData.value = +object.values[i].y; - if (isPercentage) { - // Add the formatted percentage to the point object - eventData.percent = (100 * d.y).toFixed(1) + '%'; + if (isPercentage) { + // Add the formatted percentage to the point object + eventData.percent = (100 * d.y).toFixed(1) + '%'; + } } } @@ -161,7 +163,7 @@ export default function DispatchClass(Private, config) { * @returns {Boolean} */ allowBrushing() { - const xAxis = this.handler.xAxis; + const xAxis = this.handler.categoryAxes[0]; //Allow brushing for ordered axis - date histogram and histogram return Boolean(xAxis.ordered); @@ -186,7 +188,7 @@ export default function DispatchClass(Private, config) { if (!this.isBrushable()) return; const self = this; - const xScale = this.handler.xAxis.xScale; + const xScale = this.handler.categoryAxes[0].getScale(); const brush = this.createBrush(xScale, svg); function simulateClickWithBrushEnabled(d, i) { @@ -237,7 +239,7 @@ export default function DispatchClass(Private, config) { const dimming = config.get('visualization:dimmingOpacity'); $(element).parent().find('[data-label]') .css('opacity', 1)//Opacity 1 is needed to avoid the css application - .not((els, el) => $(el).data('label') === label) + .not((els, el) => String($(el).data('label')) === label) .css('opacity', justifyOpacity(dimming)); } @@ -260,14 +262,19 @@ export default function DispatchClass(Private, config) { */ createBrush(xScale, svg) { const self = this; - const attr = self.handler._attr; - const height = attr.height; - const margin = attr.margin; + const visConfig = self.handler.visConfig; + const {width, height} = svg.node().getBBox(); + const isHorizontal = self.handler.categoryAxes[0].axisConfig.isHorizontal(); // Brush scale - const brush = d3.svg.brush() - .x(xScale) - .on('brushend', function brushEnd() { + const brush = d3.svg.brush(); + if (isHorizontal) { + brush.x(xScale); + } else { + brush.y(xScale); + } + + brush.on('brushend', function brushEnd() { // Assumes data is selected at the chart level // In this case, the number of data objects should always be 1 @@ -282,7 +289,7 @@ export default function DispatchClass(Private, config) { return self.emit('brush', { range: range, - config: attr, + config: visConfig, e: d3.event, data: data }); @@ -290,19 +297,24 @@ export default function DispatchClass(Private, config) { // if `addBrushing` is true, add brush canvas if (self.listenerCount('brush')) { - svg.insert('g', 'g') - .attr('class', 'brush') - .call(brush) - .call(function (brushG) { - // hijack the brush start event to filter out right/middle clicks - const brushHandler = brushG.on('mousedown.brush'); - if (!brushHandler) return; // touch events in use - brushG.on('mousedown.brush', function () { - if (validBrushClick(d3.event)) brushHandler.apply(this, arguments); - }); - }) - .selectAll('rect') - .attr('height', height - margin.top - margin.bottom); + const rect = svg.insert('g', 'g') + .attr('class', 'brush') + .call(brush) + .call(function (brushG) { + // hijack the brush start event to filter out right/middle clicks + const brushHandler = brushG.on('mousedown.brush'); + if (!brushHandler) return; // touch events in use + brushG.on('mousedown.brush', function () { + if (validBrushClick(d3.event)) brushHandler.apply(this, arguments); + }); + }) + .selectAll('rect'); + + if (isHorizontal) { + rect.attr('height', height); + } else { + rect.attr('width', width); + } return brush; } diff --git a/src/ui/public/vislib/lib/handler/handler.js b/src/ui/public/vislib/lib/handler.js similarity index 79% rename from src/ui/public/vislib/lib/handler/handler.js rename to src/ui/public/vislib/lib/handler.js index 6d5885020f024..c531f62284b0c 100644 --- a/src/ui/public/vislib/lib/handler/handler.js +++ b/src/ui/public/vislib/lib/handler.js @@ -3,12 +3,18 @@ import _ from 'lodash'; import $ from 'jquery'; import errors from 'ui/errors'; import Binder from 'ui/binder'; -import VislibLibDataProvider from 'ui/vislib/lib/data'; -import VislibLibLayoutLayoutProvider from 'ui/vislib/lib/layout/layout'; -export default function HandlerBaseClass(Private) { +import VislibLibLayoutLayoutProvider from './layout/layout'; +import VislibLibChartTitleProvider from './chart_title'; +import VislibLibAlertsProvider from './alerts'; +import VislibAxisProvider from './axis/axis'; +import VislibVisualizationsVisTypesProvider from '../visualizations/vis_types'; - const Data = Private(VislibLibDataProvider); +export default function HandlerBaseClass(Private) { + const chartTypes = Private(VislibVisualizationsVisTypesProvider); const Layout = Private(VislibLibLayoutLayoutProvider); + const ChartTitle = Private(VislibLibChartTitleProvider); + const Alerts = Private(VislibLibAlertsProvider); + const Axis = Private(VislibAxisProvider); /** * Handles building all the components of the visualization @@ -20,34 +26,40 @@ export default function HandlerBaseClass(Private) { * create the visualization */ class Handler { - constructor(vis, opts) { - this.data = opts.data || new Data(vis.data, vis._attr, vis.uiState); - this.vis = vis; - this.el = vis.el; - this.ChartClass = vis.ChartClass; + constructor(vis, visConfig) { + this.el = visConfig.get('el'); + this.ChartClass = chartTypes[visConfig.get('type')]; this.charts = []; - this._attr = _.defaults(vis._attr || {}, { - 'margin': {top: 10, right: 3, bottom: 5, left: 3} - }); + this.vis = vis; + this.visConfig = visConfig; + this.data = visConfig.data; - this.xAxis = opts.xAxis; - this.yAxis = opts.yAxis; - this.chartTitle = opts.chartTitle; - this.axisTitle = opts.axisTitle; - this.alerts = opts.alerts; + this.categoryAxes = visConfig.get('categoryAxes').map(axisArgs => new Axis(visConfig, axisArgs)); + this.valueAxes = visConfig.get('valueAxes').map(axisArgs => new Axis(visConfig, axisArgs)); + this.chartTitle = new ChartTitle(visConfig); + this.alerts = new Alerts(this, visConfig.get('alerts')); - this.layout = new Layout(vis.el, vis.data, vis._attr.type, opts); + if (visConfig.get('type') === 'point_series') { + this.data.stackData(this); + } + + if (visConfig.get('resize', false)) { + this.resize = visConfig.get('resize'); + } + + this.layout = new Layout(visConfig); this.binder = new Binder(); this.renderArray = _.filter([ this.layout, - this.axisTitle, this.chartTitle, - this.alerts, - this.xAxis, - this.yAxis, + this.alerts ], Boolean); + this.renderArray = this.renderArray + .concat(this.valueAxes) + .concat(this.categoryAxes); + // memoize so that the same function is returned every time, // allowing us to remove/re-add the same function this.getProxyHandler = _.memoize(function (event) { diff --git a/src/ui/public/vislib/lib/handler/handler_types.js b/src/ui/public/vislib/lib/handler/handler_types.js deleted file mode 100644 index 5473181a97f68..0000000000000 --- a/src/ui/public/vislib/lib/handler/handler_types.js +++ /dev/null @@ -1,20 +0,0 @@ -import VislibLibHandlerTypesPointSeriesProvider from 'ui/vislib/lib/handler/types/point_series'; -import VislibLibHandlerTypesPieProvider from 'ui/vislib/lib/handler/types/pie'; -import VislibLibHandlerTypesTileMapProvider from 'ui/vislib/lib/handler/types/tile_map'; - -export default function HandlerTypeFactory(Private) { - const pointSeries = Private(VislibLibHandlerTypesPointSeriesProvider); - - /** - * Handles the building of each visualization - * - * @return {Function} Returns an Object of Handler types - */ - return { - histogram: pointSeries.column, - line: pointSeries.line, - pie: Private(VislibLibHandlerTypesPieProvider), - area: pointSeries.area, - tile_map: Private(VislibLibHandlerTypesTileMapProvider) - }; -}; diff --git a/src/ui/public/vislib/lib/handler/types/pie.js b/src/ui/public/vislib/lib/handler/types/pie.js deleted file mode 100644 index f119f21f60042..0000000000000 --- a/src/ui/public/vislib/lib/handler/types/pie.js +++ /dev/null @@ -1,17 +0,0 @@ -import VislibLibHandlerHandlerProvider from 'ui/vislib/lib/handler/handler'; -import VislibLibChartTitleProvider from 'ui/vislib/lib/chart_title'; - -export default function PieHandler(Private) { - const Handler = Private(VislibLibHandlerHandlerProvider); - const ChartTitle = Private(VislibLibChartTitleProvider); - - /* - * Handler for Pie visualizations. - */ - - return function (vis) { - return new Handler(vis, { - chartTitle: new ChartTitle(vis.el) - }); - }; -}; diff --git a/src/ui/public/vislib/lib/handler/types/point_series.js b/src/ui/public/vislib/lib/handler/types/point_series.js deleted file mode 100644 index bfbc57f714dc6..0000000000000 --- a/src/ui/public/vislib/lib/handler/types/point_series.js +++ /dev/null @@ -1,99 +0,0 @@ -import VislibComponentsZeroInjectionInjectZerosProvider from 'ui/vislib/components/zero_injection/inject_zeros'; -import VislibLibHandlerHandlerProvider from 'ui/vislib/lib/handler/handler'; -import VislibLibDataProvider from 'ui/vislib/lib/data'; -import VislibLibXAxisProvider from 'ui/vislib/lib/x_axis'; -import VislibLibYAxisProvider from 'ui/vislib/lib/y_axis'; -import VislibLibAxisTitleProvider from 'ui/vislib/lib/axis_title'; -import VislibLibChartTitleProvider from 'ui/vislib/lib/chart_title'; -import VislibLibAlertsProvider from 'ui/vislib/lib/alerts'; - -export default function ColumnHandler(Private) { - const injectZeros = Private(VislibComponentsZeroInjectionInjectZerosProvider); - const Handler = Private(VislibLibHandlerHandlerProvider); - const Data = Private(VislibLibDataProvider); - const XAxis = Private(VislibLibXAxisProvider); - const YAxis = Private(VislibLibYAxisProvider); - const AxisTitle = Private(VislibLibAxisTitleProvider); - const ChartTitle = Private(VislibLibChartTitleProvider); - const Alerts = Private(VislibLibAlertsProvider); - - function getData(vis, opts) { - if (opts.zeroFill) { - return new Data(injectZeros(vis.data, vis._attr.orderBucketsBySum), vis._attr, vis.uiState); - } else { - return new Data(vis.data, vis._attr, vis.uiState); - } - } - /* - * Create handlers for Area, Column, and Line charts which - * are all nearly the same minus a few details - */ - function create(opts) { - opts = opts || {}; - - return function (vis) { - const isUserDefinedYAxis = vis._attr.setYExtents; - const data = getData(vis, opts); - - return new Handler(vis, { - data: data, - axisTitle: new AxisTitle(vis.el, data.get('xAxisLabel'), data.get('yAxisLabel')), - chartTitle: new ChartTitle(vis.el), - xAxis: new XAxis({ - el : vis.el, - xValues : data.xValues(), - ordered : data.get('ordered'), - xAxisFormatter : data.get('xAxisFormatter'), - expandLastBucket : opts.expandLastBucket, - _attr : vis._attr - }), - alerts: new Alerts(vis, data, opts.alerts), - yAxis: new YAxis({ - el : vis.el, - yMin : isUserDefinedYAxis ? vis._attr.yAxis.min : data.getYMin(), - yMax : isUserDefinedYAxis ? vis._attr.yAxis.max : data.getYMax(), - yAxisFormatter: data.get('yAxisFormatter'), - _attr: vis._attr - }) - }); - - }; - } - - return { - line: create(), - - column: create({ - zeroFill: true, - expandLastBucket: true - }), - - area: create({ - zeroFill: true, - alerts: [ - { - type: 'warning', - msg: 'Positive and negative values are not accurately represented by stacked ' + - 'area charts. Either changing the chart mode to "overlap" or using a ' + - 'bar chart is recommended.', - test: function (vis, data) { - if (!data.shouldBeStacked() || data.maxNumberOfSeries() < 2) return; - - const hasPos = data.getYMax(data._getY) > 0; - const hasNeg = data.getYMin(data._getY) < 0; - return (hasPos && hasNeg); - } - }, - { - type: 'warning', - msg: 'Parts of or the entire area chart might not be displayed due to null ' + - 'values in the data. A line chart is recommended when displaying data ' + - 'with null values.', - test: function (vis, data) { - return data.hasNullValues(); - } - } - ] - }) - }; -}; diff --git a/src/ui/public/vislib/lib/handler/types/tile_map.js b/src/ui/public/vislib/lib/handler/types/tile_map.js deleted file mode 100644 index c6ac20996a4fe..0000000000000 --- a/src/ui/public/vislib/lib/handler/types/tile_map.js +++ /dev/null @@ -1,24 +0,0 @@ -import VislibLibHandlerHandlerProvider from 'ui/vislib/lib/handler/handler'; -import VislibLibDataProvider from 'ui/vislib/lib/data'; -export default function MapHandlerProvider(Private) { - - const Handler = Private(VislibLibHandlerHandlerProvider); - const Data = Private(VislibLibDataProvider); - - return function (vis) { - const data = new Data(vis.data, vis._attr, vis.uiState); - - const MapHandler = new Handler(vis, { - data: data - }); - - MapHandler.resize = function () { - this.charts.forEach(function (chart) { - chart.resizeArea(); - }); - }; - - return MapHandler; - }; -}; - diff --git a/src/ui/public/vislib/lib/layout/layout.js b/src/ui/public/vislib/lib/layout/layout.js index 3e71225a00d92..d689e0742fe22 100644 --- a/src/ui/public/vislib/lib/layout/layout.js +++ b/src/ui/public/vislib/lib/layout/layout.js @@ -1,10 +1,12 @@ import d3 from 'd3'; import _ from 'lodash'; -import VislibLibLayoutLayoutTypesProvider from 'ui/vislib/lib/layout/layout_types'; +import $ from 'jquery'; +import VislibLibLayoutLayoutTypesProvider from './layout_types'; +import AxisProvider from 'ui/vislib/lib/axis'; export default function LayoutFactory(Private) { const layoutType = Private(VislibLibLayoutLayoutTypesProvider); - + const Axis = Private(AxisProvider); /** * Builds the visualization DOM layout * @@ -22,11 +24,11 @@ export default function LayoutFactory(Private) { * @param chartType {Object} Reference to chart functions, i.e. Pie */ class Layout { - constructor(el, data, chartType, opts) { - this.el = el; - this.data = data; - this.opts = opts; - this.layoutType = layoutType[chartType](this.el, this.data); + constructor(config) { + this.el = config.get('el'); + this.data = config.data.data; + this.opts = config; + this.layoutType = layoutType[config.get('type')](this.el, this.data); } // Render the layout @@ -39,6 +41,10 @@ export default function LayoutFactory(Private) { render() { this.removeAll(this.el); this.createLayout(this.layoutType); + // update y-axis-spacer height based on precalculated horizontal axis heights + if (this.opts.get('type') === 'point_series') { + this.updateCategoryAxisSize(); + } }; /** @@ -50,13 +56,36 @@ export default function LayoutFactory(Private) { * @returns {*} Creates the visualization layout */ createLayout(arr) { - const self = this; - - return _.each(arr, function (obj) { - self.layout(obj); + return _.each(arr, (obj) => { + this.layout(obj); }); }; + updateCategoryAxisSize() { + const visConfig = this.opts; + const axisConfig = visConfig.get('categoryAxes[0]'); + const axis = new Axis(visConfig, axisConfig); + const position = axis.axisConfig.get('position'); + + const el = $(this.el).find(`.axis-wrapper-${position}`); + + el.css('visibility', 'hidden'); + axis.render(); + const width = el.width(); + const height = el.height(); + axis.destroy(); + el.css('visibility', ''); + + if (axis.axisConfig.isHorizontal()) { + const spacerNodes = $(this.el).find(`.y-axis-spacer-block-${position}`); + el.height(`${height}px`); + spacerNodes.height(el.height()); + } else { + el.find('.y-axis-div-wrapper').width(`${width}px`); + } + }; + + /** * Appends a DOM element based on the object keys * check to see if reference to DOM element is string but not class selector diff --git a/src/ui/public/vislib/lib/layout/layout_types.js b/src/ui/public/vislib/lib/layout/layout_types.js index b06acdc4c7520..cb2dd59165b32 100644 --- a/src/ui/public/vislib/lib/layout/layout_types.js +++ b/src/ui/public/vislib/lib/layout/layout_types.js @@ -1,6 +1,6 @@ -import VislibLibLayoutTypesColumnLayoutProvider from 'ui/vislib/lib/layout/types/column_layout'; -import VislibLibLayoutTypesPieLayoutProvider from 'ui/vislib/lib/layout/types/pie_layout'; -import VislibLibLayoutTypesMapLayoutProvider from 'ui/vislib/lib/layout/types/map_layout'; +import VislibLibLayoutTypesColumnLayoutProvider from './types/column_layout'; +import VislibLibLayoutTypesPieLayoutProvider from './types/pie_layout'; +import VislibLibLayoutTypesMapLayoutProvider from './types/map_layout'; export default function LayoutTypeFactory(Private) { @@ -13,10 +13,8 @@ export default function LayoutTypeFactory(Private) { * @return {Function} Returns an Object of HTML layouts for each visualization class */ return { - histogram: Private(VislibLibLayoutTypesColumnLayoutProvider), - line: Private(VislibLibLayoutTypesColumnLayoutProvider), - area: Private(VislibLibLayoutTypesColumnLayoutProvider), pie: Private(VislibLibLayoutTypesPieLayoutProvider), - tile_map: Private(VislibLibLayoutTypesMapLayoutProvider) + tile_map: Private(VislibLibLayoutTypesMapLayoutProvider), + point_series: Private(VislibLibLayoutTypesColumnLayoutProvider) }; }; diff --git a/src/ui/public/vislib/lib/layout/splits/column_chart/chart_split.js b/src/ui/public/vislib/lib/layout/splits/column_chart/chart_split.js index f32fc7a3ce364..838d7a3b339b4 100644 --- a/src/ui/public/vislib/lib/layout/splits/column_chart/chart_split.js +++ b/src/ui/public/vislib/lib/layout/splits/column_chart/chart_split.js @@ -7,7 +7,7 @@ define(function () { * For example, if the data has rows, it returns the same number of * `.chart` elements as row objects. */ - return function split(selection) { + return function split(selection, parent) { selection.each(function (data) { const div = d3.select(this) .attr('class', function () { @@ -16,29 +16,42 @@ define(function () { } else if (data.columns) { return 'chart-wrapper-column'; } else { - return 'chart-wrapper'; + if (parent) { + return 'chart-first chart-last chart-wrapper'; + } + return this.className + ' chart-wrapper'; } }); - let divClass; + let divClass = ''; + let chartsNumber; const charts = div.selectAll('charts') .append('div') .data(function (d) { if (d.rows) { - divClass = 'chart-row'; + chartsNumber = d.rows.length; return d.rows; } else if (d.columns) { - divClass = 'chart-column'; + chartsNumber = d.columns.length; return d.columns; } else { divClass = 'chart'; + chartsNumber = 1; return [d]; } }) .enter() .append('div') - .attr('class', function () { - return divClass; + .attr('class', function (d, i) { + let fullDivClass = divClass; + if (chartsNumber > 1) { + if (i === 0) { + fullDivClass += ' chart-first'; + } else if (i === chartsNumber - 1) { + fullDivClass += ' chart-last'; + } + } + return fullDivClass; }); if (!data.series) { diff --git a/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js b/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js index 2d99d4bbd25bc..98290729082c6 100644 --- a/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js +++ b/src/ui/public/vislib/lib/layout/splits/column_chart/x_axis_split.js @@ -11,15 +11,25 @@ define(function () { return function (selection) { selection.each(function () { const div = d3.select(this); - + let columns; div.selectAll('.x-axis-div') .append('div') .data(function (d) { + columns = d.columns ? d.columns.length : 1; return d.columns ? d.columns : [d]; }) .enter() .append('div') - .attr('class', 'x-axis-div'); + .attr('class', (d, i) => { + let divClass = ''; + if (i === 0) { + divClass += ' chart-first'; + } + if (i === columns - 1) { + divClass += ' chart-last'; + } + return 'x-axis-div axis-div' + divClass; + }); }); }; }; diff --git a/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js b/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js index 93d8188073a18..2fbc55bcae522 100644 --- a/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js +++ b/src/ui/public/vislib/lib/layout/splits/column_chart/y_axis_split.js @@ -9,40 +9,31 @@ define(function () { */ // render and get bounding box width - return function (selection, parent, opts) { - const yAxis = opts && opts.yAxis; + return function (selection) { selection.each(function () { const div = d3.select(this); - - div.call(setWidth, yAxis); + let rows; div.selectAll('.y-axis-div') .append('div') .data(function (d) { + rows = d.rows ? d.rows.length : 1; return d.rows ? d.rows : [d]; }) .enter() .append('div') - .attr('class', 'y-axis-div'); + .attr('class', (d, i) => { + let divClass = ''; + if (i === 0) { + divClass += ' chart-first'; + } + if (i === rows - 1) { + divClass += ' chart-last'; + } + return 'y-axis-div axis-div' + divClass; + }); }); }; - - function setWidth(el, yAxis) { - if (!yAxis) return; - - const padding = 5; - const height = parseInt(el.node().clientHeight, 10); - - // render svg and get the width of the bounding box - const svg = d3.select('body') - .append('svg') - .attr('style', 'position:absolute; top:-10000; left:-10000'); - const width = svg.append('g') - .call(yAxis.getYAxis(height)).node().getBBox().width + padding; - svg.remove(); - - el.style('width', (width + padding) + 'px'); - } }; }); diff --git a/src/ui/public/vislib/lib/layout/types/column_layout.js b/src/ui/public/vislib/lib/layout/types/column_layout.js index b4bd968d2ae9c..d74d031501109 100644 --- a/src/ui/public/vislib/lib/layout/types/column_layout.js +++ b/src/ui/public/vislib/lib/layout/types/column_layout.js @@ -1,7 +1,7 @@ -import VislibLibLayoutSplitsColumnChartChartSplitProvider from 'ui/vislib/lib/layout/splits/column_chart/chart_split'; -import VislibLibLayoutSplitsColumnChartYAxisSplitProvider from 'ui/vislib/lib/layout/splits/column_chart/y_axis_split'; -import VislibLibLayoutSplitsColumnChartXAxisSplitProvider from 'ui/vislib/lib/layout/splits/column_chart/x_axis_split'; -import VislibLibLayoutSplitsColumnChartChartTitleSplitProvider from 'ui/vislib/lib/layout/splits/column_chart/chart_title_split'; +import VislibLibLayoutSplitsColumnChartChartSplitProvider from '../splits/column_chart/chart_split'; +import VislibLibLayoutSplitsColumnChartYAxisSplitProvider from '../splits/column_chart/y_axis_split'; +import VislibLibLayoutSplitsColumnChartXAxisSplitProvider from '../splits/column_chart/x_axis_split'; +import VislibLibLayoutSplitsColumnChartChartTitleSplitProvider from '../splits/column_chart/chart_title_split'; export default function ColumnLayoutFactory(Private) { const chartSplit = Private(VislibLibLayoutSplitsColumnChartChartSplitProvider); @@ -44,11 +44,15 @@ export default function ColumnLayoutFactory(Private) { children: [ { type: 'div', - class: 'y-axis-col', + class: 'y-axis-spacer-block y-axis-spacer-block-top' + }, + { + type: 'div', + class: 'y-axis-col axis-wrapper-left', children: [ { type: 'div', - class: 'y-axis-title' + class: 'y-axis-title axis-title' }, { type: 'div', @@ -64,7 +68,7 @@ export default function ColumnLayoutFactory(Private) { }, { type: 'div', - class: 'y-axis-spacer-block' + class: 'y-axis-spacer-block y-axis-spacer-block-bottom' } ] }, @@ -72,6 +76,21 @@ export default function ColumnLayoutFactory(Private) { type: 'div', class: 'vis-col-wrapper', children: [ + { + type: 'div', + class: 'x-axis-wrapper axis-wrapper-top', + children: [ + { + type: 'div', + class: 'x-axis-title axis-title' + }, + { + type: 'div', + class: 'x-axis-div-wrapper', + splits: xAxisSplit + } + ] + }, { type: 'div', class: 'chart-wrapper', @@ -83,7 +102,7 @@ export default function ColumnLayoutFactory(Private) { }, { type: 'div', - class: 'x-axis-wrapper', + class: 'x-axis-wrapper axis-wrapper-bottom', children: [ { type: 'div', @@ -97,11 +116,40 @@ export default function ColumnLayoutFactory(Private) { }, { type: 'div', - class: 'x-axis-title' + class: 'x-axis-title axis-title' } ] } ] + }, + { + type: 'div', + class: 'y-axis-col-wrapper', + children: [ + { + type: 'div', + class: 'y-axis-spacer-block y-axis-spacer-block-top' + }, + { + type: 'div', + class: 'y-axis-col axis-wrapper-right', + children: [ + { + type: 'div', + class: 'y-axis-div-wrapper', + splits: yAxisSplit + }, + { + type: 'div', + class: 'y-axis-title axis-title' + } + ] + }, + { + type: 'div', + class: 'y-axis-spacer-block y-axis-spacer-block-bottom' + } + ] } ] } diff --git a/src/ui/public/vislib/lib/layout/types/map_layout.js b/src/ui/public/vislib/lib/layout/types/map_layout.js index 64106cdfe1350..79e12b04730ea 100644 --- a/src/ui/public/vislib/lib/layout/types/map_layout.js +++ b/src/ui/public/vislib/lib/layout/types/map_layout.js @@ -1,4 +1,4 @@ -import VislibLibLayoutSplitsTileMapMapSplitProvider from 'ui/vislib/lib/layout/splits/tile_map/map_split'; +import VislibLibLayoutSplitsTileMapMapSplitProvider from '../splits/tile_map/map_split'; export default function ColumnLayoutFactory(Private) { const mapSplit = Private(VislibLibLayoutSplitsTileMapMapSplitProvider); diff --git a/src/ui/public/vislib/lib/layout/types/pie_layout.js b/src/ui/public/vislib/lib/layout/types/pie_layout.js index ea7bedf61405e..59617fd6660bc 100644 --- a/src/ui/public/vislib/lib/layout/types/pie_layout.js +++ b/src/ui/public/vislib/lib/layout/types/pie_layout.js @@ -1,5 +1,5 @@ -import VislibLibLayoutSplitsPieChartChartSplitProvider from 'ui/vislib/lib/layout/splits/pie_chart/chart_split'; -import VislibLibLayoutSplitsPieChartChartTitleSplitProvider from 'ui/vislib/lib/layout/splits/pie_chart/chart_title_split'; +import VislibLibLayoutSplitsPieChartChartSplitProvider from '../splits/pie_chart/chart_split'; +import VislibLibLayoutSplitsPieChartChartTitleSplitProvider from '../splits/pie_chart/chart_title_split'; export default function ColumnLayoutFactory(Private) { const chartSplit = Private(VislibLibLayoutSplitsPieChartChartSplitProvider); const chartTitleSplit = Private(VislibLibLayoutSplitsPieChartChartTitleSplitProvider); diff --git a/src/ui/public/vislib/lib/types/index.js b/src/ui/public/vislib/lib/types/index.js new file mode 100644 index 0000000000000..aef9872308cee --- /dev/null +++ b/src/ui/public/vislib/lib/types/index.js @@ -0,0 +1,21 @@ +import VislibLibTypesPointSeriesProvider from './point_series'; +import VislibLibTypesPieProvider from './pie'; +import VislibLibTypesTileMapProvider from './tile_map'; + +export default function TypeFactory(Private) { + const pointSeries = Private(VislibLibTypesPointSeriesProvider); + + /** + * Handles the building of each visualization + * + * @return {Function} Returns an Object of Handler types + */ + return { + histogram: pointSeries.column, + line: pointSeries.line, + pie: Private(VislibLibTypesPieProvider), + area: pointSeries.area, + tile_map: Private(VislibLibTypesTileMapProvider), + point_series: pointSeries.line + }; +}; diff --git a/src/ui/public/vislib/lib/types/pie.js b/src/ui/public/vislib/lib/types/pie.js new file mode 100644 index 0000000000000..f763d80ff8bf3 --- /dev/null +++ b/src/ui/public/vislib/lib/types/pie.js @@ -0,0 +1,13 @@ +import _ from 'lodash'; + +export default function PieConfig(Private) { + + return function (config) { + if (!config.chart) { + config.chart = _.defaults({}, config, { + type: 'pie' + }); + } + return config; + }; +}; diff --git a/src/ui/public/vislib/lib/types/point_series.js b/src/ui/public/vislib/lib/types/point_series.js new file mode 100644 index 0000000000000..f8d01f435fe99 --- /dev/null +++ b/src/ui/public/vislib/lib/types/point_series.js @@ -0,0 +1,142 @@ +import _ from 'lodash'; + +export default function ColumnHandler(Private) { + + const createSeries = (cfg, series) => { + const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(cfg.mode); + return { + type: 'point_series', + series: _.map(series, (seri) => { + return { + show: true, + type: cfg.type || 'line', + mode: stacked ? 'stacked' : 'normal', + interpolate: cfg.interpolate, + smoothLines: cfg.smoothLines, + drawLinesBetweenPoints: cfg.drawLinesBetweenPoints, + showCircles: cfg.showCircles, + radiusRatio: cfg.radiusRatio, + data: seri + }; + }) + }; + }; + + const createCharts = (cfg, data) => { + if (data.rows || data.columns) { + const charts = data.rows ? data.rows : data.columns; + return charts.map(chart => { + return createSeries(cfg, chart.series); + }); + } + + return [createSeries(cfg, data.series)]; + }; + /* + * Create handlers for Area, Column, and Line charts which + * are all nearly the same minus a few details + */ + function create(opts) { + opts = opts || {}; + + return function (cfg, data) { + const isUserDefinedYAxis = cfg.setYExtents; + const config = _.defaults({}, cfg, { + chartTitle: {}, + mode: 'normal' + }, opts); + + config.type = 'point_series'; + + if (!config.tooltip) { + config.tooltip = { + show: cfg.addTooltip + }; + } + + if (!config.valueAxes) { + let mode = config.mode; + if (['stacked', 'overlap'].includes(mode)) mode = 'normal'; + config.valueAxes = [ + { + id: 'ValueAxis-1', + type: 'value', + scale: { + type: config.scale, + setYExtents: config.setYExtents, + defaultYExtents: config.defaultYExtents, + min : isUserDefinedYAxis ? config.yAxis.min : undefined, + max : isUserDefinedYAxis ? config.yAxis.max : undefined, + mode : mode + }, + labels: { + axisFormatter: data.data.yAxisFormatter || data.get('yAxisFormatter') + }, + title: { + text: data.get('yAxisLabel') + } + } + ]; + } + + if (!config.categoryAxes) { + config.categoryAxes = [ + { + id: 'CategoryAxis-1', + type: 'category', + labels: { + axisFormatter: data.data.xAxisFormatter || data.get('xAxisFormatter') + }, + scale: { + expandLastBucket: opts.expandLastBucket + }, + title: { + text: data.get('xAxisLabel') + } + } + ]; + } + + if (!config.charts) { + config.charts = createCharts(cfg, data.data); + } + + return config; + }; + } + + return { + line: create(), + + column: create({ + expandLastBucket: true + }), + + area: create({ + alerts: [ + { + type: 'warning', + msg: 'Positive and negative values are not accurately represented by stacked ' + + 'area charts. Either changing the chart mode to "overlap" or using a ' + + 'bar chart is recommended.', + test: function (vis, data) { + if (!data.shouldBeStacked() || data.maxNumberOfSeries() < 2) return; + + const hasPos = data.getYMax(data._getY) > 0; + const hasNeg = data.getYMin(data._getY) < 0; + return (hasPos && hasNeg); + } + }, + { + type: 'warning', + msg: 'Parts of or the entire area chart might not be displayed due to null ' + + 'values in the data. A line chart is recommended when displaying data ' + + 'with null values.', + test: function (vis, data) { + return data.hasNullValues(); + } + } + ] + }) + }; +}; diff --git a/src/ui/public/vislib/lib/types/tile_map.js b/src/ui/public/vislib/lib/types/tile_map.js new file mode 100644 index 0000000000000..7d94f2bcfdc8a --- /dev/null +++ b/src/ui/public/vislib/lib/types/tile_map.js @@ -0,0 +1,19 @@ +import _ from 'lodash'; +export default function MapHandlerProvider(Private) { + return function (config) { + if (!config.chart) { + config.chart = _.defaults({}, config, { + type: 'tile_map' + }); + } + + config.resize = function () { + this.charts.forEach(function (chart) { + chart.resizeArea(); + }); + }; + + return config; + }; +}; + diff --git a/src/ui/public/vislib/lib/vis_config.js b/src/ui/public/vislib/lib/vis_config.js new file mode 100644 index 0000000000000..f33e2b38c45c6 --- /dev/null +++ b/src/ui/public/vislib/lib/vis_config.js @@ -0,0 +1,46 @@ +/** + * Provides vislib configuration, throws error if invalid property is accessed without providing defaults + */ +import _ from 'lodash'; +import VisTypesProvider from './types'; +import VislibLibDataProvider from './data'; + +export default function VisConfigFactory(Private) { + + const Data = Private(VislibLibDataProvider); + const visTypes = Private(VisTypesProvider); + const DEFAULT_VIS_CONFIG = { + style: { + margin : { top: 10, right: 3, bottom: 5, left: 3 } + }, + alerts: {}, + categoryAxes: [], + valueAxes: [] + }; + + + class VisConfig { + constructor(visConfigArgs, data, uiState) { + this.data = new Data(data, uiState); + + const visType = visTypes[visConfigArgs.type]; + const typeDefaults = visType(visConfigArgs, this.data); + this._values = _.defaultsDeep({}, typeDefaults, DEFAULT_VIS_CONFIG); + }; + + get(property, defaults) { + if (_.has(this._values, property) || typeof defaults !== 'undefined') { + return _.get(this._values, property, defaults); + } else { + throw new Error(`Accessing invalid config property: ${property}`); + return defaults; + } + }; + + set(property, value) { + return _.set(this._values, property, value); + }; + } + + return VisConfig; +} diff --git a/src/ui/public/vislib/lib/x_axis.js b/src/ui/public/vislib/lib/x_axis.js deleted file mode 100644 index b7bdcbc0dc5bd..0000000000000 --- a/src/ui/public/vislib/lib/x_axis.js +++ /dev/null @@ -1,513 +0,0 @@ -import d3 from 'd3'; -import $ from 'jquery'; -import _ from 'lodash'; -import moment from 'moment'; -import VislibLibErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; -export default function XAxisFactory(Private) { - - const ErrorHandler = Private(VislibLibErrorHandlerProvider); - - /** - * Adds an x axis to the visualization - * - * @class XAxis - * @constructor - * @param args {{el: (HTMLElement), xValues: (Array), ordered: (Object|*), - * xAxisFormatter: (Function), _attr: (Object|*)}} - */ - class XAxis extends ErrorHandler { - constructor(args) { - super(); - this.el = args.el; - this.xValues = args.xValues; - this.ordered = args.ordered; - this.xAxisFormatter = args.xAxisFormatter; - this.expandLastBucket = args.expandLastBucket == null ? true : args.expandLastBucket; - this._attr = _.defaults(args._attr || {}); - } - - /** - * Renders the x axis - * - * @method render - * @returns {D3.UpdateSelection} Appends x axis to visualization - */ - render() { - d3.select(this.el).selectAll('.x-axis-div').call(this.draw()); - }; - - /** - * Returns d3 x axis scale function. - * If time, return time scale, else return d3 ordinal scale for nominal data - * - * @method getScale - * @returns {*} D3 scale function - */ - getScale() { - const ordered = this.ordered; - - if (ordered && ordered.date) { - return d3.time.scale.utc(); - } - return d3.scale.ordinal(); - }; - - /** - * Add domain to the x axis scale. - * if time, return a time domain, and calculate the min date, max date, and time interval - * else, return a nominal (d3.scale.ordinal) domain, i.e. array of x axis values - * - * @method getDomain - * @param scale {Function} D3 scale - * @returns {*} D3 scale function - */ - getDomain(scale) { - const ordered = this.ordered; - - if (ordered && ordered.date) { - return this.getTimeDomain(scale, this.xValues); - } - return this.getOrdinalDomain(scale, this.xValues); - }; - - /** - * Returns D3 time domain - * - * @method getTimeDomain - * @param scale {Function} D3 scale function - * @param data {Array} - * @returns {*} D3 scale function - */ - getTimeDomain(scale, data) { - return scale.domain([this.minExtent(data), this.maxExtent(data)]); - }; - - minExtent(data) { - return this._calculateExtent(data || this.xValues, 'min'); - }; - - maxExtent(data) { - return this._calculateExtent(data || this.xValues, 'max'); - }; - - /** - * - * @param data - * @param extent - */ - _calculateExtent(data, extent) { - const ordered = this.ordered; - const opts = [ordered[extent]]; - - let point = d3[extent](data); - if (this.expandLastBucket && extent === 'max') { - point = this.addInterval(point); - } - opts.push(point); - - return d3[extent](opts.reduce(function (opts, v) { - if (!_.isNumber(v)) v = +v; - if (!isNaN(v)) opts.push(v); - return opts; - }, [])); - }; - - /** - * Add the interval to a point on the x axis, - * this properly adds dates if needed. - * - * @param {number} x - a value on the x-axis - * @returns {number} - x + the ordered interval - */ - addInterval(x) { - return this.modByInterval(x, +1); - }; - - /** - * Subtract the interval to a point on the x axis, - * this properly subtracts dates if needed. - * - * @param {number} x - a value on the x-axis - * @returns {number} - x - the ordered interval - */ - subtractInterval(x) { - return this.modByInterval(x, -1); - }; - - /** - * Modify the x value by n intervals, properly - * handling dates if needed. - * - * @param {number} x - a value on the x-axis - * @param {number} n - the number of intervals - * @returns {number} - x + n intervals - */ - modByInterval(x, n) { - const ordered = this.ordered; - if (!ordered) return x; - const interval = ordered.interval; - if (!interval) return x; - - if (!ordered.date) { - return x += (ordered.interval * n); - } - - const y = moment(x); - const method = n > 0 ? 'add' : 'subtract'; - - _.times(Math.abs(n), function () { - y[method](interval); - }); - - return y.valueOf(); - }; - - /** - * Return a nominal(d3 ordinal) domain - * - * @method getOrdinalDomain - * @param scale {Function} D3 scale function - * @param xValues {Array} Array of x axis values - * @returns {*} D3 scale function - */ - getOrdinalDomain(scale, xValues) { - return scale.domain(xValues); - }; - - /** - * Return the range for the x axis scale - * if time, return a normal range, else if nominal, return rangeBands with a default (0.1) spacer specified - * - * @method getRange - * @param scale {Function} D3 scale function - * @param width {Number} HTML Element width - * @returns {*} D3 scale function - */ - getRange(domain, width) { - const ordered = this.ordered; - - if (ordered && ordered.date) { - return domain.range([0, width]); - } - return domain.rangeBands([0, width], 0.1); - }; - - /** - * Return the x axis scale - * - * @method getXScale - * @param width {Number} HTML Element width - * @returns {*} D3 x scale function - */ - getXScale(width) { - const domain = this.getDomain(this.getScale()); - - return this.getRange(domain, width); - }; - - /** - * Creates d3 xAxis function - * - * @method getXAxis - * @param width {Number} HTML Element width - */ - getXAxis(width) { - this.xScale = this.getXScale(width); - - if (!this.xScale || _.isNaN(this.xScale)) { - throw new Error('xScale is ' + this.xScale); - } - - this.xAxis = d3.svg.axis() - .scale(this.xScale) - .ticks(10) - .tickFormat(this.xAxisFormatter) - .orient('bottom'); - }; - - /** - * Renders the x axis - * - * @method draw - * @returns {Function} Renders the x axis to a D3 selection - */ - draw() { - const self = this; - this._attr.isRotated = false; - - return function (selection) { - const n = selection[0].length; - const parentWidth = $(self.el) - .find('.x-axis-div-wrapper') - .width(); - - selection.each(function () { - - const div = d3.select(this); - const width = parentWidth / n; - const height = $(this.parentElement).height(); - - self.validateWidthandHeight(width, height); - - self.getXAxis(width); - - const svg = div.append('svg') - .attr('width', width) - .attr('height', height); - - svg.append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,0)') - .call(self.xAxis); - }); - - selection.call(self.filterOrRotate()); - }; - }; - - /** - * Returns a function that evaluates scale type and - * applies filter to tick labels on time scales - * rotates and truncates tick labels on nominal/ordinal scales - * - * @method filterOrRotate - * @returns {Function} Filters or rotates x axis tick labels - */ - filterOrRotate() { - const self = this; - const ordered = self.ordered; - - return function (selection) { - selection.each(function () { - const axis = d3.select(this); - if (ordered && ordered.date) { - axis.call(self.filterAxisLabels()); - } else { - axis.call(self.rotateAxisLabels()); - } - }); - - self.updateXaxisHeight(); - - selection.call(self.fitTitles()); - - }; - }; - - /** - * Rotate the axis tick labels within selection - * - * @returns {Function} Rotates x axis tick labels of a D3 selection - */ - rotateAxisLabels() { - const self = this; - const barWidth = self.xScale.rangeBand(); - const maxRotatedLength = 120; - const xAxisPadding = 15; - const lengths = []; - self._attr.isRotated = false; - - return function (selection) { - const text = selection.selectAll('.tick text'); - - text.each(function textWidths() { - lengths.push(d3.select(this).node().getBBox().width); - }); - const length = _.max(lengths); - self._attr.xAxisLabelHt = length + xAxisPadding; - - // if longer than bar width, rotate - if (length > barWidth) { - self._attr.isRotated = true; - } - - // if longer than maxRotatedLength, truncate - if (length > maxRotatedLength) { - self._attr.xAxisLabelHt = maxRotatedLength; - } - - if (self._attr.isRotated) { - text - .text(function truncate() { - return self.truncateLabel(this, self._attr.xAxisLabelHt); - }) - .style('text-anchor', 'end') - .attr('dx', '-.8em') - .attr('dy', '-.60em') - .attr('transform', function rotate() { - return 'rotate(-90)'; - }) - .append('title') - .text(text => text); - - selection.select('svg') - .attr('height', self._attr.xAxisLabelHt); - } - }; - }; - - /** - * Returns a string that is truncated to fit size - * - * @method truncateLabel - * @param text {HTMLElement} - * @param size {Number} - * @returns {*|jQuery} - */ - truncateLabel(text, size) { - const node = d3.select(text).node(); - let str = $(node).text(); - const width = node.getBBox().width; - const chars = str.length; - const pxPerChar = width / chars; - let endChar = 0; - const ellipsesPad = 4; - - if (width > size) { - endChar = Math.floor((size / pxPerChar) - ellipsesPad); - while (str[endChar - 1] === ' ' || str[endChar - 1] === '-' || str[endChar - 1] === ',') { - endChar = endChar - 1; - } - str = str.substr(0, endChar) + '...'; - } - return str; - }; - - /** - * Filter out text labels by width and position on axis - * trims labels that would overlap each other - * or extend past left or right edges - * if prev label pos (or 0) + half of label width is < label pos - * and label pos + half width is not > width of axis - * - * @method filterAxisLabels - * @returns {Function} - */ - filterAxisLabels() { - const self = this; - let startX = 0; - let maxW; - let par; - let myX; - let myWidth; - let halfWidth; - const padding = 1.1; - - return function (selection) { - selection.selectAll('.tick text') - .text(function (d) { - par = d3.select(this.parentNode).node(); - myX = self.xScale(d); - myWidth = par.getBBox().width * padding; - halfWidth = myWidth / 2; - maxW = $(self.el).find('.x-axis-div').width(); - - if ((startX + halfWidth) < myX && maxW > (myX + halfWidth)) { - startX = myX + halfWidth; - return self.xAxisFormatter(d); - } else { - d3.select(this.parentNode).remove(); - } - }); - }; - }; - - /** - * Returns a function that adjusts axis titles and - * chart title transforms to fit axis label divs. - * Sets transform of x-axis-title to fit .x-axis-title div width - * if x-axis-chart-titles, set transform of x-axis-chart-titles - * to fit .chart-title div width - * - * @method fitTitles - * @returns {Function} - */ - fitTitles() { - const visEls = $('.vis-wrapper'); - let xAxisChartTitle; - let yAxisChartTitle; - let text; - let titles; - - return function () { - - visEls.each(function () { - const visEl = d3.select(this); - const $visEl = $(this); - const xAxisTitle = $visEl.find('.x-axis-title'); - const yAxisTitle = $visEl.find('.y-axis-title'); - let titleWidth = xAxisTitle.width(); - let titleHeight = yAxisTitle.height(); - - text = visEl.select('.x-axis-title') - .select('svg') - .attr('width', titleWidth) - .select('text') - .attr('transform', 'translate(' + (titleWidth / 2) + ',11)'); - - text = visEl.select('.y-axis-title') - .select('svg') - .attr('height', titleHeight) - .select('text') - .attr('transform', 'translate(11,' + (titleHeight / 2) + ')rotate(-90)'); - - if ($visEl.find('.x-axis-chart-title').length) { - xAxisChartTitle = $visEl.find('.x-axis-chart-title'); - titleWidth = xAxisChartTitle.find('.chart-title').width(); - - titles = visEl.select('.x-axis-chart-title').selectAll('.chart-title'); - titles.each(function () { - text = d3.select(this) - .select('svg') - .attr('width', titleWidth) - .select('text') - .attr('transform', 'translate(' + (titleWidth / 2) + ',11)'); - }); - } - - if ($visEl.find('.y-axis-chart-title').length) { - yAxisChartTitle = $visEl.find('.y-axis-chart-title'); - titleHeight = yAxisChartTitle.find('.chart-title').height(); - - titles = visEl.select('.y-axis-chart-title').selectAll('.chart-title'); - titles.each(function () { - text = d3.select(this) - .select('svg') - .attr('height', titleHeight) - .select('text') - .attr('transform', 'translate(11,' + (titleHeight / 2) + ')rotate(-90)'); - }); - } - - }); - - }; - }; - - /** - * Appends div to make .y-axis-spacer-block - * match height of .x-axis-wrapper - * - * @method updateXaxisHeight - */ - updateXaxisHeight() { - const selection = d3.select(this.el).selectAll('.vis-wrapper'); - - selection.each(function () { - const visEl = d3.select(this); - - if (visEl.select('.inner-spacer-block').node() === null) { - visEl.select('.y-axis-spacer-block') - .append('div') - .attr('class', 'inner-spacer-block'); - } - const xAxisHt = visEl.select('.x-axis-wrapper').style('height'); - - visEl.select('.inner-spacer-block').style('height', xAxisHt); - }); - - }; - } - - return XAxis; -}; diff --git a/src/ui/public/vislib/lib/y_axis.js b/src/ui/public/vislib/lib/y_axis.js deleted file mode 100644 index f5d7d55ff0e77..0000000000000 --- a/src/ui/public/vislib/lib/y_axis.js +++ /dev/null @@ -1,236 +0,0 @@ -import d3 from 'd3'; -import _ from 'lodash'; -import $ from 'jquery'; -import errors from 'ui/errors'; -import VislibLibErrorHandlerProvider from 'ui/vislib/lib/_error_handler'; -export default function YAxisFactory(Private) { - - const ErrorHandler = Private(VislibLibErrorHandlerProvider); - - /** - * Appends y axis to the visualization - * - * @class YAxis - * @constructor - * @param args {{el: (HTMLElement), yMax: (Number), _attr: (Object|*)}} - */ - class YAxis extends ErrorHandler { - constructor(args) { - super(); - this.el = args.el; - this.scale = null; - this.domain = [args.yMin, args.yMax]; - this.yAxisFormatter = args.yAxisFormatter; - this._attr = args._attr || {}; - } - - /** - * Renders the y axis - * - * @method render - * @return {D3.UpdateSelection} Renders y axis to visualization - */ - render() { - d3.select(this.el).selectAll('.y-axis-div').call(this.draw()); - }; - - _isPercentage() { - return (this._attr.mode === 'percentage'); - }; - - _isUserDefined() { - return (this._attr.setYExtents); - }; - - _isYExtents() { - return (this._attr.defaultYExtents); - }; - - _validateUserExtents(domain) { - const self = this; - - return domain.map(function (val) { - val = parseInt(val, 10); - - if (isNaN(val)) throw new Error(val + ' is not a valid number'); - if (self._isPercentage() && self._attr.setYExtents) return val / 100; - return val; - }); - }; - - _getExtents(domain) { - const min = domain[0]; - const max = domain[1]; - - if (this._isUserDefined()) return this._validateUserExtents(domain); - if (this._isYExtents()) return domain; - if (this._attr.scale === 'log') return this._logDomain(min, max); // Negative values cannot be displayed with a log scale. - if (!this._isYExtents() && !this._isUserDefined()) return [Math.min(0, min), Math.max(0, max)]; - return domain; - }; - - _throwCustomError(message) { - throw new Error(message); - }; - - _throwLogScaleValuesError() { - throw new errors.InvalidLogScaleValues(); - }; - - /** - * Returns the appropriate D3 scale - * - * @param fnName {String} D3 scale - * @returns {*} - */ - _getScaleType(fnName) { - if (fnName === 'square root') fnName = 'sqrt'; // Rename 'square root' to 'sqrt' - fnName = fnName || 'linear'; - - if (typeof d3.scale[fnName] !== 'function') return this._throwCustomError('YAxis.getScaleType: ' + fnName + ' is not a function'); - - return d3.scale[fnName](); - }; - - /** - * Return the domain for log scale, i.e. the extent of the log scale. - * Log scales must begin at 1 since the log(0) = -Infinity - * - * @param {Number} min - * @param {Number} max - * @returns {Array} - */ - _logDomain(min, max) { - if (min < 0 || max < 0) return this._throwLogScaleValuesError(); - return [1, max]; - }; - - /** - * Creates the d3 y scale function - * - * @method getYScale - * @param height {Number} DOM Element height - * @returns {D3.Scale.QuantitiveScale|*} D3 yScale function - */ - getYScale(height) { - const scale = this._getScaleType(this._attr.scale); - const domain = this._getExtents(this.domain); - - this.yScale = scale - .domain(domain) - .range([height, 0]); - - if (!this._isUserDefined()) this.yScale.nice(); // round extents when not user defined - // Prevents bars from going off the chart when the y extents are within the domain range - if (this._attr.type === 'histogram') this.yScale.clamp(true); - return this.yScale; - }; - - getScaleType() { - return this._attr.scale; - }; - - tickFormat() { - const isPercentage = this._attr.mode === 'percentage'; - if (isPercentage) return d3.format('%'); - if (this.yAxisFormatter) return this.yAxisFormatter; - return d3.format('n'); - }; - - _validateYScale(yScale) { - if (!yScale || _.isNaN(yScale)) throw new Error('yScale is ' + yScale); - }; - - /** - * Creates the d3 y axis function - * - * @method getYAxis - * @param height {Number} DOM Element height - * @returns {D3.Svg.Axis|*} D3 yAxis function - */ - getYAxis(height) { - const yScale = this.getYScale(height); - this._validateYScale(yScale); - - // Create the d3 yAxis function - this.yAxis = d3.svg.axis() - .scale(yScale) - .tickFormat(this.tickFormat(this.domain)) - .ticks(this.tickScale(height)) - .orient('left'); - - return this.yAxis; - }; - - /** - * Create a tick scale for the y axis that modifies the number of ticks - * based on the height of the wrapping DOM element - * Avoid using even numbers in the yTickScale.range - * Causes the top most tickValue in the chart to be missing - * - * @method tickScale - * @param height {Number} DOM element height - * @returns {number} Number of y axis ticks - */ - tickScale(height) { - const yTickScale = d3.scale.linear() - .clamp(true) - .domain([20, 40, 1000]) - .range([0, 3, 11]); - - return Math.ceil(yTickScale(height)); - }; - - /** - * Renders the y axis to the visualization - * - * @method draw - * @returns {Function} Renders y axis to visualization - */ - draw() { - const self = this; - const margin = this._attr.margin; - const mode = this._attr.mode; - const isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette'); - - return function (selection) { - selection.each(function () { - const el = this; - - const div = d3.select(el); - const width = $(el).parent().width(); - const height = $(el).height(); - const adjustedHeight = height - margin.top - margin.bottom; - - // Validate whether width and height are not 0 or `NaN` - self.validateWidthandHeight(width, adjustedHeight); - - const yAxis = self.getYAxis(adjustedHeight); - - // The yAxis should not appear if mode is set to 'wiggle' or 'silhouette' - if (!isWiggleOrSilhouette) { - // Append svg and y axis - const svg = div.append('svg') - .attr('width', width) - .attr('height', height); - - svg.append('g') - .attr('class', 'y axis') - .attr('transform', 'translate(' + (width - 2) + ',' + margin.top + ')') - .call(yAxis); - - const container = svg.select('g.y.axis').node(); - if (container) { - const cWidth = Math.max(width, container.getBBox().width); - svg.attr('width', cWidth); - svg.select('g') - .attr('transform', 'translate(' + (cWidth - 2) + ',' + margin.top + ')'); - } - } - }); - }; - }; - } - - return YAxis; -}; diff --git a/src/ui/public/vislib/styles/_layout.less b/src/ui/public/vislib/styles/_layout.less index e704d18745ccf..3b6076c9333b9 100644 --- a/src/ui/public/vislib/styles/_layout.less +++ b/src/ui/public/vislib/styles/_layout.less @@ -12,6 +12,11 @@ min-height: 0; min-width: 0; overflow: hidden; + padding: 10px 0; +} + +.vis-wrapper svg { + overflow: visible; } /* YAxis logic */ @@ -31,7 +36,7 @@ } .y-axis-spacer-block { - min-height: 45px; + min-height: 0px; } .y-axis-div-wrapper { @@ -43,13 +48,14 @@ .y-axis-div { flex: 1 1 25px; - min-width: 14px; + min-width: 1px; min-height: 14px; + margin: 5px 0px; } .y-axis-title { min-height: 14px; - min-width: 14px; + min-width: 1px; } .y-axis-chart-title { @@ -57,7 +63,6 @@ flex-direction: column; min-height: 14px; min-width: 0; - width: 14px; } .y-axis-title text, .x-axis-title text { @@ -83,7 +88,6 @@ flex-direction: column; min-height: 0; min-width: 0; - margin-right: 8px; } .chart-wrapper { @@ -93,6 +97,17 @@ margin: 0; min-height: 0; min-width: 0; + margin: 5px; +} + +.chart-wrapper-row .chart-wrapper { + margin-left: 0px; + margin-right: 0px; +} + +.chart-wrapper-column .chart-wrapper { + margin-top: 0px; + margin-bottom: 0px; } .chart-wrapper-column { @@ -138,7 +153,7 @@ .x-axis-wrapper { display: flex; flex-direction: column; - min-height: 45px; + min-height: 0px; min-width: 0; overflow: visible; } @@ -146,27 +161,41 @@ .x-axis-div-wrapper { display: flex; flex-direction: row; - min-height: 20px; + min-height: 0px; min-width: 0; } .x-axis-chart-title { display: flex; flex-direction: row; - min-height: 15px; + min-height: 1px; max-height: 15px; min-width: 20px; } .x-axis-title { - min-height: 15px; + min-height: 0px; max-height: 15px; min-width: 20px; overflow: hidden; } .x-axis-div { - margin-top: -5px; - min-height: 20px; + min-height: 0px; min-width: 20px; + margin: 0px 5px; + width: 100%; +} + +.axis-wrapper-top .axis-div svg { + margin-bottom: -5px; +} + +.chart-first { + margin-top: 0px; + margin-left: 0px +} +.chart-last { + margin-bottom: 0px; + margin-right: 0px; } diff --git a/src/ui/public/vislib/styles/_svg.less b/src/ui/public/vislib/styles/_svg.less index 941d1b38372a7..ae2fd14a73792 100644 --- a/src/ui/public/vislib/styles/_svg.less +++ b/src/ui/public/vislib/styles/_svg.less @@ -12,10 +12,6 @@ } } -.x.axis path { - display: none; -} - .tick text { font-size: 8pt; fill: @svg-tick-text-color; diff --git a/src/ui/public/vislib/vis.js b/src/ui/public/vislib/vis.js index 7b60f64cebfa2..17383c203ecea 100644 --- a/src/ui/public/vislib/vis.js +++ b/src/ui/public/vislib/vis.js @@ -2,18 +2,17 @@ import _ from 'lodash'; import d3 from 'd3'; import Binder from 'ui/binder'; import errors from 'ui/errors'; -import 'ui/vislib/styles/main.less'; -import VislibLibResizeCheckerProvider from 'ui/vislib/lib/resize_checker'; import EventsProvider from 'ui/events'; -import VislibLibHandlerHandlerTypesProvider from 'ui/vislib/lib/handler/handler_types'; -import VislibVisualizationsVisTypesProvider from 'ui/vislib/visualizations/vis_types'; -export default function VisFactory(Private) { - +import './styles/main.less'; +import VislibLibResizeCheckerProvider from './lib/resize_checker'; +import VisConifgProvider from './lib/vis_config'; +import VisHandlerProvider from './lib/handler'; +export default function VisFactory(Private) { const ResizeChecker = Private(VislibLibResizeCheckerProvider); const Events = Private(EventsProvider); - const handlerTypes = Private(VislibLibHandlerHandlerTypesProvider); - const chartTypes = Private(VislibVisualizationsVisTypesProvider); + const VisConfig = Private(VisConifgProvider); + const Handler = Private(VisHandlerProvider); /** * Creates the visualizations. @@ -24,14 +23,12 @@ export default function VisFactory(Private) { * @param config {Object} Parameters that define the chart type and chart options */ class Vis extends Events { - constructor($el, config) { + constructor($el, visConfigArgs) { super(arguments); this.el = $el.get ? $el.get(0) : $el; this.binder = new Binder(); - this.ChartClass = chartTypes[config.type]; - this._attr = _.defaults({}, config || {}, { - legendOpen: true - }); + this.visConfigArgs = visConfigArgs; + this.visConfigArgs.el = this.el; // bind the resize function so it can be used as an event handler this.resize = _.bind(this.resize, this); @@ -39,6 +36,9 @@ export default function VisFactory(Private) { this.binder.on(this.resizeChecker, 'resize', this.resize); } + hasLegend() { + return this.visConfigArgs.addLegend; + } /** * Renders the visualization * @@ -46,8 +46,6 @@ export default function VisFactory(Private) { * @param data {Object} Elasticsearch query results */ render(data, uiState) { - const chartType = this._attr.type; - if (!data) { throw new Error('No valid data!'); } @@ -69,7 +67,8 @@ export default function VisFactory(Private) { uiState.on('change', this._uiStateChangeHandler); } - this.handler = handlerTypes[chartType](this) || handlerTypes.column(this); + this.visConfig = new VisConfig(this.visConfigArgs, this.data, this.uiState); + this.handler = new Handler(this, this.visConfig); this._runWithoutResizeChecker('render'); }; @@ -80,7 +79,6 @@ export default function VisFactory(Private) { */ resize() { if (!this.data) { - // TODO: need to come up with a solution for resizing when no data is available return; } @@ -140,7 +138,7 @@ export default function VisFactory(Private) { * @param val {*} Value to which the attribute name is set */ set(name, val) { - this._attr[name] = val; + this.visConfigArgs[name] = val; this.render(this.data, this.uiState); }; @@ -152,7 +150,7 @@ export default function VisFactory(Private) { * @returns {*} The value of the attribute name */ get(name) { - return this._attr[name]; + return this.visConfig.get(name); }; /** diff --git a/src/ui/public/vislib/vislib.js b/src/ui/public/vislib/vislib.js index 5f7a7ebd71100..0c0a91164f1c4 100644 --- a/src/ui/public/vislib/vislib.js +++ b/src/ui/public/vislib/vislib.js @@ -1,13 +1,13 @@ -import 'ui/vislib/lib/handler/types/pie'; -import 'ui/vislib/lib/handler/types/point_series'; -import 'ui/vislib/lib/handler/types/tile_map'; -import 'ui/vislib/lib/handler/handler_types'; -import 'ui/vislib/lib/layout/layout_types'; -import 'ui/vislib/lib/data'; -import 'ui/vislib/visualizations/_map.js'; -import 'ui/vislib/visualizations/vis_types'; -import 'ui/vislib/styles/main.less'; -import VislibVisProvider from 'ui/vislib/vis'; +import './lib/types/pie'; +import './lib/types/point_series'; +import './lib/types/tile_map'; +import './lib/types'; +import './lib/layout/layout_types'; +import './lib/data'; +import './visualizations/_map.js'; +import './visualizations/vis_types'; +import './styles/main.less'; +import VislibVisProvider from './vis'; // prefetched for faster optimization runs // end prefetching diff --git a/src/ui/public/vislib/visualizations/_chart.js b/src/ui/public/vislib/visualizations/_chart.js index 7f9248b08c3fd..522de3b7a88d9 100644 --- a/src/ui/public/vislib/visualizations/_chart.js +++ b/src/ui/public/vislib/visualizations/_chart.js @@ -1,8 +1,8 @@ import d3 from 'd3'; import _ from 'lodash'; import dataLabel from 'ui/vislib/lib/_data_label'; -import VislibLibDispatchProvider from 'ui/vislib/lib/dispatch'; -import VislibComponentsTooltipProvider from 'ui/vislib/components/tooltip'; +import VislibLibDispatchProvider from '../lib/dispatch'; +import VislibComponentsTooltipProvider from '../components/tooltip'; export default function ChartBaseClass(Private) { const Dispatch = Private(VislibLibDispatchProvider); @@ -26,7 +26,7 @@ export default function ChartBaseClass(Private) { const events = this.events = new Dispatch(handler); - if (_.get(this.handler, '_attr.addTooltip')) { + if (this.handler.visConfig && this.handler.visConfig.get('addTooltip', false)) { const $el = this.handler.el; const formatter = this.handler.data.get('tooltipFormatter'); @@ -35,7 +35,6 @@ export default function ChartBaseClass(Private) { this.tooltips.push(this.tooltip); } - this._attr = _.defaults(this.handler._attr || {}, {}); this._addIdentifier = _.bind(this._addIdentifier, this); } diff --git a/src/ui/public/vislib/visualizations/_point_series_chart.js b/src/ui/public/vislib/visualizations/_point_series_chart.js deleted file mode 100644 index 7cad5cda2dadc..0000000000000 --- a/src/ui/public/vislib/visualizations/_point_series_chart.js +++ /dev/null @@ -1,176 +0,0 @@ -import d3 from 'd3'; -import _ from 'lodash'; -import VislibVisualizationsChartProvider from 'ui/vislib/visualizations/_chart'; -import VislibComponentsTooltipProvider from 'ui/vislib/components/tooltip'; -import errors from 'ui/errors'; - -export default function PointSeriesChartProvider(Private) { - - const Chart = Private(VislibVisualizationsChartProvider); - const Tooltip = Private(VislibComponentsTooltipProvider); - const touchdownTmpl = _.template(require('ui/vislib/partials/touchdown.tmpl.html')); - - class PointSeriesChart extends Chart { - constructor(handler, chartEl, chartData) { - super(handler, chartEl, chartData); - } - - _stackMixedValues(stackCount) { - let currentStackOffsets = [0, 0]; - let currentStackIndex = 0; - - return function (d, y0, y) { - const firstStack = currentStackIndex % stackCount === 0; - const lastStack = ++currentStackIndex === stackCount; - - if (firstStack) { - currentStackOffsets = [0, 0]; - } - - if (lastStack) currentStackIndex = 0; - - if (y >= 0) { - d.y0 = currentStackOffsets[1]; - currentStackOffsets[1] += y; - } else { - d.y0 = currentStackOffsets[0]; - currentStackOffsets[0] += y; - } - }; - }; - - /** - * Stacks chart data values - * - * @method stackData - * @param data {Object} Elasticsearch query result for this chart - * @returns {Array} Stacked data objects with x, y, and y0 values - */ - stackData(data) { - const self = this; - const isHistogram = (this._attr.type === 'histogram' && this._attr.mode === 'stacked'); - const stack = this._attr.stack; - - if (isHistogram) stack.out(self._stackMixedValues(data.series.length)); - - return stack(data.series.map(function (d) { - const label = d.label; - return d.values.map(function (e, i) { - return { - _input: e, - label: label, - x: self._attr.xValue.call(d.values, e, i), - y: self._attr.yValue.call(d.values, e, i) - }; - }); - })); - }; - - - validateDataCompliesWithScalingMethod(data) { - const valuesSmallerThanOne = function (d) { - return d.values && d.values.some(e => e.y < 1); - }; - - const invalidLogScale = data.series && data.series.some(valuesSmallerThanOne); - if (this._attr.scale === 'log' && invalidLogScale) { - throw new errors.InvalidLogScaleValues(); - } - }; - - /** - * Creates rects to show buckets outside of the ordered.min and max, returns rects - * - * @param xScale {Function} D3 xScale function - * @param svg {HTMLElement} Reference to SVG - * @method createEndZones - * @returns {D3.Selection} - */ - createEndZones(svg) { - const self = this; - const xAxis = this.handler.xAxis; - const xScale = xAxis.xScale; - const ordered = xAxis.ordered; - const missingMinMax = !ordered || _.isUndefined(ordered.min) || _.isUndefined(ordered.max); - - if (missingMinMax || ordered.endzones === false) return; - - const attr = this.handler._attr; - const height = attr.height; - const width = attr.width; - const margin = attr.margin; - - // we don't want to draw endzones over our min and max values, they - // are still a part of the dataset. We want to start the endzones just - // outside of them so we will use these values rather than ordered.min/max - const oneUnit = (ordered.units || _.identity)(1); - - // points on this axis represent the amount of time they cover, - // so draw the endzones at the actual time bounds - const leftEndzone = { - x: 0, - w: Math.max(xScale(ordered.min), 0) - }; - - const rightLastVal = xAxis.expandLastBucket ? ordered.max : Math.min(ordered.max, _.last(xAxis.xValues)); - const rightStart = rightLastVal + oneUnit; - const rightEndzone = { - x: xScale(rightStart), - w: Math.max(width - xScale(rightStart), 0) - }; - - this.endzones = svg.selectAll('.layer') - .data([leftEndzone, rightEndzone]) - .enter() - .insert('g', '.brush') - .attr('class', 'endzone') - .append('rect') - .attr('class', 'zone') - .attr('x', function (d) { - return d.x; - }) - .attr('y', 0) - .attr('height', height - margin.top - margin.bottom) - .attr('width', function (d) { - return d.w; - }); - - function callPlay(event) { - const boundData = event.target.__data__; - const mouseChartXCoord = event.clientX - self.chartEl.getBoundingClientRect().left; - const wholeBucket = boundData && boundData.x != null; - - // the min and max that the endzones start in - const min = leftEndzone.w; - const max = rightEndzone.x; - - // bounds of the cursor to consider - let xLeft = mouseChartXCoord; - let xRight = mouseChartXCoord; - if (wholeBucket) { - xLeft = xScale(boundData.x); - xRight = xScale(xAxis.addInterval(boundData.x)); - } - - return { - wholeBucket: wholeBucket, - touchdown: min > xLeft || max < xRight - }; - } - - function textFormatter() { - return touchdownTmpl(callPlay(d3.event)); - } - - const endzoneTT = new Tooltip('endzones', this.handler.el, textFormatter, null); - this.tooltips.push(endzoneTT); - endzoneTT.order = 0; - endzoneTT.showCondition = function inEndzone() { - return callPlay(d3.event).touchdown; - }; - endzoneTT.render()(svg); - }; - } - - return PointSeriesChart; -}; diff --git a/src/ui/public/vislib/visualizations/area_chart.js b/src/ui/public/vislib/visualizations/area_chart.js deleted file mode 100644 index 04528482c3776..0000000000000 --- a/src/ui/public/vislib/visualizations/area_chart.js +++ /dev/null @@ -1,379 +0,0 @@ -import d3 from 'd3'; -import _ from 'lodash'; -import $ from 'jquery'; -import errors from 'ui/errors'; -import VislibVisualizationsPointSeriesChartProvider from 'ui/vislib/visualizations/_point_series_chart'; -import VislibVisualizationsTimeMarkerProvider from 'ui/vislib/visualizations/time_marker'; -export default function AreaChartFactory(Private) { - - const PointSeriesChart = Private(VislibVisualizationsPointSeriesChartProvider); - const TimeMarker = Private(VislibVisualizationsTimeMarkerProvider); - - /** - * Area chart visualization - * - * @class AreaChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific - * chart - */ - class AreaChart extends PointSeriesChart { - constructor(handler, chartEl, chartData) { - super(handler, chartEl, chartData); - - this.isOverlapping = (handler._attr.mode === 'overlap'); - - if (this.isOverlapping) { - - // Default opacity should return to 0.6 on mouseout - const defaultOpacity = 0.6; - handler._attr.defaultOpacity = defaultOpacity; - handler.highlight = function (element) { - const label = this.getAttribute('data-label'); - if (!label) return; - - const highlightOpacity = 0.8; - const highlightElements = $('[data-label]', element.parentNode).filter( - function (els, el) { - return `${$(el).data('label')}` === label; - }); - $('[data-label]', element.parentNode).not(highlightElements).css('opacity', defaultOpacity / 2); // half of the default opacity - highlightElements.css('opacity', highlightOpacity); - }; - handler.unHighlight = function (element) { - $('[data-label]', element).css('opacity', defaultOpacity); - - //The legend should keep max opacity - $('[data-label]', $(element).siblings()).css('opacity', 1); - }; - } - - this.checkIfEnoughData(); - - this._attr = _.defaults(handler._attr || {}, { - xValue: function (d) { - return d.x; - }, - yValue: function (d) { - return d.y; - } - }); - } - - /** - * Adds SVG path to area chart - * - * @method addPath - * @param svg {HTMLElement} SVG to which rect are appended - * @param layers {Array} Chart data array - * @returns {D3.UpdateSelection} SVG with path added - */ - addPath(svg, layers) { - const ordered = this.handler.data.get('ordered'); - const isTimeSeries = (ordered && ordered.date); - const isOverlapping = this.isOverlapping; - const color = this.handler.data.getColorFunc(); - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; - const interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate; - const area = d3.svg.area() - .x(function (d) { - if (isTimeSeries) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - }) - .y0(function (d) { - if (isOverlapping) { - return yScale(0); - } - - return yScale(d.y0); - }) - .y1(function (d) { - if (isOverlapping) { - return yScale(d.y); - } - - return yScale(d.y0 + d.y); - }) - .defined(function (d) { - return !_.isNull(d.y); - }) - .interpolate(interpolate); - - // Data layers - const layer = svg.selectAll('.layer') - .data(layers) - .enter() - .append('g') - .attr('class', function (d, i) { - return 'pathgroup ' + i; - }); - - // Append path - const path = layer.append('path') - .call(this._addIdentifier) - .style('fill', function (d) { - return color(d[0].label); - }) - .classed('overlap_area', function () { - return isOverlapping; - }); - - // update - path.attr('d', function (d) { - return area(d); - }); - - return path; - }; - - /** - * Adds Events to SVG circles - * - * @method addCircleEvents - * @param element {D3.UpdateSelection} SVG circles - * @returns {D3.Selection} circles with event listeners attached - */ - addCircleEvents(element, svg) { - const events = this.events; - const isBrushable = events.isBrushable(); - const brush = isBrushable ? events.addBrushEvent(svg) : undefined; - const hover = events.addHoverEvent(); - const mouseout = events.addMouseoutEvent(); - const click = events.addClickEvent(); - const attachedEvents = element.call(hover).call(mouseout).call(click); - - if (isBrushable) { - attachedEvents.call(brush); - } - - return attachedEvents; - }; - - /** - * Adds SVG circles to area chart - * - * @method addCircles - * @param svg {HTMLElement} SVG to which circles are appended - * @param data {Array} Chart data array - * @returns {D3.UpdateSelection} SVG with circles added - */ - addCircles(svg, data) { - const color = this.handler.data.getColorFunc(); - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; - const ordered = this.handler.data.get('ordered'); - const circleRadius = 12; - const circleStrokeWidth = 0; - const tooltip = this.tooltip; - const isTooltip = this._attr.addTooltip; - const isOverlapping = this.isOverlapping; - - const layer = svg.selectAll('.points') - .data(data) - .enter() - .append('g') - .attr('class', 'points area'); - - // append the circles - const circles = layer - .selectAll('circles') - .data(function appendData(data) { - return data.filter(function isZeroOrNull(d) { - return d.y !== 0 && !_.isNull(d.y); - }); - }); - - // exit - circles.exit().remove(); - - // enter - circles - .enter() - .append('circle') - .call(this._addIdentifier) - .attr('stroke', function strokeColor(d) { - return color(d.label); - }) - .attr('fill', 'transparent') - .attr('stroke-width', circleStrokeWidth); - - // update - circles - .attr('cx', function cx(d) { - if (ordered && ordered.date) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - }) - .attr('cy', function cy(d) { - if (isOverlapping) { - return yScale(d.y); - } - return yScale(d.y0 + d.y); - }) - .attr('r', circleRadius); - - // Add tooltip - if (isTooltip) { - circles.call(tooltip.render()); - } - - return circles; - }; - - /** - * Adds SVG clipPath - * - * @method addClipPath - * @param svg {HTMLElement} SVG to which clipPath is appended - * @param width {Number} SVG width - * @param height {Number} SVG height - * @returns {D3.UpdateSelection} SVG with clipPath added - */ - addClipPath(svg, width, height) { - // Prevents circles from being clipped at the top of the chart - const startX = 0; - const startY = 0; - const id = 'chart-area' + _.uniqueId(); - - // Creating clipPath - return svg - .attr('clip-path', 'url(#' + id + ')') - .append('clipPath') - .attr('id', id) - .append('rect') - .attr('x', startX) - .attr('y', startY) - .attr('width', width) - .attr('height', height); - }; - - checkIfEnoughData() { - const series = this.chartData.series; - const message = 'Area charts require more than one data point. Try adding ' + - 'an X-Axis Aggregation'; - - const notEnoughData = series.some(function (obj) { - return obj.values.length < 2; - }); - - if (notEnoughData) { - throw new errors.NotEnoughData(message); - } - }; - - validateWiggleSelection() { - const isWiggle = this._attr.mode === 'wiggle'; - const ordered = this.handler.data.get('ordered'); - - if (isWiggle && !ordered) throw new errors.InvalidWiggleSelection(); - }; - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the area chart - */ - draw() { - // Attributes - const self = this; - const xScale = this.handler.xAxis.xScale; - const $elem = $(this.chartEl); - const margin = this._attr.margin; - const elWidth = this._attr.width = $elem.width(); - const elHeight = this._attr.height = $elem.height(); - const yMin = this.handler.yAxis.yMin; - const yScale = this.handler.yAxis.yScale; - const minWidth = 20; - const minHeight = 20; - const addTimeMarker = this._attr.addTimeMarker; - const times = this._attr.times || []; - let timeMarker; - - return function (selection) { - selection.each(function (data) { - // Stack data - const layers = self.stackData(data); - - // Get the width and height - const width = elWidth; - const height = elHeight - margin.top - margin.bottom; - - if (addTimeMarker) { - timeMarker = new TimeMarker(times, xScale, height); - } - - if (width < minWidth || height < minHeight) { - throw new errors.ContainerTooSmall(); - } - self.validateWiggleSelection(); - - // Select the current DOM element - const div = d3.select(this); - - // Create the canvas for the visualization - const svg = div.append('svg') - .attr('width', width) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(0,' + margin.top + ')'); - - // add clipPath to hide circles when they go out of bounds - self.addClipPath(svg, width, height); - self.createEndZones(svg); - - // add path - self.addPath(svg, layers); - - if (yMin < 0 && self._attr.mode !== 'wiggle' && self._attr.mode !== 'silhouette') { - - // Draw line at yScale 0 value - svg.append('line') - .attr('class', 'zero-line') - .attr('x1', 0) - .attr('y1', yScale(0)) - .attr('x2', width) - .attr('y2', yScale(0)) - .style('stroke', '#ddd') - .style('stroke-width', 1); - } - - // add circles - const circles = self.addCircles(svg, layers); - - // add click and hover events to circles - self.addCircleEvents(circles, svg); - - // chart base line - svg.append('line') - .attr('class', 'base-line') - .attr('x1', 0) - .attr('y1', yScale(0)) - .attr('x2', width) - .attr('y2', yScale(0)) - .style('stroke', '#ddd') - .style('stroke-width', 1); - - if (addTimeMarker) { - timeMarker.render(svg); - } - - self.events.emit('rendered', { - chart: data - }); - - return svg; - }); - }; - }; - } - - return AreaChart; -}; diff --git a/src/ui/public/vislib/visualizations/column_chart.js b/src/ui/public/vislib/visualizations/column_chart.js deleted file mode 100644 index c587025f85564..0000000000000 --- a/src/ui/public/vislib/visualizations/column_chart.js +++ /dev/null @@ -1,329 +0,0 @@ -import d3 from 'd3'; -import _ from 'lodash'; -import $ from 'jquery'; -import moment from 'moment'; -import errors from 'ui/errors'; -import VislibVisualizationsPointSeriesChartProvider from 'ui/vislib/visualizations/_point_series_chart'; -import VislibVisualizationsTimeMarkerProvider from 'ui/vislib/visualizations/time_marker'; -export default function ColumnChartFactory(Private) { - - const PointSeriesChart = Private(VislibVisualizationsPointSeriesChartProvider); - const TimeMarker = Private(VislibVisualizationsTimeMarkerProvider); - - /** - * Vertical Bar Chart Visualization: renders vertical and/or stacked bars - * - * @class ColumnChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific chart - */ - class ColumnChart extends PointSeriesChart { - constructor(handler, chartEl, chartData) { - super(handler, chartEl, chartData); - - // Column chart specific attributes - this._attr = _.defaults(handler._attr || {}, { - xValue: function (d) { - return d.x; - }, - yValue: function (d) { - return d.y; - } - }); - } - - /** - * Adds SVG rect to Vertical Bar Chart - * - * @method addBars - * @param svg {HTMLElement} SVG to which rect are appended - * @param layers {Array} Chart data array - * @returns {D3.UpdateSelection} SVG with rect added - */ - addBars(svg, layers) { - const self = this; - const color = this.handler.data.getColorFunc(); - const tooltip = this.tooltip; - const isTooltip = this._attr.addTooltip; - - const layer = svg.selectAll('.layer') - .data(layers) - .enter().append('g') - .attr('class', function (d, i) { - return 'series ' + i; - }); - - const bars = layer.selectAll('rect') - .data(function (d) { - return d; - }); - - bars - .exit() - .remove(); - - bars - .enter() - .append('rect') - .call(this._addIdentifier) - .attr('fill', function (d) { - return color(d.label); - }); - - self.updateBars(bars); - - // Add tooltip - if (isTooltip) { - bars.call(tooltip.render()); - } - - return bars; - }; - - /** - * Determines whether bars are grouped or stacked and updates the D3 - * selection - * - * @method updateBars - * @param bars {D3.UpdateSelection} SVG with rect added - * @returns {D3.UpdateSelection} - */ - updateBars(bars) { - const offset = this._attr.mode; - - if (offset === 'grouped') { - return this.addGroupedBars(bars); - } - return this.addStackedBars(bars); - }; - - /** - * Adds stacked bars to column chart visualization - * - * @method addStackedBars - * @param bars {D3.UpdateSelection} SVG with rect added - * @returns {D3.UpdateSelection} - */ - addStackedBars(bars) { - const data = this.chartData; - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; - const height = yScale.range()[0]; - const yMin = this.handler.yAxis.yScale.domain()[0]; - - let barWidth; - if (data.ordered && data.ordered.date) { - const start = data.ordered.min; - const end = moment(data.ordered.min).add(data.ordered.interval).valueOf(); - - barWidth = xScale(end) - xScale(start); - barWidth = barWidth - Math.min(barWidth * 0.25, 15); - } - - // update - bars - .attr('x', function (d) { - return xScale(d.x); - }) - .attr('width', function () { - return barWidth || xScale.rangeBand(); - }) - .attr('y', function (d) { - if (d.y < 0) { - return yScale(d.y0); - } - - return yScale(d.y0 + d.y); - }) - .attr('height', function (d) { - if (d.y < 0) { - return Math.abs(yScale(d.y0 + d.y) - yScale(d.y0)); - } - - // Due to an issue with D3 not returning zeros correctly when using - // an offset='expand', need to add conditional statement to handle zeros - // appropriately - if (d._input.y === 0) { - return 0; - } - - // for split bars or for one series, - // last series will have d.y0 = 0 - if (d.y0 === 0 && yMin > 0) { - return yScale(yMin) - yScale(d.y); - } - - return yScale(d.y0) - yScale(d.y0 + d.y); - }); - - return bars; - }; - - /** - * Adds grouped bars to column chart visualization - * - * @method addGroupedBars - * @param bars {D3.UpdateSelection} SVG with rect added - * @returns {D3.UpdateSelection} - */ - addGroupedBars(bars) { - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; - const data = this.chartData; - const n = data.series.length; - const height = yScale.range()[0]; - const groupSpacingPercentage = 0.15; - const isTimeScale = (data.ordered && data.ordered.date); - const minWidth = 1; - let barWidth; - - // update - bars - .attr('x', function (d, i, j) { - if (isTimeScale) { - const groupWidth = xScale(data.ordered.min + data.ordered.interval) - - xScale(data.ordered.min); - const groupSpacing = groupWidth * groupSpacingPercentage; - - barWidth = (groupWidth - groupSpacing) / n; - - return xScale(d.x) + barWidth * j; - } - return xScale(d.x) + xScale.rangeBand() / n * j; - }) - .attr('width', function () { - if (barWidth < minWidth) { - throw new errors.ContainerTooSmall(); - } - - if (isTimeScale) { - return barWidth; - } - return xScale.rangeBand() / n; - }) - .attr('y', function (d) { - if (d.y < 0) { - return yScale(0); - } - - return yScale(d.y); - }) - .attr('height', function (d) { - return Math.abs(yScale(0) - yScale(d.y)); - }); - - return bars; - }; - - /** - * Adds Events to SVG rect - * Visualization is only brushable when a brush event is added - * If a brush event is added, then a function should be returned. - * - * @method addBarEvents - * @param element {D3.UpdateSelection} target - * @param svg {D3.UpdateSelection} chart SVG - * @returns {D3.Selection} rect with event listeners attached - */ - addBarEvents(element, svg) { - const events = this.events; - const isBrushable = events.isBrushable(); - const brush = isBrushable ? events.addBrushEvent(svg) : undefined; - const hover = events.addHoverEvent(); - const mouseout = events.addMouseoutEvent(); - const click = events.addClickEvent(); - const attachedEvents = element.call(hover).call(mouseout).call(click); - - if (isBrushable) { - attachedEvents.call(brush); - } - - return attachedEvents; - }; - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the vertical bar chart - */ - draw() { - const self = this; - const $elem = $(this.chartEl); - const margin = this._attr.margin; - const elWidth = this._attr.width = $elem.width(); - const elHeight = this._attr.height = $elem.height(); - const yScale = this.handler.yAxis.yScale; - const xScale = this.handler.xAxis.xScale; - const minWidth = 20; - const minHeight = 20; - const addTimeMarker = this._attr.addTimeMarker; - const times = this._attr.times || []; - let timeMarker; - - return function (selection) { - selection.each(function (data) { - const layers = self.stackData(data); - - const width = elWidth; - const height = elHeight - margin.top - margin.bottom; - if (width < minWidth || height < minHeight) { - throw new errors.ContainerTooSmall(); - } - self.validateDataCompliesWithScalingMethod(data); - - if (addTimeMarker) { - timeMarker = new TimeMarker(times, xScale, height); - } - - if ( - data.series.length > 1 && - (self._attr.scale === 'log' || self._attr.scale === 'square root') && - (self._attr.mode === 'stacked' || self._attr.mode === 'percentage') - ) { - throw new errors.StackedBarChartConfig(`Cannot display ${self._attr.mode} bar charts for multiple data series \ - with a ${self._attr.scale} scaling method. Try 'linear' scaling instead.`); - } - - const div = d3.select(this); - - const svg = div.append('svg') - .attr('width', width) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(0,' + margin.top + ')'); - - const bars = self.addBars(svg, layers); - self.createEndZones(svg); - - // Adds event listeners - self.addBarEvents(bars, svg); - - svg.append('line') - .attr('class', 'base-line') - .attr('x1', 0) - .attr('y1', yScale(0)) - .attr('x2', width) - .attr('y2', yScale(0)) - .style('stroke', '#ddd') - .style('stroke-width', 1); - - if (addTimeMarker) { - timeMarker.render(svg); - } - - self.events.emit('rendered', { - chart: data - }); - - return svg; - }); - }; - }; - } - - return ColumnChart; -}; diff --git a/src/ui/public/vislib/visualizations/line_chart.js b/src/ui/public/vislib/visualizations/line_chart.js deleted file mode 100644 index d1721c59dc63b..0000000000000 --- a/src/ui/public/vislib/visualizations/line_chart.js +++ /dev/null @@ -1,353 +0,0 @@ -import d3 from 'd3'; -import _ from 'lodash'; -import $ from 'jquery'; -import errors from 'ui/errors'; -import VislibVisualizationsPointSeriesChartProvider from 'ui/vislib/visualizations/_point_series_chart'; -import VislibVisualizationsTimeMarkerProvider from 'ui/vislib/visualizations/time_marker'; -export default function LineChartFactory(Private) { - - const PointSeriesChart = Private(VislibVisualizationsPointSeriesChartProvider); - const TimeMarker = Private(VislibVisualizationsTimeMarkerProvider); - - /** - * Line Chart Visualization - * - * @class LineChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific chart - */ - class LineChart extends PointSeriesChart { - constructor(handler, chartEl, chartData) { - super(handler, chartEl, chartData); - - // Line chart specific attributes - this._attr = _.defaults(handler._attr || {}, { - interpolate: 'linear', - xValue: function (d) { - return d.x; - }, - yValue: function (d) { - return d.y; - } - }); - } - - /** - * Adds Events to SVG circle - * - * @method addCircleEvents - * @param element{D3.UpdateSelection} Reference to SVG circle - * @returns {D3.Selection} SVG circles with event listeners attached - */ - addCircleEvents(element, svg) { - const events = this.events; - const isBrushable = events.isBrushable(); - const brush = isBrushable ? events.addBrushEvent(svg) : undefined; - const hover = events.addHoverEvent(); - const mouseout = events.addMouseoutEvent(); - const click = events.addClickEvent(); - const attachedEvents = element.call(hover).call(mouseout).call(click); - - if (isBrushable) { - attachedEvents.call(brush); - } - - return attachedEvents; - }; - - /** - * Adds circles to SVG - * - * @method addCircles - * @param svg {HTMLElement} SVG to which rect are appended - * @param data {Array} Array of object data points - * @returns {D3.UpdateSelection} SVG with circles added - */ - addCircles(svg, data) { - const self = this; - const showCircles = this._attr.showCircles; - const color = this.handler.data.getColorFunc(); - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; - const ordered = this.handler.data.get('ordered'); - const tooltip = this.tooltip; - const isTooltip = this._attr.addTooltip; - - const radii = _(data) - .map(function (series) { - return _.pluck(series, '_input.z'); - }) - .flattenDeep() - .reduce(function (result, val) { - if (result.min > val) result.min = val; - if (result.max < val) result.max = val; - return result; - }, { - min: Infinity, - max: -Infinity - }); - - const radiusStep = ((radii.max - radii.min) || (radii.max * 100)) / Math.pow(this._attr.radiusRatio, 2); - - const layer = svg.selectAll('.points') - .data(data) - .enter() - .append('g') - .attr('class', 'points line'); - - const circles = layer - .selectAll('circle') - .data(function appendData(data) { - return data.filter(function (d) { - return !_.isNull(d.y); - }); - }); - - circles - .exit() - .remove(); - - function cx(d) { - if (ordered && ordered.date) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - } - - function cy(d) { - return yScale(d.y); - } - - function cColor(d) { - return color(d.label); - } - - function colorCircle(d) { - const parent = d3.select(this).node().parentNode; - const lengthOfParent = d3.select(parent).data()[0].length; - const isVisible = (lengthOfParent === 1); - - // If only 1 point exists, show circle - if (!showCircles && !isVisible) return 'none'; - return cColor(d); - } - - function getCircleRadiusFn(modifier) { - return function getCircleRadius(d) { - const margin = self._attr.margin; - const width = self._attr.width - margin.left - margin.right; - const height = self._attr.height - margin.top - margin.bottom; - const circleRadius = (d._input.z - radii.min) / radiusStep; - - return _.min([Math.sqrt((circleRadius || 2) + 2), width, height]) + (modifier || 0); - }; - } - - - circles - .enter() - .append('circle') - .attr('r', getCircleRadiusFn()) - .attr('fill-opacity', (this._attr.drawLinesBetweenPoints ? 1 : 0.7)) - .attr('cx', cx) - .attr('cy', cy) - .attr('class', 'circle-decoration') - .call(this._addIdentifier) - .attr('fill', colorCircle); - - circles - .enter() - .append('circle') - .attr('r', getCircleRadiusFn(10)) - .attr('cx', cx) - .attr('cy', cy) - .attr('fill', 'transparent') - .attr('class', 'circle') - .call(this._addIdentifier) - .attr('stroke', cColor) - .attr('stroke-width', 0); - - if (isTooltip) { - circles.call(tooltip.render()); - } - - return circles; - }; - - /** - * Adds path to SVG - * - * @method addLines - * @param svg {HTMLElement} SVG to which path are appended - * @param data {Array} Array of object data points - * @returns {D3.UpdateSelection} SVG with paths added - */ - addLines(svg, data) { - const xScale = this.handler.xAxis.xScale; - const yScale = this.handler.yAxis.yScale; - const xAxisFormatter = this.handler.data.get('xAxisFormatter'); - const color = this.handler.data.getColorFunc(); - const ordered = this.handler.data.get('ordered'); - const interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate; - const line = d3.svg.line() - .defined(function (d) { - return !_.isNull(d.y); - }) - .interpolate(interpolate) - .x(function x(d) { - if (ordered && ordered.date) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - }) - .y(function y(d) { - return yScale(d.y); - }); - - const lines = svg - .selectAll('.lines') - .data(data) - .enter() - .append('g') - .attr('class', 'pathgroup lines'); - - lines.append('path') - .call(this._addIdentifier) - .attr('d', function lineD(d) { - return line(d.values); - }) - .attr('fill', 'none') - .attr('stroke', function lineStroke(d) { - return color(d.label); - }) - .attr('stroke-width', 2); - - return lines; - }; - - /** - * Adds SVG clipPath - * - * @method addClipPath - * @param svg {HTMLElement} SVG to which clipPath is appended - * @param width {Number} SVG width - * @param height {Number} SVG height - * @returns {D3.UpdateSelection} SVG with clipPath added - */ - addClipPath(svg, width, height) { - const clipPathBuffer = 5; - const startX = 0; - const startY = 0 - clipPathBuffer; - const id = 'chart-area' + _.uniqueId(); - - return svg - .attr('clip-path', 'url(#' + id + ')') - .append('clipPath') - .attr('id', id) - .append('rect') - .attr('x', startX) - .attr('y', startY) - .attr('width', width) - // Adding clipPathBuffer to height so it doesn't - // cutoff the lower part of the chart - .attr('height', height + clipPathBuffer); - }; - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the line chart - */ - draw() { - const self = this; - const $elem = $(this.chartEl); - const margin = this._attr.margin; - const elWidth = this._attr.width = $elem.width(); - const elHeight = this._attr.height = $elem.height(); - const scaleType = this.handler.yAxis.getScaleType(); - const yScale = this.handler.yAxis.yScale; - const xScale = this.handler.xAxis.xScale; - const minWidth = 20; - const minHeight = 20; - const startLineX = 0; - const lineStrokeWidth = 1; - const addTimeMarker = this._attr.addTimeMarker; - const times = this._attr.times || []; - let timeMarker; - - return function (selection) { - selection.each(function (data) { - const el = this; - - const layers = data.series.map(function mapSeries(d) { - const label = d.label; - return d.values.map(function mapValues(e, i) { - return { - _input: e, - label: label, - x: self._attr.xValue.call(d.values, e, i), - y: self._attr.yValue.call(d.values, e, i) - }; - }); - }); - - const width = elWidth - margin.left - margin.right; - const height = elHeight - margin.top - margin.bottom; - if (width < minWidth || height < minHeight) { - throw new errors.ContainerTooSmall(); - } - self.validateDataCompliesWithScalingMethod(data); - - if (addTimeMarker) { - timeMarker = new TimeMarker(times, xScale, height); - } - - - const div = d3.select(el); - - const svg = div.append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - - self.addClipPath(svg, width, height); - if (self._attr.drawLinesBetweenPoints) { - self.addLines(svg, data.series); - } - const circles = self.addCircles(svg, layers); - self.addCircleEvents(circles, svg); - self.createEndZones(svg); - - const scale = (scaleType === 'log') ? yScale(1) : yScale(0); - if (scale) { - svg.append('line') - .attr('class', 'base-line') - .attr('x1', startLineX) - .attr('y1', scale) - .attr('x2', width) - .attr('y2', scale) - .style('stroke', '#ddd') - .style('stroke-width', lineStrokeWidth); - } - - if (addTimeMarker) { - timeMarker.render(svg); - } - - self.events.emit('rendered', { - chart: data - }); - - return svg; - }); - }; - }; - } - - return LineChart; -}; diff --git a/src/ui/public/vislib/visualizations/marker_types/geohash_grid.js b/src/ui/public/vislib/visualizations/marker_types/geohash_grid.js index 824160fa78471..f377deaa500b2 100644 --- a/src/ui/public/vislib/visualizations/marker_types/geohash_grid.js +++ b/src/ui/public/vislib/visualizations/marker_types/geohash_grid.js @@ -1,5 +1,5 @@ import L from 'leaflet'; -import VislibVisualizationsMarkerTypesBaseMarkerProvider from 'ui/vislib/visualizations/marker_types/base_marker'; +import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker'; export default function GeohashGridMarkerFactory(Private) { const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider); diff --git a/src/ui/public/vislib/visualizations/marker_types/heatmap.js b/src/ui/public/vislib/visualizations/marker_types/heatmap.js index d5878dec75d7a..c630898210c2f 100644 --- a/src/ui/public/vislib/visualizations/marker_types/heatmap.js +++ b/src/ui/public/vislib/visualizations/marker_types/heatmap.js @@ -1,7 +1,7 @@ import d3 from 'd3'; import _ from 'lodash'; import L from 'leaflet'; -import VislibVisualizationsMarkerTypesBaseMarkerProvider from 'ui/vislib/visualizations/marker_types/base_marker'; +import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker'; export default function HeatmapMarkerFactory(Private) { const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider); diff --git a/src/ui/public/vislib/visualizations/marker_types/scaled_circles.js b/src/ui/public/vislib/visualizations/marker_types/scaled_circles.js index 9e6afcbd49ac8..7a368ce50e61b 100644 --- a/src/ui/public/vislib/visualizations/marker_types/scaled_circles.js +++ b/src/ui/public/vislib/visualizations/marker_types/scaled_circles.js @@ -1,6 +1,6 @@ import _ from 'lodash'; import L from 'leaflet'; -import VislibVisualizationsMarkerTypesBaseMarkerProvider from 'ui/vislib/visualizations/marker_types/base_marker'; +import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker'; export default function ScaledCircleMarkerFactory(Private) { const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider); diff --git a/src/ui/public/vislib/visualizations/marker_types/shaded_circles.js b/src/ui/public/vislib/visualizations/marker_types/shaded_circles.js index 2d31cdc6585d0..dc26c203d1831 100644 --- a/src/ui/public/vislib/visualizations/marker_types/shaded_circles.js +++ b/src/ui/public/vislib/visualizations/marker_types/shaded_circles.js @@ -1,6 +1,6 @@ import _ from 'lodash'; import L from 'leaflet'; -import VislibVisualizationsMarkerTypesBaseMarkerProvider from 'ui/vislib/visualizations/marker_types/base_marker'; +import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker'; export default function ShadedCircleMarkerFactory(Private) { const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider); diff --git a/src/ui/public/vislib/visualizations/pie_chart.js b/src/ui/public/vislib/visualizations/pie_chart.js index fd2ccb8a9c841..2e5e2342e98d6 100644 --- a/src/ui/public/vislib/visualizations/pie_chart.js +++ b/src/ui/public/vislib/visualizations/pie_chart.js @@ -2,11 +2,19 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; import errors from 'ui/errors'; -import VislibVisualizationsChartProvider from 'ui/vislib/visualizations/_chart'; +import VislibVisualizationsChartProvider from './_chart'; export default function PieChartFactory(Private) { const Chart = Private(VislibVisualizationsChartProvider); + const defaults = { + isDonut: false, + showTooltip: true, + color: undefined, + fillColor: undefined, + xValue: d => d.x, + yValue: d => d.y + }; /** * Pie Chart Visualization * @@ -24,11 +32,10 @@ export default function PieChartFactory(Private) { const charts = this.handler.data.getVisData(); this._validatePieData(charts); - this._attr = _.defaults(handler._attr || {}, { - isDonut: handler._attr.isDonut || false - }); + this._attr = _.defaults(handler.visConfig.get('chart', {}), defaults); } + /** * Checks whether pie slices have all zero values. * If so, an error is thrown. diff --git a/src/ui/public/vislib/visualizations/point_series.js b/src/ui/public/vislib/visualizations/point_series.js new file mode 100644 index 0000000000000..f682e222141fe --- /dev/null +++ b/src/ui/public/vislib/visualizations/point_series.js @@ -0,0 +1,248 @@ +import d3 from 'd3'; +import _ from 'lodash'; +import $ from 'jquery'; +import errors from 'ui/errors'; +import TooltipProvider from '../components/tooltip'; +import VislibVisualizationsChartProvider from './_chart'; +import VislibVisualizationsTimeMarkerProvider from './time_marker'; +import VislibVisualizationsSeriTypesProvider from './point_series/series_types'; + +export default function PointSeriesFactory(Private) { + + const Chart = Private(VislibVisualizationsChartProvider); + const Tooltip = Private(TooltipProvider); + const TimeMarker = Private(VislibVisualizationsTimeMarkerProvider); + const seriTypes = Private(VislibVisualizationsSeriTypesProvider); + const touchdownTmpl = _.template(require('../partials/touchdown.tmpl.html')); + /** + * Line Chart Visualization + * + * @class PointSeries + * @constructor + * @extends Chart + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific chart + */ + class PointSeries extends Chart { + constructor(handler, chartEl, chartData) { + super(handler, chartEl, chartData); + + this.handler = handler; + this.chartData = chartData; + this.chartEl = chartEl; + this.chartConfig = this.findChartConfig(); + this.handler.pointSeries = this; + } + + findChartConfig() { + const charts = this.handler.visConfig.get('charts'); + const chartIndex = this.handler.data.chartData().indexOf(this.chartData); + return charts[chartIndex]; + } + + addBackground(svg, width, height) { + const startX = 0; + const startY = 0; + + return svg + .append('rect') + .attr('x', startX) + .attr('y', startY) + .attr('width', width) + .attr('height', height) + .attr('fill', 'transparent') + .attr('class', 'background'); + }; + + addClipPath(svg) { + const {width, height} = svg.node().getBBox(); + const startX = 0; + const startY = 0; + this.clipPathId = 'chart-area' + _.uniqueId(); + + // Creating clipPath + return svg + .append('clipPath') + .attr('id', this.clipPathId) + .append('rect') + .attr('x', startX) + .attr('y', startY) + .attr('width', width) + .attr('height', height); + }; + + addEvents(svg) { + const isBrushable = this.events.isBrushable(); + if (isBrushable) { + const brush = this.events.addBrushEvent(svg); + return svg.call(brush); + } + }; + + createEndZones(svg) { + const self = this; + const xAxis = this.handler.categoryAxes[0]; + const xScale = xAxis.getScale(); + const ordered = xAxis.ordered; + const isHorizontal = xAxis.axisConfig.isHorizontal(); + const missingMinMax = !ordered || _.isUndefined(ordered.min) || _.isUndefined(ordered.max); + + if (missingMinMax || ordered.endzones === false) return; + + const {width, height} = svg.node().getBBox(); + + // we don't want to draw endzones over our min and max values, they + // are still a part of the dataset. We want to start the endzones just + // outside of them so we will use these values rather than ordered.min/max + const oneUnit = (ordered.units || _.identity)(1); + + // points on this axis represent the amount of time they cover, + // so draw the endzones at the actual time bounds + const leftEndzone = { + x: isHorizontal ? 0 : Math.max(xScale(ordered.min), 0), + w: isHorizontal ? Math.max(xScale(ordered.min), 0) : height - Math.max(xScale(ordered.min), 0) + }; + + const expandLastBucket = xAxis.axisConfig.get('scale.expandLastBucket'); + const rightLastVal = expandLastBucket ? ordered.max : Math.min(ordered.max, _.last(xAxis.values)); + const rightStart = rightLastVal + oneUnit; + const rightEndzone = { + x: isHorizontal ? xScale(rightStart) : 0, + w: isHorizontal ? Math.max(width - xScale(rightStart), 0) : xScale(rightStart) + }; + + this.endzones = svg.selectAll('.layer') + .data([leftEndzone, rightEndzone]) + .enter() + .insert('g', '.brush') + .attr('class', 'endzone') + .append('rect') + .attr('class', 'zone') + .attr('x', function (d) { + return isHorizontal ? d.x : 0; + }) + .attr('y', function (d) { + return isHorizontal ? 0 : d.x; + }) + .attr('height', function (d) { + return isHorizontal ? height : d.w; + }) + .attr('width', function (d) { + return isHorizontal ? d.w : width; + }); + + function callPlay(event) { + const boundData = event.target.__data__; + const mouseChartXCoord = event.clientX - self.chartEl.getBoundingClientRect().left; + const mouseChartYCoord = event.clientY - self.chartEl.getBoundingClientRect().top; + const wholeBucket = boundData && boundData.x != null; + + // the min and max that the endzones start in + const min = isHorizontal ? leftEndzone.w : rightEndzone.w; + const max = isHorizontal ? rightEndzone.x : leftEndzone.x; + + // bounds of the cursor to consider + let xLeft = isHorizontal ? mouseChartXCoord : mouseChartYCoord; + let xRight = isHorizontal ? mouseChartXCoord : mouseChartYCoord; + if (wholeBucket) { + xLeft = xScale(boundData.x); + xRight = xScale(xAxis.addInterval(boundData.x)); + } + + return { + wholeBucket: wholeBucket, + touchdown: min > xLeft || max < xRight + }; + } + + function textFormatter() { + return touchdownTmpl(callPlay(d3.event)); + } + + const endzoneTT = new Tooltip('endzones', this.handler.el, textFormatter, null); + this.tooltips.push(endzoneTT); + endzoneTT.order = 0; + endzoneTT.showCondition = function inEndzone() { + return callPlay(d3.event).touchdown; + }; + endzoneTT.render()(svg); + }; + + calculateRadiusLimits(data) { + this.radii = _(data.series) + .map(function (series) { + return _.map(series.values, 'z'); + }) + .flattenDeep() + .reduce(function (result, val) { + if (result.min > val) result.min = val; + if (result.max < val) result.max = val; + return result; + }, { + min: Infinity, + max: -Infinity + }); + } + + draw() { + let self = this; + let $elem = $(this.chartEl); + let margin = this.handler.visConfig.get('style.margin'); + const width = this.chartConfig.width = $elem.width(); + const height = this.chartConfig.height = $elem.height(); + let xScale = this.handler.categoryAxes[0].getScale(); + let minWidth = 50; + let minHeight = 50; + let addTimeMarker = this.chartConfig.addTimeMarker; + let times = this.chartConfig.times || []; + let timeMarker; + let div; + let svg; + + return function (selection) { + selection.each(function (data) { + const el = this; + + if (width < minWidth || height < minHeight) { + throw new errors.ContainerTooSmall(); + } + + if (addTimeMarker) { + timeMarker = new TimeMarker(times, xScale, height); + } + + div = d3.select(el); + + svg = div.append('svg') + .attr('width', width) + .attr('height', height); + + self.addBackground(svg, width, height); + self.addClipPath(svg); + self.addEvents(svg); + self.createEndZones(svg); + self.calculateRadiusLimits(data); + + self.series = []; + _.each(self.chartConfig.series, (seriArgs, i) => { + if (!seriArgs.show) return; + const SeriClass = seriTypes[seriArgs.type || self.handler.visConfig.get('chart.type')]; + const series = new SeriClass(self.handler, svg, data.series[i], seriArgs); + series.events = self.events; + svg.call(series.draw()); + self.series.push(series); + }); + + if (addTimeMarker) { + timeMarker.render(svg); + } + + return svg; + }); + }; + }; + } + + return PointSeries; +}; diff --git a/src/ui/public/vislib/visualizations/point_series/_point_series.js b/src/ui/public/vislib/visualizations/point_series/_point_series.js new file mode 100644 index 0000000000000..a15b35eadc844 --- /dev/null +++ b/src/ui/public/vislib/visualizations/point_series/_point_series.js @@ -0,0 +1,102 @@ +import _ from 'lodash'; +import errors from 'ui/errors'; + +export default function PointSeriesProvider(Private) { + + class PointSeries { + constructor(handler, seriesEl, seriesData, seriesConfig) { + this.handler = handler; + this.baseChart = handler.pointSeries; + this.chartEl = seriesEl; + this.chartData = seriesData; + this.seriesConfig = seriesConfig; + + this.validateDataCompliesWithScalingMethod(this.chartData); + } + + validateDataCompliesWithScalingMethod(data) { + const invalidLogScale = data.values && data.values.some(d => d.y < 1); + if (this.getValueAxis().axisConfig.isLogScale() && invalidLogScale) { + throw new errors.InvalidLogScaleValues(); + } + }; + + getStackedCount() { + return this.baseChart.chartConfig.series.reduce(function (sum, series) { + return series.mode === 'stacked' ? sum + 1 : sum; + }, 0); + }; + + getGroupedCount() { + const stacks = []; + return this.baseChart.chartConfig.series.reduce(function (sum, series) { + const valueAxis = series.valueAxis; + const isStacked = series.mode === 'stacked'; + const isHistogram = series.type === 'histogram'; + if (!isHistogram) return sum; + if (isStacked && stacks.includes(valueAxis)) return sum; + if (isStacked) stacks.push(valueAxis); + return sum + 1; + }, 0); + }; + + getStackedNum(data) { + let i = 0; + for (const seri of this.baseChart.chartConfig.series) { + if (seri.data === data) return i; + if (seri.mode === 'stacked') i++; + } + return 0; + }; + + getGroupedNum(data) { + let i = 0; + const stacks = []; + for (const seri of this.baseChart.chartConfig.series) { + const valueAxis = seri.valueAxis; + const isStacked = seri.mode === 'stacked'; + if (!isStacked) { + if (seri.data === data) return i; + i++; + } else { + if (!(valueAxis in stacks)) stacks[valueAxis] = i++; + if (seri.data === data) return stacks[valueAxis]; + } + } + return 0; + }; + + getValueAxis() { + return _.find(this.handler.valueAxes, axis => { + return axis.axisConfig.get('id') === this.seriesConfig.valueAxis; + }) || this.handler.valueAxes[0]; + }; + + getCategoryAxis() { + return _.find(this.handler.categoryAxes, axis => { + return axis.axisConfig.get('id') === this.seriesConfig.categoryAxis; + }) || this.handler.categoryAxes[0]; + }; + + addCircleEvents(element) { + const events = this.events; + const hover = events.addHoverEvent(); + const mouseout = events.addMouseoutEvent(); + const click = events.addClickEvent(); + return element.call(hover).call(mouseout).call(click); + }; + + checkIfEnoughData() { + const message = 'Area charts require more than one data point. Try adding ' + + 'an X-Axis Aggregation'; + + const notEnoughData = this.chartData.values.length < 2; + + if (notEnoughData) { + throw new errors.NotEnoughData(message); + } + }; + } + + return PointSeries; +}; diff --git a/src/ui/public/vislib/visualizations/point_series/area_chart.js b/src/ui/public/vislib/visualizations/point_series/area_chart.js new file mode 100644 index 0000000000000..a7b4a9ec51dac --- /dev/null +++ b/src/ui/public/vislib/visualizations/point_series/area_chart.js @@ -0,0 +1,238 @@ +import d3 from 'd3'; +import _ from 'lodash'; +import $ from 'jquery'; +import VislibVisualizationsPointSeriesProvider from './_point_series'; +export default function AreaChartFactory(Private) { + + const PointSeries = Private(VislibVisualizationsPointSeriesProvider); + + const defaults = { + mode: 'normal', + showCircles: true, + radiusRatio: 9, + showLines: true, + smoothLines: false, + interpolate: 'linear', + color: undefined, + fillColor: undefined, + }; + /** + * Area chart visualization + * + * @class AreaChart + * @constructor + * @extends Chart + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific + * chart + */ + class AreaChart extends PointSeries { + constructor(handler, chartEl, chartData, seriesConfigArgs) { + super(handler, chartEl, chartData, seriesConfigArgs); + + this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); + this.isOverlapping = (this.seriesConfig.mode !== 'stacked'); + if (this.isOverlapping) { + + // Default opacity should return to 0.6 on mouseout + const defaultOpacity = 0.6; + this.seriesConfig.defaultOpacity = defaultOpacity; + handler.highlight = function (element) { + const label = this.getAttribute('data-label'); + if (!label) return; + + const highlightOpacity = 0.8; + const highlightElements = $('[data-label]', element.parentNode).filter( + function (els, el) { + return `${$(el).data('label')}` === label; + }); + $('[data-label]', element.parentNode).not(highlightElements).css('opacity', defaultOpacity / 2); // half of the default opacity + highlightElements.css('opacity', highlightOpacity); + }; + handler.unHighlight = function (element) { + $('[data-label]', element).css('opacity', defaultOpacity); + + //The legend should keep max opacity + $('[data-label]', $(element).siblings()).css('opacity', 1); + }; + } + + this.checkIfEnoughData(); + } + + addPath(svg, data) { + const ordered = this.handler.data.get('ordered'); + const isTimeSeries = (ordered && ordered.date); + const isOverlapping = this.isOverlapping; + const color = this.handler.data.getColorFunc(); + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const interpolate = (this.seriesConfig.smoothLines) ? 'cardinal' : this.seriesConfig.interpolate; + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + + // Data layers + const layer = svg.append('g') + .attr('class', function (d, i) { + return 'series series-' + i; + }); + + // Append path + const path = layer.append('path') + .attr('data-label', data.label) + .style('fill', () => { + return color(data.label); + }) + .classed('overlap_area', function () { + return isOverlapping; + }) + .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); + + function x(d) { + if (isTimeSeries) { + return xScale(d.x); + } + return xScale(d.x) + xScale.rangeBand() / 2; + } + + function y1(d) { + const y0 = d.y0 || 0; + return yScale(y0 + d.y); + } + + function y0(d) { + const y0 = d.y0 || 0; + return yScale(y0); + } + + function getArea() { + if (isHorizontal) { + return d3.svg.area() + .x(x) + .y0(y0) + .y1(y1); + } else { + return d3.svg.area() + .y(x) + .x0(y0) + .x1(y1); + } + } + + // update + path.attr('d', function (d) { + const area = getArea() + .defined(function (d) { + return !_.isNull(d.y); + }) + .interpolate(interpolate); + return area(data.values); + }); + + return path; + }; + + /** + * Adds SVG circles to area chart + * + * @method addCircles + * @param svg {HTMLElement} SVG to which circles are appended + * @param data {Array} Chart data array + * @returns {D3.UpdateSelection} SVG with circles added + */ + addCircles(svg, data) { + const color = this.handler.data.getColorFunc(); + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const ordered = this.handler.data.get('ordered'); + const circleRadius = 12; + const circleStrokeWidth = 0; + const tooltip = this.baseChart.tooltip; + const isTooltip = this.handler.visConfig.get('tooltip.show'); + const isOverlapping = this.isOverlapping; + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + + const layer = svg.append('g') + .attr('class', 'points area') + .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); + + // append the circles + const circles = layer.selectAll('circles') + .data(function appendData() { + return data.values.filter(function isZeroOrNull(d) { + return d.y !== 0 && !_.isNull(d.y); + }); + }); + + // exit + circles.exit().remove(); + + // enter + circles + .enter() + .append('circle') + .attr('data-label', data.label) + .attr('stroke', () => { + return color(data.label); + }) + .attr('fill', 'transparent') + .attr('stroke-width', circleStrokeWidth); + + function cx(d) { + if (ordered && ordered.date) { + return xScale(d.x); + } + return xScale(d.x) + xScale.rangeBand() / 2; + } + + function cy(d) { + if (isOverlapping) { + return yScale(d.y); + } + return yScale(d.y0 + d.y); + } + + // update + circles + .attr('cx', isHorizontal ? cx : cy) + .attr('cy', isHorizontal ? cy : cx) + .attr('r', circleRadius); + + // Add tooltip + if (isTooltip) { + circles.call(tooltip.render()); + } + + return circles; + }; + + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the area chart + */ + draw() { + const self = this; + + return function (selection) { + selection.each(function () { + const svg = self.chartEl.append('g'); + svg.data([self.chartData]); + + self.addPath(svg, self.chartData); + const circles = self.addCircles(svg, self.chartData); + self.addCircleEvents(circles); + + self.events.emit('rendered', { + chart: self.chartData + }); + + return svg; + }); + }; + }; + } + + return AreaChart; +}; diff --git a/src/ui/public/vislib/visualizations/point_series/column_chart.js b/src/ui/public/vislib/visualizations/point_series/column_chart.js new file mode 100644 index 0000000000000..4a81ad874470a --- /dev/null +++ b/src/ui/public/vislib/visualizations/point_series/column_chart.js @@ -0,0 +1,245 @@ +import _ from 'lodash'; +import moment from 'moment'; +import errors from 'ui/errors'; +import VislibVisualizationsPointSeriesProvider from './_point_series'; +export default function ColumnChartFactory(Private) { + + const PointSeries = Private(VislibVisualizationsPointSeriesProvider); + + const defaults = { + mode: 'normal', + showTooltip: true, + color: undefined, + fillColor: undefined + }; + /** + * Vertical Bar Chart Visualization: renders vertical and/or stacked bars + * + * @class ColumnChart + * @constructor + * @extends Chart + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific chart + */ + class ColumnChart extends PointSeries { + constructor(handler, chartEl, chartData, seriesConfigArgs) { + super(handler, chartEl, chartData, seriesConfigArgs); + this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); + } + + addBars(svg, data) { + const self = this; + const color = this.handler.data.getColorFunc(); + const tooltip = this.baseChart.tooltip; + const isTooltip = this.handler.visConfig.get('tooltip.show'); + + const layer = svg.append('g') + .attr('class', 'series') + .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); + + const bars = layer.selectAll('rect') + .data(data.values); + + bars + .exit() + .remove(); + + bars + .enter() + .append('rect') + .attr('data-label', data.label) + .attr('fill', () => { + return color(data.label); + }); + + self.updateBars(bars); + + // Add tooltip + if (isTooltip) { + bars.call(tooltip.render()); + } + + return bars; + }; + + /** + * Determines whether bars are grouped or stacked and updates the D3 + * selection + * + * @method updateBars + * @param bars {D3.UpdateSelection} SVG with rect added + * @returns {D3.UpdateSelection} + */ + updateBars(bars) { + if (this.seriesConfig.mode === 'stacked') { + return this.addStackedBars(bars); + } + return this.addGroupedBars(bars); + + }; + + /** + * Adds stacked bars to column chart visualization + * + * @method addStackedBars + * @param bars {D3.UpdateSelection} SVG with rect added + * @returns {D3.UpdateSelection} + */ + addStackedBars(bars) { + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + const isTimeScale = this.getCategoryAxis().axisConfig.isTimeDomain(); + const height = yScale.range()[0]; + const yMin = yScale.domain()[0]; + const groupSpacingPercentage = 0.15; + const groupCount = this.getGroupedCount(); + const groupNum = this.getGroupedNum(this.chartData); + + let barWidth; + if (isTimeScale) { + const {min, interval} = this.handler.data.get('ordered'); + let groupWidth = xScale(min + interval) - xScale(min); + if (!isHorizontal) groupWidth *= -1; + const groupSpacing = groupWidth * groupSpacingPercentage; + + barWidth = (groupWidth - groupSpacing) / groupCount; + } + + function x(d) { + const groupPosition = isTimeScale ? barWidth * groupNum : xScale.rangeBand() / groupCount * groupNum; + return xScale(d.x) + groupPosition; + } + + function y(d) { + if ((isHorizontal && d.y < 0) || (!isHorizontal && d.y > 0)) { + return yScale(d.y0); + } + /*if (!isHorizontal && d.y < 0) return yScale(d.y);*/ + return yScale(d.y0 + d.y); + } + + function widthFunc() { + return isTimeScale ? barWidth : xScale.rangeBand() / groupCount; + } + + function heightFunc(d) { + // for split bars or for one series, + // last series will have d.y0 = 0 + if (d.y0 === 0 && yMin > 0) { + return yScale(yMin) - yScale(d.y); + } + + return Math.abs(yScale(d.y0) - yScale(d.y0 + d.y)); + } + + // update + bars + .attr('x', isHorizontal ? x : y) + .attr('width', isHorizontal ? widthFunc : heightFunc) + .attr('y', isHorizontal ? y : x) + .attr('height', isHorizontal ? heightFunc : widthFunc); + + return bars; + }; + + /** + * Adds grouped bars to column chart visualization + * + * @method addGroupedBars + * @param bars {D3.UpdateSelection} SVG with rect added + * @returns {D3.UpdateSelection} + */ + addGroupedBars(bars) { + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const groupCount = this.getGroupedCount(); + const groupNum = this.getGroupedNum(this.chartData); + const height = yScale.range()[0]; + const groupSpacingPercentage = 0.15; + const isTimeScale = this.getCategoryAxis().axisConfig.isTimeDomain(); + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + const isLogScale = this.getValueAxis().axisConfig.isLogScale(); + const minWidth = 1; + let barWidth; + + if (isTimeScale) { + const {min, interval} = this.handler.data.get('ordered'); + let groupWidth = xScale(min + interval) - xScale(min); + if (!isHorizontal) groupWidth *= -1; + const groupSpacing = groupWidth * groupSpacingPercentage; + + barWidth = (groupWidth - groupSpacing) / groupCount; + } + + function x(d) { + if (isTimeScale) { + return xScale(d.x) + barWidth * groupNum; + } + return xScale(d.x) + xScale.rangeBand() / groupCount * groupNum; + } + + function y(d) { + if ((isHorizontal && d.y < 0) || (!isHorizontal && d.y > 0)) { + return yScale(0); + } + + return yScale(d.y); + } + + function widthFunc() { + if (barWidth < minWidth) { + throw new errors.ContainerTooSmall(); + } + + if (isTimeScale) { + return barWidth; + } + return xScale.rangeBand() / groupCount; + } + + function heightFunc(d) { + const baseValue = isLogScale ? 1 : 0; + return Math.abs(yScale(baseValue) - yScale(d.y)); + } + + // update + bars + .attr('x', isHorizontal ? x : y) + .attr('width', isHorizontal ? widthFunc : heightFunc) + .attr('y', isHorizontal ? y : x) + .attr('height', isHorizontal ? heightFunc : widthFunc); + + return bars; + }; + + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the vertical bar chart + */ + draw() { + const self = this; + + return function (selection) { + selection.each(function () { + const svg = self.chartEl.append('g'); + svg.data([self.chartData]); + + const bars = self.addBars(svg, self.chartData); + self.addCircleEvents(bars); + + self.events.emit('rendered', { + chart: self.chartData + }); + + return svg; + }); + }; + }; + } + + return ColumnChart; +}; diff --git a/src/ui/public/vislib/visualizations/point_series/line_chart.js b/src/ui/public/vislib/visualizations/point_series/line_chart.js new file mode 100644 index 0000000000000..3005d58be6d98 --- /dev/null +++ b/src/ui/public/vislib/visualizations/point_series/line_chart.js @@ -0,0 +1,216 @@ +import d3 from 'd3'; +import _ from 'lodash'; +import VislibVisualizationsPointSeriesProvider from './_point_series'; +export default function LineChartFactory(Private) { + + const PointSeries = Private(VislibVisualizationsPointSeriesProvider); + + const defaults = { + mode: 'normal', + showCircles: true, + radiusRatio: 9, + showLines: true, + smoothLines: false, + interpolate: 'linear', + color: undefined, + fillColor: undefined + }; + /** + * Line Chart Visualization + * + * @class LineChart + * @constructor + * @extends Chart + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific chart + */ + class LineChart extends PointSeries { + constructor(handler, chartEl, chartData, seriesConfigArgs) { + super(handler, chartEl, chartData, seriesConfigArgs); + this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); + } + + addCircles(svg, data) { + const self = this; + const showCircles = this.seriesConfig.showCircles; + const color = this.handler.data.getColorFunc(); + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const ordered = this.handler.data.get('ordered'); + const tooltip = this.baseChart.tooltip; + const isTooltip = this.handler.visConfig.get('tooltip.show'); + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + + const radii = this.baseChart.radii; + + const radiusStep = ((radii.max - radii.min) || (radii.max * 100)) / Math.pow(this.seriesConfig.radiusRatio, 2); + + const layer = svg.append('g') + .attr('class', 'points line') + .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); + + const circles = layer + .selectAll('circle') + .data(function appendData() { + return data.values.filter(function (d) { + return !_.isNull(d.y); + }); + }); + + circles + .exit() + .remove(); + + function cx(d) { + if (ordered && ordered.date) { + return xScale(d.x); + } + return xScale(d.x) + xScale.rangeBand() / 2; + } + + function cy(d) { + return yScale(d.y); + } + + function cColor(d) { + return color(d.series); + } + + function colorCircle(d) { + const parent = d3.select(this).node().parentNode; + const lengthOfParent = d3.select(parent).data()[0].length; + const isVisible = (lengthOfParent === 1); + + // If only 1 point exists, show circle + if (!showCircles && !isVisible) return 'none'; + return cColor(d); + } + + function getCircleRadiusFn(modifier) { + return function getCircleRadius(d) { + const margin = self.handler.visConfig.get('style.margin'); + const width = self.baseChart.chartConfig.width; + const height = self.baseChart.chartConfig.height; + const circleRadius = (d.z - radii.min) / radiusStep; + + return _.min([Math.sqrt((circleRadius || 2) + 2), width, height]) + (modifier || 0); + }; + } + + circles + .enter() + .append('circle') + .attr('r', getCircleRadiusFn()) + .attr('fill-opacity', (this.seriesConfig.drawLinesBetweenPoints ? 1 : 0.7)) + .attr('cx', isHorizontal ? cx : cy) + .attr('cy', isHorizontal ? cy : cx) + .attr('class', 'circle-decoration') + .attr('data-label', data.label) + .attr('fill', colorCircle); + + circles + .enter() + .append('circle') + .attr('r', getCircleRadiusFn(10)) + .attr('cx', isHorizontal ? cx : cy) + .attr('cy', isHorizontal ? cy : cx) + .attr('fill', 'transparent') + .attr('class', 'circle') + .attr('data-label', data.label) + .attr('stroke', cColor) + .attr('stroke-width', 0); + + if (isTooltip) { + circles.call(tooltip.render()); + } + + return circles; + }; + + /** + * Adds path to SVG + * + * @method addLines + * @param svg {HTMLElement} SVG to which path are appended + * @param data {Array} Array of object data points + * @returns {D3.UpdateSelection} SVG with paths added + */ + addLine(svg, data) { + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.getValueAxis().getScale(); + const xAxisFormatter = this.handler.data.get('xAxisFormatter'); + const color = this.handler.data.getColorFunc(); + const ordered = this.handler.data.get('ordered'); + const interpolate = (this.seriesConfig.smoothLines) ? 'cardinal' : this.seriesConfig.interpolate; + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + + const line = svg.append('g') + .attr('class', 'pathgroup lines') + .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); + + function cx(d) { + if (ordered && ordered.date) { + return xScale(d.x); + } + return xScale(d.x) + xScale.rangeBand() / 2; + } + + function cy(d) { + return yScale(d.y); + } + + line.append('path') + .attr('data-label', data.label) + .attr('d', () => { + const d3Line = d3.svg.line() + .defined(function (d) { + return !_.isNull(d.y); + }) + .interpolate(interpolate) + .x(isHorizontal ? cx : cy) + .y(isHorizontal ? cy : cx); + return d3Line(data.values); + }) + .attr('fill', 'none') + .attr('stroke', () => { + return color(data.label); + }) + .attr('stroke-width', 2); + + return line; + }; + + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the line chart + */ + draw() { + const self = this; + + return function (selection) { + selection.each(function () { + + const svg = self.chartEl.append('g'); + svg.data([self.chartData]); + + if (self.seriesConfig.drawLinesBetweenPoints) { + self.addLine(svg, self.chartData); + } + const circles = self.addCircles(svg, self.chartData); + self.addCircleEvents(circles); + + self.events.emit('rendered', { + chart: self.chartData + }); + + return svg; + }); + }; + }; + } + + return LineChart; +}; diff --git a/src/ui/public/vislib/visualizations/point_series/series_types.js b/src/ui/public/vislib/visualizations/point_series/series_types.js new file mode 100644 index 0000000000000..ba21c8a89ad90 --- /dev/null +++ b/src/ui/public/vislib/visualizations/point_series/series_types.js @@ -0,0 +1,12 @@ +import VislibVisualizationsColumnChartProvider from './column_chart'; +import VislibVisualizationsLineChartProvider from './line_chart'; +import VislibVisualizationsAreaChartProvider from './area_chart'; + +export default function SeriesTypeFactory(Private) { + + return { + histogram: Private(VislibVisualizationsColumnChartProvider), + line: Private(VislibVisualizationsLineChartProvider), + area: Private(VislibVisualizationsAreaChartProvider) + }; +}; diff --git a/src/ui/public/vislib/visualizations/tile_map.js b/src/ui/public/vislib/visualizations/tile_map.js index 3978cfdaa65e0..4f0dc1b13fe9e 100644 --- a/src/ui/public/vislib/visualizations/tile_map.js +++ b/src/ui/public/vislib/visualizations/tile_map.js @@ -1,8 +1,8 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; -import VislibVisualizationsChartProvider from 'ui/vislib/visualizations/_chart'; -import VislibVisualizationsMapProvider from 'ui/vislib/visualizations/_map'; +import VislibVisualizationsChartProvider from './_chart'; +import VislibVisualizationsMapProvider from './_map'; export default function TileMapFactory(Private) { const Chart = Private(VislibVisualizationsChartProvider); @@ -106,10 +106,10 @@ export default function TileMapFactory(Private) { center: params.mapCenter, zoom: params.mapZoom, events: this.events, - markerType: this._attr.mapType, + markerType: this.handler.visConfig.get('mapType'), tooltipFormatter: this.tooltipFormatter, valueFormatter: this.valueFormatter, - attr: this._attr + attr: this.handler.visConfig._values }); // add title for splits diff --git a/src/ui/public/vislib/visualizations/vis_types.js b/src/ui/public/vislib/visualizations/vis_types.js index 0cb1a8dcf0201..b8c2244f6a336 100644 --- a/src/ui/public/vislib/visualizations/vis_types.js +++ b/src/ui/public/vislib/visualizations/vis_types.js @@ -1,8 +1,6 @@ -import VislibVisualizationsColumnChartProvider from 'ui/vislib/visualizations/column_chart'; -import VislibVisualizationsPieChartProvider from 'ui/vislib/visualizations/pie_chart'; -import VislibVisualizationsLineChartProvider from 'ui/vislib/visualizations/line_chart'; -import VislibVisualizationsAreaChartProvider from 'ui/vislib/visualizations/area_chart'; -import VislibVisualizationsTileMapProvider from 'ui/vislib/visualizations/tile_map'; +import VislibVisualizationsPointSeriesProvider from './point_series'; +import VislibVisualizationsPieChartProvider from './pie_chart'; +import VislibVisualizationsTileMapProvider from './tile_map'; export default function VisTypeFactory(Private) { @@ -15,10 +13,8 @@ export default function VisTypeFactory(Private) { * @return {Function} Returns an Object of Visualization classes */ return { - histogram: Private(VislibVisualizationsColumnChartProvider), pie: Private(VislibVisualizationsPieChartProvider), - line: Private(VislibVisualizationsLineChartProvider), - area: Private(VislibVisualizationsAreaChartProvider), - tile_map: Private(VislibVisualizationsTileMapProvider) + tile_map: Private(VislibVisualizationsTileMapProvider), + point_series: Private(VislibVisualizationsPointSeriesProvider) }; }; diff --git a/src/ui/public/vislib_vis_type/__tests__/_vislib_renderbot.js b/src/ui/public/vislib_vis_type/__tests__/_vislib_renderbot.js index 654bb5f01d5f9..5cd644a5f78e5 100644 --- a/src/ui/public/vislib_vis_type/__tests__/_vislib_renderbot.js +++ b/src/ui/public/vislib_vis_type/__tests__/_vislib_renderbot.js @@ -84,7 +84,8 @@ describe('renderbot', function exportWrapper() { }); describe('param update', function () { - let params = { one: 'fish', two: 'fish' }; + let $el = $('
testing
'); + let params = { el: $el[0], one: 'fish', two: 'fish' }; let vis = { type: _.defaults({ params: { @@ -92,15 +93,11 @@ describe('renderbot', function exportWrapper() { } }, mockVisType) }; - let $el = $('
testing
'); let createVisSpy; - let getParamsStub; let renderbot; beforeEach(function () { createVisSpy = sinon.spy(VislibRenderbot.prototype, '_createVis'); - // getParamsStub = sinon.stub(VislibRenderbot.prototype, '_getVislibParams', _identity); - // getParamsStub.returns(params); renderbot = new VislibRenderbot(vis, $el, persistedState); }); diff --git a/src/ui/public/visualize/visualize_legend.js b/src/ui/public/visualize/visualize_legend.js index 76047c1fe26e5..d8b297ef1c2a5 100644 --- a/src/ui/public/visualize/visualize_legend.js +++ b/src/ui/public/visualize/visualize_legend.js @@ -55,7 +55,7 @@ uiModules.get('kibana') }; $scope.toggleLegend = function () { - let bwcAddLegend = $scope.renderbot.vislibVis._attr.addLegend; + let bwcAddLegend = $scope.vis.params.addLegend; let bwcLegendStateDefault = bwcAddLegend == null ? true : bwcAddLegend; $scope.open = !$scope.uiState.get('vis.legendOpen', bwcLegendStateDefault); $scope.uiState.set('vis.legendOpen', $scope.open); @@ -100,11 +100,11 @@ uiModules.get('kibana') function refresh() { let vislibVis = $scope.renderbot.vislibVis; - if ($scope.uiState.get('vis.legendOpen') == null && vislibVis._attr.addLegend != null) { - $scope.open = vislibVis._attr.addLegend; + if ($scope.uiState.get('vis.legendOpen') == null && $scope.vis.params.addLegend != null) { + $scope.open = $scope.vis.params.addLegend; } - $scope.labels = getLabels($scope.data, vislibVis._attr.type); + $scope.labels = getLabels($scope.data, vislibVis.visConfigArgs.type); $scope.getColor = colorPalette(_.pluck($scope.labels, 'label'), $scope.uiState.get('vis.colors')); } diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 26af3c9846764..e1d451b1ba054 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -71,12 +71,8 @@ bdd.describe('discover app', function describeIndexTests() { }); bdd.it('should show the correct bar chart', async function () { - const expectedBarChartData = [ '3.237', - '17.674', '64.75', '125.737', '119.962', '65.712', '16.449', - '2.712', '3.675', '17.674', '59.762', '119.087', '123.812', - '61.862', '15.487', '2.362', '2.800', '15.312', '61.862', '123.2', - '118.562', '63.524', '17.587', '2.537' - ]; + const expectedBarChartData = [ 35, 189, 694, 1347, 1285, 704, 176, 29, 39, 189, 640, + 1276, 1327, 663, 166, 25, 30, 164, 663, 1320, 1270, 681, 188, 27 ]; await verifyChartData(expectedBarChartData); }); @@ -97,27 +93,16 @@ bdd.describe('discover app', function describeIndexTests() { bdd.it('should show correct data for chart interval Hourly', async function () { await PageObjects.discover.setChartInterval('Hourly'); - const expectedBarChartData = [ '1.527', '2.290', - '5.599', '7.890', '13.236', '30.290', '46.072', '55.490', '86.8', - '112', '122.181', '131.6', '132.872', '113.527', '102.581', - '81.709', '65.672', '43.781', '24.181', '14', '9.672', '6.109', - '0.763', '1.018', '2.800', '3.563', '4.327', '9.672', '12.472', - '29.272', '38.690', '54.981', '80.181', '102.327', '113.527', - '130.581', '132.363', '120.654', '107.163', '78.145', '58.545', - '43.272', '25.199', '12.218', '7.636', '3.818', '2.545', '0.509', - '2.036', '1.781', '4.327', '8.654', '9.418', '26.472', '38.945', - '61.345', '79.672', '102.836', '125.236', '130.327', '128.036', - '120.4', '96.472', '74.581', '70.509', '39.709', '25.199', '13.490', - '12.472', '4.072', '2.290', '1.018' - ]; + const expectedBarChartData = [ 4, 7, 16, 23, 38, 87, 132, 159, 248, 320, 349, 376, 380, + 324, 293, 233, 188, 125, 69, 40, 28, 17, 2, 3, 8, 10, 12, 28, 36, 84, 111, 157, 229, 292, + 324, 373, 378, 345, 306, 223, 167, 124, 72, 35, 22, 11, 7, 1, 6, 5, 12, 25, 27, 76, 111, 175, + 228, 294, 358, 372, 366, 344, 276, 213, 201, 113, 72, 39, 36, 12, 7, 3 ]; await verifyChartData(expectedBarChartData); }); bdd.it('should show correct data for chart interval Daily', async function () { const chartInterval = 'Daily'; - const expectedBarChartData = [ - '133.196', '129.192', '129.724' - ]; + const expectedBarChartData = [ 4757, 4614, 4633 ]; await PageObjects.discover.setChartInterval(chartInterval); await PageObjects.common.try(async () => { await verifyChartData(expectedBarChartData); @@ -126,7 +111,7 @@ bdd.describe('discover app', function describeIndexTests() { bdd.it('should show correct data for chart interval Weekly', async function () { const chartInterval = 'Weekly'; - const expectedBarChartData = [ '66.598', '129.458']; + const expectedBarChartData = [ 4757, 9247 ]; await PageObjects.discover.setChartInterval(chartInterval); await PageObjects.common.try(async () => { @@ -136,9 +121,7 @@ bdd.describe('discover app', function describeIndexTests() { bdd.it('browser back button should show previous interval Daily', async function () { const expectedChartInterval = 'Daily'; - const expectedBarChartData = [ - '133.196', '129.192', '129.724' - ]; + const expectedBarChartData = [ 4757, 4614, 4633 ]; await this.remote.goBack(); await PageObjects.common.try(async function tryingForTime() { @@ -150,7 +133,7 @@ bdd.describe('discover app', function describeIndexTests() { bdd.it('should show correct data for chart interval Monthly', async function () { const chartInterval = 'Monthly'; - const expectedBarChartData = [ '122.535']; + const expectedBarChartData = [ 13129 ]; await PageObjects.discover.setChartInterval(chartInterval); await verifyChartData(expectedBarChartData); @@ -158,7 +141,7 @@ bdd.describe('discover app', function describeIndexTests() { bdd.it('should show correct data for chart interval Yearly', async function () { const chartInterval = 'Yearly'; - const expectedBarChartData = [ '122.535']; + const expectedBarChartData = [ 13129 ]; await PageObjects.discover.setChartInterval(chartInterval); await verifyChartData(expectedBarChartData); @@ -166,12 +149,8 @@ bdd.describe('discover app', function describeIndexTests() { bdd.it('should show correct data for chart interval Auto', async function () { const chartInterval = 'Auto'; - const expectedBarChartData = [ '3.237', - '17.674', '64.75', '125.737', '119.962', '65.712', '16.449', - '2.712', '3.675', '17.674', '59.762', '119.087', '123.812', - '61.862', '15.487', '2.362', '2.800', '15.312', '61.862', '123.2', - '118.562', '63.524', '17.587', '2.537' - ]; + const expectedBarChartData = [ 35, 189, 694, 1347, 1285, 704, 176, 29, 39, 189, + 640, 1276, 1327, 663, 166, 25, 30, 164, 663, 1320, 1270, 681, 188, 27 ]; await PageObjects.discover.setChartInterval(chartInterval); await verifyChartData(expectedBarChartData); diff --git a/test/support/page_objects/discover_page.js b/test/support/page_objects/discover_page.js index 647dcbc2b2b0c..f8bb3720189e2 100644 --- a/test/support/page_objects/discover_page.js +++ b/test/support/page_objects/discover_page.js @@ -77,23 +77,59 @@ export default class DiscoverPage { } getBarChartData() { + var self = this; + var yAxisLabel = 0; + var yAxisHeight; + return PageObjects.header.isGlobalLoadingIndicatorHidden() .then(() => { return this.findTimeout - .findAllByCssSelector('rect[data-label="Count"]'); + .findByCssSelector('div.y-axis-div-wrapper > div > svg > g > g:last-of-type'); }) - .then(function (chartData) { - - function getChartData(chart) { - return chart - .getAttribute('height'); - } - - var getChartDataPromises = chartData.map(getChartData); - return Promise.all(getChartDataPromises); + .then(function setYAxisLabel(y) { + return y + .getVisibleText() + .then(function (yLabel) { + yAxisLabel = yLabel.replace(',', ''); + PageObjects.common.debug('yAxisLabel = ' + yAxisLabel); + return yLabel; + }); + }) + // 2). find and save the y-axis pixel size (the chart height) + .then(function getRect() { + return self + .findTimeout + .findByCssSelector('rect.background') + .then(function getRectHeight(chartAreaObj) { + return chartAreaObj + .getAttribute('height') + .then(function (theHeight) { + yAxisHeight = theHeight; // - 5; // MAGIC NUMBER - clipPath extends a bit above the top of the y-axis and below x-axis + PageObjects.common.debug('theHeight = ' + theHeight); + return theHeight; + }); + }); }) - .then(function (bars) { - return bars; + // 3). get the chart-wrapper elements + .then(function () { + return self + .findTimeout + // #kibana-body > div.content > div > div > div > div.vis-editor-canvas > visualize > div.visualize-chart > div > div.vis-col-wrapper > div.chart-wrapper > div > svg > g > g.series.\30 > rect:nth-child(1) + .findAllByCssSelector('svg > g > g.series > rect') // rect + .then(function (chartTypes) { + function getChartType(chart) { + return chart + .getAttribute('height') + .then(function (barHeight) { + return Math.round(barHeight / yAxisHeight * yAxisLabel); + }); + } + var getChartTypesPromises = chartTypes.map(getChartType); + return Promise.all(getChartTypesPromises); + }) + .then(function (bars) { + return bars; + }); }); } diff --git a/test/support/page_objects/visualize_page.js b/test/support/page_objects/visualize_page.js index c048aa9fa95c6..f345f53ea7703 100644 --- a/test/support/page_objects/visualize_page.js +++ b/test/support/page_objects/visualize_page.js @@ -502,7 +502,7 @@ export default class VisualizePage { .findByCssSelector('clipPath rect') .getAttribute('height') .then(function (theHeight) { - yAxisHeight = theHeight - 5; // MAGIC NUMBER - clipPath extends a bit above the top of the y-axis and below x-axis + yAxisHeight = theHeight; PageObjects.common.debug('theHeight = ' + theHeight); return theHeight; }); @@ -581,7 +581,7 @@ export default class VisualizePage { return self .setFindTimeout(defaultFindTimeout * 2) // #kibana-body > div.content > div > div > div > div.vis-editor-canvas > visualize > div.visualize-chart > div > div.vis-col-wrapper > div.chart-wrapper > div > svg > g > g.series.\30 > rect:nth-child(1) - .findAllByCssSelector('svg > g > g.series.\\30 > rect') // rect + .findAllByCssSelector('svg > g > g.series > rect') // rect .then(function (chartTypes) { function getChartType(chart) { return chart From e14c3fcf20d4a64c3e9ca35424cb47b6b29404ef Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 6 Dec 2016 11:00:30 -0200 Subject: [PATCH 6/8] Update connect-to-elasticsearch.asciidoc (#9361) Update documentation to show how to select an index as default. --- docs/setup/connect-to-elasticsearch.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 6ce6f540f91f9..18c22978e890a 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -32,8 +32,8 @@ sophisticated date parsing APIs that Kibana uses to determine date information, specify dates in the index pattern name. + . Click *Create* to add the index pattern. This first pattern is automatically configured as the default. -When you have more than one index pattern, you can designate which one to use as the default from -*Settings > Indices*. +When you have more than one index pattern, you can designate which one to use as the default by clicking +on the star icon above the index pattern title from *Management > Index Patterns*. All done! Kibana is now connected to your Elasticsearch data. Kibana displays a read-only list of fields configured for the matching index. From 7a5f34a044a1139731c39f1fb6d2ace1eb196e4b Mon Sep 17 00:00:00 2001 From: Steven Chen Date: Tue, 6 Dec 2016 21:07:04 +0800 Subject: [PATCH 7/8] Correct typo in 'Sharing a Dashboard' section (#9366) Change 'You can can..' to 'You can either...' --- docs/dashboard.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dashboard.asciidoc b/docs/dashboard.asciidoc index b4311a84af9a7..4b748f4ba5871 100644 --- a/docs/dashboard.asciidoc +++ b/docs/dashboard.asciidoc @@ -133,7 +133,7 @@ to open *Management/Kibana/Saved Objects/Dashboards*. [[sharing-dashboards]] == Sharing a Dashboard -You can can share a direct link to a Kibana dashboard with another user, +You can either share a direct link to a Kibana dashboard with another user, or embed the dashboard in a web page. Users must have Kibana access to view embedded dashboards. From 2ea5915ca378279836c7f50795756bfe1a1138d1 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Tue, 6 Dec 2016 10:36:27 -0500 Subject: [PATCH 8/8] Use this instead of self (#9380) --- .../courier/saved_object/saved_object.js | 295 +++++++++--------- 1 file changed, 146 insertions(+), 149 deletions(-) diff --git a/src/ui/public/courier/saved_object/saved_object.js b/src/ui/public/courier/saved_object/saved_object.js index 56e9f765dceae..68adc76233461 100644 --- a/src/ui/public/courier/saved_object/saved_object.js +++ b/src/ui/public/courier/saved_object/saved_object.js @@ -28,9 +28,6 @@ export default function SavedObjectFactory(es, kbnIndex, Promise, Private, Notif function SavedObject(config) { if (!_.isObject(config)) config = {}; - // save an easy reference to this - let self = this; - /************ * Initialize config vars ************/ @@ -40,7 +37,7 @@ export default function SavedObjectFactory(es, kbnIndex, Promise, Private, Notif // type name for this object, used as the ES-type const type = config.type; - self.getDisplayName = function () { + this.getDisplayName = function () { return type; }; @@ -49,8 +46,8 @@ export default function SavedObjectFactory(es, kbnIndex, Promise, Private, Notif * completes. * @type {boolean} */ - self.isSaving = false; - self.defaults = config.defaults || {}; + this.isSaving = false; + this.defaults = config.defaults || {}; // Create a notifier for sending alerts let notify = new Notifier({ @@ -64,14 +61,64 @@ export default function SavedObjectFactory(es, kbnIndex, Promise, Private, Notif let customInit = config.init || _.noop; // optional search source which this object configures - self.searchSource = config.searchSource ? new SearchSource() : undefined; + this.searchSource = config.searchSource ? new SearchSource() : undefined; // the id of the document - self.id = config.id || void 0; + this.id = config.id || void 0; // Whether to create a copy when the object is saved. This should eventually go away // in favor of a better rename/save flow. - self.copyOnSave = false; + this.copyOnSave = false; + + const parseSearchSource = (searchSourceJson) => { + if (!this.searchSource) return; + + // if we have a searchSource, set its state based on the searchSourceJSON field + let state; + try { + state = JSON.parse(searchSourceJson); + } catch (e) { + state = {}; + } + + let oldState = this.searchSource.toJSON(); + let fnProps = _.transform(oldState, function (dynamic, val, name) { + if (_.isFunction(val)) dynamic[name] = val; + }, {}); + + this.searchSource.set(_.defaults(state, fnProps)); + }; + + /** + * After creation or fetching from ES, ensure that the searchSources index indexPattern + * is an bonafide IndexPattern object. + * + * @return {Promise} + */ + const hydrateIndexPattern = () => { + if (!this.searchSource) { return Promise.resolve(null); } + + if (config.clearSavedIndexPattern) { + this.searchSource.set('index', undefined); + return Promise.resolve(null); + } + + let index = config.indexPattern || this.searchSource.getOwn('index'); + + if (!index) { return Promise.resolve(null); } + + // If index is not an IndexPattern object at this point, then it's a string id of an index. + if (!(index instanceof indexPatterns.IndexPattern)) { + index = indexPatterns.get(index); + } + + // At this point index will either be an IndexPattern, if cached, or a promise that + // will return an IndexPattern, if not cached. + return Promise.resolve(index) + .then((indexPattern) => { + this.searchSource.set('index', indexPattern); + }); + }; /** * Asynchronously initialize this object - will only run @@ -80,7 +127,7 @@ export default function SavedObjectFactory(es, kbnIndex, Promise, Private, Notif * @return {Promise} * @resolved {SavedObject} */ - self.init = _.once(function () { + this.init = _.once(() => { // ensure that the type is defined if (!type) throw new Error('You must define a type name to use SavedObject objects.'); @@ -88,157 +135,107 @@ export default function SavedObjectFactory(es, kbnIndex, Promise, Private, Notif docSource .index(kbnIndex) .type(type) - .id(self.id); + .id(this.id); // check that the mapping for this type is defined return mappingSetup.isDefined(type) - .then(function (defined) { - // if it is already defined skip this step - if (defined) return true; - - mapping.kibanaSavedObjectMeta = { - properties: { - // setup the searchSource mapping, even if it is not used but this type yet - searchSourceJSON: { - type: 'keyword' + .then(function (defined) { + // if it is already defined skip this step + if (defined) return true; + + mapping.kibanaSavedObjectMeta = { + properties: { + // setup the searchSource mapping, even if it is not used but this type yet + searchSourceJSON: { + type: 'keyword' + } } - } - }; + }; - // tell mappingSetup to set type - return mappingSetup.setup(type, mapping); - }) - .then(function () { - // If there is not id, then there is no document to fetch from elasticsearch - if (!self.id) { - // just assign the defaults and be done - _.assign(self, self.defaults); - return hydrateIndexPattern().then(() => { - return afterESResp.call(self); - }); - } + // tell mappingSetup to set type + return mappingSetup.setup(type, mapping); + }) + .then(() => { + // If there is not id, then there is no document to fetch from elasticsearch + if (!this.id) { + // just assign the defaults and be done + _.assign(this, this.defaults); + return hydrateIndexPattern().then(() => { + return afterESResp.call(this); + }); + } - // fetch the object from ES - return docSource.fetch().then(self.applyESResp); - }) - .then(function () { - return customInit.call(self); - }) - .then(function () { - // return our obj as the result of init() - return self; - }); + // fetch the object from ES + return docSource.fetch().then(this.applyESResp); + }) + .then(() => { + return customInit.call(this); + }) + .then(() => { + // return our obj as the result of init() + return this; + }); }); - self.applyESResp = function (resp) { - self._source = _.cloneDeep(resp._source); + this.applyESResp = (resp) => { + this._source = _.cloneDeep(resp._source); - if (resp.found != null && !resp.found) throw new errors.SavedObjectNotFound(type, self.id); + if (resp.found != null && !resp.found) throw new errors.SavedObjectNotFound(type, this.id); let meta = resp._source.kibanaSavedObjectMeta || {}; delete resp._source.kibanaSavedObjectMeta; - if (!config.indexPattern && self._source.indexPattern) { - config.indexPattern = self._source.indexPattern; - delete self._source.indexPattern; + if (!config.indexPattern && this._source.indexPattern) { + config.indexPattern = this._source.indexPattern; + delete this._source.indexPattern; } // assign the defaults to the response - _.defaults(self._source, self.defaults); + _.defaults(this._source, this.defaults); // transform the source using _deserializers - _.forOwn(mapping, function ittr(fieldMapping, fieldName) { + _.forOwn(mapping, (fieldMapping, fieldName) => { if (fieldMapping._deserialize) { - self._source[fieldName] = fieldMapping._deserialize(self._source[fieldName], resp, fieldName, fieldMapping); + this._source[fieldName] = fieldMapping._deserialize(this._source[fieldName], resp, fieldName, fieldMapping); } }); // Give obj all of the values in _source.fields - _.assign(self, self._source); - self.lastSavedTitle = self.title; + _.assign(this, this._source); + this.lastSavedTitle = this.title; return Promise.try(() => { parseSearchSource(meta.searchSourceJSON); return hydrateIndexPattern(); }) - .then(() => { - return Promise.cast(afterESResp.call(self, resp)); - }) - .then(() => { - // Any time obj is updated, re-call applyESResp - docSource.onUpdate().then(self.applyESResp, notify.fatal); - }); - }; - - function parseSearchSource(searchSourceJson) { - if (!self.searchSource) return; - - // if we have a searchSource, set its state based on the searchSourceJSON field - let state; - try { - state = JSON.parse(searchSourceJson); - } catch (e) { - state = {}; - } - - let oldState = self.searchSource.toJSON(); - let fnProps = _.transform(oldState, function (dynamic, val, name) { - if (_.isFunction(val)) dynamic[name] = val; - }, {}); - - self.searchSource.set(_.defaults(state, fnProps)); - } - - /** - * After creation or fetching from ES, ensure that the searchSources index indexPattern - * is an bonafide IndexPattern object. - * - * @return {Promise} - */ - function hydrateIndexPattern() { - if (!self.searchSource) { return Promise.resolve(null); } - - if (config.clearSavedIndexPattern) { - self.searchSource.set('index', undefined); - return Promise.resolve(null); - } - - let index = config.indexPattern || self.searchSource.getOwn('index'); - - if (!index) { return Promise.resolve(null); } - - // If index is not an IndexPattern object at this point, then it's a string id of an index. - if (!(index instanceof indexPatterns.IndexPattern)) { - index = indexPatterns.get(index); - } - - // At this point index will either be an IndexPattern, if cached, or a promise that - // will return an IndexPattern, if not cached. - return Promise.resolve(index) - .then((indexPattern) => { - self.searchSource.set('index', indexPattern); + .then(() => { + return Promise.cast(afterESResp.call(this, resp)); + }) + .then(() => { + // Any time obj is updated, re-call applyESResp + docSource.onUpdate().then(this.applyESResp, notify.fatal); }); - } + }; /** * Serialize this object * * @return {Object} */ - self.serialize = function () { + this.serialize = () => { let body = {}; - _.forOwn(mapping, function (fieldMapping, fieldName) { - if (self[fieldName] != null) { + _.forOwn(mapping, (fieldMapping, fieldName) => { + if (this[fieldName] != null) { body[fieldName] = (fieldMapping._serialize) - ? fieldMapping._serialize(self[fieldName]) - : self[fieldName]; + ? fieldMapping._serialize(this[fieldName]) + : this[fieldName]; } }); - if (self.searchSource) { + if (this.searchSource) { body.kibanaSavedObjectMeta = { - searchSourceJSON: angular.toJson(_.omit(self.searchSource.toJSON(), ['sort', 'size'])) + searchSourceJSON: angular.toJson(_.omit(this.searchSource.toJSON(), ['sort', 'size'])) }; } @@ -249,19 +246,27 @@ export default function SavedObjectFactory(es, kbnIndex, Promise, Private, Notif * Returns true if the object's original title has been changed. New objects return false. * @return {boolean} */ - self.isTitleChanged = function () { - return self._source && self._source.title !== self.title; + this.isTitleChanged = () => { + return this._source && this._source.title !== this.title; }; + /** + * Queries es to refresh the index. + * @returns {Promise} + */ + function refreshIndex() { + return es.indices.refresh({ index: kbnIndex }); + } + /** * Saves this object. * * @return {Promise} * @resolved {String} - The id of the doc */ - self.save = function () { + this.save = () => { // Save the original id in case the save fails. - let originalId = self.id; + let originalId = this.id; // Read https://github.com/elastic/kibana/issues/9056 and // https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable // exists. @@ -269,59 +274,51 @@ export default function SavedObjectFactory(es, kbnIndex, Promise, Private, Notif // to expect a 'save as' flow during a rename, we are keeping the logic the same until a better // UI/UX can be worked out. if (this.copyOnSave) { - self.id = null; + this.id = null; } // Create a unique id for this object if it doesn't have one already. - self.id = this.id || uuid.v1(); + this.id = this.id || uuid.v1(); // ensure that the docSource has the current id - docSource.id(self.id); + docSource.id(this.id); - let source = self.serialize(); + let source = this.serialize(); - self.isSaving = true; + this.isSaving = true; return docSource.doIndex(source) - .then((id) => { self.id = id; }) - .then(self.refreshIndex) + .then((id) => { this.id = id; }) + .then(refreshIndex) .then(() => { - self.isSaving = false; - self.lastSavedTitle = self.title; - return self.id; + this.isSaving = false; + this.lastSavedTitle = this.title; + return this.id; }) - .catch(function (err) { - self.isSaving = false; - self.id = originalId; + .catch((err) => { + this.isSaving = false; + this.id = originalId; return Promise.reject(err); }); }; - self.destroy = function () { + this.destroy = () => { docSource.cancelQueued(); - if (self.searchSource) { - self.searchSource.cancelQueued(); + if (this.searchSource) { + this.searchSource.cancelQueued(); } }; - /** - * Queries es to refresh the index. - * @returns {Promise} - */ - self.refreshIndex = function () { - return es.indices.refresh({ index: kbnIndex }); - }; - /** * Delete this object from Elasticsearch * @return {promise} */ - self.delete = function () { + this.delete = () => { return es.delete( { index: kbnIndex, type: type, id: this.id }) - .then(() => { return this.refreshIndex(); }); + .then(() => { return refreshIndex(); }); }; }