diff --git a/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js b/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js index a2c2cc4de455e29..6605479c0798c5f 100644 --- a/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js +++ b/src/core_plugins/kbn_vislib_vis_types/public/tile_map.js @@ -1,17 +1,17 @@ import _ from 'lodash'; import supports from 'ui/utils/supports'; -import VislibVisTypeVislibVisTypeProvider from 'ui/vislib_vis_type/vislib_vis_type'; +import MapsVisTypeVislibVisTypeProvider from 'ui/vis_maps/maps_vis_type'; import VisSchemasProvider from 'ui/vis/schemas'; import AggResponseGeoJsonGeoJsonProvider from 'ui/agg_response/geo_json/geo_json'; import FilterBarPushFilterProvider from 'ui/filter_bar/push_filter'; import tileMapTemplate from 'plugins/kbn_vislib_vis_types/editors/tile_map.html'; export default function TileMapVisType(Private, getAppState, courier, config) { - const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider); + const MapsVisType = Private(MapsVisTypeVislibVisTypeProvider); const Schemas = Private(VisSchemasProvider); const geoJsonConverter = Private(AggResponseGeoJsonGeoJsonProvider); - return new VislibVisType({ + return new MapsVisType({ name: 'tile_map', title: 'Tile map', icon: 'fa-map-marker', diff --git a/src/ui/public/vislib/__tests__/visualizations/tile_maps/map.js b/src/ui/public/vis_maps/__tests__/tile_maps/map.js similarity index 98% rename from src/ui/public/vislib/__tests__/visualizations/tile_maps/map.js rename to src/ui/public/vis_maps/__tests__/tile_maps/map.js index 2e9e1d1c69ef085..d03814db7d8d3d8 100644 --- a/src/ui/public/vislib/__tests__/visualizations/tile_maps/map.js +++ b/src/ui/public/vis_maps/__tests__/tile_maps/map.js @@ -7,7 +7,7 @@ import L from 'leaflet'; import sinon from 'auto-release-sinon'; import geoJsonData from 'fixtures/vislib/mock_data/geohash/_geo_json'; import $ from 'jquery'; -import VislibVisualizationsMapProvider from 'ui/vislib/visualizations/_map'; +import VislibVisualizationsMapProvider from 'ui/vis_maps/visualizations/_map'; // // Data // const dataArray = [ diff --git a/src/ui/public/vislib/__tests__/visualizations/tile_maps/markers.js b/src/ui/public/vis_maps/__tests__/tile_maps/markers.js similarity index 98% rename from src/ui/public/vislib/__tests__/visualizations/tile_maps/markers.js rename to src/ui/public/vis_maps/__tests__/tile_maps/markers.js index 8d067386953998e..bd9d59016dcc109 100644 --- a/src/ui/public/vislib/__tests__/visualizations/tile_maps/markers.js +++ b/src/ui/public/vis_maps/__tests__/tile_maps/markers.js @@ -7,10 +7,10 @@ import L from 'leaflet'; import sinon from 'auto-release-sinon'; import geoJsonData from 'fixtures/vislib/mock_data/geohash/_geo_json'; import $ from 'jquery'; -import VislibVisualizationsMarkerTypesBaseMarkerProvider from 'ui/vislib/visualizations/marker_types/base_marker'; -import VislibVisualizationsMarkerTypesShadedCirclesProvider from 'ui/vislib/visualizations/marker_types/shaded_circles'; -import VislibVisualizationsMarkerTypesScaledCirclesProvider from 'ui/vislib/visualizations/marker_types/scaled_circles'; -import VislibVisualizationsMarkerTypesHeatmapProvider from 'ui/vislib/visualizations/marker_types/heatmap'; +import VislibVisualizationsMarkerTypesBaseMarkerProvider from 'ui/vis_maps/visualizations/marker_types/base_marker'; +import VislibVisualizationsMarkerTypesShadedCirclesProvider from 'ui/vis_maps/visualizations/marker_types/shaded_circles'; +import VislibVisualizationsMarkerTypesScaledCirclesProvider from 'ui/vis_maps/visualizations/marker_types/scaled_circles'; +import VislibVisualizationsMarkerTypesHeatmapProvider from 'ui/vis_maps/visualizations/marker_types/heatmap'; // defaults to roughly the lower 48 US states const defaultSWCoords = [13.496, -143.789]; const defaultNECoords = [55.526, -57.919]; diff --git a/src/ui/public/vislib/__tests__/visualizations/tile_maps/tile_map.js b/src/ui/public/vis_maps/__tests__/tile_maps/tile_map.js similarity index 91% rename from src/ui/public/vislib/__tests__/visualizations/tile_maps/tile_map.js rename to src/ui/public/vis_maps/__tests__/tile_maps/tile_map.js index 8500ca9cd790f96..4ca355f57904cb1 100644 --- a/src/ui/public/vislib/__tests__/visualizations/tile_maps/tile_map.js +++ b/src/ui/public/vis_maps/__tests__/tile_maps/tile_map.js @@ -6,7 +6,7 @@ import sinon from 'auto-release-sinon'; import geoJsonData from 'fixtures/vislib/mock_data/geohash/_geo_json'; import MockMap from 'fixtures/tilemap_map'; import $ from 'jquery'; -import VislibVisualizationsTileMapProvider from 'ui/vislib/visualizations/tile_map'; +import VislibVisualizationsTileMapProvider from 'ui/vis_maps/visualizations/tile_map'; const mockChartEl = $('
'); let TileMap; @@ -18,6 +18,11 @@ function createTileMap(handler, chartEl, chartData) { get: function () { return ''; } + }, + uiState: { + get: function () { + return ''; + } } }; chartEl = chartEl || mockChartEl; @@ -32,7 +37,7 @@ describe('TileMap Tests', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Private.stub(require('ui/vislib/visualizations/_map'), MockMap); + Private.stub(require('ui/vis_maps/visualizations/_map'), MockMap); TileMap = Private(VislibVisualizationsTileMapProvider); extentsStub = sinon.stub(TileMap.prototype, '_appendGeoExtents', _.noop); })); @@ -55,12 +60,6 @@ describe('TileMap Tests', function () { it('should return a function', function () { expect(tilemap.draw()).to.be.a('function'); }); - - it('should call destroy for clean state', function () { - const destroySpy = sinon.spy(tilemap, 'destroy'); - tilemap.draw(); - expect(destroySpy.callCount).to.equal(1); - }); }); describe('appendMap', function () { diff --git a/src/ui/public/vis_maps/lib/data.js b/src/ui/public/vis_maps/lib/data.js new file mode 100644 index 000000000000000..2c84b801355a498 --- /dev/null +++ b/src/ui/public/vis_maps/lib/data.js @@ -0,0 +1,201 @@ +import d3 from 'd3'; +import _ from 'lodash'; +export default function DataFactory(Private) { + /** + * Provides an API for pulling values off the data + * and calculating values using the data + * + * @class Data + * @constructor + * @param data {Object} Elasticsearch query results + * @param attr {Object|*} Visualization options + */ + class Data { + constructor(data, uiState) { + this.uiState = uiState; + this.data = this.copyDataObj(data); + this._normalizeOrdered(); + } + + copyDataObj(data) { + const copyChart = data => { + const newData = {}; + Object.keys(data).forEach(key => { + if (key !== 'series') { + newData[key] = data[key]; + } else { + newData[key] = data[key].map(seri => { + return { + label: seri.label, + values: seri.values.map(val => { + const newVal = _.clone(val); + newVal.aggConfig = val.aggConfig; + newVal.aggConfigResult = val.aggConfigResult; + newVal.extraMetrics = val.extraMetrics; + return newVal; + }) + }; + }); + } + }); + return newData; + }; + + if (!data.series) { + const newData = {}; + Object.keys(data).forEach(key => { + if (!['rows', 'columns'].includes(key)) { + newData[key] = data[key]; + } + else { + newData[key] = data[key].map(chart => { + return copyChart(chart); + }); + } + }); + return newData; + } + return copyChart(data); + } + + /** + * Returns an array of the actual x and y data value objects + * from data with series keys + * + * @method chartData + * @returns {*} Array of data objects + */ + chartData() { + if (!this.data.series) { + const arr = this.data.rows ? this.data.rows : this.data.columns; + return _.toArray(arr); + } + return [this.data]; + }; + + /** + * Returns an array of chart data objects + * + * @method getVisData + * @returns {*} Array of chart data objects + */ + getVisData() { + let visData; + + if (this.data.rows) { + visData = this.data.rows; + } else if (this.data.columns) { + visData = this.data.columns; + } else { + visData = [this.data]; + } + + return visData; + }; + + /** + * get min and max for all cols, rows of data + * + * @method getMaxMin + * @return {Object} + */ + getGeoExtents() { + const visData = this.getVisData(); + + return _.reduce(_.pluck(visData, 'geoJson.properties'), function (minMax, props) { + return { + min: Math.min(props.min, minMax.min), + max: Math.max(props.max, minMax.max) + }; + }, {min: Infinity, max: -Infinity}); + }; + + /** + * Get attributes off the data, e.g. `tooltipFormatter` or `xAxisFormatter` + * pulls the value off the first item in the array + * these values are typically the same between data objects of the same chart + * TODO: May need to verify this or refactor + * + * @method get + * @param thing {String} Data object key + * @returns {*} Data object value + */ + get(thing, def) { + const source = (this.data.rows || this.data.columns || [this.data])[0]; + return _.get(source, thing, def); + }; + + /** + * Return an array of all value objects + * Pluck the data.series array from each data object + * Create an array of all the value objects from the series array + * + * @method flatten + * @returns {Array} Value objects + */ + flatten() { + return _(this.chartData()) + .pluck('series') + .flattenDeep() + .pluck('values') + .flattenDeep() + .value(); + }; + + /** + * ensure that the datas ordered property has a min and max + * if the data represents an ordered date range. + * + * @return {undefined} + */ + _normalizeOrdered() { + const data = this.getVisData(); + const self = this; + + data.forEach(function (d) { + if (!d.ordered || !d.ordered.date) return; + + const missingMin = d.ordered.min == null; + const missingMax = d.ordered.max == null; + + if (missingMax || missingMin) { + const extent = d3.extent(self.xValues()); + if (missingMin) d.ordered.min = extent[0]; + if (missingMax) d.ordered.max = extent[1]; + } + }); + }; + + /** + * Calculates min and max values for all map data + * series.rows is an array of arrays + * each row is an array of values + * last value in row array is bucket count + * + * @method mapDataExtents + * @param series {Array} Array of data objects + * @returns {Array} min and max values + */ + mapDataExtents(series) { + let values; + values = _.map(series.rows, function (row) { + return row[row.length - 1]; + }); + return [_.min(values), _.max(values)]; + }; + + /** + * Get the maximum number of series, considering each chart + * individually. + * + * @return {number} - the largest number of series from all charts + */ + maxNumberOfSeries() { + return this.chartData().reduce(function (max, chart) { + return Math.max(max, chart.series.length); + }, 0); + }; + } + + return Data; +}; diff --git a/src/ui/public/vis_maps/lib/dispatch.js b/src/ui/public/vis_maps/lib/dispatch.js new file mode 100644 index 000000000000000..114b7e2a6a2afb7 --- /dev/null +++ b/src/ui/public/vis_maps/lib/dispatch.js @@ -0,0 +1,315 @@ +import d3 from 'd3'; +import _ from 'lodash'; +import $ from 'jquery'; +import SimpleEmitter from 'ui/utils/simple_emitter'; + +export default function DispatchClass(Private, config) { + + /** + * Handles event responses + * + * @class Dispatch + * @constructor + * @param handler {Object} Reference to Handler Class Object + */ + + class Dispatch extends SimpleEmitter { + constructor(handler) { + super(); + this.handler = handler; + this._listeners = {}; + } + + /** + * Response to click and hover events + * + * @param d {Object} Data point + * @param i {Number} Index number of data point + * @returns {{value: *, point: *, label: *, color: *, pointIndex: *, + * series: *, config: *, data: (Object|*), + * e: (d3.event|*), handler: (Object|*)}} Event response object + */ + eventResponse(d, i) { + const datum = d._input || d; + const data = d3.event.target.nearestViewportElement ? + d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__; + const label = d.label ? d.label : (d.series || 'Count'); + const isSeries = !!(data && data.series); + const isSlices = !!(data && data.slices); + const series = isSeries ? data.series : undefined; + const slices = isSlices ? data.slices : undefined; + const handler = this.handler; + const color = _.get(handler, 'data.color'); + const isPercentage = (handler && handler.visConfig.get('mode', 'normal') === 'percentage'); + + const eventData = { + value: d.y, + point: datum, + datum: datum, + label: label, + color: color ? color(label) : undefined, + pointIndex: i, + series: series, + slices: slices, + config: handler && handler.visConfig, + data: data, + e: d3.event, + handler: handler + }; + + if (isSeries) { + // Find object with the actual d value and add it to the point object + const object = _.find(series, {'label': label}); + if (object) { + eventData.value = +object.values[i].y; + + if (isPercentage) { + // Add the formatted percentage to the point object + eventData.percent = (100 * d.y).toFixed(1) + '%'; + } + } + } + + return eventData; + }; + + /** + * Returns a function that adds events and listeners to a D3 selection + * + * @method addEvent + * @param event {String} + * @param callback {Function} + * @returns {Function} + */ + addEvent(event, callback) { + return function (selection) { + selection.each(function () { + const element = d3.select(this); + + if (typeof callback === 'function') { + return element.on(event, callback); + } + }); + }; + }; + + /** + * + * @method addHoverEvent + * @returns {Function} + */ + addHoverEvent() { + const self = this; + const isClickable = this.listenerCount('click') > 0; + const addEvent = this.addEvent; + const $el = this.handler.el; + if (!this.handler.highlight) { + this.handler.highlight = self.highlight; + } + + function hover(d, i) { + // Add pointer if item is clickable + if (isClickable) { + self.addMousePointer.call(this, arguments); + } + + self.handler.highlight.call(this, $el); + self.emit('hover', self.eventResponse(d, i)); + } + + return addEvent('mouseover', hover); + }; + + /** + * + * @method addMouseoutEvent + * @returns {Function} + */ + addMouseoutEvent() { + const self = this; + const addEvent = this.addEvent; + const $el = this.handler.el; + if (!this.handler.unHighlight) { + this.handler.unHighlight = self.unHighlight; + } + + function mouseout() { + self.handler.unHighlight.call(this, $el); + } + + return addEvent('mouseout', mouseout); + }; + + /** + * + * @method addClickEvent + * @returns {Function} + */ + addClickEvent() { + const self = this; + const addEvent = this.addEvent; + + function click(d, i) { + self.emit('click', self.eventResponse(d, i)); + } + + return addEvent('click', click); + }; + + /** + * Determine if we will allow brushing + * + * @method allowBrushing + * @returns {Boolean} + */ + allowBrushing() { + const xAxis = this.handler.categoryAxes[0]; + + //Allow brushing for ordered axis - date histogram and histogram + return Boolean(xAxis.ordered); + }; + + /** + * Determine if brushing is currently enabled + * + * @method isBrushable + * @returns {Boolean} + */ + isBrushable() { + return this.allowBrushing() && this.listenerCount('brush') > 0; + }; + + /** + * Mouseover Behavior + * + * @method addMousePointer + * @returns {d3.Selection} + */ + addMousePointer() { + return d3.select(this).style('cursor', 'pointer'); + }; + + /** + * Highlight the element that is under the cursor + * by reducing the opacity of all the elements on the graph. + * @param element {d3.Selection} + * @method highlight + */ + highlight(element) { + const label = this.getAttribute('data-label'); + if (!label) return; + + const dimming = config.get('visualization:dimmingOpacity'); + $(element).parent().find('[data-label]') + .css('opacity', 1)//Opacity 1 is needed to avoid the css application + .not((els, el) => String($(el).data('label')) === label) + .css('opacity', justifyOpacity(dimming)); + } + + /** + * Mouseout Behavior + * + * @param element {d3.Selection} + * @method unHighlight + */ + unHighlight(element) { + $('[data-label]', element.parentNode).css('opacity', 1); + }; + + /** + * Adds D3 brush to SVG and returns the brush function + * + * @param xScale {Function} D3 xScale function + * @param svg {HTMLElement} Reference to SVG + * @returns {*} Returns a D3 brush function and a SVG with a brush group attached + */ + createBrush(xScale, svg) { + const self = this; + const visConfig = self.handler.visConfig; + const {width, height} = svg.node().getBBox(); + const isHorizontal = self.handler.categoryAxes[0].axisConfig.isHorizontal(); + + // Brush scale + const brush = d3.svg.brush(); + if (isHorizontal) { + brush.x(xScale); + } else { + brush.y(xScale); + } + + brush.on('brushend', function brushEnd() { + + // Assumes data is selected at the chart level + // In this case, the number of data objects should always be 1 + const data = d3.select(this).data()[0]; + const isTimeSeries = (data.ordered && data.ordered.date); + + // Allows for brushing on d3.scale.ordinal() + const selected = xScale.domain().filter(function (d) { + return (brush.extent()[0] <= xScale(d)) && (xScale(d) <= brush.extent()[1]); + }); + const range = isTimeSeries ? brush.extent() : selected; + + return self.emit('brush', { + range: range, + config: visConfig, + e: d3.event, + data: data + }); + }); + + // if `addBrushing` is true, add brush canvas + if (self.listenerCount('brush')) { + const rect = svg.insert('g', 'g') + .attr('class', 'brush') + .call(brush) + .call(function (brushG) { + // hijack the brush start event to filter out right/middle clicks + const brushHandler = brushG.on('mousedown.brush'); + if (!brushHandler) return; // touch events in use + brushG.on('mousedown.brush', function () { + if (validBrushClick(d3.event)) brushHandler.apply(this, arguments); + }); + }) + .selectAll('rect'); + + if (isHorizontal) { + rect.attr('height', height); + } else { + rect.attr('width', width); + } + + return brush; + } + }; + } + + /** + * Determine if d3.Scale is quantitative + * + * @param element {d3.Scale} + * @method isQuantitativeScale + * @returns {boolean} + */ + function isQuantitativeScale(scale) { + //Invert is a method that only exists on quantitative scales + if (scale.invert) { + return true; + } else { + return false; + } + } + + function validBrushClick(event) { + return event.button === 0; + } + + + function justifyOpacity(opacity) { + const decimalNumber = parseFloat(opacity, 10); + const fallbackOpacity = 0.5; + return (0 <= decimalNumber && decimalNumber <= 1) ? decimalNumber : fallbackOpacity; + } + + return Dispatch; +}; diff --git a/src/ui/public/vis_maps/lib/layout.js b/src/ui/public/vis_maps/lib/layout.js new file mode 100644 index 000000000000000..a33d182b624f216 --- /dev/null +++ b/src/ui/public/vis_maps/lib/layout.js @@ -0,0 +1,50 @@ +import d3 from 'd3'; +import MapSplitProvider from './splits/map_split'; + +export default function LayoutFactory(Private) { + const mapSplit = Private(MapSplitProvider); + class Layout { + constructor(el, config, data) { + this.el = el; + this.config = config; + this.data = data; + } + + render() { + this.removeAll(); + this.createLayout(); + }; + + createLayout() { + const wrapper = this.appendElem(this.el, 'div', 'vis-wrapper'); + wrapper.datum(this.data.data); + const colWrapper = this.appendElem(wrapper.node(), 'div', 'vis-col-wrapper'); + const chartWrapper = this.appendElem(colWrapper.node(), 'div', 'chart-wrapper'); + chartWrapper.call(mapSplit, colWrapper.node(), this.config); + }; + + appendElem(el, type, className) { + if (!el || !type || !className) { + throw new Error('Function requires that an el, type, and class be provided'); + } + + if (typeof el === 'string') { + // Create a DOM reference with a d3 selection + // Need to make sure that the `el` is bound to this object + // to prevent it from being appended to another Layout + el = d3.select(this.el) + .select(el)[0][0]; + } + + return d3.select(el) + .append(type) + .attr('class', className); + }; + + removeAll() { + return d3.select(this.el).selectAll('*').remove(); + }; + } + + return Layout; +}; diff --git a/src/ui/public/vis_maps/lib/maps_config.js b/src/ui/public/vis_maps/lib/maps_config.js new file mode 100644 index 000000000000000..57c67a0c0480853 --- /dev/null +++ b/src/ui/public/vis_maps/lib/maps_config.js @@ -0,0 +1,38 @@ +/** + * Provides vislib configuration, throws error if invalid property is accessed without providing defaults + */ +import _ from 'lodash'; + +export default function MapsConfigFactory(Private) { + + const DEFAULT_VIS_CONFIG = { + style: { + margin : { top: 10, right: 3, bottom: 5, left: 3 } + }, + alerts: {}, + categoryAxes: [], + valueAxes: [] + }; + + + class MapsConfig { + constructor(mapsConfigArgs) { + this._values = _.defaultsDeep({}, mapsConfigArgs, DEFAULT_VIS_CONFIG); + }; + + get(property, defaults) { + if (_.has(this._values, property) || typeof defaults !== 'undefined') { + return _.get(this._values, property, defaults); + } else { + throw new Error(`Accessing invalid config property: ${property}`); + return defaults; + } + }; + + set(property, value) { + return _.set(this._values, property, value); + }; + } + + return MapsConfig; +} diff --git a/src/ui/public/vislib/lib/layout/splits/tile_map/map_split.js b/src/ui/public/vis_maps/lib/splits/map_split.js similarity index 100% rename from src/ui/public/vislib/lib/layout/splits/tile_map/map_split.js rename to src/ui/public/vis_maps/lib/splits/map_split.js diff --git a/src/ui/public/vis_maps/maps.js b/src/ui/public/vis_maps/maps.js new file mode 100644 index 000000000000000..f3a8c12841b19be --- /dev/null +++ b/src/ui/public/vis_maps/maps.js @@ -0,0 +1,106 @@ +import _ from 'lodash'; +import d3 from 'd3'; +import MapsConfigProvider from './lib/maps_config'; +import TileMapChartProvider from './visualizations/tile_map'; +import EventsProvider from 'ui/events'; +import MapsDataProvider from './lib/data'; +import LayoutProvider from './lib/layout'; +import './styles/_tilemap.less'; + +export default function MapsFactory(Private) { + const Events = Private(EventsProvider); + const MapsConfig = Private(MapsConfigProvider); + const TileMapChart = Private(TileMapChartProvider); + const Data = Private(MapsDataProvider); + const Layout = Private(LayoutProvider); + + class Maps extends Events { + constructor($el, vis, mapsConfigArgs) { + super(arguments); + this.el = $el.get ? $el.get(0) : $el; + this.vis = vis; + this.mapsConfigArgs = mapsConfigArgs; + + // memoize so that the same function is returned every time, + // allowing us to remove/re-add the same function + this.getProxyHandler = _.memoize(function (event) { + const self = this; + return function (e) { + self.emit(event, e); + }; + }); + + this.enable = this.chartEventProxyToggle('on'); + this.disable = this.chartEventProxyToggle('off'); + } + + chartEventProxyToggle(method) { + return function (event, chart) { + const proxyHandler = this.getProxyHandler(event); + + _.each(chart ? [chart] : this.charts, function (chart) { + chart.events[method](event, proxyHandler); + }); + }; + } + + on(event, listener) { + const first = this.listenerCount(event) === 0; + const ret = Events.prototype.on.call(this, event, listener); + const added = this.listenerCount(event) > 0; + + // if this is the first listener added for the event + // enable the event in the handler + if (first && added && this.handler) this.handler.enable(event); + + return ret; + }; + + off(event, listener) { + const last = this.listenerCount(event) === 1; + const ret = Events.prototype.off.call(this, event, listener); + const removed = this.listenerCount(event) === 0; + + // Once all listeners are removed, disable the events in the handler + if (last && removed && this.handler) this.handler.disable(event); + return ret; + }; + + render(data, uiState) { + if (!data) { + throw new Error('No valid data!'); + } + + this.uiState = uiState; + this.data = new Data(data, this.uiState); + this.visConfig = new MapsConfig(this.mapsConfigArgs, this.data, this.uiState); + this.layout = new Layout(this.el, this.visConfig, this.data); + this.draw(); + }; + + destroy() { + this.charts.forEach(chart => chart.destroy()); + d3.select(this.el).selectAll('*').remove(); + }; + + draw() { + this.layout.render(); + // todo: title + const self = this; + this.charts = []; + d3.select(this.el).selectAll('.chart').each(function (chartData) { + const chart = new TileMapChart(self, this, chartData); + + self.activeEvents().forEach(function (event) { + self.enable(event, chart); + }); + + self.charts.push(chart); + chart.render(); + }); + } + + } + + return Maps; +}; diff --git a/src/ui/public/vis_maps/maps_renderbot.js b/src/ui/public/vis_maps/maps_renderbot.js new file mode 100644 index 000000000000000..94aae9f230b6d5c --- /dev/null +++ b/src/ui/public/vis_maps/maps_renderbot.js @@ -0,0 +1,77 @@ +import _ from 'lodash'; +import MapsProvider from 'ui/vis_maps/maps'; +import VisRenderbotProvider from 'ui/vis/renderbot'; +import MapsVisTypeBuildChartDataProvider from 'ui/vislib_vis_type/build_chart_data'; +module.exports = function MapsRenderbotFactory(Private, $injector) { + const AngularPromise = $injector.get('Promise'); + let Maps = Private(MapsProvider); + let Renderbot = Private(VisRenderbotProvider); + let buildChartData = Private(MapsVisTypeBuildChartDataProvider); + + _.class(MapsRenderbot).inherits(Renderbot); + function MapsRenderbot(vis, $el, uiState) { + MapsRenderbot.Super.call(this, vis, $el, uiState); + this._createVis(); + } + + MapsRenderbot.prototype._createVis = function () { + if (this.mapsVis) this.destroy(); + this.mapsParams = this._getMapsParams(); + this.mapsVis = new Maps(this.$el[0], this.vis, this.mapsParams); + + _.each(this.vis.listeners, (listener, event) => { + this.mapsVis.on(event, listener); + }); + + if (this.mapsData) { + this.mapsVis.render(this.mapsData, this.uiState); + } + }; + + MapsRenderbot.prototype._getMapsParams = function () { + let self = this; + + return _.assign( + {}, + self.vis.type.params.defaults, + { + type: self.vis.type.name, + // Add attribute which determines whether an index is time based or not. + hasTimeField: self.vis.indexPattern && self.vis.indexPattern.hasTimeField() + }, + self.vis.params + ); + }; + + MapsRenderbot.prototype.buildChartData = buildChartData; + MapsRenderbot.prototype.render = function (esResponse) { + this.mapsData = this.buildChartData(esResponse); + return AngularPromise.delay(1).then(() => { + this.mapsVis.render(this.mapsData, this.uiState); + }); + }; + + MapsRenderbot.prototype.destroy = function () { + let self = this; + + let mapsVis = self.mapsVis; + + _.forOwn(self.vis.listeners, function (listener, event) { + mapsVis.off(event, listener); + }); + + mapsVis.destroy(); + }; + + MapsRenderbot.prototype.updateParams = function () { + let self = this; + + // get full maps params object + let newParams = self._getMapsParams(); + + // if there's been a change, replace the vis + if (!_.isEqual(newParams, self.mapsParams)) self._createVis(); + }; + + return MapsRenderbot; +}; diff --git a/src/ui/public/vis_maps/maps_vis_type.js b/src/ui/public/vis_maps/maps_vis_type.js new file mode 100644 index 000000000000000..1f730bdcf45a7da --- /dev/null +++ b/src/ui/public/vis_maps/maps_vis_type.js @@ -0,0 +1,22 @@ +import _ from 'lodash'; +import 'ui/vislib'; +import 'plugins/kbn_vislib_vis_types/controls/vislib_basic_options'; +import VisVisTypeProvider from 'ui/vis/vis_type'; +import MapsVisTypeMapsRenderbotProvider from 'ui/vis_maps/maps_renderbot'; +export default function MapsVisTypeFactory(Private) { + let VisType = Private(VisVisTypeProvider); + let MapsRenderbot = Private(MapsVisTypeMapsRenderbotProvider); + + + _.class(MapsVisType).inherits(VisType); + function MapsVisType(opts = {}) { + MapsVisType.Super.call(this, opts); + this.listeners = opts.listeners || {}; + } + + MapsVisType.prototype.createRenderbot = function (vis, $el, uiState) { + return new MapsRenderbot(vis, $el, uiState); + }; + + return MapsVisType; +}; diff --git a/src/ui/public/vislib/styles/_tilemap.less b/src/ui/public/vis_maps/styles/_tilemap.less similarity index 100% rename from src/ui/public/vislib/styles/_tilemap.less rename to src/ui/public/vis_maps/styles/_tilemap.less diff --git a/src/ui/public/vis_maps/visualizations/_chart.js b/src/ui/public/vis_maps/visualizations/_chart.js new file mode 100644 index 000000000000000..23f890dd50880e1 --- /dev/null +++ b/src/ui/public/vis_maps/visualizations/_chart.js @@ -0,0 +1,60 @@ +import d3 from 'd3'; +import _ from 'lodash'; +import VislibLibDispatchProvider from '../lib/dispatch'; +import TooltipProvider from 'ui/vis/components/tooltip'; +export default function ChartBaseClass(Private) { + + const Dispatch = Private(VislibLibDispatchProvider); + const Tooltip = Private(TooltipProvider); + /** + * The Base Class for all visualizations. + * + * @class Chart + * @constructor + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific chart + */ + class Chart { + constructor(handler, el, chartData) { + this.handler = handler; + this.chartEl = el; + this.chartData = chartData; + this.tooltips = []; + + const events = this.events = new Dispatch(handler); + + if (this.handler.visConfig && this.handler.visConfig.get('addTooltip', false)) { + const $el = this.handler.el; + const formatter = this.handler.data.get('tooltipFormatter'); + + // Add tooltip + this.tooltip = new Tooltip('chart', $el, formatter, events); + this.tooltips.push(this.tooltip); + } + } + + render() { + const selection = d3.select(this.chartEl); + selection.selectAll('*').remove(); + selection.call(this.draw()); + }; + + + /** + * Removes all DOM elements from the root element + * + * @method destroy + */ + destroy() { + const selection = d3.select(this.chartEl); + this.events.removeAllListeners(); + this.tooltips.forEach(function (tooltip) { + tooltip.destroy(); + }); + selection.remove(); + }; + } + + return Chart; +}; diff --git a/src/ui/public/vislib/visualizations/_map.js b/src/ui/public/vis_maps/visualizations/_map.js similarity index 97% rename from src/ui/public/vislib/visualizations/_map.js rename to src/ui/public/vis_maps/visualizations/_map.js index a0b5c66fab71f71..9fa887560e71109 100644 --- a/src/ui/public/vislib/visualizations/_map.js +++ b/src/ui/public/vis_maps/visualizations/_map.js @@ -7,10 +7,10 @@ marked.setOptions({ sanitize: true // Sanitize HTML tags }); -import VislibVisualizationsMarkerTypesScaledCirclesProvider from 'ui/vislib/visualizations/marker_types/scaled_circles'; -import VislibVisualizationsMarkerTypesShadedCirclesProvider from 'ui/vislib/visualizations/marker_types/shaded_circles'; -import VislibVisualizationsMarkerTypesGeohashGridProvider from 'ui/vislib/visualizations/marker_types/geohash_grid'; -import VislibVisualizationsMarkerTypesHeatmapProvider from 'ui/vislib/visualizations/marker_types/heatmap'; +import VislibVisualizationsMarkerTypesScaledCirclesProvider from './marker_types/scaled_circles'; +import VislibVisualizationsMarkerTypesShadedCirclesProvider from './marker_types/shaded_circles'; +import VislibVisualizationsMarkerTypesGeohashGridProvider from './marker_types/geohash_grid'; +import VislibVisualizationsMarkerTypesHeatmapProvider from './marker_types/heatmap'; export default function MapFactory(Private, tilemap, $sanitize) { const defaultMapZoom = 2; diff --git a/src/ui/public/vislib/visualizations/marker_types/base_marker.js b/src/ui/public/vis_maps/visualizations/marker_types/base_marker.js similarity index 100% rename from src/ui/public/vislib/visualizations/marker_types/base_marker.js rename to src/ui/public/vis_maps/visualizations/marker_types/base_marker.js diff --git a/src/ui/public/vislib/visualizations/marker_types/geohash_grid.js b/src/ui/public/vis_maps/visualizations/marker_types/geohash_grid.js similarity index 100% rename from src/ui/public/vislib/visualizations/marker_types/geohash_grid.js rename to src/ui/public/vis_maps/visualizations/marker_types/geohash_grid.js diff --git a/src/ui/public/vislib/visualizations/marker_types/heatmap.js b/src/ui/public/vis_maps/visualizations/marker_types/heatmap.js similarity index 100% rename from src/ui/public/vislib/visualizations/marker_types/heatmap.js rename to src/ui/public/vis_maps/visualizations/marker_types/heatmap.js diff --git a/src/ui/public/vislib/visualizations/marker_types/scaled_circles.js b/src/ui/public/vis_maps/visualizations/marker_types/scaled_circles.js similarity index 100% rename from src/ui/public/vislib/visualizations/marker_types/scaled_circles.js rename to src/ui/public/vis_maps/visualizations/marker_types/scaled_circles.js diff --git a/src/ui/public/vislib/visualizations/marker_types/shaded_circles.js b/src/ui/public/vis_maps/visualizations/marker_types/shaded_circles.js similarity index 100% rename from src/ui/public/vislib/visualizations/marker_types/shaded_circles.js rename to src/ui/public/vis_maps/visualizations/marker_types/shaded_circles.js diff --git a/src/ui/public/vislib/visualizations/tile_map.js b/src/ui/public/vis_maps/visualizations/tile_map.js similarity index 93% rename from src/ui/public/vislib/visualizations/tile_map.js rename to src/ui/public/vis_maps/visualizations/tile_map.js index 467d3d9f0f66437..53484f851de1d0c 100644 --- a/src/ui/public/vislib/visualizations/tile_map.js +++ b/src/ui/public/vis_maps/visualizations/tile_map.js @@ -39,9 +39,6 @@ export default function TileMapFactory(Private) { draw() { const self = this; - // clean up old maps - self.destroy(); - return function (selection) { selection.each(function () { self._appendMap(this); @@ -95,10 +92,10 @@ export default function TileMapFactory(Private) { */ _appendMap(selection) { const container = $(selection).addClass('tilemap'); - const uiStateParams = this.handler.vis ? { - mapCenter: this.handler.vis.uiState.get('mapCenter'), - mapZoom: this.handler.vis.uiState.get('mapZoom') - } : {}; + const uiStateParams = { + mapCenter: this.handler.uiState.get('mapCenter'), + mapZoom: this.handler.uiState.get('mapZoom') + }; const params = _.assign({}, _.get(this._chartData, 'geoAgg.vis.params'), uiStateParams); diff --git a/src/ui/public/vislib/lib/layout/layout_types.js b/src/ui/public/vislib/lib/layout/layout_types.js index 2be3b007b9edd10..c9680ee0985f030 100644 --- a/src/ui/public/vislib/lib/layout/layout_types.js +++ b/src/ui/public/vislib/lib/layout/layout_types.js @@ -1,6 +1,5 @@ import VislibLibLayoutTypesColumnLayoutProvider from './types/column_layout'; import VislibLibLayoutTypesPieLayoutProvider from './types/pie_layout'; -import VislibLibLayoutTypesMapLayoutProvider from './types/map_layout'; export default function LayoutTypeFactory(Private) { @@ -14,7 +13,6 @@ export default function LayoutTypeFactory(Private) { */ return { pie: Private(VislibLibLayoutTypesPieLayoutProvider), - tile_map: Private(VislibLibLayoutTypesMapLayoutProvider), point_series: Private(VislibLibLayoutTypesColumnLayoutProvider) }; } diff --git a/src/ui/public/vislib/lib/layout/types/map_layout.js b/src/ui/public/vislib/lib/layout/types/map_layout.js index 06aeada0ae51488..e69de29bb2d1d64 100644 --- a/src/ui/public/vislib/lib/layout/types/map_layout.js +++ b/src/ui/public/vislib/lib/layout/types/map_layout.js @@ -1,49 +0,0 @@ -import VislibLibLayoutSplitsTileMapMapSplitProvider from '../splits/tile_map/map_split'; -export default function ColumnLayoutFactory(Private) { - const mapSplit = Private(VislibLibLayoutSplitsTileMapMapSplitProvider); - - /* - * Specifies the visualization layout for tile maps. - * - * This is done using an array of objects. The first object has - * a `parent` DOM element, a DOM `type` (e.g. div, svg, etc), - * and a `class` (required). Each child can omit the parent object, - * but must include a type and class. - * - * Optionally, you can specify `datum` to be bound to the DOM - * element, a `splits` function that divides the selected element - * into more DOM elements based on a callback function provided, or - * a children array which nests other layout objects. - * - * Objects in children arrays are children of the current object and return - * DOM elements which are children of their respective parent element. - */ - - return function (el, data) { - if (!el || !data) { - throw new Error('Both an el and data need to be specified'); - } - - return [ - { - parent: el, - type: 'div', - class: 'vis-wrapper', - datum: data, - children: [ - { - type: 'div', - class: 'vis-col-wrapper', - children: [ - { - type: 'div', - class: 'chart-wrapper', - splits: mapSplit - } - ] - } - ] - } - ]; - }; -} diff --git a/src/ui/public/vislib/lib/types/index.js b/src/ui/public/vislib/lib/types/index.js index 140db3425a0fea7..eddc7947992f3db 100644 --- a/src/ui/public/vislib/lib/types/index.js +++ b/src/ui/public/vislib/lib/types/index.js @@ -1,6 +1,5 @@ import VislibLibTypesPointSeriesProvider from './point_series'; import VislibLibTypesPieProvider from './pie'; -import VislibLibTypesTileMapProvider from './tile_map'; export default function TypeFactory(Private) { const pointSeries = Private(VislibLibTypesPointSeriesProvider); @@ -15,7 +14,6 @@ export default function TypeFactory(Private) { line: pointSeries.line, pie: Private(VislibLibTypesPieProvider), area: pointSeries.area, - tile_map: Private(VislibLibTypesTileMapProvider), point_series: pointSeries.line }; } diff --git a/src/ui/public/vislib/lib/types/tile_map.js b/src/ui/public/vislib/lib/types/tile_map.js deleted file mode 100644 index d69f1193750969c..000000000000000 --- a/src/ui/public/vislib/lib/types/tile_map.js +++ /dev/null @@ -1,19 +0,0 @@ -import _ from 'lodash'; -export default function MapHandlerProvider(Private) { - return function (config) { - if (!config.chart) { - config.chart = _.defaults({}, config, { - type: 'tile_map' - }); - } - - config.resize = function () { - this.charts.forEach(function (chart) { - chart.resizeArea(); - }); - }; - - return config; - }; -} - diff --git a/src/ui/public/vislib/styles/main.less b/src/ui/public/vislib/styles/main.less index f44fe6823d33c89..3c1c25ab908feed 100644 --- a/src/ui/public/vislib/styles/main.less +++ b/src/ui/public/vislib/styles/main.less @@ -5,5 +5,4 @@ @import "./_legend"; @import "./_svg"; @import "./_tooltip"; -@import "./_tilemap"; @import "./_alerts"; diff --git a/src/ui/public/vislib/vislib.js b/src/ui/public/vislib/vislib.js index 0c0a91164f1c404..147ec3563d7f391 100644 --- a/src/ui/public/vislib/vislib.js +++ b/src/ui/public/vislib/vislib.js @@ -1,10 +1,8 @@ import './lib/types/pie'; import './lib/types/point_series'; -import './lib/types/tile_map'; import './lib/types'; import './lib/layout/layout_types'; import './lib/data'; -import './visualizations/_map.js'; import './visualizations/vis_types'; import './styles/main.less'; import VislibVisProvider from './vis'; diff --git a/src/ui/public/vislib/visualizations/vis_types.js b/src/ui/public/vislib/visualizations/vis_types.js index c27fe6ad6d9e21c..268ba8c849b6de6 100644 --- a/src/ui/public/vislib/visualizations/vis_types.js +++ b/src/ui/public/vislib/visualizations/vis_types.js @@ -1,6 +1,5 @@ import VislibVisualizationsPointSeriesProvider from './point_series'; import VislibVisualizationsPieChartProvider from './pie_chart'; -import VislibVisualizationsTileMapProvider from './tile_map'; export default function VisTypeFactory(Private) { @@ -14,7 +13,6 @@ export default function VisTypeFactory(Private) { */ return { pie: Private(VislibVisualizationsPieChartProvider), - tile_map: Private(VislibVisualizationsTileMapProvider), point_series: Private(VislibVisualizationsPointSeriesProvider) }; }