From 161ba75d46f0d8555da00a91a20549468357ba67 Mon Sep 17 00:00:00 2001 From: ppisljar Date: Thu, 8 Sep 2016 16:28:44 +0200 Subject: [PATCH] converting to ES6 class syntax --- src/ui/public/vislib/lib/_error_handler.js | 38 +- src/ui/public/vislib/lib/alerts.js | 161 ++- src/ui/public/vislib/lib/axis_title.js | 96 +- src/ui/public/vislib/lib/chart_title.js | 206 +-- src/ui/public/vislib/lib/data.js | 1242 ++++++++--------- src/ui/public/vislib/lib/dispatch.js | 525 ++++--- src/ui/public/vislib/lib/handler/handler.js | 335 +++-- src/ui/public/vislib/lib/layout/layout.js | 246 ++-- src/ui/public/vislib/lib/x_axis.js | 910 ++++++------ src/ui/public/vislib/lib/y_axis.js | 386 ++--- src/ui/public/vislib/vis.js | 290 ++-- src/ui/public/vislib/visualizations/_chart.js | 124 +- src/ui/public/vislib/visualizations/_map.js | 496 +++---- .../visualizations/_point_series_chart.js | 294 ++-- .../vislib/visualizations/area_chart.js | 665 ++++----- .../vislib/visualizations/column_chart.js | 583 ++++---- .../vislib/visualizations/line_chart.js | 611 ++++---- .../marker_types/base_marker.js | 448 +++--- .../marker_types/geohash_grid.js | 38 +- .../visualizations/marker_types/heatmap.js | 336 +++-- .../marker_types/scaled_circles.js | 80 +- .../marker_types/shaded_circles.js | 86 +- .../public/vislib/visualizations/pie_chart.js | 340 ++--- .../public/vislib/visualizations/tile_map.js | 195 ++- .../vislib/visualizations/time_marker.js | 120 +- 25 files changed, 4418 insertions(+), 4433 deletions(-) diff --git a/src/ui/public/vislib/lib/_error_handler.js b/src/ui/public/vislib/lib/_error_handler.js index 3baa5da76a11..d3ab53351f41 100644 --- a/src/ui/public/vislib/lib/_error_handler.js +++ b/src/ui/public/vislib/lib/_error_handler.js @@ -9,25 +9,29 @@ export default function ErrorHandlerFactory() { * @class ErrorHandler * @constructor */ - function ErrorHandler() {} + class ErrorHandler { + constructor() { - /** - * Validates the height and width are > 0 - * min size must be at least 1 px - * - * @method validateWidthandHeight - * @param width {Number} HTMLElement width - * @param height {Number} HTMLElement height - * @returns {HTMLElement} HTML div with an error message - */ - ErrorHandler.prototype.validateWidthandHeight = function (width, height) { - let badWidth = _.isNaN(width) || width <= 0; - let badHeight = _.isNaN(height) || height <= 0; - - if (badWidth || badHeight) { - throw new errors.ContainerTooSmall(); } - }; + + /** + * Validates the height and width are > 0 + * min size must be at least 1 px + * + * @method validateWidthandHeight + * @param width {Number} HTMLElement width + * @param height {Number} HTMLElement height + * @returns {HTMLElement} HTML div with an error message + */ + validateWidthandHeight(width, height) { + let badWidth = _.isNaN(width) || width <= 0; + let badHeight = _.isNaN(height) || height <= 0; + + if (badWidth || badHeight) { + throw new errors.ContainerTooSmall(); + } + }; + } return ErrorHandler; }; diff --git a/src/ui/public/vislib/lib/alerts.js b/src/ui/public/vislib/lib/alerts.js index 7f44f7a9608f..6263f70295b9 100644 --- a/src/ui/public/vislib/lib/alerts.js +++ b/src/ui/public/vislib/lib/alerts.js @@ -1,4 +1,3 @@ -import d3 from 'd3'; import $ from 'jquery'; import _ from 'lodash'; import Binder from 'ui/binder'; @@ -11,90 +10,88 @@ export default function AlertsFactory(Private) { * @constructor * @param el {HTMLElement} Reference to DOM element */ - function Alerts(vis, data, alertDefs) { - if (!(this instanceof Alerts)) { - return new Alerts(vis, data, alertDefs); + class Alerts { + constructor(vis, data, alertDefs) { + this.vis = vis; + this.data = data; + this.binder = new Binder(); + this.alertDefs = alertDefs || []; + + this.binder.jqOn(vis.el, 'mouseenter', '.vis-alerts-tray', function () { + let $tray = $(this); + hide(); + $(vis.el).on('mousemove', checkForExit); + + function hide() { + $tray.css({ + 'pointer-events': 'none', + opacity: 0.3 + }); + } + + function show() { + $(vis.el).off('mousemove', checkForExit); + $tray.css({ + 'pointer-events': 'auto', + opacity: 1 + }); + } + + function checkForExit(event) { + let pos = $tray.offset(); + if (pos.top > event.clientY || pos.left > event.clientX) return show(); + + let bottom = pos.top + $tray.height(); + if (event.clientY > bottom) return show(); + + let right = pos.left + $tray.width(); + if (event.clientX > right) return show(); + } + }); } - this.vis = vis; - this.data = data; - this.binder = new Binder(); - this.alertDefs = alertDefs || []; - - this.binder.jqOn(vis.el, 'mouseenter', '.vis-alerts-tray', function () { - let $tray = $(this); - hide(); - $(vis.el).on('mousemove', checkForExit); - - function hide() { - $tray.css({ - 'pointer-events': 'none', - opacity: 0.3 - }); - } - - function show() { - $(vis.el).off('mousemove', checkForExit); - $tray.css({ - 'pointer-events': 'auto', - opacity: 1 - }); - } - - function checkForExit(event) { - let pos = $tray.offset(); - if (pos.top > event.clientY || pos.left > event.clientX) return show(); - - let bottom = pos.top + $tray.height(); - if (event.clientY > bottom) return show(); - - let right = pos.left + $tray.width(); - if (event.clientX > right) return show(); - } - }); + /** + * Renders chart titles + * + * @method render + * @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles + */ + render() { + let vis = this.vis; + let data = this.data; + + let alerts = _(this.alertDefs) + .map(function (alertDef) { + if (!alertDef) return; + if (alertDef.test && !alertDef.test(vis, data)) return; + + let type = alertDef.type || 'info'; + let icon = alertDef.icon || type; + let msg = alertDef.msg; + + // alert container + let $icon = $('').addClass('vis-alerts-icon fa fa-' + icon); + let $text = $('

').addClass('vis-alerts-text').text(msg); + + return $('

').addClass('vis-alert vis-alert-' + type).append([$icon, $text]); + }) + .compact(); + + if (!alerts.size()) return; + + $(vis.el).find('.vis-alerts').append( + $('
').addClass('vis-alerts-tray').append(alerts.value()) + ); + }; + + /** + * Tear down the Alerts + * @return {undefined} + */ + destroy() { + this.binder.destroy(); + }; } - /** - * Renders chart titles - * - * @method render - * @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles - */ - Alerts.prototype.render = function () { - let vis = this.vis; - let data = this.data; - - let alerts = _(this.alertDefs) - .map(function (alertDef) { - if (!alertDef) return; - if (alertDef.test && !alertDef.test(vis, data)) return; - - let type = alertDef.type || 'info'; - let icon = alertDef.icon || type; - let msg = alertDef.msg; - - // alert container - let $icon = $('').addClass('vis-alerts-icon fa fa-' + icon); - let $text = $('

').addClass('vis-alerts-text').text(msg); - - return $('

').addClass('vis-alert vis-alert-' + type).append([$icon, $text]); - }) - .compact(); - - if (!alerts.size()) return; - - $(vis.el).find('.vis-alerts').append( - $('
').addClass('vis-alerts-tray').append(alerts.value()) - ); - }; - - /** - * Tear down the Alerts - * @return {undefined} - */ - Alerts.prototype.destroy = function () { - this.binder.destroy(); - }; - return Alerts; }; diff --git a/src/ui/public/vislib/lib/axis_title.js b/src/ui/public/vislib/lib/axis_title.js index 297f55a9be81..dca06a1088f3 100644 --- a/src/ui/public/vislib/lib/axis_title.js +++ b/src/ui/public/vislib/lib/axis_title.js @@ -15,62 +15,60 @@ export default function AxisTitleFactory(Private) { * @param xTitle {String} X-axis title * @param yTitle {String} Y-axis title */ - _.class(AxisTitle).inherits(ErrorHandler); - function AxisTitle(el, xTitle, yTitle) { - if (!(this instanceof AxisTitle)) { - return new AxisTitle(el, xTitle, yTitle); + class AxisTitle extends ErrorHandler { + constructor(el, xTitle, yTitle) { + super(); + this.el = el; + this.xTitle = xTitle; + this.yTitle = yTitle; } - 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 - */ - AxisTitle.prototype.render = function () { - 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)); - }; + /** + * 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 - */ - AxisTitle.prototype.draw = function (title) { - let self = this; + /** + * Appends an SVG with title text + * + * @method draw + * @param title {String} Axis title + * @returns {Function} Appends axis title to a D3 selection + */ + draw(title) { + let self = this; - return function (selection) { - selection.each(function () { - let el = this; - let div = d3.select(el); - let width = $(el).width(); - let height = $(el).height(); + return function (selection) { + selection.each(function () { + let el = this; + let div = d3.select(el); + let width = $(el).width(); + let height = $(el).height(); - self.validateWidthandHeight(width, 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); - }); + 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 b0442758a8e5..05ae69f2aa57 100644 --- a/src/ui/public/vislib/lib/chart_title.js +++ b/src/ui/public/vislib/lib/chart_title.js @@ -15,117 +15,117 @@ export default function ChartTitleFactory(Private) { * @constructor * @param el {HTMLElement} Reference to DOM element */ - _.class(ChartTitle).inherits(ErrorHandler); - function ChartTitle(el) { - if (!(this instanceof ChartTitle)) { - return new ChartTitle(el); + class ChartTitle extends ErrorHandler { + constructor(el) { + super(); + this.el = el; + this.tooltip = new Tooltip('chart-title', el, function (d) { + return '

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

'; + }); } - this.el = el; - this.tooltip = new Tooltip('chart-title', el, function (d) { - return '

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

'; - }); - } - - /** - * Renders chart titles - * - * @method render - * @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles - */ - ChartTitle.prototype.render = function () { - let el = d3.select(this.el).select('.chart-title').node(); - let width = el ? el.clientWidth : 0; - let height = el ? el.clientHeight : 0; + /** + * Renders chart titles + * + * @method render + * @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles + */ + render() { + let el = d3.select(this.el).select('.chart-title').node(); + let width = el ? el.clientWidth : 0; + let height = el ? el.clientHeight : 0; + + return d3.select(this.el).selectAll('.chart-title').call(this.draw(width, height)); + }; - 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) { + let self = this; + + return function (selection) { + selection.each(function () { + let text = d3.select(this); + let n = text[0].length; + let maxWidth = size / n * 0.9; + let length = this.getComputedTextLength(); + let str; + let avg; + let end; + + if (length > maxWidth) { + str = text.text(); + avg = length / str.length; + end = Math.floor(maxWidth / avg) - 5; + str = str.substr(0, end) + '...'; + self.addMouseEvents(text); + + return text.text(str); + } - /** - * Truncates chart title text - * - * @method truncate - * @param size {Number} Height or width of the HTML Element - * @returns {Function} Truncates text - */ - ChartTitle.prototype.truncate = function (size) { - let self = this; - - return function (selection) { - selection.each(function () { - let text = d3.select(this); - let n = text[0].length; - let maxWidth = size / n * 0.9; - let length = this.getComputedTextLength(); - let str; - let avg; - let end; - - if (length > maxWidth) { - str = text.text(); - avg = length / str.length; - end = Math.floor(maxWidth / avg) - 5; - str = str.substr(0, end) + '...'; - self.addMouseEvents(text); - - return text.text(str); - } - - return text.text(); - }); + return text.text(); + }); + }; }; - }; - /** - * 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 - */ - ChartTitle.prototype.addMouseEvents = function (target) { - if (this.tooltip) { - return target.call(this.tooltip.render()); - } - }; + /** + * 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 - */ - ChartTitle.prototype.draw = function (width, height) { - let self = this; - - return function (selection) { - selection.each(function () { - let div = d3.select(this); - let dataType = this.parentNode.__data__.rows ? 'rows' : 'columns'; - let size = dataType === 'rows' ? height : width; - let txtHtOffset = 11; - - self.validateWidthandHeight(width, height); - - div.append('svg') - .attr('width', width) - .attr('height', height) - .append('text') - .attr('transform', function () { - if (dataType === 'rows') { - return 'translate(' + txtHtOffset + ',' + height / 2 + ')rotate(270)'; - } - return 'translate(' + width / 2 + ',' + txtHtOffset + ')'; - }) - .attr('text-anchor', 'middle') - .text(function (d) { return d.label; }); - - // truncate long chart titles - div.selectAll('text') - .call(self.truncate(size)); - }); + /** + * Appends chart titles to the visualization + * + * @method draw + * @returns {Function} Appends chart titles to a D3 selection + */ + draw(width, height) { + let self = this; + + return function (selection) { + selection.each(function () { + let div = d3.select(this); + let dataType = this.parentNode.__data__.rows ? 'rows' : 'columns'; + let size = dataType === 'rows' ? height : width; + let txtHtOffset = 11; + + self.validateWidthandHeight(width, height); + + div.append('svg') + .attr('width', width) + .attr('height', height) + .append('text') + .attr('transform', function () { + if (dataType === 'rows') { + return 'translate(' + txtHtOffset + ',' + height / 2 + ')rotate(270)'; + } + return 'translate(' + width / 2 + ',' + txtHtOffset + ')'; + }) + .attr('text-anchor', 'middle') + .text(function (d) { + return d.label; + }); + + // truncate long chart titles + div.selectAll('text') + .call(self.truncate(size)); + }); + }; }; - }; + } return ChartTitle; }; diff --git a/src/ui/public/vislib/lib/data.js b/src/ui/public/vislib/lib/data.js index 5adceeb12ec4..d4a1fd878c7a 100644 --- a/src/ui/public/vislib/lib/data.js +++ b/src/ui/public/vislib/lib/data.js @@ -21,705 +21,705 @@ export default function DataFactory(Private) { * @param data {Object} Elasticsearch query results * @param attr {Object|*} Visualization options */ - function Data(data, attr, uiState) { - if (!(this instanceof Data)) { - return new Data(data, attr, uiState); - } - - this.uiState = uiState; - - let 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; - } + class Data { + constructor(data, attr, uiState) { + this.uiState = uiState; + + let 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.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); - } - return d.y; - }) - .offset(offset || 'zero') - }); - - if (attr.mode === 'stacked' && attr.type === 'histogram') { - this._attr.stack.out(function (d, y0, y) { - return self._stackNegAndPosVals(d, y0, y); + this.data = 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); + } + return d.y; + }) + .offset(offset || 'zero') }); - } - } - - Data.prototype._updateData = function () { - if (this.data.rows) { - _.map(this.data.rows, this._updateDataSeriesLabel, this); - } else if (this.data.columns) { - _.map(this.data.columns, this._updateDataSeriesLabel, this); - } else { - this._updateDataSeriesLabel(this.data); - } - }; - Data.prototype._updateDataSeriesLabel = function (eachData) { - if (eachData.series) { - eachData.series[0].label = this.get('yAxisLabel'); - } - }; - - Data.prototype._getLabels = function (data) { - if (this.type === 'series') { - let noLabel = getLabels(data).length === 1 && getLabels(data)[0] === ''; - if (noLabel) { - this._updateData(); - return [(this.get('yAxisLabel'))]; + if (attr.mode === 'stacked' && attr.type === 'histogram') { + this._attr.stack.out(function (d, y0, y) { + return self._stackNegAndPosVals(d, y0, y); + }); } - return getLabels(data); } - return this.pieNames(); - }; - - /** - * Returns true for positive numbers - */ - Data.prototype._isPositive = function (num) { - return num >= 0; - }; - /** - * Returns true for negative numbers - */ - Data.prototype._isNegative = function (num) { - return num < 0; - }; - - /** - * Adds two input values - */ - Data.prototype._addVals = function (a, b) { - return a + b; - }; + _updateData() { + if (this.data.rows) { + _.map(this.data.rows, this._updateDataSeriesLabel, this); + } else if (this.data.columns) { + _.map(this.data.columns, this._updateDataSeriesLabel, this); + } else { + this._updateDataSeriesLabel(this.data); + } + }; - /** - * Returns the results of the addition of numbers in a filtered array. - */ - Data.prototype._sumYs = function (arr, callback) { - let filteredArray = arr.filter(callback); + _updateDataSeriesLabel(eachData) { + if (eachData.series) { + eachData.series[0].label = this.get('yAxisLabel'); + } + }; - return (filteredArray.length) ? filteredArray.reduce(this._addVals) : 0; - }; + _getLabels(data) { + if (this.type === 'series') { + let noLabel = getLabels(data).length === 1 && getLabels(data)[0] === ''; + if (noLabel) { + this._updateData(); + return [(this.get('yAxisLabel'))]; + } + return getLabels(data); + } + return this.pieNames(); + }; - /** - * Calculates the d.y0 value for stacked data in D3. - */ - Data.prototype._calcYZero = function (y, arr) { - if (y >= 0) return this._sumYs(arr, this._isPositive); - return this._sumYs(arr, this._isNegative); - }; + /** + * Returns true for positive numbers + */ + _isPositive(num) { + return num >= 0; + }; - /** - * - */ - Data.prototype._getCounts = function (i, j) { - let data = this.chartData(); - let dataLengths = {}; + /** + * Returns true for negative numbers + */ + _isNegative(num) { + return num < 0; + }; - dataLengths.charts = data.length; - dataLengths.stacks = dataLengths.charts ? data[i].series.length : 0; - dataLengths.values = dataLengths.stacks ? data[i].series[j].values.length : 0; + /** + * Adds two input values + */ + _addVals(a, b) { + return a + b; + }; - return dataLengths; - }; + /** + * Returns the results of the addition of numbers in a filtered array. + */ + _sumYs(arr, callback) { + let filteredArray = arr.filter(callback); - /** - * - */ - Data.prototype._createCache = function () { - let cache = { - index: { - chart: 0, - stack: 0, - value: 0 - }, - yValsArr: [] + return (filteredArray.length) ? filteredArray.reduce(this._addVals) : 0; }; - cache.count = this._getCounts(cache.index.chart, cache.index.stack); - - return cache; - }; + /** + * 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); + }; - /** - * 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. - */ - Data.prototype._stackNegAndPosVals = function (d, y0, y) { - let data = this.chartData(); + /** + * + */ + _getCounts(i, j) { + let data = this.chartData(); + let dataLengths = {}; - // Storing counters and data characteristics needed to stack values properly - if (!this._cache) { - this._cache = this._createCache(); - } + dataLengths.charts = data.length; + dataLengths.stacks = dataLengths.charts ? data[i].series.length : 0; + dataLengths.values = dataLengths.stacks ? data[i].series[j].values.length : 0; - d.y0 = this._calcYZero(y, this._cache.yValsArr); - ++this._cache.index.stack; + return dataLengths; + }; + /** + * + */ + _createCache() { + let cache = { + index: { + chart: 0, + stack: 0, + value: 0 + }, + yValsArr: [] + }; - // last stack, or last value, reset the stack count and y value array - let 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); - } + cache.count = this._getCounts(cache.index.chart, cache.index.stack); - // last value, prepare for the next chart, if one exists - let lastValue = (this._cache.index.value >= this._cache.count.values); - if (lastValue) { - this._cache.index.value = 0; - ++this._cache.index.chart; + return cache; + }; - // no more charts, reset the queue and finish - if (this._cache.index.chart >= this._cache.count.charts) { + /** + * 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) { + let data = this.chartData(); + + // Storing counters and data characteristics needed to stack values properly + if (!this._cache) { this._cache = this._createCache(); - return; } - // get stack and value count for next chart - let 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; - } - }; - - Data.prototype.getDataType = function () { - let data = this.getVisData(); - let type; - - data.forEach(function (obj) { - if (obj.series) { - type = 'series'; - } else if (obj.slices) { - type = 'slices'; - } else if (obj.geoJson) { - type = 'geoJson'; + 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 + let 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); } - }); - return type; - }; + // last value, prepare for the next chart, if one exists + let 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 + let 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; + } + }; - /** - * Returns an array of the actual x and y data value objects - * from data with series keys - * - * @method chartData - * @returns {*} Array of data objects - */ - Data.prototype.chartData = function () { - if (!this.data.series) { - let arr = this.data.rows ? this.data.rows : this.data.columns; - return _.toArray(arr); - } - return [this.data]; - }; + getDataType() { + let data = this.getVisData(); + let type; + + data.forEach(function (obj) { + if (obj.series) { + type = 'series'; + } else if (obj.slices) { + type = 'slices'; + } else if (obj.geoJson) { + type = 'geoJson'; + } + }); - /** - * Returns an array of chart data objects - * - * @method getVisData - * @returns {*} Array of chart data objects - */ - Data.prototype.getVisData = function () { - let visData; - - if (this.data.rows) { - visData = this.data.rows; - } else if (this.data.columns) { - visData = this.data.columns; - } else { - visData = [this.data]; - } + return type; + }; - return visData; - }; + /** + * Returns an array of the actual x and y data value objects + * from data with series keys + * + * @method chartData + * @returns {*} Array of data objects + */ + chartData() { + if (!this.data.series) { + let arr = this.data.rows ? this.data.rows : this.data.columns; + return _.toArray(arr); + } + return [this.data]; + }; - /** - * get min and max for all cols, rows of data - * - * @method getMaxMin - * @return {Object} - */ - Data.prototype.getGeoExtents = function () { - let visData = this.getVisData(); + /** + * Returns an array of chart data objects + * + * @method getVisData + * @returns {*} Array of chart data objects + */ + getVisData() { + let visData; + + if (this.data.rows) { + visData = this.data.rows; + } else if (this.data.columns) { + visData = this.data.columns; + } else { + visData = [this.data]; + } - return _.reduce(_.pluck(visData, 'geoJson.properties'), function (minMax, props) { - return { - min: Math.min(props.min, minMax.min), - max: Math.max(props.max, minMax.max) - }; - }, { min: Infinity, max: -Infinity }); - }; + return visData; + }; - /** - * Returns array of chart data objects for pie data objects - * - * @method pieData - * @returns {*} Array of chart data objects - */ - Data.prototype.pieData = function () { - if (!this.data.slices) { - return this.data.rows ? this.data.rows : this.data.columns; - } - return [this.data]; - }; + /** + * get min and max for all cols, rows of data + * + * @method getMaxMin + * @return {Object} + */ + getGeoExtents() { + let visData = this.getVisData(); + + return _.reduce(_.pluck(visData, 'geoJson.properties'), function (minMax, props) { + return { + min: Math.min(props.min, minMax.min), + max: Math.max(props.max, minMax.max) + }; + }, {min: Infinity, max: -Infinity}); + }; - /** - * Get attributes off the data, e.g. `tooltipFormatter` or `xAxisFormatter` - * pulls the value off the first item in the array - * these values are typically the same between data objects of the same chart - * TODO: May need to verify this or refactor - * - * @method get - * @param thing {String} Data object key - * @returns {*} Data object value - */ - Data.prototype.get = function (thing, def) { - let source = (this.data.rows || this.data.columns || [this.data])[0]; - return _.get(source, thing, def); - }; + /** + * Returns array of chart data objects for pie data objects + * + * @method pieData + * @returns {*} Array of chart data objects + */ + pieData() { + if (!this.data.slices) { + return this.data.rows ? this.data.rows : this.data.columns; + } + return [this.data]; + }; - /** - * Returns true if null values are present - * @returns {*} - */ - Data.prototype.hasNullValues = function () { - let chartData = this.chartData(); + /** + * Get attributes off the data, e.g. `tooltipFormatter` or `xAxisFormatter` + * pulls the value off the first item in the array + * these values are typically the same between data objects of the same chart + * TODO: May need to verify this or refactor + * + * @method get + * @param thing {String} Data object key + * @returns {*} Data object value + */ + get(thing, def) { + let source = (this.data.rows || this.data.columns || [this.data])[0]; + return _.get(source, thing, def); + }; - return chartData.some(function (chart) { - return chart.series.some(function (obj) { - return obj.values.some(function (d) { - return d.y === null; + /** + * Returns true if null values are present + * @returns {*} + */ + hasNullValues() { + let chartData = this.chartData(); + + return chartData.some(function (chart) { + return chart.series.some(function (obj) { + return obj.values.some(function (d) { + return d.y === null; + }); }); }); - }); - }; - - /** - * Return an array of all value objects - * Pluck the data.series array from each data object - * Create an array of all the value objects from the series array - * - * @method flatten - * @returns {Array} Value objects - */ - Data.prototype.flatten = function () { - return _(this.chartData()) - .pluck('series') - .flattenDeep() - .pluck('values') - .flattenDeep() - .value(); - }; - - /** - * Determines whether histogram charts should be stacked - * TODO: need to make this more generic - * - * @method shouldBeStacked - * @returns {boolean} - */ - Data.prototype.shouldBeStacked = function () { - let isHistogram = (this._attr.type === 'histogram'); - let isArea = (this._attr.type === 'area'); - let isOverlapping = (this._attr.mode === 'overlap'); - let grouped = (this._attr.mode === 'grouped'); - - let stackedHisto = isHistogram && !grouped; - let stackedArea = isArea && !isOverlapping; - - return stackedHisto || stackedArea; - }; - - /** - * Validates that the Y axis min value defined by user input - * is a number. - * - * @param val {Number} Y axis min value - * @returns {Number} Y axis min value - */ - Data.prototype.validateUserDefinedYMin = function (val) { - if (!_.isNumber(val)) { - throw new Error('validateUserDefinedYMin expects a number'); - } - 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 - */ - Data.prototype.getYMin = function (getValue) { - let self = this; - let arr = []; + }; - if (this._attr.mode === 'percentage' || this._attr.mode === 'wiggle' || - this._attr.mode === 'silhouette') { - return 0; - } + /** + * Return an array of all value objects + * Pluck the data.series array from each data object + * Create an array of all the value objects from the series array + * + * @method flatten + * @returns {Array} Value objects + */ + flatten() { + return _(this.chartData()) + .pluck('series') + .flattenDeep() + .pluck('values') + .flattenDeep() + .value(); + }; - let 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; - } + /** + * Determines whether histogram charts should be stacked + * TODO: need to make this more generic + * + * @method shouldBeStacked + * @returns {boolean} + */ + shouldBeStacked() { + let isHistogram = (this._attr.type === 'histogram'); + let isArea = (this._attr.type === 'area'); + let isOverlapping = (this._attr.mode === 'overlap'); + let grouped = (this._attr.mode === 'grouped'); + + let stackedHisto = isHistogram && !grouped; + let stackedArea = isArea && !isOverlapping; + + return stackedHisto || stackedArea; + }; - let min = Infinity; + /** + * Validates that the Y axis min value defined by user input + * is a number. + * + * @param val {Number} Y axis min value + * @returns {Number} Y axis min value + */ + validateUserDefinedYMin(val) { + if (!_.isNumber(val)) { + throw new Error('validateUserDefinedYMin expects a number'); + } + return val; + }; - // for each object in the dataArray, - // push the calculated y value to the initialized array (arr) - _.each(this.chartData(), function (chart) { - let calculatedMin = self._getYExtent(chart, 'min', getValue); - if (!_.isUndefined(calculatedMin)) { - min = Math.min(min, calculatedMin); + /** + * 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) { + let self = this; + let arr = []; + + if (this._attr.mode === 'percentage' || this._attr.mode === 'wiggle' || + this._attr.mode === 'silhouette') { + return 0; } - }); - return min; - }; + let 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; + } - /** - * 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 - */ - Data.prototype.getYMax = function (getValue) { - let self = this; - let arr = []; + let min = Infinity; - if (self._attr.mode === 'percentage') { - return 1; - } + // for each object in the dataArray, + // push the calculated y value to the initialized array (arr) + _.each(this.chartData(), function (chart) { + let calculatedMin = self._getYExtent(chart, 'min', getValue); + if (!_.isUndefined(calculatedMin)) { + min = Math.min(min, calculatedMin); + } + }); - let 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; - } + return min; + }; - let max = -Infinity; + /** + * 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) { + let self = this; + let arr = []; + + if (self._attr.mode === 'percentage') { + return 1; + } - // for each object in the dataArray, - // push the calculated y value to the initialized array (arr) - _.each(this.chartData(), function (chart) { - let calculatedMax = self._getYExtent(chart, 'max', getValue); - if (!_.isUndefined(calculatedMax)) { - max = Math.max(max, calculatedMax); + let 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; } - }); - return max; - }; + let max = -Infinity; - /** - * 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 - */ - Data.prototype.stackData = function (series) { - // Should not stack values on line chart - if (this._attr.type === 'line') return series; - return this._attr.stack(series); - }; + // for each object in the dataArray, + // push the calculated y value to the initialized array (arr) + _.each(this.chartData(), function (chart) { + let calculatedMax = self._getYExtent(chart, 'max', getValue); + if (!_.isUndefined(calculatedMax)) { + max = Math.max(max, calculatedMax); + } + }); - /** - * 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(). - */ - Data.prototype._getYExtent = function (chart, extent, getValue) { - if (this.shouldBeStacked()) { - this.stackData(_.pluck(chart.series, 'values')); - getValue = getValue || this._getYStack; - } else { - getValue = getValue || this._getY; - } + return max; + }; - let points = chart.series - .reduce(function (points, series) { - return points.concat(series.values); - }, []) - .map(getValue); + /** + * 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); + }; - return d3[extent](points); - }; + /** + * 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; + } - /** - * Calculates the y stack value for each data object - */ - Data.prototype._getYStack = function (d) { - return d.y0 + d.y; - }; + let points = chart.series + .reduce(function (points, series) { + return points.concat(series.values); + }, []) + .map(getValue); - /** - * Calculates the Y max value - */ - Data.prototype._getY = function (d) { - return d.y; - }; + return d3[extent](points); + }; - /** - * Helper function for getNames - * Returns an array of objects with a name (key) value and an index value. - * The index value allows us to sort the names in the correct nested order. - * - * @method returnNames - * @param array {Array} Array of data objects - * @param index {Number} Number of times the object is nested - * @param columns {Object} Contains name formatter information - * @returns {Array} Array of labels (strings) - */ - Data.prototype.returnNames = function (array, index, columns) { - let names = []; - let self = this; - - _.forEach(array, function (obj, i) { - names.push({ - label: obj.name, - values: obj, - index: index - }); + /** + * Calculates the y stack value for each data object + */ + _getYStack(d) { + return d.y0 + d.y; + }; - if (obj.children) { - let plusIndex = index + 1; + /** + * Calculates the Y max value + */ + _getY(d) { + return d.y; + }; - _.forEach(self.returnNames(obj.children, plusIndex, columns), function (namedObj) { - names.push(namedObj); + /** + * Helper function for getNames + * Returns an array of objects with a name (key) value and an index value. + * The index value allows us to sort the names in the correct nested order. + * + * @method returnNames + * @param array {Array} Array of data objects + * @param index {Number} Number of times the object is nested + * @param columns {Object} Contains name formatter information + * @returns {Array} Array of labels (strings) + */ + returnNames(array, index, columns) { + let names = []; + let self = this; + + _.forEach(array, function (obj, i) { + names.push({ + label: obj.name, + values: obj, + index: index }); - } - }); - - return names; - }; - - /** - * Flattens hierarchical data into an array of objects with a name and index value. - * The indexed value determines the order of nesting in the data. - * Returns an array with names sorted by the index value. - * - * @method getNames - * @param data {Object} Chart data object - * @param columns {Object} Contains formatter information - * @returns {Array} Array of names (strings) - */ - Data.prototype.getNames = function (data, columns) { - let slices = data.slices; - if (slices.children) { - let namedObj = this.returnNames(slices.children, 0, columns); + if (obj.children) { + let plusIndex = index + 1; - return _(namedObj) - .sortBy(function (obj) { - return obj.index; - }) - .unique(function (d) { - return d.label; - }) - .value(); - } - }; - - /** - * Removes zeros from pie chart data - * @param slices - * @returns {*} - */ - Data.prototype._removeZeroSlices = function (slices) { - let self = this; + _.forEach(self.returnNames(obj.children, plusIndex, columns), function (namedObj) { + names.push(namedObj); + }); + } + }); - if (!slices.children) return slices; + return names; + }; - slices = _.clone(slices); - slices.children = slices.children.reduce(function (children, child) { - if (child.size !== 0) { - children.push(self._removeZeroSlices(child)); + /** + * Flattens hierarchical data into an array of objects with a name and index value. + * The indexed value determines the order of nesting in the data. + * Returns an array with names sorted by the index value. + * + * @method getNames + * @param data {Object} Chart data object + * @param columns {Object} Contains formatter information + * @returns {Array} Array of names (strings) + */ + getNames(data, columns) { + let slices = data.slices; + + if (slices.children) { + let namedObj = this.returnNames(slices.children, 0, columns); + + return _(namedObj) + .sortBy(function (obj) { + return obj.index; + }) + .unique(function (d) { + return d.label; + }) + .value(); } - return children; - }, []); - - return slices; - }; - - /** - * Returns an array of names ordered by appearance in the nested array - * of objects - * - * @method pieNames - * @returns {Array} Array of unique names (strings) - */ - Data.prototype.pieNames = function (data) { - let self = this; - let names = []; + }; - _.forEach(data, function (obj) { - let columns = obj.raw ? obj.raw.columns : undefined; - obj.slices = self._removeZeroSlices(obj.slices); + /** + * Removes zeros from pie chart data + * @param slices + * @returns {*} + */ + _removeZeroSlices(slices) { + let self = this; + + if (!slices.children) return slices; + + slices = _.clone(slices); + slices.children = slices.children.reduce(function (children, child) { + if (child.size !== 0) { + children.push(self._removeZeroSlices(child)); + } + return children; + }, []); + + return slices; + }; - _.forEach(self.getNames(obj, columns), function (name) { - names.push(name); + /** + * Returns an array of names ordered by appearance in the nested array + * of objects + * + * @method pieNames + * @returns {Array} Array of unique names (strings) + */ + pieNames(data) { + let self = this; + let names = []; + + _.forEach(data, function (obj) { + let columns = obj.raw ? obj.raw.columns : undefined; + obj.slices = self._removeZeroSlices(obj.slices); + + _.forEach(self.getNames(obj, columns), function (name) { + names.push(name); + }); }); - }); - - return _.uniq(names, 'label'); - }; - /** - * Inject zeros into the data - * - * @method injectZeros - * @returns {Object} Data object with zeros injected - */ - Data.prototype.injectZeros = function () { - return injectZeros(this.data); - }; - - /** - * Returns an array of all x axis values from the data - * - * @method xValues - * @returns {Array} Array of x axis values - */ - Data.prototype.xValues = function () { - return orderKeys(this.data); - }; - - /** - * Return an array of unique labels - * Curently, only used for vertical bar and line charts, - * or any data object with series values - * - * @method getLabels - * @returns {Array} Array of labels (strings) - */ - Data.prototype.getLabels = function () { - return getLabels(this.data); - }; + return _.uniq(names, 'label'); + }; - /** - * Returns a function that does color lookup on labels - * - * @method getColorFunc - * @returns {Function} Performs lookup on string and returns hex color - */ - Data.prototype.getColorFunc = function () { - return color(this.getLabels(), this.uiState.get('vis.colors')); - }; + /** + * Inject zeros into the data + * + * @method injectZeros + * @returns {Object} Data object with zeros injected + */ + injectZeros() { + return injectZeros(this.data); + }; - /** - * Returns a function that does color lookup on names for pie charts - * - * @method getPieColorFunc - * @returns {Function} Performs lookup on string and returns hex color - */ - Data.prototype.getPieColorFunc = function () { - return color(this.pieNames(this.getVisData()).map(function (d) { - return d.label; - }), this.uiState.get('vis.colors')); - }; + /** + * Returns an array of all x axis values from the data + * + * @method xValues + * @returns {Array} Array of x axis values + */ + xValues() { + return orderKeys(this.data); + }; - /** - * ensure that the datas ordered property has a min and max - * if the data represents an ordered date range. - * - * @return {undefined} - */ - Data.prototype._normalizeOrdered = function () { - let data = this.getVisData(); - let self = this; + /** + * Return an array of unique labels + * Curently, only used for vertical bar and line charts, + * or any data object with series values + * + * @method getLabels + * @returns {Array} Array of labels (strings) + */ + getLabels() { + return getLabels(this.data); + }; - data.forEach(function (d) { - if (!d.ordered || !d.ordered.date) return; + /** + * Returns a function that does color lookup on labels + * + * @method getColorFunc + * @returns {Function} Performs lookup on string and returns hex color + */ + getColorFunc() { + return color(this.getLabels(), this.uiState.get('vis.colors')); + }; - let missingMin = d.ordered.min == null; - let missingMax = d.ordered.max == null; + /** + * Returns a function that does color lookup on names for pie charts + * + * @method getPieColorFunc + * @returns {Function} Performs lookup on string and returns hex color + */ + getPieColorFunc() { + return color(this.pieNames(this.getVisData()).map(function (d) { + return d.label; + }), this.uiState.get('vis.colors')); + }; - if (missingMax || missingMin) { - let extent = d3.extent(self.xValues()); - if (missingMin) d.ordered.min = extent[0]; - if (missingMax) d.ordered.max = extent[1]; - } - }); - }; + /** + * ensure that the datas ordered property has a min and max + * if the data represents an ordered date range. + * + * @return {undefined} + */ + _normalizeOrdered() { + let data = this.getVisData(); + let self = this; + + data.forEach(function (d) { + if (!d.ordered || !d.ordered.date) return; + + let missingMin = d.ordered.min == null; + let missingMax = d.ordered.max == null; + + if (missingMax || missingMin) { + let extent = d3.extent(self.xValues()); + if (missingMin) d.ordered.min = extent[0]; + if (missingMax) d.ordered.max = extent[1]; + } + }); + }; - /** - * Calculates min and max values for all map data - * series.rows is an array of arrays - * each row is an array of values - * last value in row array is bucket count - * - * @method mapDataExtents - * @param series {Array} Array of data objects - * @returns {Array} min and max values - */ - Data.prototype.mapDataExtents = function (series) { - let values; - values = _.map(series.rows, function (row) { - return row[row.length - 1]; - }); - let extents = [_.min(values), _.max(values)]; - return extents; - }; + /** + * Calculates min and max values for all map data + * series.rows is an array of arrays + * each row is an array of values + * last value in row array is bucket count + * + * @method mapDataExtents + * @param series {Array} Array of data objects + * @returns {Array} min and max values + */ + mapDataExtents(series) { + let values; + values = _.map(series.rows, function (row) { + return row[row.length - 1]; + }); + let extents = [_.min(values), _.max(values)]; + return extents; + }; - /** - * Get the maximum number of series, considering each chart - * individually. - * - * @return {number} - the largest number of series from all charts - */ - Data.prototype.maxNumberOfSeries = function () { - return this.chartData().reduce(function (max, chart) { - return Math.max(max, chart.series.length); - }, 0); - }; + /** + * Get the maximum number of series, considering each chart + * individually. + * + * @return {number} - the largest number of series from all charts + */ + maxNumberOfSeries() { + return this.chartData().reduce(function (max, chart) { + return Math.max(max, chart.series.length); + }, 0); + }; + } return Data; }; diff --git a/src/ui/public/vislib/lib/dispatch.js b/src/ui/public/vislib/lib/dispatch.js index fd2820f1a402..7a66288c4431 100644 --- a/src/ui/public/vislib/lib/dispatch.js +++ b/src/ui/public/vislib/lib/dispatch.js @@ -14,300 +14,299 @@ export default function DispatchClass(Private) { * @param handler {Object} Reference to Handler Class Object */ - _.class(Dispatch).inherits(SimpleEmitter); - function Dispatch(handler) { - if (!(this instanceof Dispatch)) { - return new Dispatch(handler); + class Dispatch extends SimpleEmitter { + constructor(handler) { + super(); + this.handler = handler; + this._listeners = {}; } - Dispatch.Super.call(this); - this.handler = handler; - this._listeners = {}; - } - - /** - * Response to click and hover events - * - * @param d {Object} Data point - * @param i {Number} Index number of data point - * @returns {{value: *, point: *, label: *, color: *, pointIndex: *, + /** + * Response to click and hover events + * + * @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 - */ - Dispatch.prototype.eventResponse = function (d, i) { - let datum = d._input || d; - let data = d3.event.target.nearestViewportElement ? - d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__; - let label = d.label ? d.label : d.name; - let isSeries = !!(data && data.series); - let isSlices = !!(data && data.slices); - let series = isSeries ? data.series : undefined; - let slices = isSlices ? data.slices : undefined; - let handler = this.handler; - let color = _.get(handler, 'data.color'); - let isPercentage = (handler && handler._attr.mode === 'percentage'); - - let eventData = { - value: d.y, - point: datum, - datum: datum, - label: label, - color: color ? color(label) : undefined, - pointIndex: i, - series: series, - slices: slices, - config: handler && handler._attr, - data: data, - e: d3.event, - handler: handler - }; + */ + eventResponse(d, i) { + let datum = d._input || d; + let data = d3.event.target.nearestViewportElement ? + d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__; + let label = d.label ? d.label : d.name; + let isSeries = !!(data && data.series); + let isSlices = !!(data && data.slices); + let series = isSeries ? data.series : undefined; + let slices = isSlices ? data.slices : undefined; + let handler = this.handler; + let color = _.get(handler, 'data.color'); + let isPercentage = (handler && handler._attr.mode === 'percentage'); + + let eventData = { + value: d.y, + point: datum, + datum: datum, + label: label, + color: color ? color(label) : undefined, + pointIndex: i, + series: series, + slices: slices, + config: handler && handler._attr, + data: data, + e: d3.event, + handler: handler + }; - if (isSeries) { - // Find object with the actual d value and add it to the point object - let object = _.find(series, { 'label': d.label }); - eventData.value = +object.values[i].y; + if (isSeries) { + // Find object with the actual d value and add it to the point object + let object = _.find(series, {'label': d.label}); + 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) + '%'; + } } - } - - return eventData; - }; - /** - * Returns a function that adds events and listeners to a D3 selection - * - * @method addEvent - * @param event {String} - * @param callback {Function} - * @returns {Function} - */ - Dispatch.prototype.addEvent = function (event, callback) { - return function (selection) { - selection.each(function () { - let element = d3.select(this); - - if (typeof callback === 'function') { - return element.on(event, callback); - } - }); + return eventData; }; - }; - /** - * - * @method addHoverEvent - * @returns {Function} - */ - Dispatch.prototype.addHoverEvent = function () { - let self = this; - let isClickable = this.listenerCount('click') > 0; - let addEvent = this.addEvent; - let $el = this.handler.el; - if (!this.handler.highlight) { - this.handler.highlight = self.highlight; - } + /** + * Returns a function that adds events and listeners to a D3 selection + * + * @method addEvent + * @param event {String} + * @param callback {Function} + * @returns {Function} + */ + addEvent(event, callback) { + return function (selection) { + selection.each(function () { + let element = d3.select(this); + + if (typeof callback === 'function') { + return element.on(event, callback); + } + }); + }; + }; - function hover(d, i) { - // Add pointer if item is clickable - if (isClickable) { - self.addMousePointer.call(this, arguments); + /** + * + * @method addHoverEvent + * @returns {Function} + */ + addHoverEvent() { + let self = this; + let isClickable = this.listenerCount('click') > 0; + let addEvent = this.addEvent; + let $el = this.handler.el; + if (!this.handler.highlight) { + this.handler.highlight = self.highlight; } - self.handler.highlight.call(this, $el); - self.emit('hover', self.eventResponse(d, i)); - } - - return addEvent('mouseover', hover); - }; - - /** - * - * @method addMouseoutEvent - * @returns {Function} - */ - Dispatch.prototype.addMouseoutEvent = function () { - let self = this; - let addEvent = this.addEvent; - let $el = this.handler.el; - if (!this.handler.unHighlight) { - this.handler.unHighlight = self.unHighlight; - } - - function mouseout() { - self.handler.unHighlight.call(this, $el); - } - - return addEvent('mouseout', mouseout); - }; - - /** - * - * @method addClickEvent - * @returns {Function} - */ - Dispatch.prototype.addClickEvent = function () { - let self = this; - let addEvent = this.addEvent; - - function click(d, i) { - self.emit('click', self.eventResponse(d, i)); - } + function hover(d, i) { + // Add pointer if item is clickable + if (isClickable) { + self.addMousePointer.call(this, arguments); + } - return addEvent('click', click); - }; + self.handler.highlight.call(this, $el); + self.emit('hover', self.eventResponse(d, i)); + } - /** - * Determine if we will allow brushing - * - * @method allowBrushing - * @returns {Boolean} - */ - Dispatch.prototype.allowBrushing = function () { - let xAxis = this.handler.xAxis; - // Don't allow brushing for time based charts from non-time-based indices - let hasTimeField = this.handler.vis._attr.hasTimeField; + return addEvent('mouseover', hover); + }; - return Boolean(hasTimeField && xAxis.ordered && xAxis.xScale && _.isFunction(xAxis.xScale.invert)); - }; + /** + * + * @method addMouseoutEvent + * @returns {Function} + */ + addMouseoutEvent() { + let self = this; + let addEvent = this.addEvent; + let $el = this.handler.el; + if (!this.handler.unHighlight) { + this.handler.unHighlight = self.unHighlight; + } - /** - * Determine if brushing is currently enabled - * - * @method isBrushable - * @returns {Boolean} - */ - Dispatch.prototype.isBrushable = function () { - return this.allowBrushing() && this.listenerCount('brush') > 0; - }; + function mouseout() { + self.handler.unHighlight.call(this, $el); + } - /** - * - * @param svg - * @returns {Function} - */ - Dispatch.prototype.addBrushEvent = function (svg) { - if (!this.isBrushable()) return; + return addEvent('mouseout', mouseout); + }; - let xScale = this.handler.xAxis.xScale; - let yScale = this.handler.xAxis.yScale; - let brush = this.createBrush(xScale, svg); + /** + * + * @method addClickEvent + * @returns {Function} + */ + addClickEvent() { + let self = this; + let addEvent = this.addEvent; + + function click(d, i) { + self.emit('click', self.eventResponse(d, i)); + } - function brushEnd() { - if (!validBrushClick(d3.event)) return; + return addEvent('click', click); + }; - let bar = d3.select(this); - let startX = d3.mouse(svg.node()); - let startXInv = xScale.invert(startX[0]); + /** + * Determine if we will allow brushing + * + * @method allowBrushing + * @returns {Boolean} + */ + allowBrushing() { + let xAxis = this.handler.xAxis; + // Don't allow brushing for time based charts from non-time-based indices + let hasTimeField = this.handler.vis._attr.hasTimeField; + + return Boolean(hasTimeField && xAxis.ordered && xAxis.xScale && _.isFunction(xAxis.xScale.invert)); + }; - // Reset the brush value - brush.extent([startXInv, startXInv]); + /** + * Determine if brushing is currently enabled + * + * @method isBrushable + * @returns {Boolean} + */ + isBrushable() { + return this.allowBrushing() && this.listenerCount('brush') > 0; + }; - // Magic! - // Need to call brush on svg to see brush when brushing - // while on top of bars. - // Need to call brush on bar to allow the click event to be registered - svg.call(brush); - bar.call(brush); - } + /** + * + * @param svg + * @returns {Function} + */ + addBrushEvent(svg) { + if (!this.isBrushable()) return; + + let xScale = this.handler.xAxis.xScale; + let yScale = this.handler.xAxis.yScale; + let brush = this.createBrush(xScale, svg); + + function brushEnd() { + if (!validBrushClick(d3.event)) return; + + let bar = d3.select(this); + let startX = d3.mouse(svg.node()); + let startXInv = xScale.invert(startX[0]); + + // Reset the brush value + brush.extent([startXInv, startXInv]); + + // Magic! + // Need to call brush on svg to see brush when brushing + // while on top of bars. + // Need to call brush on bar to allow the click event to be registered + svg.call(brush); + bar.call(brush); + } - return this.addEvent('mousedown', brushEnd); - }; + return this.addEvent('mousedown', brushEnd); + }; - /** - * Mouseover Behavior - * - * @method addMousePointer - * @returns {D3.Selection} - */ - Dispatch.prototype.addMousePointer = function () { - return d3.select(this).style('cursor', 'pointer'); - }; + /** + * Mouseover Behavior + * + * @method addMousePointer + * @returns {D3.Selection} + */ + addMousePointer() { + return d3.select(this).style('cursor', 'pointer'); + }; - /** - * Mouseover Behavior - * - * @param element {D3.Selection} - * @method highlight - */ - Dispatch.prototype.highlight = function (element) { - let label = this.getAttribute('data-label'); - if (!label) return; - //Opacity 1 is needed to avoid the css application - $('[data-label]', element.parentNode).css('opacity', 1).not( - function (els, el) { return `${$(el).data('label')}` === label;} - ).css('opacity', 0.5); - }; + /** + * Mouseover Behavior + * + * @param element {D3.Selection} + * @method highlight + */ + highlight(element) { + let label = this.getAttribute('data-label'); + if (!label) return; + //Opacity 1 is needed to avoid the css application + $('[data-label]', element.parentNode).css('opacity', 1).not( + function (els, el) { + return `${$(el).data('label')}` === label; + } + ).css('opacity', 0.5); + }; - /** - * Mouseout Behavior - * - * @param element {D3.Selection} - * @method unHighlight - */ - Dispatch.prototype.unHighlight = function (element) { - $('[data-label]', element.parentNode).css('opacity', 1); - }; + /** + * Mouseout Behavior + * + * @param element {D3.Selection} + * @method unHighlight + */ + unHighlight(element) { + $('[data-label]', element.parentNode).css('opacity', 1); + }; - /** - * Adds D3 brush to SVG and returns the brush function - * - * @param xScale {Function} D3 xScale function - * @param svg {HTMLElement} Reference to SVG - * @returns {*} Returns a D3 brush function and a SVG with a brush group attached - */ - Dispatch.prototype.createBrush = function (xScale, svg) { - let self = this; - let attr = self.handler._attr; - let height = attr.height; - let margin = attr.margin; - - // Brush scale - let brush = d3.svg.brush() - .x(xScale) - .on('brushend', function brushEnd() { - - // Assumes data is selected at the chart level - // In this case, the number of data objects should always be 1 - let data = d3.select(this).data()[0]; - let isTimeSeries = (data.ordered && data.ordered.date); - - // Allows for brushing on d3.scale.ordinal() - let selected = xScale.domain().filter(function (d) { - return (brush.extent()[0] <= xScale(d)) && (xScale(d) <= brush.extent()[1]); - }); - let range = isTimeSeries ? brush.extent() : selected; - - return self.emit('brush', { - range: range, - config: attr, - e: d3.event, - data: data - }); - }); - - // 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 - let 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); + /** + * Adds D3 brush to SVG and returns the brush function + * + * @param xScale {Function} D3 xScale function + * @param svg {HTMLElement} Reference to SVG + * @returns {*} Returns a D3 brush function and a SVG with a brush group attached + */ + createBrush(xScale, svg) { + let self = this; + let attr = self.handler._attr; + let height = attr.height; + let margin = attr.margin; + + // Brush scale + let brush = d3.svg.brush() + .x(xScale) + .on('brushend', function brushEnd() { + + // Assumes data is selected at the chart level + // In this case, the number of data objects should always be 1 + let data = d3.select(this).data()[0]; + let isTimeSeries = (data.ordered && data.ordered.date); + + // Allows for brushing on d3.scale.ordinal() + let selected = xScale.domain().filter(function (d) { + return (brush.extent()[0] <= xScale(d)) && (xScale(d) <= brush.extent()[1]); + }); + let range = isTimeSeries ? brush.extent() : selected; + + return self.emit('brush', { + range: range, + config: attr, + e: d3.event, + data: data + }); }); - }) - .selectAll('rect') - .attr('height', height - margin.top - margin.bottom); - return brush; - } - }; + // 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 + let 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); + + return brush; + } + }; + } function validBrushClick(event) { return event.button === 0; diff --git a/src/ui/public/vislib/lib/handler/handler.js b/src/ui/public/vislib/lib/handler/handler.js index a74c62793186..d11f28b3d532 100644 --- a/src/ui/public/vislib/lib/handler/handler.js +++ b/src/ui/public/vislib/lib/handler/handler.js @@ -18,195 +18,192 @@ export default function HandlerBaseClass(Private) { * @param opts {Object} Reference to Visualization constructors needed to * create the visualization */ - function Handler(vis, opts) { - if (!(this instanceof Handler)) { - return new Handler(vis, opts); - } + 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; - this.charts = []; - - this._attr = _.defaults(vis._attr || {}, { - 'margin' : { top: 10, right: 3, bottom: 5, left: 3 } - }); - - this.xAxis = opts.xAxis; - this.yAxis = opts.yAxis; - this.chartTitle = opts.chartTitle; - this.axisTitle = opts.axisTitle; - this.alerts = opts.alerts; - - this.layout = new Layout(vis.el, vis.data, vis._attr.type, opts); - this.binder = new Binder(); - this.renderArray = _.filter([ - this.layout, - this.axisTitle, - this.chartTitle, - this.alerts, - this.xAxis, - this.yAxis, - ], Boolean); - - // memoize so that the same function is returned every time, - // allowing us to remove/re-add the same function - this.getProxyHandler = _.memoize(function (event) { - let self = this; - return function (e) { - self.vis.emit(event, e); - }; - }); - } + this.data = opts.data || new Data(vis.data, vis._attr, vis.uiState); + this.vis = vis; + this.el = vis.el; + this.ChartClass = vis.ChartClass; + this.charts = []; - /** - * Validates whether data is actually present in the data object - * used to render the Vis. Throws a no results error if data is not - * present. - * - * @private - */ - Handler.prototype._validateData = function () { - let dataType = this.data.type; - - if (!dataType) { - throw new errors.NoResults(); - } - }; + this._attr = _.defaults(vis._attr || {}, { + 'margin': {top: 10, right: 3, bottom: 5, left: 3} + }); - /** - * Renders the constructors that create the visualization, - * including the chart constructor - * - * @method render - * @returns {HTMLElement} With the visualization child element - */ - Handler.prototype.render = function () { - let self = this; - let charts = this.charts = []; - let selection = d3.select(this.el); + this.xAxis = opts.xAxis; + this.yAxis = opts.yAxis; + this.chartTitle = opts.chartTitle; + this.axisTitle = opts.axisTitle; + this.alerts = opts.alerts; + + this.layout = new Layout(vis.el, vis.data, vis._attr.type, opts); + this.binder = new Binder(); + this.renderArray = _.filter([ + this.layout, + this.axisTitle, + this.chartTitle, + this.alerts, + this.xAxis, + this.yAxis, + ], Boolean); + + // memoize so that the same function is returned every time, + // allowing us to remove/re-add the same function + this.getProxyHandler = _.memoize(function (event) { + let self = this; + return function (e) { + self.vis.emit(event, e); + }; + }); - selection.selectAll('*').remove(); + /** + * Enables events, i.e. binds specific events to the chart + * object(s) `on` method. For example, `click` or `mousedown` events. + * + * @method enable + * @param event {String} Event type + * @param chart {Object} Chart + * @returns {*} + */ + this.enable = this.chartEventProxyToggle('on'); + + /** + * Disables events for all charts + * + * @method disable + * @param event {String} Event type + * @param chart {Object} Chart + * @returns {*} + */ + this.disable = this.chartEventProxyToggle('off'); + } - this._validateData(); - this.renderArray.forEach(function (property) { - if (typeof property.render === 'function') { - property.render(); + /** + * Validates whether data is actually present in the data object + * used to render the Vis. Throws a no results error if data is not + * present. + * + * @private + */ + _validateData() { + let dataType = this.data.type; + + if (!dataType) { + throw new errors.NoResults(); } - }); - - // render the chart(s) - selection.selectAll('.chart') - .each(function (chartData) { - let chart = new self.ChartClass(self, this, chartData); - - self.vis.activeEvents().forEach(function (event) { - self.enable(event, chart); - }); - - charts.push(chart); - chart.render(); - }); - }; + }; + /** + * Renders the constructors that create the visualization, + * including the chart constructor + * + * @method render + * @returns {HTMLElement} With the visualization child element + */ + render() { + let self = this; + let charts = this.charts = []; + let selection = d3.select(this.el); - /** - * Enables events, i.e. binds specific events to the chart - * object(s) `on` method. For example, `click` or `mousedown` events. - * - * @method enable - * @param event {String} Event type - * @param chart {Object} Chart - * @returns {*} - */ - Handler.prototype.enable = chartEventProxyToggle('on'); + selection.selectAll('*').remove(); - /** - * Disables events for all charts - * - * @method disable - * @param event {String} Event type - * @param chart {Object} Chart - * @returns {*} - */ - Handler.prototype.disable = chartEventProxyToggle('off'); + this._validateData(); + this.renderArray.forEach(function (property) { + if (typeof property.render === 'function') { + property.render(); + } + }); + // render the chart(s) + selection.selectAll('.chart') + .each(function (chartData) { + let chart = new self.ChartClass(self, this, chartData); - function chartEventProxyToggle(method) { - return function (event, chart) { - let proxyHandler = this.getProxyHandler(event); + self.vis.activeEvents().forEach(function (event) { + self.enable(event, chart); + }); - _.each(chart ? [chart] : this.charts, function (chart) { - chart.events[method](event, proxyHandler); - }); + charts.push(chart); + chart.render(); + }); }; - } - /** - * Removes all DOM elements from the HTML element provided - * - * @method removeAll - * @param el {HTMLElement} Reference to the HTML Element that - * contains the chart - * @returns {D3.Selection|D3.Transition.Transition} With the chart - * child element removed - */ - Handler.prototype.removeAll = function (el) { - return d3.select(el).selectAll('*').remove(); - }; + chartEventProxyToggle(method) { + return function (event, chart) { + let proxyHandler = this.getProxyHandler(event); - /** - * Displays an error message in the DOM - * - * @method error - * @param message {String} Error message to display - * @returns {HTMLElement} Displays the input message - */ - Handler.prototype.error = function (message) { - this.removeAll(this.el); - - let div = d3.select(this.el) - .append('div') - // class name needs `chart` in it for the polling checkSize function - // to continuously call render on resize - .attr('class', 'visualize-error chart error'); - - if (message === 'No results found') { - div.append('div') - .attr('class', 'text-center visualize-error visualize-chart ng-scope') - .append('div').attr('class', 'item top') - .append('div').attr('class', 'item') - .append('h2').html('') - .append('h4').text(message); - - div.append('div').attr('class', 'item bottom'); - return div; + _.each(chart ? [chart] : this.charts, function (chart) { + chart.events[method](event, proxyHandler); + }); + }; } - return div.append('h4').text(message); - }; - - /** - * Destroys all the charts in the visualization - * - * @method destroy - */ - Handler.prototype.destroy = function () { - this.binder.destroy(); + /** + * Removes all DOM elements from the HTML element provided + * + * @method removeAll + * @param el {HTMLElement} Reference to the HTML Element that + * contains the chart + * @returns {D3.Selection|D3.Transition.Transition} With the chart + * child element removed + */ + removeAll(el) { + return d3.select(el).selectAll('*').remove(); + }; - this.renderArray.forEach(function (renderable) { - if (_.isFunction(renderable.destroy)) { - renderable.destroy(); + /** + * Displays an error message in the DOM + * + * @method error + * @param message {String} Error message to display + * @returns {HTMLElement} Displays the input message + */ + error(message) { + this.removeAll(this.el); + + let div = d3.select(this.el) + .append('div') + // class name needs `chart` in it for the polling checkSize function + // to continuously call render on resize + .attr('class', 'visualize-error chart error'); + + if (message === 'No results found') { + div.append('div') + .attr('class', 'text-center visualize-error visualize-chart ng-scope') + .append('div').attr('class', 'item top') + .append('div').attr('class', 'item') + .append('h2').html('') + .append('h4').text(message); + + div.append('div').attr('class', 'item bottom'); + return div; } - }); - this.charts.splice(0).forEach(function (chart) { - if (_.isFunction(chart.destroy)) { - chart.destroy(); - } - }); - }; + return div.append('h4').text(message); + }; + + /** + * Destroys all the charts in the visualization + * + * @method destroy + */ + destroy() { + this.binder.destroy(); + + this.renderArray.forEach(function (renderable) { + if (_.isFunction(renderable.destroy)) { + renderable.destroy(); + } + }); + + this.charts.splice(0).forEach(function (chart) { + if (_.isFunction(chart.destroy)) { + chart.destroy(); + } + }); + }; + } return Handler; }; diff --git a/src/ui/public/vislib/lib/layout/layout.js b/src/ui/public/vislib/lib/layout/layout.js index e0141dc71ce5..39a314bfd8e4 100644 --- a/src/ui/public/vislib/lib/layout/layout.js +++ b/src/ui/public/vislib/lib/layout/layout.js @@ -21,133 +21,131 @@ export default function LayoutFactory(Private) { * @param data {Object} Elasticsearch query results for this specific chart * @param chartType {Object} Reference to chart functions, i.e. Pie */ - function Layout(el, data, chartType, opts) { - if (!(this instanceof Layout)) { - return new Layout(el, data, chartType, opts); + class Layout { + constructor(el, data, chartType, opts) { + this.el = el; + this.data = data; + this.opts = opts; + this.layoutType = layoutType[chartType](this.el, this.data); } - this.el = el; - this.data = data; - this.opts = opts; - this.layoutType = layoutType[chartType](this.el, this.data); - } - - // Render the layout - /** - * Renders visualization HTML layout - * Remove all elements from the current visualization and creates the layout - * - * @method render - */ - Layout.prototype.render = function () { - this.removeAll(this.el); - this.createLayout(this.layoutType); - }; - - /** - * Create the layout based on the json array provided - * for each object in the layout array, call the layout function - * - * @method createLayout - * @param arr {Array} Json array - * @returns {*} Creates the visualization layout - */ - Layout.prototype.createLayout = function (arr) { - let self = this; - - return _.each(arr, function (obj) { - self.layout(obj); - }); - }; - - /** - * Appends a DOM element based on the object keys - * check to see if reference to DOM element is string but not class selector - * Create a class selector - * - * @method layout - * @param obj {Object} Instructions for creating the layout of a DOM Element - * @returns {*} DOM Element - */ - Layout.prototype.layout = function (obj) { - if (!obj.parent) { - throw new Error('No parent element provided'); - } - - if (!obj.type) { - throw new Error('No element type provided'); - } - - if (typeof obj.type !== 'string') { - throw new Error(obj.type + ' must be a string'); - } - - if (typeof obj.parent === 'string' && obj.parent.charAt(0) !== '.') { - obj.parent = '.' + obj.parent; - } - - let childEl = this.appendElem(obj.parent, obj.type, obj.class); - - if (obj.datum) { - childEl.datum(obj.datum); - } - - if (obj.splits) { - childEl.call(obj.splits, obj.parent, this.opts); - } - - if (obj.children) { - let newParent = childEl[0][0]; - - _.forEach(obj.children, function (obj) { - if (!obj.parent) { - obj.parent = newParent; - } + // Render the layout + /** + * Renders visualization HTML layout + * Remove all elements from the current visualization and creates the layout + * + * @method render + */ + render() { + this.removeAll(this.el); + this.createLayout(this.layoutType); + }; + + /** + * Create the layout based on the json array provided + * for each object in the layout array, call the layout function + * + * @method createLayout + * @param arr {Array} Json array + * @returns {*} Creates the visualization layout + */ + createLayout(arr) { + let self = this; + + return _.each(arr, function (obj) { + self.layout(obj); }); - - this.createLayout(obj.children); - } - - return childEl; - }; - - /** - * Appends a `type` of DOM element to `el` and gives it a class name attribute `className` - * - * @method appendElem - * @param el {HTMLElement} Reference to a DOM Element - * @param type {String} DOM element type - * @param className {String} CSS class name - * @returns {*} Reference to D3 Selection - */ - Layout.prototype.appendElem = function (el, type, className) { - if (!el || !type || !className) { - throw new Error('Function requires that an el, type, and class be provided'); - } - - if (typeof el === 'string') { - // Create a DOM reference with a d3 selection - // Need to make sure that the `el` is bound to this object - // to prevent it from being appended to another Layout - el = d3.select(this.el) - .select(el)[0][0]; - } - - return d3.select(el) - .append(type) - .attr('class', className); - }; - - /** - * Removes all DOM elements from DOM element - * - * @method removeAll - * @param el {HTMLElement} Reference to DOM element - * @returns {D3.Selection|D3.Transition.Transition} Reference to an empty DOM element - */ - Layout.prototype.removeAll = function (el) { - return d3.select(el).selectAll('*').remove(); - }; + }; + + /** + * Appends a DOM element based on the object keys + * check to see if reference to DOM element is string but not class selector + * Create a class selector + * + * @method layout + * @param obj {Object} Instructions for creating the layout of a DOM Element + * @returns {*} DOM Element + */ + layout(obj) { + if (!obj.parent) { + throw new Error('No parent element provided'); + } + + if (!obj.type) { + throw new Error('No element type provided'); + } + + if (typeof obj.type !== 'string') { + throw new Error(obj.type + ' must be a string'); + } + + if (typeof obj.parent === 'string' && obj.parent.charAt(0) !== '.') { + obj.parent = '.' + obj.parent; + } + + let childEl = this.appendElem(obj.parent, obj.type, obj.class); + + if (obj.datum) { + childEl.datum(obj.datum); + } + + if (obj.splits) { + childEl.call(obj.splits, obj.parent, this.opts); + } + + if (obj.children) { + let newParent = childEl[0][0]; + + _.forEach(obj.children, function (obj) { + if (!obj.parent) { + obj.parent = newParent; + } + }); + + this.createLayout(obj.children); + } + + return childEl; + }; + + /** + * Appends a `type` of DOM element to `el` and gives it a class name attribute `className` + * + * @method appendElem + * @param el {HTMLElement} Reference to a DOM Element + * @param type {String} DOM element type + * @param className {String} CSS class name + * @returns {*} Reference to D3 Selection + */ + appendElem(el, type, className) { + if (!el || !type || !className) { + throw new Error('Function requires that an el, type, and class be provided'); + } + + if (typeof el === 'string') { + // Create a DOM reference with a d3 selection + // Need to make sure that the `el` is bound to this object + // to prevent it from being appended to another Layout + el = d3.select(this.el) + .select(el)[0][0]; + } + + return d3.select(el) + .append(type) + .attr('class', className); + }; + + /** + * Removes all DOM elements from DOM element + * + * @method removeAll + * @param el {HTMLElement} Reference to DOM element + * @returns {D3.Selection|D3.Transition.Transition} Reference to an empty DOM element + */ + removeAll(el) { + return d3.select(el).selectAll('*').remove(); + }; + } return Layout; }; diff --git a/src/ui/public/vislib/lib/x_axis.js b/src/ui/public/vislib/lib/x_axis.js index 5e71a38e2e54..a2cac008d015 100644 --- a/src/ui/public/vislib/lib/x_axis.js +++ b/src/ui/public/vislib/lib/x_axis.js @@ -15,512 +15,510 @@ export default function XAxisFactory(Private) { * @param args {{el: (HTMLElement), xValues: (Array), ordered: (Object|*), * xAxisFormatter: (Function), _attr: (Object|*)}} */ - _.class(XAxis).inherits(ErrorHandler); - function XAxis(args) { - if (!(this instanceof XAxis)) { - return new XAxis(args); + 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 || {}); } - 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 - */ - XAxis.prototype.render = function () { - 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 - */ - XAxis.prototype.getScale = function () { - let 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 - */ - XAxis.prototype.getDomain = function (scale) { - let 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 - */ - XAxis.prototype.getTimeDomain = function (scale, data) { - return scale.domain([this.minExtent(data), this.maxExtent(data)]); - }; - - XAxis.prototype.minExtent = function (data) { - return this._calculateExtent(data || this.xValues, 'min'); - }; - - XAxis.prototype.maxExtent = function (data) { - return this._calculateExtent(data || this.xValues, 'max'); - }; - - /** - * - * @param data - * @param extent - */ - XAxis.prototype._calculateExtent = function (data, extent) { - let ordered = this.ordered; - let 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 - */ - XAxis.prototype.addInterval = function (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 - */ - XAxis.prototype.subtractInterval = function (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 - */ - XAxis.prototype.modByInterval = function (x, n) { - let ordered = this.ordered; - if (!ordered) return x; - let interval = ordered.interval; - if (!interval) return x; - - if (!ordered.date) { - return x += (ordered.interval * n); - } + /** + * 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()); + }; - let y = moment(x); - let method = n > 0 ? 'add' : 'subtract'; + /** + * 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() { + let ordered = this.ordered; + + if (ordered && ordered.date) { + return d3.time.scale.utc(); + } + return d3.scale.ordinal(); + }; - _.times(Math.abs(n), function () { - y[method](interval); - }); + /** + * 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) { + let ordered = this.ordered; + + if (ordered && ordered.date) { + return this.getTimeDomain(scale, this.xValues); + } + return this.getOrdinalDomain(scale, this.xValues); + }; - return y.valueOf(); - }; + /** + * 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)]); + }; - /** - * 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 - */ - XAxis.prototype.getOrdinalDomain = function (scale, xValues) { - return scale.domain(xValues); - }; + minExtent(data) { + return this._calculateExtent(data || this.xValues, 'min'); + }; - /** - * 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 - */ - XAxis.prototype.getRange = function (domain, width) { - let ordered = this.ordered; + maxExtent(data) { + return this._calculateExtent(data || this.xValues, 'max'); + }; - if (ordered && ordered.date) { - return domain.range([0, width]); - } - return domain.rangeBands([0, width], 0.1); - }; + /** + * + * @param data + * @param extent + */ + _calculateExtent(data, extent) { + let ordered = this.ordered; + let opts = [ordered[extent]]; + + let point = d3[extent](data); + if (this.expandLastBucket && extent === 'max') { + point = this.addInterval(point); + } + opts.push(point); - /** - * Return the x axis scale - * - * @method getXScale - * @param width {Number} HTML Element width - * @returns {*} D3 x scale function - */ - XAxis.prototype.getXScale = function (width) { - let domain = this.getDomain(this.getScale()); + return d3[extent](opts.reduce(function (opts, v) { + if (!_.isNumber(v)) v = +v; + if (!isNaN(v)) opts.push(v); + return opts; + }, [])); + }; - return this.getRange(domain, width); - }; + /** + * 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); + }; - /** - * Creates d3 xAxis function - * - * @method getXAxis - * @param width {Number} HTML Element width - */ - XAxis.prototype.getXAxis = function (width) { - this.xScale = this.getXScale(width); + /** + * 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); + }; - if (!this.xScale || _.isNaN(this.xScale)) { - throw new Error('xScale is ' + this.xScale); - } + /** + * 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) { + let ordered = this.ordered; + if (!ordered) return x; + let interval = ordered.interval; + if (!interval) return x; + + if (!ordered.date) { + return x += (ordered.interval * n); + } - this.xAxis = d3.svg.axis() - .scale(this.xScale) - .ticks(10) - .tickFormat(this.xAxisFormatter) - .orient('bottom'); - }; + let y = moment(x); + let method = n > 0 ? 'add' : 'subtract'; - /** - * Renders the x axis - * - * @method draw - * @returns {Function} Renders the x axis to a D3 selection - */ - XAxis.prototype.draw = function () { - let self = this; - let div; - let width; - let height; - let svg; - let parentWidth; - let n; - this._attr.isRotated = false; - - return function (selection) { - n = selection[0].length; - parentWidth = $(self.el) - .find('.x-axis-div-wrapper') - .width(); + _.times(Math.abs(n), function () { + y[method](interval); + }); - selection.each(function () { + return y.valueOf(); + }; - div = d3.select(this); - width = parentWidth / n; - height = $(this.parentElement).height(); + /** + * 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); + }; - self.validateWidthandHeight(width, height); + /** + * 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) { + let ordered = this.ordered; + + if (ordered && ordered.date) { + return domain.range([0, width]); + } + return domain.rangeBands([0, width], 0.1); + }; - self.getXAxis(width); + /** + * Return the x axis scale + * + * @method getXScale + * @param width {Number} HTML Element width + * @returns {*} D3 x scale function + */ + getXScale(width) { + let domain = this.getDomain(this.getScale()); + + return this.getRange(domain, width); + }; - svg = div.append('svg') - .attr('width', width) - .attr('height', height); + /** + * 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); + } - svg.append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0,0)') - .call(self.xAxis); - }); + this.xAxis = d3.svg.axis() + .scale(this.xScale) + .ticks(10) + .tickFormat(this.xAxisFormatter) + .orient('bottom'); + }; - selection.call(self.filterOrRotate()); + /** + * Renders the x axis + * + * @method draw + * @returns {Function} Renders the x axis to a D3 selection + */ + draw() { + let self = this; + let div; + let width; + let height; + let svg; + let parentWidth; + let n; + this._attr.isRotated = false; + + return function (selection) { + n = selection[0].length; + parentWidth = $(self.el) + .find('.x-axis-div-wrapper') + .width(); + + selection.each(function () { + + div = d3.select(this); + width = parentWidth / n; + height = $(this.parentElement).height(); + + self.validateWidthandHeight(width, height); + + self.getXAxis(width); + + 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 - */ - XAxis.prototype.filterOrRotate = function () { - let self = this; - let ordered = self.ordered; - let axis; - let labels; + /** + * 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() { + let self = this; + let ordered = self.ordered; + let axis; + let labels; + + return function (selection) { + selection.each(function () { + axis = d3.select(this); + labels = axis.selectAll('.tick text'); + if (ordered && ordered.date) { + axis.call(self.filterAxisLabels()); + } else { + axis.call(self.rotateAxisLabels()); + } + }); + + self.updateXaxisHeight(); + + selection.call(self.fitTitles()); + + }; + }; - return function (selection) { - selection.each(function () { - axis = d3.select(this); - labels = axis.selectAll('.tick text'); - if (ordered && ordered.date) { - axis.call(self.filterAxisLabels()); - } else { - axis.call(self.rotateAxisLabels()); + /** + * Rotate the axis tick labels within selection + * + * @returns {Function} Rotates x axis tick labels of a D3 selection + */ + rotateAxisLabels() { + let self = this; + let text; + let barWidth = self.xScale.rangeBand(); + let maxRotatedLength = 120; + let xAxisPadding = 15; + let svg; + let lengths = []; + let length; + self._attr.isRotated = false; + + return function (selection) { + text = selection.selectAll('.tick text'); + + text.each(function textWidths() { + lengths.push(d3.select(this).node().getBBox().width); + }); + length = _.max(lengths); + self._attr.xAxisLabelHt = length + xAxisPadding; + + // if longer than bar width, rotate + if (length > barWidth) { + self._attr.isRotated = true; } - }); - self.updateXaxisHeight(); - - selection.call(self.fitTitles()); + // 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); + } + }; }; - }; - /** - * Rotate the axis tick labels within selection - * - * @returns {Function} Rotates x axis tick labels of a D3 selection - */ - XAxis.prototype.rotateAxisLabels = function () { - let self = this; - let text; - let barWidth = self.xScale.rangeBand(); - let maxRotatedLength = 120; - let xAxisPadding = 15; - let svg; - let lengths = []; - let length; - self._attr.isRotated = false; - - return function (selection) { - text = selection.selectAll('.tick text'); - - text.each(function textWidths() { - lengths.push(d3.select(this).node().getBBox().width); - }); - 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) { + let node = d3.select(text).node(); + let str = $(node).text(); + let width = node.getBBox().width; + let chars = str.length; + let pxPerChar = width / chars; + let endChar = 0; + let 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; }; - }; - - /** - * Returns a string that is truncated to fit size - * - * @method truncateLabel - * @param text {HTMLElement} - * @param size {Number} - * @returns {*|jQuery} - */ - XAxis.prototype.truncateLabel = function (text, size) { - let node = d3.select(text).node(); - let str = $(node).text(); - let width = node.getBBox().width; - let chars = str.length; - let pxPerChar = width / chars; - let endChar = 0; - let 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} - */ - XAxis.prototype.filterAxisLabels = function () { - let self = this; - let startX = 0; - let maxW; - let par; - let myX; - let myWidth; - let halfWidth; - let 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(); - } - }); + /** + * 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() { + let self = this; + let startX = 0; + let maxW; + let par; + let myX; + let myWidth; + let halfWidth; + let 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} - */ - XAxis.prototype.fitTitles = function () { - let visEls = $('.vis-wrapper'); - let xAxisChartTitle; - let yAxisChartTitle; - let text; - let titles; - return function () { - - visEls.each(function () { - let visEl = d3.select(this); - let $visEl = $(this); - let xAxisTitle = $visEl.find('.x-axis-title'); - let 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) + /** + * 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() { + let visEls = $('.vis-wrapper'); + let xAxisChartTitle; + let yAxisChartTitle; + let text; + let titles; + + return function () { + + visEls.each(function () { + let visEl = d3.select(this); + let $visEl = $(this); + let xAxisTitle = $visEl.find('.x-axis-title'); + let 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)'); - }); - } - - 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) + 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 - */ - XAxis.prototype.updateXaxisHeight = function () { - let selection = d3.select(this.el).selectAll('.vis-wrapper'); + /** + * Appends div to make .y-axis-spacer-block + * match height of .x-axis-wrapper + * + * @method updateXaxisHeight + */ + updateXaxisHeight() { + let selection = d3.select(this.el).selectAll('.vis-wrapper'); - selection.each(function () { - let visEl = d3.select(this); + selection.each(function () { + let 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'); - } - let xAxisHt = visEl.select('.x-axis-wrapper').style('height'); + if (visEl.select('.inner-spacer-block').node() === null) { + visEl.select('.y-axis-spacer-block') + .append('div') + .attr('class', 'inner-spacer-block'); + } + let xAxisHt = visEl.select('.x-axis-wrapper').style('height'); - visEl.select('.inner-spacer-block').style('height', xAxisHt); - }); + 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 index 70eaa7419da3..a0d53a570451 100644 --- a/src/ui/public/vislib/lib/y_axis.js +++ b/src/ui/public/vislib/lib/y_axis.js @@ -14,221 +14,223 @@ export default function YAxisFactory(Private) { * @constructor * @param args {{el: (HTMLElement), yMax: (Number), _attr: (Object|*)}} */ - _.class(YAxis).inherits(ErrorHandler); - function YAxis(args) { - 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 - */ - YAxis.prototype.render = function () { - d3.select(this.el).selectAll('.y-axis-div').call(this.draw()); - }; + 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()); + }; - YAxis.prototype._isPercentage = function () { - return (this._attr.mode === 'percentage'); - }; + _isPercentage() { + return (this._attr.mode === 'percentage'); + }; - YAxis.prototype._isUserDefined = function () { - return (this._attr.setYExtents); - }; + _isUserDefined() { + return (this._attr.setYExtents); + }; - YAxis.prototype._isYExtents = function () { - return (this._attr.defaultYExtents); - }; + _isYExtents() { + return (this._attr.defaultYExtents); + }; - YAxis.prototype._validateUserExtents = function (domain) { - let self = this; + _validateUserExtents(domain) { + let self = this; - return domain.map(function (val) { - val = parseInt(val, 10); + 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; - }); - }; + if (isNaN(val)) throw new Error(val + ' is not a valid number'); + if (self._isPercentage() && self._attr.setYExtents) return val / 100; + return val; + }); + }; - YAxis.prototype._getExtents = function (domain) { - let min = domain[0]; - let max = domain[1]; + _getExtents(domain) { + let min = domain[0]; + let 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; - }; + 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; + }; - YAxis.prototype._throwCustomError = function (message) { - throw new Error(message); - }; + _throwCustomError(message) { + throw new Error(message); + }; - YAxis.prototype._throwLogScaleValuesError = function () { - throw new errors.InvalidLogScaleValues(); - }; + _throwLogScaleValuesError() { + throw new errors.InvalidLogScaleValues(); + }; - /** - * Returns the appropriate D3 scale - * - * @param fnName {String} D3 scale - * @returns {*} - */ - YAxis.prototype._getScaleType = function (fnName) { - if (fnName === 'square root') fnName = 'sqrt'; // Rename 'square root' to 'sqrt' - fnName = fnName || 'linear'; + /** + * 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'); + if (typeof d3.scale[fnName] !== 'function') return this._throwCustomError('YAxis.getScaleType: ' + fnName + ' is not a function'); - return d3.scale[fnName](); - }; + 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} - */ - YAxis.prototype._logDomain = function (min, max) { - if (min < 0 || max < 0) return this._throwLogScaleValuesError(); - return [1, max]; - }; + /** + * 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 - */ - YAxis.prototype.getYScale = function (height) { - let scale = this._getScaleType(this._attr.scale); - let 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; - }; - - YAxis.prototype.getScaleType = function () { - return this._attr.scale; - }; - - YAxis.prototype.tickFormat = function () { - let isPercentage = this._attr.mode === 'percentage'; - if (isPercentage) return d3.format('%'); - if (this.yAxisFormatter) return this.yAxisFormatter; - return d3.format('n'); - }; - - YAxis.prototype._validateYScale = function (yScale) { - if (!yScale || _.isNaN(yScale)) throw new Error('yScale is ' + yScale); - }; + /** + * Creates the d3 y scale function + * + * @method getYScale + * @param height {Number} DOM Element height + * @returns {D3.Scale.QuantitiveScale|*} D3 yScale function + */ + getYScale(height) { + let scale = this._getScaleType(this._attr.scale); + let 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; + }; - /** - * Creates the d3 y axis function - * - * @method getYAxis - * @param height {Number} DOM Element height - * @returns {D3.Svg.Axis|*} D3 yAxis function - */ - YAxis.prototype.getYAxis = function (height) { - let yScale = this.getYScale(height); - this._validateYScale(yScale); + getScaleType() { + return this._attr.scale; + }; - // Create the d3 yAxis function - this.yAxis = d3.svg.axis() - .scale(yScale) - .tickFormat(this.tickFormat(this.domain)) - .ticks(this.tickScale(height)) - .orient('left'); + tickFormat() { + let isPercentage = this._attr.mode === 'percentage'; + if (isPercentage) return d3.format('%'); + if (this.yAxisFormatter) return this.yAxisFormatter; + return d3.format('n'); + }; - return this.yAxis; - }; + _validateYScale(yScale) { + if (!yScale || _.isNaN(yScale)) throw new Error('yScale is ' + yScale); + }; - /** - * 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 - */ - YAxis.prototype.tickScale = function (height) { - let yTickScale = d3.scale.linear() - .clamp(true) - .domain([20, 40, 1000]) - .range([0, 3, 11]); + /** + * Creates the d3 y axis function + * + * @method getYAxis + * @param height {Number} DOM Element height + * @returns {D3.Svg.Axis|*} D3 yAxis function + */ + getYAxis(height) { + let 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; + }; - return Math.ceil(yTickScale(height)); - }; + /** + * 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) { + let 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 - */ - YAxis.prototype.draw = function () { - let self = this; - let margin = this._attr.margin; - let mode = this._attr.mode; - let isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette'); - - return function (selection) { - selection.each(function () { - let el = this; - - let div = d3.select(el); - let width = $(el).parent().width(); - let height = $(el).height(); - let adjustedHeight = height - margin.top - margin.bottom; - - // Validate whether width and height are not 0 or `NaN` - self.validateWidthandHeight(width, adjustedHeight); - - let yAxis = self.getYAxis(adjustedHeight); - - // The yAxis should not appear if mode is set to 'wiggle' or 'silhouette' - if (!isWiggleOrSilhouette) { - // Append svg and y axis - let 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); - - let container = svg.select('g.y.axis').node(); - if (container) { - let cWidth = Math.max(width, container.getBBox().width); - svg.attr('width', cWidth); - svg.select('g') - .attr('transform', 'translate(' + (cWidth - 2) + ',' + margin.top + ')'); + /** + * Renders the y axis to the visualization + * + * @method draw + * @returns {Function} Renders y axis to visualization + */ + draw() { + let self = this; + let margin = this._attr.margin; + let mode = this._attr.mode; + let isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette'); + + return function (selection) { + selection.each(function () { + let el = this; + + let div = d3.select(el); + let width = $(el).parent().width(); + let height = $(el).height(); + let adjustedHeight = height - margin.top - margin.bottom; + + // Validate whether width and height are not 0 or `NaN` + self.validateWidthandHeight(width, adjustedHeight); + + let yAxis = self.getYAxis(adjustedHeight); + + // The yAxis should not appear if mode is set to 'wiggle' or 'silhouette' + if (!isWiggleOrSilhouette) { + // Append svg and y axis + let 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); + + let container = svg.select('g.y.axis').node(); + if (container) { + let 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/vis.js b/src/ui/public/vislib/vis.js index 27569fa04c60..b64d2b34f9fb 100644 --- a/src/ui/public/vislib/vis.js +++ b/src/ui/public/vislib/vis.js @@ -23,164 +23,162 @@ export default function VisFactory(Private) { * @param $el {HTMLElement} jQuery selected HTML element * @param config {Object} Parameters that define the chart type and chart options */ - _.class(Vis).inherits(Events); - function Vis($el, config) { - if (!(this instanceof Vis)) { - return new Vis($el, config); - } - Vis.Super.apply(this, arguments); - this.el = $el.get ? $el.get(0) : $el; - this.binder = new Binder(); - this.ChartClass = chartTypes[config.type]; - this._attr = _.defaults({}, config || {}, { - legendOpen: true - }); - - // bind the resize function so it can be used as an event handler - this.resize = _.bind(this.resize, this); - this.resizeChecker = new ResizeChecker(this.el); - this.binder.on(this.resizeChecker, 'resize', this.resize); - } - - /** - * Renders the visualization - * - * @method render - * @param data {Object} Elasticsearch query results - */ - Vis.prototype.render = function (data, uiState) { - let chartType = this._attr.type; - - if (!data) { - throw new Error('No valid data!'); - } - - if (this.handler) { - this.data = null; - this._runOnHandler('destroy'); + class Vis extends Events { + constructor($el, config) { + 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 + }); + + // bind the resize function so it can be used as an event handler + this.resize = _.bind(this.resize, this); + this.resizeChecker = new ResizeChecker(this.el); + this.binder.on(this.resizeChecker, 'resize', this.resize); } - this.data = data; - - if (!this.uiState) { - this.uiState = uiState; - uiState.on('change', this._uiStateChangeHandler = () => this.render(this.data, this.uiState)); - } + /** + * Renders the visualization + * + * @method render + * @param data {Object} Elasticsearch query results + */ + render(data, uiState) { + let chartType = this._attr.type; + + if (!data) { + throw new Error('No valid data!'); + } - this.handler = handlerTypes[chartType](this) || handlerTypes.column(this); - this._runOnHandler('render'); - }; + if (this.handler) { + this.data = null; + this._runOnHandler('destroy'); + } - /** - * Resizes the visualization - * - * @method resize - */ - Vis.prototype.resize = function () { - if (!this.data) { - // TODO: need to come up with a solution for resizing when no data is available - return; - } + this.data = data; - if (this.handler && _.isFunction(this.handler.resize)) { - this._runOnHandler('resize'); - } else { - this.render(this.data, this.uiState); - } - }; + if (!this.uiState) { + this.uiState = uiState; + uiState.on('change', this._uiStateChangeHandler = () => this.render(this.data, this.uiState)); + } - Vis.prototype._runOnHandler = function (method) { - try { - this.handler[method](); - } catch (error) { + this.handler = handlerTypes[chartType](this) || handlerTypes.column(this); + this._runOnHandler('render'); + }; + + /** + * Resizes the visualization + * + * @method resize + */ + resize() { + if (!this.data) { + // TODO: need to come up with a solution for resizing when no data is available + return; + } - if (error instanceof errors.KbnError) { - error.displayToScreen(this.handler); + if (this.handler && _.isFunction(this.handler.resize)) { + this._runOnHandler('resize'); } else { - throw error; + this.render(this.data, this.uiState); } + }; - } - }; + _runOnHandler(method) { + try { + this.handler[method](); + } catch (error) { - /** - * Destroys the visualization - * Removes chart and all elements associated with it. - * Removes chart and all elements associated with it. - * Remove event listeners and pass destroy call down to owned objects. - * - * @method destroy - */ - Vis.prototype.destroy = function () { - let selection = d3.select(this.el).select('.vis-wrapper'); - - this.binder.destroy(); - this.resizeChecker.destroy(); - if (this.uiState) this.uiState.off('change', this._uiStateChangeHandler); - if (this.handler) this._runOnHandler('destroy'); - - selection.remove(); - selection = null; - }; - - /** - * Sets attributes on the visualization - * - * @method set - * @param name {String} An attribute name - * @param val {*} Value to which the attribute name is set - */ - Vis.prototype.set = function (name, val) { - this._attr[name] = val; - this.render(this.data, this.uiState); - }; - - /** - * Gets attributes from the visualization - * - * @method get - * @param name {String} An attribute name - * @returns {*} The value of the attribute name - */ - Vis.prototype.get = function (name) { - return this._attr[name]; - }; + if (error instanceof errors.KbnError) { + error.displayToScreen(this.handler); + } else { + throw error; + } - /** - * Turns on event listeners. - * - * @param event {String} - * @param listener{Function} - * @returns {*} - */ - Vis.prototype.on = function (event, listener) { - let first = this.listenerCount(event) === 0; - let ret = Events.prototype.on.call(this, event, listener); - let added = this.listenerCount(event) > 0; - - // if this is the first listener added for the event - // enable the event in the handler - if (first && added && this.handler) this.handler.enable(event); - - return ret; - }; - - /** - * Turns off event listeners. - * - * @param event {String} - * @param listener{Function} - * @returns {*} - */ - Vis.prototype.off = function (event, listener) { - let last = this.listenerCount(event) === 1; - let ret = Events.prototype.off.call(this, event, listener); - let removed = this.listenerCount(event) === 0; - - // Once all listeners are removed, disable the events in the handler - if (last && removed && this.handler) this.handler.disable(event); - return ret; - }; + } + }; + + /** + * Destroys the visualization + * Removes chart and all elements associated with it. + * Removes chart and all elements associated with it. + * Remove event listeners and pass destroy call down to owned objects. + * + * @method destroy + */ + destroy() { + let selection = d3.select(this.el).select('.vis-wrapper'); + + this.binder.destroy(); + this.resizeChecker.destroy(); + if (this.uiState) this.uiState.off('change', this._uiStateChangeHandler); + if (this.handler) this._runOnHandler('destroy'); + + selection.remove(); + selection = null; + }; + + /** + * Sets attributes on the visualization + * + * @method set + * @param name {String} An attribute name + * @param val {*} Value to which the attribute name is set + */ + set(name, val) { + this._attr[name] = val; + this.render(this.data, this.uiState); + }; + + /** + * Gets attributes from the visualization + * + * @method get + * @param name {String} An attribute name + * @returns {*} The value of the attribute name + */ + get(name) { + return this._attr[name]; + }; + + /** + * Turns on event listeners. + * + * @param event {String} + * @param listener{Function} + * @returns {*} + */ + on(event, listener) { + let first = this.listenerCount(event) === 0; + let ret = Events.prototype.on.call(this, event, listener); + let added = this.listenerCount(event) > 0; + + // if this is the first listener added for the event + // enable the event in the handler + if (first && added && this.handler) this.handler.enable(event); + + return ret; + }; + + /** + * Turns off event listeners. + * + * @param event {String} + * @param listener{Function} + * @returns {*} + */ + off(event, listener) { + let last = this.listenerCount(event) === 1; + let ret = Events.prototype.off.call(this, event, listener); + let removed = this.listenerCount(event) === 0; + + // Once all listeners are removed, disable the events in the handler + if (last && removed && this.handler) this.handler.disable(event); + return ret; + }; + } return Vis; }; diff --git a/src/ui/public/vislib/visualizations/_chart.js b/src/ui/public/vislib/visualizations/_chart.js index 4286df6b1627..b0cf07209827 100644 --- a/src/ui/public/vislib/visualizations/_chart.js +++ b/src/ui/public/vislib/visualizations/_chart.js @@ -18,80 +18,78 @@ export default function ChartBaseClass(Private) { * @param el {HTMLElement} HTML element to which the chart will be appended * @param chartData {Object} Elasticsearch query results for this specific chart */ - function Chart(handler, el, chartData) { - if (!(this instanceof Chart)) { - return new Chart(handler, el, chartData); - } + class Chart { + constructor(handler, el, chartData) { + this.handler = handler; + this.chartEl = el; + this.chartData = chartData; + this.tooltips = []; - this.handler = handler; - this.chartEl = el; - this.chartData = chartData; - this.tooltips = []; + let events = this.events = new Dispatch(handler); - let events = this.events = new Dispatch(handler); + if (_.get(this.handler, '_attr.addTooltip')) { + let $el = this.handler.el; + let formatter = this.handler.data.get('tooltipFormatter'); - if (_.get(this.handler, '_attr.addTooltip')) { - let $el = this.handler.el; - let formatter = this.handler.data.get('tooltipFormatter'); + // Add tooltip + this.tooltip = new Tooltip('chart', $el, formatter, events); + this.tooltips.push(this.tooltip); + } - // Add tooltip - this.tooltip = new Tooltip('chart', $el, formatter, events); - this.tooltips.push(this.tooltip); + this._attr = _.defaults(this.handler._attr || {}, {}); + this._addIdentifier = _.bind(this._addIdentifier, this); } - this._attr = _.defaults(this.handler._attr || {}, {}); - this._addIdentifier = _.bind(this._addIdentifier, this); - } + /** + * Renders the chart(s) + * + * @method render + * @returns {HTMLElement} Contains the D3 chart + */ + render() { + let selection = d3.select(this.chartEl); - /** - * Renders the chart(s) - * - * @method render - * @returns {HTMLElement} Contains the D3 chart - */ - Chart.prototype.render = function () { - let selection = d3.select(this.chartEl); - - selection.selectAll('*').remove(); - selection.call(this.draw()); - }; + selection.selectAll('*').remove(); + selection.call(this.draw()); + }; - /** - * Append the data label to the element - * - * @method _addIdentifier - * @param selection {Object} d3 select object - */ - Chart.prototype._addIdentifier = function (selection, labelProp) { - labelProp = labelProp || 'label'; - let labels = this.handler.data.labels; + /** + * Append the data label to the element + * + * @method _addIdentifier + * @param selection {Object} d3 select object + */ + _addIdentifier(selection, labelProp) { + labelProp = labelProp || 'label'; + let labels = this.handler.data.labels; - function resolveLabel(datum) { - if (labels.length === 1) return labels[0]; - if (datum[0]) return datum[0][labelProp]; - return datum[labelProp]; - } + function resolveLabel(datum) { + if (labels.length === 1) return labels[0]; + if (datum[0]) return datum[0][labelProp]; + return datum[labelProp]; + } - selection.each(function (datum) { - let label = resolveLabel(datum); - if (label != null) dataLabel(this, label); - }); - }; + selection.each(function (datum) { + let label = resolveLabel(datum); + if (label != null) dataLabel(this, label); + }); + }; - /** - * Removes all DOM elements from the root element - * - * @method destroy - */ - Chart.prototype.destroy = function () { - let selection = d3.select(this.chartEl); - this.events.removeAllListeners(); - this.tooltips.forEach(function (tooltip) { - tooltip.destroy(); - }); - selection.remove(); - selection = null; - }; + /** + * Removes all DOM elements from the root element + * + * @method destroy + */ + destroy() { + let selection = d3.select(this.chartEl); + this.events.removeAllListeners(); + this.tooltips.forEach(function (tooltip) { + tooltip.destroy(); + }); + selection.remove(); + selection = null; + }; + } return Chart; }; diff --git a/src/ui/public/vislib/visualizations/_map.js b/src/ui/public/vislib/visualizations/_map.js index b61de613488c..584968551042 100644 --- a/src/ui/public/vislib/visualizations/_map.js +++ b/src/ui/public/vislib/visualizations/_map.js @@ -41,285 +41,287 @@ export default function MapFactory(Private, tilemap, $sanitize) { * @param chartData {Object} Elasticsearch query results for this map * @param params {Object} Parameters used to build a map */ - function TileMapMap(container, chartData, params) { - this._container = $(container).get(0); - this._chartData = chartData; - - // keep a reference to all of the optional params - this._events = _.get(params, 'events'); - this._markerType = markerTypes[params.markerType] ? params.markerType : defaultMarkerType; - this._valueFormatter = params.valueFormatter || _.identity; - this._tooltipFormatter = params.tooltipFormatter || _.identity; - this._geoJson = _.get(this._chartData, 'geoJson'); - this._mapZoom = Math.max(Math.min(params.zoom || defaultMapZoom, tilemapOptions.maxZoom), tilemapOptions.minZoom); - this._mapCenter = params.center || defaultMapCenter; - this._attr = params.attr || {}; - - let mapOptions = { - minZoom: tilemapOptions.minZoom, - maxZoom: tilemapOptions.maxZoom, - noWrap: true, - maxBounds: L.latLngBounds([-90, -220], [90, 220]), - scrollWheelZoom: false, - fadeAnimation: false, - }; - - this._createMap(mapOptions); - } + class TileMapMap { + constructor(container, chartData, params) { + this._container = $(container).get(0); + this._chartData = chartData; + + // keep a reference to all of the optional params + this._events = _.get(params, 'events'); + this._markerType = markerTypes[params.markerType] ? params.markerType : defaultMarkerType; + this._valueFormatter = params.valueFormatter || _.identity; + this._tooltipFormatter = params.tooltipFormatter || _.identity; + this._geoJson = _.get(this._chartData, 'geoJson'); + this._mapZoom = Math.max(Math.min(params.zoom || defaultMapZoom, tilemapOptions.maxZoom), tilemapOptions.minZoom); + this._mapCenter = params.center || defaultMapCenter; + this._attr = params.attr || {}; + + let mapOptions = { + minZoom: tilemapOptions.minZoom, + maxZoom: tilemapOptions.maxZoom, + noWrap: true, + maxBounds: L.latLngBounds([-90, -220], [90, 220]), + scrollWheelZoom: false, + fadeAnimation: false, + }; + + this._createMap(mapOptions); + } - TileMapMap.prototype.addBoundingControl = function () { - if (this._boundingControl) return; + addBoundingControl() { + if (this._boundingControl) return; + + let self = this; + let drawOptions = {draw: {}}; + + _.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) { + if (self._events && !self._events.listenerCount(drawShape)) { + drawOptions.draw[drawShape] = false; + } else { + drawOptions.draw[drawShape] = { + shapeOptions: { + stroke: false, + color: '#000' + } + }; + } + }); - let self = this; - let drawOptions = { draw: {} }; + this._boundingControl = new L.Control.Draw(drawOptions); + this.map.addControl(this._boundingControl); + }; - _.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) { - if (self._events && !self._events.listenerCount(drawShape)) { - drawOptions.draw[drawShape] = false; - } else { - drawOptions.draw[drawShape] = { - shapeOptions: { - stroke: false, - color: '#000' - } - }; - } - }); + addFitControl() { + if (this._fitControl) return; + + let self = this; + let fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit'); + + // Add button to fit container to points + let FitControl = L.Control.extend({ + options: { + position: 'topleft' + }, + onAdd: function (map) { + $(fitContainer).html('') + .on('click', function (e) { + e.preventDefault(); + self._fitBounds(); + }); + + return fitContainer; + }, + onRemove: function (map) { + $(fitContainer).off('click'); + } + }); - this._boundingControl = new L.Control.Draw(drawOptions); - this.map.addControl(this._boundingControl); - }; + this._fitControl = new FitControl(); + this.map.addControl(this._fitControl); + }; - TileMapMap.prototype.addFitControl = function () { - if (this._fitControl) return; - - let self = this; - let fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit'); - - // Add button to fit container to points - let FitControl = L.Control.extend({ - options: { - position: 'topleft' - }, - onAdd: function (map) { - $(fitContainer).html('') - .on('click', function (e) { - e.preventDefault(); - self._fitBounds(); - }); + /** + * Adds label div to each map when data is split + * + * @method addTitle + * @param mapLabel {String} + * @return {undefined} + */ + addTitle(mapLabel) { + if (this._label) return; + + let label = this._label = L.control(); + + label.onAdd = function () { + this._div = L.DomUtil.create('div', 'tilemap-info tilemap-label'); + this.update(); + return this._div; + }; + label.update = function () { + this._div.innerHTML = '

' + _.escape(mapLabel) + '

'; + }; + + // label.addTo(this.map); + this.map.addControl(label); + }; - return fitContainer; - }, - onRemove: function (map) { - $(fitContainer).off('click'); + /** + * remove css class for desat filters on map tiles + * + * @method saturateTiles + * @return undefined + */ + saturateTiles() { + if (!this._attr.isDesaturated) { + $('img.leaflet-tile-loaded').addClass('filters-off'); } - }); - - this._fitControl = new FitControl(); - this.map.addControl(this._fitControl); - }; - - /** - * Adds label div to each map when data is split - * - * @method addTitle - * @param mapLabel {String} - * @return {undefined} - */ - TileMapMap.prototype.addTitle = function (mapLabel) { - if (this._label) return; - - let label = this._label = L.control(); - - label.onAdd = function () { - this._div = L.DomUtil.create('div', 'tilemap-info tilemap-label'); - this.update(); - return this._div; }; - label.update = function () { - this._div.innerHTML = '

' + _.escape(mapLabel) + '

'; - }; - - // label.addTo(this.map); - this.map.addControl(label); - }; - - /** - * remove css class for desat filters on map tiles - * - * @method saturateTiles - * @return undefined - */ - TileMapMap.prototype.saturateTiles = function () { - if (!this._attr.isDesaturated) { - $('img.leaflet-tile-loaded').addClass('filters-off'); - } - }; - TileMapMap.prototype.updateSize = function () { - this.map.invalidateSize({ - debounceMoveend: true - }); - }; - - TileMapMap.prototype.destroy = function () { - if (this._label) this._label.removeFrom(this.map); - if (this._fitControl) this._fitControl.removeFrom(this.map); - if (this._boundingControl) this._boundingControl.removeFrom(this.map); - if (this._markers) this._markers.destroy(); - this.map.remove(); - this.map = undefined; - }; + updateSize() { + this.map.invalidateSize({ + debounceMoveend: true + }); + }; - /** - * Switch type of data overlay for map: - * creates featurelayer from mapData (geoJson) - * - * @method _addMarkers - */ - TileMapMap.prototype._addMarkers = function () { - if (!this._geoJson) return; - if (this._markers) this._markers.destroy(); - - this._markers = this._createMarkers({ - tooltipFormatter: this._tooltipFormatter, - valueFormatter: this._valueFormatter, - attr: this._attr - }); - - if (this._geoJson.features.length > 1) { - this._markers.addLegend(); - } - }; + destroy() { + if (this._label) this._label.removeFrom(this.map); + if (this._fitControl) this._fitControl.removeFrom(this.map); + if (this._boundingControl) this._boundingControl.removeFrom(this.map); + if (this._markers) this._markers.destroy(); + this.map.remove(); + this.map = undefined; + }; - /** - * Create the marker instance using the given options - * - * @method _createMarkers - * @param options {Object} options to give to marker class - * @return {Object} marker layer - */ - TileMapMap.prototype._createMarkers = function (options) { - let MarkerType = markerTypes[this._markerType]; - return new MarkerType(this.map, this._geoJson, options); - }; + /** + * Switch type of data overlay for map: + * creates featurelayer from mapData (geoJson) + * + * @method _addMarkers + */ + _addMarkers() { + if (!this._geoJson) return; + if (this._markers) this._markers.destroy(); + + this._markers = this._createMarkers({ + tooltipFormatter: this._tooltipFormatter, + valueFormatter: this._valueFormatter, + attr: this._attr + }); - TileMapMap.prototype._attachEvents = function () { - let self = this; - let saturateTiles = self.saturateTiles.bind(self); + if (this._geoJson.features.length > 1) { + this._markers.addLegend(); + } + }; - this._tileLayer.on('tileload', saturateTiles); + /** + * Create the marker instance using the given options + * + * @method _createMarkers + * @param options {Object} options to give to marker class + * @return {Object} marker layer + */ + _createMarkers(options) { + let MarkerType = markerTypes[this._markerType]; + return new MarkerType(this.map, this._geoJson, options); + }; - this.map.on('unload', function () { - self._tileLayer.off('tileload', saturateTiles); - }); + _attachEvents() { + let self = this; + let saturateTiles = self.saturateTiles.bind(self); - this.map.on('moveend', function setZoomCenter(ev) { - if (!self.map) return; - // update internal center and zoom references - const uglyCenter = self.map.getCenter(); - self._mapCenter = [uglyCenter.lat, uglyCenter.lng]; - self._mapZoom = self.map.getZoom(); - self._addMarkers(); + this._tileLayer.on('tileload', saturateTiles); - if (!self._events) return; + this.map.on('unload', function () { + self._tileLayer.off('tileload', saturateTiles); + }); - self._events.emit('mapMoveEnd', { - chart: self._chartData, - map: self.map, - center: self._mapCenter, - zoom: self._mapZoom, + this.map.on('moveend', function setZoomCenter(ev) { + if (!self.map) return; + // update internal center and zoom references + const uglyCenter = self.map.getCenter(); + self._mapCenter = [uglyCenter.lat, uglyCenter.lng]; + self._mapZoom = self.map.getZoom(); + self._addMarkers(); + + if (!self._events) return; + + self._events.emit('mapMoveEnd', { + chart: self._chartData, + map: self.map, + center: self._mapCenter, + zoom: self._mapZoom, + }); }); - }); - this.map.on('draw:created', function (e) { - let drawType = e.layerType; - if (!self._events || !self._events.listenerCount(drawType)) return; + this.map.on('draw:created', function (e) { + let drawType = e.layerType; + if (!self._events || !self._events.listenerCount(drawType)) return; - // TODO: Different drawTypes need differ info. Need a switch on the object creation - let bounds = e.layer.getBounds(); + // TODO: Different drawTypes need differ info. Need a switch on the object creation + let bounds = e.layer.getBounds(); - let SElng = bounds.getSouthEast().lng; - if (SElng > 180) { - SElng -= 360; - } - let NWlng = bounds.getNorthWest().lng; - if (NWlng < -180) { - NWlng += 360; - } - self._events.emit(drawType, { - e: e, - chart: self._chartData, - bounds: { - top_left: { - lat: bounds.getNorthWest().lat, - lon: NWlng - }, - bottom_right: { - lat: bounds.getSouthEast().lat, - lon: SElng - } + let SElng = bounds.getSouthEast().lng; + if (SElng > 180) { + SElng -= 360; + } + let NWlng = bounds.getNorthWest().lng; + if (NWlng < -180) { + NWlng += 360; } + self._events.emit(drawType, { + e: e, + chart: self._chartData, + bounds: { + top_left: { + lat: bounds.getNorthWest().lat, + lon: NWlng + }, + bottom_right: { + lat: bounds.getSouthEast().lat, + lon: SElng + } + } + }); }); - }); - this.map.on('zoomend', function () { - if (!self.map) return; - self._mapZoom = self.map.getZoom(); - if (!self._events) return; + this.map.on('zoomend', function () { + if (!self.map) return; + self._mapZoom = self.map.getZoom(); + if (!self._events) return; - self._events.emit('mapZoomEnd', { - chart: self._chartData, - map: self.map, - zoom: self._mapZoom, + self._events.emit('mapZoomEnd', { + chart: self._chartData, + map: self.map, + zoom: self._mapZoom, + }); }); - }); - }; + }; - TileMapMap.prototype._createMap = function (mapOptions) { - if (this.map) this.destroy(); + _createMap(mapOptions) { + if (this.map) this.destroy(); - // add map tiles layer, using the mapTiles object settings - if (this._attr.wms && this._attr.wms.enabled) { - _.assign(mapOptions, { - minZoom: 1, - maxZoom: 18 - }); - this._tileLayer = L.tileLayer.wms(this._attr.wms.url, this._attr.wms.options); - } else { - this._tileLayer = L.tileLayer(mapTiles.url, mapTiles.options); - } + // add map tiles layer, using the mapTiles object settings + if (this._attr.wms && this._attr.wms.enabled) { + _.assign(mapOptions, { + minZoom: 1, + maxZoom: 18 + }); + this._tileLayer = L.tileLayer.wms(this._attr.wms.url, this._attr.wms.options); + } else { + this._tileLayer = L.tileLayer(mapTiles.url, mapTiles.options); + } - // append tile layers, center and zoom to the map options - mapOptions.layers = this._tileLayer; - mapOptions.center = this._mapCenter; - mapOptions.zoom = this._mapZoom; + // append tile layers, center and zoom to the map options + mapOptions.layers = this._tileLayer; + mapOptions.center = this._mapCenter; + mapOptions.zoom = this._mapZoom; - this.map = L.map(this._container, mapOptions); - this._attachEvents(); - this._addMarkers(); - }; + this.map = L.map(this._container, mapOptions); + this._attachEvents(); + this._addMarkers(); + }; - /** - * zoom map to fit all features in featureLayer - * - * @method _fitBounds - * @param map {Leaflet Object} - * @return {boolean} - */ - TileMapMap.prototype._fitBounds = function () { - this.map.fitBounds(this._getDataRectangles()); - }; + /** + * zoom map to fit all features in featureLayer + * + * @method _fitBounds + * @param map {Leaflet Object} + * @return {boolean} + */ + _fitBounds() { + this.map.fitBounds(this._getDataRectangles()); + }; - /** - * Get the Rectangles representing the geohash grid - * - * @return {LatLngRectangles[]} - */ - TileMapMap.prototype._getDataRectangles = function () { - if (!this._geoJson) return []; - return _.pluck(this._geoJson.features, 'properties.rectangle'); - }; + /** + * Get the Rectangles representing the geohash grid + * + * @return {LatLngRectangles[]} + */ + _getDataRectangles() { + if (!this._geoJson) return []; + return _.pluck(this._geoJson.features, 'properties.rectangle'); + }; + } return TileMapMap; }; diff --git a/src/ui/public/vislib/visualizations/_point_series_chart.js b/src/ui/public/vislib/visualizations/_point_series_chart.js index 31b60787db1d..ad6e7a7ccb49 100644 --- a/src/ui/public/vislib/visualizations/_point_series_chart.js +++ b/src/ui/public/vislib/visualizations/_point_series_chart.js @@ -10,175 +10,171 @@ export default function PointSeriesChartProvider(Private) { let Tooltip = Private(VislibComponentsTooltipProvider); let touchdownTmpl = _.template(require('ui/vislib/partials/touchdown.tmpl.html')); - _.class(PointSeriesChart).inherits(Chart); - function PointSeriesChart(handler, chartEl, chartData) { - if (!(this instanceof PointSeriesChart)) { - return new PointSeriesChart(handler, chartEl, chartData); + class PointSeriesChart extends Chart { + constructor(handler, chartEl, chartData) { + super(handler, chartEl, chartData); } - PointSeriesChart.Super.apply(this, arguments); - } + _stackMixedValues(stackCount) { + let currentStackOffsets = [0, 0]; + let currentStackIndex = 0; - PointSeriesChart.prototype._stackMixedValues = function (stackCount) { - let currentStackOffsets = [0, 0]; - let currentStackIndex = 0; + return function (d, y0, y) { + let firstStack = currentStackIndex % stackCount === 0; + let lastStack = ++currentStackIndex === stackCount; - return function (d, y0, y) { - let firstStack = currentStackIndex % stackCount === 0; - let lastStack = ++currentStackIndex === stackCount; + if (firstStack) { + currentStackOffsets = [0, 0]; + } - if (firstStack) { - currentStackOffsets = [0, 0]; - } + if (lastStack) currentStackIndex = 0; - if (lastStack) currentStackIndex = 0; + if (y >= 0) { + d.y0 = currentStackOffsets[1]; + currentStackOffsets[1] += y; + } else { + d.y0 = currentStackOffsets[0]; + currentStackOffsets[0] += y; + } + }; + }; - 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) { + let self = this; + let isHistogram = (this._attr.type === 'histogram' && this._attr.mode === 'stacked'); + let stack = this._attr.stack; + + if (isHistogram) stack.out(self._stackMixedValues(data.series.length)); + + return stack(data.series.map(function (d) { + let 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) + }; + }); + })); }; - }; - - /** - * 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 - */ - PointSeriesChart.prototype.stackData = function (data) { - let self = this; - let isHistogram = (this._attr.type === 'histogram' && this._attr.mode === 'stacked'); - let stack = this._attr.stack; - - if (isHistogram) stack.out(self._stackMixedValues(data.series.length)); - - return stack(data.series.map(function (d) { - let 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) - }; - }); - })); - }; - PointSeriesChart.prototype.validateDataCompliesWithScalingMethod = function (data) { - const invalidLogScale = data.series && data.series.some(valuesSmallerThanOne); - if (this._attr.scale === 'log' && invalidLogScale) { - throw new errors.InvalidLogScaleValues(); - } - }; - function valuesSmallerThanOne(d) { - return d.values && d.values.some(e => e.y < 1); - } + 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) { + let self = this; + let xAxis = this.handler.xAxis; + let xScale = xAxis.xScale; + let ordered = xAxis.ordered; + let missingMinMax = !ordered || _.isUndefined(ordered.min) || _.isUndefined(ordered.max); + + if (missingMinMax || ordered.endzones === false) return; + + let attr = this.handler._attr; + let height = attr.height; + let width = attr.width; + let margin = attr.margin; + let color = '#004c99'; + + // 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 + let oneUnit = (ordered.units || _.identity)(1); + let beyondMin = ordered.min - oneUnit; + let beyondMax = ordered.max + oneUnit; + + // points on this axis represent the amount of time they cover, + // so draw the endzones at the actual time bounds + let leftEndzone = { + x: 0, + w: Math.max(xScale(ordered.min), 0) + }; - /** - * 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} - */ - PointSeriesChart.prototype.createEndZones = function (svg) { - let self = this; - let xAxis = this.handler.xAxis; - let xScale = xAxis.xScale; - let ordered = xAxis.ordered; - let missingMinMax = !ordered || _.isUndefined(ordered.min) || _.isUndefined(ordered.max); - - if (missingMinMax || ordered.endzones === false) return; - - let attr = this.handler._attr; - let height = attr.height; - let width = attr.width; - let margin = attr.margin; - let color = '#004c99'; - - // 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 - let oneUnit = (ordered.units || _.identity)(1); - let beyondMin = ordered.min - oneUnit; - let beyondMax = ordered.max + oneUnit; - - // points on this axis represent the amount of time they cover, - // so draw the endzones at the actual time bounds - let leftEndzone = { - x: 0, - w: Math.max(xScale(ordered.min), 0) - }; + let rightLastVal = xAxis.expandLastBucket ? ordered.max : Math.min(ordered.max, _.last(xAxis.xValues)); + let rightStart = rightLastVal + oneUnit; + let rightEndzone = { + x: xScale(rightStart), + w: Math.max(width - xScale(rightStart), 0) + }; - let rightLastVal = xAxis.expandLastBucket ? ordered.max : Math.min(ordered.max, _.last(xAxis.xValues)); - let rightStart = rightLastVal + oneUnit; - let 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) { + let boundData = event.target.__data__; + let mouseChartXCoord = event.clientX - self.chartEl.getBoundingClientRect().left; + let wholeBucket = boundData && boundData.x != null; + + // the min and max that the endzones start in + let min = leftEndzone.w; + let 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)); + } - 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) { - let boundData = event.target.__data__; - let mouseChartXCoord = event.clientX - self.chartEl.getBoundingClientRect().left; - let wholeBucket = boundData && boundData.x != null; - - // the min and max that the endzones start in - let min = leftEndzone.w; - let 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)); + } - return { - wholeBucket: wholeBucket, - touchdown: min > xLeft || max < xRight + let 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; }; - } - - function textFormatter() { - return touchdownTmpl(callPlay(d3.event)); - } - - let 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); }; - 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 index 4b0d1d1f4883..d3820b89e949 100644 --- a/src/ui/public/vislib/visualizations/area_chart.js +++ b/src/ui/public/vislib/visualizations/area_chart.js @@ -20,364 +20,367 @@ export default function AreaChartFactory(Private) { * @param chartData {Object} Elasticsearch query results for this specific * chart */ - _.class(AreaChart).inherits(PointSeriesChart); - function AreaChart(handler, chartEl, chartData) { - if (!(this instanceof AreaChart)) { - return new AreaChart(handler, chartEl, chartData); - } - - AreaChart.Super.apply(this, arguments); - - this.isOverlapping = (handler._attr.mode === 'overlap'); - - if (this.isOverlapping) { - - // Default opacity should return to 0.6 on mouseout - let defaultOpacity = 0.6; - handler._attr.defaultOpacity = defaultOpacity; - handler.highlight = function (element) { - let label = this.getAttribute('data-label'); - if (!label) return; - - let highlightOpacity = 0.8; - let 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 - */ - AreaChart.prototype.addPath = function (svg, layers) { - let self = this; - let ordered = this.handler.data.get('ordered'); - let isTimeSeries = (ordered && ordered.date); - let isOverlapping = this.isOverlapping; - let color = this.handler.data.getColorFunc(); - let xScale = this.handler.xAxis.xScale; - let yScale = this.handler.yAxis.yScale; - let interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate; - let 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); + 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 + let defaultOpacity = 0.6; + handler._attr.defaultOpacity = defaultOpacity; + handler.highlight = function (element) { + let label = this.getAttribute('data-label'); + if (!label) return; + + let highlightOpacity = 0.8; + let 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); + }; } - return yScale(d.y0); - }) - .y1(function (d) { - if (isOverlapping) { - return yScale(d.y); - } + this.checkIfEnoughData(); - return yScale(d.y0 + d.y); - }) - .defined(function (d) { return !_.isNull(d.y); }) - .interpolate(interpolate); - - // Data layers - let layer = svg.selectAll('.layer') - .data(layers) - .enter() - .append('g') - .attr('class', function (d, i) { - return 'pathgroup ' + i; - }); - - // Append path - let 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 - */ - AreaChart.prototype.addCircleEvents = function (element, svg) { - let events = this.events; - let isBrushable = events.isBrushable(); - let brush = isBrushable ? events.addBrushEvent(svg) : undefined; - let hover = events.addHoverEvent(); - let mouseout = events.addMouseoutEvent(); - let click = events.addClickEvent(); - let attachedEvents = element.call(hover).call(mouseout).call(click); - - if (isBrushable) { - attachedEvents.call(brush); + this._attr = _.defaults(handler._attr || {}, { + xValue: function (d) { + return d.x; + }, + yValue: function (d) { + return d.y; + } + }); } - 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 - */ - AreaChart.prototype.addCircles = function (svg, data) { - let self = this; - let color = this.handler.data.getColorFunc(); - let xScale = this.handler.xAxis.xScale; - let yScale = this.handler.yAxis.yScale; - let ordered = this.handler.data.get('ordered'); - let circleRadius = 12; - let circleStrokeWidth = 0; - let tooltip = this.tooltip; - let isTooltip = this._attr.addTooltip; - let isOverlapping = this.isOverlapping; - let layer; - let circles; - - layer = svg.selectAll('.points') - .data(data) - .enter() - .append('g') - .attr('class', 'points area'); - - // append the circles - circles = layer - .selectAll('circles') - .data(function appendData(data) { - return data.filter(function isZeroOrNull(d) { - return d.y !== 0 && !_.isNull(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) { + let self = this; + let ordered = this.handler.data.get('ordered'); + let isTimeSeries = (ordered && ordered.date); + let isOverlapping = this.isOverlapping; + let color = this.handler.data.getColorFunc(); + let xScale = this.handler.xAxis.xScale; + let yScale = this.handler.yAxis.yScale; + let interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate; + let 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 + let layer = svg.selectAll('.layer') + .data(layers) + .enter() + .append('g') + .attr('class', function (d, i) { + return 'pathgroup ' + i; + }); + + // Append path + let 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); }); - }); - - // 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; - }; + return path; + }; - /** - * 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 - */ - AreaChart.prototype.addClipPath = function (svg, width, height) { - // Prevents circles from being clipped at the top of the chart - let startX = 0; - let startY = 0; - let 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); - }; - - AreaChart.prototype.checkIfEnoughData = function () { - let series = this.chartData.series; - let message = 'Area charts require more than one data point. Try adding ' + - 'an X-Axis Aggregation'; - - let notEnoughData = series.some(function (obj) { - return obj.values.length < 2; - }); - - if (notEnoughData) { - throw new errors.NotEnoughData(message); - } - }; + /** + * Adds Events to SVG circles + * + * @method addCircleEvents + * @param element {D3.UpdateSelection} SVG circles + * @returns {D3.Selection} circles with event listeners attached + */ + addCircleEvents(element, svg) { + let events = this.events; + let isBrushable = events.isBrushable(); + let brush = isBrushable ? events.addBrushEvent(svg) : undefined; + let hover = events.addHoverEvent(); + let mouseout = events.addMouseoutEvent(); + let click = events.addClickEvent(); + let attachedEvents = element.call(hover).call(mouseout).call(click); + + if (isBrushable) { + attachedEvents.call(brush); + } - AreaChart.prototype.validateWiggleSelection = function () { - let isWiggle = this._attr.mode === 'wiggle'; - let ordered = this.handler.data.get('ordered'); + return attachedEvents; + }; - if (isWiggle && !ordered) throw new errors.InvalidWiggleSelection(); - }; + /** + * 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) { + let self = this; + let color = this.handler.data.getColorFunc(); + let xScale = this.handler.xAxis.xScale; + let yScale = this.handler.yAxis.yScale; + let ordered = this.handler.data.get('ordered'); + let circleRadius = 12; + let circleStrokeWidth = 0; + let tooltip = this.tooltip; + let isTooltip = this._attr.addTooltip; + let isOverlapping = this.isOverlapping; + let layer; + let circles; + + layer = svg.selectAll('.points') + .data(data) + .enter() + .append('g') + .attr('class', 'points area'); + + // append the circles + 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()); + } - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the area chart - */ - AreaChart.prototype.draw = function () { - // Attributes - let self = this; - let xScale = this.handler.xAxis.xScale; - let $elem = $(this.chartEl); - let margin = this._attr.margin; - let elWidth = this._attr.width = $elem.width(); - let elHeight = this._attr.height = $elem.height(); - let yMin = this.handler.yAxis.yMin; - let yScale = this.handler.yAxis.yScale; - let minWidth = 20; - let minHeight = 20; - let addTimeMarker = this._attr.addTimeMarker; - let times = this._attr.times || []; - let timeMarker; - let div; - let svg; - let width; - let height; - let layers; - let circles; - let path; - - return function (selection) { - selection.each(function (data) { - // Stack data - layers = self.stackData(data); - - // Get the width and height - width = elWidth; - height = elHeight - margin.top - margin.bottom; - - if (addTimeMarker) { - timeMarker = new TimeMarker(times, xScale, height); - } + return circles; + }; - if (width < minWidth || height < minHeight) { - throw new errors.ContainerTooSmall(); - } - self.validateWiggleSelection(); + /** + * 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 + let startX = 0; + let startY = 0; + let 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); + }; - // Select the current DOM element - div = d3.select(this); + checkIfEnoughData() { + let series = this.chartData.series; + let message = 'Area charts require more than one data point. Try adding ' + + 'an X-Axis Aggregation'; - // Create the canvas for the visualization - svg = div.append('svg') - .attr('width', width) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(0,' + margin.top + ')'); + let notEnoughData = series.some(function (obj) { + return obj.values.length < 2; + }); - // add clipPath to hide circles when they go out of bounds - self.addClipPath(svg, width, height); - self.createEndZones(svg); + if (notEnoughData) { + throw new errors.NotEnoughData(message); + } + }; - // add path - path = self.addPath(svg, layers); + validateWiggleSelection() { + let isWiggle = this._attr.mode === 'wiggle'; + let ordered = this.handler.data.get('ordered'); - if (yMin < 0 && self._attr.mode !== 'wiggle' && self._attr.mode !== 'silhouette') { + if (isWiggle && !ordered) throw new errors.InvalidWiggleSelection(); + }; - // Draw line at yScale 0 value - svg.append('line') - .attr('class', 'zero-line') + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the area chart + */ + draw() { + // Attributes + let self = this; + let xScale = this.handler.xAxis.xScale; + let $elem = $(this.chartEl); + let margin = this._attr.margin; + let elWidth = this._attr.width = $elem.width(); + let elHeight = this._attr.height = $elem.height(); + let yMin = this.handler.yAxis.yMin; + let yScale = this.handler.yAxis.yScale; + let minWidth = 20; + let minHeight = 20; + let addTimeMarker = this._attr.addTimeMarker; + let times = this._attr.times || []; + let timeMarker; + let div; + let svg; + let width; + let height; + let layers; + let circles; + let path; + + return function (selection) { + selection.each(function (data) { + // Stack data + layers = self.stackData(data); + + // Get the width and height + width = elWidth; + 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 + div = d3.select(this); + + // Create the canvas for the visualization + 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 + 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 + circles = self.addCircles(svg, layers); + + // add click and hover events to circles + self.addCircleEvents(circles, svg); + + // chart base line + let 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); - } - - // add circles - circles = self.addCircles(svg, layers); - - // add click and hover events to circles - self.addCircleEvents(circles, svg); - - // chart base line - let 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); - } + if (addTimeMarker) { + timeMarker.render(svg); + } - return svg; - }); + return svg; + }); + }; }; - }; + } return AreaChart; }; diff --git a/src/ui/public/vislib/visualizations/column_chart.js b/src/ui/public/vislib/visualizations/column_chart.js index b4c8b220d37c..b9bf913cad26 100644 --- a/src/ui/public/vislib/visualizations/column_chart.js +++ b/src/ui/public/vislib/visualizations/column_chart.js @@ -20,313 +20,314 @@ export default function ColumnChartFactory(Private) { * @param el {HTMLElement} HTML element to which the chart will be appended * @param chartData {Object} Elasticsearch query results for this specific chart */ - _.class(ColumnChart).inherits(PointSeriesChart); - function ColumnChart(handler, chartEl, chartData) { - if (!(this instanceof ColumnChart)) { - return new ColumnChart(handler, chartEl, chartData); - } - - ColumnChart.Super.apply(this, arguments); - - // 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 - */ - ColumnChart.prototype.addBars = function (svg, layers) { - let self = this; - let color = this.handler.data.getColorFunc(); - let tooltip = this.tooltip; - let isTooltip = this._attr.addTooltip; - let layer; - let bars; - - layer = svg.selectAll('.layer') - .data(layers) - .enter().append('g') - .attr('class', function (d, i) { - return 'series ' + i; - }); - - 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} - */ - ColumnChart.prototype.updateBars = function (bars) { - let 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} - */ - ColumnChart.prototype.addStackedBars = function (bars) { - let data = this.chartData; - let xScale = this.handler.xAxis.xScale; - let yScale = this.handler.yAxis.yScale; - let height = yScale.range()[0]; - let yMin = this.handler.yAxis.yScale.domain()[0]; - - let barWidth; - if (data.ordered && data.ordered.date) { - let start = data.ordered.min; - let end = moment(data.ordered.min).add(data.ordered.interval).valueOf(); - - barWidth = xScale(end) - xScale(start); - barWidth = barWidth - Math.min(barWidth * 0.25, 15); + 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; + } + }); } - // 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); + /** + * 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) { + let self = this; + let color = this.handler.data.getColorFunc(); + let tooltip = this.tooltip; + let isTooltip = this._attr.addTooltip; + let layer; + let bars; + + layer = svg.selectAll('.layer') + .data(layers) + .enter().append('g') + .attr('class', function (d, i) { + return 'series ' + i; + }); + + 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 yScale(d.y0 + d.y); - }) - .attr('height', function (d) { - if (d.y < 0) { - return Math.abs(yScale(d.y0 + d.y) - yScale(d.y0)); - } + return bars; + }; - // 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; + /** + * 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) { + let offset = this._attr.mode; + + if (offset === 'grouped') { + return this.addGroupedBars(bars); } + return this.addStackedBars(bars); + }; - // 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); + /** + * Adds stacked bars to column chart visualization + * + * @method addStackedBars + * @param bars {D3.UpdateSelection} SVG with rect added + * @returns {D3.UpdateSelection} + */ + addStackedBars(bars) { + let data = this.chartData; + let xScale = this.handler.xAxis.xScale; + let yScale = this.handler.yAxis.yScale; + let height = yScale.range()[0]; + let yMin = this.handler.yAxis.yScale.domain()[0]; + + let barWidth; + if (data.ordered && data.ordered.date) { + let start = data.ordered.min; + let end = moment(data.ordered.min).add(data.ordered.interval).valueOf(); + + barWidth = xScale(end) - xScale(start); + barWidth = barWidth - Math.min(barWidth * 0.25, 15); } - return yScale(d.y0) - yScale(d.y0 + d.y); - }); - - return bars; - }; + // 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} - */ - ColumnChart.prototype.addGroupedBars = function (bars) { - let xScale = this.handler.xAxis.xScale; - let yScale = this.handler.yAxis.yScale; - let data = this.chartData; - let n = data.series.length; - let height = yScale.range()[0]; - let groupSpacingPercentage = 0.15; - let isTimeScale = (data.ordered && data.ordered.date); - let minWidth = 1; - let barWidth; - - // update - bars - .attr('x', function (d, i, j) { - if (isTimeScale) { - let groupWidth = xScale(data.ordered.min + data.ordered.interval) - - xScale(data.ordered.min); - let 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(); - } + /** + * Adds grouped bars to column chart visualization + * + * @method addGroupedBars + * @param bars {D3.UpdateSelection} SVG with rect added + * @returns {D3.UpdateSelection} + */ + addGroupedBars(bars) { + let xScale = this.handler.xAxis.xScale; + let yScale = this.handler.yAxis.yScale; + let data = this.chartData; + let n = data.series.length; + let height = yScale.range()[0]; + let groupSpacingPercentage = 0.15; + let isTimeScale = (data.ordered && data.ordered.date); + let minWidth = 1; + let barWidth; + + // update + bars + .attr('x', function (d, i, j) { + if (isTimeScale) { + let groupWidth = xScale(data.ordered.min + data.ordered.interval) - + xScale(data.ordered.min); + let 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; + }; - if (isTimeScale) { - return barWidth; - } - return xScale.rangeBand() / n; - }) - .attr('y', function (d) { - if (d.y < 0) { - return yScale(0); + /** + * 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) { + let events = this.events; + let isBrushable = events.isBrushable(); + let brush = isBrushable ? events.addBrushEvent(svg) : undefined; + let hover = events.addHoverEvent(); + let mouseout = events.addMouseoutEvent(); + let click = events.addClickEvent(); + let attachedEvents = element.call(hover).call(mouseout).call(click); + + if (isBrushable) { + attachedEvents.call(brush); } - 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 - */ - ColumnChart.prototype.addBarEvents = function (element, svg) { - let events = this.events; - let isBrushable = events.isBrushable(); - let brush = isBrushable ? events.addBrushEvent(svg) : undefined; - let hover = events.addHoverEvent(); - let mouseout = events.addMouseoutEvent(); - let click = events.addClickEvent(); - let 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 - */ - ColumnChart.prototype.draw = function () { - let self = this; - let $elem = $(this.chartEl); - let margin = this._attr.margin; - let elWidth = this._attr.width = $elem.width(); - let elHeight = this._attr.height = $elem.height(); - let yScale = this.handler.yAxis.yScale; - let xScale = this.handler.xAxis.xScale; - let minWidth = 20; - let minHeight = 20; - let addTimeMarker = this._attr.addTimeMarker; - let times = this._attr.times || []; - let timeMarker; - let div; - let svg; - let width; - let height; - let layers; - let bars; - - return function (selection) { - selection.each(function (data) { - layers = self.stackData(data); - - width = elWidth; - 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); - } + return attachedEvents; + }; - 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 \ + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the vertical bar chart + */ + draw() { + let self = this; + let $elem = $(this.chartEl); + let margin = this._attr.margin; + let elWidth = this._attr.width = $elem.width(); + let elHeight = this._attr.height = $elem.height(); + let yScale = this.handler.yAxis.yScale; + let xScale = this.handler.xAxis.xScale; + let minWidth = 20; + let minHeight = 20; + let addTimeMarker = this._attr.addTimeMarker; + let times = this._attr.times || []; + let timeMarker; + let div; + let svg; + let width; + let height; + let layers; + let bars; + + return function (selection) { + selection.each(function (data) { + layers = self.stackData(data); + + width = elWidth; + 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.`); - } - - div = d3.select(this); - - svg = div.append('svg') - .attr('width', width) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(0,' + margin.top + ')'); - - bars = self.addBars(svg, layers); - self.createEndZones(svg); - - // Adds event listeners - self.addBarEvents(bars, svg); - - let 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); - } - - return svg; - }); + } + + div = d3.select(this); + + svg = div.append('svg') + .attr('width', width) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', 'translate(0,' + margin.top + ')'); + + bars = self.addBars(svg, layers); + self.createEndZones(svg); + + // Adds event listeners + self.addBarEvents(bars, svg); + + let 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); + } + + return svg; + }); + }; }; - }; + } return ColumnChart; }; diff --git a/src/ui/public/vislib/visualizations/line_chart.js b/src/ui/public/vislib/visualizations/line_chart.js index 66b1c035a460..05c0524f8621 100644 --- a/src/ui/public/vislib/visualizations/line_chart.js +++ b/src/ui/public/vislib/visualizations/line_chart.js @@ -19,337 +19,340 @@ export default function LineChartFactory(Private) { * @param el {HTMLElement} HTML element to which the chart will be appended * @param chartData {Object} Elasticsearch query results for this specific chart */ - _.class(LineChart).inherits(PointSeriesChart); - function LineChart(handler, chartEl, chartData) { - if (!(this instanceof LineChart)) { - return new LineChart(handler, chartEl, chartData); + 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; + } + }); } - LineChart.Super.apply(this, arguments); + /** + * 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) { + let events = this.events; + let isBrushable = events.isBrushable(); + let brush = isBrushable ? events.addBrushEvent(svg) : undefined; + let hover = events.addHoverEvent(); + let mouseout = events.addMouseoutEvent(); + let click = events.addClickEvent(); + let attachedEvents = element.call(hover).call(mouseout).call(click); + + if (isBrushable) { + attachedEvents.call(brush); + } - // Line chart specific attributes - this._attr = _.defaults(handler._attr || {}, { - interpolate: 'linear', - xValue: function (d) { return d.x; }, - yValue: function (d) { return d.y; } - }); - } + return attachedEvents; + }; - /** - * Adds Events to SVG circle - * - * @method addCircleEvents - * @param element{D3.UpdateSelection} Reference to SVG circle - * @returns {D3.Selection} SVG circles with event listeners attached - */ - LineChart.prototype.addCircleEvents = function (element, svg) { - let events = this.events; - let isBrushable = events.isBrushable(); - let brush = isBrushable ? events.addBrushEvent(svg) : undefined; - let hover = events.addHoverEvent(); - let mouseout = events.addMouseoutEvent(); - let click = events.addClickEvent(); - let attachedEvents = element.call(hover).call(mouseout).call(click); - - if (isBrushable) { - attachedEvents.call(brush); - } + /** + * 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) { + let self = this; + let showCircles = this._attr.showCircles; + let color = this.handler.data.getColorFunc(); + let xScale = this.handler.xAxis.xScale; + let yScale = this.handler.yAxis.yScale; + let ordered = this.handler.data.get('ordered'); + let tooltip = this.tooltip; + let isTooltip = this._attr.addTooltip; + + let 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 + }); - return attachedEvents; - }; + let radiusStep = ((radii.max - radii.min) || (radii.max * 100)) / Math.pow(this._attr.radiusRatio, 2); - /** - * 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 - */ - LineChart.prototype.addCircles = function (svg, data) { - let self = this; - let showCircles = this._attr.showCircles; - let color = this.handler.data.getColorFunc(); - let xScale = this.handler.xAxis.xScale; - let yScale = this.handler.yAxis.yScale; - let ordered = this.handler.data.get('ordered'); - let tooltip = this.tooltip; - let isTooltip = this._attr.addTooltip; - - let 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 - }); - - let radiusStep = ((radii.max - radii.min) || (radii.max * 100)) / Math.pow(this._attr.radiusRatio, 2); - - let layer = svg.selectAll('.points') - .data(data) - .enter() - .append('g') - .attr('class', 'points line'); - - let circles = layer - .selectAll('circle') - .data(function appendData(data) { - return data.filter(function (d) { - return !_.isNull(d.y); - }); - }); + let layer = svg.selectAll('.points') + .data(data) + .enter() + .append('g') + .attr('class', 'points line'); - circles - .exit() - .remove(); + let circles = layer + .selectAll('circle') + .data(function appendData(data) { + return data.filter(function (d) { + return !_.isNull(d.y); + }); + }); - function cx(d) { - if (ordered && ordered.date) { - return xScale(d.x); + circles + .exit() + .remove(); + + function cx(d) { + if (ordered && ordered.date) { + return xScale(d.x); + } + return xScale(d.x) + xScale.rangeBand() / 2; } - return xScale(d.x) + xScale.rangeBand() / 2; - } - function cy(d) { - return yScale(d.y); - } + function cy(d) { + return yScale(d.y); + } - function cColor(d) { - return color(d.label); - } + function cColor(d) { + return color(d.label); + } - function colorCircle(d) { - let parent = d3.select(this).node().parentNode; - let lengthOfParent = d3.select(parent).data()[0].length; - let isVisible = (lengthOfParent === 1); + function colorCircle(d) { + let parent = d3.select(this).node().parentNode; + let lengthOfParent = d3.select(parent).data()[0].length; + let 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) { - let margin = self._attr.margin; - let width = self._attr.width - margin.left - margin.right; - let height = self._attr.height - margin.top - margin.bottom; - let circleRadius = (d._input.z - radii.min) / radiusStep; - - return _.min([Math.sqrt((circleRadius || 2) + 2), width, height]) + (modifier || 0); - }; - } + // If only 1 point exists, show circle + if (!showCircles && !isVisible) return 'none'; + return cColor(d); + } + function getCircleRadiusFn(modifier) { + return function getCircleRadius(d) { + let margin = self._attr.margin; + let width = self._attr.width - margin.left - margin.right; + let height = self._attr.height - margin.top - margin.bottom; + let circleRadius = (d._input.z - radii.min) / radiusStep; - 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 _.min([Math.sqrt((circleRadius || 2) + 2), width, height]) + (modifier || 0); + }; + } - 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 - */ - LineChart.prototype.addLines = function (svg, data) { - let self = this; - let xScale = this.handler.xAxis.xScale; - let yScale = this.handler.yAxis.yScale; - let xAxisFormatter = this.handler.data.get('xAxisFormatter'); - let color = this.handler.data.getColorFunc(); - let ordered = this.handler.data.get('ordered'); - let interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate; - let 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); + 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 xScale(d.x) + xScale.rangeBand() / 2; - }) - .y(function y(d) { - return yScale(d.y); - }); - let lines; - - 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 - */ - LineChart.prototype.addClipPath = function (svg, width, height) { - let clipPathBuffer = 5; - let startX = 0; - let startY = 0 - clipPathBuffer; - let 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); - }; + return circles; + }; - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the line chart - */ - LineChart.prototype.draw = function () { - let self = this; - let $elem = $(this.chartEl); - let margin = this._attr.margin; - let elWidth = this._attr.width = $elem.width(); - let elHeight = this._attr.height = $elem.height(); - let scaleType = this.handler.yAxis.getScaleType(); - let yMin = this.handler.yAxis.yMin; - let yScale = this.handler.yAxis.yScale; - let xScale = this.handler.xAxis.xScale; - let minWidth = 20; - let minHeight = 20; - let startLineX = 0; - let lineStrokeWidth = 1; - let addTimeMarker = this._attr.addTimeMarker; - let times = this._attr.times || []; - let timeMarker; - let div; - let svg; - let width; - let height; - let lines; - let circles; - - return function (selection) { - selection.each(function (data) { - let el = this; - - let layers = data.series.map(function mapSeries(d) { - let 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) - }; - }); + /** + * 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) { + let self = this; + let xScale = this.handler.xAxis.xScale; + let yScale = this.handler.yAxis.yScale; + let xAxisFormatter = this.handler.data.get('xAxisFormatter'); + let color = this.handler.data.getColorFunc(); + let ordered = this.handler.data.get('ordered'); + let interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate; + let 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); }); + let lines; - width = elWidth - margin.left - margin.right; - 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); - } - - - - div = d3.select(el); - - svg = div.append('svg') - .attr('width', width + margin.left + margin.right) - .attr('height', height + margin.top + margin.bottom) + lines = svg + .selectAll('.lines') + .data(data) + .enter() .append('g') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + .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; + }; - self.addClipPath(svg, width, height); - if (self._attr.drawLinesBetweenPoints) { - lines = self.addLines(svg, data.series); - } - circles = self.addCircles(svg, layers); - self.addCircleEvents(circles, svg); - self.createEndZones(svg); - - let 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); - } + /** + * 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) { + let clipPathBuffer = 5; + let startX = 0; + let startY = 0 - clipPathBuffer; + let 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); + }; - if (addTimeMarker) { - timeMarker.render(svg); - } + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the line chart + */ + draw() { + let self = this; + let $elem = $(this.chartEl); + let margin = this._attr.margin; + let elWidth = this._attr.width = $elem.width(); + let elHeight = this._attr.height = $elem.height(); + let scaleType = this.handler.yAxis.getScaleType(); + let yMin = this.handler.yAxis.yMin; + let yScale = this.handler.yAxis.yScale; + let xScale = this.handler.xAxis.xScale; + let minWidth = 20; + let minHeight = 20; + let startLineX = 0; + let lineStrokeWidth = 1; + let addTimeMarker = this._attr.addTimeMarker; + let times = this._attr.times || []; + let timeMarker; + let div; + let svg; + let width; + let height; + let lines; + let circles; + + return function (selection) { + selection.each(function (data) { + let el = this; + + let layers = data.series.map(function mapSeries(d) { + let 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) + }; + }); + }); - return svg; - }); + width = elWidth - margin.left - margin.right; + 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); + } + + + div = d3.select(el); + + 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) { + lines = self.addLines(svg, data.series); + } + circles = self.addCircles(svg, layers); + self.addCircleEvents(circles, svg); + self.createEndZones(svg); + + let 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); + } + + return svg; + }); + }; }; - }; + } return LineChart; }; diff --git a/src/ui/public/vislib/visualizations/marker_types/base_marker.js b/src/ui/public/vislib/visualizations/marker_types/base_marker.js index 9164e2eb94d0..9c89f4249c07 100644 --- a/src/ui/public/vislib/visualizations/marker_types/base_marker.js +++ b/src/ui/public/vislib/visualizations/marker_types/base_marker.js @@ -11,254 +11,256 @@ export default function MarkerFactory() { * @param geoJson {geoJson Object} * @param params {Object} */ - function BaseMarker(map, geoJson, params) { - this.map = map; - this.geoJson = geoJson; - this.popups = []; - - this._tooltipFormatter = params.tooltipFormatter || _.identity; - this._valueFormatter = params.valueFormatter || _.identity; - this._attr = params.attr || {}; - - // set up the default legend colors - this.quantizeLegendColors(); - } - - /** - * Adds legend div to each map when data is split - * uses d3 scale from BaseMarker.prototype.quantizeLegendColors - * - * @method addLegend - * @return {undefined} - */ - BaseMarker.prototype.addLegend = function () { - // ensure we only ever create 1 legend - if (this._legend) return; - - let self = this; - - // create the legend control, keep a reference - self._legend = L.control({position: 'bottomright'}); - - self._legend.onAdd = function () { - // creates all the neccessary DOM elements for the control, adds listeners - // on relevant map events, and returns the element containing the control - let $div = $('
').addClass('tilemap-legend'); - - _.each(self._legendColors, function (color, i) { - let labelText = self._legendQuantizer - .invertExtent(color) - .map(self._valueFormatter) - .join(' – '); - - let label = $('
').text(labelText); + class BaseMarker { + constructor(map, geoJson, params) { + this.map = map; + this.geoJson = geoJson; + this.popups = []; + + this._tooltipFormatter = params.tooltipFormatter || _.identity; + this._valueFormatter = params.valueFormatter || _.identity; + this._attr = params.attr || {}; + + // set up the default legend colors + this.quantizeLegendColors(); + } - let icon = $('').css({ - background: color, - 'border-color': self.darkerColor(color) + /** + * Adds legend div to each map when data is split + * uses d3 scale from BaseMarker.prototype.quantizeLegendColors + * + * @method addLegend + * @return {undefined} + */ + addLegend() { + // ensure we only ever create 1 legend + if (this._legend) return; + + let self = this; + + // create the legend control, keep a reference + self._legend = L.control({position: 'bottomright'}); + + self._legend.onAdd = function () { + // creates all the neccessary DOM elements for the control, adds listeners + // on relevant map events, and returns the element containing the control + let $div = $('
').addClass('tilemap-legend'); + + _.each(self._legendColors, function (color, i) { + let labelText = self._legendQuantizer + .invertExtent(color) + .map(self._valueFormatter) + .join(' – '); + + let label = $('
').text(labelText); + + let icon = $('').css({ + background: color, + 'border-color': self.darkerColor(color) + }); + + label.append(icon); + $div.append(label); }); - label.append(icon); - $div.append(label); - }); + return $div.get(0); + }; - return $div.get(0); + self._legend.addTo(self.map); }; - self._legend.addTo(self.map); - }; - - /** - * Apply style with shading to feature - * - * @method applyShadingStyle - * @param value {Object} - * @return {Object} - */ - BaseMarker.prototype.applyShadingStyle = function (value) { - let color = this._legendQuantizer(value); - - return { - fillColor: color, - color: this.darkerColor(color), - weight: 1.5, - opacity: 1, - fillOpacity: 0.75 + /** + * Apply style with shading to feature + * + * @method applyShadingStyle + * @param value {Object} + * @return {Object} + */ + applyShadingStyle(value) { + let color = this._legendQuantizer(value); + + return { + fillColor: color, + color: this.darkerColor(color), + weight: 1.5, + opacity: 1, + fillOpacity: 0.75 + }; }; - }; - /** - * Binds popup and events to each feature on map - * - * @method bindPopup - * @param feature {Object} - * @param layer {Object} - * return {undefined} - */ - BaseMarker.prototype.bindPopup = function (feature, layer) { - let self = this; - - let popup = layer.on({ - mouseover: function (e) { - let layer = e.target; - // bring layer to front if not older browser - if (!L.Browser.ie && !L.Browser.opera) { - layer.bringToFront(); + /** + * Binds popup and events to each feature on map + * + * @method bindPopup + * @param feature {Object} + * @param layer {Object} + * return {undefined} + */ + bindPopup(feature, layer) { + let self = this; + + let popup = layer.on({ + mouseover: function (e) { + let layer = e.target; + // bring layer to front if not older browser + if (!L.Browser.ie && !L.Browser.opera) { + layer.bringToFront(); + } + self._showTooltip(feature); + }, + mouseout: function (e) { + self._hidePopup(); } - self._showTooltip(feature); - }, - mouseout: function (e) { - self._hidePopup(); - } - }); + }); - self.popups.push(popup); - }; + self.popups.push(popup); + }; - /** - * d3 method returns a darker hex color, - * used for marker stroke color - * - * @method darkerColor - * @param color {String} hex color - * @param amount? {Number} amount to darken by - * @return {String} hex color - */ - BaseMarker.prototype.darkerColor = function (color, amount) { - amount = amount || 1.3; - return d3.hcl(color).darker(amount).toString(); - }; - - BaseMarker.prototype.destroy = function () { - let self = this; - - // remove popups - self.popups = self.popups.filter(function (popup) { - popup.off('mouseover').off('mouseout'); - }); - - if (self._legend) { - self.map.removeControl(self._legend); - self._legend = undefined; - } + /** + * d3 method returns a darker hex color, + * used for marker stroke color + * + * @method darkerColor + * @param color {String} hex color + * @param amount? {Number} amount to darken by + * @return {String} hex color + */ + darkerColor(color, amount) { + amount = amount || 1.3; + return d3.hcl(color).darker(amount).toString(); + }; - // remove marker layer from map - if (self._markerGroup) { - self.map.removeLayer(self._markerGroup); - self._markerGroup = undefined; - } - }; + destroy() { + let self = this; - BaseMarker.prototype._addToMap = function () { - this.map.addLayer(this._markerGroup); - }; + // remove popups + self.popups = self.popups.filter(function (popup) { + popup.off('mouseover').off('mouseout'); + }); - /** - * Creates leaflet marker group, passing options to L.geoJson - * - * @method _createMarkerGroup - * @param options {Object} Options to pass to L.geoJson - */ - BaseMarker.prototype._createMarkerGroup = function (options) { - let self = this; - let defaultOptions = { - onEachFeature: function (feature, layer) { - self.bindPopup(feature, layer); - }, - style: function (feature) { - let value = _.get(feature, 'properties.value'); - return self.applyShadingStyle(value); - }, - filter: self._filterToMapBounds() - }; + if (self._legend) { + self.map.removeControl(self._legend); + self._legend = undefined; + } - this._markerGroup = L.geoJson(this.geoJson, _.defaults(defaultOptions, options)); - this._addToMap(); - }; + // remove marker layer from map + if (self._markerGroup) { + self.map.removeLayer(self._markerGroup); + self._markerGroup = undefined; + } + }; - /** - * return whether feature is within map bounds - * - * @method _filterToMapBounds - * @param map {Leaflet Object} - * @return {boolean} - */ - BaseMarker.prototype._filterToMapBounds = function () { - let self = this; - return function (feature) { - let mapBounds = self.map.getBounds(); - let bucketRectBounds = _.get(feature, 'properties.rectangle'); - return mapBounds.intersects(bucketRectBounds); + _addToMap() { + this.map.addLayer(this._markerGroup); }; - }; - /** - * Checks if event latlng is within bounds of mapData - * features and shows tooltip for that feature - * - * @method _showTooltip - * @param feature {LeafletFeature} - * @param latLng? {Leaflet latLng} - * @return undefined - */ - BaseMarker.prototype._showTooltip = function (feature, latLng) { - if (!this.map) return; - let lat = _.get(feature, 'geometry.coordinates.1'); - let lng = _.get(feature, 'geometry.coordinates.0'); - latLng = latLng || L.latLng(lat, lng); + /** + * Creates leaflet marker group, passing options to L.geoJson + * + * @method _createMarkerGroup + * @param options {Object} Options to pass to L.geoJson + */ + _createMarkerGroup(options) { + let self = this; + let defaultOptions = { + onEachFeature: function (feature, layer) { + self.bindPopup(feature, layer); + }, + style: function (feature) { + let value = _.get(feature, 'properties.value'); + return self.applyShadingStyle(value); + }, + filter: self._filterToMapBounds() + }; + + this._markerGroup = L.geoJson(this.geoJson, _.defaults(defaultOptions, options)); + this._addToMap(); + }; - let content = this._tooltipFormatter(feature); + /** + * return whether feature is within map bounds + * + * @method _filterToMapBounds + * @param map {Leaflet Object} + * @return {boolean} + */ + _filterToMapBounds() { + let self = this; + return function (feature) { + let mapBounds = self.map.getBounds(); + let bucketRectBounds = _.get(feature, 'properties.rectangle'); + return mapBounds.intersects(bucketRectBounds); + }; + }; - if (!content) return; - this._createTooltip(content, latLng); - }; + /** + * Checks if event latlng is within bounds of mapData + * features and shows tooltip for that feature + * + * @method _showTooltip + * @param feature {LeafletFeature} + * @param latLng? {Leaflet latLng} + * @return undefined + */ + _showTooltip(feature, latLng) { + if (!this.map) return; + let lat = _.get(feature, 'geometry.coordinates.1'); + let lng = _.get(feature, 'geometry.coordinates.0'); + latLng = latLng || L.latLng(lat, lng); + + let content = this._tooltipFormatter(feature); + + if (!content) return; + this._createTooltip(content, latLng); + }; - BaseMarker.prototype._createTooltip = function (content, latLng) { - L.popup({autoPan: false}) - .setLatLng(latLng) - .setContent(content) - .openOn(this.map); - }; + _createTooltip(content, latLng) { + L.popup({autoPan: false}) + .setLatLng(latLng) + .setContent(content) + .openOn(this.map); + }; - /** - * Closes the tooltip on the map - * - * @method _hidePopup - * @return undefined - */ - BaseMarker.prototype._hidePopup = function () { - if (!this.map) return; + /** + * Closes the tooltip on the map + * + * @method _hidePopup + * @return undefined + */ + _hidePopup() { + if (!this.map) return; - this.map.closePopup(); - }; + this.map.closePopup(); + }; - /** - * d3 quantize scale returns a hex color, used for marker fill color - * - * @method quantizeLegendColors - * return {undefined} - */ - BaseMarker.prototype.quantizeLegendColors = function () { - let min = _.get(this.geoJson, 'properties.allmin', 0); - let max = _.get(this.geoJson, 'properties.allmax', 1); - let quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain(); - - let reds1 = ['#ff6128']; - let reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c']; - let reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026']; - let bottomCutoff = 2; - let middleCutoff = 24; - - if (max - min <= bottomCutoff) { - this._legendColors = reds1; - } else if (max - min <= middleCutoff) { - this._legendColors = reds3; - } else { - this._legendColors = reds5; - } + /** + * d3 quantize scale returns a hex color, used for marker fill color + * + * @method quantizeLegendColors + * return {undefined} + */ + quantizeLegendColors() { + let min = _.get(this.geoJson, 'properties.allmin', 0); + let max = _.get(this.geoJson, 'properties.allmax', 1); + let quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain(); + + let reds1 = ['#ff6128']; + let reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c']; + let reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026']; + let bottomCutoff = 2; + let middleCutoff = 24; + + if (max - min <= bottomCutoff) { + this._legendColors = reds1; + } else if (max - min <= middleCutoff) { + this._legendColors = reds3; + } else { + this._legendColors = reds5; + } - this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors); - }; + this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors); + }; + } return BaseMarker; }; 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 2dd57ec9fc03..e497f6052d6a 100644 --- a/src/ui/public/vislib/visualizations/marker_types/geohash_grid.js +++ b/src/ui/public/vislib/visualizations/marker_types/geohash_grid.js @@ -12,27 +12,27 @@ export default function GeohashGridMarkerFactory(Private) { * @param geoJson {geoJson Object} * @param params {Object} */ - _.class(GeohashGridMarker).inherits(BaseMarker); - function GeohashGridMarker(map, geoJson, params) { - let self = this; - GeohashGridMarker.Super.apply(this, arguments); + class GeohashGridMarker extends BaseMarker { + constructor(map, geoJson, params) { + super(map, geoJson, params); - // super min and max from all chart data - let min = this.geoJson.properties.allmin; - let max = this.geoJson.properties.allmax; + // super min and max from all chart data + let min = this.geoJson.properties.allmin; + let max = this.geoJson.properties.allmax; - this._createMarkerGroup({ - pointToLayer: function (feature, latlng) { - let geohashRect = feature.properties.rectangle; - // get bounds from northEast[3] and southWest[1] - // corners in geohash rectangle - let corners = [ - [geohashRect[3][0], geohashRect[3][1]], - [geohashRect[1][0], geohashRect[1][1]] - ]; - return L.rectangle(corners); - } - }); + this._createMarkerGroup({ + pointToLayer: function (feature, latlng) { + let geohashRect = feature.properties.rectangle; + // get bounds from northEast[3] and southWest[1] + // corners in geohash rectangle + let corners = [ + [geohashRect[3][0], geohashRect[3][1]], + [geohashRect[1][0], geohashRect[1][1]] + ]; + return L.rectangle(corners); + } + }); + } } return GeohashGridMarker; diff --git a/src/ui/public/vislib/visualizations/marker_types/heatmap.js b/src/ui/public/vislib/visualizations/marker_types/heatmap.js index a066295c4eb2..41dd079b82f4 100644 --- a/src/ui/public/vislib/visualizations/marker_types/heatmap.js +++ b/src/ui/public/vislib/visualizations/marker_types/heatmap.js @@ -13,198 +13,186 @@ export default function HeatmapMarkerFactory(Private) { * @param geoJson {geoJson Object} * @param params {Object} */ - _.class(HeatmapMarker).inherits(BaseMarker); - function HeatmapMarker(map, geoJson, params) { - let self = this; - this._disableTooltips = false; - HeatmapMarker.Super.apply(this, arguments); - - this._createMarkerGroup({ - radius: +this._attr.heatRadius, - blur: +this._attr.heatBlur, - maxZoom: +this._attr.heatMaxZoom, - minOpacity: +this._attr.heatMinOpacity - }); - } - - /** - * Does nothing, heatmaps don't have a legend - * - * @method addLegend - * @return {undefined} - */ - HeatmapMarker.prototype.addLegend = _.noop; - - HeatmapMarker.prototype._createMarkerGroup = function (options) { - let max = _.get(this.geoJson, 'properties.allmax'); - let points = this._dataToHeatArray(max); - - this._markerGroup = L.heatLayer(points, options); - this._fixTooltips(); - this._addToMap(); - }; - - HeatmapMarker.prototype._fixTooltips = function () { - let self = this; - let debouncedMouseMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, { - 'leading': true, - 'trailing': false - }); - - if (!this._disableTooltips && this._attr.addTooltip) { - this.map.on('mousemove', debouncedMouseMoveLocation); - this.map.on('mouseout', function () { - self.map.closePopup(); + class HeatmapMarker extends BaseMarker { + constructor(map, geoJson, params) { + super(map, geoJson, params); + this._disableTooltips = false; + + this._createMarkerGroup({ + radius: +this._attr.heatRadius, + blur: +this._attr.heatBlur, + maxZoom: +this._attr.heatMaxZoom, + minOpacity: +this._attr.heatMinOpacity }); - this.map.on('mousedown', function () { - self._disableTooltips = true; - self.map.closePopup(); - }); - this.map.on('mouseup', function () { - self._disableTooltips = false; + + this.addLegend = _.noop; + + this._getLatLng = _.memoize(function (feature) { + return L.latLng( + feature.geometry.coordinates[1], + feature.geometry.coordinates[0] + ); + }, function (feature) { + // turn coords into a string for the memoize cache + return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(','); }); } - function mouseMoveLocation(e) { - let latlng = e.latlng; + _createMarkerGroup(options) { + let max = _.get(this.geoJson, 'properties.allmax'); + let points = this._dataToHeatArray(max); - this.map.closePopup(); + this._markerGroup = L.heatLayer(points, options); + this._fixTooltips(); + this._addToMap(); + }; - // unhighlight all svgs - d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false); + _fixTooltips() { + let self = this; + let debouncedMouseMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, { + 'leading': true, + 'trailing': false + }); - if (!this.geoJson.features.length || this._disableTooltips) { - return; + if (!this._disableTooltips && this._attr.addTooltip) { + this.map.on('mousemove', debouncedMouseMoveLocation); + this.map.on('mouseout', function () { + self.map.closePopup(); + }); + this.map.on('mousedown', function () { + self._disableTooltips = true; + self.map.closePopup(); + }); + this.map.on('mouseup', function () { + self._disableTooltips = false; + }); } - // find nearest feature to event latlng - let feature = this._nearestFeature(latlng); + function mouseMoveLocation(e) { + let latlng = e.latlng; - // show tooltip if close enough to event latlng - if (this._tooltipProximity(latlng, feature)) { - this._showTooltip(feature, latlng); - } - } - }; + this.map.closePopup(); - /** - * returns a memoized Leaflet latLng for given geoJson feature - * - * @method addLatLng - * @param feature {geoJson Object} - * @return {Leaflet latLng Object} - */ - HeatmapMarker.prototype._getLatLng = _.memoize(function (feature) { - return L.latLng( - feature.geometry.coordinates[1], - feature.geometry.coordinates[0] - ); - }, function (feature) { - // turn coords into a string for the memoize cache - return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(','); - }); + // unhighlight all svgs + d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false); - /** - * Finds nearest feature in mapData to event latlng - * - * @method _nearestFeature - * @param latLng {Leaflet latLng} - * @return nearestPoint {Leaflet latLng} - */ - HeatmapMarker.prototype._nearestFeature = function (latLng) { - let self = this; - let nearest; + if (!this.geoJson.features.length || this._disableTooltips) { + return; + } - if (latLng.lng < -180 || latLng.lng > 180) { - return; - } + // find nearest feature to event latlng + let feature = this._nearestFeature(latlng); - _.reduce(this.geoJson.features, function (distance, feature) { - let featureLatLng = self._getLatLng(feature); - let dist = latLng.distanceTo(featureLatLng); - - if (dist < distance) { - nearest = feature; - return dist; + // show tooltip if close enough to event latlng + if (this._tooltipProximity(latlng, feature)) { + this._showTooltip(feature, latlng); + } + } + }; + + /** + * Finds nearest feature in mapData to event latlng + * + * @method _nearestFeature + * @param latLng {Leaflet latLng} + * @return nearestPoint {Leaflet latLng} + */ + _nearestFeature(latLng) { + let self = this; + let nearest; + + if (latLng.lng < -180 || latLng.lng > 180) { + return; } - return distance; - }, Infinity); - - return nearest; - }; - - /** - * display tooltip if feature is close enough to event latlng - * - * @method _tooltipProximity - * @param latlng {Leaflet latLng Object} - * @param feature {geoJson Object} - * @return {Boolean} - */ - HeatmapMarker.prototype._tooltipProximity = function (latlng, feature) { - if (!feature) return; - - let showTip = false; - let featureLatLng = this._getLatLng(feature); - - // zoomScale takes map zoom and returns proximity value for tooltip display - // domain (input values) is map zoom (min 1 and max 18) - // range (output values) is distance in meters - // used to compare proximity of event latlng to feature latlng - let zoomScale = d3.scale.linear() - .domain([1, 4, 7, 10, 13, 16, 18]) - .range([1000000, 300000, 100000, 15000, 2000, 150, 50]); - - let proximity = zoomScale(this.map.getZoom()); - let distance = latlng.distanceTo(featureLatLng); - - // maxLngDif is max difference in longitudes - // to prevent feature tooltip from appearing 360° - // away from event latlng - let maxLngDif = 40; - let lngDif = Math.abs(latlng.lng - featureLatLng.lng); - - if (distance < proximity && lngDif < maxLngDif) { - showTip = true; - } - - let testScale = d3.scale.pow().exponent(0.2) - .domain([1, 18]) - .range([1500000, 50]); - return showTip; - }; - - - /** - * returns data for data for heat map intensity - * if heatNormalizeData attribute is checked/true - • normalizes data for heat map intensity - * - * @method _dataToHeatArray - * @param max {Number} - * @return {Array} - */ - HeatmapMarker.prototype._dataToHeatArray = function (max) { - let self = this; - let mapData = this.geoJson; - - return this.geoJson.features.map(function (feature) { - let lat = feature.properties.center[0]; - let lng = feature.properties.center[1]; - let heatIntensity; - - if (!self._attr.heatNormalizeData) { - // show bucket value on heatmap - heatIntensity = feature.properties.value; - } else { - // show bucket value normalized to max value - heatIntensity = feature.properties.value / max; + _.reduce(this.geoJson.features, function (distance, feature) { + let featureLatLng = self._getLatLng(feature); + let dist = latLng.distanceTo(featureLatLng); + + if (dist < distance) { + nearest = feature; + return dist; + } + + return distance; + }, Infinity); + + return nearest; + }; + + /** + * display tooltip if feature is close enough to event latlng + * + * @method _tooltipProximity + * @param latlng {Leaflet latLng Object} + * @param feature {geoJson Object} + * @return {Boolean} + */ + _tooltipProximity(latlng, feature) { + if (!feature) return; + + let showTip = false; + let featureLatLng = this._getLatLng(feature); + + // zoomScale takes map zoom and returns proximity value for tooltip display + // domain (input values) is map zoom (min 1 and max 18) + // range (output values) is distance in meters + // used to compare proximity of event latlng to feature latlng + let zoomScale = d3.scale.linear() + .domain([1, 4, 7, 10, 13, 16, 18]) + .range([1000000, 300000, 100000, 15000, 2000, 150, 50]); + + let proximity = zoomScale(this.map.getZoom()); + let distance = latlng.distanceTo(featureLatLng); + + // maxLngDif is max difference in longitudes + // to prevent feature tooltip from appearing 360° + // away from event latlng + let maxLngDif = 40; + let lngDif = Math.abs(latlng.lng - featureLatLng.lng); + + if (distance < proximity && lngDif < maxLngDif) { + showTip = true; } - return [lat, lng, heatIntensity]; - }); - }; + let testScale = d3.scale.pow().exponent(0.2) + .domain([1, 18]) + .range([1500000, 50]); + return showTip; + }; + + + /** + * returns data for data for heat map intensity + * if heatNormalizeData attribute is checked/true + • normalizes data for heat map intensity + * + * @method _dataToHeatArray + * @param max {Number} + * @return {Array} + */ + _dataToHeatArray(max) { + let self = this; + let mapData = this.geoJson; + + return this.geoJson.features.map(function (feature) { + let lat = feature.properties.center[0]; + let lng = feature.properties.center[1]; + let heatIntensity; + + if (!self._attr.heatNormalizeData) { + // show bucket value on heatmap + heatIntensity = feature.properties.value; + } else { + // show bucket value normalized to max value + heatIntensity = feature.properties.value / max; + } + + return [lat, lng, heatIntensity]; + }); + }; + } + return HeatmapMarker; }; 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 8a0c84841dc7..58495bc5bcc0 100644 --- a/src/ui/public/vislib/visualizations/marker_types/scaled_circles.js +++ b/src/ui/public/vislib/visualizations/marker_types/scaled_circles.js @@ -12,48 +12,48 @@ export default function ScaledCircleMarkerFactory(Private) { * @param mapData {geoJson Object} * @param params {Object} */ - _.class(ScaledCircleMarker).inherits(BaseMarker); - function ScaledCircleMarker(map, geoJson, params) { - let self = this; - ScaledCircleMarker.Super.apply(this, arguments); - - // multiplier to reduce size of all circles - let scaleFactor = 0.6; - - this._createMarkerGroup({ - pointToLayer: function (feature, latlng) { - let value = feature.properties.value; - let scaledRadius = self._radiusScale(value) * scaleFactor; - return L.circleMarker(latlng).setRadius(scaledRadius); - } - }); + class ScaledCircleMarker extends BaseMarker { + constructor(map, geoJson, params) { + super(map, geoJson, params); + + // multiplier to reduce size of all circles + let scaleFactor = 0.6; + + this._createMarkerGroup({ + pointToLayer: (feature, latlng) => { + let value = feature.properties.value; + let scaledRadius = this._radiusScale(value) * scaleFactor; + return L.circleMarker(latlng).setRadius(scaledRadius); + } + }); + } + + /** + * radiusScale returns a number for scaled circle markers + * for relative sizing of markers + * + * @method _radiusScale + * @param value {Number} + * @return {Number} + */ + _radiusScale(value) { + let precisionBiasBase = 5; + let precisionBiasNumerator = 200; + let zoom = this.map.getZoom(); + let maxValue = this.geoJson.properties.allmax; + let precision = _.max(this.geoJson.features.map(function (feature) { + return String(feature.properties.geohash).length; + })); + + let pct = Math.abs(value) / Math.abs(maxValue); + let zoomRadius = 0.5 * Math.pow(2, zoom); + let precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision); + + // square root value percentage + return Math.pow(pct, 0.5) * zoomRadius * precisionScale; + }; } - /** - * radiusScale returns a number for scaled circle markers - * for relative sizing of markers - * - * @method _radiusScale - * @param value {Number} - * @return {Number} - */ - ScaledCircleMarker.prototype._radiusScale = function (value) { - let precisionBiasBase = 5; - let precisionBiasNumerator = 200; - let zoom = this.map.getZoom(); - let maxValue = this.geoJson.properties.allmax; - let precision = _.max(this.geoJson.features.map(function (feature) { - return String(feature.properties.geohash).length; - })); - - let pct = Math.abs(value) / Math.abs(maxValue); - let zoomRadius = 0.5 * Math.pow(2, zoom); - let precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision); - - // square root value percentage - return Math.pow(pct, 0.5) * zoomRadius * precisionScale; - }; - return ScaledCircleMarker; }; 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 c873f2f9c161..b18dc2638bdb 100644 --- a/src/ui/public/vislib/visualizations/marker_types/shaded_circles.js +++ b/src/ui/public/vislib/visualizations/marker_types/shaded_circles.js @@ -12,56 +12,58 @@ export default function ShadedCircleMarkerFactory(Private) { * @param mapData {geoJson Object} * @return {Leaflet object} featureLayer */ - _.class(ShadedCircleMarker).inherits(BaseMarker); - function ShadedCircleMarker(map, geoJson, params) { - let self = this; - ShadedCircleMarker.Super.apply(this, arguments); + class ShadedCircleMarker extends BaseMarker { + constructor(map, geoJson, params) { + super(map, geoJson, params); - // super min and max from all chart data - let min = this.geoJson.properties.allmin; - let max = this.geoJson.properties.allmax; + // super min and max from all chart data + let min = this.geoJson.properties.allmin; + let max = this.geoJson.properties.allmax; - // multiplier to reduce size of all circles - let scaleFactor = 0.8; + // multiplier to reduce size of all circles + let scaleFactor = 0.8; - this._createMarkerGroup({ - pointToLayer: function (feature, latlng) { - let radius = self._geohashMinDistance(feature) * scaleFactor; - return L.circle(latlng, radius); - } - }); - } + this._createMarkerGroup({ + pointToLayer: (feature, latlng) => { + let radius = this._geohashMinDistance(feature) * scaleFactor; + return L.circle(latlng, radius); + } + }); + } - /** - * _geohashMinDistance returns a min distance in meters for sizing - * circle markers to fit within geohash grid rectangle - * - * @method _geohashMinDistance - * @param feature {Object} - * @return {Number} - */ - ShadedCircleMarker.prototype._geohashMinDistance = function (feature) { - let centerPoint = _.get(feature, 'properties.center'); - let geohashRect = _.get(feature, 'properties.rectangle'); - // centerPoint is an array of [lat, lng] - // geohashRect is the 4 corners of the geoHash rectangle - // an array that starts at the southwest corner and proceeds - // clockwise, each value being an array of [lat, lng] + /** + * _geohashMinDistance returns a min distance in meters for sizing + * circle markers to fit within geohash grid rectangle + * + * @method _geohashMinDistance + * @param feature {Object} + * @return {Number} + */ + _geohashMinDistance(feature) { + let centerPoint = _.get(feature, 'properties.center'); + let geohashRect = _.get(feature, 'properties.rectangle'); - // center lat and southeast lng - let east = L.latLng([centerPoint[0], geohashRect[2][1]]); - // southwest lat and center lng - let north = L.latLng([geohashRect[3][0], centerPoint[1]]); + // centerPoint is an array of [lat, lng] + // geohashRect is the 4 corners of the geoHash rectangle + // an array that starts at the southwest corner and proceeds + // clockwise, each value being an array of [lat, lng] - // get latLng of geohash center point - let center = L.latLng([centerPoint[0], centerPoint[1]]); + // center lat and southeast lng + let east = L.latLng([centerPoint[0], geohashRect[2][1]]); + // southwest lat and center lng + let north = L.latLng([geohashRect[3][0], centerPoint[1]]); + + // get latLng of geohash center point + let center = L.latLng([centerPoint[0], centerPoint[1]]); + + // get smallest radius at center of geohash grid rectangle + let eastRadius = Math.floor(center.distanceTo(east)); + let northRadius = Math.floor(center.distanceTo(north)); + return _.min([eastRadius, northRadius]); + }; + } - // get smallest radius at center of geohash grid rectangle - let eastRadius = Math.floor(center.distanceTo(east)); - let northRadius = Math.floor(center.distanceTo(north)); - return _.min([eastRadius, northRadius]); - }; return ShadedCircleMarker; }; diff --git a/src/ui/public/vislib/visualizations/pie_chart.js b/src/ui/public/vislib/visualizations/pie_chart.js index b18c0820942c..b67f59819343 100644 --- a/src/ui/public/vislib/visualizations/pie_chart.js +++ b/src/ui/public/vislib/visualizations/pie_chart.js @@ -17,193 +17,197 @@ export default function PieChartFactory(Private) { * @param el {HTMLElement} HTML element to which the chart will be appended * @param chartData {Object} Elasticsearch query results for this specific chart */ - _.class(PieChart).inherits(Chart); - function PieChart(handler, chartEl, chartData) { - if (!(this instanceof PieChart)) { - return new PieChart(handler, chartEl, chartData); - } - PieChart.Super.apply(this, arguments); - - let charts = this.handler.data.getVisData(); - this._validatePieData(charts); - - this._attr = _.defaults(handler._attr || {}, { - isDonut: handler._attr.isDonut || false - }); - } + class PieChart extends Chart { + constructor(handler, chartEl, chartData) { + super(handler, chartEl, chartData); - /** - * Checks whether pie slices have all zero values. - * If so, an error is thrown. - */ - PieChart.prototype._validatePieData = function (charts) { - let isAllZeros = charts.every(function (chart) { - return chart.slices.children.length === 0; - }); + let charts = this.handler.data.getVisData(); + this._validatePieData(charts); - if (isAllZeros) { throw new errors.PieContainsAllZeros(); } - }; + this._attr = _.defaults(handler._attr || {}, { + isDonut: handler._attr.isDonut || false + }); + } - /** - * Adds Events to SVG paths - * - * @method addPathEvents - * @param element {D3.Selection} Reference to SVG path - * @returns {D3.Selection} SVG path with event listeners attached - */ - PieChart.prototype.addPathEvents = function (element) { - let events = this.events; - - return element - .call(events.addHoverEvent()) - .call(events.addMouseoutEvent()) - .call(events.addClickEvent()); - }; - - PieChart.prototype.convertToPercentage = function (slices) { - (function assignPercentages(slices) { - if (slices.sumOfChildren != null) return; - - let parent = slices; - let children = parent.children; - let parentPercent = parent.percentOfParent; - - let sum = parent.sumOfChildren = Math.abs(children.reduce(function (sum, child) { - return sum + Math.abs(child.size); - }, 0)); - - children.forEach(function (child) { - child.percentOfGroup = Math.abs(child.size) / sum; - child.percentOfParent = child.percentOfGroup; - - if (parentPercent != null) { - child.percentOfParent *= parentPercent; - } - - if (child.children) { - assignPercentages(child); - } + /** + * Checks whether pie slices have all zero values. + * If so, an error is thrown. + */ + _validatePieData(charts) { + let isAllZeros = charts.every(function (chart) { + return chart.slices.children.length === 0; }); - }(slices)); - }; - /** - * Adds pie paths to SVG - * - * @method addPath - * @param width {Number} Width of SVG - * @param height {Number} Height of SVG - * @param svg {HTMLElement} Chart SVG - * @param slices {Object} Chart data - * @returns {D3.Selection} SVG with paths attached - */ - PieChart.prototype.addPath = function (width, height, svg, slices) { - let self = this; - let marginFactor = 0.95; - let isDonut = self._attr.isDonut; - let radius = (Math.min(width, height) / 2) * marginFactor; - let color = self.handler.data.getPieColorFunc(); - let tooltip = self.tooltip; - let isTooltip = self._attr.addTooltip; - - let partition = d3.layout.partition() - .sort(null) - .value(function (d) { - return d.percentOfParent * 100; - }); - let x = d3.scale.linear() - .range([0, 2 * Math.PI]); - let y = d3.scale.sqrt() - .range([0, radius]); - let arc = d3.svg.arc() - .startAngle(function (d) { - return Math.max(0, Math.min(2 * Math.PI, x(d.x))); - }) - .endAngle(function (d) { - return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); - }) - .innerRadius(function (d) { - // option for a single layer, i.e pie chart - if (d.depth === 1 && !isDonut) { - // return no inner radius - return 0; + if (isAllZeros) { + throw new errors.PieContainsAllZeros(); } + }; - return Math.max(0, y(d.y)); - }) - .outerRadius(function (d) { - return Math.max(0, y(d.y + d.dy)); - }); - - let path = svg - .datum(slices) - .selectAll('path') - .data(partition.nodes) - .enter() - .append('path') - .attr('d', arc) - .attr('class', function (d) { - if (d.depth === 0) { return; } - return 'slice'; - }) - .call(self._addIdentifier, 'name') - .style('stroke', '#fff') - .style('fill', function (d) { - if (d.depth === 0) { return 'none'; } - return color(d.name); - }); + /** + * Adds Events to SVG paths + * + * @method addPathEvents + * @param element {D3.Selection} Reference to SVG path + * @returns {D3.Selection} SVG path with event listeners attached + */ + addPathEvents(element) { + let events = this.events; + + return element + .call(events.addHoverEvent()) + .call(events.addMouseoutEvent()) + .call(events.addClickEvent()); + }; - if (isTooltip) { - path.call(tooltip.render()); - } + convertToPercentage(slices) { + (function assignPercentages(slices) { + if (slices.sumOfChildren != null) return; - return path; - }; + let parent = slices; + let children = parent.children; + let parentPercent = parent.percentOfParent; - PieChart.prototype._validateContainerSize = function (width, height) { - let minWidth = 20; - let minHeight = 20; + let sum = parent.sumOfChildren = Math.abs(children.reduce(function (sum, child) { + return sum + Math.abs(child.size); + }, 0)); - if (width <= minWidth || height <= minHeight) { - throw new errors.ContainerTooSmall(); - } - }; + children.forEach(function (child) { + child.percentOfGroup = Math.abs(child.size) / sum; + child.percentOfParent = child.percentOfGroup; - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the pie chart - */ - PieChart.prototype.draw = function () { - let self = this; + if (parentPercent != null) { + child.percentOfParent *= parentPercent; + } - return function (selection) { - selection.each(function (data) { - let slices = data.slices; - let div = d3.select(this); - let width = $(this).width(); - let height = $(this).height(); - let path; + if (child.children) { + assignPercentages(child); + } + }); + }(slices)); + }; - if (!slices.children.length) return; + /** + * Adds pie paths to SVG + * + * @method addPath + * @param width {Number} Width of SVG + * @param height {Number} Height of SVG + * @param svg {HTMLElement} Chart SVG + * @param slices {Object} Chart data + * @returns {D3.Selection} SVG with paths attached + */ + addPath(width, height, svg, slices) { + let self = this; + let marginFactor = 0.95; + let isDonut = self._attr.isDonut; + let radius = (Math.min(width, height) / 2) * marginFactor; + let color = self.handler.data.getPieColorFunc(); + let tooltip = self.tooltip; + let isTooltip = self._attr.addTooltip; + + let partition = d3.layout.partition() + .sort(null) + .value(function (d) { + return d.percentOfParent * 100; + }); + let x = d3.scale.linear() + .range([0, 2 * Math.PI]); + let y = d3.scale.sqrt() + .range([0, radius]); + let arc = d3.svg.arc() + .startAngle(function (d) { + return Math.max(0, Math.min(2 * Math.PI, x(d.x))); + }) + .endAngle(function (d) { + return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); + }) + .innerRadius(function (d) { + // option for a single layer, i.e pie chart + if (d.depth === 1 && !isDonut) { + // return no inner radius + return 0; + } + + return Math.max(0, y(d.y)); + }) + .outerRadius(function (d) { + return Math.max(0, y(d.y + d.dy)); + }); + + let path = svg + .datum(slices) + .selectAll('path') + .data(partition.nodes) + .enter() + .append('path') + .attr('d', arc) + .attr('class', function (d) { + if (d.depth === 0) { + return; + } + return 'slice'; + }) + .call(self._addIdentifier, 'name') + .style('stroke', '#fff') + .style('fill', function (d) { + if (d.depth === 0) { + return 'none'; + } + return color(d.name); + }); + + if (isTooltip) { + path.call(tooltip.render()); + } - self.convertToPercentage(slices); - self._validateContainerSize(width, height); + return path; + }; - let svg = div.append('svg') - .attr('width', width) - .attr('height', height) - .append('g') - .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); + _validateContainerSize(width, height) { + let minWidth = 20; + let minHeight = 20; - path = self.addPath(width, height, svg, slices); - self.addPathEvents(path); + if (width <= minWidth || height <= minHeight) { + throw new errors.ContainerTooSmall(); + } + }; - return svg; - }); + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the pie chart + */ + draw() { + let self = this; + + return function (selection) { + selection.each(function (data) { + let slices = data.slices; + let div = d3.select(this); + let width = $(this).width(); + let height = $(this).height(); + let path; + + if (!slices.children.length) return; + + self.convertToPercentage(slices); + self._validateContainerSize(width, height); + + let svg = div.append('svg') + .attr('width', width) + .attr('height', height) + .append('g') + .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); + + path = self.addPath(width, height, svg, slices); + self.addPathEvents(path); + + return svg; + }); + }; }; - }; + } return PieChart; }; diff --git a/src/ui/public/vislib/visualizations/tile_map.js b/src/ui/public/vislib/visualizations/tile_map.js index bc3d3bab83a7..b41a987028c2 100644 --- a/src/ui/public/vislib/visualizations/tile_map.js +++ b/src/ui/public/vislib/visualizations/tile_map.js @@ -18,117 +18,114 @@ export default function TileMapFactory(Private) { * @param chartEl {HTMLElement} HTML element to which the map will be appended * @param chartData {Object} Elasticsearch query results for this map */ - _.class(TileMap).inherits(Chart); - function TileMap(handler, chartEl, chartData) { - if (!(this instanceof TileMap)) { - return new TileMap(handler, chartEl, chartData); - } - - TileMap.Super.apply(this, arguments); + class TileMap extends Chart { + constructor(handler, chartEl, chartData) { + super(handler, chartEl, chartData); - // track the map objects - this.maps = []; - this._chartData = chartData || {}; - _.assign(this, this._chartData); - - this._appendGeoExtents(); - } + // track the map objects + this.maps = []; + this._chartData = chartData || {}; + _.assign(this, this._chartData); - /** - * Draws tile map, called on chart render - * - * @method draw - * @return {Function} - function to add a map to a selection - */ - TileMap.prototype.draw = function () { - let self = this; + this._appendGeoExtents(); + } - // clean up old maps - self.destroy(); + /** + * Draws tile map, called on chart render + * + * @method draw + * @return {Function} - function to add a map to a selection + */ + draw() { + let self = this; + + // clean up old maps + self.destroy(); + + return function (selection) { + selection.each(function () { + self._appendMap(this); + }); + }; + }; - return function (selection) { - selection.each(function () { - self._appendMap(this); + /** + * Invalidate the size of the map, so that leaflet will resize to fit. + * then moves to center + * + * @method resizeArea + * @return {undefined} + */ + resizeArea() { + this.maps.forEach(function (map) { + map.updateSize(); }); }; - }; - /** - * Invalidate the size of the map, so that leaflet will resize to fit. - * then moves to center - * - * @method resizeArea - * @return {undefined} - */ - TileMap.prototype.resizeArea = function () { - this.maps.forEach(function (map) { - map.updateSize(); - }); - }; + /** + * clean up the maps + * + * @method destroy + * @return {undefined} + */ + destroy() { + this.maps = this.maps.filter(function (map) { + map.destroy(); + }); + }; - /** - * clean up the maps - * - * @method destroy - * @return {undefined} - */ - TileMap.prototype.destroy = function () { - this.maps = this.maps.filter(function (map) { - map.destroy(); - }); - }; + /** + * Adds allmin and allmax properties to geoJson data + * + * @method _appendMap + * @param selection {Object} d3 selection + */ + _appendGeoExtents() { + // add allmin and allmax to geoJson + let geoMinMax = this.handler.data.getGeoExtents(); + this.geoJson.properties.allmin = geoMinMax.min; + this.geoJson.properties.allmax = geoMinMax.max; + }; - /** - * Adds allmin and allmax properties to geoJson data - * - * @method _appendMap - * @param selection {Object} d3 selection - */ - TileMap.prototype._appendGeoExtents = function () { - // add allmin and allmax to geoJson - let geoMinMax = this.handler.data.getGeoExtents(); - this.geoJson.properties.allmin = geoMinMax.min; - this.geoJson.properties.allmax = geoMinMax.max; - }; + /** + * Renders map + * + * @method _appendMap + * @param selection {Object} d3 selection + */ + _appendMap(selection) { + const container = $(selection).addClass('tilemap'); + const uiStateParams = this.handler.vis ? { + mapCenter: this.handler.vis.uiState.get('mapCenter'), + mapZoom: this.handler.vis.uiState.get('mapZoom') + } : {}; + + const params = _.assign({}, _.get(this._chartData, 'geoAgg.vis.params'), uiStateParams); + + const map = new TileMapMap(container, this._chartData, { + center: params.mapCenter, + zoom: params.mapZoom, + events: this.events, + markerType: this._attr.mapType, + tooltipFormatter: this.tooltipFormatter, + valueFormatter: this.valueFormatter, + attr: this._attr + }); - /** - * Renders map - * - * @method _appendMap - * @param selection {Object} d3 selection - */ - TileMap.prototype._appendMap = function (selection) { - const container = $(selection).addClass('tilemap'); - const uiStateParams = this.handler.vis ? { - mapCenter: this.handler.vis.uiState.get('mapCenter'), - mapZoom: this.handler.vis.uiState.get('mapZoom') - } : {}; - - const params = _.assign({}, _.get(this._chartData, 'geoAgg.vis.params'), uiStateParams); - - const map = new TileMapMap(container, this._chartData, { - center: params.mapCenter, - zoom: params.mapZoom, - events: this.events, - markerType: this._attr.mapType, - tooltipFormatter: this.tooltipFormatter, - valueFormatter: this.valueFormatter, - attr: this._attr - }); - - // add title for splits - if (this.title) { - map.addTitle(this.title); - } + // add title for splits + if (this.title) { + map.addTitle(this.title); + } - // add fit to bounds control - if (_.get(this.geoJson, 'features.length') > 0) { - map.addFitControl(); - map.addBoundingControl(); - } + // add fit to bounds control + if (_.get(this.geoJson, 'features.length') > 0) { + map.addFitControl(); + map.addBoundingControl(); + } - this.maps.push(map); - }; + this.maps.push(map); + }; + } return TileMap; }; diff --git a/src/ui/public/vislib/visualizations/time_marker.js b/src/ui/public/vislib/visualizations/time_marker.js index 19fbc2fb9ee5..1606fd9d2461 100644 --- a/src/ui/public/vislib/visualizations/time_marker.js +++ b/src/ui/public/vislib/visualizations/time_marker.js @@ -2,72 +2,70 @@ import d3 from 'd3'; import dateMath from '@elastic/datemath'; export default function TimeMarkerFactory() { - function TimeMarker(times, xScale, height) { - if (!(this instanceof TimeMarker)) { - return new TimeMarker(times, xScale, height); - } - - let currentTimeArr = [{ - 'time': new Date().getTime(), - 'class': 'time-marker', - 'color': '#c80000', - 'opacity': 0.3, - 'width': 2 - }]; + class TimeMarker { + constructor(times, xScale, height) { + let currentTimeArr = [{ + 'time': new Date().getTime(), + 'class': 'time-marker', + 'color': '#c80000', + 'opacity': 0.3, + 'width': 2 + }]; - this.xScale = xScale; - this.height = height; - this.times = (times.length) ? times.map(function (d) { - return { - 'time': dateMath.parse(d.time), - 'class': d.class || 'time-marker', - 'color': d.color || '#c80000', - 'opacity': d.opacity || 0.3, - 'width': d.width || 2 - }; - }) : currentTimeArr; - } + this.xScale = xScale; + this.height = height; + this.times = (times.length) ? times.map(function (d) { + return { + 'time': dateMath.parse(d.time), + 'class': d.class || 'time-marker', + 'color': d.color || '#c80000', + 'opacity': d.opacity || 0.3, + 'width': d.width || 2 + }; + }) : currentTimeArr; + } - TimeMarker.prototype._isTimeBasedChart = function (selection) { - let data = selection.data(); - return data.every(function (datum) { - return (datum.ordered && datum.ordered.date); - }); - }; + _isTimeBasedChart(selection) { + let data = selection.data(); + return data.every(function (datum) { + return (datum.ordered && datum.ordered.date); + }); + }; - TimeMarker.prototype.render = function (selection) { - let self = this; + render(selection) { + let self = this; - // return if not time based chart - if (!self._isTimeBasedChart(selection)) return; + // return if not time based chart + if (!self._isTimeBasedChart(selection)) return; - selection.each(function () { - d3.select(this).selectAll('time-marker') - .data(self.times) - .enter().append('line') - .attr('class', function (d) { - return d.class; - }) - .attr('pointer-events', 'none') - .attr('stroke', function (d) { - return d.color; - }) - .attr('stroke-width', function (d) { - return d.width; - }) - .attr('stroke-opacity', function (d) { - return d.opacity; - }) - .attr('x1', function (d) { - return self.xScale(d.time); - }) - .attr('x2', function (d) { - return self.xScale(d.time); - }) - .attr('y1', self.height) - .attr('y2', self.xScale.range()[0]); - }); - }; + selection.each(function () { + d3.select(this).selectAll('time-marker') + .data(self.times) + .enter().append('line') + .attr('class', function (d) { + return d.class; + }) + .attr('pointer-events', 'none') + .attr('stroke', function (d) { + return d.color; + }) + .attr('stroke-width', function (d) { + return d.width; + }) + .attr('stroke-opacity', function (d) { + return d.opacity; + }) + .attr('x1', function (d) { + return self.xScale(d.time); + }) + .attr('x2', function (d) { + return self.xScale(d.time); + }) + .attr('y1', self.height) + .attr('y2', self.xScale.range()[0]); + }); + }; + } return TimeMarker; };