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 1ecd9a7b5e2db..a518465fb02eb 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 = $('