diff --git a/src/kibana/components/vislib/components/tooltip/tooltip.js b/src/kibana/components/vislib/components/tooltip/tooltip.js index 21776b0932cc..9215082ac1c3 100644 --- a/src/kibana/components/vislib/components/tooltip/tooltip.js +++ b/src/kibana/components/vislib/components/tooltip/tooltip.js @@ -3,6 +3,8 @@ define(function (require) { var _ = require('lodash'); var $ = require('jquery'); + var allContents = []; + require('css!components/vislib/styles/main'); /** @@ -14,16 +16,20 @@ define(function (require) { * @param formatter {Function} Tooltip formatter * @param events {Constructor} Allows tooltip to return event response data */ - function Tooltip(el, formatter, events) { + function Tooltip(id, el, formatter, events) { if (!(this instanceof Tooltip)) { - return new Tooltip(el, formatter, events); + return new Tooltip(id, el, formatter, events); } + + this.id = id; // unique id for this tooltip type this.el = el; + this.order = 100; // higher ordered contents are rendered below the others this.formatter = formatter; this.events = events; this.containerClass = 'vis-wrapper'; this.tooltipClass = 'vis-tooltip'; this.tooltipSizerClass = 'vis-tooltip-sizing-clone'; + this.showCondition = _.constant(true); this.$window = $(window); this.$chart = $(el).find('.' + this.containerClass); @@ -63,6 +69,9 @@ define(function (require) { return function (selection) { var $tooltip = self.$get(); var $sizer = self.$getSizer(); + var id = self.id; + var order = self.order; + var tooltipSelection = d3.select($tooltip.get(0)); if (self.container === undefined || self.container !== d3.select(self.el).select('.' + self.containerClass)) { @@ -76,44 +85,71 @@ define(function (require) { // only clear when we leave the chart, so that // moving between points doesn't make it reposition - self.previousPlacement = null; + self.$chart.removeData('previousPlacement'); }); selection.each(function (d, i) { var element = d3.select(this); - function show() { - var placement = self.previousPlacement = self.getTooltipPlacement({ + function render(html, placement) { + allContents = _.filter(allContents, function (content) { + return content.id !== id; + }); + + if (html) allContents.push({ id: id, html: html, order: order }); + + var allHtml = _(allContents) + .sortBy('order') + .pluck('html') + .compact() + .join('\n'); + + if (allHtml) { + $tooltip.html(allHtml).css('visibility', 'visible'); + } else { + $tooltip.css({ + visibility: 'hidden', + left: '-500px', + top: '-500px' + }); + } + + if (placement) { + $tooltip.css({ + left: placement.left + 'px', + top: placement.top + 'px' + }); + } + } + + element + .on('mousemove.tip', function update() { + if (!self.showCondition.call(element, d, i)) { + return render(); + } + + var event = d3.event; + var placement = self.getTooltipPlacement({ $window: self.$window, $chart: self.$chart, $el: $tooltip, $sizer: $sizer, - event: d3.event, - prev: self.previousPlacement + event: event, + prev: self.$chart.data('previousPlacement') }); + + self.$chart.data('previousPlacement', placement); if (!placement) return; var events = self.events ? self.events.eventResponse(d, i) : d; var html = tooltipFormatter(events); - if (!html) return hide(); - - // return text and position for tooltip - return tooltipSelection - .html(html) - .style('visibility', 'visible') - .style('left', placement.left + 'px') - .style('top', placement.top + 'px'); - } + if (!html) return render(); - function hide() { - return tooltipSelection.style('visibility', 'hidden') - .style('left', '-500px') - .style('top', '-500px'); - } - - element - .on('mousemove.tip', show) - .on('mouseout.tip', hide); + render(html, placement); + }) + .on('mouseout.tip', function () { + render(); + }); }); }; }; diff --git a/src/kibana/components/vislib/lib/chart_title.js b/src/kibana/components/vislib/lib/chart_title.js index 7d1bcc9a5c38..a628a1d7c58f 100644 --- a/src/kibana/components/vislib/lib/chart_title.js +++ b/src/kibana/components/vislib/lib/chart_title.js @@ -20,7 +20,7 @@ define(function (require) { } this.el = el; - this.tooltip = new Tooltip(el, function (d) { + this.tooltip = new Tooltip('chart-title', el, function (d) { return d.label; }); } diff --git a/src/kibana/components/vislib/lib/dispatch.js b/src/kibana/components/vislib/lib/dispatch.js index 0cf269ed8c99..6683c2476f71 100644 --- a/src/kibana/components/vislib/lib/dispatch.js +++ b/src/kibana/components/vislib/lib/dispatch.js @@ -1,6 +1,8 @@ define(function (require) { - return function DispatchClass(d3) { + return function DispatchClass(d3, Private) { var _ = require('lodash'); + var $ = require('jquery'); + var Tooltip = Private(require('components/vislib/components/tooltip/tooltip')); /** * Handles event responses @@ -162,7 +164,6 @@ define(function (require) { var xScale = this.handler.xAxis.xScale; var yScale = this.handler.xAxis.yScale; var brush = this.createBrush(xScale, svg); - var endZones = this.createEndZones(xScale, yScale, svg); function brushEnd() { var bar = d3.select(this); @@ -243,69 +244,6 @@ define(function (require) { } }; - /** - * Creates rects to show buckets outside of the ordered.min and max, returns rects - * - * @param xScale {Function} D3 xScale function - * @param svg {HTMLElement} Reference to SVG - * @method createEndZones - * @returns {D3.Selection} - */ - Dispatch.prototype.createEndZones = function (xScale, yScale, svg) { - var attr = this.handler._attr; - var height = attr.height; - var width = attr.width; - var margin = attr.margin; - var ordered = this.handler.xAxis.ordered; - var xVals = this.handler.xAxis.xValues; - var color = '#004c99'; - var data = [ - { - x: 0, - w: xScale(ordered.min) > 0 ? xScale(ordered.min) : 0 - }, - { - x: xScale(ordered.max), - w: width - xScale(ordered.max) > 0 ? width - xScale(ordered.max) : 0 - } - ]; - - // svg diagonal line pattern - var pattern = svg.append('defs') - .append('pattern') - .attr('id', 'DiagonalLines') - .attr('patternUnits', 'userSpaceOnUse') - .attr('patternTransform', 'rotate(45)') - .attr('x', '0') - .attr('y', '0') - .attr('width', '4') - .attr('height', '4') - .append('rect') - .attr('stroke', 'none') - .attr('fill', color) - .attr('width', 2) - .attr('height', 4); - - var endzones = svg.selectAll('.layer') - .data(data) - .enter() - .insert('g', '.brush') - .attr('class', 'endzone') - .append('rect') - .attr('class', 'zone') - .attr('x', function (d) { - return d.x; - }) - .attr('y', 0) - .attr('height', height - margin.top - margin.bottom) - .attr('width', function (d) { - return d.w; - }) - .attr('fill', 'url(#DiagonalLines)'); - - return endzones; - }; - return Dispatch; }; diff --git a/src/kibana/components/vislib/partials/touchdown.html b/src/kibana/components/vislib/partials/touchdown.html new file mode 100644 index 000000000000..1616f4fbc80c --- /dev/null +++ b/src/kibana/components/vislib/partials/touchdown.html @@ -0,0 +1,3 @@ +

+ This area is outside of the selected time range +

\ No newline at end of file diff --git a/src/kibana/components/vislib/styles/_tooltip.less b/src/kibana/components/vislib/styles/_tooltip.less index b6d27879c06c..063040239bec 100644 --- a/src/kibana/components/vislib/styles/_tooltip.less +++ b/src/kibana/components/vislib/styles/_tooltip.less @@ -1,12 +1,14 @@ @import (reference) "../../../styles/main"; +@tooltip-padding: 8px; + .vis-tooltip, .vis-tooltip-sizing-clone { visibility: hidden; line-height: 1.1; font-size: 12px; font-weight: normal; - padding: 8px; + padding: 0 @tooltip-padding @tooltip-padding; background: rgba(70, 82, 93, 0.95); color: @gray-lighter; border-radius: 4px; @@ -14,8 +16,11 @@ z-index: 120; word-wrap: break-word; max-width: 40%; + overflow: hidden; table { + margin: @tooltip-padding 0 0; + td,th { padding: 2px 4px; @@ -40,6 +45,16 @@ } } +.vis-tooltip-header { + margin: 0 (@tooltip-padding * -1); + padding: (@tooltip-padding / 2) @tooltip-padding; + text-align: center; + + &:last-child { + margin-bottom: (@tooltip-padding * -1); + } +} + .vis-tooltip-sizing-clone { visibility: hidden; position: fixed; diff --git a/src/kibana/components/vislib/visualizations/_chart.js b/src/kibana/components/vislib/visualizations/_chart.js index f60068d895cc..ad419953ef18 100644 --- a/src/kibana/components/vislib/visualizations/_chart.js +++ b/src/kibana/components/vislib/visualizations/_chart.js @@ -31,8 +31,7 @@ define(function (require) { var formatter = this.handler.data.get('tooltipFormatter'); // Add tooltip - this.tooltip = new Tooltip($el, formatter, events); - + this.tooltip = new Tooltip('chart', $el, formatter, events); } this._attr = _.defaults(handler._attr || {}, {}); diff --git a/src/kibana/components/vislib/visualizations/_point_series_chart.js b/src/kibana/components/vislib/visualizations/_point_series_chart.js new file mode 100644 index 000000000000..f35fd73270be --- /dev/null +++ b/src/kibana/components/vislib/visualizations/_point_series_chart.js @@ -0,0 +1,119 @@ +define(function (require) { + return function PointSeriesChartProvider(d3, Private) { + var _ = require('lodash'); + + var Chart = Private(require('components/vislib/visualizations/_chart')); + var Tooltip = Private(require('components/vislib/components/tooltip/tooltip')); + var touchdownHtml = require('text!components/vislib/partials/touchdown.html'); + + _(PointSeriesChart).inherits(Chart); + function PointSeriesChart(handler, chartEl, chartData) { + if (!(this instanceof PointSeriesChart)) { + return new PointSeriesChart(handler, chartEl, chartData); + } + + PointSeriesChart.Super.apply(this, arguments); + } + + /** + * Stacks chart data values + * + * @method stackData + * @param data {Object} Elasticsearch query result for this chart + * @returns {Array} Stacked data objects with x, y, and y0 values + */ + PointSeriesChart.prototype.stackData = function (data) { + var self = this; + var stack = this._attr.stack; + + return stack(data.series.map(function (d) { + var label = d.label; + return d.values.map(function (e, i) { + return { + _input: e, + label: label, + x: self._attr.xValue.call(d.values, e, i), + y: self._attr.yValue.call(d.values, e, i) + }; + }); + })); + }; + + /** + * Creates rects to show buckets outside of the ordered.min and max, returns rects + * + * @param xScale {Function} D3 xScale function + * @param svg {HTMLElement} Reference to SVG + * @method createEndZones + * @returns {D3.Selection} + */ + PointSeriesChart.prototype.createEndZones = function (svg) { + var xScale = this.handler.xAxis.xScale; + var yScale = this.handler.xAxis.yScale; + var addEvent = this.addEvent; + var attr = this.handler._attr; + var height = attr.height; + var width = attr.width; + var margin = attr.margin; + var ordered = this.handler.xAxis.ordered; + var xVals = this.handler.xAxis.xValues; + var color = '#004c99'; + + if (!ordered || ordered.min == null || ordered.max == null) return; + + var leftEndzone = { + x: 0, + w: xScale(ordered.min) > 0 ? xScale(ordered.min) : 0 + }; + + var rightEndzone = { + x: xScale(ordered.max), + w: width - xScale(ordered.max) > 0 ? width - xScale(ordered.max) : 0 + }; + + // svg diagonal line pattern + this.pattern = svg.append('defs') + .append('pattern') + .attr('id', 'DiagonalLines') + .attr('patternUnits', 'userSpaceOnUse') + .attr('patternTransform', 'rotate(45)') + .attr('x', '0') + .attr('y', '0') + .attr('width', '4') + .attr('height', '4') + .append('rect') + .attr('stroke', 'none') + .attr('fill', color) + .attr('width', 2) + .attr('height', 4); + + this.endzones = svg.selectAll('.layer') + .data([leftEndzone, rightEndzone]) + .enter() + .insert('g', '.brush') + .attr('class', 'endzone') + .append('rect') + .attr('class', 'zone') + .attr('x', function (d) { + return d.x; + }) + .attr('y', 0) + .attr('height', height - margin.top - margin.bottom) + .attr('width', function (d) { + return d.w; + }) + .attr('fill', 'url(#DiagonalLines)'); + + var touchdown = _.constant(touchdownHtml); + var endzoneTT = this.endzoneTT = new Tooltip('endzones', this.handler.el, touchdown, null); + endzoneTT.order = 0; + endzoneTT.showCondition = function inEndzone() { + var x = d3.event.offsetX; + return (x < leftEndzone.w) || (x > rightEndzone.x); + }; + endzoneTT.render()(svg); + }; + + return PointSeriesChart; + }; +}); \ No newline at end of file diff --git a/src/kibana/components/vislib/visualizations/area_chart.js b/src/kibana/components/vislib/visualizations/area_chart.js index 76189b8df6fd..82758f7ede45 100644 --- a/src/kibana/components/vislib/visualizations/area_chart.js +++ b/src/kibana/components/vislib/visualizations/area_chart.js @@ -3,7 +3,7 @@ define(function (require) { var _ = require('lodash'); var $ = require('jquery'); - var Chart = Private(require('components/vislib/visualizations/_chart')); + var PointSeriesChart = Private(require('components/vislib/visualizations/_point_series_chart')); var errors = require('errors'); require('css!components/vislib/styles/main'); @@ -18,7 +18,7 @@ define(function (require) { * @param chartData {Object} Elasticsearch query results for this specific * chart */ - _(AreaChart).inherits(Chart); + _(AreaChart).inherits(PointSeriesChart); function AreaChart(handler, chartEl, chartData) { if (!(this instanceof AreaChart)) { return new AreaChart(handler, chartEl, chartData); @@ -42,31 +42,6 @@ define(function (require) { }); } - /** - * Stacks chart data values - * TODO: refactor so that this is called from the data module - * - * @method stackData - * @param data {Object} Elasticsearch query result for this chart - * @returns {Array} Stacked data objects with x, y, and y0 values - */ - AreaChart.prototype.stackData = function (data) { - var self = this; - var stack = this._attr.stack; - - return stack(data.series.map(function (d) { - var label = d.label; - return d.values.map(function (e, i) { - return { - _input: e, - label: label, - x: self._attr.xValue.call(d.values, e, i), - y: self._attr.yValue.call(d.values, e, i) - }; - }); - })); - }; - /** * Adds SVG path to area chart * @@ -328,6 +303,7 @@ define(function (require) { // add clipPath to hide circles when they go out of bounds self.addClipPath(svg, width, height); + self.createEndZones(svg); // add path path = self.addPath(svg, layers); diff --git a/src/kibana/components/vislib/visualizations/column_chart.js b/src/kibana/components/vislib/visualizations/column_chart.js index d97fed63728f..4520136548da 100644 --- a/src/kibana/components/vislib/visualizations/column_chart.js +++ b/src/kibana/components/vislib/visualizations/column_chart.js @@ -4,7 +4,7 @@ define(function (require) { var $ = require('jquery'); var moment = require('moment'); - var Chart = Private(require('components/vislib/visualizations/_chart')); + var PointSeriesChart = Private(require('components/vislib/visualizations/_point_series_chart')); var errors = require('errors'); require('css!components/vislib/styles/main'); @@ -18,7 +18,7 @@ define(function (require) { * @param el {HTMLElement} HTML element to which the chart will be appended * @param chartData {Object} Elasticsearch query results for this specific chart */ - _(ColumnChart).inherits(Chart); + _(ColumnChart).inherits(PointSeriesChart); function ColumnChart(handler, chartEl, chartData) { if (!(this instanceof ColumnChart)) { return new ColumnChart(handler, chartEl, chartData); @@ -33,30 +33,6 @@ define(function (require) { }); } - /** - * Stacks chart data values - * - * @method stackData - * @param data {Object} Elasticsearch query result for this chart - * @returns {Array} Stacked data objects with x, y, and y0 values - */ - // TODO: refactor so that this is called from the data module - ColumnChart.prototype.stackData = function (data) { - var self = this; - - return this._attr.stack(data.series.map(function (d) { - var label = d.label; - return d.values.map(function (e, i) { - return { - _input: e, - label: label, - x: self._attr.xValue.call(d.values, e, i), - y: self._attr.yValue.call(d.values, e, i) - }; - }); - })); - }; - /** * Adds SVG rect to Vertical Bar Chart * @@ -283,6 +259,7 @@ define(function (require) { .attr('transform', 'translate(0,' + margin.top + ')'); bars = self.addBars(svg, layers); + self.createEndZones(svg); // Adds event listeners self.addBarEvents(bars, svg); diff --git a/src/kibana/components/vislib/visualizations/line_chart.js b/src/kibana/components/vislib/visualizations/line_chart.js index 00be729a3a27..e0998555b667 100644 --- a/src/kibana/components/vislib/visualizations/line_chart.js +++ b/src/kibana/components/vislib/visualizations/line_chart.js @@ -4,7 +4,7 @@ define(function (require) { var $ = require('jquery'); var errors = require('errors'); - var Chart = Private(require('components/vislib/visualizations/_chart')); + var PointSeriesChart = Private(require('components/vislib/visualizations/_point_series_chart')); require('css!components/vislib/styles/main'); /** @@ -17,7 +17,7 @@ define(function (require) { * @param el {HTMLElement} HTML element to which the chart will be appended * @param chartData {Object} Elasticsearch query results for this specific chart */ - _(LineChart).inherits(Chart); + _(LineChart).inherits(PointSeriesChart); function LineChart(handler, chartEl, chartData) { if (!(this instanceof LineChart)) { return new LineChart(handler, chartEl, chartData); @@ -265,6 +265,7 @@ define(function (require) { lines = self.addLines(svg, data.series); circles = self.addCircles(svg, layers); self.addCircleEvents(circles, svg); + self.createEndZones(svg); var line = svg .append('line')