Merge branch 'master' into template_vistype

This commit is contained in:
Spencer Alger 2014-11-03 13:26:42 -07:00
commit 56d25b4769
24 changed files with 972 additions and 784 deletions

View file

@ -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

View file

@ -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;

View file

@ -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);
.selectAll('.chart')
.each(function (chartData) {
var chart = new self.ChartClass(self, this, chartData);
var enabledEvents;
d3.rebind(chart, chart._attr.dispatch, 'on');
/*
* 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;
// Bubble events up to the Vis Class and Events Class
chart.on('click', function (e) {
self.vis.emit('click', e);
});
// Copy dispatch.on methods to chart object
d3.rebind(chart, chart.events.dispatch, 'on');
chart.on('hover', function (e) {
self.vis.emit('hover', e);
});
// Bind events to chart(s)
if (enabledEvents.length) {
enabledEvents.forEach(function (event) {
self.enable(event, chart);
});
}
}
chart.on('brush', function (e) {
self.vis.emit('brush', e);
});
charts.push(chart);
chart.render();
});
};
charts.push(chart);
chart.render();
});
/**
* 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));
};
/**
* 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);
};
/**

View file

@ -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);

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}
}

View file

@ -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";

View file

@ -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;
};
});
});

View file

@ -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;

View file

@ -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);

View file

@ -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;
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);
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]);
if (isBrushable) {
attachedEvents.call(brush);
}
// 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');
};
/**
* 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)

View file

@ -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

View file

@ -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;
});

View file

@ -0,0 +1,52 @@
define(function (require) {
var _ = require('lodash');
/**
* Filters out a list by a given filter. This is currently used to impelment:
* - fieldType filters a list of fields by their type property
* - aggFilter filters a list of aggs by their name property
*
* @returns {function} - the filter function which can be registered with angular
*/
function propFilter(prop) {
/**
* List filtering function which accepts an array or list of values that a property
* must contain
*
* @param {array} list - array of items to filter
* @param {array|string} filters - the values to match against the list. Can be
* an array, a single value as a string, or a comma
* -seperated list of items
* @return {array} - the filtered list
*/
return function (list, filters) {
if (!filters) return filters;
if (!_.isArray(filters)) filters = filters.split(',');
if (_.contains(filters, '*')) return filters;
filters = filters.map(function (filter) {
var match = true;
var value = filter;
if (filter.charAt(0) === '!') {
match = false;
value = filter.substr(1);
}
return {
match: match,
value: value
};
});
return list.filter(function (item) {
for (var i = 0; i < filters.length; i++) {
var filter = filters[i];
if ((item[prop] === filter.value) === filter.match) return true;
}
});
};
}
return propFilter;
});

View file

@ -3,34 +3,11 @@
// Or an array of types to get all fields of that type
define(function (require) {
var _ = require('lodash');
var propFilter = require('filters/_prop_filter');
require('modules')
.get('kibana')
.filter('fieldType', function () {
return function (fields, types) {
if (!types) return fields;
if (!_.isArray(types)) types = [types];
if (_.contains(types, '*')) return fields;
var filters = types.map(function (type) {
var filter = {
match: true,
type: type
};
if (type.charAt(0) === '!') {
filter.match = false;
filter.type = type.substr(1);
}
return filter;
});
return fields.filter(function (field) {
for (var i = 0; i < filters.length; i++) {
var filter = filters[i];
if ((field.type === filter.type) === filter.match) return true;
}
});
};
});
.get('kibana')
.filter('fieldType', function () {
return propFilter('type');
});
});

View file

@ -640,8 +640,6 @@ define(function (require) {
type: 'histogram',
vislibParams: {
addLegend: false,
addEvents: true,
addBrushing: true,
},
listeners: {
click: function (e) {

View file

@ -1,36 +1,10 @@
// Gets all fields of a given type.
// You may also pass "*" to get all types
// Or an array of types to get all fields of that type
define(function (require) {
var _ = require('lodash');
var propFilter = require('filters/_prop_filter');
require('modules')
.get('kibana')
.filter('aggFilter', function () {
return function (aggs, names) {
if (!names) return aggs;
if (!_.isArray(names)) names = [names];
if (_.contains(names, '*')) return aggs;
var filters = names.map(function (name) {
var filter = {
match: true,
name: name
};
if (name.charAt(0) === '!') {
filter.match = false;
filter.name = name.substr(1);
}
return filter;
});
return aggs.filter(function (agg) {
for (var i = 0; i < filters.length; i++) {
var filter = filters[i];
if ((agg.name === filter.name) === filter.match) return true;
}
});
};
});
.get('kibana')
.filter('aggFilter', function () {
return propFilter('name');
});
});

View file

@ -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',

View file

@ -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
};
events = [
'click',
'brush'
];
vis = new Vis(el[0][0], config);
vis.data = data;
handler = ColumnHandler(vis);
// handler.render(data);
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 () {

View file

@ -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');
});
});
});
});
});