diff --git a/src/kibana/components/vislib/lib/data.js b/src/kibana/components/vislib/lib/data.js index 336f54a57ea89..ad588924736d9 100644 --- a/src/kibana/components/vislib/lib/data.js +++ b/src/kibana/components/vislib/lib/data.js @@ -34,6 +34,9 @@ define(function (require) { } this.data = data; + this.type = this.getDataType(); + this.labels = (this.type === 'series') ? getLabels(data) : this.pieNames(); + this.color = color(this.labels); this._normalizeOrdered(); this._attr = _.defaults(attr || {}, { @@ -46,6 +49,21 @@ define(function (require) { }); } + Data.prototype.getDataType = function () { + var data = this.getVisData(); + var type; + + data.forEach(function (obj) { + if (obj.series) { + type = 'series'; + } else if (obj.slices) { + type = 'slices'; + } + }); + + return type; + }; + /** * Returns an array of the actual x and y data value objects * from data with series keys diff --git a/src/kibana/components/vislib/lib/dispatch.js b/src/kibana/components/vislib/lib/dispatch.js index 33947556f731f..e394fb8f99026 100644 --- a/src/kibana/components/vislib/lib/dispatch.js +++ b/src/kibana/components/vislib/lib/dispatch.js @@ -8,22 +8,16 @@ define(function (require) { * @class Dispatch * @constructor * @param handler {Object} Reference to Handler Class Object - * @param chartData {Object} Elasticsearch data object */ - function Dispatch(handler, chartData) { + function Dispatch(handler) { if (!(this instanceof Dispatch)) { - return new Dispatch(handler, chartData); + return new Dispatch(handler); } - var type = handler._attr.type; this.handler = handler; - this.chartData = chartData; - this.color = type === 'pie' ? handler.data.getPieColorFunc() : handler.data.getColorFunc(); - this._attr = _.defaults(handler._attr || {}, { - yValue: function (d) { return d.y; }, - dispatch: d3.dispatch('brush', 'click', 'hover', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout') - }); + this.dispatch = d3.dispatch('brush', 'click', 'hover', 'mouseup', + 'mousedown', 'mouseover'); } /** @@ -31,21 +25,25 @@ define(function (require) { * * @param d {Object} Data point * @param i {Number} Index number of data point - * @returns {{value: *, point: *, label: *, color: *, pointIndex: *, series: *, config: *, data: (Object|*), + * @returns {{value: *, point: *, label: *, color: *, pointIndex: *, + * series: *, config: *, data: (Object|*), * e: (d3.event|*), handler: (Object|*)}} Event response object */ Dispatch.prototype.eventResponse = function (d, i) { - var isPercentage = (this._attr.mode === 'percentage'); - var label = d.label; - var getYValue = this._attr.yValue; - var color = this.color; - var chartData = this.chartData; - var attr = this._attr; + var data = d3.event.target.nearestViewportElement.__data__; + var label = d.label ? d.label : d.name; + var isSeries = !!(data.series); + var isSlices = !!(data.slices); + var series = isSeries ? data.series : undefined; + var slices = isSlices ? data.slices : undefined; var handler = this.handler; + var color = handler.data.color; + var isPercentage = (handler._attr.mode === 'percentage'); + + if (isSeries) { - if (chartData.series) { // Find object with the actual d value and add it to the point object - var object = _.find(chartData.series, { 'label': label }); + var object = _.find(series, { 'label': d.label }); d.value = +object.values[i].y; if (isPercentage) { @@ -56,50 +54,125 @@ define(function (require) { } return { - value: getYValue(d, i), + value: d.y, point: d, label: label, color: color(label), pointIndex: i, - series: chartData.series, - config: attr, - data: chartData, + series: series, + slices: slices, + config: handler._attr, + data: data, e: d3.event, handler: handler }; }; /** - * Response to click and hover events for pie charts + * Returns a function that adds events and listeners to a D3 selection * - * @param d {Object} Data point - * @param i {Number} Index number of data point - * @returns {{value: (d.value|*), point: *, label: (d.name|*), color: *, pointIndex: *, children: *, parent: *, - * appConfig: *, config: *, data: (Object|*), e: (d3.event|*), handler: (Object|*)}} Event response object + * @method addEvent + * @param event {String} + * @param callback {Function} + * @returns {Function} */ - Dispatch.prototype.pieResponse = function (d, i) { - var label = d.name; - var color = this.color; - var chartData = this.chartData; - var attr = this._attr; - var handler = this.handler; + Dispatch.prototype.addEvent = function (event, callback) { + return function (selection) { + selection.each(function () { + var element = d3.select(this); - return { - value: d.value, - point: d, - label: label, - color: color(label), - pointIndex: i, - children: d.children ? d.children : undefined, - parent: d.parent ? d.parent : undefined, - appConfig: d.appConfig, - config: attr, - data: chartData, - e: d3.event, - handler: handler + if (typeof callback === 'function') { + return element.on(event, callback); + } + }); }; }; + /** + * + * @method addHoverEvent + * @returns {Function} + */ + Dispatch.prototype.addHoverEvent = function () { + var self = this; + var isClickable = (this.dispatch.on('click')); + var addEvent = this.addEvent; + + function hover(d, i) { + d3.event.stopPropagation(); + + // Add pointer if item is clickable + if (isClickable) { + self.addMousePointer.call(this, arguments); + } + + self.dispatch.hover.call(this, self.eventResponse(d, i)); + } + + return addEvent('mouseover', hover); + }; + + /** + * + * @method addClickEvent + * @returns {Function} + */ + Dispatch.prototype.addClickEvent = function () { + var self = this; + var addEvent = this.addEvent; + + function click(d, i) { + d3.event.stopPropagation(); + self.dispatch.click.call(this, self.eventResponse(d, i)); + } + + return addEvent('click', click); + }; + + /** + * + * @param svg + * @returns {Function} + */ + Dispatch.prototype.addBrushEvent = function (svg) { + var dispatch = this.dispatch; + var xScale = this.handler.xAxis.xScale; + var isBrushable = (dispatch.on('brush')); + var brush = this.createBrush(xScale, svg); + var addEvent = this.addEvent; + + function brushEnd() { + var bar = d3.select(this); + var startX = d3.mouse(svg.node()); + var 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); + } + + if (isBrushable) { + return addEvent('mousedown', brushEnd); + } + }; + + + /** + * Mouse over Behavior + * + * @method addMousePointer + * @returns {D3.Selection} + */ + Dispatch.prototype.addMousePointer = function () { + return d3.select(this).style('cursor', 'pointer'); + }; + /** * Adds D3 brush to SVG and returns the brush function * @@ -107,37 +180,34 @@ define(function (require) { * @param svg {HTMLElement} Reference to SVG * @returns {*} Returns a D3 brush function and a SVG with a brush group attached */ - Dispatch.prototype.addBrush = function (xScale, svg) { - var dispatch = this._attr.dispatch; - var attr = this._attr; - var chartData = this.chartData; - var isBrush = this._attr.addBrushing; - var height = this._attr.height; - var margin = this._attr.margin; + Dispatch.prototype.createBrush = function (xScale, svg) { + var dispatch = this.dispatch; + var attr = this.handler._attr; + var height = attr.height; + var margin = attr.margin; // Brush scale var brush = d3.svg.brush() .x(xScale) .on('brushend', function brushEnd() { - // response returned on brush return dispatch.brush({ range: brush.extent(), config: attr, e: d3.event, - data: chartData + data: d3.event.sourceEvent.target.__data__ }); }); // if `addBrushing` is true, add brush canvas - if (isBrush) { - svg.append('g') - .attr('class', 'brush') - .call(brush) - .selectAll('rect') - .attr('height', height - margin.top - margin.bottom); - } + if (dispatch.on('brush')) { + svg.insert('g', 'g') + .attr('class', 'brush') + .call(brush) + .selectAll('rect') + .attr('height', height - margin.top - margin.bottom); - return brush; + return brush; + } }; return Dispatch; diff --git a/src/kibana/components/vislib/lib/handler/handler.js b/src/kibana/components/vislib/lib/handler/handler.js index 0c055a2544ef9..1dbbe2f16fe36 100644 --- a/src/kibana/components/vislib/lib/handler/handler.js +++ b/src/kibana/components/vislib/lib/handler/handler.js @@ -23,6 +23,8 @@ define(function (require) { 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 } }); @@ -58,35 +60,74 @@ define(function (require) { var self = this; var charts = this.charts = []; - _.forEach(this.renderArray, function (property) { + this.renderArray.forEach(function (property) { if (typeof property.render === 'function') { property.render(); } }); + // render the chart(s) d3.select(this.el) - .selectAll('.chart') - .each(function (chartData) { - var chart = new self.ChartClass(self, this, chartData); - - d3.rebind(chart, chart._attr.dispatch, 'on'); + .selectAll('.chart') + .each(function (chartData) { + var chart = new self.ChartClass(self, this, chartData); + var enabledEvents; + + /* + * inside handler: if there are charts, bind events to charts + * functionality: track in array that event is enabled + * clean up event handlers every time it destroys the chart + * rebind them every time it creates the charts + */ + if (chart.events.dispatch) { + enabledEvents = self.vis.eventTypes.enabled; + + // Copy dispatch.on methods to chart object + d3.rebind(chart, chart.events.dispatch, 'on'); + + // Bind events to chart(s) + if (enabledEvents.length) { + enabledEvents.forEach(function (event) { + self.enable(event, chart); + }); + } + } - // Bubble events up to the Vis Class and Events Class - chart.on('click', function (e) { - self.vis.emit('click', e); - }); + charts.push(chart); + chart.render(); + }); + }; - chart.on('hover', function (e) { - self.vis.emit('hover', e); - }); - chart.on('brush', function (e) { - self.vis.emit('brush', e); - }); + /** + * Enables events, i.e. binds specific events to the chart + * object(s) `on` method. For example, `click` or `mousedown` events. + * Emits the event to the Events class. + * + * @method enable + * @param event {String} Event type + * @param chart {Object} Chart + * @returns {*} + */ + Handler.prototype.enable = function (event, chart) { + return chart.on(event, function (e) { + this.vis.emit(event, e); + }.bind(this)); + }; - charts.push(chart); - chart.render(); - }); + /** + * Disables events by passing null to the event listener. + * According to the D3 documentation for event handling: + * https://github.com/mbostock/d3/wiki/Selections#on, to remove all + * listeners for a particular event type, pass null as the listener. + * + * @method disable + * @param event {String} Event type + * @param chart {Object} Chart + * @returns {*} + */ + Handler.prototype.disable = function (event, chart) { + return chart.on(event, null); }; /** diff --git a/src/kibana/components/vislib/lib/legend.js b/src/kibana/components/vislib/lib/legend.js index 9dcb852d7db44..339cf877b4127 100644 --- a/src/kibana/components/vislib/lib/legend.js +++ b/src/kibana/components/vislib/lib/legend.js @@ -106,7 +106,6 @@ define(function (require) { var visEl = d3.select(this.el); var legendDiv = visEl.select('.' + this._attr.legendClass); var items = this.labels; - this.header(legendDiv, this); this.list(legendDiv, items, this); @@ -117,15 +116,14 @@ define(function (require) { .on('click', function legendClick() { if (self._attr.isOpen) { // close legend - visEl.select('ul.legend-ul') - .classed('hidden', true); + visEl.select('ul.legend-ul').classed('hidden', true); self._attr.isOpen = false; + // need to add reference to resize function on toggle self.vis.resize(); } else { // open legend - visEl.select('ul.legend-ul') - .classed('hidden', false); + visEl.select('ul.legend-ul').classed('hidden', false); self._attr.isOpen = true; // need to add reference to resize function on toggle @@ -147,7 +145,7 @@ define(function (require) { * The default opacity of elements in charts may be modified by the * chart constructor, and so may differ from that of the legend */ - visEl.select('.chart') + visEl.selectAll('.chart') .selectAll('.color') .style('opacity', self._attr.defaultOpacity); diff --git a/src/kibana/components/vislib/lib/tooltip.js b/src/kibana/components/vislib/lib/tooltip.js index 7cc260810c656..c33e8baf4901a 100644 --- a/src/kibana/components/vislib/lib/tooltip.js +++ b/src/kibana/components/vislib/lib/tooltip.js @@ -1,5 +1,5 @@ define(function (require) { - return function TooltipFactory(d3) { + return function TooltipFactory(d3, Private) { var $ = require('jquery'); require('css!components/vislib/styles/main'); @@ -45,11 +45,11 @@ define(function (require) { var tooltipDiv = d3.select('.' + self.tooltipClass); - selection.each(function (data, i) { + selection.each(function () { var element = d3.select(this); element - .on('mousemove.tip', function (d) { + .on('mousemove.tip', function (d, i) { var placement = self.getTooltipPlacement(d3.event); var events = self.events ? self.events.eventResponse(d, i) : d; diff --git a/src/kibana/components/vislib/styles/_error.less b/src/kibana/components/vislib/styles/_error.less new file mode 100644 index 0000000000000..0163b15bd22ea --- /dev/null +++ b/src/kibana/components/vislib/styles/_error.less @@ -0,0 +1,12 @@ +@import (reference) "lesshat.less"; + +.error { + .flex(1 1 100%); + text-align: center; + + p { + margin-top: 15%; + font-size: 18px; + text-wrap: wrap; + } +} diff --git a/src/kibana/components/vislib/styles/_layout.less b/src/kibana/components/vislib/styles/_layout.less new file mode 100644 index 0000000000000..6bc0783fd2f19 --- /dev/null +++ b/src/kibana/components/vislib/styles/_layout.less @@ -0,0 +1,144 @@ +@import (reference) "lesshat.less"; + +.visualize-chart { + .display(flex); + .flex(1 1 100%); +} + +.vis-wrapper { + .display(flex); + .flex(1 1 100%); + .flex-direction(row); + margin: 10px 0 0 6px; +} + +/* YAxis logic */ +.y-axis-col-wrapper { + .display(flex); + .flex-direction(column); +} + +.y-axis-col { + .display(flex); + .flex-direction(row); + .flex(1 0 50px); +} + +.y-axis-spacer-block { + min-height: 45px; +} + +.y-axis-div-wrapper { + .display(flex); + .flex-direction(column); + width: 38px; + min-height: 20px; +} + +.y-axis-div { + .flex(1 1 25px); + min-width: 14px; + min-height: 14px; +} + +.y-axis-title { + min-height: 14px; + min-width: 14px; +} + +.y-axis-chart-title { + .display(flex); + .flex-direction(column); + min-height: 14px; + width: 14px; +} + +.y-axis-title text { + font-size: 11px; +} + +.chart-title { + .flex(1 1 100%); + min-height: 14px; + min-width: 14px; +} + +.chart-title text { + font-size: 11px; + fill: #848e96; +} + +.vis-col-wrapper { + .display(flex); + .flex(1 0 20px); + .flex-direction(column); +} + +.chart-wrapper { + .display(flex); + .flex(1 0 20px); + overflow: visible; + margin: 0 5px 0 0; +} + +.chart-wrapper-column { + .display(flex); + .flex(1 0 20px); + .flex-direction(row); +} + +.chart-wrapper-row { + .display(flex); + .flex-direction(column); + .flex(1 0 100%); +} + +.chart { + .flex(1 1 100%); + overflow: visible; +} + +.chart-row { + .flex(1 1); +} + +.chart-column { + .flex(1 1); +} + +.x-axis-wrapper { + .display(flex); + .flex-direction(column); + min-height: 45px; + overflow: visible; +} + +.x-axis-div-wrapper { + .display(flex); + .flex-direction(row); + min-height: 15px; +} + +.x-axis-chart-title { + .display(flex); + .flex-direction(row); + min-height: 15px; + max-height: 15px; + min-width: 20px; +} + +.x-axis-title { + min-height: 15px; + max-height: 15px; + min-width: 20px; +} + +.x-axis-title text { + font-size: 11px; +} + +.x-axis-div { + margin-top: -5px; + min-height: 20px; + min-width: 20px; +} diff --git a/src/kibana/components/vislib/styles/_legend.less b/src/kibana/components/vislib/styles/_legend.less new file mode 100644 index 0000000000000..d2907c487ffd3 --- /dev/null +++ b/src/kibana/components/vislib/styles/_legend.less @@ -0,0 +1,59 @@ +@import (reference) "lesshat.less"; + +.legend-col-wrapper { + .flex(0 1 auto); + z-index: 10; + overflow-x: hidden; + overflow-y: auto; + min-width: 40px; + + .header { + width: 100%; + height: 26px; + margin: 0 0 14px 0; + visibility: visible; + } + + .legend-ul { + list-style-type: none; + margin: 0 0 0 14px; + padding: 0; + visibility: visible; + .display(flex); + .flex-direction(column); + + li.color { + min-height: 22px; + margin: 0 18px 0 18px; + padding: 3px 0 3px 0; + text-align: left; + font-size: 12px; + line-height: 13px; + color: #666; + cursor: default; + text-align: left; + .flex-grow(2); + word-wrap: break-word; + max-width: 150px; + + .dots { + width: 10px; + height: 10px; + border-radius: 50%; + float: left; + margin: 1px 0 0 -14px; + } + } + } + + .legend-ul.hidden { + visibility: hidden; + } + + .legend-toggle { + position: relative; + float: right; + right: 9px; + top: 9px; + } +} diff --git a/src/kibana/components/vislib/styles/_svg.less b/src/kibana/components/vislib/styles/_svg.less new file mode 100644 index 0000000000000..e15f23a2e8caf --- /dev/null +++ b/src/kibana/components/vislib/styles/_svg.less @@ -0,0 +1,63 @@ +/* Axis Styles */ +.axis { + shape-rendering: crispEdges; + stroke-width: 1px; + + line, path { + stroke: #ddd; + fill: none; + shape-rendering: crispEdges; + } +} + +.x.axis path { + display: none; +} + +.tick text { + font-size: 7pt; + fill: #848e96; +} + +/* Brush Styling */ +.brush .extent { + stroke: #fff; + fill-opacity: .125; + shape-rendering: crispEdges; +} + +/* SVG Element Default Styling */ +rect { + stroke: none; + opacity: 1; + + &:hover { + stroke: #333; + } +} + +circle { + opacity: 0; + + &:hover { + opacity: 1; + stroke: #333; + } +} + +path { + &:hover { + opacity: 1; + } +} + +/* Visualization Styling */ +.line { + circle { + opacity: 1; + } + + &:hover { + stroke: #333; + } +} diff --git a/src/kibana/components/vislib/styles/_tooltip.less b/src/kibana/components/vislib/styles/_tooltip.less new file mode 100644 index 0000000000000..20d6b1a434759 --- /dev/null +++ b/src/kibana/components/vislib/styles/_tooltip.less @@ -0,0 +1,36 @@ +@import (reference) "../../../styles/main"; + +.vis-tooltip { + visibility: hidden; + line-height: 1.1; + font-size: 12px; + font-weight: normal; + padding: 8px; + background: rgba(70, 82, 93, 0.95); + color: @gray-lighter; + border-radius: 4px; + position: fixed; + z-index: 120; + word-wrap: break-word; + max-width: 40%; + + table { + td,th { + padding: 2px 4px; + } + + // if there is a header, give it a border that matches + // those in the body + thead tr { + border-bottom: 1px solid @gray; + } + + // only apply to tr in the body, not the header + tbody tr { + border-top: 1px solid @gray; + &:first-child { + border-top: none; + } + } + } +} diff --git a/src/kibana/components/vislib/styles/main.less b/src/kibana/components/vislib/styles/main.less index 3f8f1f7b112f1..e7533d8832239 100644 --- a/src/kibana/components/vislib/styles/main.less +++ b/src/kibana/components/vislib/styles/main.less @@ -1,410 +1,5 @@ -@import (reference) "../../../styles/main.less"; -@import (reference) "lesshat.less"; - -/* vislib styles */ -.arc path { - stroke: #fff; -} - -div.columns { - .display(flex); - .flex-direction(row); - .flex(1 1 100%); - margin: 0; - padding: 0; -} - -div.rows { - .display(flex); - .flex-direction(column); - .flex(1 1 100%); - margin: 0; - padding: 0; -} - -.row-labels, .column-labels { - margin: 0; - padding: 10; -} - -visualize { - margin:0; - padding:0; -} - -.visualize-chart { - .display(flex); - .flex(1 1 100%); -} - -.visualize-wrapper { - .display(flex); - .flex-direction(row); -} - -.y-axis-wrapper { - .display(flex); - .flex(1 1); - .flex-direction(column); -} - -.y-axis-filler-div { - .flex(1 1); -} - -div.x-axis-label { - position: absolute; - width: 100%; - text-align: center; - bottom: 15px; - font-size: 7pt; - color: #848e96; -} - -div.y-axis-label { - position: absolute; - left: -25px; - top: 42.5%; - font-size: 7pt; - color: #848e96; - -webkit-transform: rotate(270deg); - -moz-transform: rotate(270deg); - -o-transform: rotate(270deg); - -ms-transform: rotate(270deg); - transform: rotate(270deg); -} - -/* legend */ -.legend-col-wrapper { - .flex(0 1 auto); - z-index: 10; - overflow-x: hidden; - overflow-y: auto; - min-width: 40px; -} - -.header { - width: 100%; - height: 26px; - margin: 0 0 14px 0; -} - -.legend { - width: 100%; - height: 26px; - margin: 0 0 14px 0; -} - -.legend-ul { - list-style-type: none; - margin: 0 0 0 14px; - padding: 0; - visibility: visible; - .display(flex); - .flex-direction(column); -} - -.legend-ul.hidden { - visibility: hidden; -} - -li.color { - min-height: 22px; - margin: 0 18px 0 18px; - padding: 3px 0 3px 0; - text-align: left; - font-size: 12px; - line-height: 13px; - color: #666; - cursor: default; - text-align: left; - .flex-grow(2); - word-wrap: break-word; - max-width: 150px; -} -.wordwrap { - word-wrap: break-word; - max-width: 150px; -} - -.dots { - width: 10px; - height: 10px; - border-radius: 50%; - float: left; - margin: 1px 0 0 -14px; -} - -.legend-toggle { - position: relative; - float: right; - right: 9px; - top: 9px; -} - -.column-labels { - color: #777; - font-size: 10pt; - font-weight: normal; - display: block; - margin: 0; - padding: 0; - padding-left: 1.0em; - line-height: 2.0em; -} - -/* histogram axis and label styles */ -.vis-canvas { -} - -.chart-bkgd { - fill: #ffffff; -} - -p.rows-label, p.columns-label { - display: block; - background: #eff3f4; - padding: 0; - margin: 0; - font-size: 9pt; - fill: #46525d; - text-align: center; - line-height: 1.9em; -} - -.charts-label { - font-size: 8pt; - fill: #848e96; -} - -.x-axis-label, .y-axis-label { - font-size: 7pt; - fill: #848e96; -} - -.tick text { - font-size: 7pt; - fill: #848e96; -} - -.axis { - shape-rendering: crispEdges; - stroke-width: 1px; -} - -.axis line, .axis path { - stroke: #ddd; - fill: none; - shape-rendering: crispEdges; -} - -.legend-box { - fill: #ffffff; -} - -.brush .extent { - stroke: #fff; - fill-opacity: .125; - shape-rendering: crispEdges; -} - -.layer .rect:hover { - stroke: 3px; -} - -.vis-tooltip { - visibility: hidden; - line-height: 1.1; - font-size: 12px; - font-weight: normal; - padding: 8px; - background: rgba(70, 82, 93, 0.95); - color: @gray-lighter; - border-radius: 4px; - position: fixed; - z-index: 120; - word-wrap: break-word; - max-width: 40%; - - table { - td,th { - padding: 2px 4px; - } - - // if there is a header, give it a border that matches - // those in the body - thead tr { - border-bottom: 1px solid @gray; - } - - // only apply to tr in the body, not the header - tbody tr { - border-top: 1px solid @gray; - &:first-child { - border-top: none; - } - } - } -} - -.rect { - stroke: transparent; - stroke-width: 2; -} -.rect.hover { - stroke: #333; -} - -/* Flex Box Layout */ -.vis-wrapper { - .display(flex); - .flex(1 1 100%); - .flex-direction(row); - margin: 10px 0 0 6px; -} - -.error { - .flex(1 1 100%); - text-align: center; - - p { - margin-top: 15%; - font-size: 18px; - text-wrap: wrap; - } -} - -/* YAxis logic */ -.y-axis-col-wrapper { - .display(flex); - .flex-direction(column); -} - -.y-axis-col { - .display(flex); - .flex-direction(row); - .flex(1 0 50px); -} - -.y-axis-spacer-block { - min-height: 45px; -} - -.y-axis-div-wrapper { - .display(flex); - .flex-direction(column); - width: 38px; - min-height: 20px; -} - -.y-axis-div { - .flex(1 1 25px); - min-width: 14px; - min-height: 14px; -} - -.y-axis-title { - min-height: 14px; - min-width: 14px; -} - -.y-axis-chart-title { - .display(flex); - .flex-direction(column); - min-height: 14px; - width: 14px; -} - -.y-axis-title text { - font-size: 11px; -} - -.chart-title { - .flex(1 1 100%); - min-height: 14px; - min-width: 14px; -} - -.chart-title text { - font-size: 11px; - fill: #848e96; -} - -.vis-col-wrapper { - .display(flex); - .flex(1 0 20px); - .flex-direction(column); -} - -.chart-wrapper { - .display(flex); - .flex(1 0 20px); - overflow: visible; - margin: 0 5px 0 0; -} - -.chart-wrapper-column { - .display(flex); - .flex(1 0 20px); - .flex-direction(row); -} - -.chart-wrapper-row { - .display(flex); - .flex-direction(column); - .flex(1 0 100%); -} - -.chart { - .flex(1 1 100%); - overflow: visible; -} - -.chart-row { - .flex(1 1); -} - -.chart-column { - .flex(1 1); -} - -.x-axis-wrapper { - .display(flex); - .flex-direction(column); - min-height: 45px; - overflow: visible; -} - -.x-axis-div-wrapper { - .display(flex); - .flex-direction(row); - min-height: 15px; -} - -.x-axis-chart-title { - .display(flex); - .flex-direction(row); - min-height: 15px; - max-height: 15px; - min-width: 20px; -} - -.x-axis-title { - min-height: 15px; - max-height: 15px; - min-width: 20px; -} - -.x-axis-title text { - font-size: 11px; -} - -.x-axis-div { - margin-top: -5px; - min-height: 20px; - min-width: 20px; -} - -.x.axis path { - display: none; -} +@import "_error"; +@import "_layout"; +@import "_legend"; +@import "_svg"; +@import "_tooltip"; diff --git a/src/kibana/components/vislib/vis.js b/src/kibana/components/vislib/vis.js index 2ac42bf35e28e..e126ed975bcf1 100644 --- a/src/kibana/components/vislib/vis.js +++ b/src/kibana/components/vislib/vis.js @@ -27,6 +27,9 @@ define(function (require) { this.el = $el.get ? $el.get(0) : $el; this.ChartClass = chartTypes[config.type]; this._attr = _.defaults(config || {}, {}); + this.eventTypes = { + enabled: [] + }; // bind the resize function so it can be used as an event handler this.resize = _.bind(this.resize, this); @@ -81,6 +84,7 @@ define(function (require) { /** * 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 @@ -114,6 +118,68 @@ define(function (require) { return this._attr[name]; }; + /** + * Turns on event listeners. + * + * @param event {String} + * @param listener{Function} + * @returns {*} + */ + Vis.prototype.on = function (event, listener) { + var ret = Events.prototype.on.call(this, event, listener); // Adds event to _listeners array + var listeners = this._listeners[event].length; + var charts = (this.handler && this.handler.charts); + var chartCount = charts ? charts.length : 0; + var enabledEvents = this.eventTypes.enabled; + var eventAbsent = (enabledEvents.indexOf(event) === -1); + + // if this is the first listener added for the event + // and charts are available, bind the event to the chart(s) + // `on` method + if (listeners === 1 && chartCount > 0) { + charts.forEach(function (chart) { + this.handler.enable(event, chart); + }, this); + } + + // Keep track of enabled events + if (eventAbsent) { + enabledEvents.push(event); + } + + return ret; + }; + + /** + * Turns off event listeners. + * + * @param event {String} + * @param listener{Function} + * @returns {*} + */ + Vis.prototype.off = function (event, listener) { + var ret = Events.prototype.off.call(this, event, listener); // Removes event from _listeners array + var listeners = (!!this._listeners[event] && this._listeners[event].length !== 0); + var charts = (this.handler && this.handler.charts); + var chartCount = charts ? charts.length : 0; + var eventIndex = this.eventTypes.enabled.indexOf(event); + var eventPresent = (eventIndex !== -1); + + // Once the listener array reaches zero, turn off event + if (!listeners && eventPresent) { + if (chartCount > 0) { + charts.forEach(function (chart) { + this.handler.disable(event, chart); + }, this); + } + + // Remove event from enabled array + this.eventTypes.enabled.splice(eventIndex, 1); + } + + return ret; + }; + return Vis; }; -}); \ No newline at end of file +}); diff --git a/src/kibana/components/vislib/visualizations/_chart.js b/src/kibana/components/vislib/visualizations/_chart.js index aa093ffd76883..445ba9ce1258f 100644 --- a/src/kibana/components/vislib/visualizations/_chart.js +++ b/src/kibana/components/vislib/visualizations/_chart.js @@ -24,7 +24,7 @@ define(function (require) { this.chartEl = el; this.chartData = chartData; - var events = this.events = new Dispatch(handler, chartData); + var events = this.events = new Dispatch(handler); if (handler._attr.addTooltip) { var $el = this.handler.el; diff --git a/src/kibana/components/vislib/visualizations/area_chart.js b/src/kibana/components/vislib/visualizations/area_chart.js index f4a709881296e..b6c1964baf6f8 100644 --- a/src/kibana/components/vislib/visualizations/area_chart.js +++ b/src/kibana/components/vislib/visualizations/area_chart.js @@ -136,38 +136,15 @@ define(function (require) { * Adds Events to SVG circles * * @method addCircleEvents - * @param circles {D3.UpdateSelection} SVG circles - * @returns {HTMLElement} circles with event listeners attached + * @param element {D3.UpdateSelection} SVG circles + * @returns {D3.Selection} circles with event listeners attached */ - AreaChart.prototype.addCircleEvents = function (circles) { + AreaChart.prototype.addCircleEvents = function (element) { var events = this.events; - var dispatch = this.events._attr.dispatch; - circles - .on('mouseover.circle', function mouseOverCircle(d, i) { - var circle = this; - - d3.select(circle) - .classed('hover', true) - .style('stroke', '#333') - .style('cursor', 'pointer') - .style('opacity', 1); - - dispatch.hover(events.eventResponse(d, i)); - d3.event.stopPropagation(); - }) - .on('click.circle', function clickCircle(d, i) { - dispatch.click(events.eventResponse(d, i)); - d3.event.stopPropagation(); - }) - .on('mouseout.circle', function mouseOutCircle() { - var circle = this; - - d3.select(circle) - .classed('hover', false) - .style('stroke', null) - .style('opacity', 0); - }); + return element + .call(events.addHoverEvent()) + .call(events.addClickEvent()); }; /** @@ -179,7 +156,6 @@ define(function (require) { * @returns {D3.UpdateSelection} SVG with circles added */ AreaChart.prototype.addCircles = function (svg, data) { - var self = this; var color = this.handler.data.getColorFunc(); var xScale = this.handler.xAxis.xScale; var yScale = this.handler.yAxis.yScale; @@ -196,7 +172,7 @@ define(function (require) { .data(data) .enter() .append('g') - .attr('class', 'points'); + .attr('class', 'points area'); // Append the bars circles = layer @@ -237,8 +213,7 @@ define(function (require) { } return yScale(d.y0 + d.y); }) - .attr('r', circleRadius) - .style('opacity', 0); + .attr('r', circleRadius); // Add tooltip if (isTooltip) { @@ -328,9 +303,6 @@ define(function (require) { // add clipPath to hide circles when they go out of bounds self.addClipPath(svg, width, height); - // addBrush canvas - self.events.addBrush(xScale, svg); - // add path path = self.addPath(svg, layers); diff --git a/src/kibana/components/vislib/visualizations/column_chart.js b/src/kibana/components/vislib/visualizations/column_chart.js index 4a9f8dde3053d..1465f2b13d002 100644 --- a/src/kibana/components/vislib/visualizations/column_chart.js +++ b/src/kibana/components/vislib/visualizations/column_chart.js @@ -217,77 +217,27 @@ define(function (require) { /** * 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 svg {HTMLElement} chart SVG - * @param bars {D3.UpdateSelection} SVG rect - * @param brush {Function} D3 brush function - * @returns {HTMLElement} rect with event listeners attached + * @param element {D3.UpdateSelection} target + * @param svg {D3.UpdateSelection} chart SVG + * @returns {D3.Selection} rect with event listeners attached */ - ColumnChart.prototype.addBarEvents = function (svg, bars, brush) { - var self = this; + ColumnChart.prototype.addBarEvents = function (element, svg) { var events = this.events; - var dispatch = this.events._attr.dispatch; - var addBrush = this._attr.addBrushing; - var xScale = this.handler.xAxis.xScale; - - bars - .on('mouseover.bar', function (d, i) { - self.mouseOverBar(this); - dispatch.hover(events.eventResponse(d, i)); - d3.event.stopPropagation(); - }) - .on('mousedown.bar', function () { - if (addBrush) { - var bar = d3.select(this); - var startX = d3.mouse(svg.node()); - var 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); - } - }) - .on('click.bar', function (d, i) { - dispatch.click(events.eventResponse(d, i)); - d3.event.stopPropagation(); - }) - .on('mouseout.bar', function () { - self.mouseOutBar(this); - }); - }; - - /** - * Mouseover Behavior - * - * @method mouseOverBar - * @param that {Object} Reference to this object - * @returns {D3.Selection} this object with '.hover' class true - */ - ColumnChart.prototype.mouseOverBar = function (that) { - return d3.select(that) - .classed('hover', true) - .style('stroke', '#333') - .style('cursor', 'pointer'); - }; + var isBrushable = (typeof events.dispatch.on('brush') === 'function'); + var brush = isBrushable ? events.addBrushEvent(svg) : undefined; + var hover = events.addHoverEvent(); + var click = events.addClickEvent(); + var attachedEvents = element.call(hover).call(click); + + if (isBrushable) { + attachedEvents.call(brush); + } - /** - * Mouseout Behavior - * - * @method mouseOutBar - * @param that {Object} Reference to this object - * @returns {D3.Selection} this object with '.hover' class false - */ - ColumnChart.prototype.mouseOutBar = function (that) { - return d3.select(that) - .classed('hover', false) - .style('stroke', null); + return attachedEvents; }; /** @@ -298,20 +248,17 @@ define(function (require) { */ ColumnChart.prototype.draw = function () { var self = this; - var xScale = this.handler.xAxis.xScale; var $elem = $(this.chartEl); var margin = this._attr.margin; var elWidth = this._attr.width = $elem.width(); var elHeight = this._attr.height = $elem.height(); var minWidth = 20; var minHeight = 20; - var isEvents = this._attr.addEvents; var div; var svg; var width; var height; var layers; - var brush; var bars; return function (selection) { @@ -333,12 +280,10 @@ define(function (require) { .append('g') .attr('transform', 'translate(0,' + margin.top + ')'); - brush = self.events.addBrush(xScale, svg); bars = self.addBars(svg, layers); - if (isEvents) { - self.addBarEvents(svg, bars, brush); - } + // Adds event listeners + self.addBarEvents(bars, svg); var line = svg.append('line') .attr('x1', 0) diff --git a/src/kibana/components/vislib/visualizations/line_chart.js b/src/kibana/components/vislib/visualizations/line_chart.js index 8457c06a53070..229eaccca7533 100644 --- a/src/kibana/components/vislib/visualizations/line_chart.js +++ b/src/kibana/components/vislib/visualizations/line_chart.js @@ -36,36 +36,15 @@ define(function (require) { * Adds Events to SVG circle * * @method addCircleEvents - * @param circles {D3.UpdateSelection} Reference to SVG circle - * @returns {D3.UpdateSelection} SVG circles with event listeners attached + * @param element{D3.UpdateSelection} Reference to SVG circle + * @returns {D3.Selection} SVG circles with event listeners attached */ - LineChart.prototype.addCircleEvents = function (circles) { + LineChart.prototype.addCircleEvents = function (element) { var events = this.events; - var dispatch = this.events._attr.dispatch; - circles - .on('mouseover.circle', function mouseOverCircle(d, i) { - var circle = this; - - d3.select(circle) - .classed('hover', true) - .style('stroke', '#333') - .style('cursor', 'pointer'); - - dispatch.hover(events.eventResponse(d, i)); - d3.event.stopPropagation(); - }) - .on('click.circle', function clickCircle(d, i) { - dispatch.click(events.eventResponse(d, i)); - d3.event.stopPropagation(); - }) - .on('mouseout.circle', function mouseOutCircle() { - var circle = this; - - d3.select(circle) - .classed('hover', false) - .style('stroke', null); - }); + return element + .call(events.addHoverEvent()) + .call(events.addClickEvent()); }; /** @@ -93,7 +72,7 @@ define(function (require) { .data(data) .enter() .append('g') - .attr('class', 'points'); + .attr('class', 'points line'); circles = layer .selectAll('rect') @@ -274,12 +253,8 @@ define(function (require) { .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); self.addClipPath(svg, width, height); - - self.events.addBrush(xScale, svg); - lines = self.addLines(svg, data.series); circles = self.addCircles(svg, layers); - self.addCircleEvents(circles); var line = svg diff --git a/src/kibana/components/vislib/visualizations/pie_chart.js b/src/kibana/components/vislib/visualizations/pie_chart.js index 19e4fd9591bc8..b024e7a89bfd5 100644 --- a/src/kibana/components/vislib/visualizations/pie_chart.js +++ b/src/kibana/components/vislib/visualizations/pie_chart.js @@ -24,12 +24,9 @@ define(function (require) { } PieChart.Super.apply(this, arguments); - this.columns = handler.data.data.raw.columns; - this._attr = _.defaults(handler._attr || {}, { isDonut: handler._attr.isDonut || false, - getSize: function (d) { return d.size; }, - dispatch: d3.dispatch('brush', 'click', 'hover', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout') + getSize: function (d) { return d.size; } }); } @@ -37,30 +34,15 @@ define(function (require) { * Adds Events to SVG paths * * @method addPathEvents - * @param path {D3.Selection} Reference to SVG path + * @param element {D3.Selection} Reference to SVG path * @returns {D3.Selection} SVG path with event listeners attached */ - PieChart.prototype.addPathEvents = function (path) { + PieChart.prototype.addPathEvents = function (element) { var events = this.events; - var dispatch = this.events._attr.dispatch; - - path - .on('mouseover.pie', function mouseOverPie(d, i) { - d3.select(this) - .classed('hover', true) - .style('cursor', 'pointer'); - dispatch.hover(events.pieResponse(d, i)); - d3.event.stopPropagation(); - }) - .on('click.pie', function clickPie(d, i) { - dispatch.click(events.pieResponse(d, i)); - d3.event.stopPropagation(); - }) - .on('mouseout.pie', function mouseOutPie() { - d3.select(this) - .classed('hover', false); - }); + return element + .call(events.addHoverEvent()) + .call(events.addClickEvent()); }; /** @@ -109,7 +91,9 @@ define(function (require) { var isTooltip = this._attr.addTooltip; var self = this; var path; - var fieldFormatter = function (d) { return d; }; + var fieldFormatter = function (label) { + return label; + }; path = svg .datum(slices) @@ -149,7 +133,6 @@ define(function (require) { */ PieChart.prototype.draw = function () { var self = this; - var isEvents = this._attr.addEvents; return function (selection) { selection.each(function (data) { @@ -160,6 +143,7 @@ define(function (require) { var height = $(el).height(); var minWidth = 20; var minHeight = 20; + var path; if (width <= minWidth || height <= minHeight) { throw new errors.ContainerTooSmall(); @@ -171,11 +155,8 @@ define(function (require) { .append('g') .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); - var path = self.addPath(width, height, svg, slices); - - if (isEvents) { - self.addPathEvents(path); - } + path = self.addPath(width, height, svg, slices); + self.addPathEvents(path); return svg; }); diff --git a/src/kibana/plugins/discover/controllers/discover.js b/src/kibana/plugins/discover/controllers/discover.js index cd9882d18b702..223439c0d8ea0 100644 --- a/src/kibana/plugins/discover/controllers/discover.js +++ b/src/kibana/plugins/discover/controllers/discover.js @@ -640,8 +640,6 @@ define(function (require) { type: 'histogram', vislibParams: { addLegend: false, - addEvents: true, - addBrushing: true, }, listeners: { click: function (e) { diff --git a/tasks/config/less.js b/tasks/config/less.js index 0f3d4b56ec1d2..27c8c4fd7522b 100644 --- a/tasks/config/less.js +++ b/tasks/config/less.js @@ -6,7 +6,6 @@ module.exports = { '<%= src %>/kibana/components/*/*.less', '<%= src %>/kibana/styles/main.less', '<%= src %>/kibana/components/vislib/styles/main.less', - '<%= src %>/kibana/components/**/*.less', '<%= plugins %>/dashboard/styles/main.less', '<%= plugins %>/discover/styles/main.less', '<%= plugins %>/settings/styles/main.less', diff --git a/test/unit/specs/vislib/lib/handler.js b/test/unit/specs/vislib/lib/handler.js index 96976a1140f76..5892b8d4309c6 100644 --- a/test/unit/specs/vislib/lib/handler.js +++ b/test/unit/specs/vislib/lib/handler.js @@ -8,15 +8,6 @@ define(function (require) { angular.module('HandlerFactory', ['kibana']); describe('VisLib Handler Test Suite', function () { - var Vis; - var Data; - var Handler; - var handler; - var ColumnHandler; - var vis; - var el; - var example; - var config; var data = { hits : 621, label : '', @@ -81,7 +72,15 @@ define(function (require) { xAxisLabel: 'Date Histogram', yAxisLabel: 'Count' }; - + var Vis; + var Data; + var Handler; + var handler; + var ColumnHandler; + var vis; + var el; + var config; + var events; beforeEach(function () { module('VisFactory'); @@ -106,54 +105,93 @@ define(function (require) { addLegend: true }; - vis = new Vis(el[0][0], config); - vis.data = data; - - handler = ColumnHandler(vis); + events = [ + 'click', + 'brush' + ]; -// handler.render(data); + vis = new Vis(el[0][0], config); + vis.render(data); }); }); afterEach(function () { + vis.destroy(); el.remove(); }); -// describe('render Method', function () { -// it('should instantiate all constructors ', function () { -// expect(!!handler.layout).to.be(true); -// expect(!!handler.legend).to.be(true); -// expect(!!handler.tooltip).to.be(true); -// expect(!!handler.xAxis).to.be(true); -// expect(!!handler.yAxis).to.be(true); -// expect(!!handler.axisTitle).to.be(true); -// expect(!!handler.chartTitle).to.be(true); -// }); -// -// it('should append all DOM Elements for the visualization', function () { -// expect($('.vis-wrapper').length).to.be(1); -// expect($('.y-axis-col-wrapper').length).to.be(1); -// expect($('.vis-col-wrapper').length).to.be(1); -// expect($('.legend-col-wrapper').length).to.be(1); -// expect($('.k4tip').length).to.be(1); -// expect($('.y-axis-col').length).to.be(1); -// expect($('.y-axis-title').length).to.be(1); -// expect($('.y-axis-chart-title').length).to.be(0); -// expect($('.y-axis-div-wrapper').length).to.be(1); -// expect($('.y-axis-spacer-block').length).to.be(1); -// expect($('.chart-wrapper').length).to.be(1); -// expect($('.x-axis-wrapper').length).to.be(1); -// expect($('.x-axis-div-wrapper').length).to.be(1); -// expect($('.x-axis-chart-title').length).to.be(0); -// expect($('.x-axis-title').length).to.be(1); -// expect($('svg').length).to.be(5); -// }); -// }); + describe('render Method', function () { + //it('should instantiate all constructors ', function () { + // expect(!!vis.handler.layout).to.be(true); + // expect(!!vis.handler.xAxis).to.be(true); + // expect(!!vis.handler.yAxis).to.be(true); + // expect(!!vis.handler.axisTitle).to.be(true); + // expect(!!vis.handler.chartTitle).to.be(true); + //}); + // + //it('should append all DOM Elements for the visualization', function () { + // expect($('.vis-wrapper').length).to.be(1); + // expect($('.y-axis-col-wrapper').length).to.be(1); + // expect($('.vis-col-wrapper').length).to.be(1); + // expect($('.y-axis-col').length).to.be(1); + // expect($('.y-axis-title').length).to.be(1); + // expect($('.y-axis-chart-title').length).to.be(0); + // expect($('.y-axis-div-wrapper').length).to.be(1); + // expect($('.y-axis-spacer-block').length).to.be(1); + // expect($('.chart-wrapper').length).to.be(1); + // expect($('.x-axis-wrapper').length).to.be(1); + // expect($('.x-axis-div-wrapper').length).to.be(1); + // expect($('.x-axis-chart-title').length).to.be(0); + // expect($('.x-axis-title').length).to.be(1); + // expect($('svg').length).to.be(5); + //}); + }); + + describe('enable Method', function () { + var charts; + + beforeEach(function () { + charts = vis.handler.charts; + + charts.forEach(function (chart) { + events.forEach(function (event) { + vis.handler.enable(event, chart); + }); + }); + }); + + it('should add events to chart and emit to the Events class', function () { + charts.forEach(function (chart, i) { + expect(typeof chart.on(events[i])).to.be('function'); + }); + }); + }); + + describe('disable Method', function () { + var charts; + + beforeEach(function () { + charts = vis.handler.charts; + + charts.forEach(function (chart) { + events.forEach(function (event) { + vis.handler.disable(event, chart); + }); + }); + }); + + it('should remove events from the chart', function () { + charts.forEach(function (chart, i) { + expect(typeof chart.on(events[i])).to.be('undefined'); + }); + }); + + }); describe('removeAll Method', function () { beforeEach(function () { inject(function () { - handler.removeAll(el[0][0]); + vis.handler.removeAll(el[0][0]); }); }); @@ -164,7 +202,7 @@ define(function (require) { describe('error Method', function () { beforeEach(function () { - handler.error('This is an error!'); + vis.handler.error('This is an error!'); }); it('should return an error classed DOM element with a text message', function () { diff --git a/test/unit/specs/vislib/vis.js b/test/unit/specs/vislib/vis.js index eaaf9bdbeac03..4636987635938 100644 --- a/test/unit/specs/vislib/vis.js +++ b/test/unit/specs/vislib/vis.js @@ -6,6 +6,8 @@ define(function (require) { angular.module('VisFactory', ['kibana']); describe('VisLib Vis Test Suite', function () { + var beforeEvent = 'click'; + var afterEvent = 'brush'; var Vis; var vis; var el; @@ -87,8 +89,10 @@ define(function (require) { }); afterEach(function () { - el.remove(); + vis.off(beforeEvent); + vis.off(afterEvent); vis.destroy(); + el.remove(); }); describe('render Method', function () { @@ -145,5 +149,176 @@ define(function (require) { expect(vis.get('type')).to.be('histogram'); }); }); + + describe('on Method', function () { + var events = [ + beforeEvent, + afterEvent + ]; + var listeners; + var listener1; + var listener2; + + beforeEach(function () { + listeners = []; + listener1 = function (e) { + console.log(e, 'listener1'); + }; + listener2 = function (e) { + console.log(e, 'listener2'); + }; + listeners.push(listener1); + listeners.push(listener2); + + // Add event and listeners to chart + listeners.forEach(function (listener) { + vis.on(beforeEvent, listener); + }); + + // Render chart + vis.render(data); + + // Add event after charts have rendered + listeners.forEach(function (listener) { + vis.on(afterEvent, listener); + }); + }); + + afterEach(function () { + vis.off(beforeEvent); + vis.off(afterEvent); + }); + + it('should add an event and its listeners to the _listeners object', function () { + // Test for presence of beforeEvent in _listener object + expect(vis._listeners[beforeEvent] instanceof Array).to.be(true); + + vis._listeners[beforeEvent].forEach(function (listener, i) { + expect(typeof listener.handler).to.be('function'); + expect(listener.handler).to.be(listeners[i]); + }); + + vis._listeners[afterEvent].forEach(function (listener, i) { + expect(typeof listener.handler).to.be('function'); + expect(listener.handler).to.be(listeners[i]); + }); + }); + + it('should add an event to the eventTypes.enabled array', function () { + vis.eventTypes.enabled.forEach(function (eventType, i) { + expect(eventType).to.be(events[i]); + }); + }); + + it('should attach an event and its listeners to the chart', function () { + var charts = vis.handler.charts; + + charts.forEach(function (chart, i) { + expect(typeof chart.on(beforeEvent) === 'function'); + expect(typeof chart.on(afterEvent) === 'function'); + expect(chart.on(beforeEvent) === listeners[i]); + expect(chart.on(afterEvent) === listeners[i]); + }); + }); + }); + + describe('off Method', function () { + var listeners; + var listener1; + var listener2; + + beforeEach(function () { + listeners = []; + listener1 = function (e) { + console.log(e, 'listener1'); + }; + listener2 = function (e) { + console.log(e, 'listener2'); + }; + listeners.push(listener1); + listeners.push(listener2); + + // Add event and listeners to chart + listeners.forEach(function (listener) { + vis.on(beforeEvent, listener); + }); + + // Turn off event listener before chart rendered + vis.off(beforeEvent, listener1); + + // Render chart + vis.render(data); + + // Add event after charts have rendered + listeners.forEach(function (listener) { + vis.on(afterEvent, listener); + }); + + // Turn off event listener after chart is rendered + vis.off(afterEvent, listener1); + }); + + afterEach(function () { + vis.off(beforeEvent); + vis.off(afterEvent); + }); + + it('should remove a listener from the _listeners[event] array', function () { + var charts = vis.handler.charts; + + expect(vis._listeners[beforeEvent].length).to.be(1); + expect(vis._listeners[afterEvent].length).to.be(1); + + // should still have the 2 events in the eventTypes enabled array + expect(vis.eventTypes.enabled.length).to.be(2); + + // Test that listener that was not removed is still present + vis._listeners[beforeEvent].forEach(function (listener) { + expect(typeof listener.handler).to.be('function'); + expect(listener.handler).to.be(listener2); + }); + + vis._listeners[afterEvent].forEach(function (listener) { + expect(typeof listener.handler).to.be('function'); + expect(listener.handler).to.be(listener2); + }); + + // Events should still be attached to charts + charts.forEach(function (chart) { + expect(typeof chart.on(beforeEvent)).to.be('function'); + expect(typeof chart.on(afterEvent)).to.be('function'); + }); + }); + + it('should remove the event and all listeners when only event passed an argument', function () { + var charts = vis.handler.charts; + vis.off(afterEvent); + + // should remove 'brush' from _listeners object + expect(vis._listeners[afterEvent]).to.be(undefined); + + // should remove 'brush' from eventTypes.enabled array + expect(vis.eventTypes.enabled.length).to.be(1); + + // should remove the event from the charts + charts.forEach(function (chart) { + expect(typeof chart.on(afterEvent)).to.be('undefined'); + }); + }); + + it('should remove the event from the eventTypes.enabled array as well as ' + + 'from the chart when the _listeners array has a length of 0', function () { + var charts = vis.handler.charts; + vis.off(afterEvent, listener2); + + expect(vis._listeners[afterEvent].length).to.be(0); + expect(vis.eventTypes.enabled.length).to.be(1); + + charts.forEach(function (chart) { + expect(typeof chart.on(afterEvent)).to.be('undefined'); + }); + }); + }); + }); });