text: /*
   * Licensed to Elasticsearch B.V. under one or more contributor
   * license agreements. See the NOTICE file distributed with
   * this work for additional information regarding copyright
   * ownership. Elasticsearch B.V. licenses this file to you under
   * the Apache License, Version 2.0 (the "License"); you may
   * not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *    http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an
   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   * KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations
   * under the License.
   */
  
  require('./flot');
  import _ from 'lodash';
  import $ from 'jquery';
  import moment from 'moment-timezone';
  import observeResize from '../../lib/observe_resize';
  import { calculateInterval, DEFAULT_TIME_FORMAT } from '../../../common/lib';
  import { timefilter } from 'ui/timefilter';
  
  const DEBOUNCE_DELAY = 50;
  
  export default function timechartFn(Private, config, $rootScope, $compile) {
    return function () {
      return {
        help: 'Draw a timeseries chart',
        render: function ($scope, $elem) {
          const template = '<div class="chart-top-title"></div><div class="chart-canvas"></div>';
          const tickFormatters = require('plugins/timelion/services/tick_formatters')();
          const getxAxisFormatter = Private(require('plugins/timelion/panels/timechart/xaxis_formatter'));
          const generateTicks = Private(require('plugins/timelion/panels/timechart/tick_generator'));
  
          // TODO: I wonder if we should supply our own moment that sets this every time?
          // could just use angular's injection to provide a moment service?
          moment.tz.setDefault(config.get('dateFormat:tz'));
  
          const render = $scope.seriesList.render || {};
  
          $scope.chart = $scope.seriesList.list;
          $scope.interval = $scope.interval;
          $scope.search = $scope.search || _.noop;
  
          let legendValueNumbers;
          let legendCaption;
          const debouncedSetLegendNumbers = _.debounce(setLegendNumbers, DEBOUNCE_DELAY, {
            maxWait: DEBOUNCE_DELAY,
            leading: true,
            trailing: false
          });
          // ensure legend is the same height with or without a caption so legend items do not move around
          const emptyCaption = '<br>';
  
          const defaultOptions = {
            xaxis: {
              mode: 'time',
              tickLength: 5,
              timezone: 'browser',
            },
            selection: {
              mode: 'x',
              color: '#ccc'
            },
            crosshair: {
              mode: 'x',
              color: '#C66',
              lineWidth: 2
            },
            grid: {
              show: render.grid,
              borderWidth: 0,
              borderColor: null,
              margin: 10,
              hoverable: true,
              autoHighlight: false
            },
            legend: {
              backgroundColor: null,
              position: 'nw',
              labelBoxBorderColor: 'rgb(255,255,255,0)',
              labelFormatter: function (label, series) {
                const wrapperSpan = document.createElement('span');
                const labelSpan = document.createElement('span');
                const numberSpan = document.createElement('span');
  
                wrapperSpan.setAttribute('class', 'ngLegendValue');
                wrapperSpan.setAttribute('kbn-accessible-click', '');
                wrapperSpan.setAttribute('ng-click', `toggleSeries(${series._id})`);
                wrapperSpan.setAttribute('ng-focus', `focusSeries(${series._id})`);
                wrapperSpan.setAttribute('ng-mouseover', `highlightSeries(${series._id})`);
  
                labelSpan.setAttribute('ng-non-bindable', '');
                labelSpan.appendChild(document.createTextNode(label));
                numberSpan.setAttribute('class', 'ngLegendValueNumber');
  
                wrapperSpan.appendChild(labelSpan);
                wrapperSpan.appendChild(numberSpan);
  
                return wrapperSpan.outerHTML;
              }
            },
            colors: ['#01A4A4', '#C66', '#D0D102', '#616161', '#00A1CB', '#32742C', '#F18D05', '#113F8C', '#61AE24', '#D70060']
          };
  
          const originalColorMap = new Map();
          $scope.chart.forEach((series, seriesIndex) => {
            if (!series.color) {
              const colorIndex = seriesIndex % defaultOptions.colors.length;
              series.color = defaultOptions.colors[colorIndex];
            }
            originalColorMap.set(series, series.color);
          });
  
          let highlightedSeries;
          let focusedSeries;
          function unhighlightSeries() {
            if (highlightedSeries === null) {
              return;
            }
  
            highlightedSeries = null;
            focusedSeries = null;
            $scope.chart.forEach((series) => {
              series.color = originalColorMap.get(series); // reset the colors
            });
            drawPlot($scope.chart);
          }
          $scope.highlightSeries = _.debounce(function (id) {
            if (highlightedSeries === id) {
              return;
            }
  
            highlightedSeries = id;
            $scope.chart.forEach((series, seriesIndex) => {
              if (seriesIndex !== id) {
                series.color = 'rgba(128,128,128,0.1)'; // mark as grey
              } else {
                series.color = originalColorMap.get(series); // color it like it was
              }
            });
            drawPlot($scope.chart);
          }, DEBOUNCE_DELAY);
          $scope.focusSeries = function (id) {
            focusedSeries = id;
            $scope.highlightSeries(id);
          };
  
          $scope.toggleSeries = function (id) {
            const series = $scope.chart[id];
            series._hide = !series._hide;
            drawPlot($scope.chart);
          };
  
          const cancelResize = observeResize($elem, function () {
            drawPlot($scope.chart);
          });
  
          $scope.$on('$destroy', function () {
            cancelResize();
            $elem.off('plothover');
            $elem.off('plotselected');
            $elem.off('mouseleave');
          });
  
          $elem.on('plothover',  function (event, pos, item) {
            $rootScope.$broadcast('timelionPlotHover', event, pos, item);
          });
  
          $elem.on('plotselected', function (event, ranges) {
            timefilter.setTime({
              from: moment(ranges.xaxis.from),
              to: moment(ranges.xaxis.to),
              mode: 'absolute',
            });
          });
  
          $elem.on('mouseleave', function () {
            $rootScope.$broadcast('timelionPlotLeave');
          });
  
          $scope.$on('timelionPlotHover', function (angularEvent, flotEvent, pos) {
            if (!$scope.plot) return;
            $scope.plot.setCrosshair(pos);
            debouncedSetLegendNumbers(pos);
          });
  
          $scope.$on('timelionPlotLeave', function () {
            if (!$scope.plot) return;
            $scope.plot.clearCrosshair();
            clearLegendNumbers();
          });
  
          // Shamelessly borrowed from the flotCrosshairs example
          function setLegendNumbers(pos) {
            unhighlightSeries();
  
            const plot = $scope.plot;
  
            const axes = plot.getAxes();
            if (pos.x < axes.xaxis.min || pos.x > axes.xaxis.max) {
              return;
            }
  
            let i;
            let j;
            const dataset = plot.getData();
            if (legendCaption) {
              legendCaption.text(moment(pos.x).format(_.get(dataset, '[0]._global.legend.timeFormat', DEFAULT_TIME_FORMAT)));
            }
            for (i = 0; i < dataset.length; ++i) {
  
              const series = dataset[i];
              const precision = _.get(series, '_meta.precision', 2);
  
              if (series._hide) continue;
  
              // Nearest point
              for (j = 0; j < series.data.length; ++j) {
                if (series.data[j][0] > pos.x) break;
              }
  
              let y;
              try {
                y = series.data[j][1];
              } catch (e) {
                y = null;
              }
  
              if (y != null) {
                let label = y.toFixed(precision);
                if (series.yaxis.tickFormatter) {
                  label = series.yaxis.tickFormatter(label, series.yaxis);
                }
                legendValueNumbers.eq(i).text(`(${label})`);
              } else {
                legendValueNumbers.eq(i).empty();
              }
            }
          }
  
          function clearLegendNumbers() {
            if (legendCaption) {
              legendCaption.html(emptyCaption);
            }
            _.each(legendValueNumbers, function (num) {
              $(num).empty();
            });
          }
  
          let legendScope = $scope.$new();
          function drawPlot(plotConfig) {
            if (!$('.chart-canvas', $elem).length) $elem.html(template);
            const canvasElem = $('.chart-canvas', $elem);
  
            // we can't use `$.plot` to draw the chart when the height or width is 0
            // so, we'll need another event to trigger drawPlot to actually draw it
            if (canvasElem.height() === 0 || canvasElem.width() === 0) {
              return;
            }
  
            const title = _(plotConfig).map('_title').compact().last();
            $('.chart-top-title', $elem).text(title == null ? '' : title);
  
            const options = _.cloneDeep(defaultOptions);
  
            // Get the X-axis tick format
            const time = timefilter.getBounds();
            const interval = calculateInterval(
              time.min.valueOf(),
              time.max.valueOf(),
              config.get('timelion:target_buckets') || 200,
              $scope.interval,
              config.get('timelion:min_interval') || '1ms',
            );
            const format = getxAxisFormatter(interval);
  
            // Use moment to format ticks so we get timezone correction
            options.xaxis.tickFormatter = function (val) {
              return moment(val).format(format);
            };
  
            // Calculate how many ticks can fit on the axis
            const tickLetterWidth = 7;
            const tickPadding = 45;
            options.xaxis.ticks = Math.floor($elem.width() / ((format.length * tickLetterWidth) + tickPadding));
  
            const series = _.map(plotConfig, function (series, index) {
              series = _.cloneDeep(_.defaults(series, {
                shadowSize: 0,
                lines: {
                  lineWidth: 3
                }
              }));
              series._id = index;
  
              if (series.color) {
                const span = document.createElement('span');
                span.style.color = series.color;
                series.color = span.style.color;
              }
  
              if (series._hide) {
                series.data = [];
                series.stack = false;
                //series.color = "#ddd";
                series.label = '(hidden) ' + series.label;
              }
  
              if (series._global) {
                _.merge(options, series._global, function (objVal, srcVal) {
                  // This is kind of gross, it means that you can't replace a global value with a null
                  // best you can do is an empty string. Deal with it.
                  if (objVal == null) return srcVal;
                  if (srcVal == null) return objVal;
                });
              }
  
              return series;
            });
  
            if (options.yaxes) {
              options.yaxes.forEach(yaxis => {
                if (yaxis && yaxis.units) {
                  yaxis.tickFormatter = tickFormatters[yaxis.units.type];
                  const byteModes = ['bytes', 'bytes/s'];
                  if (byteModes.includes(yaxis.units.type)) {
                    yaxis.tickGenerator = generateTicks;
                  }
                }
              });
            }
  
            $scope.plot = $.plot(canvasElem, _.compact(series), options);
  
            if ($scope.plot) {
              $scope.$emit('timelionChartRendered');
            }
  
            legendScope.$destroy();
            legendScope = $scope.$new();
            // Used to toggle the series, and for displaying values on hover
            legendValueNumbers = canvasElem.find('.ngLegendValueNumber');
            _.each(canvasElem.find('.ngLegendValue'), function (elem) {
              $compile(elem)(legendScope);
            });
  
            if (_.get($scope.plot.getData(), '[0]._global.legend.showTime', true)) {
              legendCaption = $('<caption class="timChart__legendCaption"></caption>');
              legendCaption.html(emptyCaption);
              canvasElem.find('div.legend table').append(legendCaption);
  
              // legend has been re-created. Apply focus on legend element when previously set
              if (focusedSeries || focusedSeries === 0) {
                const $legendLabels = canvasElem.find('div.legend table .legendLabel>span');
                $legendLabels.get(focusedSeries).focus();
              }
            }
          }
          $scope.$watch('chart', drawPlot);
        }
      };
    };
  }
  